source: frontend/node_modules/eslint-plugin-react/lib/rules/jsx-no-literals.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 2 weeks ago

Fix frontend appearance

  • Property mode set to 100644
File size: 16.5 KB
Line 
1/**
2 * @fileoverview Prevent using string literals in React component definition
3 * @author Caleb Morris
4 * @author David Buchan-Swanson
5 */
6
7'use strict';
8
9const iterFrom = require('es-iterator-helpers/Iterator.from');
10const map = require('es-iterator-helpers/Iterator.prototype.map');
11const some = require('es-iterator-helpers/Iterator.prototype.some');
12const flatMap = require('es-iterator-helpers/Iterator.prototype.flatMap');
13const fromEntries = require('object.fromentries');
14const entries = require('object.entries');
15
16const docsUrl = require('../util/docsUrl');
17const report = require('../util/report');
18const getText = require('../util/eslint').getText;
19
20/** @typedef {import('eslint').Rule.RuleModule} RuleModule */
21
22/** @typedef {import('../../types/rules/jsx-no-literals').Config} Config */
23/** @typedef {import('../../types/rules/jsx-no-literals').RawConfig} RawConfig */
24/** @typedef {import('../../types/rules/jsx-no-literals').ResolvedConfig} ResolvedConfig */
25/** @typedef {import('../../types/rules/jsx-no-literals').OverrideConfig} OverrideConfig */
26/** @typedef {import('../../types/rules/jsx-no-literals').ElementConfig} ElementConfig */
27
28// ------------------------------------------------------------------------------
29// Rule Definition
30// ------------------------------------------------------------------------------
31
32/**
33 * @param {unknown} value
34 * @returns {string | unknown}
35 */
36function trimIfString(value) {
37 return typeof value === 'string' ? value.trim() : value;
38}
39
40const reOverridableElement = /^[A-Z][\w.]*$/;
41const reIsWhiteSpace = /^[\s]+$/;
42const jsxElementTypes = new Set(['JSXElement', 'JSXFragment']);
43const standardJSXNodeParentTypes = new Set(['JSXAttribute', 'JSXElement', 'JSXExpressionContainer', 'JSXFragment']);
44
45const messages = {
46 invalidPropValue: 'Invalid prop value: "{{text}}"',
47 invalidPropValueInElement: 'Invalid prop value: "{{text}}" in {{element}}',
48 noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"',
49 noStringsInAttributesInElement: 'Strings not allowed in attributes: "{{text}}" in {{element}}',
50 noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"',
51 noStringsInJSXInElement: 'Strings not allowed in JSX files: "{{text}}" in {{element}}',
52 literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"',
53 literalNotInJSXExpressionInElement: 'Missing JSX expression container around literal string: "{{text}}" in {{element}}',
54};
55
56/** @type {Exclude<RuleModule['meta']['schema'], unknown[] | false>['properties']} */
57const commonPropertiesSchema = {
58 noStrings: {
59 type: 'boolean',
60 },
61 allowedStrings: {
62 type: 'array',
63 uniqueItems: true,
64 items: {
65 type: 'string',
66 },
67 },
68 ignoreProps: {
69 type: 'boolean',
70 },
71 noAttributeStrings: {
72 type: 'boolean',
73 },
74};
75
76// eslint-disable-next-line valid-jsdoc
77/**
78 * Normalizes the element portion of the config
79 * @param {RawConfig} config
80 * @returns {ElementConfig}
81 */
82function normalizeElementConfig(config) {
83 return {
84 type: 'element',
85 noStrings: !!config.noStrings,
86 allowedStrings: config.allowedStrings
87 ? new Set(map(iterFrom(config.allowedStrings), trimIfString))
88 : new Set(),
89 ignoreProps: !!config.ignoreProps,
90 noAttributeStrings: !!config.noAttributeStrings,
91 };
92}
93
94// eslint-disable-next-line valid-jsdoc
95/**
96 * Normalizes the config and applies default values to all config options
97 * @param {RawConfig} config
98 * @returns {Config}
99 */
100function normalizeConfig(config) {
101 /** @type {Config} */
102 const normalizedConfig = Object.assign(normalizeElementConfig(config), {
103 elementOverrides: {},
104 });
105
106 if (config.elementOverrides) {
107 normalizedConfig.elementOverrides = fromEntries(
108 flatMap(
109 iterFrom(entries(config.elementOverrides)),
110 (entry) => {
111 const elementName = entry[0];
112 const rawElementConfig = entry[1];
113
114 if (!reOverridableElement.test(elementName)) {
115 return [];
116 }
117
118 return [[
119 elementName,
120 Object.assign(normalizeElementConfig(rawElementConfig), {
121 type: 'override',
122 name: elementName,
123 allowElement: !!rawElementConfig.allowElement,
124 applyToNestedElements: typeof rawElementConfig.applyToNestedElements === 'undefined' || !!rawElementConfig.applyToNestedElements,
125 }),
126 ]];
127 }
128 )
129 );
130 }
131
132 return normalizedConfig;
133}
134
135const elementOverrides = {
136 type: 'object',
137 patternProperties: {
138 [reOverridableElement.source]: {
139 type: 'object',
140 properties: Object.assign(
141 { applyToNestedElements: { type: 'boolean' } },
142 commonPropertiesSchema
143 ),
144
145 },
146 },
147};
148
149/** @type {RuleModule} */
150module.exports = {
151 meta: /** @type {RuleModule['meta']} */ ({
152 docs: {
153 description: 'Disallow usage of string literals in JSX',
154 category: 'Stylistic Issues',
155 recommended: false,
156 url: docsUrl('jsx-no-literals'),
157 },
158
159 messages,
160
161 schema: [{
162 type: 'object',
163 properties: Object.assign(
164 { elementOverrides },
165 commonPropertiesSchema
166 ),
167 additionalProperties: false,
168 }],
169 }),
170
171 create(context) {
172 /** @type {RawConfig} */
173 const rawConfig = (context.options.length && context.options[0]) || {};
174 const config = normalizeConfig(rawConfig);
175
176 const hasElementOverrides = Object.keys(config.elementOverrides).length > 0;
177
178 /** @type {Map<string, string>} */
179 const renamedImportMap = new Map();
180
181 /**
182 * Determines if the given expression is a require statement. Supports
183 * nested MemberExpresions. ie `require('foo').nested.property`
184 * @param {ASTNode} node
185 * @returns {boolean}
186 */
187 function isRequireStatement(node) {
188 if (node.type === 'CallExpression') {
189 if (node.callee.type === 'Identifier') {
190 return node.callee.name === 'require';
191 }
192 }
193 if (node.type === 'MemberExpression') {
194 return isRequireStatement(node.object);
195 }
196
197 return false;
198 }
199
200 /** @typedef {{ name: string, compoundName?: string }} ElementNameFragment */
201
202 /**
203 * Gets the name of the given JSX element. Supports nested
204 * JSXMemeberExpressions. ie `<Namesapce.Component.SubComponent />`
205 * @param {ASTNode} node
206 * @returns {ElementNameFragment | undefined}
207 */
208 function getJSXElementName(node) {
209 if (node.openingElement.name.type === 'JSXIdentifier') {
210 const name = node.openingElement.name.name;
211 return {
212 name: renamedImportMap.get(name) || name,
213 compoundName: undefined,
214 };
215 }
216
217 /** @type {string[]} */
218 const nameFragments = [];
219
220 if (node.openingElement.name.type === 'JSXMemberExpression') {
221 /** @type {ASTNode} */
222 let current = node.openingElement.name;
223 while (current.type === 'JSXMemberExpression') {
224 if (current.property.type === 'JSXIdentifier') {
225 nameFragments.unshift(current.property.name);
226 }
227
228 current = current.object;
229 }
230
231 if (current.type === 'JSXIdentifier') {
232 nameFragments.unshift(current.name);
233
234 const rootFragment = nameFragments[0];
235 if (rootFragment) {
236 const rootFragmentRenamed = renamedImportMap.get(rootFragment);
237 if (rootFragmentRenamed) {
238 nameFragments[0] = rootFragmentRenamed;
239 }
240 }
241
242 const nameFragment = nameFragments[nameFragments.length - 1];
243 if (nameFragment) {
244 return {
245 name: nameFragment,
246 compoundName: nameFragments.join('.'),
247 };
248 }
249 }
250 }
251 }
252
253 /**
254 * Gets all JSXElement ancestor nodes for the given node
255 * @param {ASTNode} node
256 * @returns {ASTNode[]}
257 */
258 function getJSXElementAncestors(node) {
259 /** @type {ASTNode[]} */
260 const ancestors = [];
261
262 let current = node;
263 while (current) {
264 if (current.type === 'JSXElement') {
265 ancestors.push(current);
266 }
267
268 current = current.parent;
269 }
270
271 return ancestors;
272 }
273
274 /**
275 * @param {ASTNode} node
276 * @returns {ASTNode}
277 */
278 function getParentIgnoringBinaryExpressions(node) {
279 let current = node;
280 while (current.parent.type === 'BinaryExpression') {
281 current = current.parent;
282 }
283 return current.parent;
284 }
285
286 /**
287 * @param {ASTNode} node
288 * @returns {{ parent: ASTNode, grandParent: ASTNode }}
289 */
290 function getParentAndGrandParent(node) {
291 const parent = getParentIgnoringBinaryExpressions(node);
292 return {
293 parent,
294 grandParent: parent.parent,
295 };
296 }
297
298 /**
299 * @param {ASTNode} node
300 * @returns {boolean}
301 */
302 function hasJSXElementParentOrGrandParent(node) {
303 const ancestors = getParentAndGrandParent(node);
304 return some(iterFrom([ancestors.parent, ancestors.grandParent]), (parent) => jsxElementTypes.has(parent.type));
305 }
306
307 // eslint-disable-next-line valid-jsdoc
308 /**
309 * Determines whether a given node's value and its immediate parent are
310 * viable text nodes that can/should be reported on
311 * @param {ASTNode} node
312 * @param {ResolvedConfig} resolvedConfig
313 * @returns {boolean}
314 */
315 function isViableTextNode(node, resolvedConfig) {
316 const textValues = iterFrom([trimIfString(node.raw), trimIfString(node.value)]);
317 if (some(textValues, (value) => resolvedConfig.allowedStrings.has(value))) {
318 return false;
319 }
320
321 const parent = getParentIgnoringBinaryExpressions(node);
322
323 let isStandardJSXNode = false;
324 if (typeof node.value === 'string' && !reIsWhiteSpace.test(node.value) && standardJSXNodeParentTypes.has(parent.type)) {
325 if (resolvedConfig.noAttributeStrings) {
326 isStandardJSXNode = parent.type === 'JSXAttribute' || parent.type === 'JSXElement';
327 } else {
328 isStandardJSXNode = parent.type !== 'JSXAttribute';
329 }
330 }
331
332 if (resolvedConfig.noStrings) {
333 return isStandardJSXNode;
334 }
335
336 return isStandardJSXNode && parent.type !== 'JSXExpressionContainer';
337 }
338
339 // eslint-disable-next-line valid-jsdoc
340 /**
341 * Gets an override config for a given node. For any given node, we also
342 * need to traverse the ancestor tree to determine if an ancestor's config
343 * will also apply to the current node.
344 * @param {ASTNode} node
345 * @returns {OverrideConfig | undefined}
346 */
347 function getOverrideConfig(node) {
348 if (!hasElementOverrides) {
349 return;
350 }
351
352 const allAncestorElements = getJSXElementAncestors(node);
353 if (!allAncestorElements.length) {
354 return;
355 }
356
357 for (const ancestorElement of allAncestorElements) {
358 const isClosestJSXAncestor = ancestorElement === allAncestorElements[0];
359
360 const ancestor = getJSXElementName(ancestorElement);
361 if (ancestor) {
362 if (ancestor.name) {
363 const ancestorElements = config.elementOverrides[ancestor.name];
364 const ancestorConfig = ancestor.compoundName
365 ? config.elementOverrides[ancestor.compoundName] || ancestorElements
366 : ancestorElements;
367
368 if (ancestorConfig) {
369 if (isClosestJSXAncestor || ancestorConfig.applyToNestedElements) {
370 return ancestorConfig;
371 }
372 }
373 }
374 }
375 }
376 }
377
378 // eslint-disable-next-line valid-jsdoc
379 /**
380 * @param {ResolvedConfig} resolvedConfig
381 * @returns {boolean}
382 */
383 function shouldAllowElement(resolvedConfig) {
384 return resolvedConfig.type === 'override' && 'allowElement' in resolvedConfig && !!resolvedConfig.allowElement;
385 }
386
387 // eslint-disable-next-line valid-jsdoc
388 /**
389 * @param {boolean} ancestorIsJSXElement
390 * @param {ResolvedConfig} resolvedConfig
391 * @returns {string}
392 */
393 function defaultMessageId(ancestorIsJSXElement, resolvedConfig) {
394 if (resolvedConfig.noAttributeStrings && !ancestorIsJSXElement) {
395 return resolvedConfig.type === 'override' ? 'noStringsInAttributesInElement' : 'noStringsInAttributes';
396 }
397
398 if (resolvedConfig.noStrings) {
399 return resolvedConfig.type === 'override' ? 'noStringsInJSXInElement' : 'noStringsInJSX';
400 }
401
402 return resolvedConfig.type === 'override' ? 'literalNotInJSXExpressionInElement' : 'literalNotInJSXExpression';
403 }
404
405 // eslint-disable-next-line valid-jsdoc
406 /**
407 * @param {ASTNode} node
408 * @param {string} messageId
409 * @param {ResolvedConfig} resolvedConfig
410 */
411 function reportLiteralNode(node, messageId, resolvedConfig) {
412 report(context, messages[messageId], messageId, {
413 node,
414 data: {
415 text: getText(context, node).trim(),
416 element: resolvedConfig.type === 'override' && 'name' in resolvedConfig ? resolvedConfig.name : undefined,
417 },
418 });
419 }
420
421 // --------------------------------------------------------------------------
422 // Public
423 // --------------------------------------------------------------------------
424
425 return Object.assign(hasElementOverrides ? {
426 // Get renamed import local names mapped to their imported name
427 ImportDeclaration(node) {
428 node.specifiers
429 .filter((s) => s.type === 'ImportSpecifier')
430 .forEach((specifier) => {
431 renamedImportMap.set(
432 (specifier.local || specifier.imported).name,
433 specifier.imported.name
434 );
435 });
436 },
437
438 // Get renamed destructured local names mapped to their imported name
439 VariableDeclaration(node) {
440 node.declarations
441 .filter((d) => (
442 d.type === 'VariableDeclarator'
443 && isRequireStatement(d.init)
444 && d.id.type === 'ObjectPattern'
445 ))
446 .forEach((declaration) => {
447 declaration.id.properties
448 .filter((property) => (
449 property.type === 'Property'
450 && property.key.type === 'Identifier'
451 && property.value.type === 'Identifier'
452 ))
453 .forEach((property) => {
454 renamedImportMap.set(property.value.name, property.key.name);
455 });
456 });
457 },
458 } : false, {
459 Literal(node) {
460 const resolvedConfig = getOverrideConfig(node) || config;
461
462 const hasJSXParentOrGrandParent = hasJSXElementParentOrGrandParent(node);
463 if (hasJSXParentOrGrandParent && shouldAllowElement(resolvedConfig)) {
464 return;
465 }
466
467 if (isViableTextNode(node, resolvedConfig)) {
468 if (hasJSXParentOrGrandParent || !config.ignoreProps) {
469 reportLiteralNode(node, defaultMessageId(hasJSXParentOrGrandParent, resolvedConfig), resolvedConfig);
470 }
471 }
472 },
473
474 JSXAttribute(node) {
475 const isLiteralString = node.value && node.value.type === 'Literal'
476 && typeof node.value.value === 'string';
477 const isStringLiteral = node.value && node.value.type === 'StringLiteral';
478
479 if (isLiteralString || isStringLiteral) {
480 const resolvedConfig = getOverrideConfig(node) || config;
481
482 if (
483 resolvedConfig.noStrings
484 && !resolvedConfig.ignoreProps
485 && !resolvedConfig.allowedStrings.has(node.value.value)
486 ) {
487 const messageId = resolvedConfig.type === 'override' ? 'invalidPropValueInElement' : 'invalidPropValue';
488 reportLiteralNode(node, messageId, resolvedConfig);
489 }
490 }
491 },
492
493 JSXText(node) {
494 const resolvedConfig = getOverrideConfig(node) || config;
495
496 if (shouldAllowElement(resolvedConfig)) {
497 return;
498 }
499
500 if (isViableTextNode(node, resolvedConfig)) {
501 const hasJSXParendOrGrantParent = hasJSXElementParentOrGrandParent(node);
502 reportLiteralNode(node, defaultMessageId(hasJSXParendOrGrantParent, resolvedConfig), resolvedConfig);
503 }
504 },
505
506 TemplateLiteral(node) {
507 const ancestors = getParentAndGrandParent(node);
508 const isParentJSXExpressionCont = ancestors.parent.type === 'JSXExpressionContainer';
509 const isParentJSXElement = ancestors.grandParent.type === 'JSXElement';
510
511 if (isParentJSXExpressionCont) {
512 const resolvedConfig = getOverrideConfig(node) || config;
513
514 if (
515 resolvedConfig.noStrings
516 && (isParentJSXElement || !resolvedConfig.ignoreProps)
517 ) {
518 reportLiteralNode(node, defaultMessageId(isParentJSXElement, resolvedConfig), resolvedConfig);
519 }
520 }
521 },
522 });
523 },
524};
Note: See TracBrowser for help on using the repository browser.