Skip to content

Commit 0e9f1b1

Browse files
authored
Merge pull request #1729 from ThiefMaster/jsx-indent-props-aligned
Add JSX prop alignment based on the first prop & between-prop space consistency
2 parents 05a0607 + c854e1d commit 0e9f1b1

File tree

3 files changed

+196
-43
lines changed

3 files changed

+196
-43
lines changed

docs/rules/jsx-indent-props.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ firstName="John"
2929

3030
## Rule Options
3131

32-
It takes an option as the second parameter which can be `"tab"` for tab-based indentation or a positive number for space indentations.
32+
It takes an option as the second parameter which can be `"tab"` for tab-based indentation, a positive number for space indentations or `"first"` for aligning the first prop for each line with the tag's first prop.
33+
Note that using the `"first"` option allows very inconsistent indentation unless you also enable a rule that enforces the position of the first prop.
3334

3435
```js
3536
...
36-
"react/jsx-indent-props": [<enabled>, 'tab'|<number>]
37+
"react/jsx-indent-props": [<enabled>, 'tab'|<number>|'first']
3738
...
3839
```
3940

@@ -51,6 +52,13 @@ The following patterns are considered warnings:
5152
<Hello
5253
firstName="John"
5354
/>
55+
56+
// aligned with first prop
57+
// [2, 'first']
58+
<Hello
59+
firstName="John"
60+
lastName="Doe"
61+
/>
5462
```
5563

5664
The following patterns are **not** warnings:
@@ -77,6 +85,21 @@ The following patterns are **not** warnings:
7785
<Hello
7886
firstName="John"
7987
/>
88+
89+
// aligned with first prop
90+
// [2, 'first']
91+
<Hello
92+
firstName="John"
93+
lastName="Doe"
94+
/>
95+
96+
<Hello
97+
firstName="John"
98+
lastName="Doe"
99+
/>
100+
101+
<Hello firstName="Jane"
102+
lastName="Doe" />
80103
```
81104

82105
## When not to use

