Repository: yannickcr/eslint-plugin-react
Pull request #1641 (2018-01-11 18:29:48)

Add beforeClosing option to jsx-tag-spacing rule
- Add `beforeClosing` option to `jsx-spacing-rule` which enforces spacing before closing JSX tags
- Fixes issue #1396
- Updated docs
- Add unit tests


Patch:
diff --git a/docs/rules/jsx-tag-spacing.md b/docs/rules/jsx-tag-spacing.md
index 10ae7176..1bcc1b90 100644
--- a/docs/rules/jsx-tag-spacing.md
+++ b/docs/rules/jsx-tag-spacing.md
@@ -1,6 +1,6 @@
 # Validate whitespace in and around the JSX opening and closing brackets (react/jsx-tag-spacing)
 
-Enforce or forbid spaces after the opening bracket, before the closing bracket of self-closing elements, and between the angle bracket and slash of JSX closing or self-closing elements.
+Enforce or forbid spaces after the opening bracket, before the closing bracket, before the closing bracket of self-closing elements, and between the angle bracket and slash of JSX closing or self-closing elements.
 
 **Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.
 
@@ -8,7 +8,7 @@ Enforce or forbid spaces after the opening bracket, before the closing bracket o
 
 This rule checks the whitespace inside and surrounding the JSX syntactic elements.
 
-This rule takes one argument, an object with 3 possible keys: `closingSlash`, `beforeSelfClosing` and `afterOpening`. Each key can receive the value `"allow"` to disable that specific check.
+This rule takes one argument, an object with 4 possible keys: `closingSlash`, `beforeSelfClosing`, `afterOpening`, and `beforeClosing`. Each key can receive the value `"allow"` to disable that specific check.
 
 The default values are:
 
