Legend:
- Unmodified
- Added
- Removed
-
imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-no-literals.js
rd565449 r0c6b92a 9 9 const iterFrom = require('es-iterator-helpers/Iterator.from'); 10 10 const map = require('es-iterator-helpers/Iterator.prototype.map'); 11 const some = require('es-iterator-helpers/Iterator.prototype.some'); 12 const flatMap = require('es-iterator-helpers/Iterator.prototype.flatMap'); 13 const fromEntries = require('object.fromentries'); 14 const entries = require('object.entries'); 11 15 12 16 const docsUrl = require('../util/docsUrl'); … … 18 22 // ------------------------------------------------------------------------------ 19 23 20 function trimIfString(val) { 21 return typeof val === 'string' ? val.trim() : val; 24 /** 25 * @param {unknown} value 26 * @returns {string | unknown} 27 */ 28 function trimIfString(value) { 29 return typeof value === 'string' ? value.trim() : value; 22 30 } 31 32 const reOverridableElement = /^[A-Z][\w.]*$/; 33 const reIsWhiteSpace = /^[\s]+$/; 34 const jsxElementTypes = new Set(['JSXElement', 'JSXFragment']); 35 const standardJSXNodeParentTypes = new Set(['JSXAttribute', 'JSXElement', 'JSXExpressionContainer', 'JSXFragment']); 23 36 24 37 const messages = { 25 38 invalidPropValue: 'Invalid prop value: "{{text}}"', 39 invalidPropValueInElement: 'Invalid prop value: "{{text}}" in {{element}}', 26 40 noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"', 41 noStringsInAttributesInElement: 'Strings not allowed in attributes: "{{text}}" in {{element}}', 27 42 noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"', 43 noStringsInJSXInElement: 'Strings not allowed in JSX files: "{{text}}" in {{element}}', 28 44 literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"', 45 literalNotInJSXExpressionInElement: 'Missing JSX expression container around literal string: "{{text}}" in {{element}}', 29 46 }; 30 47 31 /** @type {import('eslint').Rule.RuleModule} */ 48 /** @type {Exclude<import('eslint').Rule.RuleModule['meta']['schema'], unknown[]>['properties']} */ 49 const 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 */ 119 function 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 */ 136 function 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 171 const 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 }, 183 }; 184 32 185 module.exports = { 33 meta: {186 meta: /** @type {import('eslint').Rule.RuleModule["meta"]} */ ({ 34 187 docs: { 35 188 description: 'Disallow usage of string literals in JSX', … … 43 196 schema: [{ 44 197 type: 'object', 45 properties: { 46 noStrings: { 47 type: 'boolean', 48 }, 49 allowedStrings: { 50 type: 'array', 51 uniqueItems: true, 52 items: { 53 type: 'string', 54 }, 55 }, 56 ignoreProps: { 57 type: 'boolean', 58 }, 59 noAttributeStrings: { 60 type: 'boolean', 61 }, 62 }, 198 properties: Object.assign( 199 { elementOverrides }, 200 commonPropertiesSchema 201 ), 63 202 additionalProperties: false, 64 203 }], 65 } ,204 }), 66 205 67 206 create(context) { 68 const defaults = { 69 noStrings: false, 70 allowedStrings: [], 71 ignoreProps: false, 72 noAttributeStrings: false, 73 }; 74 const config = Object.assign({}, defaults, context.options[0] || {}); 75 config.allowedStrings = new Set(map(iterFrom(config.allowedStrings), trimIfString)); 76 77 function defaultMessageId() { 78 const ancestorIsJSXElement = arguments.length >= 1 && arguments[0]; 79 if (config.noAttributeStrings && !ancestorIsJSXElement) { 80 return 'noStringsInAttributes'; 81 } 82 if (config.noStrings) { 83 return 'noStringsInJSX'; 84 } 85 return 'literalNotInJSXExpression'; 86 } 87 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 } 227 } 228 if (node.type === 'MemberExpression') { 229 return isRequireStatement(node.object); 230 } 231 232 return false; 233 } 234 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 }; 250 } 251 252 /** @type {string[]} */ 253 const nameFragments = []; 254 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); 261 } 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 }; 283 } 284 } 285 } 286 } 287 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; 304 } 305 306 return ancestors; 307 } 308 309 /** 310 * @param {ASTNode} node 311 * @returns {ASTNode} 312 */ 88 313 function getParentIgnoringBinaryExpressions(node) { 89 314 let current = node; … … 94 319 } 95 320 96 function getValidation(node) { 97 const values = [trimIfString(node.raw), trimIfString(node.value)]; 98 if (values.some((value) => config.allowedStrings.has(value))) { 99 return false; 100 } 101 321 /** 322 * @param {ASTNode} node 323 * @returns {{ parent: ASTNode, grandParent: ASTNode }} 324 */ 325 function getParentAndGrandParent(node) { 102 326 const parent = getParentIgnoringBinaryExpressions(node); 103 104 function isParentNodeStandard() {105 if (!/^[\s]+$/.test(node.value) && typeof node.value === 'string' && parent.type.includes('JSX')) {106 if (config.noAttributeStrings) {107 return parent.type === 'JSXAttribute' || parent.type === 'JSXElement';108 }109 if (!config.noAttributeStrings) {110 return parent.type !== 'JSXAttribute';111 }112 }113 114 return false;115 }116 117 const standard = isParentNodeStandard();118 119 if (config.noStrings) {120 return standard;121 }122 return standard && parent.type !== 'JSXExpressionContainer';123 }124 125 function getParentAndGrandParentType(node) {126 const parent = getParentIgnoringBinaryExpressions(node);127 const parentType = parent.type;128 const grandParentType = parent.parent.type;129 130 327 return { 131 328 parent, 132 parentType,133 grandParentType,134 329 grandParent: parent.parent, 135 330 }; 136 331 } 137 332 333 /** 334 * @param {ASTNode} node 335 * @returns {boolean} 336 */ 138 337 function hasJSXElementParentOrGrandParent(node) { 139 const parents = getParentAndGrandParentType(node); 140 const parentType = parents.parentType; 141 const grandParentType = parents.grandParentType; 142 143 return parentType === 'JSXFragment' || parentType === 'JSXElement' || grandParentType === 'JSXElement'; 144 } 145 146 function reportLiteralNode(node, messageId) { 147 const ancestorIsJSXElement = hasJSXElementParentOrGrandParent(node); 148 messageId = messageId || defaultMessageId(ancestorIsJSXElement); 149 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 } 354 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'; 371 } 372 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]; 392 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) { 150 442 report(context, messages[messageId], messageId, { 151 443 node, 152 444 data: { 153 445 text: getText(context, node).trim(), 446 element: resolvedConfig.type === 'override' && 'name' in resolvedConfig ? resolvedConfig.name : undefined, 154 447 }, 155 448 }); … … 160 453 // -------------------------------------------------------------------------- 161 454 162 return { 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, { 163 489 Literal(node) { 164 if (getValidation(node) && (hasJSXElementParentOrGrandParent(node) || !config.ignoreProps)) { 165 reportLiteralNode(node); 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 } 166 501 } 167 502 }, 168 503 169 504 JSXAttribute(node) { 170 const isNodeValueString = node && node.value && node.value.type === 'Literal' && typeof node.value.value === 'string' && !config.allowedStrings.has(node.value.value); 171 172 if (config.noStrings && !config.ignoreProps && isNodeValueString) { 173 const messageId = 'invalidPropValue'; 174 reportLiteralNode(node, messageId); 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; 511 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 } 175 520 } 176 521 }, 177 522 178 523 JSXText(node) { 179 if (getValidation(node)) { 180 reportLiteralNode(node); 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); 181 533 } 182 534 }, 183 535 184 536 TemplateLiteral(node) { 185 const parents = getParentAndGrandParentType(node); 186 const parentType = parents.parentType; 187 const grandParentType = parents.grandParentType; 188 const isParentJSXExpressionCont = parentType === 'JSXExpressionContainer'; 189 const isParentJSXElement = parentType === 'JSXElement' || grandParentType === 'JSXElement'; 190 191 if (isParentJSXExpressionCont && config.noStrings && (isParentJSXElement || !config.ignoreProps)) { 192 reportLiteralNode(node); 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 } 193 550 } 194 551 }, 195 } ;552 }); 196 553 }, 197 554 };
Note:
See TracChangeset
for help on using the changeset viewer.