Skip to content

Commit c9d8647

Browse files
author
Evgueni Naverniouk
committed
New boolean-prop-naming rule for enforcing the naming of boolean prop types
1 parent c568d49 commit c9d8647

File tree

5 files changed

+733
-1
lines changed

5 files changed

+733
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
8080

8181
# List of supported rules
8282

83+
* [react/boolean-prop-naming](docs/rules/boolean-prop-naming.md): Enforces consistent naming for boolean props
8384
* [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md): Prevent extraneous defaultProps on components
8485
* [react/display-name](docs/rules/display-name.md): Prevent missing `displayName` in a React component definition
8586
* [react/forbid-component-props](docs/rules/forbid-component-props.md): Forbid certain props on Components

docs/rules/boolean-prop-naming.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Enforces consistent naming for boolean props (react/boolean-prop-naming)
2+
3+
Allows you to enforce a consistent naming pattern for props which expect a boolean value.
4+
5+
## Rule Details
6+
7+
The following patterns are considered warnings:
8+
9+
```jsx
10+
var Hello = createReactClass({
11+
propTypes: {
12+
enabled: PropTypes.bool
13+
},
14+
render: function() { return <div />; };
15+
});
16+
```
17+
18+
The following patterns are not considered warnings:
19+
20+
```jsx
21+
var Hello = createReactClass({
22+
propTypes: {
23+
isEnabled: PropTypes.bool
24+
},
25+
render: function() { return <div />; };
26+
});
27+
```
28+
29+
## Rule Options
30+
31+
```js
32+
...
33+
"react/boolean-prop-naming": [<enabled>, { "propTypeNames": Array<string>, "rule": <string> }]
34+
...
35+
```
36+
37+
### `propTypeNames`
38+
39+
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:
40+
41+
```jsx
42+
"react/boolean-prop-naming": ["error", { "propTypeNames": ["bool", "mutuallyExclusiveTrueProps"] }]
43+
```
44+
45+
### `rule`
46+
47+
The RegExp pattern to use when validating the name of the prop. Some common examples are:
48+
49+
For supporting "is" naming:
50+
51+
- isEnabled
52+
- isAFK
53+
54+
```jsx
55+
"react/boolean-prop-naming": ["error", { "rule": "^is[A-Z]([A-Za-z0-9]?)+" }]
56+
```
57+
58+
For supporting "is" and "has" naming:
59+
60+
- isEnabled
61+
- isAFK
62+
- hasCondition
63+
- hasLOL
64+
65+
```jsx
66+
"react/boolean-prop-naming": ["error", { "rule": "^(is|has)[A-Z]([A-Za-z0-9]?)+" }]
67+
```

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ var allRules = {
6464
'no-children-prop': require('./lib/rules/no-children-prop'),
6565
'void-dom-elements-no-children': require('./lib/rules/void-dom-elements-no-children'),
6666
'jsx-tag-spacing': require('./lib/rules/jsx-tag-spacing'),
67-
'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update')
67+
'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'),
68+
'boolean-prop-naming': require('./lib/rules/boolean-prop-naming')
6869
};
6970

