From b8217ed5f4104bd18b54f77ea108ec081ebbbd49 Mon Sep 17 00:00:00 2001 From: Pearce Date: Fri, 30 Aug 2024 12:15:53 -0700 Subject: [PATCH] [New] `jsx-no-literals` Add `elementOverrides` option and the ability to ignore this rule on specific elements --- CHANGELOG.md | 2 + docs/rules/jsx-no-literals.md | 55 +++ lib/rules/jsx-no-literals.js | 537 ++++++++++++++++++++++++----- tests/lib/rules/jsx-no-literals.js | 506 +++++++++++++++++++++++++++ 4 files changed, 1011 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 985e31b716..3bd1e7de37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ### Added * [`no-string-refs`]: allow this.refs in > 18.3.0 ([#3807][] @henryqdineen) +* [`jsx-no-literals`] Add `elementOverrides` option and the ability to ignore this rule on specific elements ([#3812][] @Pearce-Ropion) ### Fixed * [`function-component-definition`], [`boolean-prop-naming`], [`jsx-first-prop-new-line`], [`jsx-props-no-multi-spaces`], `propTypes`: use type args ([#3629][] @HenryBrown0) @@ -20,6 +21,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange [#3632]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3632 +[#3812]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3812 [#3629]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3629 [#3817]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3817 [#3807]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3807 diff --git a/docs/rules/jsx-no-literals.md b/docs/rules/jsx-no-literals.md index 1a69313951..0ff292cadc 100644 --- a/docs/rules/jsx-no-literals.md +++ b/docs/rules/jsx-no-literals.md @@ -34,6 +34,61 @@ The supported options are: - `allowedStrings` - An array of unique string values that would otherwise warn, but will be ignored. - `ignoreProps` (default: `false`) - When `true` the rule ignores literals used in props, wrapped or unwrapped. - `noAttributeStrings` (default: `false`) - Enforces no string literals used in attributes when set to `true`. +- `elementOverrides` - An object where the keys are the element names and the values are objects with the same options as above. This allows you to specify different options for different elements. + +### `elementOverrides` + +The `elementOverrides` option allows you to specify different options for different elements. This is useful when you want to enforce different rules for different elements. For example, you may want to allow string literals in `Button` elements, but not in the rest of your application. + +The element name only accepts component names. +HTML element tag names are not supported. Component names are case-sensitive and should exactly match the name of the component as it is used in the JSX. +It can also be the name of a compound component (ie. `Modal.Button`). + +Specifying options creates a new context for the rule, so the rule will only apply the new options to the specified element and its children (if `applyToNestedElements` is `true` - see below). +This means that the root rule options will not apply to the specified element. + +In addition to the options above (`noStrings`, `allowedStrings`, `noAttributeStrings` and `ignoreProps`), you can also specify the the following options that are specific to `elementOverrides`: + +- `allowElement` (default: `false`) - When `true` the rule will allow the specified element to have string literals as children, wrapped or unwrapped without warning. +- `applyToNestedElements` (default: `true`) - When `false` the rule will not apply the current options set to nested elements. This is useful when you want to apply the rule to a specific element, but not to its children. + +**Note**: As this rule has no way of differentiating between different componets with the same name, it is recommended to use this option with specific components that are unique to your application. + +#### `elementOverrides` Examples + +The following are **correct** examples that demonstrate how to use the `elementOverrides` option: + +```js +// "react/jsx-no-literals": [, {"elementOverrides": { "Button": {"allowElement": true} }}] + +var Hello =
{'test'}
; +var World = ; +``` + +```js +// "react/jsx-no-literals": [, {"elementOverrides": { "Text": {"allowElement": true} }}] + +var World = Hello world; +``` + +```js +// "react/jsx-no-literals": [, {"elementOverrides": { "Text": {"allowElement": true, "applyToNestedElements": false} }}] + +var linkText = 'world'; +var World = Hello {linkText}; +``` + +```js +// "react/jsx-no-literals": [, {"noStrings": true, "elementOverrides": { "Button": {"noStrings": false} }}] +// OR +// "react/jsx-no-literals": [, {"noStrings": true, "elementOverrides": { "Button": {} }}] + +var test = 'test' +var Hello =
{test}
; +var World = ; +``` + +## Examples To use, you can specify as follows: diff --git a/lib/rules/jsx-no-literals.js b/lib/rules/jsx-no-literals.js index 8280eade5b..ab2553d189 100644 --- a/lib/rules/jsx-no-literals.js +++ b/lib/rules/jsx-no-literals.js @@ -8,6 +8,10 @@ const iterFrom = require('es-iterator-helpers/Iterator.from'); const map = require('es-iterator-helpers/Iterator.prototype.map'); +const some = require('es-iterator-helpers/Iterator.prototype.some'); +const flatMap = require('es-iterator-helpers/Iterator.prototype.flatMap'); +const fromEntries = require('object.fromentries'); +const entries = require('object.entries'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -17,15 +21,165 @@ const getText = require('../util/eslint').getText; // Rule Definition // ------------------------------------------------------------------------------ -function trimIfString(val) { - return typeof val === 'string' ? val.trim() : val; +/** + * @param {unknown} value + * @returns {string | unknown} + */ +function trimIfString(value) { + return typeof value === 'string' ? value.trim() : value; } +const reOverridableElement = /^[A-Z][\w.]*$/; +const reIsWhiteSpace = /^[\s]+$/; +const jsxElementTypes = new Set(['JSXElement', 'JSXFragment']); +const standardJSXNodeParentTypes = new Set(['JSXAttribute', 'JSXElement', 'JSXExpressionContainer', 'JSXFragment']); + const messages = { invalidPropValue: 'Invalid prop value: "{{text}}"', + invalidPropValueInElement: 'Invalid prop value: "{{text}}" in {{element}}', noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"', + noStringsInAttributesInElement: 'Strings not allowed in attributes: "{{text}}" in {{element}}', noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"', + noStringsInJSXInElement: 'Strings not allowed in JSX files: "{{text}}" in {{element}}', literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"', + literalNotInJSXExpressionInElement: 'Missing JSX expression container around literal string: "{{text}}" in {{element}}', +}; + +/** @type {Exclude['properties']} */ +const commonPropertiesSchema = { + noStrings: { + type: 'boolean', + }, + allowedStrings: { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + }, + }, + ignoreProps: { + type: 'boolean', + }, + noAttributeStrings: { + type: 'boolean', + }, +}; + +/** + * @typedef RawElementConfigProperties + * @property {boolean} [noStrings] + * @property {string[]} [allowedStrings] + * @property {boolean} [ignoreProps] + * @property {boolean} [noAttributeStrings] + * + * @typedef RawOverrideConfigProperties + * @property {boolean} [allowElement] + * @property {boolean} [applyToNestedElements=true] + * + * @typedef {RawElementConfigProperties} RawElementConfig + * @typedef {RawElementConfigProperties & RawElementConfigProperties} RawOverrideConfig + * + * @typedef RawElementOverrides + * @property {Record} [elementOverrides] + * + * @typedef {RawElementConfig & RawElementOverrides} RawConfig + * + * ---------------------------------------------------------------------- + * + * @typedef ElementConfigType + * @property {'element'} type + * + * @typedef ElementConfigProperties + * @property {boolean} noStrings + * @property {Set} allowedStrings + * @property {boolean} ignoreProps + * @property {boolean} noAttributeStrings + * + * @typedef OverrideConfigProperties + * @property {'override'} type + * @property {string} name + * @property {boolean} allowElement + * @property {boolean} applyToNestedElements + * + * @typedef {ElementConfigType & ElementConfigProperties} ElementConfig + * @typedef {OverrideConfigProperties & ElementConfigProperties} OverrideConfig + * + * @typedef ElementOverrides + * @property {Record} elementOverrides + * + * @typedef {ElementConfig & ElementOverrides} Config + * @typedef {Config | OverrideConfig} ResolvedConfig + */ + +/** + * Normalizes the element portion of the config + * @param {RawConfig} config + * @returns {ElementConfig} + */ +function normalizeElementConfig(config) { + return { + type: 'element', + noStrings: !!config.noStrings, + allowedStrings: config.allowedStrings + ? new Set(map(iterFrom(config.allowedStrings), trimIfString)) + : new Set(), + ignoreProps: !!config.ignoreProps, + noAttributeStrings: !!config.noAttributeStrings, + }; +} + +/** + * Normalizes the config and applies default values to all config options + * @param {RawConfig} config + * @returns {Config} + */ +function normalizeConfig(config) { + /** @type {Config} */ + const normalizedConfig = Object.assign(normalizeElementConfig(config), { + elementOverrides: {}, + }); + + if (config.elementOverrides) { + normalizedConfig.elementOverrides = fromEntries( + flatMap( + iterFrom(entries(config.elementOverrides)), + (entry) => { + const elementName = entry[0]; + const rawElementConfig = entry[1]; + + if (!reOverridableElement.test(elementName)) { + return []; + } + + return [[ + elementName, + Object.assign(normalizeElementConfig(rawElementConfig), { + type: 'override', + name: elementName, + allowElement: !!rawElementConfig.allowElement, + applyToNestedElements: typeof rawElementConfig.applyToNestedElements === 'undefined' || !!rawElementConfig.applyToNestedElements, + }), + ]]; + } + ) + ); + } + + return normalizedConfig; +} + +const elementOverrides = { + type: 'object', + patternProperties: { + [reOverridableElement.source]: { + type: 'object', + properties: Object.assign( + { applyToNestedElements: { type: 'boolean' } }, + commonPropertiesSchema + ), + + }, + }, }; /** @type {import('eslint').Rule.RuleModule} */ @@ -42,114 +196,255 @@ module.exports = { schema: [{ type: 'object', - properties: { - noStrings: { - type: 'boolean', - }, - allowedStrings: { - type: 'array', - uniqueItems: true, - items: { - type: 'string', - }, - }, - ignoreProps: { - type: 'boolean', - }, - noAttributeStrings: { - type: 'boolean', - }, - }, + properties: Object.assign( + { elementOverrides }, + commonPropertiesSchema + ), additionalProperties: false, }], }, create(context) { - const defaults = { - noStrings: false, - allowedStrings: [], - ignoreProps: false, - noAttributeStrings: false, - }; - const config = Object.assign({}, defaults, context.options[0] || {}); - config.allowedStrings = new Set(map(iterFrom(config.allowedStrings), trimIfString)); - - function defaultMessageId(ancestorIsJSXElement) { - if (config.noAttributeStrings && !ancestorIsJSXElement) { - return 'noStringsInAttributes'; + /** @type {RawConfig} */ + const rawConfig = (context.options.length && context.options[0]) || {}; + const config = normalizeConfig(rawConfig); + + const hasElementOverrides = Object.keys(config.elementOverrides).length > 0; + + /** @type {Map} */ + const renamedImportMap = new Map(); + + /** + * Determines if the given expression is a require statement. Supports + * nested MemberExpresions. ie `require('foo').nested.property` + * @param {ASTNode} node + * @returns {boolean} + */ + function isRequireStatement(node) { + if (node.type === 'CallExpression') { + if (node.callee.type === 'Identifier') { + return node.callee.name === 'require'; + } } - if (config.noStrings) { - return 'noStringsInJSX'; + if (node.type === 'MemberExpression') { + return isRequireStatement(node.object); } - return 'literalNotInJSXExpression'; - } - function getParentIgnoringBinaryExpressions(node) { - let current = node; - while (current.parent.type === 'BinaryExpression') { - current = current.parent; - } - return current.parent; + return false; } - function getValidation(node) { - const values = [trimIfString(node.raw), trimIfString(node.value)]; - if (values.some((value) => config.allowedStrings.has(value))) { - return false; + /** @typedef {{ name: string, compoundName?: string }} ElementNameFragment */ + + /** + * Gets the name of the given JSX element. Supports nested + * JSXMemeberExpressions. ie `` + * @param {ASTNode} node + * @returns {ElementNameFragment | undefined} + */ + function getJSXElementName(node) { + if (node.openingElement.name.type === 'JSXIdentifier') { + const name = node.openingElement.name.name; + return { + name: renamedImportMap.get(name) || name, + compoundName: undefined, + }; } - const parent = getParentIgnoringBinaryExpressions(node); + /** @type {string[]} */ + const nameFragments = []; - function isParentNodeStandard() { - if (!/^[\s]+$/.test(node.value) && typeof node.value === 'string' && parent.type.includes('JSX')) { - if (config.noAttributeStrings) { - return parent.type === 'JSXAttribute' || parent.type === 'JSXElement'; + if (node.openingElement.name.type === 'JSXMemberExpression') { + /** @type {ASTNode} */ + let current = node.openingElement.name; + while (current.type === 'JSXMemberExpression') { + if (current.property.type === 'JSXIdentifier') { + nameFragments.unshift(current.property.name); } - if (!config.noAttributeStrings) { - return parent.type !== 'JSXAttribute'; + + current = current.object; + } + + if (current.type === 'JSXIdentifier') { + nameFragments.unshift(current.name); + + const rootFragment = nameFragments[0]; + if (rootFragment) { + const rootFragmentRenamed = renamedImportMap.get(rootFragment); + if (rootFragmentRenamed) { + nameFragments[0] = rootFragmentRenamed; + } + } + + const nameFragment = nameFragments[nameFragments.length - 1]; + if (nameFragment) { + return { + name: nameFragment, + compoundName: nameFragments.join('.'), + }; } } + } + } - return false; + /** + * Gets all JSXElement ancestor nodes for the given node + * @param {ASTNode} node + * @returns {ASTNode[]} + */ + function getJSXElementAncestors(node) { + /** @type {ASTNode[]} */ + const ancestors = []; + + let current = node; + while (current) { + if (current.type === 'JSXElement') { + ancestors.push(current); + } + + current = current.parent; } - const standard = isParentNodeStandard(); + return ancestors; + } - if (config.noStrings) { - return standard; + /** + * @param {ASTNode} node + * @returns {ASTNode} + */ + function getParentIgnoringBinaryExpressions(node) { + let current = node; + while (current.parent.type === 'BinaryExpression') { + current = current.parent; } - return standard && parent.type !== 'JSXExpressionContainer'; + return current.parent; } - function getParentAndGrandParentType(node) { + /** + * @param {ASTNode} node + * @returns {{ parent: ASTNode, grandParent: ASTNode }} + */ + function getParentAndGrandParent(node) { const parent = getParentIgnoringBinaryExpressions(node); - const parentType = parent.type; - const grandParentType = parent.parent.type; - return { parent, - parentType, - grandParentType, grandParent: parent.parent, }; } + /** + * @param {ASTNode} node + * @returns {boolean} + */ function hasJSXElementParentOrGrandParent(node) { - const parents = getParentAndGrandParentType(node); - const parentType = parents.parentType; - const grandParentType = parents.grandParentType; + const ancestors = getParentAndGrandParent(node); + return some(iterFrom([ancestors.parent, ancestors.grandParent]), (parent) => jsxElementTypes.has(parent.type)); + } + + /** + * Determines whether a given node's value and its immediate parent are + * viable text nodes that can/should be reported on + * @param {ASTNode} node + * @param {ResolvedConfig} resolvedConfig + * @returns {boolean} + */ + function isViableTextNode(node, resolvedConfig) { + const textValues = iterFrom([trimIfString(node.raw), trimIfString(node.value)]); + if (some(textValues, (value) => resolvedConfig.allowedStrings.has(value))) { + return false; + } + + const parent = getParentIgnoringBinaryExpressions(node); + + let isStandardJSXNode = false; + if (typeof node.value === 'string' && !reIsWhiteSpace.test(node.value) && standardJSXNodeParentTypes.has(parent.type)) { + if (resolvedConfig.noAttributeStrings) { + isStandardJSXNode = parent.type === 'JSXAttribute' || parent.type === 'JSXElement'; + } else { + isStandardJSXNode = parent.type !== 'JSXAttribute'; + } + } + + if (resolvedConfig.noStrings) { + return isStandardJSXNode; + } + + return isStandardJSXNode && parent.type !== 'JSXExpressionContainer'; + } + + /** + * Gets an override config for a given node. For any given node, we also + * need to traverse the ancestor tree to determine if an ancestor's config + * will also apply to the current node. + * @param {ASTNode} node + * @returns {OverrideConfig | undefined} + */ + function getOverrideConfig(node) { + if (!hasElementOverrides) { + return; + } - return parentType === 'JSXFragment' || parentType === 'JSXElement' || grandParentType === 'JSXElement'; + const allAncestorElements = getJSXElementAncestors(node); + if (!allAncestorElements.length) { + return; + } + + for (const ancestorElement of allAncestorElements) { + const isClosestJSXAncestor = ancestorElement === allAncestorElements[0]; + + const ancestor = getJSXElementName(ancestorElement); + if (ancestor) { + if (ancestor.name) { + const ancestorElements = config.elementOverrides[ancestor.name]; + const ancestorConfig = ancestor.compoundName + ? config.elementOverrides[ancestor.compoundName] || ancestorElements + : ancestorElements; + + if (ancestorConfig) { + if (isClosestJSXAncestor || ancestorConfig.applyToNestedElements) { + return ancestorConfig; + } + } + } + } + } } - function reportLiteralNode(node, messageId) { - const ancestorIsJSXElement = hasJSXElementParentOrGrandParent(node); - messageId = messageId || defaultMessageId(ancestorIsJSXElement); + /** + * @param {ResolvedConfig} resolvedConfig + * @returns {boolean} + */ + function shouldAllowElement(resolvedConfig) { + return resolvedConfig.type === 'override' && 'allowElement' in resolvedConfig && !!resolvedConfig.allowElement; + } + /** + * @param {boolean} ancestorIsJSXElement + * @param {ResolvedConfig} resolvedConfig + * @returns {string} + */ + function defaultMessageId(ancestorIsJSXElement, resolvedConfig) { + if (resolvedConfig.noAttributeStrings && !ancestorIsJSXElement) { + return resolvedConfig.type === 'override' ? 'noStringsInAttributesInElement' : 'noStringsInAttributes'; + } + + if (resolvedConfig.noStrings) { + return resolvedConfig.type === 'override' ? 'noStringsInJSXInElement' : 'noStringsInJSX'; + } + + return resolvedConfig.type === 'override' ? 'literalNotInJSXExpressionInElement' : 'literalNotInJSXExpression'; + } + + /** + * @param {ASTNode} node + * @param {string} messageId + * @param {ResolvedConfig} resolvedConfig + */ + function reportLiteralNode(node, messageId, resolvedConfig) { report(context, messages[messageId], messageId, { node, data: { text: getText(context, node).trim(), + element: resolvedConfig.type === 'override' && 'name' in resolvedConfig ? resolvedConfig.name : undefined, }, }); } @@ -158,39 +453,103 @@ module.exports = { // Public // -------------------------------------------------------------------------- - return { + return Object.assign(hasElementOverrides ? { + // Get renamed import local names mapped to their imported name + ImportDeclaration(node) { + node.specifiers + .filter((s) => s.type === 'ImportSpecifier') + .forEach((specifier) => { + renamedImportMap.set( + (specifier.local || specifier.imported).name, + specifier.imported.name + ); + }); + }, + + // Get renamed destructured local names mapped to their imported name + VariableDeclaration(node) { + node.declarations + .filter((d) => ( + d.type === 'VariableDeclarator' + && isRequireStatement(d.init) + && d.id.type === 'ObjectPattern' + )) + .forEach((declaration) => { + declaration.id.properties + .filter((property) => ( + property.type === 'Property' + && property.key.type === 'Identifier' + && property.value.type === 'Identifier' + )) + .forEach((property) => { + renamedImportMap.set(property.value.name, property.key.name); + }); + }); + }, + } : false, { Literal(node) { - if (getValidation(node) && (hasJSXElementParentOrGrandParent(node) || !config.ignoreProps)) { - reportLiteralNode(node); + const resolvedConfig = getOverrideConfig(node) || config; + + const hasJSXParentOrGrandParent = hasJSXElementParentOrGrandParent(node); + if (hasJSXParentOrGrandParent && shouldAllowElement(resolvedConfig)) { + return; + } + + if (isViableTextNode(node, resolvedConfig)) { + if (hasJSXParentOrGrandParent || !config.ignoreProps) { + reportLiteralNode(node, defaultMessageId(hasJSXParentOrGrandParent, resolvedConfig), resolvedConfig); + } } }, JSXAttribute(node) { - const isNodeValueString = node && node.value && node.value.type === 'Literal' && typeof node.value.value === 'string' && !config.allowedStrings.has(node.value.value); + const isLiteralString = node.value.type === 'Literal' + && typeof node.value.value === 'string'; + const isStringLiteral = node.value.type === 'StringLiteral'; + + if (isLiteralString || isStringLiteral) { + const resolvedConfig = getOverrideConfig(node) || config; - if (config.noStrings && !config.ignoreProps && isNodeValueString) { - const messageId = 'invalidPropValue'; - reportLiteralNode(node, messageId); + if ( + resolvedConfig.noStrings + && !resolvedConfig.ignoreProps + && !resolvedConfig.allowedStrings.has(node.value.value) + ) { + const messageId = resolvedConfig.type === 'override' ? 'invalidPropValueInElement' : 'invalidPropValue'; + reportLiteralNode(node, messageId, resolvedConfig); + } } }, JSXText(node) { - if (getValidation(node)) { - reportLiteralNode(node); + const resolvedConfig = getOverrideConfig(node) || config; + + if (shouldAllowElement(resolvedConfig)) { + return; + } + + if (isViableTextNode(node, resolvedConfig)) { + const hasJSXParendOrGrantParent = hasJSXElementParentOrGrandParent(node); + reportLiteralNode(node, defaultMessageId(hasJSXParendOrGrantParent, resolvedConfig), resolvedConfig); } }, TemplateLiteral(node) { - const parents = getParentAndGrandParentType(node); - const parentType = parents.parentType; - const grandParentType = parents.grandParentType; - const isParentJSXExpressionCont = parentType === 'JSXExpressionContainer'; - const isParentJSXElement = parentType === 'JSXElement' || grandParentType === 'JSXElement'; - - if (isParentJSXExpressionCont && config.noStrings && (isParentJSXElement || !config.ignoreProps)) { - reportLiteralNode(node); + const ancestors = getParentAndGrandParent(node); + const isParentJSXExpressionCont = ancestors.parent.type === 'JSXExpressionContainer'; + const isParentJSXElement = ancestors.grandParent.type === 'JSXElement'; + + if (isParentJSXExpressionCont) { + const resolvedConfig = getOverrideConfig(node) || config; + + if ( + resolvedConfig.noStrings + && (isParentJSXElement || !resolvedConfig.ignoreProps) + ) { + reportLiteralNode(node, defaultMessageId(isParentJSXElement, resolvedConfig), resolvedConfig); + } } }, - }; + }); }, }; diff --git a/tests/lib/rules/jsx-no-literals.js b/tests/lib/rules/jsx-no-literals.js index e064b9f525..d1f6135f82 100644 --- a/tests/lib/rules/jsx-no-literals.js +++ b/tests/lib/rules/jsx-no-literals.js @@ -296,6 +296,186 @@ ruleTester.run('jsx-no-literals', rule, { `, options: [{ noStrings: true, allowedStrings: ['—', '—'] }], }, + { + code: ` + foo + `, + options: [{ elementOverrides: { T: { allowElement: true } } }], + }, + { + code: ` + foo
bar
+ `, + options: [{ elementOverrides: { T: { allowElement: true } } }], + }, + { + code: ` + foo
{'bar'}
+ `, + options: [{ elementOverrides: { T: { allowElement: true, applyToNestedElements: false } } }], + }, + { + code: ` +
+
{'foo'}
+ {2} +
+ `, + options: [{ elementOverrides: { T: { noStrings: true } } }], + }, + { + code: ` + {2}
{2}
+ `, + options: [{ elementOverrides: { T: { noStrings: true } } }], + }, + { + code: ` + {2}
{'foo'}
+ `, + options: [{ elementOverrides: { T: { noStrings: true, applyToNestedElements: false } } }], + }, + { + code: ` +
+
{'foo'}
+ foo +
+ `, + options: [{ elementOverrides: { T: { allowedStrings: ['foo'] } } }], + }, + { + code: ` + foo
foo
+ `, + options: [{ elementOverrides: { T: { allowedStrings: ['foo'] } } }], + }, + { + code: ` + foo
{'foo'}
+ `, + options: [{ elementOverrides: { T: { allowedStrings: ['foo'], applyToNestedElements: false } } }], + }, + { + code: ` +
+
+ +
+ `, + options: [{ noStrings: true, elementOverrides: { T: { noStrings: true, ignoreProps: true } } }], + }, + { + code: ` +
+ `, + options: [{ noStrings: true, elementOverrides: { T: { noStrings: true, ignoreProps: true } } }], + }, + { + code: ` +
+ `, + options: [{ noStrings: true, elementOverrides: { T: { noStrings: true, ignoreProps: true, applyToNestedElements: false } } }], + }, + { + code: ` +
+
+ +
+ `, + options: [{ elementOverrides: { T: { noAttributeStrings: true } } }], + }, + { + code: ` +
+ `, + options: [{ elementOverrides: { T: { noAttributeStrings: true } } }], + }, + { + code: ` +
+ `, + options: [{ elementOverrides: { T: { noAttributeStrings: true, applyToNestedElements: false } } }], + }, + { + code: ` + foofoo + `, + options: [{ elementOverrides: { T: { allowedStrings: ['foo'] }, U: { allowedStrings: ['foo'] } } }], + }, + { + code: ` + import { T } from 'foo'; + {'foo'} + `, + }, + { + code: ` + import { T as U } from 'foo'; + foo + `, + options: [{ elementOverrides: { T: { allowElement: true } } }], + }, + { + code: ` + const { T: U } = require('foo'); + foo + `, + options: [{ elementOverrides: { T: { allowElement: true } } }], + }, + { + code: ` + const { T: U } = require('foo').Foo; + foo + `, + options: [{ elementOverrides: { T: { allowElement: true } } }], + }, + { + code: ` + const { T: U } = require('foo').Foo.Foo; + foo + `, + options: [{ elementOverrides: { T: { allowElement: true } } }], + }, + { + code: ` + const foo = 2; + foo + `, + options: [{ elementOverrides: { T: { allowElement: true } } }], + }, + { + code: ` + foo + `, + options: [{ elementOverrides: { 'T.U': { allowElement: true } } }], + }, + { + code: ` + import { T as U } from 'foo'; + foo + `, + options: [{ elementOverrides: { 'T.U': { allowElement: true } } }], + }, + { + code: ` + foo + `, + options: [{ elementOverrides: { Fragment: { allowElement: true } } }], + }, + { + code: ` + foo + `, + options: [{ elementOverrides: { 'React.Fragment': { allowElement: true } } }], + }, + { + code: ` +
{'foo'}
+ `, + options: [{ elementOverrides: { div: { allowElement: true } } }], + }, ]), invalid: parsers.all([ @@ -521,6 +701,7 @@ ruleTester.run('jsx-no-literals', rule, { }, ], }, + /* eslint-disable no-template-curly-in-string */ { code: '', options: [{ noStrings: true, ignoreProps: false }], @@ -541,6 +722,7 @@ ruleTester.run('jsx-no-literals', rule, { }, ], }, + /* eslint-enable no-template-curly-in-string */ { code: '', options: [{ noStrings: true, ignoreProps: false }], @@ -663,5 +845,329 @@ ruleTester.run('jsx-no-literals', rule, { }, ], }, + { + code: ` +
+
foo
+ bar +
+ `, + options: [{ elementOverrides: { T: {} } }], + errors: [ + { messageId: 'literalNotInJSXExpression', data: { text: 'foo' } }, + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'bar', element: 'T' } }, + ], + }, + { + code: ` +
+
foo
+ bar +
+ `, + options: [{ elementOverrides: { T: { allowElement: true } } }], + errors: [ + { messageId: 'literalNotInJSXExpression', data: { text: 'foo' } }, + ], + }, + { + code: ` + foo
bar
+ `, + options: [{ elementOverrides: { T: { allowElement: true, applyToNestedElements: false } } }], + errors: [{ messageId: 'literalNotInJSXExpression', data: { text: 'bar' } }], + }, + { + code: ` +
+
foo
+ {'bar'} +
+ `, + options: [{ elementOverrides: { T: { noStrings: true } } }], + errors: [ + { messageId: 'literalNotInJSXExpression', data: { text: 'foo' } }, + { messageId: 'noStringsInJSXInElement', data: { text: '\'bar\'', element: 'T' } }, + ], + }, + { + code: ` +
+
foo
+ {'bar'}
{'baz'}
+
+ `, + options: [{ elementOverrides: { T: { noStrings: true } } }], + errors: [ + { messageId: 'literalNotInJSXExpression', data: { text: 'foo' } }, + { messageId: 'noStringsInJSXInElement', data: { text: '\'bar\'', element: 'T' } }, + { messageId: 'noStringsInJSXInElement', data: { text: '\'baz\'', element: 'T' } }, + ], + }, + { + code: ` +
+
foo
+ {'bar'}
{'baz'}
+
+ `, + options: [{ elementOverrides: { T: { noStrings: true, applyToNestedElements: false } } }], + errors: [ + { messageId: 'literalNotInJSXExpression', data: { text: 'foo' } }, + { messageId: 'noStringsInJSXInElement', data: { text: '\'bar\'', element: 'T' } }, + ], + }, + { + code: ` +
+
{'foo'}
+ {'foo'} +
+ `, + options: [{ noStrings: true, elementOverrides: { T: { noStrings: true, allowedStrings: ['foo'] } } }], + errors: [ + { messageId: 'noStringsInJSX', data: { text: '\'foo\'' } }, + ], + }, + { + code: ` +
+
{'foo'}
+ {'foo'}
{'foo'}
+
+ `, + options: [{ noStrings: true, elementOverrides: { T: { noStrings: true, allowedStrings: ['foo'] } } }], + errors: [ + { messageId: 'noStringsInJSX', data: { text: '\'foo\'' } }, + ], + }, + { + code: ` +
+
{'foo'}
+ {'foo'}
{'foo'}
+
+ `, + options: [{ noStrings: true, elementOverrides: { T: { noStrings: true, allowedStrings: ['foo'], applyToNestedElements: false } } }], + errors: [ + { messageId: 'noStringsInJSX', data: { text: '\'foo\'' } }, + { messageId: 'noStringsInJSX', data: { text: '\'foo\'' } }, + ], + }, + { + code: ` +
+
+ +
+ `, + options: [{ noStrings: true, elementOverrides: { T: { noStrings: true, ignoreProps: true } } }], + errors: [ + { messageId: 'invalidPropValue', data: { text: 'foo1="bar"' } }, + ], + }, + { + code: ` +
+
+
+
+ `, + options: [{ noStrings: true, elementOverrides: { T: { noStrings: true, ignoreProps: true } } }], + errors: [ + { messageId: 'invalidPropValue', data: { text: 'foo1="bar"' } }, + ], + }, + { + code: ` +
+
+
+
+ `, + options: [{ noStrings: true, elementOverrides: { T: { noStrings: true, ignoreProps: true, applyToNestedElements: false } } }], + errors: [ + { messageId: 'invalidPropValue', data: { text: 'foo1="bar"' } }, + { messageId: 'invalidPropValue', data: { text: 'foo3="bar"' } }, + ], + }, + { + code: ` +
+
+ +
+ `, + options: [{ elementOverrides: { T: { noAttributeStrings: true } } }], + errors: [ + { messageId: 'noStringsInAttributesInElement', data: { text: '"bar2"', element: 'T' } }, + ], + }, + { + code: ` +
+
+
+
+ `, + options: [{ elementOverrides: { T: { noAttributeStrings: true } } }], + errors: [ + { messageId: 'noStringsInAttributesInElement', data: { text: '"bar2"', element: 'T' } }, + { messageId: 'noStringsInAttributesInElement', data: { text: '"bar3"', element: 'T' } }, + ], + }, + { + code: ` +
+
+
+
+ `, + options: [{ elementOverrides: { T: { noAttributeStrings: true, applyToNestedElements: false } } }], + errors: [ + { messageId: 'noStringsInAttributesInElement', data: { text: '"bar2"', element: 'T' } }, + ], + }, + { + code: ` +
+
{'foo'}
+ {'bar'} +
+ `, + options: [{ noStrings: true, elementOverrides: { T: {} } }], + errors: [ + { messageId: 'noStringsInJSX', data: { text: '\'foo\'' } }, + ], + }, + { + code: ` +
+
foo
+ foo +
+ `, + options: [{ allowedStrings: ['foo'], elementOverrides: { T: {} } }], + errors: [ + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'foo', element: 'T' } }, + ], + }, + { + code: ` +
+
foo
+ foo + bar + baz +
+ `, + options: [{ allowedStrings: ['foo'], elementOverrides: { T: { allowedStrings: ['bar'] } } }], + errors: [ + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'foo', element: 'T' } }, + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'baz', element: 'T' } }, + ], + }, + { + code: ` +
+
+ +
+ `, + options: [{ noStrings: true, ignoreProps: true, elementOverrides: { T: { noStrings: true } } }], + errors: [ + { messageId: 'invalidPropValueInElement', data: { text: 'foo2="bar2"', element: 'T' } }, + ], + }, + { + code: ` +
+
+ +
+ `, + options: [{ noAttributeStrings: true, elementOverrides: { T: {} } }], + errors: [ + { messageId: 'noStringsInAttributes', data: { text: '"bar1"' } }, + ], + }, + { + code: ` +
+ foo + bar +
+ `, + options: [{ elementOverrides: { T: {}, U: {} } }], + errors: [ + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'foo', element: 'T' } }, + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'bar', element: 'U' } }, + ], + }, + { + code: ` +
+ foo + bar +
+ `, + options: [{ elementOverrides: { T: {}, U: { allowElement: true } } }], + errors: [ + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'foo', element: 'T' } }, + ], + }, + { + code: ` + foo bar + `, + options: [{ elementOverrides: { T: {}, U: { allowElement: true } } }], + errors: [ + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'foo', element: 'T' } }, + ], + }, + { + code: ` + {'foo'}{'bar'} + `, + options: [{ elementOverrides: { T: { noStrings: true }, U: {} } }], + errors: [ + { messageId: 'noStringsInJSXInElement', data: { text: '\'foo\'', element: 'T' } }, + ], + }, + { + code: ` + foofoo + `, + options: [{ elementOverrides: { T: { allowedStrings: ['foo'] }, U: {} } }], + errors: [ + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'foo', element: 'U' } }, + ], + }, + { + code: ` + foofoo + `, + options: [{ elementOverrides: { T: {}, U: { allowedStrings: ['foo'] } } }], + errors: [ + { messageId: 'literalNotInJSXExpressionInElement', data: { text: 'foo', element: 'T' } }, + ], + }, + { + code: ` +
+ foo + foo +
+ `, + options: [{ elementOverrides: { 'React.Fragment': { allowElement: true } } }], + errors: [{ messageId: 'literalNotInJSXExpression', data: { text: 'foo' } }], + }, + { + code: ` +
foo
+ `, + options: [{ elementOverrides: { div: { allowElement: true } } }], + errors: [{ messageId: 'literalNotInJSXExpression', data: { text: 'foo' } }], + }, ]), });