-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New
boolean-prop-naming
rule for enforcing the naming of boolean pr…
…op types
- Loading branch information
Evgueni Naverniouk
committed
Jun 20, 2017
1 parent
c568d49
commit fe71deb
Showing
5 changed files
with
744 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]?)+" }] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = {}; | ||
} | ||
}; | ||
}) | ||
}; |
Oops, something went wrong.