Skip to content

Commit

Permalink
New boolean-prop-naming rule for enforcing the naming of boolean pr…
Browse files Browse the repository at this point in the history
…op types
  • Loading branch information
Evgueni Naverniouk committed Jun 20, 2017
1 parent c568d49 commit fe71deb
Show file tree
Hide file tree
Showing 5 changed files with 744 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,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
Expand Down
67 changes: 67 additions & 0 deletions docs/rules/boolean-prop-naming.md
Original file line number Diff line number Diff line change
@@ -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]?)+" }]
```
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
240 changes: 240 additions & 0 deletions lib/rules/boolean-prop-naming.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/**
* @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 ? config.propTypeNames : ['bool'];

// Remembers all Flowtype object definitions
var ObjectTypeAnnonations = {};

/**
* 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);
if (
(
prop.type === 'ObjectTypeProperty' &&
prop.value.type === 'BooleanTypeAnnotation' &&
getPropName(prop).search(rule) < 0
) || (
propKey &&
propTypeNames.indexOf(propKey) >= 0 &&
getPropName(prop).search(rule) < 0
)
) {
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 specified 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') {
ObjectTypeAnnonations[node.id.name] = node.right;
}
},

'Program:exit': function() {
if (!rule) {
return;
}

const list = components.list();
for (let component in list) {
// 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 = ObjectTypeAnnonations[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
ObjectTypeAnnonations = {};
}
};
})
};

0 comments on commit fe71deb

Please sign in to comment.