Skip to content

Commit 448f86c

Browse files
iiisonljharb
authored andcommitted
[New]: jsx-no-literals: add validateProps option to ignore props validation
1 parent 55b605f commit 448f86c

File tree

3 files changed

+136
-22
lines changed

3 files changed

+136
-22
lines changed

docs/rules/jsx-no-literals.md

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,17 @@ 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+
* `validateProps`(`false` default) - Enforces no 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"], "validateProps": true}]
3839
```
3940

40-
In this configuration, the following are considered warnings:
41+
With `noStrings` set to `true`, the following are considered warnings:
4142

4243
```jsx
4344
var Hello = <div>test</div>;
@@ -65,6 +66,7 @@ var Hello = <div><Text {...message} /></div>
6566
var Hello = <div>{translate('my.translation.key')}</div>
6667
```
6768

69+
6870
```jsx
6971
// an allowed string
7072
var Hello = <div>allowed</div>
@@ -77,6 +79,50 @@ var Hello = <div>
7779
</div>;
7880
```
7981

82+
With `validateProps` set to `true`, the following are considered warnings:
83+
84+
```jsx
85+
var Hello = <div class='xx' />;
86+
```
87+
88+
```jsx
89+
var Hello = <div class={'xx'} />;
90+
```
91+
92+
```jsx
93+
var Hello = <div class={`xx`} />;
94+
```
95+
96+
The following are **not** considered warnings:
97+
98+
```jsx
99+
// spread props object
100+
var Hello = <Text {...props} />
101+
```
102+
103+
```jsx
104+
// use variable for prop values
105+
var Hello = <div class={xx} />
106+
```
107+
108+
```jsx
109+
// cache
110+
class Comp1 extends Component {
111+
asdf() {}
112+
113+
render() {
114+
return (
115+
<div onClick={this.asdf}>
116+
{'asdjfl'}
117+
test
118+
{'foo'}
119+
</div>
120+
);
121+
}
122+
}
123+
```
124+
80125
## When Not To Use It
81126

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

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+
validateProps: {
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: [], validateProps: 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.validateProps)) {
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.validateProps && isNodeValueString) {
126+
const customMessage = 'Invalid attribute 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.validateProps)) {
106145
reportLiteralNode(node);
107146
}
108147
}
109-
110148
};
111149
}
112150
};

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

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ ruleTester.run('jsx-no-literals', rule, {
183183
class Comp1 extends Component {
184184
asdf() {}
185185
render() {
186-
return <Foo bar={this.asdf} />;
186+
return <Foo bar={this.asdf} class='xx' />;
187187
}
188188
}
189189
`,
@@ -260,6 +260,18 @@ ruleTester.run('jsx-no-literals', rule, {
260260
}
261261
`,
262262
options: [{noStrings: true, allowedStrings: [' foo ']}]
263+
}, {
264+
code: `
265+
class Comp1 extends Component {
266+
asdf() {}
267+
render() {
268+
const xx = 'xx';
269+
270+
return <Foo bar={this.asdf} class={xx} />;
271+
}
272+
} `,
273+
parser: parsers.BABEL_ESLINT,
274+
options: [{noStrings: true, validateProps: true}]
263275
}
264276
],
265277

@@ -415,33 +427,33 @@ ruleTester.run('jsx-no-literals', rule, {
415427
errors: [{message: stringsMessage('`Test`')}]
416428
}, {
417429
code: '<Foo bar={`Test`} />',
418-
options: [{noStrings: true}],
430+
options: [{noStrings: true, validateProps: true}],
419431
errors: [{message: stringsMessage('`Test`')}]
420432
}, {
421433
code: '<Foo bar={`${baz}`} />',
422-
options: [{noStrings: true}],
434+
options: [{noStrings: true, validateProps: true}],
423435
errors: [{message: stringsMessage('`${baz}`')}]
424436
}, {
425437
code: '<Foo bar={`Test ${baz}`} />',
426-
options: [{noStrings: true}],
438+
options: [{noStrings: true, validateProps: true}],
427439
errors: [{message: stringsMessage('`Test ${baz}`')}]
428440
}, {
429441
code: '<Foo bar={`foo` + \'bar\'} />',
430-
options: [{noStrings: true}],
442+
options: [{noStrings: true, validateProps: true}],
431443
errors: [
432444
{message: stringsMessage('`foo`')},
433445
{message: stringsMessage('\'bar\'')}
434446
]
435447
}, {
436448
code: '<Foo bar={`foo` + `bar`} />',
437-
options: [{noStrings: true}],
449+
options: [{noStrings: true, validateProps: true}],
438450
errors: [
439451
{message: stringsMessage('`foo`')},
440452
{message: stringsMessage('`bar`')}
441453
]
442454
}, {
443455
code: '<Foo bar={\'foo\' + `bar`} />',
444-
options: [{noStrings: true}],
456+
options: [{noStrings: true, validateProps: true}],
445457
errors: [
446458
{message: stringsMessage('\'foo\'')},
447459
{message: stringsMessage('`bar`')}
@@ -455,10 +467,28 @@ ruleTester.run('jsx-no-literals', rule, {
455467
}
456468
`,
457469
options: [{noStrings: true, allowedStrings: ['asd']}],
470+
errors: [
471+
{message: stringsMessage('asdf')}
472+
]
473+
}, {
474+
code: `
475+
class Comp1 extends Component {
476+
render() {
477+
return <div bar={'foo'}>asdf</div>
478+
}
479+
}
480+
`,
481+
options: [{noStrings: true, allowedStrings: ['asd'], validateProps: true}],
458482
errors: [
459483
{message: stringsMessage('\'foo\'')},
460484
{message: stringsMessage('asdf')}
461485
]
486+
}, {
487+
code: '<Foo bar={\'bar\'} />',
488+
options: [{noStrings: true, validateProps: true}],
489+
errors: [
490+
{message: stringsMessage('\'bar\'')}
491+
]
462492
}
463493
]
464494
});

0 commit comments

Comments
 (0)