diff --git a/README.md b/README.md
index da14674c1e..f3d761e0e3 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 0000000000..f9c26fc186
--- /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
+
+```
+
+```js
+
+```
+
+The following patterns are not considered warnings:
+
+```js
+
+```
+
+```js
+
+```
+
+## Rule Options
+
+```js
+...
+"jsx-handler-names": [, {
+ "eventHandlerPrefix": ,
+ "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 a89ead305e..8de54d2280 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 0000000000..587f14c74a
--- /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.`
+ * @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
+}];
diff --git a/tests/lib/rules/jsx-handler-names.js b/tests/lib/rules/jsx-handler-names.js
new file mode 100644
index 0000000000..d4873c0dab
--- /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: [
+ ''
+ ].join('\n'),
+ ecmaFeatures: {
+ jsx: true
+ }
+ }, {
+ code: [
+ ''
+ ].join('\n'),
+ ecmaFeatures: {
+ jsx: true
+ }
+ }, {
+ code: [
+ ''
+ ].join('\n'),
+ ecmaFeatures: {
+ jsx: true
+ }
+ }, {
+ code: [
+ ''
+ ].join('\n'),
+ ecmaFeatures: {
+ jsx: true
+ }
+ }, {
+ code: [
+ ''
+ ].join('\n'),
+ ecmaFeatures: {
+ jsx: true
+ }
+ }],
+
+ invalid: [{
+ code: [
+ ''
+ ].join('\n'),
+ ecmaFeatures: {
+ jsx: true
+ },
+ errors: [{message: 'Handler function for onChange prop key must be named handleChange'}]
+ }, {
+ code: [
+ ''
+ ].join('\n'),
+ ecmaFeatures: {
+ jsx: true
+ },
+ errors: [{message: 'Prop key for handleChange must be named onChange'}]
+ }, {
+ code: [
+ ''
+ ].join('\n'),
+ ecmaFeatures: {
+ jsx: true
+ },
+ errors: [{message: 'Handler function for onChange prop key must be named handleChange'}]
+ }]
+});