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

main
Last change on this file since 0c6b92a was 0c6b92a, checked in by stefan toskovski <stefantoska84@…>, 5 weeks ago

Pred finalna verzija

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