Ignore:
Timestamp:
12/12/24 17:06:06 (5 weeks ago)
Author:
stefan toskovski <stefantoska84@…>
Branches:
main
Parents:
d565449
Message:

Pred finalna verzija

File:
1 edited

Legend:

Unmodified
Added
Removed
  • imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-no-literals.js

    rd565449 r0c6b92a  
    99const iterFrom = require('es-iterator-helpers/Iterator.from');
    1010const 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');
    1115
    1216const docsUrl = require('../util/docsUrl');
     
    1822// ------------------------------------------------------------------------------
    1923
    20 function trimIfString(val) {
    21   return typeof val === 'string' ? val.trim() : val;
     24/**
     25 * @param {unknown} value
     26 * @returns {string | unknown}
     27 */
     28function trimIfString(value) {
     29  return typeof value === 'string' ? value.trim() : value;
    2230}
     31
     32const reOverridableElement = /^[A-Z][\w.]*$/;
     33const reIsWhiteSpace = /^[\s]+$/;
     34const jsxElementTypes = new Set(['JSXElement', 'JSXFragment']);
     35const standardJSXNodeParentTypes = new Set(['JSXAttribute', 'JSXElement', 'JSXExpressionContainer', 'JSXFragment']);
    2336
    2437const messages = {
    2538  invalidPropValue: 'Invalid prop value: "{{text}}"',
     39  invalidPropValueInElement: 'Invalid prop value: "{{text}}" in {{element}}',
    2640  noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"',
     41  noStringsInAttributesInElement: 'Strings not allowed in attributes: "{{text}}" in {{element}}',
    2742  noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"',
     43  noStringsInJSXInElement: 'Strings not allowed in JSX files: "{{text}}" in {{element}}',
    2844  literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"',
     45  literalNotInJSXExpressionInElement: 'Missing JSX expression container around literal string: "{{text}}" in {{element}}',
    2946};
    3047
    31 /** @type {import('eslint').Rule.RuleModule} */
     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  },
     183};
     184
    32185module.exports = {
    33   meta: {
     186  meta: /** @type {import('eslint').Rule.RuleModule["meta"]} */ ({
    34187    docs: {
    35188      description: 'Disallow usage of string literals in JSX',
     
    43196    schema: [{
    44197      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      ),
    63202      additionalProperties: false,
    64203    }],
    65   },
     204  }),
    66205
    67206  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     */
    88313    function getParentIgnoringBinaryExpressions(node) {
    89314      let current = node;
     
    94319    }
    95320
    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) {
    102326      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 
    130327      return {
    131328        parent,
    132         parentType,
    133         grandParentType,
    134329        grandParent: parent.parent,
    135330      };
    136331    }
    137332
     333    /**
     334     * @param {ASTNode} node
     335     * @returns {boolean}
     336     */
    138337    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) {
    150442      report(context, messages[messageId], messageId, {
    151443        node,
    152444        data: {
    153445          text: getText(context, node).trim(),
     446          element: resolvedConfig.type === 'override' && 'name' in resolvedConfig ? resolvedConfig.name : undefined,
    154447        },
    155448      });
     
    160453    // --------------------------------------------------------------------------
    161454
    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, {
    163489      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          }
    166501        }
    167502      },
    168503
    169504      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          }
    175520        }
    176521      },
    177522
    178523      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);
    181533        }
    182534      },
    183535
    184536      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          }
    193550        }
    194551      },
    195     };
     552    });
    196553  },
    197554};
Note: See TracChangeset for help on using the changeset viewer.