Skip to content

Commit 13e649e

Browse files
iiisonshospodarets
authored andcommitted
[New]: jsx-no-literals: add ignoreProps option to ignore props validation
Co-authored-by: Bharat Soni <[email protected]> Co-authored-by: Serg Hospodarets <[email protected]>
1 parent 185d95c commit 13e649e

File tree

3 files changed

+143
-51
lines changed

3 files changed

+143
-51
lines changed

docs/rules/jsx-no-literals.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ var Hello = <div>
2828

2929
There are two options:
3030

31-
* `noStrings` - Enforces no string literals used as children, wrapped or unwrapped.
31+
* `noStrings`(`false` default) - Enforces no string literals used as children, wrapped or unwrapped.
3232
* `allowedStrings` - An array of unique string values that would otherwise warn, but will be ignored.
33+
* `ignoreProps`(`false` default) - When `true` the rule ignores literals used in props, wrapped or unwrapped.
3334

3435
To use, you can specify as follows:
3536

3637
```js
37-
"react/jsx-no-literals": [<enabled>, {"noStrings": true, "allowedStrings": ["allowed"]}]
38+
"react/jsx-no-literals": [<enabled>, {"noStrings": true, "allowedStrings": ["allowed"], "ignoreProps": false}]
3839
```
3940

