Repository: yannickcr/eslint-plugin-react (js)

Issue:
Rule proposal: jsx-tag-spacing
For some reason, `/ >` is valid JSX, as is `/` followed by any kind of whitespace until the `>`. This rule would require that the self-closing terminator is always written as an atomic `/>`.

Not having a check for this also allows for funny workarounds for [jsx-closing-bracket-location](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-closing-bracket-location.md), such as this (when using `after-props`):

``` js
<SomeComponent
  with='many'
  props='specified' /
>
```

Update: https://github.com/yannickcr/eslint-plugin-react/issues/693#issuecomment-234842584


Pull request description:
Add jsx-tag-spacing rule (Fixes #693)
Replaces #706 after some discussion summarised on https://github.com/yannickcr/eslint-plugin-react/issues/693#issuecomment-234842584.


Patch:
diff --git a/README.md b/README.md
index e8c80b72..03184694 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,7 @@ Finally, enable all of the rules that you would like to use.  Use [our preset](#
 * [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
 * [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting
 * [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable)
+* [react/jsx-tag-spacing](docs/rules/jsx-tag-spacing.md): Validate whitespace in and around the JSX opening and closing brackets (fixable)
 * [react/jsx-uses-react](docs/rules/jsx-uses-react.md): Prevent React to be incorrectly marked as unused
 * [react/jsx-uses-vars](docs/rules/jsx-uses-vars.md): Prevent variables used in JSX to be incorrectly marked as unused
 * [react/jsx-wrap-multilines](docs/rules/jsx-wrap-multilines.md): Prevent missing parentheses around multilines JSX (fixable)
diff --git a/docs/rules/jsx-tag-spacing.md b/docs/rules/jsx-tag-spacing.md
new file mode 100644
index 00000000..bd0f4f40
--- /dev/null
+++ b/docs/rules/jsx-tag-spacing.md
@@ -0,0 +1,179 @@
+# Validate whitespace in and around the JSX opening and closing brackets (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.
+
+## Rule Details
+
+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.
+
+The default values are:
+
+```json
+{
+  "closingSlash": "never",
+  "beforeSelfClosing": "always",
+  "afterOpening": "never"
+}
+```
+
+The options for each sub-option are documented in the following subsections.
+
+### `closingSlash`
+
+This check can be set to `"always"`, `"never"` or `"allow"` (to disable it).
+
+If it is `"never"`, the check warns whenever a space is separating the two characters in the JSX tokens `</` and `/>`. If it is `"always"` then it warns whenever a space is missing separating the mentioned two characters. The default value of this check is `"never"`.
+
+The following patterns are considered warnings when configured `"never"`:
+
+```jsx
+<App/ >
+<input/
+>
+<Provider>< /Provider>
+```
+
+The following patterns are not considered warnings when configured `"never"`:
+
+```jsx
+<App/>
+<input/>
+<Provider></Provider>
+```
+
+The following patterns are considered warnings when configured `"always"`:
+
+```jsx
+<Hello/>
+<Goodbye></Goodbye>
+```
+
+The following patterns are not considered warnings when configured `"never"`:
+
+```jsx
+<Hello/ >
+<Goodbye>< /Goodbye>
+```
+
+### `beforeSelfClosing`
+
+This check can be set to `"always"`, `"never"` or `"allow"` (to disable it).
+
+If it is `"always"`, the check warns whenever a space is missing before the closing bracket. If `"never"` then it warns if a space is present before the closing bracket. The default value of this check is `"always"`.
+
+The following patterns are considered warnings when configured `"always"`:
+
+```jsx
+<Hello/>
+<Hello firstname="John"/>
+```
+
+The following patterns are not considered warnings when configured `"always"`:
+
+```jsx
+<Hello />
+<Hello firstName="John" />
+<Hello
+  firstName="John"
+  lastName="Smith"
+/>
+```
+
+The following patterns are considered warnings when configured `"never"`:
+
+```jsx
+<Hello />
+<Hello firstName="John" />
+```
+
+The following patterns are not considered warnings when configured `"never"`:
+
+```jsx
+<Hello/>
+<Hello firstname="John"/>
+<Hello
+  firstName="John"
+  lastName="Smith"
+/>
+```
+
+### `afterOpening`
+
+This check can be set to `"always"`, `"never"`, `"allow-multiline"` or `"allow"` (to disable it).
+
+If it is `"always"`, the check warns whenever a space is missing after the opening bracket of either a JSX opening element or closing element. If `"never"` then it warns if a space is present after the opening bracket of either a JSX opening element or closing element. If `"allow-multiline"` then it behaves like `"never"`, but allows if the separator includes a newline character. The default value of this check is `"never"`.
+
+The following patterns are considered warnings when configured `"always"`:
+
+```jsx
+<Hello></Hello>
+<Hello firstname="John"/>
+<Hello
+  firstName="John"
+  lastName="Smith"
+/>
+```
+
+The following patterns are not considered warnings when configured `"always"`:
+
+```jsx
+< Hello></ Hello>
+< Hello firstName="John"/>
+<
+  Hello
+  firstName="John"
+  lastName="Smith"
+/>
+```
+
+The following patterns are considered warnings when configured `"never"`:
+
+```jsx
+< Hello></ Hello>
+< Hello firstName="John"/>
+<
+  Hello
+  firstName="John"
+  lastName="Smith"
+/>
+```
+
+The following patterns are not considered warnings when configured `"never"`:
+
+```jsx
+<Hello></Hello>
+<Hello firstname="John"/>
+<Hello
+  firstName="John"
+  lastName="Smith"
+/>
+```
+
+The following patterns are considered warnings when configured `"allow-multiline"`:
+
+```jsx
+< Hello></ Hello>
+< Hello firstName="John"/>
+< Hello
+  firstName="John"
+  lastName="Smith"
+/>
+```
+
+The following patterns are not considered warnings when configured `"allow-multiline"`:
+
+```jsx
+<Hello></Hello>
+<Hello firstName="John"/>
+<
+  Hello
+  firstName="John"
+  lastName="Smith"
+/>
+```
+
+## 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/index.js b/index.js
index 758e8d84..59a74d5d 100644
--- a/index.js
+++ b/index.js
@@ -55,7 +55,8 @@ var allRules = {
   'no-children-prop': require('./lib/rules/no-children-prop'),
   'no-comment-textnodes': require('./lib/rules/no-comment-textnodes'),
   'require-extension': require('./lib/rules/require-extension'),
-  'wrap-multilines': require('./lib/rules/wrap-multilines')
+  'wrap-multilines': require('./lib/rules/wrap-multilines'),
+  'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing')
 };
 
 function filterRules(rules, predicate) {
diff --git a/lib/rules/jsx-tag-spacing.js b/lib/rules/jsx-tag-spacing.js
new file mode 100644
index 00000000..899dc0aa
--- /dev/null
+++ b/lib/rules/jsx-tag-spacing.js
@@ -0,0 +1,250 @@
+/**
+ * @fileoverview Validates whitespace in and around the JSX opening and closing brackets
+ * @author Diogo Franco (Kovensky)
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Find the token before the closing bracket.
+ * @param {ASTNode} node - The JSX element node.
+ * @returns {Token} The token before the closing bracket.
+ */
+function getTokenBeforeClosingBracket(node) {
+  var attributes = node.attributes;
+  if (attributes.length === 0) {
+    return node.name;
+  }
+  return attributes[ attributes.length - 1 ];
+}
+
+// ------------------------------------------------------------------------------
+// Validators
+// ------------------------------------------------------------------------------
+
+function validateClosingSlash(context, node, option) {
+  var sourceCode = context.getSourceCode();
+
+  var SELF_CLOSING_NEVER_MESSAGE = 'Whitespace is forbidden between `/` and `>`; write `/>`';
+  var SELF_CLOSING_ALWAYS_MESSAGE = 'Whitespace is required between `/` and `>`; write `/ >`';
+  var NEVER_MESSAGE = 'Whitespace is forbidden between `<` and `/`; write `</`';
+  var ALWAYS_MESSAGE = 'Whitespace is required between `<` and `/`; write `< /`';
+
+  var adjacent;
+
+  if (node.selfClosing) {
+    var lastTokens = sourceCode.getLastTokens(node, 2);
+
+    adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]);
+
+    if (option === 'never') {
+      if (!adjacent) {
+        context.report({
+          node: node,
+          loc: {
+            start: lastTokens[0].loc.start,
+            end: lastTokens[1].loc.end
+          },
+          message: SELF_CLOSING_NEVER_MESSAGE,
+          fix: function(fixer) {
+            return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]);
+          }
+        });
+      }
+    } else if (option === 'always' && adjacent) {
+      context.report({
+        node: node,
+        loc: {
+          start: lastTokens[0].loc.start,
+          end: lastTokens[1].loc.end
+        },
+        message: SELF_CLOSING_ALWAYS_MESSAGE,
+        fix: function(fixer) {
+          return fixer.insertTextBefore(lastTokens[1], ' ');
+        }
+      });
+    }
+  } else {
+    var firstTokens = sourceCode.getFirstTokens(node, 2);
+
+    adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]);
+
+    if (option === 'never') {
+      if (!adjacent) {
+        context.report({
+          node: node,
+          loc: {
+            start: firstTokens[0].loc.start,
+            end: firstTokens[1].loc.end
+          },
+          message: NEVER_MESSAGE,
+          fix: function(fixer) {
+            return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]);
+          }
+        });
+      }
+    } else if (option === 'always' && adjacent) {
+      context.report({
+        node: node,
+        loc: {
+          start: firstTokens[0].loc.start,
+          end: firstTokens[1].loc.end
+        },
+        message: ALWAYS_MESSAGE,
+        fix: function(fixer) {
+          return fixer.insertTextBefore(firstTokens[1], ' ');
+        }
+      });
+    }
+  }
+}
+
+function validateBeforeSelfClosing(context, node, option) {
+  var sourceCode = context.getSourceCode();
+
+  var NEVER_MESSAGE = 'A space is forbidden before closing bracket';
+  var ALWAYS_MESSAGE = 'A space is required before closing bracket';
+
+  var leftToken = getTokenBeforeClosingBracket(node);
+  var closingSlash = sourceCode.getTokenAfter(leftToken);
+
+  if (leftToken.loc.end.line !== closingSlash.loc.start.line) {
+    return;
+  }
+
+  if (option === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
+    context.report({
+      node: node,
+      loc: closingSlash.loc.start,
+      message: ALWAYS_MESSAGE,
+      fix: function(fixer) {
+        return fixer.insertTextBefore(closingSlash, ' ');
+      }
+    });
+  } else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) {
+    context.report({
+      node: node,
+      loc: closingSlash.loc.start,
+      message: NEVER_MESSAGE,
+      fix: function(fixer) {
+        var previousToken = sourceCode.getTokenBefore(closingSlash);
+        return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]);
+      }
+    });
+  }
+}
+
+function validateAfterOpening(context, node, option) {
+  var sourceCode = context.getSourceCode();
+
+  var NEVER_MESSAGE = 'A space is forbidden after opening bracket';
+  var ALWAYS_MESSAGE = 'A space is required after opening bracket';
+
+  var openingToken = sourceCode.getTokenBefore(node.name);
+
+  if (option === 'allow-multiline') {
+    if (openingToken.loc.start.line !== node.name.loc.start.line) {
+      return;
+    }
+  }
+
+  var adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name);
+
+  if (option === 'never' || option === 'allow-multiline') {
+    if (!adjacent) {
+      context.report({
+        node: node,
+        loc: {
+          start: openingToken.loc.start,
+          end: node.name.loc.start
+        },
+        message: NEVER_MESSAGE,
+        fix: function(fixer) {
+          return fixer.removeRange([openingToken.range[1], node.name.range[0]]);
+        }
+      });
+    }
+  } else if (option === 'always' && adjacent) {
+    context.report({
+      node: node,
+      loc: {
+        start: openingToken.loc.start,
+        end: node.name.loc.start
+      },
+      message: ALWAYS_MESSAGE,
+      fix: function(fixer) {
+        return fixer.insertTextBefore(node.name, ' ');
+      }
+    });
+  }
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {},
+    fixable: 'whitespace',
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          closingSlash: {
+            enum: ['always', 'never', 'allow']
+          },
+          beforeSelfClosing: {
+            enum: ['always', 'never', 'allow']
+          },
+          afterOpening: {
+            enum: ['always', 'allow-multiline', 'never', 'allow']
+          }
+        },
+        default: {
+          closingSlash: 'never',
+          beforeSelfClosing: 'always',
+          afterOpening: 'never'
+        },
+        additionalProperties: false
+      }
+    ]
+  },
+  create: function (context) {
+    var options = {
+      closingSlash: 'never',
+      beforeSelfClosing: 'always',
+      afterOpening: 'never'
+    };
+    for (var key in options) {
+      if (options.hasOwnProperty(key) && context.options[0].hasOwnProperty(key)) {
+        options[key] = context.options[0][key];
+      }
+    }
+
+    return {
+      JSXOpeningElement: function (node) {
+        if (options.closingSlash !== 'allow' && node.selfClosing) {
+          validateClosingSlash(context, node, options.closingSlash);
+        }
+        if (options.afterOpening !== 'allow') {
+          validateAfterOpening(context, node, options.afterOpening);
+        }
+        if (options.beforeSelfClosing !== 'allow' && node.selfClosing) {
+          validateBeforeSelfClosing(context, node, options.beforeSelfClosing);
+        }
+      },
+      JSXClosingElement: function (node) {
+        if (options.afterOpening !== 'allow') {
+          validateAfterOpening(context, node, options.afterOpening);
+        }
+        if (options.closingSlash !== 'allow') {
+          validateClosingSlash(context, node, options.closingSlash);
+        }
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/jsx-tag-spacing.js b/tests/lib/rules/jsx-tag-spacing.js
new file mode 100644
index 00000000..5432cf0f
--- /dev/null
+++ b/tests/lib/rules/jsx-tag-spacing.js
@@ -0,0 +1,308 @@
+/**
+ * @fileoverview Tests for jsx-tag-spacing
+ * @author Diogo Franco (Kovensky)
+ */
+
+'use strict';
+
+// -----------------------------------------------------------------------------
+// Requirements
+// -----------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/jsx-tag-spacing');
+var RuleTester = require('eslint').RuleTester;
+
+var parserOptions = {
+  ecmaVersion: 6,
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+// -----------------------------------------------------------------------------
+// Helpers
+// -----------------------------------------------------------------------------
+
+// generate options object that disables checks other than the tested one
+
+function closingSlashOptions(option) {
+  return [{
+    closingSlash: option,
+    beforeSelfClosing: 'allow',
+    afterOpening: 'allow'
+  }];
+}
+
+function beforeSelfClosingOptions(option) {
+  return [{
+    closingSlash: 'allow',
+    beforeSelfClosing: option,
+    afterOpening: 'allow'
+  }];
+}
+
+function afterOpeningOptions(option) {
+  return [{
+    closingSlash: 'allow',
+    beforeSelfClosing: 'allow',
+    afterOpening: option
+  }];
+}
+
+// -----------------------------------------------------------------------------
+// Tests
+// -----------------------------------------------------------------------------
+
+var ruleTester = new RuleTester({
+  parserOptions: parserOptions
+});
+ruleTester.run('jsx-tag-spacing', rule, {
+  valid: [{
+    code: '<App />',
+    options: beforeSelfClosingOptions('always')
+  }, {
+    code: '<App foo />',
+    options: beforeSelfClosingOptions('always')
+  }, {
+    code: '<App foo={bar} />',
+    options: beforeSelfClosingOptions('always')
+  }, {
+    code: '<App {...props} />',
+    options: beforeSelfClosingOptions('always')
+  }, {
+    code: '<App></App>',
+    options: beforeSelfClosingOptions('always')
+  }, {
+    code: [
+      '<App',
+      '  foo={bar}',
+      '/>'
+    ].join('\n'),
+    options: beforeSelfClosingOptions('always')
+  }, {
+    code: '<App/>',
+    options: beforeSelfClosingOptions('never')
+  }, {
+    code: '<App foo/>',
+    options: beforeSelfClosingOptions('never')
+  }, {
+    code: '<App foo={bar}/>',
+    options: beforeSelfClosingOptions('never')
+  }, {
+    code: '<App {...props}/>',
+    options: beforeSelfClosingOptions('never')
+  }, {
+    code: '<App></App>',
+    options: beforeSelfClosingOptions('never')
+  }, {
+    code: [
+      '<App',
+      '  foo={bar}',
+      '/>'
+    ].join('\n'),
+    options: beforeSelfClosingOptions('never')
+  }, {
+    code: '<App/>;',
+    options: closingSlashOptions('never')
+  }, {
+    code: '<App />;',
+    options: closingSlashOptions('never')
+  }, {
+    code: '<div className="bar"></div>;',
+    options: closingSlashOptions('never')
+  }, {
+    code: '<div className="bar"></ div>;',
+    options: closingSlashOptions('never')
+  }, {
+    code: '<App prop="foo">< /App>',
+    options: closingSlashOptions('always')
+  }, {
+    code: '<p/ >',
+    options: closingSlashOptions('always')
+  }, {
+    code: '<App/>',
+    options: afterOpeningOptions('never')
+  }, {
+    code: '<App></App>',
+    options: afterOpeningOptions('never')
+  }, {
+    code: '< App></ App>',
+    options: afterOpeningOptions('always')
+  }, {
+    code: '< App/>',
+    options: afterOpeningOptions('always')
+  }, {
+    code: [
+      '<',
+      'App/>'
+    ].join('\n'),
+    options: afterOpeningOptions('allow-multiline')
+  }, {
+    code: '<App/>',
+    options: [{
+      closingSlash: 'never',
+      beforeSelfClosing: 'never',
+      afterOpening: 'never'
+    }]
+  }, {
+    code: '< App / >',
+    options: [{
+      closingSlash: 'always',
+      beforeSelfClosing: 'always',
+      afterOpening: 'always'
+    }]
+  }],
+
+  invalid: [{
+    code: '<App/>',
+    output: '<App />',
+    options: beforeSelfClosingOptions('always'),
+    errors: [
+      {message: 'A space is required before closing bracket'}
+    ]
+  }, {
+    code: '<App foo/>',
+    output: '<App foo />',
+    options: beforeSelfClosingOptions('always'),
+    errors: [
+      {message: 'A space is required before closing bracket'}
+    ]
+  }, {
+    code: '<App foo={bar}/>',
+    output: '<App foo={bar} />',
+    options: beforeSelfClosingOptions('always'),
+    errors: [
+      {message: 'A space is required before closing bracket'}
+    ]
+  }, {
+    code: '<App {...props}/>',
+    output: '<App {...props} />',
+    options: beforeSelfClosingOptions('always'),
+    errors: [
+      {message: 'A space is required before closing bracket'}
+    ]
+  }, {
+    code: '<App />',
+    output: '<App/>',
+    options: beforeSelfClosingOptions('never'),
+    errors: [
+      {message: 'A space is forbidden before closing bracket'}
+    ]
+  }, {
+    code: '<App foo />',
+    output: '<App foo/>',
+    options: beforeSelfClosingOptions('never'),
+    errors: [
+      {message: 'A space is forbidden before closing bracket'}
+    ]
+  }, {
+    code: '<App foo={bar} />',
+    output: '<App foo={bar}/>',
+    options: beforeSelfClosingOptions('never'),
+    errors: [
+      {message: 'A space is forbidden before closing bracket'}
+    ]
+  }, {
+    code: '<App {...props} />',
+    output: '<App {...props}/>',
+    options: beforeSelfClosingOptions('never'),
+    errors: [
+      {message: 'A space is forbidden before closing bracket'}
+    ]
+  }, {
+    code: '<App/ >;',
+    output: '<App/>;',
+    errors: [{message: 'Whitespace is forbidden between `/` and `>`; write `/>`'}],
+    options: closingSlashOptions('never')
+  }, {
+    code: [
+      '<App/',
+      '>'
+    ].join('\n'),
+    output: '<App/>',
+    errors: [{message: 'Whitespace is forbidden between `/` and `>`; write `/>`'}],
+    options: closingSlashOptions('never')
+  }, {
+    code: '<div className="bar">< /div>;',
+    output: '<div className="bar"></div>;',
+    errors: [{message: 'Whitespace is forbidden between `<` and `/`; write `</`'}],
+    options: closingSlashOptions('never')
+  }, {
+    code: [
+      '<div className="bar"><',
+      '/div>;'
+    ].join('\n'),
+    output: '<div className="bar"></div>;',
+    errors: [{message: 'Whitespace is forbidden between `<` and `/`; write `</`'}],
+    options: closingSlashOptions('never')
+  }, {
+    code: '<App prop="foo"></App>',
+    output: '<App prop="foo">< /App>',
+    errors: [{message: 'Whitespace is required between `<` and `/`; write `< /`'}],
+    options: closingSlashOptions('always')
+  }, {
+    code: '<p/>',
+    output: '<p/ >',
+    errors: [{message: 'Whitespace is required between `/` and `>`; write `/ >`'}],
+    options: closingSlashOptions('always')
+  }, {
+    code: '< App/>',
+    output: '<App/>',
+    errors: [{message: 'A space is forbidden after opening bracket'}],
+    options: afterOpeningOptions('never')
+  }, {
+    code: '< App></App>',
+    output: '<App></App>',
+    errors: [{message: 'A space is forbidden after opening bracket'}],
+    options: afterOpeningOptions('never')
+  }, {
+    code: '<App></ App>',
+    output: '<App></App>',
+    errors: [{message: 'A space is forbidden after opening bracket'}],
+    options: afterOpeningOptions('never')
+  }, {
+    code: '< App></ App>',
+    output: '<App></App>',
+    errors: [
+      {message: 'A space is forbidden after opening bracket'},
+      {message: 'A space is forbidden after opening bracket'}
+    ],
+    options: afterOpeningOptions('never')
+  }, {
+    code: [
+      '<',
+      'App/>'
+    ].join('\n'),
+    output: '<App/>',
+    errors: [{message: 'A space is forbidden after opening bracket'}],
+    options: afterOpeningOptions('never')
+  }, {
+    code: '<App></ App>',
+    output: '< App></ App>',
+    errors: [{message: 'A space is required after opening bracket'}],
+    options: afterOpeningOptions('always')
+  }, {
+    code: '< App></App>',
+    output: '< App></ App>',
+    errors: [{message: 'A space is required after opening bracket'}],
+    options: afterOpeningOptions('always')
+  }, {
+    code: '<App></App>',
+    output: '< App></ App>',
+    errors: [
+      {message: 'A space is required after opening bracket'},
+      {message: 'A space is required after opening bracket'}
+    ],
+    options: afterOpeningOptions('always')
+  }, {
+    code: '<App/>',
+    output: '< App/>',
+    errors: [{message: 'A space is required after opening bracket'}],
+    options: afterOpeningOptions('always')
+  }, {
+    code: '< App/>',
+    output: '<App/>',
+    errors: [{message: 'A space is forbidden after opening bracket'}],
+    options: afterOpeningOptions('allow-multiline')
+  }]
+});