Repository: yannickcr/eslint-plugin-react
Pull request #1215 (2017-05-20 18:35:08)

Add indentation rule for closing tag of multi-line jsx
For issue #1206:

This rule validates that the closing tag for a multi-line jsx component matches the indentation of the opening tag.


Patch:
diff --git a/README.md b/README.md
index bd7b856b..cd2fa5b6 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,7 @@ Finally, enable all of the rules that you would like to use.  Use [our preset](#
 
 * [react/jsx-boolean-value](docs/rules/jsx-boolean-value.md): Enforce boolean attributes notation in JSX (fixable)
 * [react/jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md): Validate closing bracket location in JSX (fixable)
+* [react/jsx-closing-tag-location](docs/rules/jsx-closing-tag-location.md): Validate closing tag location in JSX (fixable)
 * [react/jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes (fixable)
 * [react/jsx-equals-spacing](docs/rules/jsx-equals-spacing.md): Enforce or disallow spaces around equal signs in JSX attributes (fixable)
 * [react/jsx-filename-extension](docs/rules/jsx-filename-extension.md): Restrict file extensions that may contain JSX
diff --git a/docs/rules/jsx-closing-tag-location.md b/docs/rules/jsx-closing-tag-location.md
new file mode 100644
index 00000000..94f1c611
--- /dev/null
+++ b/docs/rules/jsx-closing-tag-location.md
@@ -0,0 +1,38 @@
+# Validate closing tag location in JSX (react/jsx-closing-tag-location)
+
+Enforce the closing tag location for multiline JSX elements.
+
+**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.
+
+## Rule Details
+
+This rule checks all JSX multiline elements with children (non-self-closing) and verifies the location of the closing tag. The expectation is that the closing tag is aligned with the opening tag on its own line.
+
+The following patterns are considered warnings:
+
+```jsx
+<Hello>
+  marklar
+  </Hello>
+```
+
+```jsx
+<Hello>
+  marklar</Hello>
+```
+
+The following are not considered warnings:
+
+```jsx
+<Hello>
+  marklar
+</Hello>
+```
+
+```jsx
+<Hello>marklar</Hello>
+```
+
+## When not to use
+
+If you do not care about closing tag JSX alignment then you can disable this rule.
diff --git a/index.js b/index.js
index 85385275..de9a575b 100644
--- a/index.js
+++ b/index.js
@@ -41,6 +41,7 @@ var allRules = {
   'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
   'jsx-indent': require('./lib/rules/jsx-indent'),
   'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
+  'jsx-closing-tag-location': require('./lib/rules/jsx-closing-tag-location'),
   'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),
   'no-direct-mutation-state': require('./lib/rules/no-direct-mutation-state'),
   'forbid-component-props': require('./lib/rules/forbid-component-props'),
diff --git a/lib/rules/jsx-closing-tag-location.js b/lib/rules/jsx-closing-tag-location.js
new file mode 100644
index 00000000..9059d529
--- /dev/null
+++ b/lib/rules/jsx-closing-tag-location.js
@@ -0,0 +1,87 @@
+/**
+ * @fileoverview Validate closing tag location in JSX
+ * @author Ross Solomon
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Validate closing tag location for multiline JSX',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+    fixable: 'whitespace'
+  },
+
+  create: function(context) {
+    var sourceCode = context.getSourceCode();
+
+    /**
+     * Checks if the node is the first in its line, excluding whitespace.
+     * @param {ASTNode} node The node to check
+     * @return {Boolean} true if its the first node in its line
+     */
+    function isNodeFirstInLine(node) {
+      let token = node;
+      let lines;
+      do {
+        token = sourceCode.getTokenBefore(token);
+        lines = token.type === 'JSXText'
+          ? token.value.split('\n')
+          : null;
+      } while (
+        token.type === 'JSXText' &&
+        /^\s*$/.test(lines[lines.length - 1])
+      );
+
+      var startLine = node.loc.start.line;
+      var endLine = token ? token.loc.end.line : -1;
+      return startLine !== endLine;
+    }
+
+    return {
+      JSXClosingElement: function(node) {
+        if (!node.parent) {
+          return;
+        }
+
+        const opening = node.parent.openingElement;
+        if (opening.loc.start.line === node.loc.start.line) {
+          return;
+        }
+
+        if (opening.loc.start.column === node.loc.start.column) {
+          return;
+        }
+
+        let message;
+        if (!isNodeFirstInLine(node)) {
+          message = 'Closing tag of a multiline JSX expression must be on its own line.';
+        } else {
+          message = 'Expected closing tag to match indentation of opening.';
+        }
+
+        context.report({
+          node: node,
+          loc: node.loc,
+          message,
+          fix: function(fixer) {
+            const indent = Array(opening.loc.start.column + 1).join(' ');
+            if (isNodeFirstInLine(node)) {
+              return fixer.replaceTextRange(
+                [node.start - node.loc.start.column, node.start],
+                indent
+              );
+            }
+
+            return fixer.insertTextBefore(node, `\n${indent}`);
+          }
+        });
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/jsx-closing-tag-location.js b/tests/lib/rules/jsx-closing-tag-location.js
new file mode 100644
index 00000000..af1e475a
--- /dev/null
+++ b/tests/lib/rules/jsx-closing-tag-location.js
@@ -0,0 +1,65 @@
+/**
+ * @fileoverview Validate closing tag location in JSX
+ * @author Ross Solomon
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/jsx-closing-tag-location');
+const RuleTester = require('eslint').RuleTester;
+const parserOptions = {
+  sourceType: 'module',
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+const MESSAGE_MATCH_INDENTATION = [{message: 'Expected closing tag to match indentation of opening.'}];
+const MESSAGE_OWN_LINE = [{message: 'Closing tag of a multiline JSX expression must be on its own line.'}];
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({parserOptions});
+ruleTester.run('jsx-closing-tag-location', rule, {
+  valid: [{
+    code: [
+      '<App>',
+      '  foo',
+      '</App>'
+    ].join('\n')
+  }, {
+    code: [
+      '<App>foo</App>'
+    ].join('\n')
+  }],
+
+  invalid: [{
+    code: [
+      '<App>',
+      '  foo',
+      '  </App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  foo',
+      '</App>'
+    ].join('\n'),
+    errors: MESSAGE_MATCH_INDENTATION
+  }, {
+    code: [
+      '<App>',
+      '  foo</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  foo',
+      '</App>'
+    ].join('\n'),
+    errors: MESSAGE_OWN_LINE
+  }]
+});