Repository: jsx-eslint/eslint-plugin-react
Pull request #1861 (2018-06-29 06:22:21)

[New] add `sort-default-props` and deprecate `jsx-sort-default-props`
Pt. 1 of #1834


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc5c6bc5..0d8a066c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,8 +7,10 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 
 ### Added
 * [`hook-use-state`]: add `allowDestructuredState` option ([#3449][] @ljharb)
+* add [`sort-default-props`] and deprecate [`jsx-sort-default-props`] ([#1861][] @alexzherdev)
 
 [#3449]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3449
+[#1861]: https://github.com/jsx-eslint/eslint-plugin-react/pull/1861
 
 ## [7.31.9] - 2022.10.09
 
@@ -4023,6 +4025,7 @@ If you're still not using React 15 you can keep the old behavior by setting the
 [`require-render-return`]: docs/rules/require-render-return.md
 [`self-closing-comp`]: docs/rules/self-closing-comp.md
 [`sort-comp`]: docs/rules/sort-comp.md
+[`sort-default-props`]: docs/rules/sort-default-props.md
 [`sort-prop-types`]: docs/rules/sort-prop-types.md
 [`state-in-constructor`]: docs/rules/state-in-constructor.md
 [`static-property-placement`]: docs/rules/static-property-placement.md
diff --git a/README.md b/README.md
index a7c98e57..2862f522 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,7 @@ Enable the rules that you would like to use.
 | ✔ |  |  | [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) | Disallow extra closing tags for components without children |
 |  |  |  | [react/sort-comp](docs/rules/sort-comp.md) | Enforce component methods order |
+|  |  |  | [react/sort-default-props](docs/rules/sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting |
 |  |  |  | [react/sort-prop-types](docs/rules/sort-prop-types.md) | Enforce propTypes declarations alphabetical sorting |
 |  |  |  | [react/state-in-constructor](docs/rules/state-in-constructor.md) | Enforce class component state initialization style |
 |  |  |  | [react/static-property-placement](docs/rules/static-property-placement.md) | Enforces where React component static properties should be positioned. |
@@ -216,7 +217,7 @@ Enable the rules that you would like to use.
 |  |  |  | [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md) | Enforce PascalCase for user-defined JSX components |
 |  | 🔧 |  | [react/jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md) | Disallow multiple spaces between inline JSX props |
 |  |  |  | [react/jsx-props-no-spreading](docs/rules/jsx-props-no-spreading.md) | Disallow JSX prop spreading |
-|  |  |  | [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting |
+|  |  |  | [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md) | Enforce defaultProps declarations alphabetical sorting. ❌ This rule is deprecated. |
 |  | 🔧 |  | [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) | Enforce spacing before closing bracket in JSX. ❌ This rule is deprecated. |
 |  | 🔧 |  | [react/jsx-tag-spacing](docs/rules/jsx-tag-spacing.md) | Enforce whitespace in and around the JSX opening and closing brackets |
diff --git a/docs/rules/jsx-sort-default-props.md b/docs/rules/jsx-sort-default-props.md
index 6188e8d1..db6331ab 100644
--- a/docs/rules/jsx-sort-default-props.md
+++ b/docs/rules/jsx-sort-default-props.md
@@ -1,6 +1,6 @@
 # Enforce defaultProps declarations alphabetical sorting (react/jsx-sort-default-props)
 
-💼 This rule is enabled in the following [configs](https://github.com/jsx-eslint/eslint-plugin-react#shareable-configurations): `all`.
+❌ This rule is deprecated. Please use the [`sort-default-props`](./sort-default-props.md) rule instead.
 
 Some developers prefer to sort `defaultProps` declarations alphabetically to be able to find necessary declarations easier at a later time. Others feel that it adds complexity and becomes a burden to maintain.
 
diff --git a/docs/rules/sort-default-props.md b/docs/rules/sort-default-props.md
new file mode 100644
index 00000000..fd4bb07f
--- /dev/null
+++ b/docs/rules/sort-default-props.md
@@ -0,0 +1,187 @@
+# Enforce defaultProps declarations alphabetical sorting (react/sort-default-props)
+
+💼 This rule is enabled in the following [configs](https://github.com/jsx-eslint/eslint-plugin-react#shareable-configurations): `all`.
+
+Some developers prefer to sort `defaultProps` declarations alphabetically to be able to find necessary declarations easier at a later time. Others feel that it adds complexity and becomes a burden to maintain.
+
+## Rule Details
+
+This rule checks all components and verifies that all `defaultProps` declarations are sorted alphabetically. A spread attribute resets the verification. The default configuration of the rule is case-sensitive.
+
+The following patterns are considered warnings:
+
+```jsx
+var Component = createReactClass({
+...
+  getDefaultProps: function() {
+    return {
+      z: "z",
+      a: "a",
+      b: "b"
+    };
+  },
+...
+});
+
+class Component extends React.Component {
+  ...
+}
+Component.defaultProps = {
+  z: "z",
+  a: "a",
+  b: "b"
+};
+
+class Component extends React.Component {
+  static defaultProps = {
+    z: "z",
+    y: "y",
+    a: "a"
+  }
+  render() {
+    return <div />;
+  }
+}
+
+const Component = (props) => (...);
+Component.defaultProps = {
+  z: "z",
+  y: "y",
+  a: "a"
+};
+
+const defaults = {
+  b: "b"
+};
+const types = {
+  a: PropTypes.string,
+  b: PropTypes.string,
+  c: PropTypes.string'
+};
+function StatelessComponentWithSpreadInPropTypes({ a, b, c }) {
+  return <div>{a}{b}{c}</div>;
+}
+StatelessComponentWithSpreadInPropTypes.propTypes = types;
+StatelessComponentWithSpreadInPropTypes.defaultProps = {
+  c: "c",
+  a: "a",
+  ...defaults,
+};
+
+export default class ClassWithSpreadInPropTypes extends BaseClass {
+  static propTypes = {
+    a: PropTypes.string,
+    b: PropTypes.string,
+    c: PropTypes.string,
+    d: PropTypes.string,
+    e: PropTypes.string,
+    f: PropTypes.string
+  }
+  static defaultProps = {
+    b: "b",
+    a: "a",
+    ...c.defaultProps,
+    f: "f",
+    e: "e",
+    ...d.defaultProps
+  }
+}
+```
+
+The following patterns are considered okay and do **not** cause warnings:
+
+```jsx
+var Component = createReactClass({
+...
+  getDefaultProps: function() {
+    return {
+      a: "a",
+      b: "b",
+      c: "c"
+    };
+  },
+...
+});
+
+class Component extends React.Component {
+  ...
+}
+Component.defaultProps = {
+  a: "a",
+  b: "b",
+  c: "c"
+};
+
+class Component extends React.Component {
+  static defaultProps = {
+    a: PropTypes.any,
+    b: PropTypes.any,
+    c: PropTypes.any
+  }
+  render() {
+    return <div />;
+  }
+}
+
+const Component = (props) => (...);
+Component.defaultProps = {
+  a: "a",
+  y: "y",
+  z: "z"
+};
+
+const defaults = {
+  b: "b"
+};
+const types = {
+  a: PropTypes.string,
+  b: PropTypes.string,
+  c: PropTypes.string'
+};
+function StatelessComponentWithSpreadInPropTypes({ a, b, c }) {
+  return <div>{a}{b}{c}</div>;
+}
+StatelessComponentWithSpreadInPropTypes.propTypes = types;
+StatelessComponentWithSpreadInPropTypes.defaultProps = {
+  a: "a",
+  c: "c",
+  ...defaults,
+};
+
+export default class ClassWithSpreadInPropTypes extends BaseClass {
+  static propTypes = {
+    a: PropTypes.string,
+    b: PropTypes.string,
+    c: PropTypes.string,
+    d: PropTypes.string,
+    e: PropTypes.string,
+    f: PropTypes.string
+  }
+  static defaultProps = {
+    a: "a",
+    b: "b",
+    ...c.defaultProps,
+    e: "e",
+    f: "f",
+    ...d.defaultProps
+  }
+}
+```
+
+## Rule Options
+
+```js
+...
+"react/sort-default-props": [<enabled>, {
+  "ignoreCase": <boolean>,
+}]
+...
+```
+
+### `ignoreCase`
+
+When `true` the rule ignores the case-sensitivity of the declarations order.
+
+## When not to use
+
+This rule is a formatting preference and not following it won't negatively affect the quality of your code. If alphabetizing `defaultProps` declarations isn't a part of your coding standards, then you can leave this rule off.
diff --git a/index.js b/index.js
index be3c94a9..12c60874 100644
--- a/index.js
+++ b/index.js
@@ -98,6 +98,7 @@ const allRules = {
   'require-render-return': require('./lib/rules/require-render-return'),
   'self-closing-comp': require('./lib/rules/self-closing-comp'),
   'sort-comp': require('./lib/rules/sort-comp'),
+  'sort-default-props': require('./lib/rules/sort-default-props'),
   'sort-prop-types': require('./lib/rules/sort-prop-types'),
   'state-in-constructor': require('./lib/rules/state-in-constructor'),
   'static-property-placement': require('./lib/rules/static-property-placement'),
diff --git a/lib/rules/jsx-sort-default-props.js b/lib/rules/jsx-sort-default-props.js
index f7062b81..a9519763 100644
--- a/lib/rules/jsx-sort-default-props.js
+++ b/lib/rules/jsx-sort-default-props.js
@@ -1,6 +1,7 @@
 /**
  * @fileoverview Enforce default props alphabetical sorting
  * @author Vladimir Kattsov
+ * @deprecated
  */
 
 'use strict';
@@ -8,6 +9,9 @@
 const variableUtil = require('../util/variable');
 const docsUrl = require('../util/docsUrl');
 const report = require('../util/report');
+const log = require('../util/log');
+
+let isWarnedForDeprecation = false;
 
 // ------------------------------------------------------------------------------
 // Rule Definition
@@ -19,6 +23,7 @@ const messages = {
 
 module.exports = {
   meta: {
+    deprecated: true,
     docs: {
       description: 'Enforce defaultProps declarations alphabetical sorting',
       category: 'Stylistic Issues',
@@ -168,6 +173,15 @@ module.exports = {
 
         checkNode(node.parent.right);
       },
+
+      Program() {
+        if (isWarnedForDeprecation) {
+          return;
+        }
+
+        log('The react/jsx-sort-default-props rule is deprecated. It has been renamed to `react/sort-default-props`.');
+        isWarnedForDeprecation = true;
+      },
     };
   },
 };
diff --git a/lib/rules/sort-default-props.js b/lib/rules/sort-default-props.js
new file mode 100644
index 00000000..aae419ba
--- /dev/null
+++ b/lib/rules/sort-default-props.js
@@ -0,0 +1,174 @@
+/**
+ * @fileoverview Enforce default props alphabetical sorting
+ * @author Vladimir Kattsov
+ * @deprecated
+ */
+
+'use strict';
+
+const variableUtil = require('../util/variable');
+const docsUrl = require('../util/docsUrl');
+const report = require('../util/report');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+const messages = {
+  propsNotSorted: 'Default prop types declarations should be sorted alphabetically',
+};
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Enforce defaultProps declarations alphabetical sorting',
+      category: 'Stylistic Issues',
+      recommended: false,
+      url: docsUrl('sort-default-props'),
+    },
+    // fixable: 'code',
+
+    messages,
+
+    schema: [{
+      type: 'object',
+      properties: {
+        ignoreCase: {
+          type: 'boolean',
+        },
+      },
+      additionalProperties: false,
+    }],
+  },
+
+  create(context) {
+    const configuration = context.options[0] || {};
+    const ignoreCase = configuration.ignoreCase || false;
+
+    /**
+     * Get properties name
+     * @param {Object} node - Property.
+     * @returns {String} Property name.
+     */
+    function getPropertyName(node) {
+      if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
+        return node.key.name;
+      }
+      if (node.type === 'MemberExpression') {
+        return node.property.name;
+      // Special case for class properties
+      // (babel-eslint@5 does not expose property name so we have to rely on tokens)
+      }
+      if (node.type === 'ClassProperty') {
+        const tokens = context.getSourceCode().getFirstTokens(node, 2);
+        return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value;
+      }
+      return '';
+    }
+
+    /**
+     * Checks if the Identifier node passed in looks like a defaultProps declaration.
+     * @param   {ASTNode}  node The node to check. Must be an Identifier node.
+     * @returns {Boolean}       `true` if the node is a defaultProps declaration, `false` if not
+     */
+    function isDefaultPropsDeclaration(node) {
+      const propName = getPropertyName(node);
+      return (propName === 'defaultProps' || propName === 'getDefaultProps');
+    }
+
+    function getKey(node) {
+      return context.getSourceCode().getText(node.key || node.argument);
+    }
+
+    /**
+     * Find a variable by name in the current scope.
+     * @param  {string} name Name of the variable to look for.
+     * @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise.
+     */
+    function findVariableByName(name) {
+      const variable = variableUtil.variablesInScope(context).find((item) => item.name === name);
+
+      if (!variable || !variable.defs[0] || !variable.defs[0].node) {
+        return null;
+      }
+
+      if (variable.defs[0].node.type === 'TypeAlias') {
+        return variable.defs[0].node.right;
+      }
+
+      return variable.defs[0].node.init;
+    }
+
+    /**
+     * Checks if defaultProps declarations are sorted
+     * @param {Array} declarations The array of AST nodes being checked.
+     * @returns {void}
+     */
+    function checkSorted(declarations) {
+      // function fix(fixer) {
+      //   return propTypesSortUtil.fixPropTypesSort(fixer, context, declarations, ignoreCase);
+      // }
+
+      declarations.reduce((prev, curr, idx, decls) => {
+        if (/Spread(?:Property|Element)$/.test(curr.type)) {
+          return decls[idx + 1];
+        }
+
+        let prevPropName = getKey(prev);
+        let currentPropName = getKey(curr);
+
+        if (ignoreCase) {
+          prevPropName = prevPropName.toLowerCase();
+          currentPropName = currentPropName.toLowerCase();
+        }
+
+        if (currentPropName < prevPropName) {
+          report(context, messages.propsNotSorted, 'propsNotSorted', {
+            node: curr,
+            // fix
+          });
+
+          return prev;
+        }
+
+        return curr;
+      }, declarations[0]);
+    }
+
+    function checkNode(node) {
+      if (!node) {
+        return;
+      }
+      if (node.type === 'ObjectExpression') {
+        checkSorted(node.properties);
+      } else if (node.type === 'Identifier') {
+        const propTypesObject = findVariableByName(node.name);
+        if (propTypesObject && propTypesObject.properties) {
+          checkSorted(propTypesObject.properties);
+        }
+      }
+    }
+
+    // --------------------------------------------------------------------------
+    // Public API
+    // --------------------------------------------------------------------------
+
+    return {
+      'ClassProperty, PropertyDefinition'(node) {
+        if (!isDefaultPropsDeclaration(node)) {
+          return;
+        }
+
+        checkNode(node.value);
+      },
+
+      MemberExpression(node) {
+        if (!isDefaultPropsDeclaration(node)) {
+          return;
+        }
+
+        checkNode(node.parent.right);
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/sort-default-props.js b/tests/lib/rules/sort-default-props.js
new file mode 100644
index 00000000..8e3a3597
--- /dev/null
+++ b/tests/lib/rules/sort-default-props.js
@@ -0,0 +1,938 @@
+/**
+ * @fileoverview Tests for sort-default-props
+ * @author Vladimir Kattsov
+ */
+
+'use strict';
+
+// -----------------------------------------------------------------------------
+// Requirements
+// -----------------------------------------------------------------------------
+
+const babelEslintVersion = require('babel-eslint/package.json').version;
+const semver = require('semver');
+const RuleTester = require('eslint').RuleTester;
+
+const rule = require('../../../lib/rules/sort-default-props');
+
+const parsers = require('../../helpers/parsers');
+
+const parserOptions = {
+  ecmaVersion: 2018,
+  sourceType: 'module',
+  ecmaFeatures: {
+    jsx: true,
+  },
+};
+
+// -----------------------------------------------------------------------------
+// Tests
+// -----------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions });
+ruleTester.run('sort-default-props', rule, {
+  valid: parsers.all([].concat(
+    {
+      code: `
+        var First = createReactClass({
+          render: function() {
+            return <div />;
+          }
+        });
+      `,
+    },
+    {
+      code: `
+        var First = createReactClass({
+          propTypes: {
+            A: PropTypes.any,
+            Z: PropTypes.string,
+            a: PropTypes.any,
+            z: PropTypes.string
+          },
+          getDefaultProps: function() {
+            return {
+              A: "A",
+              Z: "Z",
+              a: "a",
+              z: "z"
+            };
+          },
+          render: function() {
+            return <div />;
+          }
+        });
+      `,
+    },
+    {
+      code: `
+        var First = createReactClass({
+          propTypes: {
+            a: PropTypes.any,
+            A: PropTypes.any,
+            z: PropTypes.string,
+            Z: PropTypes.string
+          },
+          getDefaultProps: function() {
+            return {
+              a: "a",
+              A: "A",
+              z: "z",
+              Z: "Z"
+            };
+          },
+          render: function() {
+            return <div />;
+          }
+        });
+      `,
+      options: [{ ignoreCase: true }],
+    },
+    {
+      code: `
+        var First = createReactClass({
+          propTypes: {
+            a: PropTypes.any,
+            z: PropTypes.string
+          },
+          getDefaultProps: function() {
+            return {
+              a: "a",
+              z: "z"
+            };
+          },
+          render: function() {
+            return <div />;
+          }
+        });
+        var Second = createReactClass({
+          propTypes: {
+            AA: PropTypes.any,
+            ZZ: PropTypes.string
+          },
+          getDefaultProps: function() {
+            return {
+              AA: "AA",
+              ZZ: "ZZ"
+            };
+          },
+          render: function() {
+            return <div />;
+          }
+        });
+      `,
+    },
+    {
+      code: `
+        class First extends React.Component {
+          render() {
+            return <div />;
+          }
+        }
+        First.propTypes = {
+          a: PropTypes.string,
+          z: PropTypes.string
+        };
+        First.propTypes.justforcheck = PropTypes.string;
+        First.defaultProps = {
+          a: a,
+          z: z
+        };
+        First.defaultProps.justforcheck = "justforcheck";
+      `,
+    },
+    {
+      code: `
+        class First extends React.Component {
+          render() {
+            return <div />;
+          }
+        }
+        First.propTypes = {
+          a: PropTypes.any,
+          A: PropTypes.any,
+          z: PropTypes.string,
+          Z: PropTypes.string
+        };
+        First.defaultProps = {
+          a: "a",
+          A: "A",
+          z: "z",
+          Z: "Z"
+        };
+      `,
+      options: [{ ignoreCase: true }],
+    },
+    {
+      code: `
+        class Component extends React.Component {
+          static propTypes = {
+            a: PropTypes.any,
+            b: PropTypes.any,
+            c: PropTypes.any
+          };
+          static defaultProps = {
+            a: "a",
+            b: "b",
+            c: "c"
+          };
+          render() {
+            return <div />;
+          }
+        }
+      `,
+      features: ['class fields'],
+    },
+    {
+      code: `
+        class Hello extends React.Component {
+          render() {
+            return <div>Hello</div>;
+          }
+        }
+        Hello.propTypes = {
+          "aria-controls": PropTypes.string
+        };
+        Hello.defaultProps = {
+          "aria-controls": "aria-controls"
+        };
+      `,
+      options: [{ ignoreCase: true }],
+    },
+    semver.satisfies(babelEslintVersion, '< 9') ? {
+    // Invalid code, should not be validated
+      code: `
+        class Component extends React.Component {
+          propTypes: {
+            a: PropTypes.any,
+            c: PropTypes.any,
+            b: PropTypes.any
+          };
+          defaultProps: {
+            a: "a",
+            c: "c",
+            b: "b"
+          };
+          render() {
+            return <div />;
+          }
+        }
+      `,
+      parser: parsers.BABEL_ESLINT,
+    } : [],
+    {
+      code: `
+        var Hello = createReactClass({
+          render: function() {
+            let { a, ...b } = obj;
+            let c = { ...d };
+            return <div />;
+          }
+        });
+      `,
+    },
+    {
+      code: `
+        var First = createReactClass({
+          propTypes: {
+            barRequired: PropTypes.func.isRequired,
+            onBar: PropTypes.func,
+            z: PropTypes.any
+          },
+          getDefaultProps: function() {
+            return {
+              barRequired: "barRequired",
+              onBar: "onBar",
+              z: "z"
+            };
+          },
+          render: function() {
+            return <div />;
+          }
+        });
+      `,
+    },
+    {
+      code: `
+        export default class ClassWithSpreadInPropTypes extends BaseClass {
+          static propTypes = {
+            b: PropTypes.string,
+            ...c.propTypes,
+            a: PropTypes.string
+          }
+          static defaultProps = {
+            b: "b",
+            ...c.defaultProps,
+            a: "a"
+          }
+        }
+      `,
+      features: ['class fields'],
+    },
+    {
+      code: `
+        export default class ClassWithSpreadInPropTypes extends BaseClass {
+          static propTypes = {
+            a: PropTypes.string,
+            b: PropTypes.string,
+            c: PropTypes.string,
+            d: PropTypes.string,
+            e: PropTypes.string,
+            f: PropTypes.string
+          }
+          static defaultProps = {
+            a: "a",
+            b: "b",
+            ...c.defaultProps,
+            e: "e",
+            f: "f",
+            ...d.defaultProps
+          }
+        }
+      `,
+      features: ['class fields'],
+    },
+    {
+      code: `
+        const defaults = {
+          b: "b"
+        };
+        const types = {
+          a: PropTypes.string,
+          b: PropTypes.string,
+          c: PropTypes.string
+        };
+        function StatelessComponentWithSpreadInPropTypes({ a, b, c }) {
+          return <div>{a}{b}{c}</div>;
+        }
+        StatelessComponentWithSpreadInPropTypes.propTypes = types;
+        StatelessComponentWithSpreadInPropTypes.defaultProps = {
+          c: "c",
+          ...defaults,
+          a: "a"
+        };
+      `,
+    },
+    {
+      code: `
+        const propTypes = require('./externalPropTypes')
+        const defaultProps = require('./externalDefaultProps')
+        const TextFieldLabel = (props) => {
+          return <div />;
+        };
+        TextFieldLabel.propTypes = propTypes;
+        TextFieldLabel.defaultProps = defaultProps;
+      `,
+    },
+    {
+      code: `
+        const First = (props) => <div />;
+        export const propTypes = {
+            a: PropTypes.any,
+            z: PropTypes.string,
+        };
+        export const defaultProps = {
+            a: "a",
+            z: "z",
+        };
+        First.propTypes = propTypes;
+        First.defaultProps = defaultProps;
+      `,
+    },
+    {
+      code: `
+        const defaults = {
+          b: "b"
+        };
+        const First = (props) => <div />;
+        export const propTypes = {
+            a: PropTypes.string,
+            b: PropTypes.string,
+            z: PropTypes.string,
+        };
+        export const defaultProps = {
+            ...defaults,
+            a: "a",
+            z: "z",
+        };
+        First.propTypes = propTypes;
+        First.defaultProps = defaultProps;
+      `,
+    },
+    {
+      code: `
+        class First extends React.Component {
+          render() {
+            return <div />;
+          }
+        }
+        
+        First.defaultProps = {
+            a: PropTypes.any,
+            onBar: PropTypes.func,
+            onFoo: PropTypes.func,
+            z: PropTypes.string,
+        };
+      `,
+    }
+  )),
+
+  invalid: parsers.all([
+    {
+      code: `
+        class Component extends React.Component {
+          static propTypes = {
+            a: PropTypes.any,
+            b: PropTypes.any,
+            c: PropTypes.any
+          };
+          static defaultProps = {
+            a: "a",
+            c: "c",
+            b: "b"
+          };
+          render() {
+            return <div />;
+          }
+        }
+      `,
+      features: ['class fields'],
+      errors: [
+        {
+          messageId: 'propsNotSorted',
+          line: 11,
+          column: 13,
+          type: 'Property',
+        },
+      ], /* ,
+      output: `
+        class Component extends React.Component {
+          static propTypes = {
+            a: PropTypes.any,
+            b: PropTypes.any,
+            c: PropTypes.any
+          };
+          static defaultProps = {
+            a: "a",
+            b: "b",
+            c: "c"
+          };
+          render() {
+            return <div />;
+          }
+        }
+      ` */
+    },
+    {
+      code: `
+        class Component extends React.Component {
+          static propTypes = {
+            a: PropTypes.any,
+            b: PropTypes.any,
+            c: PropTypes.any
+          };
+          static defaultProps = {
+            c: "c",
+            b: "b",
+            a: "a"
+          };
+          render() {
+            return <div />;
+          }
+        }
+      `,
+      /* output: `
+        class Component extends React.Component {
+          static propTypes = {
+            a: PropTypes.any,
+            b: PropTypes.any,
+            c: PropTypes.any
+          };
+          static defaultProps = {
+            a: "a",
+            b: "b",
+            c: "c"
+          };
+          render() {
+            return <div />;
+          }
+        }
+      `, */
+      features: ['class fields'],
+      errors: 2,
+    },
+    {
+      code: `
+        class Component extends React.Component {
+          static propTypes = {
+            a: PropTypes.any,
+            b: PropTypes.any
+          };
+          static defaultProps = {
+            Z: "Z",
+            a: "a",
+          };
+          render() {
+            return <div />;
+          }
+        }
+      `,
+      /* output: `
+        class Component extends React.Component {
+          static propTypes = {
+            a: PropTypes.any,
+            b: PropTypes.any
+          };
+          static defaultProps = {
+            a: "a",
+            Z: "Z",
+          };
+          render() {
+            return <div />;
+          }
+        }
+      `, */
+      features: ['class fields'],
+      options: [{ ignoreCase: true }],
+      errors: [
+        {
+          messageId: 'propsNotSorted',
+          line: 9,
+          column: 13,
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `
+        class Component extends React.Component {
+          static propTypes = {
+            a: PropTypes.any,
+            z: PropTypes.any
+          };
+          static defaultProps = {
+            a: "a",
+            Z: "Z",
+          };
+          render() {
+            return <div />;
+          }
+        }
+      `,
+      /* output: `
+        class Component extends React.Component {
+          static propTypes = {
+            a: PropTypes.any,
+            z: PropTypes.any
+          };
+          static defaultProps = {
+            Z: "Z",
+            a: "a",
+          };
+          render() {
+            return <div />;
+          }
+        }
+      `, */
+      features: ['class fields'],
+      errors: [
+        {
+          messageId: 'propsNotSorted',
+          line: 9,
+          column: 13,
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `
+        class Hello extends React.Component {
+          render() {
+            return <div>Hello</div>;
+          }
+        }
+        Hello.propTypes = {
+          "a": PropTypes.string,
+          "b": PropTypes.string
+        };
+        Hello.defaultProps = {
+          "b": "b",
+          "a": "a"
+        };
+      `,
+      /* output: `
+        class Hello extends React.Component {
+          render() {
+            return <div>Hello</div>;
+          }
+        }
+        Hello.propTypes = {
+          "a": PropTypes.string,
+          "b": PropTypes.string
+        };
+        Hello.defaultProps = {
+          "a": "a",
+          "b": "b"
+        };
+      `, */
+      errors: [
+        {
+          messageId: 'propsNotSorted',
+          line: 13,
+          column: 11,
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `
+        class Hello extends React.Component {
+          render() {
+            return <div>Hello</div>;
+          }
+        }
+        Hello.propTypes = {
+          "a": PropTypes.string,
+          "b": PropTypes.string,
+          "c": PropTypes.string
+        };
+        Hello.defaultProps = {
+          "c": "c",
+          "b": "b",
+          "a": "a"
+        };
+      `,
+      /* output: `
+        class Hello extends React.Component {
+          render() {
+            return <div>Hello</div>;
+          }
+        }
+        Hello.propTypes = {
+          "a": PropTypes.string,
+          "b": PropTypes.string,
+          "c": PropTypes.string
+        };
+        Hello.defaultProps = {
+          "a": "a",
+          "b": "b",
+          "c": "c"
+        };
+      `, */
+      errors: 2,
+    },
+    {
+      code: `
+        class Hello extends React.Component {
+          render() {
+            return <div>Hello</div>;
+          }
+        }
+        Hello.propTypes = {
+          "a": PropTypes.string,
+          "B": PropTypes.string,
+        };
+        Hello.defaultProps = {
+          "a": "a",
+          "B": "B",
+        };
+      `,
+      /* output: `
+        class Hello extends React.Component {
+          render() {
+            return <div>Hello</div>;
+          }
+        }
+        Hello.propTypes = {
+          "a": PropTypes.string,
+          "B": PropTypes.string,
+        };
+        Hello.defaultProps = {
+          "B": "B",
+          "a": "a",
+        };
+      `, */
+      errors: [
+        {
+          messageId: 'propsNotSorted',
+          line: 13,
+          column: 11,
+          type: 'Property',
+        },
+      ],
+    },
+    // {
+    // Disabled test for comments -- fails
+    //   code: `
+    //     class Hello extends React.Component {
+    //       render() {
+    //         return <div>Hello</div>;
+    //       }
+    //     }
+    //     Hello.propTypes = {
+    //       "a": PropTypes.string,
+    //       "B": PropTypes.string,
+    //     };
+    //     Hello.defaultProps = {
+    //       /* a */
+    //       "a": "a",
+    //       /* B */
+    //       "B": "B",
+    //     };
+    //   `,
+    //   errors: [
+    //     {
+    //       messageId: 'propsNotSorted',
+    //       line: 14,
+    //       column: 3,
+    //       type: 'Property'
+    //     }
+    //   ],
+    //   output: `
+    //     class Hello extends React.Component {
+    //       render() {
+    //         return <div>Hello</div>;
+    //       }
+    //     }
+    //     Hello.propTypes = {
+    //       "a": PropTypes.string,
+    //       "B": PropTypes.string,
+    //     };
+    //     Hello.defaultProps = {
+    //       /* B */
+    //       "B": "B",
+    //       /* a */
+    //       "a": "a",
+    //     };
+    //   `
+    // },
+    {
+      code: `
+        class Hello extends React.Component {
+          render() {
+            return <div>Hello</div>;
+          }
+        }
+        Hello.propTypes = {
+          "a": PropTypes.string,
+          "B": PropTypes.string,
+        };
+        Hello.defaultProps = {
+          "B": "B",
+          "a": "a",
+        };
+      `,
+      /* output: `
+        class Hello extends React.Component {
+          render() {
+            return <div>Hello</div>;
+          }
+        }
+        Hello.propTypes = {
+          "a": PropTypes.string,
+          "B": PropTypes.string,
+        };
+        Hello.defaultProps = {
+          "a": "a",
+          "B": "B",
+        };
+      `, */
+      options: [{ ignoreCase: true }],
+      errors: [
+        {
+          messageId: 'propsNotSorted',
+          line: 13,
+          column: 11,
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `
+        const First = (props) => <div />;
+        const propTypes = {
+          z: PropTypes.string,
+          a: PropTypes.any,
+        };
+        const defaultProps = {
+          z: "z",
+          a: "a",
+        };
+        First.propTypes = propTypes;
+        First.defaultProps = defaultProps;
+      `,
+      /* output: `
+        const First = (props) => <div />;
+        const propTypes = {
+          z: PropTypes.string,
+          a: PropTypes.any,
+        };
+        const defaultProps = {
+          a: "a",
+          z: "z",
+        };
+        First.propTypes = propTypes;
+        First.defaultProps = defaultProps;
+      `, */
+      errors: [
+        {
+          messageId: 'propsNotSorted',
+          line: 9,
+          column: 11,
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `
+        export default class ClassWithSpreadInPropTypes extends BaseClass {
+          static propTypes = {
+            b: PropTypes.string,
+            ...c.propTypes,
+            a: PropTypes.string
+          }
+          static defaultProps = {
+            b: "b",
+            a: "a",
+            ...c.defaultProps
+          }
+        }
+      `,
+      /* output: `
+        export default class ClassWithSpreadInPropTypes extends BaseClass {
+          static propTypes = {
+            b: PropTypes.string,
+            ...c.propTypes,
+            a: PropTypes.string
+          }
+          static defaultProps = {
+            a: "a",
+            b: "b",
+            ...c.defaultProps
+          }
+        }
+      `, */
+      features: ['class fields'],
+      errors: [
+        {
+          messageId: 'propsNotSorted',
+          line: 10,
+          column: 13,
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `
+        export default class ClassWithSpreadInPropTypes extends BaseClass {
+          static propTypes = {
+            a: PropTypes.string,
+            b: PropTypes.string,
+            c: PropTypes.string,
+            d: PropTypes.string,
+            e: PropTypes.string,
+            f: PropTypes.string
+          }
+          static defaultProps = {
+            b: "b",
+            a: "a",
+            ...c.defaultProps,
+            f: "f",
+            e: "e",
+            ...d.defaultProps
+          }
+        }
+      `,
+      /* output: `
+        export default class ClassWithSpreadInPropTypes extends BaseClass {
+          static propTypes = {
+            a: PropTypes.string,
+            b: PropTypes.string,
+            c: PropTypes.string,
+            d: PropTypes.string,
+            e: PropTypes.string,
+            f: PropTypes.string
+          }
+          static defaultProps = {
+            a: "a",
+            b: "b",
+            ...c.defaultProps,
+            e: "e",
+            f: "f",
+            ...d.defaultProps
+          }
+        }
+      `, */
+      features: ['class fields'],
+      errors: 2,
+    },
+    {
+      code: `
+        const defaults = {
+          b: "b"
+        };
+        const types = {
+          a: PropTypes.string,
+          b: PropTypes.string,
+          c: PropTypes.string
+        };
+        function StatelessComponentWithSpreadInPropTypes({ a, b, c }) {
+          return <div>{a}{b}{c}</div>;
+        }
+        StatelessComponentWithSpreadInPropTypes.propTypes = types;
+        StatelessComponentWithSpreadInPropTypes.defaultProps = {
+          c: "c",
+          a: "a",
+          ...defaults,
+        };
+      `,
+      /* output: `
+        const defaults = {
+          b: "b"
+        };
+        const types = {
+          a: PropTypes.string,
+          b: PropTypes.string,
+          c: PropTypes.string
+        };
+        function StatelessComponentWithSpreadInPropTypes({ a, b, c }) {
+          return <div>{a}{b}{c}</div>;
+        }
+        StatelessComponentWithSpreadInPropTypes.propTypes = types;
+        StatelessComponentWithSpreadInPropTypes.defaultProps = {
+          a: "a",
+          c: "c",
+          ...defaults,
+        };
+      `, */
+      errors: [
+        {
+          messageId: 'propsNotSorted',
+          line: 16,
+          column: 11,
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `
+        class First extends React.Component {
+          render() {
+            return <div />;
+          }
+        }
+        
+        First.defaultProps = {
+            a: PropTypes.any,
+            z: PropTypes.string,
+            onFoo: PropTypes.func,
+            onBar: PropTypes.func,
+        };
+      `,
+      errors: [
+        { messageId: 'propsNotSorted', line: 11 },
+        { messageId: 'propsNotSorted', line: 12 },
+      ],
+    },
+  ]),
+});