@@ -16,7 +16,8 @@ The default values are:
 {
   "closingSlash": "never",
   "beforeSelfClosing": "always",
-  "afterOpening": "never"
+  "afterOpening": "never",
+  "beforeClosing": "allow"
 }
 ```
 
@@ -176,6 +177,48 @@ The following patterns are **not** considered warnings when configured `"allow-m
 />
 ```
 
+### `beforeClosing`
+
+This check can be set to `"always"`, `"never"`, or `"allow"` (to disable it).
+
+If it is `"always"` the check warns whenever whitespace is missing before the closing bracket of a JSX opening element or whenever a space is missing before the closing bracket closing element. If `"never"`, then it warns if a space is present before the closing bracket of either a JSX opening element or closing element. This rule will never warn for self closing JSX elements. The default value of this check is `"allow"`.
+
+The following patterns are considered warnings when configured `"always"`:
+
+```jsx
+<Hello></Hello>
+<Hello></Hello >
+<Hello ></Hello>
+```
+
+The following patterns are **not** considered warnings when configured `"always"`:
+
+```jsx
+<Hello ></Hello >
+<Hello
+  firstName="John"
+>
+</Hello >
+```
+
+The following patterns are considered warnings when configured `"never"`:
+
+```jsx
+<Hello ></Hello>
+<Hello></Hello >
+<Hello ></Hello >
+```
+
+The following patterns are **not** considered warnings when configured `"never"`:
+
+```jsx
+<Hello></Hello>
+<Hello
+  firstName="John"
+>
+</Hello>
+```
+
 ## When Not To Use It
 
 You can turn this rule off if you are not concerned with the consistency of spacing in or around JSX brackets.
diff --git a/lib/rules/jsx-tag-spacing.js b/lib/rules/jsx-tag-spacing.js
index 35904484..f5dac140 100644
--- a/lib/rules/jsx-tag-spacing.js
+++ b/lib/rules/jsx-tag-spacing.js
@@ -169,6 +169,52 @@ function validateAfterOpening(context, node, option) {
   }
 }
 
+function validateBeforeClosing(context, node, option) {
+  // Don't enforce this rule for self closing tags
+  if (!node.selfClosing) {
+    const sourceCode = context.getSourceCode();
+
+    const NEVER_MESSAGE = 'A space is forbidden before closing bracket';
+    const ALWAYS_MESSAGE = 'Whitespace is required before closing bracket';
+
+    const lastTokens = sourceCode.getLastTokens(node, 2);
+    const closingToken = lastTokens[1];
+    const leftToken = lastTokens[0];
+
+    if (leftToken.loc.start.line !== closingToken.loc.start.line) {
+      return;
+    }
+
+    const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken);
+
+    if (option === 'never' && !adjacent) {
+      context.report({
+        node: node,
+        loc: {
+          start: leftToken.loc.end,
+          end: closingToken.loc.start
+        },
+        message: NEVER_MESSAGE,
+        fix: function(fixer) {
+          return fixer.removeRange([leftToken.range[1], closingToken.range[0]]);
+        }
+      });
+    } else if (option === 'always' && adjacent) {
+      context.report({
+        node: node,
+        loc: {
+          start: leftToken.loc.end,
+          end: closingToken.loc.start
+        },
+        message: ALWAYS_MESSAGE,
+        fix: function(fixer) {
+          return fixer.insertTextBefore(closingToken, ' ');
+        }
+      });
+    }
+  }
+}
+
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -191,12 +237,16 @@ module.exports = {
           },
           afterOpening: {
             enum: ['always', 'allow-multiline', 'never', 'allow']
+          },
+          beforeClosing: {
+            enum: ['always', 'never', 'allow']
           }
         },
         default: {
           closingSlash: 'never',
           beforeSelfClosing: 'always',
-          afterOpening: 'never'
+          afterOpening: 'never',
+          beforeClosing: 'allow'
         },
         additionalProperties: false
       }
@@ -206,7 +256,8 @@ module.exports = {
     const options = {
       closingSlash: 'never',
       beforeSelfClosing: 'always',
-      afterOpening: 'never'
+      afterOpening: 'never',
+      beforeClosing: 'allow'
     };
     for (const key in options) {
       if (has(options, key) && has(context.options[0] || {}, key)) {
@@ -225,6 +276,9 @@ module.exports = {
         if (options.beforeSelfClosing !== 'allow' && node.selfClosing) {
           validateBeforeSelfClosing(context, node, options.beforeSelfClosing);
         }
+        if (options.beforeClosing !== 'allow') {
+          validateBeforeClosing(context, node, options.beforeClosing);
+        }
       },
       JSXClosingElement: function (node) {
         if (options.afterOpening !== 'allow') {
@@ -233,6 +287,9 @@ module.exports = {
         if (options.closingSlash !== 'allow') {
           validateClosingSlash(context, node, options.closingSlash);
         }
+        if (options.beforeClosing !== 'allow') {
+          validateBeforeClosing(context, node, options.beforeClosing);
+        }
       }
     };
   }


Test patch:
diff --git a/tests/lib/rules/jsx-tag-spacing.js b/tests/lib/rules/jsx-tag-spacing.js
index beeefb8f..44f0ddf1 100644
--- a/tests/lib/rules/jsx-tag-spacing.js
+++ b/tests/lib/rules/jsx-tag-spacing.js
@@ -31,7 +31,8 @@ function closingSlashOptions(option) {
   return [{
     closingSlash: option,
     beforeSelfClosing: 'allow',
-    afterOpening: 'allow'
+    afterOpening: 'allow',
+    beforeClosing: 'allow'
   }];
 }
 
@@ -39,7 +40,8 @@ function beforeSelfClosingOptions(option) {
   return [{
     closingSlash: 'allow',
     beforeSelfClosing: option,
-    afterOpening: 'allow'
+    afterOpening: 'allow',
+    beforeClosing: 'allow'
   }];
 }
 
@@ -47,7 +49,17 @@ function afterOpeningOptions(option) {
   return [{
     closingSlash: 'allow',
     beforeSelfClosing: 'allow',
-    afterOpening: option
+    afterOpening: option,
+    beforeClosing: 'allow'
+  }];
+}
+
+function beforeClosingOptions(option) {
+  return [{
+    closingSlash: 'allow',
+    beforeSelfClosing: 'allow',
+    afterOpening: 'allow',
+    beforeClosing: option
   }];
 }
 
@@ -139,19 +151,62 @@ ruleTester.run('jsx-tag-spacing', rule, {
       'App/>'
     ].join('\n'),
     options: afterOpeningOptions('allow-multiline')
+  }, {
+    code: '<App />',
+    options: beforeClosingOptions('never')
+  }, {
+    code: '<App></App>',
+    options: beforeClosingOptions('never')
+  }, {
+    code: [
+      '<App',
+      'foo="bar"',
+      '>',
+      '</App>'
+    ].join('\n'),
+    options: beforeClosingOptions('never')
+  }, {
+    code: [
+      '<App',
+      '   foo="bar"',
+      '>',
+      '</App>'
+    ].join('\n'),
+    options: beforeClosingOptions('never')
+  }, {
+    code: '<App ></App >',
+    options: beforeClosingOptions('always')
+  }, {
+    code: [
+      '<App',
+      'foo="bar"',
+      '>',
+      '</App >'
+    ].join('\n'),
+    options: beforeClosingOptions('always')
+  }, {
+    code: [
+      '<App',
+      '    foo="bar"',
+      '>',
+      '</App >'
+    ].join('\n'),
+    options: beforeClosingOptions('always')
   }, {
     code: '<App/>',
     options: [{
       closingSlash: 'never',
       beforeSelfClosing: 'never',
-      afterOpening: 'never'
+      afterOpening: 'never',
+      beforeClosing: 'never'
     }]
   }, {
     code: '< App / >',
     options: [{
       closingSlash: 'always',
       beforeSelfClosing: 'always',
-      afterOpening: 'always'
+      afterOpening: 'always',
+      beforeClosing: 'always'
     }]
   }],
 
@@ -306,5 +361,55 @@ ruleTester.run('jsx-tag-spacing', rule, {
     output: '<App/>',
     errors: [{message: 'A space is forbidden after opening bracket'}],
     options: afterOpeningOptions('allow-multiline')
+  }, {
+    code: '<App ></App>',
+    output: '<App></App>',
+    errors: [{message: 'A space is forbidden before closing bracket'}],
+    options: beforeClosingOptions('never')
+  }, {
+    code: '<App></App >',
+    output: '<App></App>',
+    errors: [{message: 'A space is forbidden before closing bracket'}],
+    options: beforeClosingOptions('never')
+  }, {
+    code: [
+      '<App',
+      'foo="bar"',
+      '>',
+      '</App >'
+    ].join('\n'),
+    output: [
+      '<App',
+      'foo="bar"',
+      '>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: 'A space is forbidden before closing bracket'}],
+    options: beforeClosingOptions('never')
+  }, {
+    code: '<App></App >',
+    output: '<App ></App >',
+    errors: [{message: 'Whitespace is required before closing bracket'}],
+    options: beforeClosingOptions('always')
+  }, {
+    code: '<App ></App>',
+    output: '<App ></App >',
+    errors: [{message: 'Whitespace is required before closing bracket'}],
+    options: beforeClosingOptions('always')
+  }, {
+    code: [
+      '<App',
+      'foo="bar"',
+      '>',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App',
+      'foo="bar"',
+      '>',
+      '</App >'
+    ].join('\n'),
+    errors: [{message: 'Whitespace is required before closing bracket'}],
+    options: beforeClosingOptions('always')
   }]
 });