4041
In this configuration, the following are considered warnings:
@@ -53,6 +54,19 @@ var Hello = <div>
5354
</div>;
5455
```
5556

57+
```jsx
58+
var Hello = <div class='xx' />;
59+
```
60+
61+
```jsx
62+
var Hello = <div class={'xx'} />;
63+
```
64+
65+
```jsx
66+
var Hello = <div class={`xx`} />;
67+
```
68+
69+
5670
The following are **not** considered warnings:
5771

5872
```jsx
@@ -77,6 +91,33 @@ var Hello = <div>
7791
</div>;
7892
```
7993

94+
```jsx
95+
// spread props object
96+
var Hello = <Text {...props} />
97+
```
98+
99+
```jsx
100+
// use variable for prop values
101+
var Hello = <div class={xx} />
102+
```
103+
104+
```jsx
105+
// cache
106+
class Comp1 extends Component {
107+
asdf() {}
108+
109+
render() {
110+
return (
111+
<div onClick={this.asdf}>
112+
{'asdjfl'}
113+
test
114+
{'foo'}
115+
</div>
116+
);
117+
}
118+
}
119+
```
120+
80121
## When Not To Use It
81122

82123
If you do not want to enforce any style JSX literals, then you can disable this rule.

lib/rules/jsx-no-literals.js

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const docsUrl = require('../util/docsUrl');
1212
// Rule Definition
1313
// ------------------------------------------------------------------------------
1414

15+
function trimIfString(val) {
16+
return typeof val === 'string' ? val.trim() : val;
17+
}
18+
1519
module.exports = {
1620
meta: {
1721
docs: {
@@ -33,29 +37,30 @@ module.exports = {
3337
items: {
3438
type: 'string'
3539
}
40+
},
41+
ignoreProps: {
42+
type: 'boolean'
3643
}
3744
},
3845
additionalProperties: false
3946
}]
4047
},
4148

4249
create(context) {
43-
function trimIfString(val) {
44-
return typeof val === 'string' ? val.trim() : val;
45-
}
46-
47-
const defaults = {noStrings: false, allowedStrings: []};
50+
const defaults = {noStrings: false, allowedStrings: [], ignoreProps: false};
4851
const config = Object.assign({}, defaults, context.options[0] || {});
4952
config.allowedStrings = new Set(config.allowedStrings.map(trimIfString));
5053

5154
const message = config.noStrings ?
5255
'Strings not allowed in JSX files' :
5356
'Missing JSX expression container around literal string';
5457

55-
function reportLiteralNode(node) {
58+
function reportLiteralNode(node, customMessage) {
59+
const errorMessage = customMessage || message;
60+
5661
context.report({
5762
node,
58-
message: `${message}: “${context.getSourceCode().getText(node).trim()}”`
63+
message: `${errorMessage}: “${context.getSourceCode().getText(node).trim()}”`
5964
});
6065
}
6166

@@ -82,31 +87,64 @@ module.exports = {
8287
return standard && parent.type !== 'JSXExpressionContainer';
8388
}
8489

90+
function getParentAndGrandParentType(node) {
91+
const parent = getParentIgnoringBinaryExpressions(node);
92+
const parentType = parent.type;
93+
const grandParentType = parent.parent.type;
94+
95+
return {
96+
parent,
97+
parentType,
98+
grandParentType,
99+
grandParent: parent.parent
100+
};
101+
}
102+
103+
function hasJSXElementParentOrGrandParent(node) {
104+
const parents = getParentAndGrandParentType(node);
105+
const parentType = parents.parentType;
106+
const grandParentType = parents.grandParentType;
107+
108+
return parentType === 'JSXFragment' || parentType === 'JSXElement' || grandParentType === 'JSXElement';
109+
}
110+
85111
// --------------------------------------------------------------------------
86112
// Public
87113
// --------------------------------------------------------------------------
88114

89115
return {
90-
91116
Literal(node) {
92-
if (getValidation(node)) {
117+
if (getValidation(node) && (hasJSXElementParentOrGrandParent(node) || !config.ignoreProps)) {
93118
reportLiteralNode(node);
94119
}
95120
},
96121

122+
JSXAttribute(node) {
123+
const isNodeValueString = node.value && node.value && node.value.type === 'Literal' && typeof node.value.value === 'string';
124+
125+
if (config.noStrings && !config.ignoreProps && isNodeValueString) {
126+
const customMessage = 'Invalid prop value';
127+
reportLiteralNode(node, customMessage);
128+
}
129+
},
130+
97131
JSXText(node) {
98132
if (getValidation(node)) {
99133
reportLiteralNode(node);
100134
}
101135
},
102136

103137
TemplateLiteral(node) {
104-
const parent = getParentIgnoringBinaryExpressions(node);
105-
if (config.noStrings && parent.type === 'JSXExpressionContainer') {
138+
const parents = getParentAndGrandParentType(node);
139+
const parentType = parents.parentType;
140+
const grandParentType = parents.grandParentType;
141+
const isParentJSXExpressionCont = parentType === 'JSXExpressionContainer';
142+
const isParentJSXElement = parentType === 'JSXElement' || grandParentType === 'JSXElement';
143+
144+
if (isParentJSXExpressionCont && config.noStrings && (isParentJSXElement || !config.ignoreProps)) {
106145
reportLiteralNode(node);
107146
}
108147
}
109-
110148
};
111149
}
112150
};

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

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ function jsxMessage(str) {
3535
return `Missing JSX expression container around literal string: “${str}”`;
3636
}
3737

38+
function invalidProp(str) {
39+
return `Invalid prop value: “${str}”`;
40+
}
41+
3842
const ruleTester = new RuleTester({parserOptions});
3943
ruleTester.run('jsx-no-literals', rule, {
4044

@@ -140,29 +144,29 @@ ruleTester.run('jsx-no-literals', rule, {
140144
</Foo>
141145
`,
142146
parser: parsers.BABEL_ESLINT,
143-
options: [{noStrings: true}]
147+
options: [{noStrings: true, ignoreProps: true}]
144148
}, {
145149
code: `
146150
<Foo bar="test">
147151
{translate('my.translate.key')}
148152
</Foo>
149153
`,
150154
parser: parsers.BABEL_ESLINT,
151-
options: [{noStrings: true}]
155+
options: [{noStrings: true, ignoreProps: true}]
152156
}, {
153157
code: `
154158
<Foo bar="test">
155159
{intl.formatText(message)}
156160
</Foo>
157161
`,
158-
options: [{noStrings: true}]
162+
options: [{noStrings: true, ignoreProps: true}]
159163
}, {
160164
code: `
161165
<Foo bar="test">
162166
{translate('my.translate.key')}
163167
</Foo>
164168
`,
165-
options: [{noStrings: true}]
169+
options: [{noStrings: true, ignoreProps: true}]
166170
}, {
167171
code: '<Foo bar={true} />',
168172
options: [{noStrings: true}]
@@ -183,11 +187,11 @@ ruleTester.run('jsx-no-literals', rule, {
183187
class Comp1 extends Component {
184188
asdf() {}
185189
render() {
186-
return <Foo bar={this.asdf} />;
190+
return <Foo bar={this.asdf} class='xx' />;
187191
}
188192
}
189193
`,
190-
options: [{noStrings: true}]
194+
options: [{noStrings: true, ignoreProps: true}]
191195
}, {
192196
code: `
193197
class Comp1 extends Component {
@@ -260,6 +264,18 @@ ruleTester.run('jsx-no-literals', rule, {
260264
}
261265
`,
262266
options: [{noStrings: true, allowedStrings: [' foo ']}]
267+
}, {
268+
code: `
269+
class Comp1 extends Component {
270+
asdf() {}
271+
render() {
272+
const xx = 'xx';
273+
274+
return <Foo bar={this.asdf} class={xx} />;
275+
}
276+
} `,
277+
parser: parsers.BABEL_ESLINT,
278+
options: [{noStrings: true, ignoreProps: false}]
263279
}
264280
],
265281

@@ -369,42 +385,33 @@ ruleTester.run('jsx-no-literals', rule, {
369385
{'Test'}
370386
</Foo>
371387
`,
372-
parser: parsers.BABEL_ESLINT,
373-
options: [{noStrings: true}],
374-
errors: [{message: stringsMessage('\'Test\'')}]
375-
}, {
376-
code: `
377-
<Foo bar="test">
378-
{'Test'}
379-
</Foo>
380-
`,
381-
options: [{noStrings: true}],
382-
errors: [{message: stringsMessage('\'Test\'')}]
388+
options: [{noStrings: true, ignoreProps: false}],
389+
errors: [
390+
{message: invalidProp('bar="test"')},
391+
{message: stringsMessage('\'Test\'')}
392+
]
383393
}, {
384394
code: `
385395
<Foo bar="test">
386396
{'Test' + name}
387397
</Foo>
388398
`,
389-
options: [{noStrings: true}],
390-
errors: [{message: stringsMessage('\'Test\'')}]
391-
}, {
392-
code: `
393-
<Foo bar="test">
394-
Test
395-
</Foo>
396-
`,
397-
parser: parsers.BABEL_ESLINT,
398-
options: [{noStrings: true}],
399-
errors: [{message: stringsMessage('Test')}]
399+
options: [{noStrings: true, ignoreProps: false}],
400+
errors: [
401+
{message: invalidProp('bar="test"')},
402+
{message: stringsMessage('\'Test\'')}
403+
]
400404
}, {
401405
code: `
402406
<Foo bar="test">
403407
Test
404408
</Foo>
405409
`,
406-
options: [{noStrings: true}],
407-
errors: [{message: stringsMessage('Test')}]
410+
options: [{noStrings: true, ignoreProps: false}],
411+
errors: [
412+
{message: invalidProp('bar="test"')},
413+
{message: stringsMessage('Test')}
414+
]
408415
}, {
409416
code: `
410417
<Foo>
@@ -415,33 +422,33 @@ ruleTester.run('jsx-no-literals', rule, {
415422
errors: [{message: stringsMessage('`Test`')}]
416423
}, {
417424
code: '<Foo bar={`Test`} />',
418-
options: [{noStrings: true}],
425+
options: [{noStrings: true, ignoreProps: false}],
419426
errors: [{message: stringsMessage('`Test`')}]
420427
}, {
421428
code: '<Foo bar={`${baz}`} />',
422-
options: [{noStrings: true}],
429+
options: [{noStrings: true, ignoreProps: false}],
423430
errors: [{message: stringsMessage('`${baz}`')}]
424431
}, {
425432
code: '<Foo bar={`Test ${baz}`} />',
426-
options: [{noStrings: true}],
433+
options: [{noStrings: true, ignoreProps: false}],
427434
errors: [{message: stringsMessage('`Test ${baz}`')}]
428435
}, {
429436
code: '<Foo bar={`foo` + \'bar\'} />',
430-
options: [{noStrings: true}],
437+
options: [{noStrings: true, ignoreProps: false}],
431438
errors: [
432439
{message: stringsMessage('`foo`')},
433440
{message: stringsMessage('\'bar\'')}
434441
]
435442
}, {
436443
code: '<Foo bar={`foo` + `bar`} />',
437-
options: [{noStrings: true}],
444+
options: [{noStrings: true, ignoreProps: false}],
438445
errors: [
439446
{message: stringsMessage('`foo`')},
440447
{message: stringsMessage('`bar`')}
441448
]
442449
}, {
443450
code: '<Foo bar={\'foo\' + `bar`} />',
444-
options: [{noStrings: true}],
451+
options: [{noStrings: true, ignoreProps: false}],
445452
errors: [
446453
{message: stringsMessage('\'foo\'')},
447454
{message: stringsMessage('`bar`')}
@@ -454,11 +461,17 @@ ruleTester.run('jsx-no-literals', rule, {
454461
}
455462
}
456463
`,
457-
options: [{noStrings: true, allowedStrings: ['asd']}],
464+
options: [{noStrings: true, allowedStrings: ['asd'], ignoreProps: false}],
458465
errors: [
459466
{message: stringsMessage('\'foo\'')},
460467
{message: stringsMessage('asdf')}
461468
]
469+
}, {
470+
code: '<Foo bar={\'bar\'} />',
471+
options: [{noStrings: true, ignoreProps: false}],
472+
errors: [
473+
{message: stringsMessage('\'bar\'')}
474+
]
462475
}
463476
]
464477
});

0 commit comments

Comments
 (0)