Repository: yannickcr/eslint-plugin-react
Pull request #315 (2015-11-17 21:33:33)

jsx-handler-names
Add a new rule to enforce prefixing of event handler methods, both on components and when passed as props.

yannickcr: Looks good to me. Can you squash your commits? Thanks.

Patch:
diff --git a/README.md b/README.md
index da14674c..f3d761e0 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ Finally, enable all of the rules that you would like to use.
     "react/jsx-boolean-value": 1,
     "react/jsx-closing-bracket-location": 1,
     "react/jsx-curly-spacing": 1,
+    "react/jsx-handler-names": 1,
     "react/jsx-indent-props": 1,
     "react/jsx-key": 1,
     "react/jsx-max-props-per-line": 1,
@@ -89,6 +90,7 @@ Finally, enable all of the rules that you would like to use.
 * [jsx-boolean-value](docs/rules/jsx-boolean-value.md): Enforce boolean attributes notation in JSX
 * [jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md): Validate closing bracket location in JSX
 * [jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes
+* [jsx-handler-names](docs/rules/jsx-handler-names.md): Enforce event handler naming conventions in JSX
 * [jsx-indent-props](docs/rules/jsx-indent-props.md): Validate props indentation in JSX
 * [jsx-key](docs/rules/jsx-key.md): Validate JSX has key prop when in array or iterator
 * [jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md): Limit maximum of props on a single line in JSX
diff --git a/docs/rules/jsx-handler-names.md b/docs/rules/jsx-handler-names.md
new file mode 100644
index 00000000..f9c26fc1
--- /dev/null
+++ b/docs/rules/jsx-handler-names.md
@@ -0,0 +1,43 @@
+# Enforce event handler naming conventions in JSX (jsx-handler-names)
+
+Ensures that any component or prop methods used to handle events are correctly prefixed.
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```js
+<MyComponent handleChange={this.handleChange} />
+```
+
+```js
+<MyComponent onChange={this.componentChanged} />
+```
+
+The following patterns are not considered warnings:
+
+```js
+<MyComponent onChange={this.handleChange} />
+```
+
+```js
+<MyComponent onChange={this.props.onFoo} />
+```
+
+## Rule Options
+
+```js
+...
+"jsx-handler-names": [<enabled>, {
+  "eventHandlerPrefix": <eventHandlerPrefix>,
+  "eventHandlerPropPrefix": <eventHandlerPropPrefix>
+}]
+...
+```
+
+* `eventHandlerPrefix`: Prefix for component methods used as event handlers. Defaults to `handle`
+* `eventHandlerPropPrefix`: Prefix for props that are used as event handlers. Defaults to `on`
+
+## When Not To Use It
+
+If you are not using JSX, or if you don't want to enforce specific naming conventions for event handlers.
\ No newline at end of file
diff --git a/index.js b/index.js
index a89ead30..8de54d22 100644
--- a/index.js
+++ b/index.js
@@ -14,6 +14,7 @@ module.exports = {
     'no-did-update-set-state': require('./lib/rules/no-did-update-set-state'),
     'react-in-jsx-scope': require('./lib/rules/react-in-jsx-scope'),
     'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'),
+    'jsx-handler-names': require('./lib/rules/jsx-handler-names'),
     'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
     'jsx-no-bind': require('./lib/rules/jsx-no-bind'),
     'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
@@ -48,6 +49,7 @@ module.exports = {
     'no-did-update-set-state': 0,
     'react-in-jsx-scope': 0,
     'jsx-uses-vars': 1,
+    'jsx-handler-names': 0,
     'jsx-pascal-case': 0,
     'jsx-no-bind': 0,
     'jsx-no-undef': 0,
diff --git a/lib/rules/jsx-handler-names.js b/lib/rules/jsx-handler-names.js
new file mode 100644
index 00000000..587f14c7
--- /dev/null
+++ b/lib/rules/jsx-handler-names.js
@@ -0,0 +1,85 @@
+/**
+ * @fileoverview Enforce event handler naming conventions in JSX
+ * @author Jake Marsh
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = function(context) {
+
+  var configuration = context.options[0] || {};
+  var eventHandlerPrefix = configuration.eventHandlerPrefix || 'handle';
+  var eventHandlerPropPrefix = configuration.eventHandlerPropPrefix || 'on';
+
+  var EVENT_HANDLER_REGEX = new RegExp('^((this\.props\.' + eventHandlerPropPrefix + ')'
+                                       + '|((.*\.)?' + eventHandlerPrefix + ')).+$');
+  var PROP_EVENT_HANDLER_REGEX = new RegExp('^' + eventHandlerPropPrefix + '.+$');
+
+  /**
+   * Get full prop value for a handler, i.e. `this.props.<name>`
+   * @param {Object} node.value.expression for JSXAttribute
+   * @return {String} Full prop value
+   */
+  function rebuildPropValue(valueNode) {
+    var valueNodeObject = valueNode.object;
+    var subObjectType = valueNodeObject.object ? valueNodeObject.object.type : '';
+    var propertyName = valueNodeObject.property ? valueNodeObject.property.name : '';
+    var propValue = valueNode.property ? valueNode.property.name : '';
+
+    if (propertyName.length) {
+      propValue = propertyName + '.' + propValue;
+    }
+
+    if (subObjectType === 'ThisExpression') {
+      propValue = 'this.' + propValue;
+    }
+
+    return propValue;
+  }
+
+  return {
+    JSXAttribute: function(node) {
+      if (!node.value || !node.value.expression || !node.value.expression.object) {
+        return;
+      }
+
+      var propKey = typeof node.name === 'object' ? node.name.name : node.name;
+      var propValue = rebuildPropValue(node.value.expression);
+
+      var propIsEventHandler = PROP_EVENT_HANDLER_REGEX.test(propKey);
+      var propFnIsNamedCorrectly = EVENT_HANDLER_REGEX.test(propValue);
+      var eventName;
+
+      if (propIsEventHandler && !propFnIsNamedCorrectly) {
+        eventName = propKey.split(eventHandlerPropPrefix)[1];
+        context.report(
+          node,
+          'Handler function for ' + propKey + ' prop key must be named ' + eventHandlerPrefix + eventName
+        );
+      } else if (propFnIsNamedCorrectly && !propIsEventHandler) {
+        eventName = propValue.split(eventHandlerPrefix)[1];
+        context.report(
+          node,
+          'Prop key for ' + propValue + ' must be named ' + eventHandlerPropPrefix + eventName
+        );
+      }
+    }
+  };
+
+};
+
+module.exports.schema = [{
+  type: 'object',
+  properties: {
+    eventHandlerPrefix: {
+      type: 'string'
+    },
+    eventHandlerPropPrefix: {
+      type: 'string'
+    }
+  },
+  additionalProperties: false
+}];


Test patch:
diff --git a/tests/lib/rules/jsx-handler-names.js b/tests/lib/rules/jsx-handler-names.js
new file mode 100644
index 00000000..d4873c0d
--- /dev/null
+++ b/tests/lib/rules/jsx-handler-names.js
@@ -0,0 +1,82 @@
+/**
+ * @fileoverview Tests for jsx-handler-names
+ * @author Jake Marsh
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/jsx-handler-names');
+var RuleTester = require('eslint').RuleTester;
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run('jsx-handler-names', rule, {
+  valid: [{
+    code: [
+      '<TestComponent onChange={this.handleChange} />'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    }
+  }, {
+    code: [
+      '<TestComponent onChange={this.props.onChange} />'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    }
+  }, {
+    code: [
+      '<TestComponent onChange={this.props.onFoo} />'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    }
+  }, {
+    code: [
+      '<TestComponent isSelected={this.props.isSelected} />'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    }
+  }, {
+    code: [
+      '<TestComponent shouldDisplay={this.state.shouldDisplay} />'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    }
+  }],
+
+  invalid: [{
+    code: [
+      '<TestComponent onChange={this.doSomethingOnChange} />'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    },
+    errors: [{message: 'Handler function for onChange prop key must be named handleChange'}]
+  }, {
+    code: [
+      '<TestComponent handleChange={this.handleChange} />'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    },
+    errors: [{message: 'Prop key for handleChange must be named onChange'}]
+  }, {
+    code: [
+      '<TestComponent onChange={this.onChange} />'
+    ].join('\n'),
+    ecmaFeatures: {
+      jsx: true
+    },
+    errors: [{message: 'Handler function for onChange prop key must be named handleChange'}]
+  }]
+});