Repository: yannickcr/eslint-plugin-react
Pull request #770 (2016-08-15 21:58:11)

Add auto fix for self-closing-comp
If you would like comments removed just let me know. Thanks for the awesome project.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13428034..323faa6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,29 @@ All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
 
+## [6.1.2] - 2016-08-17
+### Fixed
+* Fix nested spread handling in `no-danger-with-children` ([#771][] @petersendidit)
+
+### Changed
+* Documentation improvements
+
+[6.1.2]: https://github.com/yannickcr/eslint-plugin-react/compare/v6.1.1...v6.1.2
+[#771]: https://github.com/yannickcr/eslint-plugin-react/issues/771
+
+## [6.1.1] - 2016-08-16
+### Fixed
+* Fix `prop-types` on annotated components ([#766][])
+* Fix `no-danger-with-children` spread support ([#767][] @petersendidit)
+
+### Changed
+* Documentation improvements ([#769][] @daltones)
+
+[6.1.1]: https://github.com/yannickcr/eslint-plugin-react/compare/v6.1.0...v6.1.1
+[#766]: https://github.com/yannickcr/eslint-plugin-react/issues/766
+[#767]: https://github.com/yannickcr/eslint-plugin-react/issues/767
+[#769]: https://github.com/yannickcr/eslint-plugin-react/pull/769
+
 ## [6.1.0] - 2016-08-14
 ### Added
 * Add `React.PureComponent` support ([#737][])
diff --git a/README.md b/README.md
index 376951cf..3a9cdf9e 100644
--- a/README.md
+++ b/README.md
@@ -100,10 +100,9 @@ Finally, enable all of the rules that you would like to use.  Use [our preset](#
 * [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless React Components to be written as a pure function
 * [react/prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition
 * [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing `React` when using JSX
-* [react/require-extension](docs/rules/require-extension.md): Restrict file extensions that may be required
 * [react/require-optimization](docs/rules/require-optimization.md): Enforce React components to have a shouldComponentUpdate method
 * [react/require-render-return](docs/rules/require-render-return.md): Enforce ES5 or ES6 class for returning value in render function
-* [react/self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children
+* [react/self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children (fixable)
 * [react/sort-comp](docs/rules/sort-comp.md): Enforce component methods order
 * [react/sort-prop-types](docs/rules/sort-prop-types.md): Enforce propTypes declarations alphabetical sorting
 
diff --git a/docs/rules/jsx-space-before-closing.md b/docs/rules/jsx-space-before-closing.md
index 6b162f35..a58096d0 100644
--- a/docs/rules/jsx-space-before-closing.md
+++ b/docs/rules/jsx-space-before-closing.md
@@ -2,6 +2,8 @@
 
 Enforce or forbid spaces before the closing bracket of self-closing JSX elements.
 
+**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.
+
 ## Rule Details
 
 This rule checks if there is one or more spaces before the closing bracket of self-closing JSX elements.
diff --git a/docs/rules/self-closing-comp.md b/docs/rules/self-closing-comp.md
index 938a7b51..156c9cbd 100644
--- a/docs/rules/self-closing-comp.md
+++ b/docs/rules/self-closing-comp.md
@@ -2,6 +2,8 @@
 
 Components without children can be self-closed to avoid unnecessary extra closing tag.
 
+**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.
+
 ## Rule Details
 
 The following patterns are considered warnings:
diff --git a/docs/rules/sort-comp.md b/docs/rules/sort-comp.md
index da7f07c1..62c005be 100644
--- a/docs/rules/sort-comp.md
+++ b/docs/rules/sort-comp.md
@@ -89,6 +89,7 @@ The default configuration is:
 * `lifecycle` is referring to the `lifecycle` group defined in `groups`.
 * `everything-else` is a special group that match all the methods that do not match any of the other groups.
 * `render` is referring to the `render` method.
+* `type-annotations`. This group is not speficied by default, but can be used enforce flow annotations to be at the top.
 
 You can override this configuration to match your needs.
 
@@ -171,6 +172,48 @@ var Hello = React.createClass({
 });
 ```
 
+If you want to flow annotations to be at the top:
+
+```js
+"react/sort-comp": [1, {
+  order: [
+    'type-annotations',
+    'static-methods',
+    'lifecycle',
+    'everything-else',
+    'render',
+  ],
+}]
+```
+
+With the above configuration, the following patterns are considered warnings:
+
+```js
+class Hello extends React.Component<any, Props, void> {
+  onClick() { this._someElem = true; }
+  props: Props;
+  _someElem: bool;
+  render() {
+    return <div>Hello</div>;
+  }
+}
+```
+
+With the above configuration, the following patterns are not considered warnings:
+
+```js
+type Props = {};
+class Hello extends React.Component<any, Props, void> {
+  props: Props;
+  _someElem: bool;
+  onClick() { this._someElem = true; }
+  render() {
+    return <div>Hello</div>;
+  }
+}
+```
+
+
 ## When Not To Use It
 
 This rule is a formatting preference and not following it won't negatively affect the quality of your code. If components organisation isn't a part of your coding standards, then you can leave this rule off.
diff --git a/lib/rules/no-danger-with-children.js b/lib/rules/no-danger-with-children.js
index 8482b0fc..98bcb73c 100644
--- a/lib/rules/no-danger-with-children.js
+++ b/lib/rules/no-danger-with-children.js
@@ -19,14 +19,30 @@ module.exports = {
     schema: [] // no options
   },
   create: function(context) {
+    function findSpreadVariable(name) {
+      return variableUtil.variablesInScope(context).find(function (item) {
+        return item.name === name;
+      });
+    }
     /**
      * Takes a ObjectExpression and returns the value of the prop if it has it
      * @param {object} node - ObjectExpression node
      * @param {string} propName - name of the prop to look for
      */
     function findObjectProp(node, propName) {
+      if (!node.properties) {
+        return false;
+      }
       return node.properties.find(function(prop) {
-        return prop.key.name === propName;
+        if (prop.type === 'Property') {
+          return prop.key.name === propName;
+        } else if (prop.type === 'ExperimentalSpreadProperty') {
+          var variable = findSpreadVariable(prop.argument.name);
+          if (variable && variable.defs[0].node.init) {
+            return findObjectProp(variable.defs[0].node.init, propName);
+          }
+        }
+        return false;
       });
     }
 
@@ -39,9 +55,7 @@ module.exports = {
       var attributes = node.openingElement.attributes;
       return attributes.find(function (attribute) {
         if (attribute.type === 'JSXSpreadAttribute') {
-          var variable = variableUtil.variablesInScope(context).find(function (item) {
-            return item.name === attribute.argument.name;
-          });
+          var variable = findSpreadVariable(attribute.argument.name);
           if (variable && variable.defs[0].node.init) {
             return findObjectProp(variable.defs[0].node.init, propName);
           }
diff --git a/lib/rules/self-closing-comp.js b/lib/rules/self-closing-comp.js
index b04f5fdd..3835336f 100644
--- a/lib/rules/self-closing-comp.js
+++ b/lib/rules/self-closing-comp.js
@@ -15,6 +15,7 @@ module.exports = {
       category: 'Stylistic Issues',
       recommended: false
     },
+    fixable: 'code',
 
     schema: [{
       type: 'object',
@@ -75,7 +76,17 @@ module.exports = {
         }
         context.report({
           node: node,
-          message: 'Empty components are self-closing'
+          message: 'Empty components are self-closing',
+          fix: function(fixer) {
+            // Represents the last character of the JSXOpeningElement, the '>' character
+            var openingElementEnding = node.end - 1;
+            // Represents the last character of the JSXClosingElement, the '>' character
+            var closingElementEnding = node.parent.closingElement.end;
+
+            // Replace />.*<\/.*>/ with '/>'
+            var range = [openingElementEnding, closingElementEnding];
+            return fixer.replaceTextRange(range, ' />');
+          }
         });
       }
     };
diff --git a/lib/rules/sort-comp.js b/lib/rules/sort-comp.js
index 81c36371..0474a1df 100644
--- a/lib/rules/sort-comp.js
+++ b/lib/rules/sort-comp.js
@@ -136,6 +136,15 @@ module.exports = {
         }
       }
 
+      if (method.typeAnnotation) {
+        for (i = 0, j = methodsOrder.length; i < j; i++) {
+          if (methodsOrder[i] === 'type-annotations') {
+            indexes.push(i);
+            break;
+          }
+        }
+      }
+
       // Either this is not a static method or static methods are not specified
       // in the methodsOrder.
       if (indexes.length === 0) {
@@ -353,7 +362,8 @@ module.exports = {
       var propertiesInfos = properties.map(function(node) {
         return {
           name: getPropertyName(node),
-          static: node.static
+          static: node.static,
+          typeAnnotation: !!node.typeAnnotation && node.value === null
         };
       });
 
diff --git a/package.json b/package.json
index 3dd3edad..ee9fd541 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-react",
-  "version": "6.1.0",
+  "version": "6.1.2",
   "author": "Yannick Croissant <yannick.croissant+npm@gmail.com>",
   "description": "React specific linting rules for ESLint",
   "main": "index.js",


Test patch:
diff --git a/tests/lib/rules/no-danger-with-children.js b/tests/lib/rules/no-danger-with-children.js
index cc045ae5..e97d57ef 100644
--- a/tests/lib/rules/no-danger-with-children.js
+++ b/tests/lib/rules/no-danger-with-children.js
@@ -14,6 +14,7 @@ var RuleTester = require('eslint').RuleTester;
 var parserOptions = {
   ecmaVersion: 6,
   ecmaFeatures: {
+    experimentalObjectRestSpread: true,
     jsx: true
   }
 };
@@ -46,7 +47,16 @@ ruleTester.run('no-danger-with-children', rule, {
     },
     {
       code: [
-        'const props = { children: "Children" };',
+        'const moreProps = { className: "eslint" };',
+        'const props = { children: "Children", ...moreProps };',
+        '<div {...props} />'
+      ].join('\n'),
+      parserOptions: parserOptions
+    },
+    {
+      code: [
+        'const otherProps = { children: "Children" };',
+        'const { a, b, ...props } = otherProps;',
         '<div {...props} />'
       ].join('\n'),
       parserOptions: parserOptions
@@ -102,8 +112,7 @@ ruleTester.run('no-danger-with-children', rule, {
     {
       code: [
         'const props = { children: "Children", dangerouslySetInnerHTML: { __html: "HTML" } };',
-        '<div {...props} />',
-        '//foobar'
+        '<div {...props} />'
       ].join('\n'),
       errors: [{message: 'Only set one of `children` or `props.dangerouslySetInnerHTML`'}],
       parserOptions: parserOptions
@@ -185,6 +194,16 @@ ruleTester.run('no-danger-with-children', rule, {
       ].join('\n'),
       errors: [{message: 'Only set one of `children` or `props.dangerouslySetInnerHTML`'}],
       parserOptions: parserOptions
+    },
+    {
+      code: [
+        'const moreProps = { children: "Children" };',
+        'const otherProps = { ...moreProps };',
+        'const props = { ...otherProps, dangerouslySetInnerHTML: { __html: "HTML" } };',
+        'React.createElement("div", props);'
+      ].join('\n'),
+      errors: [{message: 'Only set one of `children` or `props.dangerouslySetInnerHTML`'}],
+      parserOptions: parserOptions
     }
   ]
 });
diff --git a/tests/lib/rules/self-closing-comp.js b/tests/lib/rules/self-closing-comp.js
index d1c00e07..5cb9202e 100644
--- a/tests/lib/rules/self-closing-comp.js
+++ b/tests/lib/rules/self-closing-comp.js
@@ -107,12 +107,14 @@ ruleTester.run('self-closing-comp', rule, {
   invalid: [
     {
       code: 'var contentContainer = <div className="content"></div>;',
+      output: 'var contentContainer = <div className="content" />;',
       parserOptions: parserOptions,
       errors: [{
         message: 'Empty components are self-closing'
       }]
     }, {
       code: 'var contentContainer = <div className="content"></div>;',
+      output: 'var contentContainer = <div className="content" />;',
       options: [],
       parserOptions: parserOptions,
       errors: [{
@@ -120,18 +122,21 @@ ruleTester.run('self-closing-comp', rule, {
       }]
     }, {
       code: 'var HelloJohn = <Hello name="John"></Hello>;',
+      output: 'var HelloJohn = <Hello name="John" />;',
       parserOptions: parserOptions,
       errors: [{
         message: 'Empty components are self-closing'
       }]
     }, {
       code: 'var HelloJohn = <Hello name="John">\n</Hello>;',
+      output: 'var HelloJohn = <Hello name="John" />;',
       parserOptions: parserOptions,
       errors: [{
         message: 'Empty components are self-closing'
       }]
     }, {
       code: 'var HelloJohn = <Hello name="John"> </Hello>;',
+      output: 'var HelloJohn = <Hello name="John" />;',
       parserOptions: parserOptions,
       errors: [{
         message: 'Empty components are self-closing'
@@ -139,6 +144,7 @@ ruleTester.run('self-closing-comp', rule, {
     },
     {
       code: 'var HelloJohn = <Hello name="John"></Hello>;',
+      output: 'var HelloJohn = <Hello name="John" />;',
       options: [],
       parserOptions: parserOptions,
       errors: [{
@@ -146,6 +152,7 @@ ruleTester.run('self-closing-comp', rule, {
       }]
     }, {
       code: 'var HelloJohn = <Hello name="John">\n</Hello>;',
+      output: 'var HelloJohn = <Hello name="John" />;',
       options: [],
       parserOptions: parserOptions,
       errors: [{
@@ -153,6 +160,7 @@ ruleTester.run('self-closing-comp', rule, {
       }]
     }, {
       code: 'var HelloJohn = <Hello name="John"> </Hello>;',
+      output: 'var HelloJohn = <Hello name="John" />;',
       options: [],
       parserOptions: parserOptions,
       errors: [{
@@ -160,6 +168,7 @@ ruleTester.run('self-closing-comp', rule, {
       }]
     }, {
       code: 'var contentContainer = <div className="content"></div>;',
+      output: 'var contentContainer = <div className="content" />;',
       options: [{html: true}],
       parserOptions: parserOptions,
       errors: [{
@@ -167,6 +176,7 @@ ruleTester.run('self-closing-comp', rule, {
       }]
     }, {
       code: 'var contentContainer = <div className="content">\n</div>;',
+      output: 'var contentContainer = <div className="content" />;',
       options: [{html: true}],
       parserOptions: parserOptions,
       errors: [{
@@ -174,6 +184,7 @@ ruleTester.run('self-closing-comp', rule, {
       }]
     }, {
       code: 'var contentContainer = <div className="content"> </div>;',
+      output: 'var contentContainer = <div className="content" />;',
       options: [{html: true}],
       parserOptions: parserOptions,
       errors: [{
diff --git a/tests/lib/rules/sort-comp.js b/tests/lib/rules/sort-comp.js
index bb7349ea..83d13581 100644
--- a/tests/lib/rules/sort-comp.js
+++ b/tests/lib/rules/sort-comp.js
@@ -203,6 +203,51 @@ ruleTester.run('sort-comp', rule, {
       '});'
     ].join('\n'),
     parser: 'babel-eslint'
+  }, {
+    // Type Annotations should be first
+    code: [
+      'class Hello extends React.Component {',
+      '  props: { text: string };',
+      '  constructor() {}',
+      '  render() {',
+      '    return <div>{this.props.text}</div>;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    parserOptions: parserOptions,
+    options: [{
+      order: [
+        'type-annotations',
+        'static-methods',
+        'lifecycle',
+        'everything-else',
+        'render'
+      ]
+    }]
+  }, {
+    // Properties with Type Annotations should not be at the top
+    code: [
+      'class Hello extends React.Component {',
+      '  props: { text: string };',
+      '  constructor() {}',
+      '  state: Object = {};',
+      '  render() {',
+      '    return <div>{this.props.text}</div>;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    parserOptions: parserOptions,
+    options: [{
+      order: [
+        'type-annotations',
+        'static-methods',
+        'lifecycle',
+        'everything-else',
+        'render'
+      ]
+    }]
   }],
 
   invalid: [{
@@ -273,5 +318,65 @@ ruleTester.run('sort-comp', rule, {
     parser: 'babel-eslint',
     parserOptions: parserOptions,
     errors: [{message: 'render should be placed after displayName'}]
+  }, {
+    // Type Annotations should not be at the top by default
+    code: [
+      'class Hello extends React.Component {',
+      '  props: { text: string };',
+      '  constructor() {}',
+      '  state: Object = {};',
+      '  render() {',
+      '    return <div>{this.props.text}</div>;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    parserOptions: parserOptions,
+    errors: [{message: 'props should be placed after state'}]
+  }, {
+    // Type Annotations should be first
+    code: [
+      'class Hello extends React.Component {',
+      '  constructor() {}',
+      '  props: { text: string };',
+      '  render() {',
+      '    return <div>{this.props.text}</div>;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    errors: [{message: 'constructor should be placed after props'}],
+    options: [{
+      order: [
+        'type-annotations',
+        'static-methods',
+        'lifecycle',
+        'everything-else',
+        'render'
+      ]
+    }]
+  }, {
+    // Properties with Type Annotations should not be at the top
+    code: [
+      'class Hello extends React.Component {',
+      '  props: { text: string };',
+      '  state: Object = {};',
+      '  constructor() {}',
+      '  render() {',
+      '    return <div>{this.props.text}</div>;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    errors: [{message: 'state should be placed after constructor'}],
+    options: [{
+      order: [
+        'type-annotations',
+        'static-methods',
+        'lifecycle',
+        'everything-else',
+        'render'
+      ]
+    }]
   }]
 });