Skip to content

Commit 9358489

Browse files
authored
Merge pull request #2086 from jomasti/issue-2035
Handle JSX attribute indentation in jsx-indent
2 parents 78e9ea7 + 6ddc708 commit 9358489

File tree

4 files changed

+217
-4
lines changed

4 files changed

+217
-4
lines changed

docs/rules/jsx-indent.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ The following patterns are considered warnings:
3131
## Rule Options
3232

3333
It takes an option as the second parameter which can be `"tab"` for tab-based indentation or a positive number for space indentations.
34+
To enable checking the indentation of attributes, use the third parameter to turn on the `checkAttributes` option (default is false).
3435

3536
```js
3637
...
37-
"react/jsx-indent": [<enabled>, 'tab'|<number>]
38+
"react/jsx-indent": [<enabled>, 'tab'|<number>, {checkAttributes: <boolean>}]
3839
...
3940
```
4041

@@ -52,6 +53,14 @@ The following patterns are considered warnings:
5253
<App>
5354
<Hello />
5455
</App>
56+
57+
// [2, 2, {checkAttributes: true}]
58+
<App render={
59+
<Hello render={
60+
(bar) => <div>hi</div>
61+
}
62+
/>
63+
</App>
5564
```
5665
5766
The following patterns are **not** warnings:
@@ -75,6 +84,14 @@ The following patterns are **not** warnings:
7584
<App>
7685
<Hello />
7786
</App>
87+
88+
// [2, 2, {checkAttributes: false}]
89+
<App render={
90+
<Hello render={
91+
(bar) => <div>hi</div>
92+
}
93+
/>
94+
</App>
7895
```
7996
8097
## When not to use

lib/rules/jsx-indent.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ module.exports = {
5050
}, {
5151
type: 'integer'
5252
}]
53+
}, {
54+
type: 'object',
55+
properties: {
56+
checkAttributes: {
57+
type: 'boolean'
58+
}
59+
},
60+
additionalProperties: false
5361
}]
5462
},
5563

@@ -73,6 +81,8 @@ module.exports = {
7381
}
7482

7583
const indentChar = indentType === 'space' ? ' ' : '\t';
84+
const options = context.options[1] || {};
85+
const checkAttributes = options.checkAttributes || false;
7686

