| 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');
|
|---|
| 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');
|
|---|
| 15 |
|
|---|
| 16 | const docsUrl = require('../util/docsUrl');
|
|---|
| 17 | const report = require('../util/report');
|
|---|
| 18 | const 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 | */
|
|---|
| 36 | function trimIfString(value) {
|
|---|
| 37 | return typeof value === 'string' ? value.trim() : value;
|
|---|
| 38 | }
|
|---|
| 39 |
|
|---|
| 40 | const reOverridableElement = /^[A-Z][\w.]*$/;
|
|---|
| 41 | const reIsWhiteSpace = /^[\s]+$/;
|
|---|
| 42 | const jsxElementTypes = new Set(['JSXElement', 'JSXFragment']);
|
|---|
| 43 | const standardJSXNodeParentTypes = new Set(['JSXAttribute', 'JSXElement', 'JSXExpressionContainer', 'JSXFragment']);
|
|---|
| 44 |
|
|---|
| 45 | const 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']} */
|
|---|
| 57 | const 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 | */
|
|---|
| 82 | function 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 | */
|
|---|
| 100 | function 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 |
|
|---|
| 135 | const 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} */
|
|---|
| 150 | module.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 | };
|
|---|