Skip to content

Commit 9f3b740

Browse files
authored
Merge pull request #1264 from EvNaverniouk/boolean-prop-naming
New `boolean-prop-naming` rule for enforcing the naming of booleans
2 parents 589113f + da0affa commit 9f3b740

File tree

5 files changed

+745
-1
lines changed

5 files changed

+745
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
8181

8282
# List of supported rules
8383

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

docs/rules/boolean-prop-naming.md

+67
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. The default value for this option is set to: `"^(is|has)[A-Z]([A-Za-z0-9]?)+"` to enforce `is` and `has` prefixes.
48+
49+
For supporting "is" and "has" naming (default):
50+
51+
- isEnabled
52+
- isAFK
53+
- hasCondition
54+
- hasLOL
55+
56+
```jsx
57+
"react/boolean-prop-naming": ["error", { "rule": "^(is|has)[A-Z]([A-Za-z0-9]?)+" }]
58+
```
59+
60+
For supporting "is" naming:
61+
62+
- isEnabled
63+
- isAFK
64+
65+
```jsx
66+
"react/boolean-prop-naming": ["error", { "rule": "^is[A-Z]([A-Za-z0-9]?)+" }]
67+
```

index.js

+2-1
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

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

0 commit comments

Comments
 (0)