Skip to content

Commit

Permalink
Merge pull request #2086 from jomasti/issue-2035
Browse files Browse the repository at this point in the history
Handle JSX attribute indentation in jsx-indent
  • Loading branch information
ljharb committed Dec 16, 2018
2 parents 78e9ea7 + 6ddc708 commit 9358489
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 4 deletions.
19 changes: 18 additions & 1 deletion docs/rules/jsx-indent.md
Expand Up @@ -31,10 +31,11 @@ The following patterns are considered warnings:
## Rule Options

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

```js
...
"react/jsx-indent": [<enabled>, 'tab'|<number>]
"react/jsx-indent": [<enabled>, 'tab'|<number>, {checkAttributes: <boolean>}]
...
```

Expand All @@ -52,6 +53,14 @@ The following patterns are considered warnings:
<App>
<Hello />
</App>

// [2, 2, {checkAttributes: true}]
<App render={
<Hello render={
(bar) => <div>hi</div>
}
/>
</App>
```
The following patterns are **not** warnings:
Expand All @@ -75,6 +84,14 @@ The following patterns are **not** warnings:
<App>
<Hello />
</App>

// [2, 2, {checkAttributes: false}]
<App render={
<Hello render={
(bar) => <div>hi</div>
}
/>
</App>
```
## When not to use
Expand Down
22 changes: 22 additions & 0 deletions lib/rules/jsx-indent.js
Expand Up @@ -50,6 +50,14 @@ module.exports = {
}, {
type: 'integer'
}]
}, {
type: 'object',
properties: {
checkAttributes: {
type: 'boolean'
}
},
additionalProperties: false
}]
},

Expand All @@ -73,6 +81,8 @@ module.exports = {
}

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

/**
* Responsible for fixing the indentation issue fix
Expand Down Expand Up @@ -242,11 +252,23 @@ module.exports = {
checkNodesIndent(node, peerElementIndent);
}

function handleAttribute(node) {
if (!checkAttributes || node.value.type !== 'JSXExpressionContainer') {
return;
}
const nameIndent = getNodeIndent(node.name);
const lastToken = sourceCode.getLastToken(node.value);
const firstInLine = astUtil.getFirstNodeInLine(context, lastToken);
const indent = node.name.loc.start.line === firstInLine.loc.start.line ? 0 : nameIndent;
checkNodesIndent(firstInLine, indent);
}

return {
JSXOpeningElement: handleOpeningElement,
JSXOpeningFragment: handleOpeningElement,
JSXClosingElement: handleClosingElement,
JSXClosingFragment: handleClosingElement,
JSXAttribute: handleAttribute,
JSXExpressionContainer: function(node) {
if (!node.parent) {
return;
Expand Down
18 changes: 15 additions & 3 deletions lib/util/ast.js
Expand Up @@ -68,13 +68,14 @@ function getComponentProperties(node) {
}
}


/**
* Checks if the node is the first in its line, excluding whitespace.
* Gets the first node in a line from the initial node, excluding whitespace.
* @param {Object} context The node to check
* @param {ASTNode} node The node to check
* @return {Boolean} true if it's the first node in its line
* @return {ASTNode} the first node in the line
*/
function isNodeFirstInLine(context, node) {
function getFirstNodeInLine(context, node) {
const sourceCode = context.getSourceCode();
let token = node;
let lines;
Expand All @@ -87,7 +88,17 @@ function isNodeFirstInLine(context, node) {
token.type === 'JSXText' &&
/^\s*$/.test(lines[lines.length - 1])
);
return token;
}

/**
* Checks if the node is the first in its line, excluding whitespace.
* @param {Object} context The node to check
* @param {ASTNode} node The node to check
* @return {Boolean} true if it's the first node in its line
*/
function isNodeFirstInLine(context, node) {
const token = getFirstNodeInLine(context, node);
const startLine = node.loc.start.line;
const endLine = token ? token.loc.end.line : -1;
return startLine !== endLine;
Expand Down Expand Up @@ -131,6 +142,7 @@ function isClass(node) {

module.exports = {
findReturnStatement: findReturnStatement,
getFirstNodeInLine: getFirstNodeInLine,
getPropertyName: getPropertyName,
getPropertyNameNode: getPropertyNameNode,
getComponentProperties: getComponentProperties,
Expand Down
162 changes: 162 additions & 0 deletions tests/lib/rules/jsx-indent.js
Expand Up @@ -716,6 +716,110 @@ ruleTester.run('jsx-indent', rule, {
`,
parser: 'babel-eslint',
options: [2]
}, {
code: `
const Component = () => (
<View
ListFooterComponent={(
<View
rowSpan={3}
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
/>
)}
/>
);
`,
output: `
const Component = () => (
<View
ListFooterComponent={(
<View
rowSpan={3}
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
/>
)}
/>
);
`,
options: [2]
}, {
code: `
const Component = () => (
\t<View
\t\tListFooterComponent={(
\t\t\t<View
\t\t\t\trowSpan={3}
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
\t\t\t/>
)}
\t/>
);
`,
output: `
const Component = () => (
\t<View
\t\tListFooterComponent={(
\t\t\t<View
\t\t\t\trowSpan={3}
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
\t\t\t/>
\t\t)}
\t/>
);
`,
options: ['tab']
}, {
code: `
const Component = () => (
<View
ListFooterComponent={(
<View
rowSpan={3}
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
/>
)}
/>
);
`,
output: `
const Component = () => (
<View
ListFooterComponent={(
<View
rowSpan={3}
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
/>
)}
/>
);
`,
options: [2, {checkAttributes: false}]
}, {
code: `
const Component = () => (
\t<View
\t\tListFooterComponent={(
\t\t\t<View
\t\t\t\trowSpan={3}
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
\t\t\t/>
)}
\t/>
);
`,
output: `
const Component = () => (
\t<View
\t\tListFooterComponent={(
\t\t\t<View
\t\t\t\trowSpan={3}
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
\t\t\t/>
\t\t)}
\t/>
);
`,
options: ['tab', {checkAttributes: false}]
}],

invalid: [{
Expand Down Expand Up @@ -1478,5 +1582,63 @@ ruleTester.run('jsx-indent', rule, {
errors: [
{message: 'Expected indentation of 4 space characters but found 2.'}
]
}, {
code: `
const Component = () => (
<View
ListFooterComponent={(
<View
rowSpan={3}
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
/>
)}
/>
);
`,
output: `
const Component = () => (
<View
ListFooterComponent={(
<View
rowSpan={3}
placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
/>
)}
/>
);
`,
options: [2, {checkAttributes: true}],
errors: [
{message: 'Expected indentation of 8 space characters but found 4.'}
]
}, {
code: `
const Component = () => (
\t<View
\t\tListFooterComponent={(
\t\t\t<View
\t\t\t\trowSpan={3}
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
\t\t\t/>
)}
\t/>
);
`,
output: `
const Component = () => (
\t<View
\t\tListFooterComponent={(
\t\t\t<View
\t\t\t\trowSpan={3}
\t\t\t\tplaceholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do"
\t\t\t/>
\t\t)}
\t/>
);
`,
options: ['tab', {checkAttributes: true}],
errors: [
{message: 'Expected indentation of 2 tab characters but found 0.'}
]
}]
});

0 comments on commit 9358489

Please sign in to comment.