Skip to content

Commit d76c2fb

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

File tree

5 files changed

+742
-1
lines changed

5 files changed

+742
-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. 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

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

0 commit comments

Comments
 (0)