eslint-plugin-react
Advanced tools
Comparing version 3.5.1 to 3.6.0
@@ -6,2 +6,30 @@ # Change Log | ||
## [3.6.0] - 2015-10-18 | ||
### Added | ||
* Add support for stateless function components to `display-name` and `prop-types` ([#237][]) | ||
* Add callbacksLast option to `jsx-sort-props` and `jsx-sort-prop-types` ([#242][] @Daniel15) | ||
* Add `prefer-es6-class` rule ([#247][] @hamiltondanielb) | ||
### Fixed | ||
* Fix `forbid-prop-types` crash with destructured PropTypes ([#230][] @epmatsw) | ||
* Fix `forbid-prop-types` to do not modify AST directly ([#249][] @rhysd) | ||
* Fix `prop-types` crash with empty destructuring ([#251][]) | ||
* Fix `prop-types` to not validate computed keys in destructuring ([#236][]) | ||
### Changed | ||
* Update dependencies | ||
* Improve components detection ([#233][]) | ||
* Documentation improvements ([#248][] @dguo) | ||
[3.6.0]: https://github.com/yannickcr/eslint-plugin-react/compare/v3.5.1...v3.6.0 | ||
[#237]: https://github.com/yannickcr/eslint-plugin-react/issues/237 | ||
[#242]: https://github.com/yannickcr/eslint-plugin-react/pull/242 | ||
[#247]: https://github.com/yannickcr/eslint-plugin-react/issues/247 | ||
[#230]: https://github.com/yannickcr/eslint-plugin-react/issues/230 | ||
[#249]: https://github.com/yannickcr/eslint-plugin-react/issues/249 | ||
[#251]: https://github.com/yannickcr/eslint-plugin-react/issues/251 | ||
[#236]: https://github.com/yannickcr/eslint-plugin-react/issues/236 | ||
[#233]: https://github.com/yannickcr/eslint-plugin-react/issues/233 | ||
[#248]: https://github.com/yannickcr/eslint-plugin-react/pull/248 | ||
## [3.5.1] - 2015-10-01 | ||
@@ -8,0 +36,0 @@ ### Fixed |
@@ -32,3 +32,4 @@ 'use strict'; | ||
'no-direct-mutation-state': require('./lib/rules/no-direct-mutation-state'), | ||
'forbid-prop-types': require('./lib/rules/forbid-prop-types') | ||
'forbid-prop-types': require('./lib/rules/forbid-prop-types'), | ||
'prefer-es6-class': require('./lib/rules/prefer-es6-class') | ||
}, | ||
@@ -63,4 +64,5 @@ rulesConfig: { | ||
'no-direct-mutation-state': 0, | ||
'forbid-prop-types': 0 | ||
'forbid-prop-types': 0, | ||
'prefer-es6-class': 0 | ||
} | ||
}; |
@@ -25,15 +25,2 @@ /** | ||
/** | ||
* Checks if the component must be validated | ||
* @param {Object} component The component to process | ||
* @returns {Boolean} True if the component must be validated, false if not. | ||
*/ | ||
function mustBeValidated(component) { | ||
return ( | ||
component && | ||
component.isReactComponent && | ||
!component.hasDisplayName | ||
); | ||
} | ||
/** | ||
* Checks if we are declaring a display name | ||
@@ -44,3 +31,2 @@ * @param {ASTNode} node The AST node being checked. | ||
function isDisplayNameDeclaration(node) { | ||
// Special case for class properties | ||
@@ -131,3 +117,2 @@ // (babel-eslint does not expose property name so we have to rely on tokens) | ||
} | ||
markDisplayNameAsDeclared(node); | ||
@@ -155,2 +140,3 @@ }, | ||
ClassDeclaration: function(node) { | ||
componentList.set(context, node); | ||
if (!acceptTranspilerName || !hasTranspilerName(node)) { | ||
@@ -163,30 +149,25 @@ return; | ||
ObjectExpression: function(node) { | ||
// Search for the displayName declaration | ||
node.properties.forEach(function(property) { | ||
if (!property.key) { | ||
return; | ||
} | ||
if (!isDisplayNameDeclaration(property.key)) { | ||
return; | ||
} | ||
markDisplayNameAsDeclared(node); | ||
}); | ||
// Has transpiler name | ||
if (acceptTranspilerName && hasTranspilerName(node)) { | ||
markDisplayNameAsDeclared(node); | ||
} | ||
if (componentUtil.isComponentDefinition(node)) { | ||
componentList.set(context, node, { | ||
isReactComponent: true | ||
componentList.set(context, node); | ||
if (!acceptTranspilerName || !hasTranspilerName(node)) { | ||
// Search for the displayName declaration | ||
node.properties.forEach(function(property) { | ||
if (!property.key || !isDisplayNameDeclaration(property.key)) { | ||
return; | ||
} | ||
markDisplayNameAsDeclared(node); | ||
}); | ||
return; | ||
} | ||
markDisplayNameAsDeclared(node); | ||
}, | ||
ReturnStatement: function(node) { | ||
componentList.set(context, node); | ||
}, | ||
'Program:exit': function() { | ||
var list = componentList.getList(); | ||
// Report missing display name for all classes | ||
// Report missing display name for all components | ||
for (var component in list) { | ||
if (!list.hasOwnProperty(component) || !mustBeValidated(list[component])) { | ||
if (!list.hasOwnProperty(component) || list[component].hasDisplayName) { | ||
continue; | ||
@@ -196,14 +177,4 @@ } | ||
} | ||
}, | ||
ReturnStatement: function(node) { | ||
if (!componentUtil.isReactComponent(context, node)) { | ||
return; | ||
} | ||
componentList.set(context, node, { | ||
isReactComponent: true | ||
}); | ||
} | ||
}; | ||
}; | ||
@@ -210,0 +181,0 @@ |
@@ -56,20 +56,26 @@ /** | ||
declarations.forEach(function(declaration) { | ||
var target; | ||
var value = declaration.value; | ||
if ( | ||
declaration.value.type === 'MemberExpression' && | ||
declaration.value.property && | ||
declaration.value.property.name && | ||
declaration.value.property.name === 'isRequired' | ||
value.type === 'MemberExpression' && | ||
value.property && | ||
value.property.name && | ||
value.property.name === 'isRequired' | ||
) { | ||
declaration.value = declaration.value.object; | ||
value = value.object; | ||
} | ||
if ( | ||
declaration.value.type === 'CallExpression' && | ||
declaration.value.callee.type === 'MemberExpression' | ||
value.type === 'CallExpression' && | ||
value.callee.type === 'MemberExpression' | ||
) { | ||
declaration.value = declaration.value.callee; | ||
value = value.callee; | ||
} | ||
if (isForbidden(declaration.value.property.name)) { | ||
context.report(declaration, 'Prop type `' + declaration.value.property.name + '` is forbidden'); | ||
if (value.property) { | ||
target = value.property.name; | ||
} else if (value.type === 'Identifier') { | ||
target = value.name; | ||
} | ||
if (isForbidden(target)) { | ||
context.report(declaration, 'Prop type `' + target + '` is forbidden'); | ||
} | ||
}); | ||
@@ -76,0 +82,0 @@ } |
@@ -13,2 +13,3 @@ /** | ||
var configuration = context.options[0] || {}; | ||
var callbacksLast = configuration.callbacksLast || false; | ||
var ignoreCase = configuration.ignoreCase || false; | ||
@@ -43,2 +44,6 @@ | ||
function isCallbackPropName(propName) { | ||
return /^on[A-Z]/.test(propName); | ||
} | ||
/** | ||
@@ -52,10 +57,24 @@ * Checks if propTypes declarations are sorted | ||
var prevPropName = getKey(prev); | ||
var currenPropName = getKey(curr); | ||
var currentPropName = getKey(curr); | ||
var previousIsCallback = isCallbackPropName(prevPropName); | ||
var currentIsCallback = isCallbackPropName(currentPropName); | ||
if (ignoreCase) { | ||
prevPropName = prevPropName.toLowerCase(); | ||
currenPropName = currenPropName.toLowerCase(); | ||
currentPropName = currentPropName.toLowerCase(); | ||
} | ||
if (currenPropName < prevPropName) { | ||
if (callbacksLast) { | ||
if (!previousIsCallback && currentIsCallback) { | ||
// Entering the callback prop section | ||
return curr; | ||
} | ||
if (previousIsCallback && !currentIsCallback) { | ||
// Encountered a non-callback prop after a callback prop | ||
context.report(prev, 'Callback prop types must be listed after all other prop types'); | ||
return prev; | ||
} | ||
} | ||
if (currentPropName < prevPropName) { | ||
context.report(curr, 'Prop types declarations should be sorted alphabetically'); | ||
@@ -106,2 +125,5 @@ return prev; | ||
properties: { | ||
callbacksLast: { | ||
type: 'boolean' | ||
}, | ||
ignoreCase: { | ||
@@ -108,0 +130,0 @@ type: 'boolean' |
@@ -11,2 +11,6 @@ /** | ||
function isCallbackPropName(propName) { | ||
return /^on[A-Z]/.test(propName); | ||
} | ||
module.exports = function(context) { | ||
@@ -16,2 +20,3 @@ | ||
var ignoreCase = configuration.ignoreCase || false; | ||
var callbacksLast = configuration.callbacksLast || false; | ||
@@ -25,11 +30,25 @@ return { | ||
var lastPropName = memo.name.name; | ||
var currenPropName = decl.name.name; | ||
var previousPropName = memo.name.name; | ||
var currentPropName = decl.name.name; | ||
var previousIsCallback = isCallbackPropName(previousPropName); | ||
var currentIsCallback = isCallbackPropName(currentPropName); | ||
if (ignoreCase) { | ||
lastPropName = lastPropName.toLowerCase(); | ||
currenPropName = currenPropName.toLowerCase(); | ||
previousPropName = previousPropName.toLowerCase(); | ||
currentPropName = currentPropName.toLowerCase(); | ||
} | ||
if (currenPropName < lastPropName) { | ||
if (callbacksLast) { | ||
if (!previousIsCallback && currentIsCallback) { | ||
// Entering the callback prop section | ||
return decl; | ||
} | ||
if (previousIsCallback && !currentIsCallback) { | ||
// Encountered a non-callback prop after a callback prop | ||
context.report(memo, 'Callbacks must be listed after all other props'); | ||
return memo; | ||
} | ||
} | ||
if (currentPropName < previousPropName) { | ||
context.report(decl, 'Props should be sorted alphabetically'); | ||
@@ -48,2 +67,7 @@ return memo; | ||
properties: { | ||
// Whether callbacks (prefixed with "on") should be listed at the very end, | ||
// after all other props. | ||
callbacksLast: { | ||
type: 'boolean' | ||
}, | ||
ignoreCase: { | ||
@@ -50,0 +74,0 @@ type: 'boolean' |
@@ -24,6 +24,3 @@ /** | ||
function isValid(component) { | ||
var isNotReactComponent = Boolean(component && !component.isReactComponent); | ||
var doNotMutateSetState = Boolean(component && !component.mutateSetState); | ||
return isNotReactComponent || doNotMutateSetState; | ||
return Boolean(component && !component.mutateSetState); | ||
} | ||
@@ -49,2 +46,10 @@ | ||
ObjectExpression: function(node) { | ||
componentList.set(context, node); | ||
}, | ||
ClassDeclaration: function(node) { | ||
componentList.set(context, node); | ||
}, | ||
AssignmentExpression: function(node) { | ||
@@ -81,11 +86,2 @@ var item; | ||
} | ||
}, | ||
ReturnStatement: function(node) { | ||
if (!componentUtil.isReactComponent(context, node)) { | ||
return; | ||
} | ||
componentList.set(context, node, { | ||
isReactComponent: true | ||
}); | ||
} | ||
@@ -92,0 +88,0 @@ }; |
@@ -25,2 +25,11 @@ /** | ||
return { | ||
ClassDeclaration: function(node) { | ||
componentList.set(context, node); | ||
}, | ||
ObjectExpression: function(node) { | ||
componentList.set(context, node); | ||
}, | ||
'Program:exit': function() { | ||
@@ -40,9 +49,2 @@ if (componentList.count() <= 1) { | ||
} | ||
}, | ||
ReturnStatement: function(node) { | ||
if (!componentUtil.isReactComponent(context, node)) { | ||
return; | ||
} | ||
componentList.set(context, node); | ||
} | ||
@@ -49,0 +51,0 @@ }; |
@@ -34,6 +34,5 @@ /** | ||
function isPropTypesUsage(node) { | ||
return Boolean( | ||
node.object.type === 'ThisExpression' && | ||
node.property.name === 'props' | ||
); | ||
var isClassUsage = node.object.type === 'ThisExpression' && node.property.name === 'props'; | ||
var isStatelessFunctionUsage = node.object.name === 'props'; | ||
return isClassUsage || isStatelessFunctionUsage; | ||
} | ||
@@ -94,3 +93,2 @@ | ||
component && | ||
component.isReactComponent && | ||
component.usedPropTypes && | ||
@@ -320,2 +318,5 @@ !component.ignorePropsValidation | ||
function getPropertyName(node) { | ||
if (componentUtil.getNode(context, node)) { | ||
node = node.parent; | ||
} | ||
var property = node.property; | ||
@@ -358,3 +359,3 @@ if (property) { | ||
case 'MemberExpression': | ||
name = getPropertyName(node.parent); | ||
name = getPropertyName(node); | ||
if (name) { | ||
@@ -370,2 +371,3 @@ allNames = parentNames.concat(name); | ||
node.parent.id.properties && | ||
node.parent.id.properties.length && | ||
getKeyValue(node.parent.id.properties[0]) | ||
@@ -406,3 +408,3 @@ ) { | ||
allNames: allNames, | ||
node: node.parent.property | ||
node: node.object.name !== 'props' ? node.parent.property : node.property | ||
}); | ||
@@ -412,3 +414,3 @@ break; | ||
for (var k = 0, l = properties.length; k < l; k++) { | ||
if (hasSpreadOperator(properties[k])) { | ||
if (hasSpreadOperator(properties[k]) || properties[k].computed) { | ||
continue; | ||
@@ -529,2 +531,6 @@ } | ||
ClassDeclaration: function(node) { | ||
componentList.set(context, node); | ||
}, | ||
ClassProperty: function(node) { | ||
@@ -587,2 +593,3 @@ if (!isPropTypesDeclaration(node)) { | ||
ObjectExpression: function(node) { | ||
componentList.set(context, node); | ||
// Search for the proptypes declaration | ||
@@ -595,8 +602,6 @@ node.properties.forEach(function(property) { | ||
}); | ||
}, | ||
if (componentUtil.isComponentDefinition(node)) { | ||
componentList.set(context, node, { | ||
isReactComponent: true | ||
}); | ||
} | ||
ReturnStatement: function(node) { | ||
componentList.set(context, node); | ||
}, | ||
@@ -613,11 +618,2 @@ | ||
} | ||
}, | ||
ReturnStatement: function(node) { | ||
if (!componentUtil.isReactComponent(context, node)) { | ||
return; | ||
} | ||
componentList.set(context, node, { | ||
isReactComponent: true | ||
}); | ||
} | ||
@@ -624,0 +620,0 @@ }; |
@@ -88,3 +88,2 @@ /** | ||
component && | ||
component.isReactComponent && | ||
!component.hasDisplayName | ||
@@ -350,5 +349,13 @@ ); | ||
return { | ||
ClassDeclaration: function(node) { | ||
componentList.set(context, node); | ||
}, | ||
ObjectExpression: function(node) { | ||
componentList.set(context, node); | ||
}, | ||
'Program:exit': function() { | ||
var list = componentList.getList(); | ||
for (var component in list) { | ||
@@ -363,11 +370,2 @@ if (!list.hasOwnProperty(component) || !mustBeValidated(list[component])) { | ||
reportErrors(); | ||
}, | ||
ReturnStatement: function(node) { | ||
if (!componentUtil.isReactComponent(context, node)) { | ||
return; | ||
} | ||
componentList.set(context, node, { | ||
isReactComponent: true | ||
}); | ||
} | ||
@@ -374,0 +372,0 @@ }; |
@@ -12,18 +12,45 @@ /** | ||
/** | ||
* Detect if we are in a React Component | ||
* A React component is defined has an object/class with a property "render" | ||
* that return a JSXElement or null/false | ||
* Detect if the node is a component definition | ||
* @param {Object} context The current rule context. | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {Boolean} True if we are in a React Component, false if not. | ||
* @returns {Boolean} True the node is a component definition, false if not. | ||
*/ | ||
function isReactComponent(context, node) { | ||
function isComponentDefinition(context, node) { | ||
switch (node.type) { | ||
case 'ObjectExpression': | ||
if (node.parent && node.parent.callee && context.getSource(node.parent.callee) === 'React.createClass') { | ||
return true; | ||
} | ||
break; | ||
case 'ClassDeclaration': | ||
var superClass = node.superClass && context.getSource(node.superClass); | ||
if (superClass === 'Component' || superClass === 'React.Component') { | ||
return true; | ||
} | ||
break; | ||
default: | ||
break; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Check if we are in a stateless function component | ||
* @param {Object} context The current rule context. | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {Boolean} True if we are in a stateless function component, false if not. | ||
*/ | ||
function isStatelessFunctionComponent(context, node) { | ||
if (node.type !== 'ReturnStatement') { | ||
throw new Error('React Component detection must be done from a ReturnStatement ASTNode'); | ||
return false; | ||
} | ||
var scope = context.getScope(); | ||
while (scope.upper && scope.type !== 'function') { | ||
while (scope) { | ||
if (scope.type === 'class') { | ||
return false; | ||
} | ||
scope = scope.upper; | ||
} | ||
var returnsJSX = | ||
@@ -39,32 +66,19 @@ node.argument && | ||
; | ||
var isComponentRender = | ||
(returnsJSX || returnsReactCreateElement) && | ||
scope.block.parent.key && scope.block.parent.key.name === 'render' | ||
; | ||
var isEmptyComponentRender = | ||
node.argument && | ||
node.argument.type === 'Literal' && (node.argument.value === null || node.argument.value === false) && | ||
scope.block.parent.key && scope.block.parent.key.name === 'render' | ||
; | ||
return Boolean(isEmptyComponentRender || isComponentRender); | ||
return Boolean(returnsJSX || returnsReactCreateElement); | ||
} | ||
/** | ||
* Detect if the node is a component definition | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {Boolean} True the node is a component definition, false if not. | ||
* Get the identifiers of a React component ASTNode | ||
* @param {ASTNode} node The React component ASTNode being checked. | ||
* @returns {Object} The component identifiers. | ||
*/ | ||
function isComponentDefinition(node) { | ||
var isES6Component = node.type === 'ClassDeclaration'; | ||
var isES5Component = Boolean( | ||
node.type === 'ObjectExpression' && | ||
node.parent && | ||
node.parent.callee && | ||
node.parent.callee.object && | ||
node.parent.callee.property && | ||
node.parent.callee.object.name === 'React' && | ||
node.parent.callee.property.name === 'createClass' | ||
); | ||
return isES5Component || isES6Component; | ||
function getIdentifiers(node) { | ||
var name = node.id && node.id.name || DEFAULT_COMPONENT_NAME; | ||
var id = name + ':' + node.loc.start.line + ':' + node.loc.start.column; | ||
return { | ||
id: id, | ||
name: name | ||
}; | ||
} | ||
@@ -78,4 +92,3 @@ | ||
*/ | ||
function getNode(context, node) { | ||
var componentNode = null; | ||
function getNode(context, node, list) { | ||
var ancestors = context.getAncestors().reverse(); | ||
@@ -86,24 +99,21 @@ | ||
for (var i = 0, j = ancestors.length; i < j; i++) { | ||
if (isComponentDefinition(ancestors[i])) { | ||
componentNode = ancestors[i]; | ||
break; | ||
if (isComponentDefinition(context, ancestors[i])) { | ||
return ancestors[i]; | ||
} | ||
// Node is already in the component list | ||
var identifiers = getIdentifiers(ancestors[i]); | ||
if (list && list[identifiers.id]) { | ||
return ancestors[i]; | ||
} | ||
} | ||
return componentNode; | ||
} | ||
if (isStatelessFunctionComponent(context, node)) { | ||
var scope = context.getScope(); | ||
while (scope.upper && scope.type !== 'function') { | ||
scope = scope.upper; | ||
} | ||
return scope.block; | ||
} | ||
/** | ||
* Get the identifiers of a React component ASTNode | ||
* @param {ASTNode} node The React component ASTNode being checked. | ||
* @returns {Object} The component identifiers. | ||
*/ | ||
function getIdentifiers(node) { | ||
var name = node.id && node.id.name || DEFAULT_COMPONENT_NAME; | ||
var id = name + ':' + node.loc.start.line + ':' + node.loc.start.column; | ||
return { | ||
id: id, | ||
name: name | ||
}; | ||
return null; | ||
} | ||
@@ -166,6 +176,7 @@ | ||
List.prototype.set = function(context, node, customProperties) { | ||
var componentNode = getNode(context, node); | ||
var componentNode = getNode(context, node, this._list); | ||
if (!componentNode) { | ||
return null; | ||
} | ||
var identifiers = getIdentifiers(componentNode); | ||
@@ -219,3 +230,2 @@ | ||
DEFAULT_COMPONENT_NAME: DEFAULT_COMPONENT_NAME, | ||
isReactComponent: isReactComponent, | ||
getNode: getNode, | ||
@@ -222,0 +232,0 @@ isComponentDefinition: isComponentDefinition, |
{ | ||
"name": "eslint-plugin-react", | ||
"version": "3.5.1", | ||
"version": "3.6.0", | ||
"author": "Yannick Croissant <yannick.croissant+npm@gmail.com>", | ||
@@ -28,4 +28,4 @@ "description": "React specific linting rules for ESLint", | ||
"coveralls": "2.11.4", | ||
"eslint": "1.5.1", | ||
"istanbul": "0.3.21", | ||
"eslint": "1.7.1", | ||
"istanbul": "0.4.0", | ||
"mocha": "2.3.3" | ||
@@ -32,0 +32,0 @@ }, |
@@ -76,3 +76,4 @@ ESLint-plugin-React | ||
"react/sort-comp": 1, | ||
"react/wrap-multilines": 1 | ||
"react/wrap-multilines": 1, | ||
"prefer-es6-class": 1, | ||
} | ||
@@ -112,2 +113,3 @@ } | ||
* [wrap-multilines](docs/rules/wrap-multilines.md): Prevent missing parentheses around multilines JSX | ||
* [prefer-es6-class](docs/rules/prefer-es6-class.md): Prefer es6 class instead of createClass for React Components | ||
@@ -114,0 +116,0 @@ ## To Do |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
132988
36
3083
146