Skip to content

Commit ad9aebe

Browse files
committed
Make jsx-curly-brace-presence bail out in getting rid of JSX expressions when some chars exist
1 parent 1f14fad commit ad9aebe

File tree

3 files changed

+113
-97
lines changed

3 files changed

+113
-97
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ will be warned and fixed to:
122122
<App prop="Hello world">Hello world</App>;
123123
```
124124

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.
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. Also, double quotes will be escaped in the fix.
126126

127127
For example:
128128

@@ -137,12 +137,14 @@ will warned and fixed to:
137137
<App prop={"Hello \"foo\" world"}>{"Hello 'foo' \"bar\" world"}</App>;
138138
```
139139

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:
140+
* If the rule is set to get rid of unnecessary curly braces(JSX expression) and there are characters that need to be escaped in its JSX form, such as quote characters, [forbidden JSX text characters](https://facebook.github.io/jsx/), escaped characters and anything that looks like HTML names, the code will not be warned because the fix may make the code less readable.
141141

142142
The following pattern will not be given a warning even if `'never'` is passed.
143143

144144
```jsx
145+
<Color text={"\u00a0"} />
145146
<App>{"Hello \u00b7 world"}</App>;
147+
<style type="text/css">{'.main { margin-top: 0; }'}</style>;
146148
```
147149

148150
## When Not To Use It

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

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,39 @@ module.exports = {
5959
{props: ruleOptions, children: ruleOptions} :
6060
Object.assign({}, DEFAULT_CONFIG, ruleOptions);
6161

62-
function containsBackslashForEscaping(rawStringValue) {
62+
function containsBackslash(rawStringValue) {
6363
return rawStringValue.includes('\\');
6464
}
6565

66+
function containsHTMLEntity(rawStringValue) {
67+
return rawStringValue.match(/&([A-Za-z\d]+);/g);
68+
}
69+
70+
function containsDisallowedJSXTextChars(rawStringValue) {
71+
return rawStringValue.match(/{|<|>|}/g);
72+
}
73+
74+
function containsQuoteCharacters(value) {
75+
return value.match(/'|"/g);
76+
}
77+
6678
function escapeDoubleQuotes(rawStringValue) {
6779
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
6880
}
6981

82+
function escapeBackslashes(rawStringValue) {
83+
return rawStringValue.replace(/\\/g, '\\\\');
84+
}
85+
86+
function needToEscapeCharacterForJSX(raw, cooked) {
87+
return (
88+
containsBackslash(raw) ||
89+
containsHTMLEntity(raw) ||
90+
containsDisallowedJSXTextChars(raw) ||
91+
containsQuoteCharacters(cooked)
92+
);
93+
}
94+
7095
/**
7196
* Report and fix an unnecessary curly brace violation on a node
7297
* @param {ASTNode} node - The AST node with an unnecessary JSX expression
@@ -83,11 +108,10 @@ module.exports = {
83108

84109
let textToReplace;
85110
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-
)}"`;
111+
textToReplace = `"${expressionType === 'TemplateLiteral' ?
112+
expression.quasis[0].value.raw :
113+
expression.raw.substring(1, expression.raw.length - 1)
114+
}"`;
91115
} else {
92116
textToReplace = expressionType === 'TemplateLiteral' ?
93117
expression.quasis[0].value.cooked : expression.value;
@@ -103,34 +127,42 @@ module.exports = {
103127
node: literalNode,
104128
message: 'Need to wrap this literal in a JSX expression.',
105129
fix: function(fixer) {
130+
// Leave it to the author to fix as it can be fixed by either a
131+
// real character or unicode
132+
if (containsHTMLEntity(literalNode.raw)) {
133+
return null;
134+
}
135+
106136
const expression = literalNode.parent.type === 'JSXAttribute' ?
107-
`{"${escapeDoubleQuotes(
137+
`{"${escapeDoubleQuotes(escapeBackslashes(
108138
literalNode.raw.substring(1, literalNode.raw.length - 1)
109-
)}"}` :
139+
))}"}` :
110140
`{${JSON.stringify(literalNode.value)}}`;
111141

112142
return fixer.replaceText(literalNode, expression);
113143
}
114144
});
115145
}
116146

147+
// Bail out if there is any character that needs to be escaped in JSX
148+
// because escaping decreases readiblity and the original code may be more
149+
// readible anyway or intentional for other specific reasons
117150
function lintUnnecessaryCurly(JSXExpressionNode) {
118151
const expression = JSXExpressionNode.expression;
119152
const expressionType = expression.type;
120-
const parentType = JSXExpressionNode.parent.type;
121153

122154
if (
123155
expressionType === 'Literal' &&
124-
typeof expression.value === 'string' && (
125-
parentType === 'JSXAttribute' ||
126-
!containsBackslashForEscaping(expression.raw))
156+
typeof expression.value === 'string' &&
157+
!needToEscapeCharacterForJSX(expression.raw, expression.value)
127158
) {
128159
reportUnnecessaryCurly(JSXExpressionNode);
129160
} else if (
130161
expressionType === 'TemplateLiteral' &&
131-
expression.expressions.length === 0 && (
132-
parentType === 'JSXAttribute' ||
133-
!containsBackslashForEscaping(expression.quasis[0].value.raw))
162+
expression.expressions.length === 0 &&
163+
!needToEscapeCharacterForJSX(
164+
expression.quasis[0].value.raw, expression.quasis[0].value.cooked
165+
)
134166
) {
135167
reportUnnecessaryCurly(JSXExpressionNode);
136168
}
@@ -170,17 +202,13 @@ module.exports = {
170202

171203
return {
172204
JSXExpressionContainer: node => {
173-
const parent = node.parent;
174-
175-
if (shouldCheckForUnnecessaryCurly(parent, userConfig)) {
205+
if (shouldCheckForUnnecessaryCurly(node.parent, userConfig)) {
176206
lintUnnecessaryCurly(node);
177207
}
178208
},
179209

180210
Literal: node => {
181-
const parentType = node.parent.type;
182-
183-
if (shouldCheckForMissingCurly(parentType, userConfig)) {
211+
if (shouldCheckForMissingCurly(node.parent.type, userConfig)) {
184212
reportMissingCurly(node);
185213
}
186214
}

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

Lines changed: 60 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -169,49 +169,53 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
169169
{
170170
code: '<MyComponent prop={`bar ${word} foo`}>{`foo ${word}`}</MyComponent>',
171171
options: ['never']
172-
}
173-
],
174-
175-
invalid: [
172+
},
176173
{
177-
code: '<App prop={`foo`} />',
178-
output: '<App prop="foo" />',
179-
options: [{props: 'never'}],
180-
errors: [{message: unnecessaryCurlyMessage}]
174+
code: '<MyComponent>{"div { margin-top: 0; }"}</MyComponent>',
175+
options: ['never']
181176
},
182177
{
183-
code: '<App prop={\'foo \\u00b7 bar\'}>foo</App>',
184-
output: '<App prop=\"foo \\u00b7 bar\">foo</App>',
185-
options: [{props: 'never'}],
186-
errors: [{message: unnecessaryCurlyMessage}]
178+
code: '<MyComponent>{"<Foo />"}</MyComponent>',
179+
options: ['never']
187180
},
188181
{
189-
code: '<App prop={`foo \\n bar`}>foo</App>',
190-
output: '<App prop="foo \\n bar">foo</App>',
191-
options: [{props: 'never'}],
192-
errors: [{message: unnecessaryCurlyMessage}]
182+
code: '<MyComponent prop={"{ style: true }"}>bar</MyComponent>',
183+
options: ['never']
193184
},
194185
{
195-
code: '<App prop={`foo \\u00b7 bar`}>foo</App>',
196-
output: '<App prop="foo \\u00b7 bar">foo</App>',
197-
options: [{props: 'never'}],
198-
errors: [{message: unnecessaryCurlyMessage}]
186+
code: '<MyComponent prop={"< style: true >"}>foo</MyComponent>',
187+
options: ['never']
199188
},
200189
{
201-
code: '<App prop={`foo \\\'foo\\\' bar`}>foo</App>',
202-
output: '<App prop="foo \\\'foo\\\' bar">foo</App>',
203-
options: [{props: 'never'}],
204-
errors: [{message: unnecessaryCurlyMessage}]
190+
code: '<MyComponent prop={"Hello \\u1026 world"}>bar</MyComponent>',
191+
options: ['never']
205192
},
206193
{
207-
code: '<App prop={`foo \\\"foo\\\" bar`}>foo</App>',
208-
output: '<App prop="foo \\\"foo\\\" bar">foo</App>',
209-
options: [{props: 'never'}],
210-
errors: [{message: unnecessaryCurlyMessage}]
194+
code: '<MyComponent>{"Hello \\u1026 world"}</MyComponent>',
195+
options: ['never']
211196
},
212197
{
213-
code: '<App prop={`foo "foo" bar`}>foo</App>',
214-
output: '<App prop="foo \\\"foo\\\" bar">foo</App>',
198+
code: '<MyComponent prop={"Hello &middot; world"}>bar</MyComponent>',
199+
options: ['never']
200+
},
201+
{
202+
code: '<MyComponent>{"Hello &middot; world"}</MyComponent>',
203+
options: ['never']
204+
},
205+
{
206+
code: '<MyComponent>{"Hello \\n world"}</MyComponent>',
207+
options: ['never']
208+
},
209+
{
210+
code: ['<a a={"start\\', '\\', 'end"}/>'].join('/n'),
211+
options: ['never']
212+
}
213+
],
214+
215+
invalid: [
216+
{
217+
code: '<App prop={`foo`} />',
218+
output: '<App prop="foo" />',
215219
options: [{props: 'never'}],
216220
errors: [{message: unnecessaryCurlyMessage}]
217221
},
@@ -227,12 +231,6 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
227231
options: [{children: 'never'}],
228232
errors: [{message: unnecessaryCurlyMessage}]
229233
},
230-
{
231-
code: '<App>{`foo "foo" bar`}</App>',
232-
output: '<App>foo "foo" bar</App>',
233-
options: [{children: 'never'}],
234-
errors: [{message: unnecessaryCurlyMessage}]
235-
},
236234
{
237235
code: '<MyComponent>{\'foo\'}</MyComponent>',
238236
output: '<MyComponent>foo</MyComponent>',
@@ -255,18 +253,6 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
255253
options: [{props: 'never'}],
256254
errors: [{message: unnecessaryCurlyMessage}]
257255
},
258-
{
259-
code: '<MyComponent prop={\"bar \'foo\' \"}>foo</MyComponent>',
260-
output: '<MyComponent prop=\"bar \'foo\' \">foo</MyComponent>',
261-
options: [{props: 'never'}],
262-
errors: [{message: unnecessaryCurlyMessage}]
263-
},
264-
{
265-
code: '<MyComponent prop={\'bar \"foo\" \'}>foo</MyComponent>',
266-
output: '<MyComponent prop=\"bar \\\"foo\\\" \">foo</MyComponent>',
267-
options: [{props: 'never'}],
268-
errors: [{message: unnecessaryCurlyMessage}]
269-
},
270256
{
271257
code: '<MyComponent prop=\'bar\'>foo</MyComponent>',
272258
output: '<MyComponent prop={\"bar\"}>foo</MyComponent>',
@@ -316,11 +302,23 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
316302
errors: [{message: missingCurlyMessage}]
317303
},
318304
{
319-
code: '<MyComponent>foo\\nbar</MyComponent>',
320-
output: '<MyComponent>{\"foo\\\\nbar\"}</MyComponent>',
305+
code: '<MyComponent>foo \\n bar</MyComponent>',
306+
output: '<MyComponent>{\"foo \\\\n bar\"}</MyComponent>',
307+
options: [{children: 'always'}],
308+
errors: [{message: missingCurlyMessage}]
309+
},
310+
{
311+
code: '<MyComponent>foo \\u1234 bar</MyComponent>',
312+
output: '<MyComponent>{\"foo \\\\u1234 bar\"}</MyComponent>',
321313
options: [{children: 'always'}],
322314
errors: [{message: missingCurlyMessage}]
323315
},
316+
{
317+
code: '<MyComponent prop=\'foo \\u1234 bar\' />',
318+
output: '<MyComponent prop={"foo \\\\u1234 bar"} />',
319+
options: [{props: 'always'}],
320+
errors: [{message: missingCurlyMessage}]
321+
},
324322
{
325323
code: '<MyComponent prop={\'bar\'}>{\'foo\'}</MyComponent>',
326324
output: '<MyComponent prop=\"bar\">foo</MyComponent>',
@@ -337,30 +335,6 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
337335
{message: missingCurlyMessage}, {message: missingCurlyMessage}
338336
]
339337
},
340-
{
341-
code: '<App>{\'foo "bar"\'}</App>',
342-
output: '<App>foo \"bar\"</App>',
343-
options: [{children: 'never'}],
344-
errors: [{message: unnecessaryCurlyMessage}]
345-
},
346-
{
347-
code: '<App>{\"foo \'bar\'\"}</App>',
348-
output: '<App>foo \'bar\'</App>',
349-
errors: [{message: unnecessaryCurlyMessage}],
350-
options: [{children: 'never'}]
351-
},
352-
{
353-
code: '<App prop={\"foo \'bar\' foo \"}>foo</App>',
354-
output: '<App prop=\"foo \'bar\' foo \">foo</App>',
355-
errors: [{message: unnecessaryCurlyMessage}],
356-
options: [{props: 'never'}]
357-
},
358-
{
359-
code: '<App prop={\'foo "bar"\'}>foo</App>',
360-
output: '<App prop=\"foo \\\"bar\\\"\">foo</App>',
361-
errors: [{message: unnecessaryCurlyMessage}],
362-
options: [{props: 'never'}]
363-
},
364338
{
365339
code: '<App prop={\'foo\'} attr={\" foo \"} />',
366340
output: '<App prop=\"foo\" attr=\" foo \" />',
@@ -388,6 +362,18 @@ ruleTester.run('jsx-curly-brace-presence', rule, {
388362
output: '<App prop={\'foo\'} attr={\"bar\"} />',
389363
errors: [{message: missingCurlyMessage}],
390364
options: [{props: 'always'}]
365+
},
366+
{
367+
code: '<App prop=\'foo &middot; bar\' />',
368+
output: '<App prop=\'foo &middot; bar\' />',
369+
errors: [{message: missingCurlyMessage}],
370+
options: [{props: 'always'}]
371+
},
372+
{
373+
code: '<App>foo &middot; bar</App>',
374+
output: '<App>foo &middot; bar</App>',
375+
errors: [{message: missingCurlyMessage}],
376+
options: [{children: 'always'}]
391377
}
392378
]
393379
});

0 commit comments

Comments
 (0)