[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 |
|
---|
| 9 | const iterFrom = require('es-iterator-helpers/Iterator.from');
|
---|
| 10 | const map = require('es-iterator-helpers/Iterator.prototype.map');
|
---|
[0c6b92a] | 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');
|
---|
[d565449] | 15 |
|
---|
| 16 | const docsUrl = require('../util/docsUrl');
|
---|
| 17 | const report = require('../util/report');
|
---|
| 18 | const getText = require('../util/eslint').getText;
|
---|
| 19 |
|
---|
| 20 | // ------------------------------------------------------------------------------
|
---|
| 21 | // Rule Definition
|
---|
| 22 | // ------------------------------------------------------------------------------
|
---|
| 23 |
|
---|
[0c6b92a] | 24 | /**
|
---|
| 25 | * @param {unknown} value
|
---|
| 26 | * @returns {string | unknown}
|
---|
| 27 | */
|
---|
| 28 | function trimIfString(value) {
|
---|
| 29 | return typeof value === 'string' ? value.trim() : value;
|
---|
[d565449] | 30 | }
|
---|
| 31 |
|
---|
[0c6b92a] | 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']);
|
---|
| 36 |
|
---|
[d565449] | 37 | const 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']} */
|
---|
| 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 | },
|
---|
[d565449] | 183 | };
|
---|
| 184 |
|
---|
| 185 | module.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 | };
|
---|