Skip to content

Commit b62259f

Browse files
ushiboyljharb
authored andcommitted
[New] jsx-no-literals: add restrictedAttributes option
Closes #3003.
1 parent dc656eb commit b62259f

5 files changed

Lines changed: 237 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1111
* [`jsx-handler-names`]: support namespaced component names ([#3943][] @takuji)
1212
* [`jsx-no-leaked-render`]: add `ignoreAttributes` option ([#3441][] @aleclarson)
1313
* [`jsx-sort-props`]: add `sortFirst` option ([#3965][] @loderunner)
14+
* [`jsx-no-literals`]: add `restrictedAttributes` option ([#3950][] @ushiboy)
1415

1516
### Fixed
1617
* [`no-unknown-property`]: allow `onLoad` on `body` ([#3923][] @DerekStapleton)
@@ -26,6 +27,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
2627
[#3958]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3958
2728
[#3980]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3980
2829
[#3965]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3965
30+
[#3950]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3950
2931
[#3943]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3943
3032
[#3942]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3942
3133
[#3930]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3930

docs/rules/jsx-no-literals.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The supported options are:
3636
- `allowedStrings` - An array of unique string values that would otherwise warn, but will be ignored.
3737
- `ignoreProps` (default: `false`) - When `true` the rule ignores literals used in props, wrapped or unwrapped.
3838
- `noAttributeStrings` (default: `false`) - Enforces no string literals used in attributes when set to `true`.
39+
- `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.
3940
- `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.
4041

4142
### `elementOverrides`

lib/rules/jsx-no-literals.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ const messages = {
5151
noStringsInJSXInElement: 'Strings not allowed in JSX files: "{{text}}" in {{element}}',
5252
literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"',
5353
literalNotInJSXExpressionInElement: 'Missing JSX expression container around literal string: "{{text}}" in {{element}}',
54+
restrictedAttributeString: 'Restricted attribute string: "{{text}}" in {{attribute}}',
55+
restrictedAttributeStringInElement: 'Restricted attribute string: "{{text}}" in {{attribute}} of {{element}}',
5456
};
5557

5658
/** @type {Exclude<RuleModule['meta']['schema'], unknown[] | false>['properties']} */
@@ -71,6 +73,13 @@ const commonPropertiesSchema = {
7173
noAttributeStrings: {
7274
type: 'boolean',
7375
},
76+
restrictedAttributes: {
77+
type: 'array',
78+
uniqueItems: true,
79+
items: {
80+
type: 'string',
81+
},
82+
},
7483
};
7584

7685
// eslint-disable-next-line valid-jsdoc
@@ -88,6 +97,9 @@ function normalizeElementConfig(config) {
8897
: new Set(),
8998
ignoreProps: !!config.ignoreProps,
9099
noAttributeStrings: !!config.noAttributeStrings,
100+
restrictedAttributes: config.restrictedAttributes
101+
? new Set(map(iterFrom(config.restrictedAttributes), trimIfString))
102+
: new Set(),
91103
};
92104
}
93105

@@ -478,6 +490,26 @@ module.exports = {
478490

479491
if (isLiteralString || isStringLiteral) {
480492
const resolvedConfig = getOverrideConfig(node) || config;
493+
const restrictedAttributes = resolvedConfig.restrictedAttributes;
494+
495+
if (restrictedAttributes.size > 0 && node.name && node.name.type === 'JSXIdentifier') {
496+
const attributeName = node.name.name;
497+
498+
if (restrictedAttributes.has(attributeName)) {
499+
if (!resolvedConfig.allowedStrings.has(String(trimIfString(node.value.value)))) {
500+
const messageId = resolvedConfig.type === 'override' ? 'restrictedAttributeStringInElement' : 'restrictedAttributeString';
501+
report(context, messages[messageId], messageId, {
502+
node,
503+
data: {
504+
text: getText(context, node.value).trim(),
505+
attribute: attributeName,
506+
element: resolvedConfig.type === 'override' && 'name' in resolvedConfig ? resolvedConfig.name : undefined,
507+
},
508+
});
509+
}
510+
return;
511+
}
512+
}
481513

482514
if (
483515
resolvedConfig.noStrings

tests/lib/rules/jsx-no-literals.js

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,35 @@ ruleTester.run('jsx-no-literals', rule, {
296296
`,
297297
options: [{ noStrings: true, allowedStrings: ['&mdash;', '—'] }],
298298
},
299+
{
300+
code: `
301+
<img src="image.jpg" alt="text" />
302+
`,
303+
options: [{ restrictedAttributes: ['className', 'id'] }],
304+
},
305+
{
306+
code: `
307+
<div className="allowed" />
308+
`,
309+
options: [{ restrictedAttributes: ['className'], allowedStrings: ['allowed'] }],
310+
},
311+
{
312+
code: `
313+
<div className="test" title="hello" />
314+
`,
315+
options: [{
316+
noStrings: true,
317+
ignoreProps: true,
318+
restrictedAttributes: ['className'],
319+
allowedStrings: ['test'],
320+
}],
321+
},
322+
{
323+
code: `
324+
<div className="test" id="foo" />
325+
`,
326+
options: [{ restrictedAttributes: [] }],
327+
},
299328
{
300329
code: `
301330
<T>foo</T>
@@ -476,6 +505,45 @@ ruleTester.run('jsx-no-literals', rule, {
476505
`,
477506
options: [{ elementOverrides: { div: { allowElement: true } } }],
478507
},
508+
{
509+
code: `
510+
<div>
511+
<Input type="text" />
512+
<Button className="primary" />
513+
<Image src="photo.jpg" />
514+
</div>
515+
`,
516+
options: [{
517+
elementOverrides: {
518+
Input: { restrictedAttributes: ['placeholder'] },
519+
Button: { restrictedAttributes: ['type'] },
520+
},
521+
}],
522+
},
523+
{
524+
code: `
525+
<div title="container">
526+
<Button className="btn" />
527+
</div>
528+
`,
529+
options: [{
530+
restrictedAttributes: ['className'],
531+
elementOverrides: {
532+
Button: { restrictedAttributes: ['disabled'] },
533+
},
534+
}],
535+
},
536+
{
537+
code: `
538+
<Button className="btn" />
539+
`,
540+
options: [{
541+
noAttributeStrings: true,
542+
elementOverrides: {
543+
Button: { restrictedAttributes: ['type'] },
544+
},
545+
}],
546+
},
479547
]),
480548

481549
invalid: parsers.all([
@@ -845,6 +913,68 @@ ruleTester.run('jsx-no-literals', rule, {
845913
},
846914
],
847915
},
916+
{
917+
code: `
918+
<div className="test" />
919+
`,
920+
options: [{ restrictedAttributes: ['className'] }],
921+
errors: [{
922+
messageId: 'restrictedAttributeString',
923+
data: { text: '"test"', attribute: 'className' },
924+
}],
925+
},
926+
{
927+
code: `
928+
<div className="test" id="foo" title="bar" />
929+
`,
930+
options: [{ restrictedAttributes: ['className', 'id'] }],
931+
errors: [
932+
{ messageId: 'restrictedAttributeString', data: { text: '"test"', attribute: 'className' } },
933+
{ messageId: 'restrictedAttributeString', data: { text: '"foo"', attribute: 'id' } },
934+
],
935+
},
936+
{
937+
code: `
938+
<div src="image.jpg" />
939+
`,
940+
options: [{
941+
noAttributeStrings: true,
942+
restrictedAttributes: ['className'],
943+
}],
944+
errors: [{ messageId: 'noStringsInAttributes', data: { text: '"image.jpg"' } }],
945+
},
946+
{
947+
code: `
948+
<div title="text">test</div>
949+
`,
950+
options: [{
951+
restrictedAttributes: ['title'],
952+
noStrings: true,
953+
}],
954+
errors: [
955+
{ messageId: 'restrictedAttributeString', data: { text: '"text"', attribute: 'title' } },
956+
{ messageId: 'noStringsInJSX', data: { text: 'test' } },
957+
],
958+
},
959+
{
960+
code: `
961+
<div className="test" title="hello" />
962+
`,
963+
options: [{ noStrings: true, ignoreProps: false, restrictedAttributes: ['className'] }],
964+
errors: [
965+
{ messageId: 'restrictedAttributeString', data: { text: '"test"', attribute: 'className' } },
966+
{ messageId: 'invalidPropValue', data: { text: 'title="hello"' } },
967+
],
968+
},
969+
{
970+
code: `
971+
<div className="test" title="hello" />
972+
`,
973+
options: [{ noStrings: true, ignoreProps: true, restrictedAttributes: ['className'] }],
974+
errors: [
975+
{ messageId: 'restrictedAttributeString', data: { text: '"test"', attribute: 'className' } },
976+
],
977+
},
848978
{
849979
code: `
850980
<div>
@@ -1169,5 +1299,75 @@ ruleTester.run('jsx-no-literals', rule, {
11691299
options: [{ elementOverrides: { div: { allowElement: true } } }],
11701300
errors: [{ messageId: 'literalNotInJSXExpression', data: { text: 'foo' } }],
11711301
},
1302+
{
1303+
code: `
1304+
<div>
1305+
<div type="text" />
1306+
<Button type="submit" />
1307+
</div>
1308+
`,
1309+
options: [{
1310+
elementOverrides: {
1311+
Button: { restrictedAttributes: ['type'] },
1312+
},
1313+
}],
1314+
errors: [
1315+
{ messageId: 'restrictedAttributeStringInElement', data: { text: '"submit"', attribute: 'type', element: 'Button' } },
1316+
],
1317+
},
1318+
{
1319+
code: `
1320+
<div>
1321+
<Input placeholder="Enter text" type="password" />
1322+
<Button type="submit" disabled="true" />
1323+
</div>
1324+
`,
1325+
options: [{
1326+
elementOverrides: {
1327+
Input: { restrictedAttributes: ['placeholder'] },
1328+
Button: { restrictedAttributes: ['disabled'] },
1329+
},
1330+
}],
1331+
errors: [
1332+
{ messageId: 'restrictedAttributeStringInElement', data: { text: '"Enter text"', attribute: 'placeholder', element: 'Input' } },
1333+
{ messageId: 'restrictedAttributeStringInElement', data: { text: '"true"', attribute: 'disabled', element: 'Button' } },
1334+
],
1335+
},
1336+
{
1337+
code: `
1338+
<div>
1339+
<div className="wrapper" id="main" />
1340+
<Button className="btn" id="submit-btn" />
1341+
</div>
1342+
`,
1343+
options: [{
1344+
restrictedAttributes: ['className'],
1345+
elementOverrides: {
1346+
Button: { restrictedAttributes: ['id'] },
1347+
},
1348+
}],
1349+
errors: [
1350+
{ messageId: 'restrictedAttributeString', data: { text: '"wrapper"', attribute: 'className' } },
1351+
{ messageId: 'restrictedAttributeStringInElement', data: { text: '"submit-btn"', attribute: 'id', element: 'Button' } },
1352+
],
1353+
},
1354+
{
1355+
code: `
1356+
<div>
1357+
<div foo1="bar1" />
1358+
<T foo2="bar2" />
1359+
</div>
1360+
`,
1361+
options: [{
1362+
noAttributeStrings: true,
1363+
elementOverrides: {
1364+
T: { restrictedAttributes: ['foo2'] },
1365+
},
1366+
}],
1367+
errors: [
1368+
{ messageId: 'noStringsInAttributes', data: { text: '"bar1"' } },
1369+
{ messageId: 'restrictedAttributeStringInElement', data: { text: '"bar2"', attribute: 'foo2', element: 'T' } },
1370+
],
1371+
},
11721372
]),
11731373
});

types/rules/jsx-no-literals.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ type RawElementConfig = {
33
allowedStrings?: string[];
44
ignoreProps?: boolean;
55
noAttributeStrings?: boolean;
6+
restrictedAttributes?: string[];
67
};
78

89
type RawOverrideConfig = {
@@ -25,6 +26,7 @@ interface ElementConfigProperties {
2526
allowedStrings: Set<string>;
2627
ignoreProps: boolean;
2728
noAttributeStrings: boolean;
29+
restrictedAttributes: Set<string>;
2830
}
2931

3032
interface OverrideConfigProperties {

0 commit comments

Comments
 (0)