Repository: yannickcr/eslint-plugin-react
Pull request #1264 (2017-06-19 19:55:51)

New `boolean-prop-naming` rule for enforcing the naming of booleans
At my workplace, we prefer to name our boolean props as "isSomething" or "hasWhatever". This is an ESLint rule which allows devs to specify a Regex pattern for how they want their boolean props named.

Tested with createReactClass, ES6 components, stateless components and Flowtype.

I support, in the future, this rule can be expanded to support naming rules for ANY props, but that felt like overkill for the time being. Hopefully this rule will be useful as is.

EvNaverniouk: > What about custom propTypes that enforce booleans but aren't PropTypes.bool? For example, mutuallyExclusiveTrueProps from https://www.npmjs.com/package/airbnb-prop-types.
> 
> Can there be a way to customize the name of custom propTypes that are booleans?

Good idea. I've added a new `propTypeNames` configuration option.

Patch:
diff --git a/README.md b/README.md
index 2735746b..2109a9a1 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@ Finally, enable all of the rules that you would like to use.  Use [our preset](#
 
 # List of supported rules
 
+* [react/boolean-prop-naming](docs/rules/boolean-prop-naming.md): Enforces consistent naming for boolean props
 * [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md): Prevent extraneous defaultProps on components
 * [react/display-name](docs/rules/display-name.md): Prevent missing `displayName` in a React component definition
 * [react/forbid-component-props](docs/rules/forbid-component-props.md): Forbid certain props on Components
diff --git a/docs/rules/boolean-prop-naming.md b/docs/rules/boolean-prop-naming.md
new file mode 100644
index 00000000..a43d91e5
--- /dev/null
+++ b/docs/rules/boolean-prop-naming.md
@@ -0,0 +1,67 @@
+# Enforces consistent naming for boolean props (react/boolean-prop-naming)
+
+Allows you to enforce a consistent naming pattern for props which expect a boolean value.
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```jsx
+var Hello = createReactClass({
+  propTypes: {
+    enabled: PropTypes.bool
+  },
+  render: function() { return <div />; };
+});
+```
+
+The following patterns are not considered warnings:
+
+```jsx
+var Hello = createReactClass({
+  propTypes: {
+    isEnabled: PropTypes.bool
+  },
+  render: function() { return <div />; };
+});
+```
+
+## Rule Options
+
+```js
+...
+"react/boolean-prop-naming": [<enabled>, { "propTypeNames": Array<string>, "rule": <string> }]
+...
+```
+
+### `propTypeNames`
+
+The list of prop type names that are considered to be booleans. By default this is set to `['bool']` but you can include other custom types like so:
+
+```jsx
+"react/boolean-prop-naming": ["error", { "propTypeNames": ["bool", "mutuallyExclusiveTrueProps"] }]
+```
+
+### `rule`
+
+The RegExp pattern to use when validating the name of the prop. The default value for this option is set to: `"^(is|has)[A-Z]([A-Za-z0-9]?)+"` to enforce `is` and `has` prefixes.
+
+For supporting "is" and "has" naming (default):
+
+- isEnabled
+- isAFK
+- hasCondition
+- hasLOL
+
+```jsx
+"react/boolean-prop-naming": ["error", { "rule": "^(is|has)[A-Z]([A-Za-z0-9]?)+" }]
+```
+
+For supporting "is" naming:
+
+- isEnabled
+- isAFK
+
+```jsx
+"react/boolean-prop-naming": ["error", { "rule": "^is[A-Z]([A-Za-z0-9]?)+" }]
+```
\ No newline at end of file
diff --git a/index.js b/index.js
index d9c1df21..862b929f 100644
--- a/index.js
+++ b/index.js
@@ -64,7 +64,8 @@ var allRules = {
   'no-children-prop': require('./lib/rules/no-children-prop'),
   'void-dom-elements-no-children': require('./lib/rules/void-dom-elements-no-children'),
   'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'),
-  'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update')
+  'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'),
+  'boolean-prop-naming': require('./lib/rules/boolean-prop-naming')
 };
 
 function filterRules(rules, predicate) {
diff --git a/lib/rules/boolean-prop-naming.js b/lib/rules/boolean-prop-naming.js
new file mode 100644
index 00000000..be944bb0
--- /dev/null
+++ b/lib/rules/boolean-prop-naming.js
@@ -0,0 +1,241 @@
+/**
+ * @fileoverview Enforces consistent naming for boolean props
+ * @author Evgueni Naverniouk
+ */
+'use strict';
+
+const has = require('has');
+const Components = require('../util/Components');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      category: 'Stylistic Issues',
+      description: 'Enforces consistent naming for boolean props',
+      recommended: false
+    },
+
+    schema: [{
+      additionalProperties: false,
+      properties: {
+        propTypeNames: {
+          items: {
+            type: 'string'
+          },
+          minItems: 1,
+          type: 'array',
+          uniqueItems: true
+        },
+        rule: {
+          default: '^(is|has)[A-Z]([A-Za-z0-9]?)+',
+          minLength: 1,
+          type: 'string'
+        }
+      },
+      type: 'object'
+    }]
+  },
+
+  create: Components.detect(function(context, components, utils) {
+    const sourceCode = context.getSourceCode();
+    const config = context.options[0] || {};
+    const rule = config.rule ? new RegExp(config.rule) : null;
+    const propTypeNames = config.propTypeNames || ['bool'];
+
+    // Remembers all Flowtype object definitions
+    var objectTypeAnnotations = new Map();
+
+    /**
+     * 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') {
+        const tokens = context.getFirstTokens(node, 2);
+        if (tokens[0].value === 'propTypes' || (tokens[1] && tokens[1].value === 'propTypes')) {
+          return true;
+        }
+        // Flow support
+        if (node.typeAnnotation && node.key.name === 'props') {
+          return true;
+        }
+        return false;
+      }
+
+      return Boolean(
+        node &&
+        node.name === 'propTypes'
+      );
+    }
+
+    /**
+     * Returns the prop key to ensure we handle the following cases:
+     * propTypes: {
+     *   full: React.PropTypes.bool,
+     *   short: PropTypes.bool,
+     *   direct: bool
+     * }
+     * @param {Object} node The node we're getting the name of
+     */
+    function getPropKey(node) {
+      if (node.value.property) {
+        return node.value.property.name;
+      }
+      if (node.value.type === 'Identifier') {
+        return node.value.name;
+      }
+      return null;
+    }
+
+    /**
+	 * Returns the name of the given node (prop)
+	 * @param {Object} node The node we're getting the name of
+	 */
+    function getPropName(node) {
+      // Due to this bug https://github.com/babel/babel-eslint/issues/307
+      // we can't get the name of the Flow object key name. So we have
+      // to hack around it for now.
+      if (node.type === 'ObjectTypeProperty') {
+        return sourceCode.getFirstToken(node).value;
+      }
+
+      return node.key.name;
+    }
+
+    /**
+     * Checks and mark props with invalid naming
+     * @param {Object} node The component node we're testing
+     * @param {Array} proptypes A list of Property object (for each proptype defined)
+     */
+    function validatePropNaming(node, proptypes) {
+      const component = components.get(node) || node;
+      const invalidProps = component.invalidProps || [];
+
+      proptypes.forEach(function (prop) {
+        const propKey = getPropKey(prop);
+        const flowCheck = (
+          prop.type === 'ObjectTypeProperty' &&
+          prop.value.type === 'BooleanTypeAnnotation' &&
+          rule.test(getPropName(prop)) === false
+        );
+        const regularCheck = (
+          propKey &&
+          propTypeNames.indexOf(propKey) >= 0 &&
+          rule.test(getPropName(prop)) === false
+        );
+
+        if (flowCheck || regularCheck) {
+          invalidProps.push(prop);
+        }
+      });
+
+      components.set(node, {
+        invalidProps: invalidProps
+      });
+    }
+
+    /**
+     * Reports invalid prop naming
+     * @param {Object} component The component to process
+     */
+    function reportInvalidNaming(component) {
+      component.invalidProps.forEach(function (propNode) {
+        const propName = getPropName(propNode);
+        context.report({
+          node: propNode,
+          message: `Prop name (${propName}) doesn't match rule (${config.rule})`,
+          data: {
+            component: propName
+          }
+        });
+      });
+    }
+
+    // --------------------------------------------------------------------------
+    // Public
+    // --------------------------------------------------------------------------
+
+    return {
+      ClassProperty: function(node) {
+        if (!rule || !isPropTypesDeclaration(node)) {
+          return;
+        }
+        if (node.value && node.value.properties) {
+          validatePropNaming(node, node.value.properties);
+        }
+        if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
+          validatePropNaming(node, node.typeAnnotation.typeAnnotation.properties);
+        }
+      },
+
+      MemberExpression: function(node) {
+        if (!rule || !isPropTypesDeclaration(node.property)) {
+          return;
+        }
+        const component = utils.getRelatedComponent(node);
+        if (!component) {
+          return;
+        }
+        validatePropNaming(component.node, node.parent.right.properties);
+      },
+
+      ObjectExpression: function(node) {
+        if (!rule) {
+          return;
+        }
+
+        // Search for the proptypes declaration
+        node.properties.forEach(function(property) {
+          if (!isPropTypesDeclaration(property.key)) {
+            return;
+          }
+          validatePropNaming(node, property.value.properties);
+        });
+      },
+
+      TypeAlias: function(node) {
+        // Cache all ObjectType annotations, we will check them at the end
+        if (node.right.type === 'ObjectTypeAnnotation') {
+          objectTypeAnnotations.set(node.id.name, node.right);
+        }
+      },
+
+      'Program:exit': function() {
+        if (!rule) {
+          return;
+        }
+
+        const list = components.list();
+        Object.keys(list).forEach(function (component) {
+          // If this is a functional component that uses a global type, check it
+          if (
+            list[component].node.type === 'FunctionDeclaration' &&
+            list[component].node.params &&
+            list[component].node.params.length &&
+            list[component].node.params[0].typeAnnotation
+          ) {
+            const typeNode = list[component].node.params[0].typeAnnotation;
+            const propType = objectTypeAnnotations.get(typeNode.typeAnnotation.id.name);
+            if (propType) {
+              validatePropNaming(list[component].node, propType.properties);
+            }
+          }
+
+          if (!has(list, component) || (list[component].invalidProps || []).length) {
+            reportInvalidNaming(list[component]);
+          }
+        });
+
+        // Reset cache
+        objectTypeAnnotations.clear();
+      }
+    };
+  })
+};


