Skip to content

Commit 44e568a

Browse files
golopotljharb
authored andcommitted
[New] jsx-curly-newline rule
fixes jsx-eslint#1493
1 parent 859def6 commit 44e568a

File tree

5 files changed

+636
-0
lines changed

5 files changed

+636
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2576,3 +2576,4 @@ If you're still not using React 15 you can keep the old behavior by setting the
25762576
[`state-in-constructor`]: docs/rules/state-in-constructor.md
25772577
[`jsx-props-no-spreading`]: docs/rules/jsx-props-no-spreading.md
25782578
[`static-property-placement`]: docs/rules/static-property-placement.md
2579+
[`jsx-curly-newline`]: docs/rules/jsx-curly-newline.md

docs/rules/jsx-curly-newline.md

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Enforce linebreaks in curly braces in JSX attributes and expressions. (react/jsx-curly-newline)
2+
3+
Many style guides require or disallow newlines inside of jsx curly expressions.
4+
5+
**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.
6+
7+
## Rule Details
8+
9+
This rule enforces consistent linebreaks inside of curlies of jsx curly expressions.
10+
11+
## Rule Options
12+
13+
This rule accepts either an object option:
14+
15+
```ts
16+
{
17+
multiline: "consistent" | "forbid" | "require", // default to 'consistent'
18+
singleline: "consistent" | "forbid" | "require", // default to 'consistent'
19+
}
20+
```
21+
Option `multiline` takes effect when the jsx expression inside the curlies occupies multiple lines.
22+
23+
Option `singleline` takes effect when the jsx expression inside the curlies occupies a single line.
24+
25+
* `consistent` enforces either both curly braces have a line break directly inside them, or no line breaks are present.
26+
* `forbid` disallows linebreaks directly inside curly braces.
27+
* `require` enforces the presence of linebreaks directly inside curlies.
28+
29+
or a string option:
30+
31+
* `consistent` (default) is an alias for `{ multiline: "consistent", singleline: "consistent" }`.
32+
* `never` is an alias for `{ multiline: "forbid", singleline: "forbid" }`
33+
34+
or an
35+
36+
### consistent (default)
37+
38+
When `consistent` or `{ multiline: "consistent", singleline: "consistent" }` is set, the following patterns are considered warnings:
39+
40+
```jsx
41+
<div>
42+
{ foo
43+
}
44+
</div>
45+
46+
<div>
47+
{
48+
foo }
49+
</div>
50+
51+
<div>
52+
{ foo &&
53+
foo.bar
54+
}
55+
</div>
56+
```
57+
58+
The following patterns are **not** warnings:
59+
60+
```jsx
61+
<div>
62+
{ foo }
63+
</div>
64+
65+
<div>
66+
{
67+
foo
68+
}
69+
</div>
70+
```
71+
72+
### never
73+
74+
When `never` or `{ multiline: "forbid", singleline: "forbid" }` is set, the following patterns are considered warnings:
75+
76+
```jsx
77+
<div>
78+
{
79+
foo &&
80+
foo.bar
81+
}
82+
</div>
83+
84+
<div>
85+
{
86+
foo
87+
}
88+
</div>
89+
90+
<div>
91+
{ foo
92+
}
93+
</div>
94+
```
95+
96+
The following patterns are **not** warnings:
97+
98+
```jsx
99+
<div>
100+
{ foo &&
101+
foo.bar }
102+
</div>
103+
104+
<div>
105+
{ foo }
106+
</div>
107+
```
108+
109+
## require
110+
111+
When `{ multiline: "require", singleline: "require" }` is set, the following patterns are considered warnings:
112+
113+
```jsx
114+
<div>
115+
{ foo &&
116+
foo.bar }
117+
</div>
118+
119+
<div>
120+
{ foo }
121+
</div>
122+
123+
<div>
124+
{ foo
125+
}
126+
</div>
127+
```
128+
129+
The following patterns are **not** warnings:
130+
131+
```jsx
132+
<div>
133+
{
134+
foo &&
135+
foo.bar
136+
}
137+
</div>
138+
139+
<div>
140+
{
141+
foo
142+
}
143+
</div>
144+
```
145+
146+
147+
## When Not To Use It
148+
149+
You can turn this rule off if you are not concerned with the consistency of padding linebreaks inside of JSX attributes or expressions.

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const allRules = {
1919
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
2020
'jsx-closing-tag-location': require('./lib/rules/jsx-closing-tag-location'),
2121
'jsx-curly-spacing': require('./lib/rules/jsx-curly-spacing'),
22+
'jsx-curly-newline': require('./lib/rules/jsx-curly-newline'),
2223
'jsx-equals-spacing': require('./lib/rules/jsx-equals-spacing'),
2324
'jsx-filename-extension': require('./lib/rules/jsx-filename-extension'),
2425
'jsx-first-prop-new-line': require('./lib/rules/jsx-first-prop-new-line'),

lib/rules/jsx-curly-newline.js

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* @fileoverview enforce consistent line breaks inside jsx curly
3+
*/
4+
5+
'use strict';
6+
7+
const docsUrl = require('../util/docsUrl');
8+
9+
// ------------------------------------------------------------------------------
10+
// Rule Definition
11+
// ------------------------------------------------------------------------------
12+
13+
function getNormalizedOption(context) {
14+
const rawOption = context.options[0] || 'consistent';
15+
16+
if (rawOption === 'consistent') {
17+
return {
18+
multiline: 'consistent',
19+
singleline: 'consistent'
20+
};
21+
}
22+
23+
if (rawOption === 'never') {
24+
return {
25+
multiline: 'forbid',
26+
singleline: 'forbid'
27+
};
28+
}
29+
30+
return {
31+
multiline: rawOption.multiline || 'consistent',
32+
singleline: rawOption.singleline || 'consistent'
33+
};
34+
}
35+
36+
module.exports = {
37+
meta: {
38+
type: 'layout',
39+
40+
docs: {
41+
description: 'enforce consistent line breaks inside jsx curly',
42+
category: 'Stylistic Issues',
43+
recommended: false,
44+
url: docsUrl('jsx-curly-newline')
45+
},
46+
47+
fixable: 'whitespace',
48+
49+
schema: [
50+
{
51+
oneOf: [
52+
{
53+
enum: ['consistent', 'never']
54+
},
55+
{
56+
type: 'object',
57+
properties: {
58+
singleline: {enum: ['consistent', 'require', 'forbid']},
59+
multiline: {enum: ['consistent', 'require', 'forbid']}
60+
},
61+
additionalProperties: false
62+
}
63+
]
64+
}
65+
],
66+
67+
68+
messages: {
69+
expectedBefore: 'Expected newline before \'}\'.',
70+
expectedAfter: 'Expected newline after \'{\'.',
71+
unexpectedBefore: 'Unexpected newline before \'{\'.',
72+
unexpectedAfter: 'Unexpected newline after \'}\'.'
73+
}
74+
},
75+
76+
create(context) {
77+
const sourceCode = context.getSourceCode();
78+
const option = getNormalizedOption(context);
79+
80+
// ----------------------------------------------------------------------
81+
// Helpers
82+
// ----------------------------------------------------------------------
83+
84+
/**
85+
* Determines whether two adjacent tokens are on the same line.
86+
* @param {Object} left - The left token object.
87+
* @param {Object} right - The right token object.
88+
* @returns {boolean} Whether or not the tokens are on the same line.
89+
*/
90+
function isTokenOnSameLine(left, right) {
91+
return left.loc.end.line === right.loc.start.line;
92+
}
93+
94+
/**
95+
* Determines whether there should be newlines inside curlys
96+
* @param {ASTNode} expression The expression contained in the curlys
97+
* @param {boolean} hasLeftNewline `true` if the left curly has a newline in the current code.
98+
* @returns {boolean} `true` if there should be newlines inside the function curlys
99+
*/
100+
function shouldHaveNewlines(expression, hasLeftNewline) {
101+
const isMultiline = expression.loc.start.line !== expression.loc.end.line;
102+
103+
switch (isMultiline ? option.multiline : option.singleline) {
104+
case 'forbid': return false;
105+
case 'require': return true;
106+
case 'consistent':
107+
default: return hasLeftNewline;
108+
}
109+
}
110+
111+
/**
112+
* Validates curlys
113+
* @param {Object} curlys An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
114+
* @param {ASTNode} expression The expression inside the curly
115+
* @returns {void}
116+
*/
117+
function validateCurlys(curlys, expression) {
118+
const leftCurly = curlys.leftCurly;
119+
const rightCurly = curlys.rightCurly;
120+
const tokenAfterLeftCurly = sourceCode.getTokenAfter(leftCurly);
121+
const tokenBeforeRightCurly = sourceCode.getTokenBefore(rightCurly);
122+
const hasLeftNewline = !isTokenOnSameLine(leftCurly, tokenAfterLeftCurly);
123+
const hasRightNewline = !isTokenOnSameLine(tokenBeforeRightCurly, rightCurly);
124+
const needsNewlines = shouldHaveNewlines(expression, hasLeftNewline);
125+
126+
if (hasLeftNewline && !needsNewlines) {
127+
context.report({
128+
node: leftCurly,
129+
messageId: 'unexpectedAfter',
130+
fix(fixer) {
131+
return sourceCode
132+
.getText()
133+
.slice(leftCurly.range[1], tokenAfterLeftCurly.range[0])
134+
.trim() ?
135+
null : // If there is a comment between the { and the first element, don't do a fix.
136+
fixer.removeRange([leftCurly.range[1], tokenAfterLeftCurly.range[0]]);
137+
}
138+
});
139+
} else if (!hasLeftNewline && needsNewlines) {
140+
context.report({
141+
node: leftCurly,
142+
messageId: 'expectedAfter',
143+
fix: fixer => fixer.insertTextAfter(leftCurly, '\n')
144+
});
145+
}
146+
147+
if (hasRightNewline && !needsNewlines) {
148+
context.report({
149+
node: rightCurly,
150+
messageId: 'unexpectedBefore',
151+
fix(fixer) {
152+
return sourceCode
153+
.getText()
154+
.slice(tokenBeforeRightCurly.range[1], rightCurly.range[0])
155+
.trim() ?
156+
null : // If there is a comment between the last element and the }, don't do a fix.
157+
fixer.removeRange([
158+
tokenBeforeRightCurly.range[1],
159+
rightCurly.range[0]
160+
]);
161+
}
162+
});
163+
} else if (!hasRightNewline && needsNewlines) {
164+
context.report({
165+
node: rightCurly,
166+
messageId: 'expectedBefore',
167+
fix: fixer => fixer.insertTextBefore(rightCurly, '\n')
168+
});
169+
}
170+
}
171+
172+
173+
// ----------------------------------------------------------------------
174+
// Public
175+
// ----------------------------------------------------------------------
176+
177+
return {
178+
JSXExpressionContainer(node) {
179+
const curlyTokens = {
180+
leftCurly: sourceCode.getFirstToken(node),
181+
rightCurly: sourceCode.getLastToken(node)
182+
};
183+
validateCurlys(curlyTokens, node.expression);
184+
}
185+
};
186+
}
187+
};

0 commit comments

Comments
 (0)