Repository: yannickcr/eslint-plugin-react
Pull request #1891 (2018-07-20 15:11:02)

Add fixer for sort-prop-types
This adds adds a fixer for the `react/sort-prop-types` rule. 

It fixes ordering issues including the `sortShapeProp` and the `ignoreCase` option.

~~It doesn't work yet for `callbacksLast` or `requiredFirst`, but that could be added later. (Right now those errors would just be ignored)~~


Patch:
diff --git a/docs/rules/sort-prop-types.md b/docs/rules/sort-prop-types.md
index cc48bc10..4577a604 100644
--- a/docs/rules/sort-prop-types.md
+++ b/docs/rules/sort-prop-types.md
@@ -2,6 +2,9 @@
 
 Some developers prefer to sort propTypes declarations alphabetically to be able to find necessary declaration easier at the later time. Others feel that it adds complexity and becomes burden to maintain.
 
+**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.
+
+
 ## Rule Details
 
 This rule checks all components and verifies that all propTypes declarations are sorted alphabetically. A spread attribute resets the verification. The default configuration of the rule is case-sensitive.
diff --git a/lib/rules/sort-prop-types.js b/lib/rules/sort-prop-types.js
index 174ee43c..022b7f7c 100644
--- a/lib/rules/sort-prop-types.js
+++ b/lib/rules/sort-prop-types.js
@@ -20,6 +20,8 @@ module.exports = {
       url: docsUrl('sort-prop-types')
     },
 
