Skip to content

Commit eeed16c

Browse files
author
ushiboy
committed
feat: jsx-no-literals add restrictedAttributes option
1 parent f2869fd commit eeed16c

File tree

4 files changed

+199
-0
lines changed

4 files changed

+199
-0
lines changed

docs/rules/jsx-no-literals.md

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

3940
### `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+
return; // Skip reporting this attribute if it's not in the restricted list
500+
}
501+
502+
const messageId = resolvedConfig.type === 'override' ? 'restrictedAttributeStringInElement' : 'restrictedAttributeString';
503+
report(context, messages[messageId], messageId, {
504+
node,
505+
data: {
506+
text: node.value.value,
507+
attribute: attributeName,
508+
element: resolvedConfig.type === 'override' && 'name' in resolvedConfig ? resolvedConfig.name : undefined,
509+
},
510+
});
511+
return;
512+
}
481513

482514
if (
483515
resolvedConfig.noStrings

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

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,18 @@ 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="test" id="foo" />
308+
`,
309+
options: [{ restrictedAttributes: [] }],
310+
},
299311
{
300312
code: `
301313
<T>foo</T>
@@ -476,6 +488,45 @@ ruleTester.run('jsx-no-literals', rule, {
476488
`,
477489
options: [{ elementOverrides: { div: { allowElement: true } } }],
478490
},
491+
{
492+
code: `
493+
<div>
494+
<Input type="text" />
495+
<Button className="primary" />
496+
<Image src="photo.jpg" />
497+
</div>
498+
`,
499+
options: [{
500+
elementOverrides: {
501+
Input: { restrictedAttributes: ['placeholder'] },
502+
Button: { restrictedAttributes: ['type'] },
503+
},
504+
}],
505+
},
506+
{
507+
code: `
508+
<div title="container">
509+
<Button className="btn" />
510+
</div>
511+
`,
512+
options: [{
513+
restrictedAttributes: ['className'],
514+
elementOverrides: {
515+
Button: { restrictedAttributes: ['disabled'] },
516+
},
517+
}],
518+
},
519+
{
520+
code: `
521+
<Button className="btn" />
522+
`,
523+
options: [{
524+
noAttributeStrings: true,
525+
elementOverrides: {
526+
Button: { restrictedAttributes: ['type'] },
527+
},
528+
}],
529+
},
479530
]),
480531

481532
invalid: parsers.all([
@@ -845,6 +896,49 @@ ruleTester.run('jsx-no-literals', rule, {
845896
},
846897
],
847898
},
899+
{
900+
code: `
901+
<div className="test" />
902+
`,
903+
options: [{ restrictedAttributes: ['className'] }],
904+
errors: [{
905+
messageId: 'restrictedAttributeString',
906+
data: { text: 'test', attribute: 'className' },
907+
}],
908+
},
909+
{
910+
code: `
911+
<div className="test" id="foo" title="bar" />
912+
`,
913+
options: [{ restrictedAttributes: ['className', 'id'] }],
914+
errors: [
915+
{ messageId: 'restrictedAttributeString', data: { text: 'test', attribute: 'className' } },
916+
{ messageId: 'restrictedAttributeString', data: { text: 'foo', attribute: 'id' } },
917+
],
918+
},
919+
{
920+
code: `
921+
<div src="image.jpg" />
922+
`,
923+
options: [{
924+
noAttributeStrings: true,
925+
restrictedAttributes: ['className'],
926+
}],
927+
errors: [{ messageId: 'noStringsInAttributes', data: { text: '"image.jpg"' } }],
928+
},
929+
{
930+
code: `
931+
<div title="text">test</div>
932+
`,
933+
options: [{
934+
restrictedAttributes: ['title'],
935+
noStrings: true,
936+
}],
937+
errors: [
938+
{ messageId: 'restrictedAttributeString', data: { text: 'text', attribute: 'title' } },
939+
{ messageId: 'noStringsInJSX', data: { text: 'test' } },
940+
],
941+
},
848942
{
849943
code: `
850944
<div>
@@ -1169,5 +1263,75 @@ ruleTester.run('jsx-no-literals', rule, {
11691263
options: [{ elementOverrides: { div: { allowElement: true } } }],
11701264
errors: [{ messageId: 'literalNotInJSXExpression', data: { text: 'foo' } }],
11711265
},
1266+
{
1267+
code: `
1268+
<div>
1269+
<div type="text" />
1270+
<Button type="submit" />
1271+
</div>
1272+
`,
1273+
options: [{
1274+
elementOverrides: {
1275+
Button: { restrictedAttributes: ['type'] },
1276+
},
1277+
}],
1278+
errors: [
1279+
{ messageId: 'restrictedAttributeStringInElement', data: { text: 'submit', attribute: 'type', element: 'Button' } },
1280+
],
1281+
},
1282+
{
1283+
code: `
1284+
<div>
1285+
<Input placeholder="Enter text" type="password" />
1286+
<Button type="submit" disabled="true" />
1287+
</div>
1288+
`,
1289+
options: [{
1290+
elementOverrides: {
1291+
Input: { restrictedAttributes: ['placeholder'] },
1292+
Button: { restrictedAttributes: ['disabled'] },
1293+
},
1294+
}],
1295+
errors: [
1296+
{ messageId: 'restrictedAttributeStringInElement', data: { text: 'Enter text', attribute: 'placeholder', element: 'Input' } },
1297+
{ messageId: 'restrictedAttributeStringInElement', data: { text: 'true', attribute: 'disabled', element: 'Button' } },
1298+
],
1299+
},
1300+
{
1301+
code: `
1302+
<div>
1303+
<div className="wrapper" id="main" />
1304+
<Button className="btn" id="submit-btn" />
1305+
</div>
1306+
`,
1307+
options: [{
1308+
restrictedAttributes: ['className'],
1309+
elementOverrides: {
1310+
Button: { restrictedAttributes: ['id'] },
1311+
},
1312+
}],
1313+
errors: [
1314+
{ messageId: 'restrictedAttributeString', data: { text: 'wrapper', attribute: 'className' } },
1315+
{ messageId: 'restrictedAttributeStringInElement', data: { text: 'submit-btn', attribute: 'id', element: 'Button' } },
1316+
],
1317+
},
1318+
{
1319+
code: `
1320+
<div>
1321+
<div foo1="bar1" />
1322+
<T foo2="bar2" />
1323+
</div>
1324+
`,
1325+
options: [{
1326+
noAttributeStrings: true,
1327+
elementOverrides: {
1328+
T: { restrictedAttributes: ['foo2'] },
1329+
},
1330+
}],
1331+
errors: [
1332+
{ messageId: 'noStringsInAttributes', data: { text: '"bar1"' } },
1333+
{ messageId: 'restrictedAttributeStringInElement', data: { text: 'bar2', attribute: 'foo2', element: 'T' } },
1334+
],
1335+
},
11721336
]),
11731337
});

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)