Skip to content

Commit 22ff80a

Browse files
committed
New: jsx-curly-newline rule
fixes jsx-eslint#1493
1 parent f0c0b4d commit 22ff80a

File tree

5 files changed

+587
-0
lines changed

5 files changed

+587
-0
lines changed

.vscode/launch.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "launch",
10+
"name": "Launch Program",
11+
"program": "${workspaceFolder}/tests/lib/rules/jsx-curly-newline.js"
12+
}
13+
]
14+
}

docs/rules/jsx-curly-newline.md

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 has a string option:
14+
15+
* `consistent` enforces either both of linebreaks around curlies are present, or none are present.
16+
* `multiline` enforces that when the contained expression spans multiple line, require both linebreaks. When the contained expression is single line, disallow both line breaks.
17+
* `multiline-lax` (default) enforces that when the contained expression spans multiple line, require both linebreaks. When the contained expression is single line, disallow inconsistent line break.
18+
19+
### consistent
20+
21+
When `consistent` is set, the following patterns are considered warnings:
22+
23+
```jsx
24+
<div>
25+
{ foo
26+
}
27+
</div>
28+
29+
<div>
30+
{
31+
foo }
32+
</div>
33+
```
34+
35+
The following patterns are **not** warnings:
36+
37+
```jsx
38+
<div>
39+
{ foo }
40+
</div>
41+
42+
<div>
43+
{
44+
foo
45+
}
46+
</div>
47+
```
48+
49+
### multiline
50+
51+
When `multiline` is set, the following patterns are considered warnings:
52+
53+
```jsx
54+
<div>
55+
{ foo &&
56+
foo.bar }
57+
</div>
58+
59+
<div>
60+
{
61+
foo
62+
}
63+
</div>
64+
65+
<div>
66+
{ foo
67+
}
68+
</div>
69+
```
70+
71+
The following patterns are **not** warnings:
72+
73+
```jsx
74+
<div>
75+
{
76+
foo &&
77+
foo.bar
78+
}
79+
</div>
80+
81+
<div>
82+
{ foo }
83+
</div>
84+
```
85+
86+
### multiline-lax
87+
88+
When `multiline-lax` (default) is set, the following patterns are considered warnings:
89+
90+
```jsx
91+
<div>
92+
{ foo &&
93+
foo.bar }
94+
</div>
95+
96+
<div>
97+
{ foo
98+
}
99+
</div>
100+
```
101+
The following patterns are **not** warnings:
102+
103+
```jsx
104+
<div>
105+
{
106+
foo &&
107+
foo.bar
108+
}
109+
</div>
110+
111+
<div>
112+
{
113+
foo
114+
}
115+
</div>
116+
117+
<div>
118+
{ foo }
119+
</div>
120+
121+
```
122+
123+
## When Not To Use It
124+
125+
You can turn this rule off if you are not concerned with the consistency around the linebreaks inside of JSX attributes or expressions.

index.js

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

lib/rules/jsx-curly-newline.js

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

0 commit comments

Comments
 (0)