+    fixable: 'code',
+
     schema: [{
       type: 'object',
       properties: {
@@ -71,6 +73,46 @@ module.exports = {
       );
     }
 
+    function getShapeProperties (node) {
+      return node.arguments && node.arguments[0] && node.arguments[0].properties;
+    }
+
+    function sorter(a, b) {
+      let aKey = getKey(a);
+      let bKey = getKey(b);
+      if (requiredFirst) {
+        if (isRequiredProp(a) && !isRequiredProp(b)) {
+          return -1;
+        }
+        if (!isRequiredProp(a) && isRequiredProp(b)) {
+          return 1;
+        }
+      }
+
+      if (callbacksLast) {
+        if (isCallbackPropName(aKey) && !isCallbackPropName(bKey)) {
+          return 1;
+        }
+        if (!isCallbackPropName(aKey) && isCallbackPropName(bKey)) {
+          return -1;
+        }
+      }
+
+      if (ignoreCase) {
+        aKey = aKey.toLowerCase();
+        bKey = bKey.toLowerCase();
+      }
+
+      if (aKey < bKey) {
+        return -1;
+      }
+      if (aKey > bKey) {
+        return 1;
+      }
+      return 0;
+    }
+
+
     /**
      * Checks if propTypes declarations are sorted
      * @param {Array} declarations The array of AST nodes being checked.
@@ -83,6 +125,48 @@ module.exports = {
         return;
       }
 
+      function fix(fixer) {
+        function sortInSource(allNodes, source) {
+          const originalSource = source;
+          const nodeGroups = allNodes.reduce((acc, curr) => {
+            if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') {
+              acc.push([]);
+            } else {
+              acc[acc.length - 1].push(curr);
+            }
+            return acc;
+          }, [[]]);
+
+          nodeGroups.forEach(nodes => {
+            const sortedAttributes = nodes.slice().sort(sorter);
+
+            for (let i = nodes.length - 1; i >= 0; i--) {
+              const sortedAttr = sortedAttributes[i];
+              const attr = nodes[i];
+              let sortedAttrText = sourceCode.getText(sortedAttr);
+              if (sortShapeProp && isShapeProp(sortedAttr.value)) {
+                const shape = getShapeProperties(sortedAttr.value);
+                if (shape) {
+                  const attrSource = sortInSource(
+                    shape,
+                    originalSource
+                  );
+                  sortedAttrText = attrSource.slice(sortedAttr.range[0], sortedAttr.range[1]);
+                }
+              }
+              source = `${source.slice(0, attr.range[0])}${sortedAttrText}${source.slice(attr.range[1])}`;
+            }
+          });
+          return source;
+        }
+
+        const source = sortInSource(declarations, context.getSourceCode().getText());
+
+        const rangeStart = declarations[0].range[0];
+        const rangeEnd = declarations[declarations.length - 1].range[1];
+        return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd));
+      }
+
       declarations.reduce((prev, curr, idx, decls) => {
         if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') {
           return decls[idx + 1];
@@ -109,7 +193,8 @@ module.exports = {
             // Encountered a non-required prop after a required prop
             context.report({
               node: curr,
-              message: 'Required prop types must be listed before all other prop types'
+              message: 'Required prop types must be listed before all other prop types',
+              fix
             });
             return curr;
           }
@@ -124,7 +209,8 @@ module.exports = {
             // Encountered a non-callback prop after a callback prop
             context.report({
               node: prev,
-              message: 'Callback prop types must be listed after all other prop types'
+              message: 'Callback prop types must be listed after all other prop types',
+              fix
             });
             return prev;
           }
@@ -133,7 +219,8 @@ module.exports = {
         if (currentPropName < prevPropName) {
           context.report({
             node: curr,
-            message: 'Prop types declarations should be sorted alphabetically'
+            message: 'Prop types declarations should be sorted alphabetically',
+            fix
           });
           return prev;
         }
@@ -166,7 +253,7 @@ module.exports = {
 
     return {
       CallExpression: function(node) {
-        if (!sortShapeProp || !isShapeProp(node) || (!node.arguments && !node.arguments[0])) {
+        if (!sortShapeProp || !isShapeProp(node) || !(node.arguments && node.arguments[0])) {
           return;
         }
         checkSorted(node.arguments[0].properties);


Test patch:
diff --git a/tests/lib/rules/sort-prop-types.js b/tests/lib/rules/sort-prop-types.js
index 26ccf896..4b5c4d42 100644
--- a/tests/lib/rules/sort-prop-types.js
+++ b/tests/lib/rules/sort-prop-types.js
@@ -401,7 +401,18 @@ ruleTester.run('sort-prop-types', rule, {
       line: 4,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'var First = createReactClass({',
+      '  propTypes: {',
+      '    a: PropTypes.any,',
+      '    z: PropTypes.string',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'var First = createReactClass({',
@@ -419,7 +430,18 @@ ruleTester.run('sort-prop-types', rule, {
       line: 4,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'var First = createReactClass({',
+      '  propTypes: {',
+      '    Z: PropTypes.any,',
+      '    z: PropTypes.any',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'var First = createReactClass({',
@@ -440,7 +462,18 @@ ruleTester.run('sort-prop-types', rule, {
       line: 4,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'var First = createReactClass({',
+      '  propTypes: {',
+      '    a: PropTypes.any,',
+      '    Z: PropTypes.any',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'var First = createReactClass({',
@@ -455,7 +488,20 @@ ruleTester.run('sort-prop-types', rule, {
       '  }',
       '});'
     ].join('\n'),
-    errors: 2
+    errors: 2,
+    output: [
+      'var First = createReactClass({',
+      '  propTypes: {',
+      '    A: PropTypes.any,',
+      '    Z: PropTypes.string,',
+      '    a: PropTypes.any,',
+      '    z: PropTypes.string',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'var First = createReactClass({',
@@ -477,7 +523,27 @@ ruleTester.run('sort-prop-types', rule, {
       '  }',
       '});'
     ].join('\n'),
-    errors: 2
+    errors: 2,
+    output: [
+      'var First = createReactClass({',
+      '  propTypes: {',
+      '    Zz: PropTypes.string,',
+      '    a: PropTypes.any',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});',
+      'var Second = createReactClass({',
+      '  propTypes: {',
+      '    ZZ: PropTypes.string,',
+      '    aAA: PropTypes.any',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'class First extends React.Component {',
@@ -499,7 +565,27 @@ ruleTester.run('sort-prop-types', rule, {
       '    ZZ: PropTypes.string',
       '};'
     ].join('\n'),
-    errors: 2
+    errors: 2,
+    output: [
+      'class First extends React.Component {',
+      '  render() {',
+      '    return <div />;',
+      '  }',
+      '}',
+      'First.propTypes = {',
+      '    bb: PropTypes.string,',
+      '    yy: PropTypes.any',
+      '};',
+      'class Second extends React.Component {',
+      '  render() {',
+      '    return <div />;',
+      '  }',
+      '}',
+      'Second.propTypes = {',
+      '    ZZ: PropTypes.string,',
+      '    aAA: PropTypes.any',
+      '};'
+    ].join('\n')
   }, {
     code: [
       'class Component extends React.Component {',
@@ -514,7 +600,19 @@ ruleTester.run('sort-prop-types', rule, {
       '}'
     ].join('\n'),
     parser: 'babel-eslint',
-    errors: 2
+    errors: 2,
+    output: [
+      'class Component extends React.Component {',
+      '  static propTypes = {',
+      '    a: PropTypes.any,',
+      '    y: PropTypes.any,',
+      '    z: PropTypes.any',
+      '  };',
+      '  render() {',
+      '    return <div />;',
+      '  }',
+      '}'
+    ].join('\n')
   }, {
     code: [
       'class Component extends React.Component {',
@@ -532,7 +630,19 @@ ruleTester.run('sort-prop-types', rule, {
     settings: {
       propWrapperFunctions: ['forbidExtraProps']
     },
-    errors: 2
+    errors: 2,
+    output: [
+      'class Component extends React.Component {',
+      '  static propTypes = forbidExtraProps({',
+      '    a: PropTypes.any,',
+      '    y: PropTypes.any,',
+      '    z: PropTypes.any',
+      '  });',
+      '  render() {',
+      '    return <div />;',
+      '  }',
+      '}'
+    ].join('\n')
   }, {
     code: [
       'var First = createReactClass({',
@@ -555,7 +665,20 @@ ruleTester.run('sort-prop-types', rule, {
       line: 6,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'var First = createReactClass({',
+      '  propTypes: {',
+      '    a: PropTypes.any,',
+      '    z: PropTypes.string,',
+      '    onBar: PropTypes.func,',
+      '    onFoo: PropTypes.func',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'class Component extends React.Component {',
@@ -579,7 +702,20 @@ ruleTester.run('sort-prop-types', rule, {
       line: 6,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'class Component extends React.Component {',
+      '  static propTypes = {',
+      '    a: PropTypes.any,',
+      '    z: PropTypes.string,',
+      '    onBar: PropTypes.func,',
+      '    onFoo: PropTypes.func',
+      '  };',
+      '  render() {',
+      '    return <div />;',
+      '  }',
+      '}'
+    ].join('\n')
   }, {
     code: [
       'class First extends React.Component {',
@@ -602,7 +738,20 @@ ruleTester.run('sort-prop-types', rule, {
       line: 10,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'class First extends React.Component {',
+      '  render() {',
+      '    return <div />;',
+      '  }',
+      '}',
+      'First.propTypes = {',
+      '    a: PropTypes.any,',
+      '    z: PropTypes.string,',
+      '    onBar: PropTypes.func,',
+      '    onFoo: PropTypes.func',
+      '};'
+    ].join('\n')
   }, {
     code: [
       'class First extends React.Component {',
@@ -628,7 +777,20 @@ ruleTester.run('sort-prop-types', rule, {
       line: 10,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'class First extends React.Component {',
+      '  render() {',
+      '    return <div />;',
+      '  }',
+      '}',
+      'First.propTypes = forbidExtraProps({',
+      '    a: PropTypes.any,',
+      '    z: PropTypes.string,',
+      '    onBar: PropTypes.func,',
+      '    onFoo: PropTypes.func',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'const First = (props) => <div />;',
@@ -646,7 +808,15 @@ ruleTester.run('sort-prop-types', rule, {
       line: 4,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'const First = (props) => <div />;',
+      'const propTypes = {',
+      '    a: PropTypes.any,',
+      '    z: PropTypes.string,',
+      '};',
+      'First.propTypes = forbidExtraProps(propTypes);'
+    ].join('\n')
   }, {
     code: [
       'const First = (props) => <div />;',
@@ -664,7 +834,15 @@ ruleTester.run('sort-prop-types', rule, {
       line: 4,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'const First = (props) => <div />;',
+      'const propTypes = {',
+      '    a: PropTypes.any,',
+      '    z: PropTypes.string,',
+      '};',
+      'First.propTypes = propTypes;'
+    ].join('\n')
   }, {
     code: [
       'var First = createReactClass({',
@@ -687,7 +865,20 @@ ruleTester.run('sort-prop-types', rule, {
       line: 5,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'var First = createReactClass({',
+      '  propTypes: {',
+      '    a: PropTypes.any,',
+      '    z: PropTypes.string,',
+      '    onBar: PropTypes.func,',
+      '    onFoo: PropTypes.func',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'var First = createReactClass({',
@@ -709,7 +900,19 @@ ruleTester.run('sort-prop-types', rule, {
       line: 4,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'var First = createReactClass({',
+      '  propTypes: {',
+      '    barRequired: PropTypes.string.isRequired,',
+      '    fooRequired: PropTypes.string.isRequired,',
+      '    a: PropTypes.any',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'var First = createReactClass({',
@@ -731,7 +934,19 @@ ruleTester.run('sort-prop-types', rule, {
       line: 4,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'var First = createReactClass({',
+      '  propTypes: {',
+      '    barRequired: PropTypes.string.isRequired,',
+      '    a: PropTypes.any,',
+      '    onFoo: PropTypes.func',
+      '  },',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
   }, {
     code: [
       'export default class ClassWithSpreadInPropTypes extends BaseClass {',
@@ -749,7 +964,49 @@ ruleTester.run('sort-prop-types', rule, {
       line: 6,
       column: 5,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'export default class ClassWithSpreadInPropTypes extends BaseClass {',
+      '  static propTypes = {',
+      '    b: PropTypes.string,',
+      '    ...a.propTypes,',
+      '    c: PropTypes.string,',
+      '    d: PropTypes.string',
+      '  }',
+      '}'
+    ].join('\n')
+  }, {
+    code: [
+      'export default class ClassWithSpreadInPropTypes extends BaseClass {',
+      '  static propTypes = {',
+      '    b: PropTypes.string,',
+      '    ...a.propTypes,',
+      '    f: PropTypes.string,',
+      '    d: PropTypes.string,',
+      '    ...e.propTypes,',
+      '    c: PropTypes.string',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 6,
+      column: 5,
+      type: 'Property'
+    }],
+    output: [
+      'export default class ClassWithSpreadInPropTypes extends BaseClass {',
+      '  static propTypes = {',
+      '    b: PropTypes.string,',
+      '    ...a.propTypes,',
+      '    d: PropTypes.string,',
+      '    f: PropTypes.string,',
+      '    ...e.propTypes,',
+      '    c: PropTypes.string',
+      '  }',
+      '}'
+    ].join('\n')
   }, {
     code: [
       'const propTypes = {',
@@ -766,7 +1023,17 @@ ruleTester.run('sort-prop-types', rule, {
       line: 3,
       column: 3,
       type: 'Property'
-    }]
+    }],
+    output: [
+      'const propTypes = {',
+      '  a: PropTypes.string,',
+      '  b: PropTypes.string,',
+      '};',
+      'const TextFieldLabel = (props) => {',
+      '  return <div />;',
+      '};',
+      'TextFieldLabel.propTypes = propTypes;'
+    ].join('\n')
   }, {
     code: `
       class Component extends React.Component {
@@ -797,7 +1064,91 @@ ruleTester.run('sort-prop-types', rule, {
       line: 13,
       column: 11,
       type: 'Property'
-    }]
+    }],
+    output: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        x: PropTypes.any,
+        y: PropTypes.any,
+        z: PropTypes.shape({
+          a: PropTypes.any,
+          b: PropTypes.bool,
+          c: PropTypes.any,
+        }),
+      };
+    `
+  }, {
+    code: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        x: PropTypes.any,
+        z: PropTypes.shape(),
+        y: PropTypes.any,
+      };
+    `,
+    options: [{
+      sortShapeProp: true
+    }],
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 10,
+      column: 9,
+      type: 'Property'
+    }],
+    output: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        x: PropTypes.any,
+        y: PropTypes.any,
+        z: PropTypes.shape(),
+      };
+    `
+  }, {
+    code: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        x: PropTypes.any,
+        z: PropTypes.shape(someType),
+        y: PropTypes.any,
+      };
+    `,
+    options: [{
+      sortShapeProp: true
+    }],
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 10,
+      column: 9,
+      type: 'Property'
+    }],
+    output: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        x: PropTypes.any,
+        y: PropTypes.any,
+        z: PropTypes.shape(someType),
+      };
+    `
   }, {
     code: `
       class Component extends React.Component {
@@ -844,7 +1195,24 @@ ruleTester.run('sort-prop-types', rule, {
       line: 14,
       column: 11,
       type: 'Property'
-    }]
+    }],
+    output: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        a: PropTypes.shape({
+          C: PropTypes.string,
+          a: PropTypes.any,
+          b: PropTypes.bool,
+          c: PropTypes.any,
+        }),
+        y: PropTypes.any,
+        z: PropTypes.any,
+      };
+    `
   }, {
     code: `
       class Component extends React.Component {
@@ -877,7 +1245,24 @@ ruleTester.run('sort-prop-types', rule, {
       line: 14,
       column: 11,
       type: 'Property'
-    }]
+    }],
+    output: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        x: PropTypes.any,
+        y: PropTypes.any,
+        z: PropTypes.shape({
+          a: PropTypes.any,
+          b: PropTypes.bool,
+          c: PropTypes.any,
+          C: PropTypes.string,
+        }),
+      };
+    `
   }, {
     code: `
       class Component extends React.Component {
@@ -905,7 +1290,24 @@ ruleTester.run('sort-prop-types', rule, {
       line: 12,
       column: 11,
       type: 'Property'
-    }]
+    }],
+    output: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        x: PropTypes.any,
+        y: PropTypes.any,
+        z: PropTypes.shape({
+          c: PropTypes.number.isRequired,
+          a: PropTypes.string,
+          b: PropTypes.any,
+          d: PropTypes.bool,
+        }),
+      };
+    `
   }, {
     code: `
       class Component extends React.Component {
@@ -939,7 +1341,25 @@ ruleTester.run('sort-prop-types', rule, {
       line: 14,
       column: 11,
       type: 'Property'
-    }]
+    }],
+    output: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        x: PropTypes.any,
+        y: PropTypes.any,
+        z: PropTypes.shape({
+          a: PropTypes.string,
+          b: PropTypes.any,
+          c: PropTypes.number.isRequired,
+          d: PropTypes.bool,
+          onFoo: PropTypes.func,
+        }),
+      };
+    `
   }, {
     code: `
       class Component extends React.Component {
@@ -973,7 +1393,26 @@ ruleTester.run('sort-prop-types', rule, {
       line: 16,
       column: 11,
       type: 'Property'
-    }]
+    }],
+    output: `
+      class Component extends React.Component {
+        render() {
+          return <div />;
+        }
+      }
+      Component.propTypes = {
+        x: PropTypes.any,
+        y: PropTypes.any,
+        z: PropTypes.shape({
+          a: PropTypes.string,
+          b: PropTypes.any,
+          c: PropTypes.number.isRequired,
+          ...otherPropTypes,
+          d: PropTypes.string,
+          f: PropTypes.bool,
+        }),
+      };
+    `
   }, {
     code: `
       class Component extends React.Component {
@@ -1015,6 +1454,22 @@ ruleTester.run('sort-prop-types', rule, {
       line: 9,
       column: 13,
       type: 'Property'
-    }]
+    }],
+    output: `
+      class Component extends React.Component {
+        static propTypes = {
+          a: PropTypes.shape({
+            a: PropTypes.any,
+            b: PropTypes.bool,
+            c: PropTypes.any,
+          }),
+          y: PropTypes.any,
+          z: PropTypes.any,
+        };
+        render() {
+          return <div />;
+        }
+      }
+    `
   }]
 });