Skip to content

Commit d27f42e

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

File tree

5 files changed

+744
-1
lines changed

5 files changed

+744
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
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

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: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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+
if (
124+
(
125+
prop.type === 'ObjectTypeProperty' &&
126+
prop.value.type === 'BooleanTypeAnnotation' &&
127+
rule.exec(getPropName(prop)) === null
128+
) || (
129+
propKey &&
130+
propTypeNames.indexOf(propKey) >= 0 &&
131+
rule.exec(getPropName(prop)) === null
132+
)
133+
) {
134+
invalidProps.push(prop);
135+
}
136+
});
137+
138+
components.set(node, {
139+
invalidProps: invalidProps
140+
});
141+
}
142+
143+
/**
144+
* Reports invalid prop naming
145+
* @param {Object} component The component to process
146+
*/
147+
function reportInvalidNaming(component) {
148+
component.invalidProps.forEach(function (propNode) {
149+
const propName = getPropName(propNode);
150+
context.report({
151+
node: propNode,
152+
message: `Prop name (${propName}) doesn't match rule (${config.rule})`,
153+
data: {
154+
component: propName
155+
}
156+
});
157+
});
158+
}
159+
160+
// --------------------------------------------------------------------------
161+
// Public
162+
// --------------------------------------------------------------------------
163+
164+
return {
165+
ClassProperty: function(node) {
166+
if (!rule || !isPropTypesDeclaration(node)) {
167+
return;
168+
}
169+
if (node.value && node.value.properties) {
170+
validatePropNaming(node, node.value.properties);
171+
}
172+
if (node.typeAnnotation && node.typeAnnotation.typeAnnotation) {
173+
validatePropNaming(node, node.typeAnnotation.typeAnnotation.properties);
174+
}
175+
},
176+
177+
MemberExpression: function(node) {
178+
if (!rule || !isPropTypesDeclaration(node.property)) {
179+
return;
180+
}
181+
const component = utils.getRelatedComponent(node);
182+
if (!component) {
183+
return;
184+
}
185+
validatePropNaming(component.node, node.parent.right.properties);
186+
},
187+
188+
ObjectExpression: function(node) {
189+
if (!rule) {
190+
return;
191+
}
192+
193+
// Search for the proptypes declaration
194+
node.properties.forEach(function(property) {
195+
if (!isPropTypesDeclaration(property.key)) {
196+
return;
197+
}
198+
validatePropNaming(node, property.value.properties);
199+
});
200+
},
201+
202+
TypeAlias: function(node) {
203+
// Cache all ObjectType annotations, we will check them at the end
204+
if (node.right.type === 'ObjectTypeAnnotation') {
205+
objectTypeAnnotations.set(node.id.name, node.right);
206+
}
207+
},
208+
209+
'Program:exit': function() {
210+
if (!rule) {
211+
return;
212+
}
213+
214+
const list = components.list();
215+
Object.keys(list).forEach(function (component) {
216+
// If this is a functional component that uses a global type, check it
217+
if (
218+
list[component].node.type === 'FunctionDeclaration' &&
219+
list[component].node.params &&
220+
list[component].node.params.length &&
221+
list[component].node.params[0].typeAnnotation
222+
) {
223+
const typeNode = list[component].node.params[0].typeAnnotation;
224+
const propType = objectTypeAnnotations.get(typeNode.typeAnnotation.id.name);
225+
if (propType) {
226+
validatePropNaming(list[component].node, propType.properties);
227+
}
228+
}
229+
230+
if (!has(list, component) || (list[component].invalidProps || []).length) {
231+
reportInvalidNaming(list[component]);
232+
}
233+
});
234+
235+
// Reset cache
236+
objectTypeAnnotations.clear();
237+
}
238+
};
239+
})
240+
};

0 commit comments

Comments
 (0)