Skip to content

Commit ed63c01

Browse files
committed
[New] jsx-curly-brace-presence: add "propElementValues" config option
Fixes #3184
1 parent 541ea43 commit ed63c01

File tree

4 files changed

+103
-28
lines changed

4 files changed

+103
-28
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
88
### Added
99
* add [`hook-use-state`] rule to enforce symmetric useState hook variable names ([#2921][] @duncanbeevers)
1010
* [`jsx-no-target-blank`]: Improve fixer with option `allowReferrer` ([#3167][] @apepper)
11+
* [`jsx-curly-brace-presence`]: add "propElementValues" config option ([#3191][] @ljharb)
1112

1213
### Fixed
1314
* [`prop-types`], `propTypes`: add support for exported type inference ([#3163][] @vedadeepta)
@@ -22,6 +23,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
2223
* [Docs] [`display-name`]: improve examples ([#3189][] @golopot)
2324
* [Refactor] [`no-invalid-html-attribute`]: sort HTML_ELEMENTS and messages ([#3182][] @Primajin)
2425

26+
[#3191]: https://github.com/yannickcr/eslint-plugin-react/pull/3191
2527
[#3190]: https://github.com/yannickcr/eslint-plugin-react/pull/3190
2628
[#3189]: https://github.com/yannickcr/eslint-plugin-react/pull/3189
2729
[#3186]: https://github.com/yannickcr/eslint-plugin-react/pull/3186

docs/rules/jsx-curly-brace-presence.md

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ For situations where JSX expressions are unnecessary, please refer to [the React
88

99
## Rule Details
1010

11-
By default, this rule will check for and warn about unnecessary curly braces in both JSX props and children.
11+
By default, this rule will check for and warn about unnecessary curly braces in both JSX props and children. For the sake of backwards compatibility, prop values that are JSX elements are not considered by default.
1212

13-
You can pass in options to enforce the presence of curly braces on JSX props or children or both. The same options are available for not allowing unnecessary curly braces as well as ignoring the check.
13+
You can pass in options to enforce the presence of curly braces on JSX props, children, JSX prop values that are JSX elements, or any combination of the three. The same options are available for not allowing unnecessary curly braces as well as ignoring the check.
14+
15+
**Note**: it is _highly recommended_ that you configure this rule with an object, and that you set "propElementValues" to "always". The ability to omit curly braces around prop values that are JSX elements is obscure, and intentionally undocumented, and should not be relied upon.
1416

1517
## Rule Options
1618

1719
```js
1820
...
19-
"react/jsx-curly-brace-presence": [<enabled>, { "props": <string>, "children": <string> }]
21+
"react/jsx-curly-brace-presence": [<enabled>, { "props": <string>, "children": <string>, "propElementValues": <string> }]
2022
...
2123
```
2224

@@ -32,9 +34,9 @@ or alternatively
3234

3335
They are `always`, `never` and `ignore` for checking on JSX props and children.
3436

35-
* `always`: always enforce curly braces inside JSX props or/and children
36-
* `never`: never allow unnecessary curly braces inside JSX props or/and children
37-
* `ignore`: ignore the rule for JSX props or/and children
37+
* `always`: always enforce curly braces inside JSX props, children, and/or JSX prop values that are JSX Elements
38+
* `never`: never allow unnecessary curly braces inside JSX props, children, and/or JSX prop values that are JSX Elements
39+
* `ignore`: ignore the rule for JSX props, children, and/or JSX prop values that are JSX Elements
3840

3941
If passed in the option to fix, this is how a style violation will get fixed
4042

@@ -73,9 +75,31 @@ They can be fixed to:
7375
<App prop="Hello world" attr="foo" />;
7476
```
7577

78+
Examples of **incorrect** code for this rule, when configured with `{ props: "always", children: "always", "propElementValues": "always" }`:
79+
```jsx
80+
<App prop=<div /> />;
81+
```
82+
83+
They can be fixed to:
84+
85+
```jsx
86+
<App prop={<div />} />;
87+
```
88+
89+
Examples of **incorrect** code for this rule, when configured with `{ props: "never", children: "never", "propElementValues": "never" }`:
90+
```jsx
91+
<App prop={<div />} />;
92+
```
93+
94+
They can be fixed to:
95+
96+
```jsx
97+
<App prop=<div /> />;
98+
```
99+
76100
### Alternative syntax
77101

78-
The options are also `always`, `never` and `ignore` for the same meanings.
102+
The options are also `always`, `never`, and `ignore` for the same meanings.
79103

80104
In this syntax, only a string is provided and the default will be set to that option for checking on both JSX props and children.
81105

lib/rules/jsx-curly-brace-presence.js

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const OPTION_VALUES = [
2525
OPTION_NEVER,
2626
OPTION_IGNORE,
2727
];
28-
const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER };
28+
const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER, propElementValues: OPTION_IGNORE };
2929

3030
// ------------------------------------------------------------------------------
3131
// Rule Definition
@@ -58,6 +58,7 @@ module.exports = {
5858
properties: {
5959
props: { enum: OPTION_VALUES },
6060
children: { enum: OPTION_VALUES },
61+
propElementValues: { enum: OPTION_VALUES },
6162
},
6263
additionalProperties: false,
6364
},
@@ -73,7 +74,7 @@ module.exports = {
7374
const HTML_ENTITY_REGEX = () => /&[A-Za-z\d#]+;/g;
7475
const ruleOptions = context.options[0];
7576
const userConfig = typeof ruleOptions === 'string'
76-
? { props: ruleOptions, children: ruleOptions }
77+
? { props: ruleOptions, children: ruleOptions, propElementValues: ruleOptions }
7778
: Object.assign({}, DEFAULT_CONFIG, ruleOptions);
7879

7980
function containsLineTerminators(rawStringValue) {
@@ -173,22 +174,28 @@ module.exports = {
173174
node: JSXExpressionNode,
174175
fix(fixer) {
175176
const expression = JSXExpressionNode.expression;
176-
const expressionType = expression.type;
177-
const parentType = JSXExpressionNode.parent.type;
178177

179178
let textToReplace;
180-
if (parentType === 'JSXAttribute') {
181-
textToReplace = `"${expressionType === 'TemplateLiteral'
182-
? expression.quasis[0].value.raw
183-
: expression.raw.substring(1, expression.raw.length - 1)
184-
}"`;
185-
} else if (jsxUtil.isJSX(expression)) {
179+
if (jsxUtil.isJSX(expression)) {
186180
const sourceCode = context.getSourceCode();
187-
188181
textToReplace = sourceCode.getText(expression);
189182
} else {
190-
textToReplace = expressionType === 'TemplateLiteral'
191-
? expression.quasis[0].value.cooked : expression.value;
183+
const expressionType = expression && expression.type;
184+
const parentType = JSXExpressionNode.parent.type;
185+
186+
if (parentType === 'JSXAttribute') {
187+
textToReplace = `"${expressionType === 'TemplateLiteral'
188+
? expression.quasis[0].value.raw
189+
: expression.raw.substring(1, expression.raw.length - 1)
190+
}"`;
191+
} else if (jsxUtil.isJSX(expression)) {
192+
const sourceCode = context.getSourceCode();
193+
194+
textToReplace = sourceCode.getText(expression);
195+
} else {
196+
textToReplace = expressionType === 'TemplateLiteral'
197+
? expression.quasis[0].value.cooked : expression.value;
198+
}
192199
}
193200

194201
return fixer.replaceText(JSXExpressionNode, textToReplace);
@@ -200,6 +207,10 @@ module.exports = {
200207
report(context, messages.missingCurly, 'missingCurly', {
201208
node: literalNode,
202209
fix(fixer) {
210+
if (jsxUtil.isJSX(literalNode)) {
211+
return fixer.replaceText(literalNode, `{${context.getSourceCode().getText(literalNode)}}`);
212+
}
213+
203214
// If a HTML entity name is found, bail out because it can be fixed
204215
// by either using the real character or the unicode equivalent.
205216
// If it contains any line terminator character, bail out as well.
@@ -323,7 +334,8 @@ module.exports = {
323334

324335
return adjSiblings.some((x) => x.type && arrayIncludes(['JSXExpressionContainer', 'JSXElement'], x.type));
325336
}
326-
function shouldCheckForUnnecessaryCurly(parent, node, config) {
337+
function shouldCheckForUnnecessaryCurly(node, config) {
338+
const parent = node.parent;
327339
// Bail out if the parent is a JSXAttribute & its contents aren't
328340
// StringLiteral or TemplateLiteral since e.g
329341
// <App prop1={<CustomEl />} prop2={<CustomEl>...</CustomEl>} />
@@ -358,6 +370,9 @@ module.exports = {
358370
}
359371

360372
function shouldCheckForMissingCurly(node, config) {
373+
if (jsxUtil.isJSX(node)) {
374+
return config.propElementValues !== OPTION_IGNORE;
375+
}
361376
if (
362377
isLineBreak(node.raw)
363378
|| containsOnlyHtmlEntities(node.raw)
@@ -381,13 +396,19 @@ module.exports = {
381396
// --------------------------------------------------------------------------
382397

383398
return {
384-
JSXExpressionContainer: (node) => {
385-
if (shouldCheckForUnnecessaryCurly(node.parent, node, userConfig)) {
399+
'JSXAttribute > JSXExpressionContainer > JSXElement'(node) {
400+
if (userConfig.propElementValues === OPTION_NEVER) {
401+
reportUnnecessaryCurly(node.parent);
402+
}
403+
},
404+
405+
JSXExpressionContainer(node) {
406+
if (shouldCheckForUnnecessaryCurly(node, userConfig)) {
386407
lintUnnecessaryCurly(node);
387408
}
388409
},
389410

390-
'Literal, JSXText': (node) => {
411+
'JSXAttribute > JSXElement, Literal, JSXText'(node) {
391412
if (shouldCheckForMissingCurly(node, userConfig)) {
392413
reportMissingCurly(node);
393414
}

tests/lib/rules/jsx-curly-brace-presence.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,23 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
438438
</App>
439439
`,
440440
},
441-
] : [])
441+
] : []),
442+
{
443+
code: `<App horror=<div /> />`,
444+
features: ['no-ts'],
445+
},
446+
{
447+
code: `<App horror={<div />} />`,
448+
},
449+
{
450+
code: `<App horror=<div /> />`,
451+
options: [{ propElementValues: 'ignore' }],
452+
features: ['no-ts'],
453+
},
454+
{
455+
code: `<App horror={<div />} />`,
456+
options: [{ propElementValues: 'ignore' }],
457+
}
442458
)),
443459

444460
invalid: parsers.all([].concat(
@@ -851,9 +867,7 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
851867
&nbsp;
852868
</App>
853869
`,
854-
errors: [
855-
{ messageId: 'missingCurly' },
856-
],
870+
errors: [{ messageId: 'missingCurly' }],
857871
options: [{ children: 'always' }],
858872
},
859873
{
@@ -889,6 +903,20 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
889903
output: '<MyComponent prop="< style: true >">foo</MyComponent>',
890904
errors: [{ messageId: 'unnecessaryCurly' }],
891905
options: ['never'],
906+
},
907+
{
908+
code: `<App horror=<div /> />`,
909+
output: `<App horror={<div />} />`,
910+
errors: [{ messageId: 'missingCurly' }],
911+
options: [{ props: 'always', children: 'always', propElementValues: 'always' }],
912+
features: ['no-ts'],
913+
},
914+
{
915+
code: `<App horror={<div />} />`,
916+
output: `<App horror=<div /> />`,
917+
errors: [{ messageId: 'unnecessaryCurly' }],
918+
options: [{ props: 'never', children: 'never', propElementValues: 'never' }],
919+
features: ['no-ts'],
892920
}
893921
)),
894922
});

0 commit comments

Comments
 (0)