Repository: yannickcr/eslint-plugin-react
Pull request #1483 (2017-10-16 19:43:15)

Add jsx-sort-default-props rule
Related to https://github.com/yannickcr/eslint-plugin-react/issues/281

- [x] Add support for
```js
var First = createReactClass({
  getDefaultProps: function() {
    return {
      barRequired: "barRequired",
      onBar: "onBar"
    };
  }
});
```
- [x] Add more tests
- [x] Remove useless code

fsmaia: Any advances? I have some spare time, may I help?
b0gok: @fsmaia sorry, haven't free time for a while. So help is welcome :)
ljharb: @b0gok the ideal is for you to add @fsmaia to your fork, so they can update this PR directly - if not, then @fsmaia, please post here a link to your commits on your own fork, and I'll add them for you. Please do *not* open up a duplicate PR.
b0gok: @ljharb ok.
@fsmaia I add you as collaborator in my fork.
fsmaia: Added some SFC examples in the documentation and tests. Had to change the rule source, 'cause unnecessary/commented code was useful right in SFC scenario.
fsmaia: @ljharb: Are we ready to merge? Any further work or change needed? Thanks
ljharb: Also, please rebase and force push this so as to remove merge commits.

Patch:
diff --git a/README.md b/README.md
index 883b4106..0b8101a0 100644
--- a/README.md
+++ b/README.md
@@ -156,6 +156,7 @@ Enable the rules that you would like to use.
 * [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX
 * [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
 * [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
+* [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md): Enforce default props alphabetical sorting
 * [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting (fixable)
 * [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)
diff --git a/docs/rules/jsx-sort-default-props.md b/docs/rules/jsx-sort-default-props.md
new file mode 100644
index 00000000..d4b48f81
--- /dev/null
+++ b/docs/rules/jsx-sort-default-props.md
@@ -0,0 +1,185 @@
+# Enforce defaultProps declarations alphabetical sorting (react/jsx-sort-default-props)
+
+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/jsx-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 1b3a1238..7b539e26 100644
--- a/index.js
+++ b/index.js
@@ -35,6 +35,7 @@ const allRules = {
   'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
   'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
   'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
+  'jsx-sort-default-props': require('./lib/rules/jsx-sort-default-props'),
   'jsx-sort-props': require('./lib/rules/jsx-sort-props'),
   'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),
   'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'),
diff --git a/lib/rules/jsx-sort-default-props.js b/lib/rules/jsx-sort-default-props.js
new file mode 100644
index 00000000..967e0e41
--- /dev/null
+++ b/lib/rules/jsx-sort-default-props.js
@@ -0,0 +1,166 @@
+/**
+ * @fileoverview Enforce default props alphabetical sorting
+ * @author Vladimir Kattsov
+ */
+'use strict';
+
+const variableUtil = require('../util/variable');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Enforce default props alphabetical sorting',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+
+    schema: [{
+      type: 'object',
+      properties: {
+        ignoreCase: {
+          type: 'boolean'
+        }
+      },
+      additionalProperties: false
+    }]
+  },
+
+  create: function(context) {
+    const sourceCode = context.getSourceCode();
+    const configuration = context.options[0] || {};
+    const ignoreCase = configuration.ignoreCase || false;
+    const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []);
+
+    /**
+     * 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;
+      } else 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)
+      } else if (node.type === 'ClassProperty') {
+        const tokens = context.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 sourceCode.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) {
+      declarations.reduce((prev, curr, idx, decls) => {
+        if (/SpreadProperty$/.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) {
+          context.report({
+            node: curr,
+            message: 'Default prop types declarations should be sorted alphabetically'
+          });
+
+          return prev;
+        }
+
+        return curr;
+      }, declarations[0]);
+    }
+
+    function checkNode(node) {
+      switch (node && node.type) {
+        case 'ObjectExpression':
+          checkSorted(node.properties);
+          break;
+        case 'Identifier':
+          const propTypesObject = findVariableByName(node.name);
+          if (propTypesObject && propTypesObject.properties) {
+            checkSorted(propTypesObject.properties);
+          }
+          break;
+        case 'CallExpression':
+          const innerNode = node.arguments && node.arguments[0];
+          if (propWrapperFunctions.has(node.callee.name) && innerNode) {
+            checkNode(innerNode);
+          }
+          break;
+        default:
+          break;
+      }
+    }
+
+    // --------------------------------------------------------------------------
+    // Public API
+    // --------------------------------------------------------------------------
+
+    return {
+      ClassProperty: function(node) {
+        if (!isDefaultPropsDeclaration(node)) {
+          return;
+        }
+
+        checkNode(node.value);
+      },
+
+      MemberExpression: function(node) {
+        if (!isDefaultPropsDeclaration(node)) {
+          return;
+        }
+
+        checkNode(node.parent.right);
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/jsx-sort-default-props.js b/tests/lib/rules/jsx-sort-default-props.js
new file mode 100644
index 00000000..c4090487
--- /dev/null
+++ b/tests/lib/rules/jsx-sort-default-props.js
@@ -0,0 +1,613 @@
+/**
+ * @fileoverview Tests for jsx-sort-default-props
+ * @author Vladimir Kattsov
+ */
+'use strict';
+
+// -----------------------------------------------------------------------------
+// Requirements
+// -----------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/jsx-sort-default-props');
+const RuleTester = require('eslint').RuleTester;
+
+const parserOptions = {
+  ecmaVersion: 8,
+  sourceType: 'module',
+  ecmaFeatures: {
+    experimentalObjectRestSpread: true,
+    jsx: true
+  }
+};
+
+require('babel-eslint');
+
+// -----------------------------------------------------------------------------
+// Tests
+// -----------------------------------------------------------------------------
+
+const ERROR_MESSAGE = 'Default prop types declarations should be sorted alphabetically';
+
+const ruleTester = new RuleTester({parserOptions});
+ruleTester.run('jsx-sort-default-props', rule, {
+  valid: [{
+    code: [
+      'var First = createReactClass({',
+      '  render: function() {',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
+  }, {
+    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 />;',
+      '  }',
+      '});'
+    ].join('\n')
+  }, {
+    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 />;',
+      '  }',
+      '});'
+    ].join('\n'),
+    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 />;',
+      '  }',
+      '});'
+    ].join('\n')
+  }, {
+    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";'
+    ].join('\n')
+  }, {
+    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"',
+      '};'
+    ].join('\n'),
+    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 />;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint'
+  }, {
+    code: [
+      'class Hello extends React.Component {',
+      '  render() {',
+      '    return <div>Hello</div>;',
+      '  }',
+      '}',
+      'Hello.propTypes = {',
+      '  "aria-controls": PropTypes.string',
+      '};',
+      'Hello.defaultProps = {',
+      '  "aria-controls": "aria-controls"',
+      '};'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    options: [{
+      ignoreCase: true
+    }]
+  }, {
+    // 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 />;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint'
+  }, {
+    code: [
+      'var Hello = createReactClass({',
+      '  render: function() {',
+      '    let { a, ...b } = obj;',
+      '    let c = { ...d };',
+      '    return <div />;',
+      '  }',
+      '});'
+    ].join('\n')
+  }, {
+    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 />;',
+      '  }',
+      '});'
+    ].join('\n')
+  }, {
+    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"',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint'
+  }, {
+    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',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint'
+  }, {
+    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"',
+      '};'
+    ].join('\n'),
+    parser: 'babel-eslint'
+  }, {
+    code: [
+      'const propTypes = require(\'./externalPropTypes\')',
+      'const defaultProps = require(\'./externalDefaultProps\')',
+      'const TextFieldLabel = (props) => {',
+      '  return <div />;',
+      '};',
+      'TextFieldLabel.propTypes = propTypes;',
+      'TextFieldLabel.defaultProps = defaultProps;'
+    ].join('\n')
+  }, {
+    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;'
+    ].join('\n')
+  }],
+
+  invalid: [{
+    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 />;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 10,
+      column: 5,
+      type: 'Property'
+    }]
+  }, {
+    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 />;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    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 />;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    options: [{
+      ignoreCase: true
+    }],
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 8,
+      column: 5,
+      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 />;',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 8,
+      column: 5,
+      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"',
+      '};'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 12,
+      column: 3,
+      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"',
+      '};'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    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",',
+      '};'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 12,
+      column: 3,
+      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",',
+      '};'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    options: [{
+      ignoreCase: true
+    }],
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 12,
+      column: 3,
+      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;'
+    ].join('\n'),
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 8,
+      column: 3,
+      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',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 9,
+      column: 5,
+      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',
+      '  }',
+      '}'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    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,',
+      '};'
+    ].join('\n'),
+    parser: 'babel-eslint',
+    errors: [{
+      message: ERROR_MESSAGE,
+      line: 15,
+      column: 3,
+      type: 'Property'
+    }]
+  }]
+});