diff --git a/docs/rules/jsx-indent.md b/docs/rules/jsx-indent.md index 6f8b73df53..44bebb1fb7 100644 --- a/docs/rules/jsx-indent.md +++ b/docs/rules/jsx-indent.md @@ -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": [, 'tab'|] +"react/jsx-indent": [, 'tab'|, {checkAttributes: }] ... ``` @@ -52,6 +53,14 @@ The following patterns are considered warnings: + +// [2, 2, {checkAttributes: true}] +
hi
+} + /> +
``` The following patterns are **not** warnings: @@ -75,6 +84,14 @@ The following patterns are **not** warnings: + +// [2, 2, {checkAttributes: false}] +
hi
+} + /> +
``` ## When not to use diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js index 1ba0d75cdc..b79f724cba 100644 --- a/lib/rules/jsx-indent.js +++ b/lib/rules/jsx-indent.js @@ -50,6 +50,14 @@ module.exports = { }, { type: 'integer' }] + }, { + type: 'object', + properties: { + checkAttributes: { + type: 'boolean' + } + }, + additionalProperties: false }] }, @@ -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 @@ -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; diff --git a/lib/util/ast.js b/lib/util/ast.js index 9af7e0c5ce..90d41a2d5b 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -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; @@ -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; @@ -131,6 +142,7 @@ function isClass(node) { module.exports = { findReturnStatement: findReturnStatement, + getFirstNodeInLine: getFirstNodeInLine, getPropertyName: getPropertyName, getPropertyNameNode: getPropertyNameNode, getComponentProperties: getComponentProperties, diff --git a/tests/lib/rules/jsx-indent.js b/tests/lib/rules/jsx-indent.js index fc44dd4a5a..75584a32bf 100644 --- a/tests/lib/rules/jsx-indent.js +++ b/tests/lib/rules/jsx-indent.js @@ -716,6 +716,110 @@ ruleTester.run('jsx-indent', rule, { `, parser: 'babel-eslint', options: [2] + }, { + code: ` + const Component = () => ( + + )} + /> + ); + `, + output: ` + const Component = () => ( + + )} + /> + ); + `, + options: [2] + }, { + code: ` +const Component = () => ( +\t +)} +\t/> +); + `, + output: ` +const Component = () => ( +\t +\t\t)} +\t/> +); + `, + options: ['tab'] + }, { + code: ` + const Component = () => ( + + )} + /> + ); + `, + output: ` + const Component = () => ( + + )} + /> + ); + `, + options: [2, {checkAttributes: false}] + }, { + code: ` +const Component = () => ( +\t +)} +\t/> +); + `, + output: ` +const Component = () => ( +\t +\t\t)} +\t/> +); + `, + options: ['tab', {checkAttributes: false}] }], invalid: [{ @@ -1478,5 +1582,63 @@ ruleTester.run('jsx-indent', rule, { errors: [ {message: 'Expected indentation of 4 space characters but found 2.'} ] + }, { + code: ` + const Component = () => ( + + )} + /> + ); + `, + output: ` + const Component = () => ( + + )} + /> + ); + `, + options: [2, {checkAttributes: true}], + errors: [ + {message: 'Expected indentation of 8 space characters but found 4.'} + ] + }, { + code: ` +const Component = () => ( +\t +)} +\t/> +); + `, + output: ` +const Component = () => ( +\t +\t\t)} +\t/> +); + `, + options: ['tab', {checkAttributes: true}], + errors: [ + {message: 'Expected indentation of 2 tab characters but found 0.'} + ] }] });