7071
function filterRules(rules, predicate) {

lib/rules/boolean-prop-naming.js

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/**
2+
* @fileoverview Enforces consistent naming for boolean props
3+
* @author Evgueni Naverniouk
4+
*/
5+
'use strict';
6+
7+
const has = require('has');
8+
const Components = require('../util/Components');
9+
10+
// ------------------------------------------------------------------------------
11+
// Rule Definition
12+
// ------------------------------------------------------------------------------
13+
14+
module.exports = {
15+
meta: {
16+
docs: {
17+
category: 'Stylistic Issues',
18+
description: 'Enforces consistent naming for boolean props',
19+
recommended: false
20+
},
21+
22+
schema: [{
23+
additionalProperties: false,
24+
properties: {
25+
propTypeNames: {
26+
items: {
27+
type: 'string'
28+
},
29+
type: 'array'
30+
},
31+
rule: {
32+
default: null,
33+
type: 'string'
34+
}
35+
},
36+
type: 'object'
37+
}]
38+
},
39+
40+
create: Components.detect(function(context, components, utils) {
41+
const sourceCode = context.getSourceCode();
42+
const config = context.options[0] || {};
43+
const rule = config.rule ? new RegExp(config.rule) : null;
44+
const propTypeNames = config.propTypeNames ? config.propTypeNames : ['bool'];
45+
46+
// Remembers all Flowtype object definitions
47+
var ObjectTypeAnnonations = {};
48+
49+
/**
50+
* Checks if node is `propTypes` declaration
51+
* @param {ASTNode} node The AST node being checked.
52+
* @returns {Boolean} True if node is `propTypes` declaration, false if not.
53+
*/
54+
function isPropTypesDeclaration(node) {
55+
// Special case for class properties
56+
// (babel-eslint does not expose property name so we have to rely on tokens)
57+
if (node.type === 'ClassProperty') {
58+
const tokens = context.getFirstTokens(node, 2);
59+
if (tokens[0].value === 'propTypes' || (tokens[1] && tokens[1].value === 'propTypes')) {
60+
return true;
61+
}
62+
// Flow support
63+
if (node.typeAnnotation && node.key.name === 'props') {
64+
return true;
65+
}
66+
return false;
67+
}
68+
69+
return Boolean(
70+
node &&
71+
node.name === 'propTypes'
72+
);
73+
}
74+
75+
/**
76+
* Returns the prop key to ensure we handle the following cases:
77+
* propTypes: {
78+
* full: React.PropTypes.bool,
79+
* short: PropTypes.bool,
80+
* direct: bool
81+
* }
82+
* @param {Object} node The node we're getting the name of
83+
*/
84+
function getPropKey(node) {
85+
if (node.value.property) {
86+
return node.value.property.name;
87+
}
88+
if (node.value.type === 'Identifier') {
89+
return node.value.name;
90+
}
91+
return null;
92+
}
93+
94+
/**
95+
* Returns the name of the given node (prop)
96+
* @param {Object} node The node we're getting the name of
97+
*/
98+
function getPropName(node) {
99+
// Due to this bug https://github.com/babel/babel-eslint/issues/307
100+
// we can't get the name of the Flow object key name. So we have
101+
// to hack around it for now.
102+
if (node.type === 'ObjectTypeProperty') {
103+
return sourceCode.getFirstToken(node).value;
104+
}
105+
106+
return node.key.name;
107+
}
108+
109+
/**
110+
* Checks and mark props with invalid naming
111+
* @param {Object} node The component node we're testing
112+
* @param {Array} proptypes A list of Property object (for each proptype defined)
113+
*/
114+
function validatePropNaming(node, proptypes) {
115+
const component = components.get(node) || node;
116+
const invalidProps = component.invalidProps || [];
117+
118+
proptypes.forEach(function (prop) {
119+
const propKey = getPropKey(prop);
120+
if (
121+
(
122+
prop.type === 'ObjectTypeProperty' &&
123+
prop.value.type === 'BooleanTypeAnnotation' &&
124+
getPropName(prop).search(rule) < 0
125+
) || (
126+
propKey &&
127+
propTypeNames.indexOf(propKey) >= 0 &&
128+
getPropName(prop).search(rule) < 0
129+
)
130+
) {
131+
invalidProps.push(prop);
132+
}
133+
});
134+
135+
components.set(node, {
136+
invalidProps: invalidProps
137+
});
138+
}
139+
140+
/**
141+
* Reports invalid prop naming
142+
* @param {Object} component The component to process
143+
*/
144+
function reportInvalidNaming(component) {
145+
component.invalidProps.forEach(function (propNode) {
146+
const propName = getPropName(propNode);
147+
context.report({
148+
node: propNode,
149+
message: `Prop name (${propName}) doesn't match specified rule (${config.rule})`,
150+
data: {
151+
component: propName
152+
}
153+
});
154+
});
155+
}
156+
157+
// --------------------------------------------------------------------------
158+
// Public
159+
// --------------------------------------------------------------------------
160+
161+
return {
162+
ClassProperty: function(node) {
163+
if (!rule || !isPropTypesDeclaration(node)) {
164+
return;
165+
}
166+
if (node.value && node.value.properties) {
167+
validatePropNaming(node, node.value.properties);
168+
}
169+
if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
170+
validatePropNaming(node, node.typeAnnotation.typeAnnotation.properties);
171+
}
172+
},
173+
174+
MemberExpression: function(node) {
175+
if (!rule || !isPropTypesDeclaration(node.property)) {
176+
return;
177+
}
178+
const component = utils.getRelatedComponent(node);
179+
if (!component) {
180+
return;
181+
}
182+
validatePropNaming(component.node, node.parent.right.properties);
183+
},
184+
185+
ObjectExpression: function(node) {
186+
if (!rule) {
187+
return;
188+
}
189+
190+
// Search for the proptypes declaration
191+
node.properties.forEach(function(property) {
192+
if (!isPropTypesDeclaration(property.key)) {
193+
return;
194+
}
195+
validatePropNaming(node, property.value.properties);
196+
});
197+
},
198+
199+
TypeAlias: function(node) {
200+
// Cache all ObjectType annotations, we will check them at the end
201+
if (node.right.type === 'ObjectTypeAnnotation') {
202+
ObjectTypeAnnonations[node.id.name] = node.right;
203+
}
204+
},
205+
206+
'Program:exit': function() {
207+
if (!rule) {
208+
return;
209+
}
210+
211+
const list = components.list();
212+
for (let component in list) {
213+
// If this is a functional component that uses a global type, check it
214+
if (
215+
list[component].node.type === 'FunctionDeclaration' &&
216+
list[component].node.params &&
217+
list[component].node.params.length &&
218+
list[component].node.params[0].typeAnnotation
219+
) {
220+
const typeNode = list[component].node.params[0].typeAnnotation;
221+
const propType = ObjectTypeAnnonations[typeNode.typeAnnotation.id.name];
222+
if (propType) {
223+
validatePropNaming(list[component].node, propType.properties);
224+
}
225+
}
226+
227+
if (!has(list, component) || (list[component].invalidProps || []).length) {
228+
reportInvalidNaming(list[component]);
229+
}
230+
}
231+
232+
// Reset cache
233+
ObjectTypeAnnonations = {};
234+
}
235+
};
236+
})
237+
};

0 commit comments

Comments
 (0)