Test patch:
diff --git a/tests/lib/rules/boolean-prop-naming.js b/tests/lib/rules/boolean-prop-naming.js
new file mode 100644
index 00000000..e792b938
--- /dev/null
+++ b/tests/lib/rules/boolean-prop-naming.js
@@ -0,0 +1,434 @@
+/**
+ * @fileoverview Enforces consistent naming for boolean props
+ * @author Evgueni Naverniouk
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/boolean-prop-naming');
+var RuleTester = require('eslint').RuleTester;
+
+require('babel-eslint');
+
+var parserOptions = {
+  ecmaVersion: 6,
+  sourceType: 'module',
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester({parserOptions});
+ruleTester.run('boolean-prop-naming', rule, {
+
+  valid: [{
+    // Should support both `is` and `has` prefixes by default
+    code: [
+      'var Hello = createReactClass({',
+      '  propTypes: {isSomething: PropTypes.bool, hasValue: PropTypes.bool},',
+      '  render: function() { return <div />; }',
+      '});'
+    ].join('\n')
+  }, {
+    // createReactClass components with PropTypes
+    code: [
+      'var Hello = createReactClass({',
+      '  propTypes: {isSomething: PropTypes.bool},',
+      '  render: function() { return <div />; }',
+      '});'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }]
+  }, {
+    // createReactClass components with React.PropTypes
+    code: [
+      'var Hello = createReactClass({',
+      '  propTypes: {isSomething: React.PropTypes.bool},',
+      '  render: function() { return <div />; }',
+      '});'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }]
+  }, {
+    // React.createClass components with PropTypes
+    code: [
+      'var Hello = React.createClass({',
+      '  propTypes: {isSomething: PropTypes.bool},',
+      '  render: function() { return <div />; }',
+      '});'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    settings: {
+      react: {
+        createClass: 'createClass'
+      }
+    }
+  }, {
+    // React.createClass components with non-boolean PropTypes
+    code: [
+      'var Hello = React.createClass({',
+      '  propTypes: {something: PropTypes.any},',
+      '  render: function() { return <div />; }',
+      '});'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    settings: {
+      react: {
+        createClass: 'createClass'
+      }
+    }
+  }, {
+    // ES6 components as React.Component with boolean PropTypes
+    code: [
+      'class Hello extends React.Component {',
+      '  render () { return <div />; }',
+      '}',
+      'Hello.propTypes = {isSomething: PropTypes.bool}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }]
+  }, {
+    // ES6 components as React.Component with non-boolean PropTypes
+    code: [
+      'class Hello extends React.Component {',
+      '  render () { return <div />; }',
+      '}',
+      'Hello.propTypes = {something: PropTypes.any}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }]
+  }, {
+    // ES6 components as Component with boolean PropTypes
+    code: [
+      'class Hello extends Component {',
+      '  render () { return <div />; }',
+      '}',
+      'Hello.propTypes = {isSomething: PropTypes.bool}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }]
+  }, {
+    // ES6 components with static class properties and PropTypes
+    code: [
+      'class Hello extends React.Component {',
+      '  static propTypes = {isSomething: PropTypes.bool};',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint'
+  }, {
+    // ES6 components with static class properties and React.PropTypes
+    code: [
+      'class Hello extends React.Component {',
+      '  static propTypes = {isSomething: React.PropTypes.bool};',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint'
+  }, {
+    // ES6 components with static class properties an non-booleans
+    code: [
+      'class Hello extends React.Component {',
+      '  static propTypes = {something: PropTypes.any};',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint'
+  }, {
+    // ES6 components and Flowtype booleans
+    code: [
+      'class Hello extends React.Component {',
+      '  props: {isSomething: boolean};',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint'
+  }, {
+    // ES6 components and Flowtype non-booleans
+    code: [
+      'class Hello extends React.Component {',
+      '  props: {something: any};',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint'
+  }, {
+    // Stateless components
+    code: [
+      'var Hello = ({isSomething}) => { return <div /> }',
+      'Hello.propTypes = {isSomething: PropTypes.bool};'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint'
+  }, {
+    // Functional components and Flowtype booleans
+    code: [
+      'type Props = {',
+      '  isSomething: boolean;',
+      '};',
+      'function Hello(props: Props): React.Element { return <div /> }'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint'
+  }, {
+    // Custom `propTypeNames` option
+    code: [
+      'class Hello extends React.Component {',
+      '  static propTypes = {',
+      '    isSomething: PropTypes.mutuallyExclusiveTrueProps,',
+      '    something: PropTypes.bool',
+      '  };',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      propTypeNames: ['mutuallyExclusiveTrueProps'],
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint'
+  }, {
+    // Custom PropTypes that are specified as variables
+    code: [
+      'class Hello extends React.Component {',
+      '  static propTypes = {',
+      '    isSomething: mutuallyExclusiveTrueProps,',
+      '    isSomethingElse: bool',
+      '  };',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      propTypeNames: ['bool', 'mutuallyExclusiveTrueProps'],
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint'
+  }],
+
+  invalid: [{
+    // createReactClass components with PropTypes
+    code: [
+      'var Hello = createReactClass({',
+      '  propTypes: {something: PropTypes.bool},',
+      '  render: function() { return <div />; }',
+      '});'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // createReactClass components with React.PropTypes
+    code: [
+      'var Hello = createReactClass({',
+      '  propTypes: {something: React.PropTypes.bool},',
+      '  render: function() { return <div />; }',
+      '});'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // React.createClass components with PropTypes
+    code: [
+      'var Hello = React.createClass({',
+      '  propTypes: {something: PropTypes.bool},',
+      '  render: function() { return <div />; }',
+      '});'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    settings: {
+      react: {
+        createClass: 'createClass'
+      }
+    },
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // ES6 components as React.Component with boolean PropTypes
+    code: [
+      'class Hello extends React.Component {',
+      '  render () { return <div />; }',
+      '}',
+      'Hello.propTypes = {something: PropTypes.bool}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // ES6 components as Component with non-boolean PropTypes
+    code: [
+      'class Hello extends Component {',
+      '  render () { return <div />; }',
+      '}',
+      'Hello.propTypes = {something: PropTypes.bool}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // ES6 components as React.Component with non-boolean PropTypes
+    code: [
+      'class Hello extends React.Component {',
+      '  static propTypes = {something: PropTypes.bool};',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint',
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // ES6 components and Flowtype non-booleans
+    code: [
+      'class Hello extends React.Component {',
+      '  props: {something: boolean};',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint',
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // Stateless components
+    code: [
+      'var Hello = ({something}) => { return <div /> }',
+      'Hello.propTypes = {something: PropTypes.bool};'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint',
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // Functional components and Flowtype booleans
+    code: [
+      'type Props = {',
+      '  something: boolean;',
+      '};',
+      'function Hello(props: Props): React.Element { return <div /> }'
+    ].join('\n'),
+    options: [{
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint',
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // Custom `propTypeNames` option
+    code: [
+      'class Hello extends React.Component {',
+      '  static propTypes = {something: PropTypes.mutuallyExclusiveTrueProps};',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      propTypeNames: ['bool', 'mutuallyExclusiveTrueProps'],
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint',
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // Should fail for every invalid prop
+    code: [
+      'class Hello extends React.Component {',
+      '  static propTypes = {',
+      '    something: PropTypes.mutuallyExclusiveTrueProps,',
+      '    somethingElse: PropTypes.bool',
+      '  };',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      propTypeNames: ['bool', 'mutuallyExclusiveTrueProps'],
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint',
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }, {
+      message: 'Prop name (somethingElse) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }, {
+    // Custom PropTypes that are specified as variables
+    code: [
+      'class Hello extends React.Component {',
+      '  static propTypes = {',
+      '    something: mutuallyExclusiveTrueProps,',
+      '    somethingElse: bool',
+      '  };',
+      '  render () { return <div />; }',
+      '}'
+    ].join('\n'),
+    options: [{
+      propTypeNames: ['bool', 'mutuallyExclusiveTrueProps'],
+      rule: '^is[A-Z]([A-Za-z0-9]?)+'
+    }],
+    parser: 'babel-eslint',
+    errors: [{
+      message: 'Prop name (something) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }, {
+      message: 'Prop name (somethingElse) doesn\'t match rule (^is[A-Z]([A-Za-z0-9]?)+)'
+    }]
+  }]
+});