source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-curly-brace-presence.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100755
File size: 13.6 KB
Line 
1/**
2 * @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX
3 * @author Jacky Ho
4 * @author Simon Lydell
5 */
6
7'use strict';
8
9const arrayIncludes = require('array-includes');
10
11const docsUrl = require('../util/docsUrl');
12const jsxUtil = require('../util/jsx');
13const report = require('../util/report');
14const eslintUtil = require('../util/eslint');
15
16const getSourceCode = eslintUtil.getSourceCode;
17const getText = eslintUtil.getText;
18
19// ------------------------------------------------------------------------------
20// Constants
21// ------------------------------------------------------------------------------
22
23const OPTION_ALWAYS = 'always';
24const OPTION_NEVER = 'never';
25const OPTION_IGNORE = 'ignore';
26
27const OPTION_VALUES = [
28 OPTION_ALWAYS,
29 OPTION_NEVER,
30 OPTION_IGNORE,
31];
32const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER, propElementValues: OPTION_IGNORE };
33
34// ------------------------------------------------------------------------------
35// Rule Definition
36// ------------------------------------------------------------------------------
37
38const messages = {
39 unnecessaryCurly: 'Curly braces are unnecessary here.',
40 missingCurly: 'Need to wrap this literal in a JSX expression.',
41};
42
43/** @type {import('eslint').Rule.RuleModule} */
44module.exports = {
45 meta: {
46 docs: {
47 description: 'Disallow unnecessary JSX expressions when literals alone are sufficient or enforce JSX expressions on literals in JSX children or attributes',
48 category: 'Stylistic Issues',
49 recommended: false,
50 url: docsUrl('jsx-curly-brace-presence'),
51 },
52 fixable: 'code',
53
54 messages,
55
56 schema: [
57 {
58 anyOf: [
59 {
60 type: 'object',
61 properties: {
62 props: { enum: OPTION_VALUES },
63 children: { enum: OPTION_VALUES },
64 propElementValues: { enum: OPTION_VALUES },
65 },
66 additionalProperties: false,
67 },
68 {
69 enum: OPTION_VALUES,
70 },
71 ],
72 },
73 ],
74 },
75
76 create(context) {
77 const HTML_ENTITY_REGEX = () => /&[A-Za-z\d#]+;/g;
78 const ruleOptions = context.options[0];
79 const userConfig = typeof ruleOptions === 'string'
80 ? { props: ruleOptions, children: ruleOptions, propElementValues: OPTION_IGNORE }
81 : Object.assign({}, DEFAULT_CONFIG, ruleOptions);
82
83 function containsLineTerminators(rawStringValue) {
84 return /[\n\r\u2028\u2029]/.test(rawStringValue);
85 }
86
87 function containsBackslash(rawStringValue) {
88 return arrayIncludes(rawStringValue, '\\');
89 }
90
91 function containsHTMLEntity(rawStringValue) {
92 return HTML_ENTITY_REGEX().test(rawStringValue);
93 }
94
95 function containsOnlyHtmlEntities(rawStringValue) {
96 return rawStringValue.replace(HTML_ENTITY_REGEX(), '').trim() === '';
97 }
98
99 function containsDisallowedJSXTextChars(rawStringValue) {
100 return /[{<>}]/.test(rawStringValue);
101 }
102
103 function containsQuoteCharacters(value) {
104 return /['"]/.test(value);
105 }
106
107 function containsMultilineComment(value) {
108 return /\/\*/.test(value);
109 }
110
111 function escapeDoubleQuotes(rawStringValue) {
112 return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
113 }
114
115 function escapeBackslashes(rawStringValue) {
116 return rawStringValue.replace(/\\/g, '\\\\');
117 }
118
119 function needToEscapeCharacterForJSX(raw, node) {
120 return (
121 containsBackslash(raw)
122 || containsHTMLEntity(raw)
123 || (node.parent.type !== 'JSXAttribute' && containsDisallowedJSXTextChars(raw))
124 );
125 }
126
127 function containsWhitespaceExpression(child) {
128 if (child.type === 'JSXExpressionContainer') {
129 const value = child.expression.value;
130 return value ? jsxUtil.isWhiteSpaces(value) : false;
131 }
132 return false;
133 }
134
135 function isLineBreak(text) {
136 return containsLineTerminators(text) && text.trim() === '';
137 }
138
139 function wrapNonHTMLEntities(text) {
140 const HTML_ENTITY = '<HTML_ENTITY>';
141 const withCurlyBraces = text.split(HTML_ENTITY_REGEX()).map((word) => (
142 word === '' ? '' : `{${JSON.stringify(word)}}`
143 )).join(HTML_ENTITY);
144
145 const htmlEntities = text.match(HTML_ENTITY_REGEX());
146 return htmlEntities.reduce((acc, htmlEntity) => (
147 acc.replace(HTML_ENTITY, htmlEntity)
148 ), withCurlyBraces);
149 }
150
151 function wrapWithCurlyBraces(rawText) {
152 if (!containsLineTerminators(rawText)) {
153 return `{${JSON.stringify(rawText)}}`;
154 }
155
156 return rawText.split('\n').map((line) => {
157 if (line.trim() === '') {
158 return line;
159 }
160 const firstCharIndex = line.search(/[^\s]/);
161 const leftWhitespace = line.slice(0, firstCharIndex);
162 const text = line.slice(firstCharIndex);
163
164 if (containsHTMLEntity(line)) {
165 return `${leftWhitespace}${wrapNonHTMLEntities(text)}`;
166 }
167 return `${leftWhitespace}{${JSON.stringify(text)}}`;
168 }).join('\n');
169 }
170
171 /**
172 * Report and fix an unnecessary curly brace violation on a node
173 * @param {ASTNode} JSXExpressionNode - The AST node with an unnecessary JSX expression
174 */
175 function reportUnnecessaryCurly(JSXExpressionNode) {
176 report(context, messages.unnecessaryCurly, 'unnecessaryCurly', {
177 node: JSXExpressionNode,
178 fix(fixer) {
179 const expression = JSXExpressionNode.expression;
180
181 let textToReplace;
182 if (jsxUtil.isJSX(expression)) {
183 textToReplace = getText(context, expression);
184 } else {
185 const expressionType = expression && expression.type;
186 const parentType = JSXExpressionNode.parent.type;
187
188 if (parentType === 'JSXAttribute') {
189 textToReplace = `"${expressionType === 'TemplateLiteral'
190 ? expression.quasis[0].value.raw
191 : expression.raw.slice(1, -1)
192 }"`;
193 } else if (jsxUtil.isJSX(expression)) {
194 textToReplace = getText(context, expression);
195 } else {
196 textToReplace = expressionType === 'TemplateLiteral'
197 ? expression.quasis[0].value.cooked : expression.value;
198 }
199 }
200
201 return fixer.replaceText(JSXExpressionNode, textToReplace);
202 },
203 });
204 }
205
206 function reportMissingCurly(literalNode) {
207 report(context, messages.missingCurly, 'missingCurly', {
208 node: literalNode,
209 fix(fixer) {
210 if (jsxUtil.isJSX(literalNode)) {
211 return fixer.replaceText(literalNode, `{${getText(context, literalNode)}}`);
212 }
213
214 // If a HTML entity name is found, bail out because it can be fixed
215 // by either using the real character or the unicode equivalent.
216 // If it contains any line terminator character, bail out as well.
217 if (
218 containsOnlyHtmlEntities(literalNode.raw)
219 || (literalNode.parent.type === 'JSXAttribute' && containsLineTerminators(literalNode.raw))
220 || isLineBreak(literalNode.raw)
221 ) {
222 return null;
223 }
224
225 const expression = literalNode.parent.type === 'JSXAttribute'
226 ? `{"${escapeDoubleQuotes(escapeBackslashes(
227 literalNode.raw.slice(1, -1)
228 ))}"}`
229 : wrapWithCurlyBraces(literalNode.raw);
230
231 return fixer.replaceText(literalNode, expression);
232 },
233 });
234 }
235
236 function isWhiteSpaceLiteral(node) {
237 return node.type && node.type === 'Literal' && node.value && jsxUtil.isWhiteSpaces(node.value);
238 }
239
240 function isStringWithTrailingWhiteSpaces(value) {
241 return /^\s|\s$/.test(value);
242 }
243
244 function isLiteralWithTrailingWhiteSpaces(node) {
245 return node.type && node.type === 'Literal' && node.value && isStringWithTrailingWhiteSpaces(node.value);
246 }
247
248 // Bail out if there is any character that needs to be escaped in JSX
249 // because escaping decreases readability and the original code may be more
250 // readable anyway or intentional for other specific reasons
251 function lintUnnecessaryCurly(JSXExpressionNode) {
252 const expression = JSXExpressionNode.expression;
253 const expressionType = expression.type;
254
255 const sourceCode = getSourceCode(context);
256 // Curly braces containing comments are necessary
257 if (sourceCode.getCommentsInside && sourceCode.getCommentsInside(JSXExpressionNode).length > 0) {
258 return;
259 }
260
261 if (
262 (expressionType === 'Literal' || expressionType === 'JSXText')
263 && typeof expression.value === 'string'
264 && (
265 (JSXExpressionNode.parent.type === 'JSXAttribute' && !isWhiteSpaceLiteral(expression))
266 || !isLiteralWithTrailingWhiteSpaces(expression)
267 )
268 && !containsMultilineComment(expression.value)
269 && !needToEscapeCharacterForJSX(expression.raw, JSXExpressionNode) && (
270 jsxUtil.isJSX(JSXExpressionNode.parent)
271 || !containsQuoteCharacters(expression.value)
272 )
273 ) {
274 reportUnnecessaryCurly(JSXExpressionNode);
275 } else if (
276 expressionType === 'TemplateLiteral'
277 && expression.expressions.length === 0
278 && expression.quasis[0].value.raw.indexOf('\n') === -1
279 && !isStringWithTrailingWhiteSpaces(expression.quasis[0].value.raw)
280 && !needToEscapeCharacterForJSX(expression.quasis[0].value.raw, JSXExpressionNode)
281 && !containsQuoteCharacters(expression.quasis[0].value.cooked)
282 ) {
283 reportUnnecessaryCurly(JSXExpressionNode);
284 } else if (jsxUtil.isJSX(expression)) {
285 reportUnnecessaryCurly(JSXExpressionNode);
286 }
287 }
288
289 function areRuleConditionsSatisfied(parent, config, ruleCondition) {
290 return (
291 parent.type === 'JSXAttribute'
292 && typeof config.props === 'string'
293 && config.props === ruleCondition
294 ) || (
295 jsxUtil.isJSX(parent)
296 && typeof config.children === 'string'
297 && config.children === ruleCondition
298 );
299 }
300
301 function getAdjacentSiblings(node, children) {
302 for (let i = 1; i < children.length - 1; i++) {
303 const child = children[i];
304 if (node === child) {
305 return [children[i - 1], children[i + 1]];
306 }
307 }
308 if (node === children[0] && children[1]) {
309 return [children[1]];
310 }
311 if (node === children[children.length - 1] && children[children.length - 2]) {
312 return [children[children.length - 2]];
313 }
314 return [];
315 }
316
317 function hasAdjacentJsxExpressionContainers(node, children) {
318 if (!children) {
319 return false;
320 }
321 const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child));
322 const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral);
323
324 return adjSiblings.some((x) => x.type && x.type === 'JSXExpressionContainer');
325 }
326 function hasAdjacentJsx(node, children) {
327 if (!children) {
328 return false;
329 }
330 const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child));
331 const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral);
332
333 return adjSiblings.some((x) => x.type && arrayIncludes(['JSXExpressionContainer', 'JSXElement'], x.type));
334 }
335 function shouldCheckForUnnecessaryCurly(node, config) {
336 const parent = node.parent;
337 // Bail out if the parent is a JSXAttribute & its contents aren't
338 // StringLiteral or TemplateLiteral since e.g
339 // <App prop1={<CustomEl />} prop2={<CustomEl>...</CustomEl>} />
340
341 if (
342 parent.type && parent.type === 'JSXAttribute'
343 && (node.expression && node.expression.type
344 && node.expression.type !== 'Literal'
345 && node.expression.type !== 'StringLiteral'
346 && node.expression.type !== 'TemplateLiteral')
347 ) {
348 return false;
349 }
350
351 // If there are adjacent `JsxExpressionContainer` then there is no need,
352 // to check for unnecessary curly braces.
353 if (jsxUtil.isJSX(parent) && hasAdjacentJsxExpressionContainers(node, parent.children)) {
354 return false;
355 }
356 if (containsWhitespaceExpression(node) && hasAdjacentJsx(node, parent.children)) {
357 return false;
358 }
359 if (
360 parent.children
361 && parent.children.length === 1
362 && containsWhitespaceExpression(node)
363 ) {
364 return false;
365 }
366
367 return areRuleConditionsSatisfied(parent, config, OPTION_NEVER);
368 }
369
370 function shouldCheckForMissingCurly(node, config) {
371 if (jsxUtil.isJSX(node)) {
372 return config.propElementValues !== OPTION_IGNORE;
373 }
374 if (
375 isLineBreak(node.raw)
376 || containsOnlyHtmlEntities(node.raw)
377 ) {
378 return false;
379 }
380 const parent = node.parent;
381 if (
382 parent.children
383 && parent.children.length === 1
384 && containsWhitespaceExpression(parent.children[0])
385 ) {
386 return false;
387 }
388
389 return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS);
390 }
391
392 // --------------------------------------------------------------------------
393 // Public
394 // --------------------------------------------------------------------------
395
396 return {
397 'JSXAttribute > JSXExpressionContainer > JSXElement'(node) {
398 if (userConfig.propElementValues === OPTION_NEVER) {
399 reportUnnecessaryCurly(node.parent);
400 }
401 },
402
403 JSXExpressionContainer(node) {
404 if (shouldCheckForUnnecessaryCurly(node, userConfig)) {
405 lintUnnecessaryCurly(node);
406 }
407 },
408
409 'JSXAttribute > JSXElement, Literal, JSXText'(node) {
410 if (shouldCheckForMissingCurly(node, userConfig)) {
411 reportMissingCurly(node);
412 }
413 },
414 };
415 },
416};
Note: See TracBrowser for help on using the repository browser.