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

Issue:
Rule proposal: sorted propTypes
Similar to #16, some people might might to enforce that propTypes are sorted alphabetically.

Good:

```jsx
propTypes: {
  aProp: React.PropTypes.object,
  bProp: React.PropTypes.object,
  cProp: React.PropTypes.object,
},
```

Bad:

```jsx
propTypes: {
  bProp: React.PropTypes.object,
  aProp: React.PropTypes.object,
  cProp: React.PropTypes.object,
},
```

Pull request description:
[added] The rule to check propTypes declarations sorting.
Tests and Docs are included.

The `jsx-sort-prop-types` rule recognizes all supported patterns by this plugin
including those with ES6 classes syntax and `static propTypes`.

```js
var Component = React.createClass({
  propTypes: {
    a: React.PropTypes.number,
    b: React.PropTypes.any
  }
...
```

```js
class Component extends React.Component {
  ...
}
Component.propTypes = {
  a: React.PropTypes.any,
  b: React.PropTypes.string
};
```

```js
class Component extends React.Component {
  static propTypes = {
    a: React.PropTypes.any,
    b: React.PropTypes.any,
    c: React.PropTypes.any
  }
  render() {
    return <div />;
  }
}
```

By default it is disabled. 
Also it allows case ignoring.

Closes #38.

In the case this PR is merged it needs DRY refactoring.
(`isPropTypesDeclaration()` is the one to extract and `isDisplayNameDeclaration()` has similar code)

Patch:
diff --git a/README.md b/README.md
index 667d0442..b3d28811 100755
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ Finally, enable all of the rules that you would like to use.
     "react/jsx-quotes": 1,
     "react/jsx-no-undef": 1,
     "react/jsx-sort-props": 1,
+    "react/jsx-sort-prop-types": 1,
     "react/jsx-uses-react": 1,
     "react/jsx-uses-vars": 1,
     "react/no-did-mount-set-state": 1,