7787
/**
7888
* Responsible for fixing the indentation issue fix
@@ -242,11 +252,23 @@ module.exports = {
242252
checkNodesIndent(node, peerElementIndent);
243253
}
244254

255+
function handleAttribute(node) {
256+
if (!checkAttributes || node.value.type !== 'JSXExpressionContainer') {
257+
return;
258+
}
259+
const nameIndent = getNodeIndent(node.name);
260+
const lastToken = sourceCode.getLastToken(node.value);
261+
const firstInLine = astUtil.getFirstNodeInLine(context, lastToken);
262+
const indent = node.name.loc.start.line === firstInLine.loc.start.line ? 0 : nameIndent;
263+
checkNodesIndent(firstInLine, indent);
264+
}
265+
245266
return {
246267
JSXOpeningElement: handleOpeningElement,
247268
JSXOpeningFragment: handleOpeningElement,
248269
JSXClosingElement: handleClosingElement,
249270
JSXClosingFragment: handleClosingElement,
271+
JSXAttribute: handleAttribute,
250272
JSXExpressionContainer: function(node) {
251273
if (!node.parent) {
252274
return;

lib/util/ast.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,14 @@ function getComponentProperties(node) {
6868
}
6969
}
7070

71+
7172
/**
72-
* Checks if the node is the first in its line, excluding whitespace.
73+
* Gets the first node in a line from the initial node, excluding whitespace.
7374
* @param {Object} context The node to check
7475
* @param {ASTNode} node The node to check
75-
* @return {Boolean} true if it's the first node in its line
76+
* @return {ASTNode} the first node in the line
7677
*/
77-
function isNodeFirstInLine(context, node) {
78+
function getFirstNodeInLine(context, node) {
7879
const sourceCode = context.getSourceCode();
7980
let token = node;
8081
let lines;
@@ -87,7 +88,17 @@ function isNodeFirstInLine(context, node) {
8788
token.type === 'JSXText' &&
8889
/^\s*$/.test(lines[lines.length - 1])
8990
);
91+
return token;
92+
}
9093

94+
/**
95+
* Checks if the node is the first in its line, excluding whitespace.
96+
* @param {Object} context The node to check
97+
* @param {ASTNode} node The node to check
98+
* @return {Boolean} true if it's the first node in its line
99+
*/
100+
function isNodeFirstInLine(context, node) {
101+
const token = getFirstNodeInLine(context, node);
91102
const startLine = node.loc.start.line;
92103
const endLine = token ? token.loc.end.line : -1;
93104
return startLine !== endLine;
@@ -131,6 +142,7 @@ function isClass(node) {
131142

132143
module.exports = {
133144
findReturnStatement: findReturnStatement,
145+
getFirstNodeInLine: getFirstNodeInLine,
134146
getPropertyName: getPropertyName,
135147
getPropertyNameNode: getPropertyNameNode,
136148
getComponentProperties: getComponentProperties,

tests/lib/rules/jsx-indent.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,110 @@ ruleTester.run('jsx-indent', rule, {
716716
`,
717717
parser: 'babel-eslint',
718718
options: [2]
719+
}, {
720+
code: `
721+
const Component = () => (
722+
<View
723+
ListFooterComponent={(
724+
<View
725+
rowSpan={3}
726+
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
727+
/>
728+
)}
729+
/>
730+
);
731+
`,
732+
output: `
733+
const Component = () => (
734+
<View
735+
ListFooterComponent={(
736+
<View
737+
rowSpan={3}
738+
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
739+
/>
740+
)}
741+
/>
742+
);
743+
`,
744+
options: [2]
745+
}, {
746+
code: `
747+
const Component = () => (
748+
\t<View
749+
\t\tListFooterComponent={(
750+
\t\t\t<View
751+
\t\t\t\trowSpan={3}
752+
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
753+
\t\t\t/>
754+
)}
755+
\t/>
756+
);
757+
`,
758+
output: `
759+
const Component = () => (
760+
\t<View
761+
\t\tListFooterComponent={(
762+
\t\t\t<View
763+
\t\t\t\trowSpan={3}
764+
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
765+
\t\t\t/>
766+
\t\t)}
767+
\t/>
768+
);
769+
`,
770+
options: ['tab']
771+
}, {
772+
code: `
773+
const Component = () => (
774+
<View
775+
ListFooterComponent={(
776+
<View
777+
rowSpan={3}
778+
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
779+
/>
780+
)}
781+
/>
782+
);
783+
`,
784+
output: `
785+
const Component = () => (
786+
<View
787+
ListFooterComponent={(
788+
<View
789+
rowSpan={3}
790+
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
791+
/>
792+
)}
793+
/>
794+
);
795+
`,
796+
options: [2, {checkAttributes: false}]
797+
}, {
798+
code: `
799+
const Component = () => (
800+
\t<View
801+
\t\tListFooterComponent={(
802+
\t\t\t<View
803+
\t\t\t\trowSpan={3}
804+
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
805+
\t\t\t/>
806+
)}
807+
\t/>
808+
);
809+
`,
810+
output: `
811+
const Component = () => (
812+
\t<View
813+
\t\tListFooterComponent={(
814+
\t\t\t<View
815+
\t\t\t\trowSpan={3}
816+
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
817+
\t\t\t/>
818+
\t\t)}
819+
\t/>
820+
);
821+
`,
822+
options: ['tab', {checkAttributes: false}]
719823
}],
720824

721825
invalid: [{
@@ -1478,5 +1582,63 @@ ruleTester.run('jsx-indent', rule, {
14781582
errors: [
14791583
{message: 'Expected indentation of 4 space characters but found 2.'}
14801584
]
1585+
}, {
1586+
code: `
1587+
const Component = () => (
1588+
<View
1589+
ListFooterComponent={(
1590+
<View
1591+
rowSpan={3}
1592+
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
1593+
/>
1594+
)}
1595+
/>
1596+
);
1597+
`,
1598+
output: `
1599+
const Component = () => (
1600+
<View
1601+
ListFooterComponent={(
1602+
<View
1603+
rowSpan={3}
1604+
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
1605+
/>
1606+
)}
1607+
/>
1608+
);
1609+
`,
1610+
options: [2, {checkAttributes: true}],
1611+
errors: [
1612+
{message: 'Expected indentation of 8 space characters but found 4.'}
1613+
]
1614+
}, {
1615+
code: `
1616+
const Component = () => (
1617+
\t<View
1618+
\t\tListFooterComponent={(
1619+
\t\t\t<View
1620+
\t\t\t\trowSpan={3}
1621+
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
1622+
\t\t\t/>
1623+
)}
1624+
\t/>
1625+
);
1626+
`,
1627+
output: `
1628+
const Component = () => (
1629+
\t<View
1630+
\t\tListFooterComponent={(
1631+
\t\t\t<View
1632+
\t\t\t\trowSpan={3}
1633+
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
1634+
\t\t\t/>
1635+
\t\t)}
1636+
\t/>
1637+
);
1638+
`,
1639+
options: ['tab', {checkAttributes: true}],
1640+
errors: [
1641+
{message: 'Expected indentation of 2 tab characters but found 0.'}
1642+
]
14811643
}]
14821644
});

0 commit comments

Comments
 (0)