diff --git a/lib/rules/boolean-prop-naming.js b/lib/rules/boolean-prop-naming.js index f65b8101f4..d1be3364aa 100644 --- a/lib/rules/boolean-prop-naming.js +++ b/lib/rules/boolean-prop-naming.js @@ -4,7 +4,6 @@ */ 'use strict'; -const has = require('has'); const Components = require('../util/Components'); const propsUtil = require('../util/props'); const docsUrl = require('../util/docsUrl'); @@ -248,7 +247,7 @@ module.exports = { } } - if (!has(list, component) || (list[component].invalidProps || []).length) { + if (list[component].invalidProps && list[component].invalidProps.length > 0) { reportInvalidNaming(list[component]); } }); diff --git a/lib/rules/default-props-match-prop-types.js b/lib/rules/default-props-match-prop-types.js index 79723f18a8..203bd2db2d 100644 --- a/lib/rules/default-props-match-prop-types.js +++ b/lib/rules/default-props-match-prop-types.js @@ -5,7 +5,6 @@ */ 'use strict'; -const has = require('has'); const Components = require('../util/Components'); const variableUtil = require('../util/variable'); const annotations = require('../util/annotations'); @@ -595,11 +594,7 @@ module.exports = { stack = null; const list = components.list(); - for (const component in list) { - if (!has(list, component)) { - continue; - } - + Object.keys(list).forEach(component => { // If no defaultProps could be found, we don't report anything. if (!list[component].defaultProps) { return; @@ -609,7 +604,7 @@ module.exports = { list[component].propTypes, list[component].defaultProps || {} ); - } + }); } }; }) diff --git a/lib/rules/display-name.js b/lib/rules/display-name.js index 4e40d3fcb7..c1e02be3cd 100644 --- a/lib/rules/display-name.js +++ b/lib/rules/display-name.js @@ -4,7 +4,6 @@ */ 'use strict'; -const has = require('has'); const Components = require('../util/Components'); const astUtil = require('../util/ast'); const docsUrl = require('../util/docsUrl'); @@ -216,12 +215,9 @@ module.exports = { 'Program:exit': function() { const list = components.list(); // Report missing display name for all components - for (const component in list) { - if (!has(list, component) || list[component].hasDisplayName) { - continue; - } + Object.keys(list).filter(component => !list[component].hasDisplayName).forEach(component => { reportMissingDisplayName(list[component]); - } + }); } }; }) diff --git a/lib/rules/no-multi-comp.js b/lib/rules/no-multi-comp.js index 4d6082d767..9bb61899d1 100644 --- a/lib/rules/no-multi-comp.js +++ b/lib/rules/no-multi-comp.js @@ -4,7 +4,6 @@ */ 'use strict'; -const has = require('has'); const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); @@ -59,17 +58,15 @@ module.exports = { } const list = components.list(); - let i = 0; - for (const component in list) { - if (!has(list, component) || isIgnored(list[component]) || ++i === 1) { - continue; + Object.keys(list).filter(component => !isIgnored(list[component])).forEach((component, i) => { + if (i >= 1) { + context.report({ + node: list[component].node, + message: MULTI_COMP_MESSAGE + }); } - context.report({ - node: list[component].node, - message: MULTI_COMP_MESSAGE - }); - } + }); } }; }) diff --git a/lib/rules/no-set-state.js b/lib/rules/no-set-state.js index 3b18b5c182..f5dc97f699 100644 --- a/lib/rules/no-set-state.js +++ b/lib/rules/no-set-state.js @@ -4,7 +4,6 @@ */ 'use strict'; -const has = require('has'); const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); @@ -74,12 +73,9 @@ module.exports = { 'Program:exit': function() { const list = components.list(); - for (const component in list) { - if (!has(list, component) || isValid(list[component])) { - continue; - } + Object.keys(list).filter(component => !isValid(list[component])).forEach(component => { reportSetStateUsages(list[component]); - } + }); } }; }) diff --git a/lib/rules/no-unused-prop-types.js b/lib/rules/no-unused-prop-types.js index 3948d67398..0e80cfd179 100644 --- a/lib/rules/no-unused-prop-types.js +++ b/lib/rules/no-unused-prop-types.js @@ -7,22 +7,9 @@ // As for exceptions for props.children or props.className (and alike) look at // https://github.com/yannickcr/eslint-plugin-react/issues/7 -const has = require('has'); const Components = require('../util/Components'); -const astUtil = require('../util/ast'); -const versionUtil = require('../util/version'); const docsUrl = require('../util/docsUrl'); -// ------------------------------------------------------------------------------ -// Constants -// ------------------------------------------------------------------------------ - -const DIRECT_PROPS_REGEX = /^props\s*(\.|\[)/; -const DIRECT_NEXT_PROPS_REGEX = /^nextProps\s*(\.|\[)/; -const DIRECT_PREV_PROPS_REGEX = /^prevProps\s*(\.|\[)/; -const LIFE_CYCLE_METHODS = ['componentWillReceiveProps', 'shouldComponentUpdate', 'componentWillUpdate', 'componentDidUpdate']; -const ASYNC_SAFE_LIFE_CYCLE_METHODS = ['getDerivedStateFromProps', 'getSnapshotBeforeUpdate', 'UNSAFE_componentWillReceiveProps', 'UNSAFE_componentWillUpdate']; - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -53,91 +40,11 @@ module.exports = { }] }, - create: Components.detect((context, components, utils) => { - const sourceCode = context.getSourceCode(); - const checkAsyncSafeLifeCycles = versionUtil.testReactVersion(context, '16.3.0'); + create: Components.detect((context, components) => { const defaults = {skipShapeProps: true, customValidators: []}; const configuration = Object.assign({}, defaults, context.options[0] || {}); const UNUSED_MESSAGE = '\'{{name}}\' PropType is defined but prop is never used'; - /** - * Check if we are in a lifecycle method - * @return {boolean} true if we are in a class constructor, false if not - **/ - function inLifeCycleMethod() { - let scope = context.getScope(); - while (scope) { - if (scope.block && scope.block.parent && scope.block.parent.key) { - const name = scope.block.parent.key.name; - - if (LIFE_CYCLE_METHODS.indexOf(name) >= 0) { - return true; - } else if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(name) >= 0) { - return true; - } - } - scope = scope.upper; - } - return false; - } - - /** - * Check if the current node is in a setState updater method - * @return {boolean} true if we are in a setState updater, false if not - */ - function inSetStateUpdater() { - let scope = context.getScope(); - while (scope) { - if ( - scope.block && scope.block.parent - && scope.block.parent.type === 'CallExpression' - && scope.block.parent.callee.property - && scope.block.parent.callee.property.name === 'setState' - // Make sure we are in the updater not the callback - && scope.block.parent.arguments[0].start === scope.block.start - ) { - return true; - } - scope = scope.upper; - } - return false; - } - - function isPropArgumentInSetStateUpdater(node) { - let scope = context.getScope(); - while (scope) { - if ( - scope.block && scope.block.parent - && scope.block.parent.type === 'CallExpression' - && scope.block.parent.callee.property - && scope.block.parent.callee.property.name === 'setState' - // Make sure we are in the updater not the callback - && scope.block.parent.arguments[0].start === scope.block.start - && scope.block.parent.arguments[0].params - && scope.block.parent.arguments[0].params.length > 1 - ) { - return scope.block.parent.arguments[0].params[1].name === node.object.name; - } - scope = scope.upper; - } - return false; - } - - /** - * Checks if we are using a prop - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if we are using a prop, false if not. - */ - function isPropTypesUsage(node) { - const isClassUsage = ( - (utils.getParentES6Component() || utils.getParentES5Component()) && - ((node.object.type === 'ThisExpression' && node.property.name === 'props') - || isPropArgumentInSetStateUpdater(node)) - ); - const isStatelessFunctionUsage = node.object.name === 'props'; - return isClassUsage || isStatelessFunctionUsage || inLifeCycleMethod(); - } - /** * Checks if the component must be validated * @param {Object} component The component to process @@ -150,56 +57,6 @@ module.exports = { ); } - /** - * Returns true if the given node is a React Component lifecycle method - * @param {ASTNode} node The AST node being checked. - * @return {Boolean} True if the node is a lifecycle method - */ - function isNodeALifeCycleMethod(node) { - const nodeKeyName = (node.key || {}).name; - - if (node.kind === 'constructor') { - return true; - } else if (LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) { - return true; - } else if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) { - return true; - } - - return false; - } - - /** - * Returns true if the given node is inside a React Component lifecycle - * method. - * @param {ASTNode} node The AST node being checked. - * @return {Boolean} True if the node is inside a lifecycle method - */ - function isInLifeCycleMethod(node) { - if ((node.type === 'MethodDefinition' || node.type === 'Property') && isNodeALifeCycleMethod(node)) { - return true; - } - - if (node.parent) { - return isInLifeCycleMethod(node.parent); - } - - return false; - } - - /** - * Checks if a prop init name matches common naming patterns - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if the prop name matches - */ - function isPropAttributeName (node) { - return ( - node.init.name === 'props' || - node.init.name === 'nextProps' || - node.init.name === 'prevProps' - ); - } - /** * Checks if a prop is used * @param {ASTNode} node The AST node being checked. @@ -222,229 +79,6 @@ module.exports = { return false; } - /** - * Checks if the prop has spread operator. - * @param {ASTNode} node The AST node being marked. - * @returns {Boolean} True if the prop has spread operator, false if not. - */ - function hasSpreadOperator(node) { - const tokens = sourceCode.getTokens(node); - return tokens.length && tokens[0].value === '...'; - } - - /** - * Removes quotes from around an identifier. - * @param {string} the identifier to strip - */ - function stripQuotes(string) { - return string.replace(/^\'|\'$/g, ''); - } - - /** - * Retrieve the name of a key node - * @param {ASTNode} node The AST node with the key. - * @return {string} the name of the key - */ - function getKeyValue(node) { - if (node.type === 'ObjectTypeProperty') { - const tokens = context.getFirstTokens(node, 2); - return (tokens[0].value === '+' || tokens[0].value === '-' - ? tokens[1].value - : stripQuotes(tokens[0].value) - ); - } - const key = node.key || node.argument; - return key.type === 'Identifier' ? key.name : key.value; - } - - /** - * Check if we are in a class constructor - * @return {boolean} true if we are in a class constructor, false if not - */ - function inConstructor() { - let scope = context.getScope(); - while (scope) { - if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { - return true; - } - scope = scope.upper; - } - return false; - } - - /** - * Retrieve the name of a property node - * @param {ASTNode} node The AST node with the property. - * @return {string} the name of the property or undefined if not found - */ - function getPropertyName(node) { - const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node)); - const isDirectNextProp = DIRECT_NEXT_PROPS_REGEX.test(sourceCode.getText(node)); - const isDirectPrevProp = DIRECT_PREV_PROPS_REGEX.test(sourceCode.getText(node)); - const isDirectSetStateProp = isPropArgumentInSetStateUpdater(node); - const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component(); - const isNotInConstructor = !inConstructor(node); - const isNotInLifeCycleMethod = !inLifeCycleMethod(); - const isNotInSetStateUpdater = !inSetStateUpdater(); - if ((isDirectProp || isDirectNextProp || isDirectPrevProp || isDirectSetStateProp) - && isInClassComponent - && isNotInConstructor - && isNotInLifeCycleMethod - && isNotInSetStateUpdater - ) { - return void 0; - } - if (!isDirectProp && !isDirectNextProp && !isDirectPrevProp && !isDirectSetStateProp) { - node = node.parent; - } - const property = node.property; - if (property) { - switch (property.type) { - case 'Identifier': - if (node.computed) { - return '__COMPUTED_PROP__'; - } - return property.name; - case 'MemberExpression': - return void 0; - case 'Literal': - // Accept computed properties that are literal strings - if (typeof property.value === 'string') { - return property.value; - } - // falls through - default: - if (node.computed) { - return '__COMPUTED_PROP__'; - } - break; - } - } - return void 0; - } - - /** - * Mark a prop type as used - * @param {ASTNode} node The AST node being marked. - */ - function markPropTypesAsUsed(node, parentNames) { - parentNames = parentNames || []; - let type; - let name; - let allNames; - let properties; - switch (node.type) { - case 'MemberExpression': - name = getPropertyName(node); - if (name) { - allNames = parentNames.concat(name); - if (node.parent.type === 'MemberExpression') { - markPropTypesAsUsed(node.parent, allNames); - } - // Do not mark computed props as used. - type = name !== '__COMPUTED_PROP__' ? 'direct' : null; - } else if ( - node.parent.id && - node.parent.id.properties && - node.parent.id.properties.length && - getKeyValue(node.parent.id.properties[0]) - ) { - type = 'destructuring'; - properties = node.parent.id.properties; - } - break; - case 'ArrowFunctionExpression': - case 'FunctionDeclaration': - case 'FunctionExpression': - if (node.params.length === 0) { - break; - } - type = 'destructuring'; - properties = node.params[0].properties; - if (inSetStateUpdater()) { - properties = node.params[1].properties; - } - break; - case 'VariableDeclarator': - for (let i = 0, j = node.id.properties.length; i < j; i++) { - // let {props: {firstname}} = this - const thisDestructuring = ( - node.id.properties[i].key && ( - (node.id.properties[i].key.name === 'props' || node.id.properties[i].key.value === 'props') && - node.id.properties[i].value.type === 'ObjectPattern' - ) - ); - // let {firstname} = props - const genericDestructuring = isPropAttributeName(node) && ( - utils.getParentStatelessComponent() || - isInLifeCycleMethod(node) - ); - - if (thisDestructuring) { - properties = node.id.properties[i].value.properties; - } else if (genericDestructuring) { - properties = node.id.properties; - } else { - continue; - } - type = 'destructuring'; - break; - } - break; - default: - throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`); - } - - const component = components.get(utils.getParentComponent()); - const usedPropTypes = component && component.usedPropTypes || []; - let ignoreUnusedPropTypesValidation = component && component.ignoreUnusedPropTypesValidation || false; - - switch (type) { - case 'direct': - // Ignore Object methods - if (Object.prototype[name]) { - break; - } - - usedPropTypes.push({ - name: name, - allNames: allNames - }); - break; - case 'destructuring': - for (let k = 0, l = (properties || []).length; k < l; k++) { - if (hasSpreadOperator(properties[k]) || properties[k].computed) { - ignoreUnusedPropTypesValidation = true; - break; - } - const propName = getKeyValue(properties[k]); - - let currentNode = node; - allNames = []; - while (currentNode.property && currentNode.property.name !== 'props') { - allNames.unshift(currentNode.property.name); - currentNode = currentNode.object; - } - allNames.push(propName); - - if (propName) { - usedPropTypes.push({ - allNames: allNames, - name: propName - }); - } - } - break; - default: - break; - } - - components.set(component ? component.node : node, { - usedPropTypes: usedPropTypes, - ignoreUnusedPropTypesValidation: ignoreUnusedPropTypesValidation - }); - } - /** * Used to recursively loop through each declared prop type * @param {Object} component The component to process @@ -490,104 +124,20 @@ module.exports = { reportUnusedPropType(component, component.declaredPropTypes); } - /** - * @param {ASTNode} node We expect either an ArrowFunctionExpression, - * FunctionDeclaration, or FunctionExpression - */ - function markDestructuredFunctionArgumentsAsUsed(node) { - const destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern'; - if (destructuring && components.get(node)) { - markPropTypesAsUsed(node); - } - } - - function handleSetStateUpdater(node) { - if (!node.params || node.params.length < 2 || !inSetStateUpdater()) { - return; - } - markPropTypesAsUsed(node); - } - - /** - * Handle both stateless functions and setState updater functions. - * @param {ASTNode} node We expect either an ArrowFunctionExpression, - * FunctionDeclaration, or FunctionExpression - */ - function handleFunctionLikeExpressions(node) { - handleSetStateUpdater(node); - markDestructuredFunctionArgumentsAsUsed(node); - } - - function handleCustomValidators(component) { - const propTypes = component.declaredPropTypes; - if (!propTypes) { - return; - } - - Object.keys(propTypes).forEach(key => { - const node = propTypes[key].node; - - if (astUtil.isFunctionLikeExpression(node)) { - markPropTypesAsUsed(node); - } - }); - } - // -------------------------------------------------------------------------- // Public // -------------------------------------------------------------------------- return { - VariableDeclarator: function(node) { - const destructuring = node.init && node.id && node.id.type === 'ObjectPattern'; - // let {props: {firstname}} = this - const thisDestructuring = destructuring && node.init.type === 'ThisExpression'; - // let {firstname} = props - const statelessDestructuring = destructuring && isPropAttributeName(node) && ( - utils.getParentStatelessComponent() || - isInLifeCycleMethod(node) - ); - - if (!thisDestructuring && !statelessDestructuring) { - return; - } - markPropTypesAsUsed(node); - }, - - FunctionDeclaration: handleFunctionLikeExpressions, - - ArrowFunctionExpression: handleFunctionLikeExpressions, - - FunctionExpression: handleFunctionLikeExpressions, - - MemberExpression: function(node) { - if (isPropTypesUsage(node)) { - markPropTypesAsUsed(node); - } - }, - - ObjectPattern: function(node) { - // If the object pattern is a destructured props object in a lifecycle - // method -- mark it for used props. - if (isNodeALifeCycleMethod(node.parent.parent)) { - node.properties.forEach((property, i) => { - if (i === 0) { - markPropTypesAsUsed(node.parent); - } - }); - } - }, - 'Program:exit': function() { const list = components.list(); // Report undeclared proptypes for all classes - for (const component in list) { - if (!has(list, component) || !mustBeValidated(list[component])) { - continue; + Object.keys(list).filter(component => mustBeValidated(list[component])).forEach(component => { + if (!mustBeValidated(list[component])) { + return; } - handleCustomValidators(list[component]); reportUnusedPropTypes(list[component]); - } + }); } }; }) diff --git a/lib/rules/prefer-stateless-function.js b/lib/rules/prefer-stateless-function.js index 13f79193e5..4550cb54d1 100644 --- a/lib/rules/prefer-stateless-function.js +++ b/lib/rules/prefer-stateless-function.js @@ -6,7 +6,6 @@ */ 'use strict'; -const has = require('has'); const Components = require('../util/Components'); const versionUtil = require('../util/version'); const astUtil = require('../util/ast'); @@ -357,9 +356,8 @@ module.exports = { 'Program:exit': function() { const list = components.list(); - for (const component in list) { + Object.keys(list).forEach(component => { if ( - !has(list, component) || hasOtherProperties(list[component].node) || list[component].useThis || list[component].useRef || @@ -368,17 +366,17 @@ module.exports = { list[component].useDecorators || (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) ) { - continue; + return; } if (list[component].hasSCU && list[component].usePropsOrContext) { - continue; + return; } context.report({ node: list[component].node, message: 'Component should be written as a pure function' }); - } + }); } }; }) diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index c35e811bb4..16eaf6ee24 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -7,17 +7,9 @@ // As for exceptions for props.children or props.className (and alike) look at // https://github.com/yannickcr/eslint-plugin-react/issues/7 -const has = require('has'); const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); -// ------------------------------------------------------------------------------ -// Constants -// ------------------------------------------------------------------------------ - -const PROPS_REGEX = /^(props|nextProps)$/; -const DIRECT_PROPS_REGEX = /^(props|nextProps)\s*(\.|\[)/; - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -54,94 +46,13 @@ module.exports = { }] }, - create: Components.detect((context, components, utils) => { - const sourceCode = context.getSourceCode(); + create: Components.detect((context, components) => { const configuration = context.options[0] || {}; const ignored = configuration.ignore || []; const skipUndeclared = configuration.skipUndeclared || false; const MISSING_MESSAGE = '\'{{name}}\' is missing in props validation'; - /** - * Check if we are in a class constructor - * @return {boolean} true if we are in a class constructor, false if not - */ - function inConstructor() { - let scope = context.getScope(); - while (scope) { - if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { - return true; - } - scope = scope.upper; - } - return false; - } - - /** - * Check if we are in a class constructor - * @return {boolean} true if we are in a class constructor, false if not - */ - function inComponentWillReceiveProps() { - let scope = context.getScope(); - while (scope) { - if ( - scope.block && scope.block.parent && - scope.block.parent.key && scope.block.parent.key.name === 'componentWillReceiveProps' - ) { - return true; - } - scope = scope.upper; - } - return false; - } - - /** - * Check if we are in a class constructor - * @return {boolean} true if we are in a class constructor, false if not - */ - function inShouldComponentUpdate() { - let scope = context.getScope(); - while (scope) { - if ( - scope.block && scope.block.parent && - scope.block.parent.key && scope.block.parent.key.name === 'shouldComponentUpdate' - ) { - return true; - } - scope = scope.upper; - } - return false; - } - - /** - * Checks if a prop is being assigned a value props.bar = 'bar' - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} - */ - - function isAssignmentToProp(node) { - return ( - node.parent && - node.parent.type === 'AssignmentExpression' && - node.parent.left === node - ); - } - - /** - * Checks if we are using a prop - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if we are using a prop, false if not. - */ - function isPropTypesUsage(node) { - const isClassUsage = ( - (utils.getParentES6Component() || utils.getParentES5Component()) && - node.object.type === 'ThisExpression' && node.property.name === 'props' - ); - const isStatelessFunctionUsage = node.object.name === 'props' && !isAssignmentToProp(node); - const isNextPropsUsage = node.object.name === 'nextProps' && (inComponentWillReceiveProps() || inShouldComponentUpdate()); - return isClassUsage || isStatelessFunctionUsage || isNextPropsUsage; - } - /** * Checks if the prop is ignored * @param {String} name Name of the prop to check. @@ -247,214 +158,6 @@ module.exports = { return false; } - /** - * Checks if the prop has spread operator. - * @param {ASTNode} node The AST node being marked. - * @returns {Boolean} True if the prop has spread operator, false if not. - */ - function hasSpreadOperator(node) { - const tokens = sourceCode.getTokens(node); - return tokens.length && tokens[0].value === '...'; - } - - /** - * Removes quotes from around an identifier. - * @param {string} the identifier to strip - */ - function stripQuotes(string) { - return string.replace(/^\'|\'$/g, ''); - } - - /** - * Retrieve the name of a key node - * @param {ASTNode} node The AST node with the key. - * @return {string} the name of the key - */ - function getKeyValue(node) { - if (node.type === 'ObjectTypeProperty') { - const tokens = context.getFirstTokens(node, 2); - return (tokens[0].value === '+' || tokens[0].value === '-' - ? tokens[1].value - : stripQuotes(tokens[0].value) - ); - } - const key = node.key || node.argument; - return key.type === 'Identifier' ? key.name : key.value; - } - - /** - * Retrieve the name of a property node - * @param {ASTNode} node The AST node with the property. - * @return {string} the name of the property or undefined if not found - */ - function getPropertyName(node) { - const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node)); - const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component(); - const isNotInConstructor = !inConstructor(); - const isNotInComponentWillReceiveProps = !inComponentWillReceiveProps(); - const isNotInShouldComponentUpdate = !inShouldComponentUpdate(); - if (isDirectProp && isInClassComponent && isNotInConstructor && isNotInComponentWillReceiveProps - && isNotInShouldComponentUpdate) { - return void 0; - } - if (!isDirectProp) { - node = node.parent; - } - const property = node.property; - if (property) { - switch (property.type) { - case 'Identifier': - if (node.computed) { - return '__COMPUTED_PROP__'; - } - return property.name; - case 'MemberExpression': - return void 0; - case 'Literal': - // Accept computed properties that are literal strings - if (typeof property.value === 'string') { - return property.value; - } - // falls through - default: - if (node.computed) { - return '__COMPUTED_PROP__'; - } - break; - } - } - return void 0; - } - - /** - * Mark a prop type as used - * @param {ASTNode} node The AST node being marked. - */ - function markPropTypesAsUsed(node, parentNames) { - parentNames = parentNames || []; - let type; - let name; - let allNames; - let properties; - switch (node.type) { - case 'MemberExpression': - name = getPropertyName(node); - if (name) { - allNames = parentNames.concat(name); - if (node.parent.type === 'MemberExpression') { - markPropTypesAsUsed(node.parent, allNames); - } - // Do not mark computed props as used. - type = name !== '__COMPUTED_PROP__' ? 'direct' : null; - } else if ( - node.parent.id && - node.parent.id.properties && - node.parent.id.properties.length && - getKeyValue(node.parent.id.properties[0]) - ) { - type = 'destructuring'; - properties = node.parent.id.properties; - } - break; - case 'ArrowFunctionExpression': - case 'FunctionDeclaration': - case 'FunctionExpression': - type = 'destructuring'; - properties = node.params[0].properties; - break; - case 'MethodDefinition': - const destructuring = node.value && node.value.params && node.value.params[0] && node.value.params[0].type === 'ObjectPattern'; - if (destructuring) { - type = 'destructuring'; - properties = node.value.params[0].properties; - break; - } else { - return; - } - case 'VariableDeclarator': - for (let i = 0, j = node.id.properties.length; i < j; i++) { - // let {props: {firstname}} = this - const thisDestructuring = ( - !hasSpreadOperator(node.id.properties[i]) && - (PROPS_REGEX.test(node.id.properties[i].key.name) || PROPS_REGEX.test(node.id.properties[i].key.value)) && - node.id.properties[i].value.type === 'ObjectPattern' - ); - // let {firstname} = props - const directDestructuring = - PROPS_REGEX.test(node.init.name) && - (utils.getParentStatelessComponent() || inConstructor() || inComponentWillReceiveProps()) - ; - - if (thisDestructuring) { - properties = node.id.properties[i].value.properties; - } else if (directDestructuring) { - properties = node.id.properties; - } else { - continue; - } - type = 'destructuring'; - break; - } - break; - default: - throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`); - } - - const component = components.get(utils.getParentComponent()); - const usedPropTypes = (component && component.usedPropTypes || []).slice(); - - switch (type) { - case 'direct': - // Ignore Object methods - if (Object.prototype[name]) { - break; - } - - const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node)); - - usedPropTypes.push({ - name: name, - allNames: allNames, - node: ( - !isDirectProp && !inConstructor() && !inComponentWillReceiveProps() ? - node.parent.property : - node.property - ) - }); - break; - case 'destructuring': - for (let k = 0, l = properties.length; k < l; k++) { - if (hasSpreadOperator(properties[k]) || properties[k].computed) { - continue; - } - const propName = getKeyValue(properties[k]); - - let currentNode = node; - allNames = []; - while (currentNode.property && !PROPS_REGEX.test(currentNode.property.name)) { - allNames.unshift(currentNode.property.name); - currentNode = currentNode.object; - } - allNames.push(propName); - - if (propName) { - usedPropTypes.push({ - name: propName, - allNames: allNames, - node: properties[k] - }); - } - } - break; - default: - break; - } - - components.set(node, { - usedPropTypes: usedPropTypes - }); - } - /** * Reports undeclared proptypes for a given component * @param {Object} component The component to process @@ -478,76 +181,17 @@ module.exports = { } } - /** - * @param {ASTNode} node We expect either an ArrowFunctionExpression, - * FunctionDeclaration, or FunctionExpression - */ - function markDestructuredFunctionArgumentsAsUsed(node) { - const destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern'; - if (destructuring && components.get(node)) { - markPropTypesAsUsed(node); - } - } - // -------------------------------------------------------------------------- // Public // -------------------------------------------------------------------------- return { - VariableDeclarator: function(node) { - const destructuring = node.init && node.id && node.id.type === 'ObjectPattern'; - // let {props: {firstname}} = this - const thisDestructuring = destructuring && node.init.type === 'ThisExpression'; - // let {firstname} = props - const directDestructuring = - destructuring && - PROPS_REGEX.test(node.init.name) && - (utils.getParentStatelessComponent() || inConstructor() || inComponentWillReceiveProps()) - ; - - if (!thisDestructuring && !directDestructuring) { - return; - } - markPropTypesAsUsed(node); - }, - - FunctionDeclaration: markDestructuredFunctionArgumentsAsUsed, - - ArrowFunctionExpression: markDestructuredFunctionArgumentsAsUsed, - - FunctionExpression: function(node) { - if (node.parent.type === 'MethodDefinition') { - return; - } - markDestructuredFunctionArgumentsAsUsed(node); - }, - - MemberExpression: function(node) { - if (isPropTypesUsage(node)) { - markPropTypesAsUsed(node); - } - }, - - MethodDefinition: function(node) { - const destructuring = node.value && node.value.params && node.value.params[0] && node.value.params[0].type === 'ObjectPattern'; - if (node.key.name === 'componentWillReceiveProps' && destructuring) { - markPropTypesAsUsed(node); - } - - if (node.key.name === 'shouldComponentUpdate' && destructuring) { - markPropTypesAsUsed(node); - } - }, - 'Program:exit': function() { const list = components.list(); // Report undeclared proptypes for all classes - for (const component in list) { - if (!has(list, component) || !mustBeValidated(list[component])) { - continue; - } + Object.keys(list).filter(component => mustBeValidated(list[component])).forEach(component => { reportUndeclaredPropTypes(list[component]); - } + }); } }; }) diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index 0704e8e323..ea8b4b153b 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -4,7 +4,6 @@ */ 'use strict'; -const has = require('has'); const Components = require('../util/Components'); const variableUtil = require('../util/variable'); const annotations = require('../util/annotations'); @@ -626,21 +625,12 @@ module.exports = { stack = null; const list = components.list(); - for (const component in list) { - if (!has(list, component)) { - continue; - } - - // If no propTypes could be found, we don't report anything. - if (!list[component].propTypes) { - continue; - } - + Object.keys(list).filter(component => list[component].propTypes).forEach(component => { reportPropTypesWithoutDefault( list[component].propTypes, list[component].defaultProps || {} ); - } + }); } }; }) diff --git a/lib/rules/require-optimization.js b/lib/rules/require-optimization.js index 670c7fc2ed..e447bdcc5a 100644 --- a/lib/rules/require-optimization.js +++ b/lib/rules/require-optimization.js @@ -4,7 +4,6 @@ */ 'use strict'; -const has = require('has'); const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); @@ -221,12 +220,9 @@ module.exports = { const list = components.list(); // Report missing shouldComponentUpdate for all components - for (const component in list) { - if (!has(list, component) || list[component].hasSCU) { - continue; - } + Object.keys(list).filter(component => !list[component].hasSCU).forEach(component => { reportMissingOptimization(list[component]); - } + }); } }; }) diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js index 139465045e..0c20334482 100644 --- a/lib/rules/require-render-return.js +++ b/lib/rules/require-render-return.js @@ -4,7 +4,6 @@ */ 'use strict'; -const has = require('has'); const Components = require('../util/Components'); const astUtil = require('../util/ast'); const docsUrl = require('../util/docsUrl'); @@ -79,20 +78,19 @@ module.exports = { 'Program:exit': function() { const list = components.list(); - for (const component in list) { + Object.keys(list).forEach(component => { if ( - !has(list, component) || !hasRenderMethod(list[component].node) || list[component].hasReturnStatement || (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) ) { - continue; + return; } context.report({ node: list[component].node, message: 'Your render method should have return statement' }); - } + }); } }; }) diff --git a/lib/rules/sort-comp.js b/lib/rules/sort-comp.js index 4e2602a74e..3997021ad0 100644 --- a/lib/rules/sort-comp.js +++ b/lib/rules/sort-comp.js @@ -444,13 +444,10 @@ module.exports = { return { 'Program:exit': function() { const list = components.list(); - for (const component in list) { - if (!has(list, component)) { - continue; - } + Object.keys(list).forEach(component => { const properties = astUtil.getComponentProperties(list[component].node); checkPropsOrder(properties); - } + }); reportErrors(); } diff --git a/lib/util/Components.js b/lib/util/Components.js index 8ed7bbd2c3..b7e2a99a63 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -4,7 +4,6 @@ */ 'use strict'; -const has = require('has'); const util = require('util'); const doctrine = require('doctrine'); const variableUtil = require('./variable'); @@ -12,6 +11,7 @@ const pragmaUtil = require('./pragma'); const astUtil = require('./ast'); const propTypes = require('./propTypes'); const jsxUtil = require('./jsx'); +const usedPropTypesUtil = require('./usedPropTypes'); function getId(node) { return node && node.range.join(':'); @@ -37,6 +37,7 @@ function mergeUsedPropTypes(propsList, newPropsList) { propsToAdd.push(newProp); } }); + return propsList.concat(propsToAdd); } @@ -123,10 +124,7 @@ class Components { const usedPropTypes = {}; // Find props used in components for which we are not confident - for (const i in this._list) { - if (!has(this._list, i) || this._list[i].confidence >= 2) { - continue; - } + Object.keys(this._list).filter(i => this._list[i].confidence < 2).forEach(i => { let component = null; let node = null; node = this._list[i].node; @@ -142,21 +140,19 @@ class Components { const newUsedProps = (this._list[i].usedPropTypes || []).filter(propType => !propType.node || propType.node.kind !== 'init'); const componentId = getId(component.node); - usedPropTypes[componentId] = (usedPropTypes[componentId] || []).concat(newUsedProps); + + usedPropTypes[componentId] = mergeUsedPropTypes(usedPropTypes[componentId] || [], newUsedProps); } - } + }); // Assign used props in not confident components to the parent component - for (const j in this._list) { - if (!has(this._list, j) || this._list[j].confidence < 2) { - continue; - } + Object.keys(this._list).filter(j => this._list[j].confidence >= 2).forEach(j => { const id = getId(this._list[j].node); list[j] = this._list[j]; if (usedPropTypes[id]) { - list[j].usedPropTypes = (list[j].usedPropTypes || []).concat(usedPropTypes[id]); + list[j].usedPropTypes = mergeUsedPropTypes(list[j].usedPropTypes || [], usedPropTypes[id]); } - } + }); return list; } @@ -167,14 +163,7 @@ class Components { * @returns {Number} Components list length */ length() { - let length = 0; - for (const i in this._list) { - if (!has(this._list, i) || this._list[i].confidence < 2) { - continue; - } - length++; - } - return length; + return Object.keys(this._list).filter(i => this._list[i].confidence >= 2).length; } } @@ -720,7 +709,11 @@ function componentRule(rule, context) { const ruleInstructions = rule(context, components, utils); const updatedRuleInstructions = util._extend({}, ruleInstructions); const propTypesInstructions = propTypes(context, components, utils); - const allKeys = new Set(Object.keys(detectionInstructions).concat(Object.keys(propTypesInstructions))); + const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils); + const allKeys = new Set(Object.keys(detectionInstructions).concat( + Object.keys(propTypesInstructions), + Object.keys(usedPropTypesInstructions) + )); allKeys.forEach(instruction => { updatedRuleInstructions[instruction] = function(node) { if (instruction in detectionInstructions) { @@ -729,6 +722,9 @@ function componentRule(rule, context) { if (instruction in propTypesInstructions) { propTypesInstructions[instruction](node); } + if (instruction in usedPropTypesInstructions) { + usedPropTypesInstructions[instruction](node); + } return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : void 0; }; }); diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index d58af934de..f0ca66ee8e 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -666,13 +666,6 @@ module.exports = function propTypesInstructions(context, components, utils) { } }, - JSXSpreadAttribute: function(node) { - const component = components.get(utils.getParentComponent()); - components.set(component ? component.node : node, { - ignoreUnusedPropTypesValidation: true - }); - }, - TypeAlias: function(node) { setInTypeScope(node.id.name, node.right); }, diff --git a/lib/util/usedPropTypes.js b/lib/util/usedPropTypes.js new file mode 100644 index 0000000000..dd3becf331 --- /dev/null +++ b/lib/util/usedPropTypes.js @@ -0,0 +1,521 @@ +/** + * @fileoverview Common used propTypes detection functionality. + */ +'use strict'; + +const astUtil = require('./ast'); +const versionUtil = require('./version'); + +// ------------------------------------------------------------------------------ +// Constants +// ------------------------------------------------------------------------------ + +const DIRECT_PROPS_REGEX = /^props\s*(\.|\[)/; +const DIRECT_NEXT_PROPS_REGEX = /^nextProps\s*(\.|\[)/; +const DIRECT_PREV_PROPS_REGEX = /^prevProps\s*(\.|\[)/; +const LIFE_CYCLE_METHODS = ['componentWillReceiveProps', 'shouldComponentUpdate', 'componentWillUpdate', 'componentDidUpdate']; +const ASYNC_SAFE_LIFE_CYCLE_METHODS = ['getDerivedStateFromProps', 'getSnapshotBeforeUpdate', 'UNSAFE_componentWillReceiveProps', 'UNSAFE_componentWillUpdate']; + +/** + * Checks if a prop init name matches common naming patterns + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if the prop name matches + */ +function isPropAttributeName (node) { + return ( + node.init.name === 'props' || + node.init.name === 'nextProps' || + node.init.name === 'prevProps' + ); +} + +/** + * 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.ignorePropsValidation); +} + +module.exports = function usedPropTypesInstructions(context, components, utils) { + const sourceCode = context.getSourceCode(); + const checkAsyncSafeLifeCycles = versionUtil.testReactVersion(context, '16.3.0'); + + /** + * Check if we are in a class constructor + * @return {boolean} true if we are in a class constructor, false if not + */ + function inComponentWillReceiveProps() { + let scope = context.getScope(); + while (scope) { + if ( + scope.block + && scope.block.parent + && scope.block.parent.key + && scope.block.parent.key.name === 'componentWillReceiveProps' + ) { + return true; + } + scope = scope.upper; + } + return false; + } + + /** + * Check if we are in a lifecycle method + * @return {boolean} true if we are in a class constructor, false if not + **/ + function inLifeCycleMethod() { + let scope = context.getScope(); + while (scope) { + if (scope.block && scope.block.parent && scope.block.parent.key) { + const name = scope.block.parent.key.name; + + if (LIFE_CYCLE_METHODS.indexOf(name) >= 0) { + return true; + } + if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(name) >= 0) { + return true; + } + } + scope = scope.upper; + } + return false; + } + + /** + * Returns true if the given node is a React Component lifecycle method + * @param {ASTNode} node The AST node being checked. + * @return {Boolean} True if the node is a lifecycle method + */ + function isNodeALifeCycleMethod(node) { + const nodeKeyName = (node.key || {}).name; + + if (node.kind === 'constructor') { + return true; + } + if (LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) { + return true; + } + if (checkAsyncSafeLifeCycles && ASYNC_SAFE_LIFE_CYCLE_METHODS.indexOf(nodeKeyName) >= 0) { + return true; + } + + return false; + } + + /** + * Returns true if the given node is inside a React Component lifecycle + * method. + * @param {ASTNode} node The AST node being checked. + * @return {Boolean} True if the node is inside a lifecycle method + */ + function isInLifeCycleMethod(node) { + if ((node.type === 'MethodDefinition' || node.type === 'Property') && isNodeALifeCycleMethod(node)) { + return true; + } + + if (node.parent) { + return isInLifeCycleMethod(node.parent); + } + + return false; + } + + /** + * Check if the current node is in a setState updater method + * @return {boolean} true if we are in a setState updater, false if not + */ + function inSetStateUpdater() { + let scope = context.getScope(); + while (scope) { + if ( + scope.block && scope.block.parent + && scope.block.parent.type === 'CallExpression' + && scope.block.parent.callee.property + && scope.block.parent.callee.property.name === 'setState' + // Make sure we are in the updater not the callback + && scope.block.parent.arguments[0].start === scope.block.start + ) { + return true; + } + scope = scope.upper; + } + return false; + } + + function isPropArgumentInSetStateUpdater(node) { + let scope = context.getScope(); + while (scope) { + if ( + scope.block && scope.block.parent + && scope.block.parent.type === 'CallExpression' + && scope.block.parent.callee.property + && scope.block.parent.callee.property.name === 'setState' + // Make sure we are in the updater not the callback + && scope.block.parent.arguments[0].start === scope.block.start + && scope.block.parent.arguments[0].params + && scope.block.parent.arguments[0].params.length > 1 + ) { + return scope.block.parent.arguments[0].params[1].name === node.object.name; + } + scope = scope.upper; + } + return false; + } + + /** + * Checks if the prop has spread operator. + * @param {ASTNode} node The AST node being marked. + * @returns {Boolean} True if the prop has spread operator, false if not. + */ + function hasSpreadOperator(node) { + const tokens = sourceCode.getTokens(node); + return tokens.length && tokens[0].value === '...'; + } + + /** + * Removes quotes from around an identifier. + * @param {string} the identifier to strip + */ + function stripQuotes(string) { + return string.replace(/^\'|\'$/g, ''); + } + + /** + * Retrieve the name of a key node + * @param {ASTNode} node The AST node with the key. + * @return {string} the name of the key + */ + function getKeyValue(node) { + if (node.type === 'ObjectTypeProperty') { + const tokens = context.getFirstTokens(node, 2); + return (tokens[0].value === '+' || tokens[0].value === '-' + ? tokens[1].value + : stripQuotes(tokens[0].value) + ); + } + const key = node.key || node.argument; + return key.type === 'Identifier' ? key.name : key.value; + } + + /** + * Check if we are in a class constructor + * @return {boolean} true if we are in a class constructor, false if not + */ + function inConstructor() { + let scope = context.getScope(); + while (scope) { + if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { + return true; + } + scope = scope.upper; + } + return false; + } + + /** + * Retrieve the name of a property node + * @param {ASTNode} node The AST node with the property. + * @return {string} the name of the property or undefined if not found + */ + function getPropertyName(node) { + const isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node)); + const isDirectNextProp = DIRECT_NEXT_PROPS_REGEX.test(sourceCode.getText(node)); + const isDirectPrevProp = DIRECT_PREV_PROPS_REGEX.test(sourceCode.getText(node)); + const isDirectSetStateProp = isPropArgumentInSetStateUpdater(node); + const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component(); + const isNotInConstructor = !inConstructor(node); + const isNotInLifeCycleMethod = !inLifeCycleMethod(); + const isNotInSetStateUpdater = !inSetStateUpdater(); + if ((isDirectProp || isDirectNextProp || isDirectPrevProp || isDirectSetStateProp) + && isInClassComponent + && isNotInConstructor + && isNotInLifeCycleMethod + && isNotInSetStateUpdater + ) { + return void 0; + } + if (!isDirectProp && !isDirectNextProp && !isDirectPrevProp && !isDirectSetStateProp) { + node = node.parent; + } + const property = node.property; + if (property) { + switch (property.type) { + case 'Identifier': + if (node.computed) { + return '__COMPUTED_PROP__'; + } + return property.name; + case 'MemberExpression': + return void 0; + case 'Literal': + // Accept computed properties that are literal strings + if (typeof property.value === 'string') { + return property.value; + } + // falls through + default: + if (node.computed) { + return '__COMPUTED_PROP__'; + } + break; + } + } + return void 0; + } + + /** + * Checks if a prop is being assigned a value props.bar = 'bar' + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} + */ + function isAssignmentToProp(node) { + return ( + node.parent && + node.parent.type === 'AssignmentExpression' && + node.parent.left === node + ); + } + + /** + * Checks if we are using a prop + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if we are using a prop, false if not. + */ + function isPropTypesUsage(node) { + const isClassUsage = ( + (utils.getParentES6Component() || utils.getParentES5Component()) && + ((node.object.type === 'ThisExpression' && node.property.name === 'props') + || isPropArgumentInSetStateUpdater(node)) + ); + const isStatelessFunctionUsage = node.object.name === 'props' && !isAssignmentToProp(node); + return isClassUsage || isStatelessFunctionUsage || inLifeCycleMethod(); + } + + /** + * Mark a prop type as used + * @param {ASTNode} node The AST node being marked. + */ + function markPropTypesAsUsed(node, parentNames) { + parentNames = parentNames || []; + let type; + let name; + let allNames; + let properties; + switch (node.type) { + case 'MemberExpression': + name = getPropertyName(node); + if (name) { + allNames = parentNames.concat(name); + if (node.parent.type === 'MemberExpression') { + markPropTypesAsUsed(node.parent, allNames); + } + // Do not mark computed props as used. + type = name !== '__COMPUTED_PROP__' ? 'direct' : null; + } else if ( + node.parent.id && + node.parent.id.properties && + node.parent.id.properties.length && + getKeyValue(node.parent.id.properties[0]) + ) { + type = 'destructuring'; + properties = node.parent.id.properties; + } + break; + case 'ArrowFunctionExpression': + case 'FunctionDeclaration': + case 'FunctionExpression': + if (node.params.length === 0) { + break; + } + type = 'destructuring'; + properties = node.params[0].properties; + if (inSetStateUpdater()) { + properties = node.params[1].properties; + } + break; + case 'VariableDeclarator': + for (let i = 0, j = node.id.properties.length; i < j; i++) { + // let {props: {firstname}} = this + const thisDestructuring = ( + node.id.properties[i].key && ( + (node.id.properties[i].key.name === 'props' || node.id.properties[i].key.value === 'props') && + node.id.properties[i].value.type === 'ObjectPattern' + ) + ); + // let {firstname} = props + const genericDestructuring = isPropAttributeName(node) && ( + utils.getParentStatelessComponent() || + isInLifeCycleMethod(node) + ); + + if (thisDestructuring) { + properties = node.id.properties[i].value.properties; + } else if (genericDestructuring) { + properties = node.id.properties; + } else { + continue; + } + type = 'destructuring'; + break; + } + break; + default: + throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`); + } + + const component = components.get(utils.getParentComponent()); + const usedPropTypes = component && component.usedPropTypes || []; + let ignoreUnusedPropTypesValidation = component && component.ignoreUnusedPropTypesValidation || false; + + switch (type) { + case 'direct': + // Ignore Object methods + if (name in Object.prototype) { + break; + } + + const nodeSource = sourceCode.getText(node); + const isDirectProp = DIRECT_PROPS_REGEX.test(nodeSource) || DIRECT_NEXT_PROPS_REGEX.test(nodeSource); + usedPropTypes.push({ + name: name, + allNames: allNames, + node: ( + !isDirectProp && !inConstructor() && !inComponentWillReceiveProps() ? + node.parent.property : + node.property + ) + }); + break; + case 'destructuring': + for (let k = 0, l = (properties || []).length; k < l; k++) { + if (hasSpreadOperator(properties[k]) || properties[k].computed) { + ignoreUnusedPropTypesValidation = true; + break; + } + const propName = getKeyValue(properties[k]); + + let currentNode = node; + allNames = []; + while (currentNode.property && currentNode.property.name !== 'props') { + allNames.unshift(currentNode.property.name); + currentNode = currentNode.object; + } + allNames.push(propName); + if (propName) { + usedPropTypes.push({ + allNames: allNames, + name: propName, + node: properties[k] + }); + } + } + break; + default: + break; + } + + components.set(component ? component.node : node, { + usedPropTypes: usedPropTypes, + ignoreUnusedPropTypesValidation: ignoreUnusedPropTypesValidation + }); + } + + /** + * @param {ASTNode} node We expect either an ArrowFunctionExpression, + * FunctionDeclaration, or FunctionExpression + */ + function markDestructuredFunctionArgumentsAsUsed(node) { + const destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern'; + if (destructuring && components.get(node)) { + markPropTypesAsUsed(node); + } + } + + function handleSetStateUpdater(node) { + if (!node.params || node.params.length < 2 || !inSetStateUpdater()) { + return; + } + markPropTypesAsUsed(node); + } + + /** + * Handle both stateless functions and setState updater functions. + * @param {ASTNode} node We expect either an ArrowFunctionExpression, + * FunctionDeclaration, or FunctionExpression + */ + function handleFunctionLikeExpressions(node) { + handleSetStateUpdater(node); + markDestructuredFunctionArgumentsAsUsed(node); + } + + function handleCustomValidators(component) { + const propTypes = component.declaredPropTypes; + if (!propTypes) { + return; + } + + Object.keys(propTypes).forEach(key => { + const node = propTypes[key].node; + + if (astUtil.isFunctionLikeExpression(node)) { + markPropTypesAsUsed(node); + } + }); + } + + return { + VariableDeclarator: function(node) { + const destructuring = node.init && node.id && node.id.type === 'ObjectPattern'; + // let {props: {firstname}} = this + const thisDestructuring = destructuring && node.init.type === 'ThisExpression'; + // let {firstname} = props + const statelessDestructuring = destructuring && isPropAttributeName(node) && ( + utils.getParentStatelessComponent() || + isInLifeCycleMethod(node) + ); + + if (!thisDestructuring && !statelessDestructuring) { + return; + } + markPropTypesAsUsed(node); + }, + + FunctionDeclaration: handleFunctionLikeExpressions, + + ArrowFunctionExpression: handleFunctionLikeExpressions, + + FunctionExpression: handleFunctionLikeExpressions, + + JSXSpreadAttribute: function(node) { + const component = components.get(utils.getParentComponent()); + components.set(component ? component.node : node, { + ignoreUnusedPropTypesValidation: true + }); + }, + + MemberExpression: function(node) { + if (isPropTypesUsage(node)) { + markPropTypesAsUsed(node); + } + }, + + ObjectPattern: function(node) { + // If the object pattern is a destructured props object in a lifecycle + // method -- mark it for used props. + if (isNodeALifeCycleMethod(node.parent.parent) && node.properties.length > 0) { + markPropTypesAsUsed(node.parent); + } + }, + + 'Program:exit': function() { + const list = components.list(); + + Object.keys(list).filter(component => mustBeValidated(list[component])).forEach(component => { + handleCustomValidators(list[component]); + }); + } + }; +}; diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 4cbe55fa26..25d65635d3 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -2030,6 +2030,44 @@ ruleTester.run('prop-types', rule, { Slider.propTypes = RcSlider.propTypes; ` + }, + { + code: ` + class Foo extends React.Component { + bar() { + this.setState((state, props) => ({ current: props.current })); + } + render() { + return
; + } + } + + Foo.propTypes = { + current: PropTypes.number.isRequired, + }; + ` + }, + { + code: ` + class Foo extends React.Component { + static getDerivedStateFromProps(props) { + const { foo } = props; + return { + foobar: foo + }; + } + + render() { + const { foobar } = this.state; + return