lib/rules/jsx-indent-props.js

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ module.exports = {
4747

4848
schema: [{
4949
oneOf: [{
50-
enum: ['tab']
50+
enum: ['tab', 'first']
5151
}, {
5252
type: 'integer'
5353
}]
@@ -64,7 +64,10 @@ module.exports = {
6464
const sourceCode = context.getSourceCode();
6565

6666
if (context.options.length) {
67-
if (context.options[0] === 'tab') {
67+
if (context.options[0] === 'first') {
68+
indentSize = 'first';
69+
indentType = 'space';
70+
} else if (context.options[0] === 'tab') {
6871
indentSize = 1;
6972
indentType = 'tab';
7073
} else if (typeof context.options[0] === 'number') {
@@ -78,62 +81,41 @@ module.exports = {
7881
* @param {ASTNode} node Node violating the indent rule
7982
* @param {Number} needed Expected indentation character count
8083
* @param {Number} gotten Indentation character count in the actual node/code
81-
* @param {Object=} loc Error line and column location
8284
*/
83-
function report(node, needed, gotten, loc) {
85+
function report(node, needed, gotten) {
8486
const msgContext = {
8587
needed: needed,
8688
type: indentType,
8789
characters: needed === 1 ? 'character' : 'characters',
8890
gotten: gotten
8991
};
9092

91-
if (loc) {
92-
context.report({
93-
node: node,
94-
loc: loc,
95-
message: MESSAGE,
96-
data: msgContext
97-
});
98-
} else {
99-
context.report({
100-
node: node,
101-
message: MESSAGE,
102-
data: msgContext,
103-
fix: function(fixer) {
104-
return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]],
105-
Array(needed + 1).join(indentType === 'space' ? ' ' : '\t'));
106-
}
107-
});
108-
}
93+
context.report({
94+
node: node,
95+
message: MESSAGE,
96+
data: msgContext,
97+
fix: function(fixer) {
98+
return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]],
99+
Array(needed + 1).join(indentType === 'space' ? ' ' : '\t'));
100+
}
101+
});
109102
}
110103

111104
/**
112105
* Get node indent
113106
* @param {ASTNode} node Node to examine
114-
* @param {Boolean} byLastLine get indent of node's last line
115-
* @param {Boolean} excludeCommas skip comma on start of line
116107
* @return {Number} Indent
117108
*/
118-
function getNodeIndent(node, byLastLine, excludeCommas) {
119-
byLastLine = byLastLine || false;
120-
excludeCommas = excludeCommas || false;
121-
109+
function getNodeIndent(node) {
122110
let src = sourceCode.getText(node, node.loc.start.column + extraColumnStart);
123111
const lines = src.split('\n');
124-
if (byLastLine) {
125-
src = lines[lines.length - 1];
126-
} else {
127-
src = lines[0];
128-
}
129-
130-
const skip = excludeCommas ? ',' : '';
112+
src = lines[0];
131113

132114
let regExp;
133115
if (indentType === 'space') {
134-
regExp = new RegExp(`^[ ${skip}]+`);
116+
regExp = /^[ ]+/;
135117
} else {
136-
regExp = new RegExp(`^[\t${skip}]+`);
118+
regExp = /^[\t]+/;
137119
}
138120

139121
const indent = regExp.exec(src);
@@ -146,9 +128,9 @@ module.exports = {
146128
* @param {Number} indent needed indent
147129
* @param {Boolean} excludeCommas skip comma on start of line
148130
*/
149-
function checkNodesIndent(nodes, indent, excludeCommas) {
131+
function checkNodesIndent(nodes, indent) {
150132
nodes.forEach(node => {
151-
const nodeIndent = getNodeIndent(node, false, excludeCommas);
133+
const nodeIndent = getNodeIndent(node);
152134
if (
153135
node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' &&
154136
nodeIndent !== indent && astUtil.isNodeFirstInLine(context, node)
@@ -160,8 +142,18 @@ module.exports = {
160142

161143
return {
162144
JSXOpeningElement: function(node) {
163-
const elementIndent = getNodeIndent(node);
164-
checkNodesIndent(node.attributes, elementIndent + indentSize);
145+
if (!node.attributes.length) {
146+
return;
147+
}
148+
let propIndent;
149+
if (indentSize === 'first') {
150+
const firstPropNode = node.attributes[0];
151+
propIndent = firstPropNode.loc.start.column;
152+
} else {
153+
const elementIndent = getNodeIndent(node);
154+
propIndent = elementIndent + indentSize;
155+
}
156+
checkNodesIndent(node.attributes, propIndent);
165157
}
166158
};
167159
}

tests/lib/rules/jsx-indent-props.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,83 @@ ruleTester.run('jsx-indent-props', rule, {
5959
'/>'
6060
].join('\n'),
6161
options: ['tab']
62+
}, {
63+
code: [
64+
'<App/>'
65+
].join('\n'),
66+
options: ['first']
67+
}, {
68+
code: [
69+
'<App aaa',
70+
' b',
71+
' cc',
72+
'/>'
73+
].join('\n'),
74+
options: ['first']
75+
}, {
76+
code: [
77+
'<App aaa',
78+
' b',
79+
' cc',
80+
'/>'
81+
].join('\n'),
82+
options: ['first']
83+
}, {
84+
code: [
85+
'const test = <App aaa',
86+
' b',
87+
' cc',
88+
' />'
89+
].join('\n'),
90+
options: ['first']
91+
}, {
92+
code: [
93+
'<App aaa x',
94+
' b y',
95+
' cc',
96+
'/>'
97+
].join('\n'),
98+
options: ['first']
99+
}, {
100+
code: [
101+
'const test = <App aaa x',
102+
' b y',
103+
' cc',
104+
' />'
105+
].join('\n'),
106+
options: ['first']
107+
}, {
108+
code: [
109+
'<App aaa',
110+
' b',
111+
'>',
112+
' <Child c',
113+
' d/>',
114+
'</App>'
115+
].join('\n'),
116+
options: ['first']
117+
}, {
118+
code: [
119+
'<Fragment>',
120+
' <App aaa',
121+
' b',
122+
' cc',
123+
' />',
124+
' <OtherApp a',
125+
' bbb',
126+
' c',
127+
' />',
128+
'</Fragment>'
129+
].join('\n'),
130+
options: ['first']
131+
}, {
132+
code: [
133+
'<App',
134+
' a',
135+
' b',
136+
'/>'
137+
].join('\n'),
138+
options: ['first']
62139
}],
63140

64141
invalid: [{
@@ -112,5 +189,66 @@ ruleTester.run('jsx-indent-props', rule, {
112189
].join('\n'),
113190
options: ['tab'],
114191
errors: [{message: 'Expected indentation of 1 tab character but found 3.'}]
192+
}, {
193+
code: [
194+
'<App a',
195+
' b',
196+
'/>'
197+
].join('\n'),
198+
output: [
199+
'<App a',
200+
' b',
201+
'/>'
202+
].join('\n'),
203+
options: ['first'],
204+
errors: [{message: 'Expected indentation of 5 space characters but found 2.'}]
205+
}, {
206+
code: [
207+
'<App a',
208+
' b',
209+
'/>'
210+
].join('\n'),
211+
output: [
212+
'<App a',
213+
' b',
214+
'/>'
215+
].join('\n'),
216+
options: ['first'],
217+
errors: [{message: 'Expected indentation of 6 space characters but found 3.'}]
218+
}, {
219+
code: [
220+
'<App',
221+
' a',
222+
' b',
223+
'/>'
224+
].join('\n'),
225+
output: [
226+
'<App',
227+
' a',
228+
' b',
229+
'/>'
230+
].join('\n'),
231+
options: ['first'],
232+
errors: [{message: 'Expected indentation of 6 space characters but found 3.'}]
233+
}, {
234+
code: [
235+
'<App',
236+
' a',
237+
' b',
238+
' c',
239+
'/>'
240+
].join('\n'),
241+
output: [
242+
'<App',
243+
' a',
244+
' b',
245+
' c',
246+
'/>'
247+
].join('\n'),
248+
options: ['first'],
249+
errors: [
250+
{message: 'Expected indentation of 2 space characters but found 1.'},
251+
{message: 'Expected indentation of 2 space characters but found 3.'}
252+
]
115253
}]
116254
});

0 commit comments

Comments
 (0)