diff --git a/docs/rules/jsx-no-literals.md b/docs/rules/jsx-no-literals.md index 0ff292cadc..c88c7f977c 100644 --- a/docs/rules/jsx-no-literals.md +++ b/docs/rules/jsx-no-literals.md @@ -34,6 +34,7 @@ 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`. +- `restrictedAttributes` - An array of unique attribute names where string literals should be restricted. Only the specified attributes will be checked for string literals when this option is used. **Note**: When `noAttributeStrings` is `true`, this option is ignored at the root level. - `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` diff --git a/lib/rules/jsx-no-literals.js b/lib/rules/jsx-no-literals.js index 45c9a54c48..5008464e8e 100644 --- a/lib/rules/jsx-no-literals.js +++ b/lib/rules/jsx-no-literals.js @@ -51,6 +51,8 @@ const messages = { 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}}', + restrictedAttributeString: 'Restricted attribute string: "{{text}}" in {{attribute}}', + restrictedAttributeStringInElement: 'Restricted attribute string: "{{text}}" in {{attribute}} of {{element}}', }; /** @type {Exclude['properties']} */ @@ -71,6 +73,13 @@ const commonPropertiesSchema = { noAttributeStrings: { type: 'boolean', }, + restrictedAttributes: { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + }, + }, }; // eslint-disable-next-line valid-jsdoc @@ -88,6 +97,9 @@ function normalizeElementConfig(config) { : new Set(), ignoreProps: !!config.ignoreProps, noAttributeStrings: !!config.noAttributeStrings, + restrictedAttributes: config.restrictedAttributes + ? new Set(map(iterFrom(config.restrictedAttributes), trimIfString)) + : new Set(), }; } @@ -478,6 +490,26 @@ module.exports = { if (isLiteralString || isStringLiteral) { const resolvedConfig = getOverrideConfig(node) || config; + const restrictedAttributes = resolvedConfig.restrictedAttributes; + + if (restrictedAttributes.size > 0 && node.name && node.name.type === 'JSXIdentifier') { + const attributeName = node.name.name; + + if (!restrictedAttributes.has(attributeName)) { + return; // Skip reporting this attribute if it's not in the restricted list + } + + const messageId = resolvedConfig.type === 'override' ? 'restrictedAttributeStringInElement' : 'restrictedAttributeString'; + report(context, messages[messageId], messageId, { + node, + data: { + text: node.value.value, + attribute: attributeName, + element: resolvedConfig.type === 'override' && 'name' in resolvedConfig ? resolvedConfig.name : undefined, + }, + }); + return; + } if ( resolvedConfig.noStrings diff --git a/tests/lib/rules/jsx-no-literals.js b/tests/lib/rules/jsx-no-literals.js index d1f6135f82..62b08d972b 100644 --- a/tests/lib/rules/jsx-no-literals.js +++ b/tests/lib/rules/jsx-no-literals.js @@ -296,6 +296,18 @@ ruleTester.run('jsx-no-literals', rule, { `, options: [{ noStrings: true, allowedStrings: ['—', '—'] }], }, + { + code: ` + text + `, + options: [{ restrictedAttributes: ['className', 'id'] }], + }, + { + code: ` +
+ `, + options: [{ restrictedAttributes: [] }], + }, { code: ` foo @@ -476,6 +488,45 @@ ruleTester.run('jsx-no-literals', rule, { `, options: [{ elementOverrides: { div: { allowElement: true } } }], }, + { + code: ` +
+ +
+ `, + options: [{ + elementOverrides: { + Input: { restrictedAttributes: ['placeholder'] }, + Button: { restrictedAttributes: ['type'] }, + }, + }], + }, + { + code: ` +
+
+ `, + options: [{ + restrictedAttributes: ['className'], + elementOverrides: { + Button: { restrictedAttributes: ['disabled'] }, + }, + }], + }, + { + code: ` +
+ `, + options: [{ + elementOverrides: { + Button: { restrictedAttributes: ['type'] }, + }, + }], + errors: [ + { messageId: 'restrictedAttributeStringInElement', data: { text: 'submit', attribute: 'type', element: 'Button' } }, + ], + }, + { + code: ` +
+ +
+ `, + options: [{ + elementOverrides: { + Input: { restrictedAttributes: ['placeholder'] }, + Button: { restrictedAttributes: ['disabled'] }, + }, + }], + errors: [ + { messageId: 'restrictedAttributeStringInElement', data: { text: 'Enter text', attribute: 'placeholder', element: 'Input' } }, + { messageId: 'restrictedAttributeStringInElement', data: { text: 'true', attribute: 'disabled', element: 'Button' } }, + ], + }, + { + code: ` +
+
+
+ `, + options: [{ + restrictedAttributes: ['className'], + elementOverrides: { + Button: { restrictedAttributes: ['id'] }, + }, + }], + errors: [ + { messageId: 'restrictedAttributeString', data: { text: 'wrapper', attribute: 'className' } }, + { messageId: 'restrictedAttributeStringInElement', data: { text: 'submit-btn', attribute: 'id', element: 'Button' } }, + ], + }, + { + code: ` +
+
+ +
+ `, + options: [{ + noAttributeStrings: true, + elementOverrides: { + T: { restrictedAttributes: ['foo2'] }, + }, + }], + errors: [ + { messageId: 'noStringsInAttributes', data: { text: '"bar1"' } }, + { messageId: 'restrictedAttributeStringInElement', data: { text: 'bar2', attribute: 'foo2', element: 'T' } }, + ], + }, ]), }); diff --git a/types/rules/jsx-no-literals.d.ts b/types/rules/jsx-no-literals.d.ts index c1b9638958..75d366f563 100644 --- a/types/rules/jsx-no-literals.d.ts +++ b/types/rules/jsx-no-literals.d.ts @@ -3,6 +3,7 @@ type RawElementConfig = { allowedStrings?: string[]; ignoreProps?: boolean; noAttributeStrings?: boolean; + restrictedAttributes?: string[]; }; type RawOverrideConfig = { @@ -25,6 +26,7 @@ interface ElementConfigProperties { allowedStrings: Set; ignoreProps: boolean; noAttributeStrings: boolean; + restrictedAttributes: Set; } interface OverrideConfigProperties {