Skip to content

Commit 7119df9

Browse files
committed
Add jsx-indent-props rule (fixes jsx-eslint#15, fixes jsx-eslint#181)
1 parent 3628b03 commit 7119df9

File tree

5 files changed

+329
-2
lines changed

5 files changed

+329
-2
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Finally, enable all of the rules that you would like to use.
4646
"react/jsx-boolean-value": 1,
4747
"react/jsx-curly-spacing": 1,
4848
"react/jsx-max-props-per-line": 1,
49+
"react/jsx-indent-props": 1,
4950
"react/jsx-no-duplicate-props": 1,
5051
"react/jsx-no-undef": 1,
5152
"react/jsx-quotes": 1,
@@ -74,6 +75,7 @@ Finally, enable all of the rules that you would like to use.
7475
* [jsx-boolean-value](docs/rules/jsx-boolean-value.md): Enforce boolean attributes notation in JSX
7576
* [jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes
7677
* [jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md): Limit maximum of props on a single line in JSX
78+
* [jsx-indent-props](docs/rules/jsx-indent-props.md): Validate props indentation in JSX
7779
* [jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Prevent duplicate props in JSX
7880
* [jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent usage of unwrapped JSX strings
7981
* [jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX

docs/rules/jsx-indent-props.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Validate props indentation in JSX (jsx-indent-props)
2+
3+
This option validates a specific indentation style for props.
4+
5+
## Rule Details
6+
7+
This rule is aimed to enforce consistent indentation style. The default style is `4 spaces`.
8+
9+
The following patterns are considered warnings:
10+
11+
```jsx
12+
// 2 spaces indentation
13+
<Hello
14+
firstName="John"
15+
/>
16+
17+
// no indentation
18+
<Hello
19+
firstName="John"
20+
/>
21+
22+
// 1 tab indentation
23+
<Hello
24+
firstName="John"
25+
/>
26+
```
27+
28+
## Rule Options
29+
30+
It takes an option as the second parameter which can be `"tab"` for tab-based indentation or a positive number for space indentations.
31+
32+
```js
33+
...
34+
"jsx-indent-props": [<enabled>, 'tab'|<number>]
35+
...
36+
```
37+
38+
The following patterns are considered warnings:
39+
40+
```jsx
41+
// 2 spaces indentation
42+
// [2, 2]
43+
<Hello
44+
firstName="John"
45+
/>
46+
47+
// tab indentation
48+
// [2, 'tab']
49+
<Hello
50+
firstName="John"
51+
/>
52+
```
53+
54+
The following patterns are not warnings:
55+
56+
```jsx
57+
58+
// 2 spaces indentation
59+
// [2, 2]
60+
<Hello
61+
firstName="John"
62+
/>
63+
64+
<Hello
65+
firstName="John" />
66+
67+
// tab indentation
68+
// [2, 'tab']
69+
<Hello
70+
firstName="John"
71+
/>
72+
73+
// no indentation
74+
// [2, 0]
75+
<Hello
76+
firstName="John"
77+
/>
78+
```
79+
80+
## When not to use
81+
82+
If you are not using JSX then you can disable this rule.

index.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ module.exports = {
2424
'require-extension': require('./lib/rules/require-extension'),
2525
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
2626
'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
27-
'jsx-no-literals': require('./lib/rules/jsx-no-literals')
27+
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
28+
'jsx-indent-props': require('./lib/rules/jsx-indent-props')
2829
},
2930
rulesConfig: {
3031
'jsx-uses-react': 0,
@@ -49,6 +50,7 @@ module.exports = {
4950
'require-extension': 0,
5051
'jsx-no-duplicate-props': 0,
5152
'jsx-max-props-per-line': 0,
52-
'jsx-no-literals': 0
53+
'jsx-no-literals': 0,
54+
'jsx-indent-props': 0
5355
}
5456
};

lib/rules/jsx-indent-props.js

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* @fileoverview Validate props indentation in JSX
3+
* @author Yannick Croissant
4+
5+
* This rule has been ported and modified from eslint and nodeca.
6+
* @author Vitaly Puzrin
7+
* @author Gyandeep Singh
8+
* @copyright 2015 Vitaly Puzrin. All rights reserved.
9+
* @copyright 2015 Gyandeep Singh. All rights reserved.
10+
Copyright (C) 2014 by Vitaly Puzrin
11+
12+
Permission is hereby granted, free of charge, to any person obtaining a copy
13+
of this software and associated documentation files (the 'Software'), to deal
14+
in the Software without restriction, including without limitation the rights
15+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16+
copies of the Software, and to permit persons to whom the Software is
17+
furnished to do so, subject to the following conditions:
18+
19+
The above copyright notice and this permission notice shall be included in
20+
all copies or substantial portions of the Software.
21+
22+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28+
THE SOFTWARE.
29+
*/
30+
'use strict';
31+
32+
// ------------------------------------------------------------------------------
33+
// Rule Definition
34+
// ------------------------------------------------------------------------------
35+
module.exports = function(context) {
36+
37+
var MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.';
38+
39+
var extraColumnStart = 0;
40+
var indentType = 'space';
41+
var indentSize = 4;
42+
43+
if (context.options.length) {
44+
if (context.options[0] === 'tab') {
45+
indentSize = 1;
46+
indentType = 'tab';
47+
} else if (typeof context.options[0] === 'number') {
48+
indentSize = context.options[0];
49+
indentType = 'space';
50+
}
51+
}
52+
53+
/**
54+
* Reports a given indent violation and properly pluralizes the message
55+
* @param {ASTNode} node Node violating the indent rule
56+
* @param {Number} needed Expected indentation character count
57+
* @param {Number} gotten Indentation character count in the actual node/code
58+
* @param {Object=} loc Error line and column location
59+
*/
60+
function report(node, needed, gotten, loc) {
61+
var msgContext = {
62+
needed: needed,
63+
type: indentType,
64+
characters: needed === 1 ? 'character' : 'characters',
65+
gotten: gotten
66+
};
67+
68+
if (loc) {
69+
context.report(node, loc, MESSAGE, msgContext);
70+
} else {
71+
context.report(node, MESSAGE, msgContext);
72+
}
73+
}
74+
75+
/**
76+
* Get node indent
77+
* @param {ASTNode} node Node to examine
78+
* @param {Boolean} byLastLine get indent of node's last line
79+
* @param {Boolean} excludeCommas skip comma on start of line
80+
* @return {Number} Indent
81+
*/
82+
function getNodeIndent(node, byLastLine, excludeCommas) {
83+
byLastLine = byLastLine || false;
84+
excludeCommas = excludeCommas || false;
85+
86+
var src = context.getSource(node, node.loc.start.column + extraColumnStart);
87+
var lines = src.split('\n');
88+
if (byLastLine) {
89+
src = lines[lines.length - 1];
90+
} else {
91+
src = lines[0];
92+
}
93+
94+
var skip = excludeCommas ? ',' : '';
95+
96+
var regExp;
97+
if (indentType === 'space') {
98+
regExp = new RegExp('^[ ' + skip + ']+');
99+
} else {
100+
regExp = new RegExp('^[\t' + skip + ']+');
101+
}
102+
103+
var indent = regExp.exec(src);
104+
return indent ? indent[0].length : 0;
105+
}
106+
107+
/**
108+
* Checks node is the first in its own start line. By default it looks by start line.
109+
* @param {ASTNode} node The node to check
110+
* @param {Boolean} [byEndLocation] Lookup based on start position or end
111+
* @return {Boolean} true if its the first in the its start line
112+
*/
113+
function isNodeFirstInLine(node, byEndLocation) {
114+
var firstToken = byEndLocation === true ? context.getLastToken(node, 1) : context.getTokenBefore(node);
115+
var startLine = byEndLocation === true ? node.loc.end.line : node.loc.start.line;
116+
var endLine = firstToken ? firstToken.loc.end.line : -1;
117+
118+
return startLine !== endLine;
119+
}
120+
121+
/**
122+
* Check indent for nodes list
123+
* @param {ASTNode[]} nodes list of node objects
124+
* @param {Number} indent needed indent
125+
* @param {Boolean} excludeCommas skip comma on start of line
126+
*/
127+
function checkNodesIndent(nodes, indent, excludeCommas) {
128+
nodes.forEach(function(node) {
129+
var nodeIndent = getNodeIndent(node, false, excludeCommas);
130+
if (
131+
node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' &&
132+
nodeIndent !== indent && isNodeFirstInLine(node)
133+
) {
134+
report(node, indent, nodeIndent);
135+
}
136+
});
137+
}
138+
139+
return {
140+
JSXOpeningElement: function(node) {
141+
var elementIndent = getNodeIndent(node);
142+
checkNodesIndent(node.attributes, elementIndent + indentSize);
143+
}
144+
};
145+
146+
};
147+
148+
module.exports.schema = [{
149+
oneOf: [{
150+
enum: ['tab']
151+
}, {
152+
type: 'integer'
153+
}]
154+
}];

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

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @fileoverview Validate props indentation in JSX
3+
* @author Yannick Croissant
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
var rule = require('../../../lib/rules/jsx-indent-props');
12+
var RuleTester = require('eslint').RuleTester;
13+
14+
// ------------------------------------------------------------------------------
15+
// Tests
16+
// ------------------------------------------------------------------------------
17+
18+
var ruleTester = new RuleTester();
19+
ruleTester.run('jsx-indent-props', rule, {
20+
valid: [{
21+
code: [
22+
'<App foo',
23+
'/>'
24+
].join('\n'),
25+
ecmaFeatures: {jsx: true}
26+
}, {
27+
code: [
28+
'<App',
29+
' foo',
30+
'/>'
31+
].join('\n'),
32+
options: [2],
33+
ecmaFeatures: {jsx: true}
34+
}, {
35+
code: [
36+
'<App',
37+
'foo',
38+
'/>'
39+
].join('\n'),
40+
options: [0],
41+
ecmaFeatures: {jsx: true}
42+
}, {
43+
code: [
44+
' <App',
45+
'foo',
46+
' />'
47+
].join('\n'),
48+
options: [-2],
49+
ecmaFeatures: {jsx: true}
50+
}, {
51+
code: [
52+
'<App',
53+
'\tfoo',
54+
'/>'
55+
].join('\n'),
56+
options: ['tab'],
57+
ecmaFeatures: {jsx: true}
58+
}],
59+
60+
invalid: [{
61+
code: [
62+
'<App',
63+
' foo',
64+
'/>'
65+
].join('\n'),
66+
ecmaFeatures: {jsx: true},
67+
errors: [{message: 'Expected indentation of 4 space characters but found 2.'}]
68+
}, {
69+
code: [
70+
'<App',
71+
' foo',
72+
'/>'
73+
].join('\n'),
74+
options: [2],
75+
ecmaFeatures: {jsx: true},
76+
errors: [{message: 'Expected indentation of 2 space characters but found 4.'}]
77+
}, {
78+
code: [
79+
'<App',
80+
' foo',
81+
'/>'
82+
].join('\n'),
83+
options: ['tab'],
84+
ecmaFeatures: {jsx: true},
85+
errors: [{message: 'Expected indentation of 1 tab character but found 0.'}]
86+
}]
87+
});

0 commit comments

Comments
 (0)