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'}] + }] +});