Skip to content

Commit 07b348d

Browse files
authored
Merge pull request #1349 from jackyho112/add-new-rule-no-unnecessary-curly-brace-squashed
[new] [jsx-curly-brace-presence] Disallow unnecessary JSX expressions or enforce them
2 parents 99d8ad8 + 990daca commit 07b348d

File tree

5 files changed

+734
-0
lines changed

5 files changed

+734
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
142142
* [react/jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent usage of unwrapped JSX strings
143143
* [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='_blank'`
144144
* [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX
145+
* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
145146
* [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
146147
* [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting (fixable)
147148
* [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable)
+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Enforce curly braces or disallow unnecessary curly braces in JSX props and/or children. (react/jsx-curly-brace-presence)
2+
3+
This rule allows you to enforce curly braces or disallow unnecessary curly braces in JSX props and/or children.
4+
5+
For situations where JSX expressions are unnecessary, please refer to [the React doc](https://facebook.github.io/react/docs/jsx-in-depth.html) and [this page about JSX gotchas](https://github.com/facebook/react/blob/v15.4.0-rc.3/docs/docs/02.3-jsx-gotchas.md#html-entities).
6+
7+
**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line
8+
9+
## Rule Details
10+
11+
By default, this rule will check for and warn about unnecessary curly braces in both JSX props and children.
12+
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.
14+
15+
## Rule Options
16+
17+
```js
18+
...
19+
"react/jsx-curly-brace-presence": [<enabled>, { "props": <string>, "children": <string> }]
20+
...
21+
```
22+
23+
or alternatively
24+
25+
```js
26+
...
27+
"react/jsx-curly-brace-presence": [<enabled>, <string>]
28+
...
29+
```
30+
31+
### Valid options for <string>
32+
33+
They are `always`, `never` and `ignore` for checking on JSX props and children.
34+
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
38+
39+
If passed in the option to fix, this is how a style violation will get fixed
40+
41+
* `always`: wrap a JSX attribute in curly braces/JSX expression and/or a JSX child the same way but also with double quotes
42+
* `never`: get rid of curly braces from a JSX attribute and/or a JSX child
43+
44+
- All fixing operations use double quotes.
45+
46+
For examples:
47+
48+
When `{ props: "always", children: "always" }` is set, the following patterns will be given warnings.
49+
50+
```jsx
51+
<App>Hello world</App>;
52+
<App prop='Hello world'>{'Hello world'}</App>;
53+
```
54+
55+
They can be fixed to:
56+
57+
```jsx
58+
<App>{"Hello world"}</App>;
59+
<App prop={"Hello world"}>{'Hello world'}</App>;
60+
```
61+
62+
When `{ props: "never", children: "never" }` is set, the following patterns will be given warnings.
63+
64+
```jsx
65+
<App>{'Hello world'}</App>;
66+
<App prop={'Hello world'} attr={"foo"} />;
67+
```
68+
69+
They can be fixed to:
70+
71+
```jsx
72+
<App>Hello world</App>;
73+
<App prop="Hello world" attr="foo" />;
74+
```
75+
76+
### Alternative syntax
77+
78+
The options are also `always`, `never` and `ignore` for the same meanings.
79+
80+
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.
81+
82+
For examples:
83+
84+
When `'always'` is set, the following patterns will be given warnings.
85+
86+
```jsx
87+
<App>Hello world</App>;
88+
<App prop='Hello world' attr="foo">Hello world</App>;
89+
```
90+
91+
They can be fixed to:
92+
```jsx
93+
<App>{"Hello world"}</App>;
94+
<App prop={"Hello world"} attr={"foo"}>{"Hello world"}</App>;
95+
```
96+
97+
When `'never'` is set, the following pattern will be given warnings.
98+
99+
```jsx
100+
<App prop={'foo'} attr={"bar"}>{'Hello world'}</App>;
101+
```
102+
103+
It can fixed to:
104+
105+
```jsx
106+
<App prop="foo" attr="bar">Hello world</App>;
107+
```
108+
109+
## Edge cases
110+
111+
The fix also deals with template literals, strings with quotes, and strings with escapes characters.
112+
113+
* If the rule is set to get rid of unnecessary curly braces and the template literal inside a JSX expression has no expression, it will throw a warning and be fixed with double quotes. For example:
114+
115+
```jsx
116+
<App prop={`Hello world`}>{`Hello world`}</App>;
117+
```
118+
119+
will be warned and fixed to:
120+
121+
```jsx
122+
<App prop="Hello world">Hello world</App>;
123+
```
124+
125+
* If the rule is set to enforce curly braces and the strings have quotes, it will be fixed with double quotes for JSX children and the normal way for JSX attributes.
126+
127+
For example:
128+
129+
130+
```jsx
131+
<App prop='Hello "foo" world'>Hello 'foo' "bar" world</App>;
132+
```
133+
134+
will warned and fixed to:
135+
136+
```jsx
137+
<App prop={"Hello \"foo\" world"}>{"Hello 'foo' \"bar\" world"}</App>;
138+
```
139+
140+
* If the rule is set to get rid of unnecessary curly braces and the strings have escaped characters, it will not warn or fix for JSX children because JSX expressions are necessary in this case. For instance:
141+
142+
The following pattern will not be given a warning even if `'never'` is passed.
143+
144+
```jsx
145+
<App>{"Hello \u00b7 world"}</App>;
146+
```
147+
148+
## When Not To Use It
149+
150+
You should turn this rule off if you are not concerned about maintaining consistency regarding the use of curly braces in JSX props and/or children as well as the use of unnecessary JSX expressions.

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const allRules = {
2828
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
2929
'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
3030
'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
31+
'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
3132
'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
3233
'jsx-sort-props': require('./lib/rules/jsx-sort-props'),
3334
'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),

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

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* @fileoverview Enforce curly braces or disallow unnecessary curly brace in JSX
3+
* @author Jacky Ho
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Constants
9+
// ------------------------------------------------------------------------------
10+
11+
const OPTION_ALWAYS = 'always';
12+
const OPTION_NEVER = 'never';
13+
const OPTION_IGNORE = 'ignore';
14+
15+
const OPTION_VALUES = [
16+
OPTION_ALWAYS,
17+
OPTION_NEVER,
18+
OPTION_IGNORE
19+
];
20+
const DEFAULT_CONFIG = {props: OPTION_NEVER, children: OPTION_NEVER};
21+
22+
// ------------------------------------------------------------------------------
23+
// Rule Definition
24+
// ------------------------------------------------------------------------------
25+
26+
module.exports = {
27+
meta: {
28+
docs: {
29+
description:
30+
'Disallow unnecessary JSX expressions when literals alone are sufficient ' +
31+
'or enfore JSX expressions on literals in JSX children or attributes',
32+
category: 'Stylistic Issues',
33+
recommended: false
34+
},
35+
fixable: 'code',
36+
37+
schema: [
38+
{
39+
oneOf: [
40+
{
41+
type: 'object',
42+
properties: {
43+
props: {enum: OPTION_VALUES, default: OPTION_NEVER},
44+
children: {enum: OPTION_VALUES, default: OPTION_NEVER}
45+
},
46+
additionalProperties: false
47+
},
48+
{
49+
enum: OPTION_VALUES
50+
}
51+
]
52+
}
53+
]
54+
},
55+
56+
create: function(context) {
57+
const ruleOptions = context.options[0];
58+
const userConfig = typeof ruleOptions === 'string' ?
59+
{props: ruleOptions, children: ruleOptions} :
60+
Object.assign({}, DEFAULT_CONFIG, ruleOptions);
61+
62+
function containsBackslashForEscaping(rawStringValue) {
63+
return rawStringValue.includes('\\');
64+
}
65+
66+
function escapeDoubleQuotes(rawStringValue) {
67+
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
68+
}
69+
70+
/**
71+
* Report and fix an unnecessary curly brace violation on a node
72+
* @param {ASTNode} node - The AST node with an unnecessary JSX expression
73+
* @param {String} text - The text to replace the unnecessary JSX expression
74+
*/
75+
function reportUnnecessaryCurly(JSXExpressionNode) {
76+
context.report({
77+
node: JSXExpressionNode,
78+
message: 'Curly braces are unnecessary here.',
79+
fix: function(fixer) {
80+
const expression = JSXExpressionNode.expression;
81+
const expressionType = expression.type;
82+
const parentType = JSXExpressionNode.parent.type;
83+
84+
let textToReplace;
85+
if (parentType === 'JSXAttribute') {
86+
textToReplace = `"${escapeDoubleQuotes(
87+
expressionType === 'TemplateLiteral' ?
88+
expression.quasis[0].value.raw :
89+
expression.raw.substring(1, expression.raw.length - 1)
90+
)}"`;
91+
} else {
92+
textToReplace = expressionType === 'TemplateLiteral' ?
93+
expression.quasis[0].value.cooked : expression.value;
94+
}
95+
96+
return fixer.replaceText(JSXExpressionNode, textToReplace);
97+
}
98+
});
99+
}
100+
101+
function reportMissingCurly(literalNode) {
102+
context.report({
103+
node: literalNode,
104+
message: 'Need to wrap this literal in a JSX expression.',
105+
fix: function(fixer) {
106+
const expression = literalNode.parent.type === 'JSXAttribute' ?
107+
`{"${escapeDoubleQuotes(
108+
literalNode.raw.substring(1, literalNode.raw.length - 1)
109+
)}"}` :
110+
`{${JSON.stringify(literalNode.value)}}`;
111+
112+
return fixer.replaceText(literalNode, expression);
113+
}
114+
});
115+
}
116+
117+
function lintUnnecessaryCurly(JSXExpressionNode) {
118+
const expression = JSXExpressionNode.expression;
119+
const expressionType = expression.type;
120+
const parentType = JSXExpressionNode.parent.type;
121+
122+
if (
123+
expressionType === 'Literal' &&
124+
typeof expression.value === 'string' && (
125+
parentType === 'JSXAttribute' ||
126+
!containsBackslashForEscaping(expression.raw))
127+
) {
128+
reportUnnecessaryCurly(JSXExpressionNode);
129+
} else if (
130+
expressionType === 'TemplateLiteral' &&
131+
expression.expressions.length === 0 && (
132+
parentType === 'JSXAttribute' ||
133+
!containsBackslashForEscaping(expression.quasis[0].value.raw))
134+
) {
135+
reportUnnecessaryCurly(JSXExpressionNode);
136+
}
137+
}
138+
139+
function areRuleConditionsSatisfied(parentType, config, ruleCondition) {
140+
return (
141+
parentType === 'JSXAttribute' &&
142+
typeof config.props === 'string' &&
143+
config.props === ruleCondition
144+
) || (
145+
parentType === 'JSXElement' &&
146+
typeof config.children === 'string' &&
147+
config.children === ruleCondition
148+
);
149+
}
150+
151+
function shouldCheckForUnnecessaryCurly(parent, config) {
152+
const parentType = parent.type;
153+
154+
// If there are more than one JSX child, there is no need to check for
155+
// unnecessary curly braces.
156+
if (parentType === 'JSXElement' && parent.children.length !== 1) {
157+
return false;
158+
}
159+
160+
return areRuleConditionsSatisfied(parentType, config, OPTION_NEVER);
161+
}
162+
163+
function shouldCheckForMissingCurly(parentType, config) {
164+
return areRuleConditionsSatisfied(parentType, config, OPTION_ALWAYS);
165+
}
166+
167+
// --------------------------------------------------------------------------
168+
// Public
169+
// --------------------------------------------------------------------------
170+
171+
return {
172+
JSXExpressionContainer: node => {
173+
const parent = node.parent;
174+
175+
if (shouldCheckForUnnecessaryCurly(parent, userConfig)) {
176+
lintUnnecessaryCurly(node);
177+
}
178+
},
179+
180+
Literal: node => {
181+
const parentType = node.parent.type;
182+
183+
if (shouldCheckForMissingCurly(parentType, userConfig)) {
184+
reportMissingCurly(node);
185+
}
186+
}
187+
};
188+
}
189+
};

0 commit comments

Comments
 (0)