Skip to content

Commit

Permalink
add jsx-handler-names rule
Browse files Browse the repository at this point in the history
  • Loading branch information
jakemmarsh committed Nov 23, 2015
1 parent add052f commit eab3bd8
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions docs/rules/jsx-handler-names.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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,
Expand Down
85 changes: 85 additions & 0 deletions lib/rules/jsx-handler-names.js
Original file line number Diff line number Diff line change
@@ -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
}];
82 changes: 82 additions & 0 deletions tests/lib/rules/jsx-handler-names.js
Original file line number Diff line number Diff line change
@@ -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'}]
}]
});

0 comments on commit eab3bd8

Please sign in to comment.