@@ -68,6 +69,7 @@ Finally, enable all of the rules that you would like to use.
 * [jsx-quotes](docs/rules/jsx-quotes.md): Enforce quote style for JSX attributes
 * [jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX
 * [jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting
+* [jsx-sort-prop-types](docs/rules/jsx-sort-prop-types.md): Enforce propTypes declarations alphabetical sorting
 * [jsx-uses-react](docs/rules/jsx-uses-react.md): Prevent React to be incorrectly marked as unused
 * [jsx-uses-vars](docs/rules/jsx-uses-vars.md): Prevent variables used in JSX to be incorrectly marked as unused
 * [no-did-mount-set-state](docs/rules/no-did-mount-set-state.md): Prevent usage of setState in componentDidMount
@@ -111,4 +113,3 @@ ESLint-plugin-React is licensed under the [MIT License](http://www.opensource.or
 
 [status-url]: https://github.com/yannickcr/eslint-plugin-react/pulse
 [status-image]: http://img.shields.io/badge/status-maintained-brightgreen.svg?style=flat-square
-
diff --git a/docs/rules/jsx-sort-prop-types.md b/docs/rules/jsx-sort-prop-types.md
new file mode 100644
index 00000000..f1f10681
--- /dev/null
+++ b/docs/rules/jsx-sort-prop-types.md
@@ -0,0 +1,91 @@
+# Enforce propTypes declarations alphabetical sorting (jsx-sort-prop-types)
+
+Some developers prefer to sort propsTypes 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.
+
+## Rule Details
+
+This rule checks all JSX components and verifies that all propsTypes declarations are sorted alphabetically.
+The default configuration of the rule is case-sensitive.
+This rule is off by default.
+
+The following patterns are considered warnings:
+
+```js
+var Component = React.createClass({
+  propTypes: {
+    z: React.PropTypes.number,
+    a: React.PropTypes.any,
+    b: React.PropTypes.string
+  },
+...
+});
+
+class Component extends React.Component {
+  ...
+}
+Component.propTypes = {
+  z: React.PropTypes.number,
+  a: React.PropTypes.any,
+  b: React.PropTypes.string
+};
+
+class Component extends React.Component {
+  static propTypes = {
+    z: React.PropTypes.any,
+    y: React.PropTypes.any,
+    a: React.PropTypes.any
+  }
+  render() {
+    return <div />;
+  }
+}
+```
+
+The following patterns are considered okay and do not cause warnings:
+
+```js
+var Component = React.createClass({
+  propTypes: {
+    a: React.PropTypes.number,
+    b: React.PropTypes.any,
+    c: React.PropTypes.string
+  },
+...
+});
+
+class Component extends React.Component {
+  ...
+}
+Component.propTypes = {
+  a: React.PropTypes.string,
+  b: React.PropTypes.any,
+  c: React.PropTypes.string
+};
+
+class Component extends React.Component {
+  static propTypes = {
+    a: React.PropTypes.any,
+    b: React.PropTypes.any,
+    c: React.PropTypes.any
+  }
+  render() {
+    return <div />;
+  }
+}
+```
+
+## Rule Options
+
+```js
+...
+"jsx-sort-prop-types": [<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 props declarations isn't a part of your coding standards, then you can leave this rule off.
diff --git a/docs/rules/jsx-sort-props.md b/docs/rules/jsx-sort-props.md
index cf405af9..3e027906 100755
--- a/docs/rules/jsx-sort-props.md
+++ b/docs/rules/jsx-sort-props.md
@@ -40,4 +40,4 @@ The following patterns are considered okay and do not cause warnings:
 
 ## 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 props isn't a part of your coding standards, then you can leave this rule off.
+This rule is a formatting preference and not following it won't negatively affect the quality of your code. If you alphabetizing props isn't a part of your coding standards, then you can leave this rule off.
diff --git a/index.js b/index.js
index e31712ff..aaa6f682 100755
--- a/index.js
+++ b/index.js
@@ -16,6 +16,7 @@ module.exports = {
     'jsx-quotes': require('./lib/rules/jsx-quotes'),
     'no-unknown-property': require('./lib/rules/no-unknown-property'),
     'jsx-sort-props': require('./lib/rules/jsx-sort-props'),
+    'jsx-sort-prop-types': require('./lib/rules/jsx-sort-prop-types'),
     'jsx-boolean-value': require('./lib/rules/jsx-boolean-value')
   },
   rulesConfig: {
@@ -33,6 +34,7 @@ module.exports = {
     'jsx-quotes': 0,
     'no-unknown-property': 0,
     'jsx-sort-props': 0,
+    'jsx-sort-prop-types': 0,
     'jsx-boolean-value': 0
   }
 };
diff --git a/lib/rules/jsx-sort-prop-types.js b/lib/rules/jsx-sort-prop-types.js
new file mode 100644
index 00000000..bce91a4d
--- /dev/null
+++ b/lib/rules/jsx-sort-prop-types.js
@@ -0,0 +1,90 @@
+/**
+ * @fileoverview Enforce propTypes declarations alphabetical sorting
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = function(context) {
+
+  var configuration = context.options[0] || {};
+  var ignoreCase = configuration.ignoreCase || false;
+
+  /**
+   * Checks if node is `propTypes` declaration
+   * @param {ASTNode} node The AST node being checked.
+   * @returns {Boolean} True if node is `propTypes` declaration, false if not.
+   */
+  function isPropTypesDeclaration(node) {
+
+    // Special case for class properties
+    // (babel-eslint does not expose property name so we have to rely on tokens)
+    if (node.type === 'ClassProperty') {
+      var tokens = context.getFirstTokens(node, 2);
+      if (tokens[0].value === 'propTypes' || tokens[1].value === 'propTypes') {
+        return true;
+      }
+      return false;
+    }
+
+    return Boolean(
+      node &&
+      node.name === 'propTypes'
+    );
+  }
+
+  /**
+   * Checks if propTypes declarations are sorted
+   * @param {Array} declarations The array of AST nodes being checked.
+   * @returns {void}
+   */
+  function checkSorted(declarations) {
+    declarations.reduce(function(prev, curr) {
+      var prevPropName = prev.key.name;
+      var currenPropName = curr.key.name;
+
+      if (ignoreCase) {
+        prevPropName = prevPropName.toLowerCase();
+        currenPropName = currenPropName.toLowerCase();
+      }
+
+      if (currenPropName < prevPropName) {
+        context.report(curr, 'Prop types declarations should be sorted alphabetically');
+        return prev;
+      }
+
+      return curr;
+    }, declarations[0]);
+  }
+
+  return {
+    ClassProperty: function(node) {
+      if (isPropTypesDeclaration(node) && node.value.type === 'ObjectExpression') {
+        checkSorted(node.value.properties);
+      }
+    },
+
+    MemberExpression: function(node) {
+      if (isPropTypesDeclaration(node.property)) {
+        var right = node.parent.right;
+        if (right && right.type === 'ObjectExpression') {
+          checkSorted(right.properties);
+        }
+      }
+    },
+
+    ObjectExpression: function(node) {
+      node.properties.forEach(function(property) {
+        if (!isPropTypesDeclaration(property.key)) {
+          return;
+        }
+        if (property.value.type === 'ObjectExpression') {
+          checkSorted(property.value.properties);
+        }
+      });
+    }
+
+  };
+};


Test patch:
diff --git a/tests/lib/rules/jsx-sort-prop-types.js b/tests/lib/rules/jsx-sort-prop-types.js
new file mode 100755
index 00000000..19ca8ed3
--- /dev/null
+++ b/tests/lib/rules/jsx-sort-prop-types.js
@@ -0,0 +1,324 @@
+/**
+ * @fileoverview Tests for jsx-sort-prop-types
+ */
+'use strict';
+
+// -----------------------------------------------------------------------------
+// Requirements
+// -----------------------------------------------------------------------------
+
+var eslint = require('eslint').linter;
+var ESLintTester = require('eslint-tester');
+
+require('babel-eslint');
+
+// -----------------------------------------------------------------------------
+// Tests
+// -----------------------------------------------------------------------------
+
+var ERROR_MESSAGE = 'Prop types declarations should be sorted alphabetically';
+
+var eslintTester = new ESLintTester(eslint);
+eslintTester.addRuleTest('lib/rules/jsx-sort-prop-types', {
+  valid: [
+    {
+      code: [
+        'var First = React.createClass({',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      ecmaFeatures: {
+        jsx: true
+      }
+    }, {
+      code: [
+        'var First = React.createClass({',
+        '  propTypes: externalPropTypes,',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      ecmaFeatures: {
+        jsx: true
+      }
+    }, {
+      code: [
+        'var First = React.createClass({',
+        '  propTypes: {',
+        '    A: React.PropTypes.any,',
+        '    Z: React.PropTypes.string,',
+        '    a: React.PropTypes.any,',
+        '    z: React.PropTypes.string',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      ecmaFeatures: {
+        jsx: true
+      }
+    }, {
+      code: [
+        'var First = React.createClass({',
+        '  propTypes: {',
+        '    a: React.PropTypes.any,',
+        '    A: React.PropTypes.any,',
+        '    z: React.PropTypes.string,',
+        '    Z: React.PropTypes.string',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      args: [1, {
+        ignoreCase: true
+      }],
+      ecmaFeatures: {
+        jsx: true
+      }
+    }, {
+      code: [
+        'var First = React.createClass({',
+        '  propTypes: {',
+        '    a: React.PropTypes.any,',
+        '    z: React.PropTypes.string',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});',
+        'var Second = React.createClass({',
+        '  propTypes: {',
+        '    AA: React.PropTypes.any,',
+        '    ZZ: React.PropTypes.string',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      ecmaFeatures: {
+        jsx: true
+      }
+    }, {
+      code: [
+        'class First extends React.Component {',
+        '  render() {',
+        '    return <div />;',
+        '  }',
+        '}',
+        'First.propTypes = {',
+        '  a: React.PropTypes.string,',
+        '  z: React.PropTypes.string',
+        '};',
+        'First.propTypes.justforcheck = React.PropTypes.string;'
+      ].join('\n'),
+      ecmaFeatures: {
+        classes: true,
+        jsx: true
+      }
+    }, {
+      code: [
+        'class First extends React.Component {',
+        '  render() {',
+        '    return <div />;',
+        '  }',
+        '}',
+        'First.propTypes = {',
+        '  a: React.PropTypes.any,',
+        '  A: React.PropTypes.any,',
+        '  z: React.PropTypes.string,',
+        '  Z: React.PropTypes.string',
+        '};'
+      ].join('\n'),
+      args: [1, {
+        ignoreCase: true
+      }],
+      ecmaFeatures: {
+        classes: true,
+        jsx: true
+      }
+    }, {
+      code: [
+        'class Component extends React.Component {',
+        '  static propTypes = {',
+        '    a: React.PropTypes.any,',
+        '    b: React.PropTypes.any,',
+        '    c: React.PropTypes.any',
+        '  }',
+        '  render() {',
+        '    return <div />;',
+        '  }',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint',
+      ecmaFeatures: {
+        classes: true,
+        jsx: true
+      }
+    }
+  ],
+
+  invalid: [
+    {
+      code: [
+        'var First = React.createClass({',
+        '  propTypes: {',
+        '    z: React.PropTypes.string,',
+        '    a: React.PropTypes.any',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      ecmaFeatures: {
+        jsx: true
+      },
+      errors: [{
+        message: ERROR_MESSAGE,
+        line: 4,
+        column: 4,
+        type: 'Property'
+      }]
+    }, {
+      code: [
+        'var First = React.createClass({',
+        '  propTypes: {',
+        '    z: React.PropTypes.any,',
+        '    Z: React.PropTypes.any',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      ecmaFeatures: {
+        jsx: true
+      },
+      errors: [{
+        message: ERROR_MESSAGE,
+        line: 4,
+        column: 4,
+        type: 'Property'
+      }]
+    }, {
+      code: [
+        'var First = React.createClass({',
+        '  propTypes: {',
+        '    Z: React.PropTypes.any,',
+        '    a: React.PropTypes.any',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      args: [1, {
+        ignoreCase: true
+      }],
+      ecmaFeatures: {
+        jsx: true
+      },
+      errors: [{
+        message: ERROR_MESSAGE,
+        line: 4,
+        column: 4,
+        type: 'Property'
+      }]
+    }, {
+      code: [
+        'var First = React.createClass({',
+        '  propTypes: {',
+        '    a: React.PropTypes.any,',
+        '    A: React.PropTypes.any,',
+        '    z: React.PropTypes.string,',
+        '    Z: React.PropTypes.string',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      ecmaFeatures: {
+        jsx: true
+      },
+      errors: 2
+    }, {
+      code: [
+        'var First = React.createClass({',
+        '  propTypes: {',
+        '    a: React.PropTypes.any,',
+        '    Zz: React.PropTypes.string',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});',
+        'var Second = React.createClass({',
+        '  propTypes: {',
+        '    aAA: React.PropTypes.any,',
+        '    ZZ: React.PropTypes.string',
+        '  },',
+        '  render: function() {',
+        '    return <div />;',
+        '  }',
+        '});'
+      ].join('\n'),
+      ecmaFeatures: {
+        jsx: true
+      },
+      errors: 2
+    }, {
+      code: [
+        'class First extends React.Component {',
+        '  render() {',
+        '    return <div />;',
+        '  }',
+        '}',
+        'First.propTypes = {',
+        '    yy: React.PropTypes.any,',
+        '    bb: React.PropTypes.string',
+        '};',
+        'class Second extends React.Component {',
+        '  render() {',
+        '    return <div />;',
+        '  }',
+        '}',
+        'Second.propTypes = {',
+        '    aAA: React.PropTypes.any,',
+        '    ZZ: React.PropTypes.string',
+        '};'
+      ].join('\n'),
+      ecmaFeatures: {
+        classes: true,
+        jsx: true
+      },
+      errors: 2
+    }, {
+      code: [
+        'class Component extends React.Component {',
+        '  static propTypes = {',
+        '    z: React.PropTypes.any,',
+        '    y: React.PropTypes.any,',
+        '    a: React.PropTypes.any',
+        '  }',
+        '  render() {',
+        '    return <div />;',
+        '  }',
+        '}'
+      ].join('\n'),
+      parser: 'babel-eslint',
+      ecmaFeatures: {
+        classes: true,
+        jsx: true
+      },
+      errors: 2
+    }
+  ]
+});