diff --git a/lib/util/usedPropTypes.js b/lib/util/usedPropTypes.js index 13f5162e68..12dd0d2fa8 100755 --- a/lib/util/usedPropTypes.js +++ b/lib/util/usedPropTypes.js @@ -12,23 +12,16 @@ const ast = require('./ast'); // 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. + * Checks if the string is one of `props`, `nextProps`, or `prevProps` + * @param {string} name 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' - ); +function isCommonVariableNameForProps(name) { + return name === 'props' || name === 'nextProps' || name === 'prevProps'; } /** @@ -40,26 +33,6 @@ function mustBeValidated(component) { return !!(component && !component.ignorePropsValidation); } -/** - * Check if we are in a class constructor - * @return {boolean} true if we are in a class constructor, false if not - */ -function inComponentWillReceiveProps(context) { - 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 @@ -143,7 +116,10 @@ function inSetStateUpdater(context) { return false; } -function isPropArgumentInSetStateUpdater(context, node) { +function isPropArgumentInSetStateUpdater(context, name) { + if (typeof name !== 'string') { + return; + } let scope = context.getScope(); while (scope) { if ( @@ -156,13 +132,29 @@ function isPropArgumentInSetStateUpdater(context, node) { 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; + return scope.block.parent.arguments[0].params[1].name === name; } scope = scope.upper; } return false; } +function isInClassComponent(utils) { + return utils.getParentES6Component() || utils.getParentES5Component(); +} + +/** + * Checks if the node is `this.props` + * @param {ASTNode|undefined} node + * @returns {boolean} + */ +function isThisDotProps(node) { + return !!node && + node.type === 'MemberExpression' && + node.object.type === 'ThisExpression' && + node.property.name === 'props'; +} + /** * Checks if the prop has spread operator. * @param {ASTNode} node The AST node being marked. @@ -178,27 +170,7 @@ function hasSpreadOperator(context, node) { * @param {ASTNode} node The AST node with the property. * @return {string|undefined} the name of the property or undefined if not found */ -function getPropertyName(node, context, utils, checkAsyncSafeLifeCycles) { - const sourceCode = context.getSourceCode(); - 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(context, node); - const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component(); - const isNotInConstructor = !utils.inConstructor(node); - const isNotInLifeCycleMethod = !inLifeCycleMethod(context, checkAsyncSafeLifeCycles); - const isNotInSetStateUpdater = !inSetStateUpdater(context); - if ((isDirectProp || isDirectNextProp || isDirectPrevProp || isDirectSetStateProp) && - isInClassComponent && - isNotInConstructor && - isNotInLifeCycleMethod && - isNotInSetStateUpdater - ) { - return; - } - if (!isDirectProp && !isDirectNextProp && !isDirectPrevProp && !isDirectSetStateProp) { - node = node.parent; - } +function getPropertyName(node) { const property = node.property; if (property) { switch (property.type) { @@ -225,21 +197,34 @@ function getPropertyName(node, context, utils, checkAsyncSafeLifeCycles) { } /** - * 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. + * Checks if the node is a propTypes usage of the form `this.props.*`, `props.*`, `prevProps.*`, or `nextProps.*`. + * @param {ASTNode} node + * @param {Context} context + * @param {Object} utils + * @param {boolean} checkAsyncSafeLifeCycles + * @returns {boolean} */ -function isPropTypesUsage(node, context, utils, checkAsyncSafeLifeCycles) { - const isThisPropsUsage = node.object.type === 'ThisExpression' && node.property.name === 'props'; - const isPropsUsage = isThisPropsUsage || node.object.name === 'nextProps' || node.object.name === 'prevProps'; - const isClassUsage = ( - (utils.getParentES6Component() || utils.getParentES5Component()) && - (isThisPropsUsage || isPropArgumentInSetStateUpdater(context, node)) - ); - const isStatelessFunctionUsage = node.object.name === 'props' && !ast.isAssignmentLHS(node); - return isClassUsage || - isStatelessFunctionUsage || - (isPropsUsage && inLifeCycleMethod(context, checkAsyncSafeLifeCycles)); +function isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles) { + if (isInClassComponent(utils)) { + // this.props.* + if (isThisDotProps(node.object)) { + return true; + } + // props.* or prevProps.* or nextProps.* + if ( + isCommonVariableNameForProps(node.object.name) && + (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor()) + ) { + return true; + } + // this.setState((_, props) => props.*)) + if (isPropArgumentInSetStateUpdater(context, node.object.name)) { + return true; + } + return false; + } + // props.* in function component + return node.object.name === 'props' && !ast.isAssignmentLHS(node); } module.exports = function usedPropTypesInstructions(context, components, utils) { @@ -258,7 +243,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils) let properties; switch (node.type) { case 'MemberExpression': - name = getPropertyName(node, context, utils, checkAsyncSafeLifeCycles); + name = getPropertyName(node); if (name) { allNames = parentNames.concat(name); if ( @@ -268,16 +253,16 @@ module.exports = function usedPropTypesInstructions(context, components, utils) ) { markPropTypesAsUsed(node.parent, allNames); } + // Handle the destructuring part of `const {foo} = props.a.b` + if ( + node.parent.type === 'VariableDeclarator' && + node.parent.id.type === 'ObjectPattern' + ) { + node.parent.id.parent = node.parent; // patch for bug in eslint@4 in which ObjectPattern has no parent + markPropTypesAsUsed(node.parent.id, 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 && - ast.getKeyValue(context, node.parent.id.properties[0]) - ) { - type = 'destructuring'; - properties = node.parent.id.properties; } break; case 'ArrowFunctionExpression': @@ -293,31 +278,9 @@ module.exports = function usedPropTypesInstructions(context, components, utils) propParam.properties; break; } - case 'VariableDeclarator': - node.id.properties.some((property) => { - // let {props: {firstname}} = this - const thisDestructuring = ( - property.key && ( - (property.key.name === 'props' || property.key.value === 'props') && - property.value.type === 'ObjectPattern' - ) - ); - // let {firstname} = props - const genericDestructuring = isPropAttributeName(node) && ( - utils.getParentStatelessComponent() || - isInLifeCycleMethod(node, checkAsyncSafeLifeCycles) - ); - - if (thisDestructuring) { - properties = property.value.properties; - } else if (genericDestructuring) { - properties = node.id.properties; - } else { - return false; - } - type = 'destructuring'; - return true; - }); + case 'ObjectPattern': + type = 'destructuring'; + properties = node.properties; break; default: throw new Error(`${node.type} ASTNodes are not handled by markPropTypesAsUsed`); @@ -334,15 +297,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils) break; } - const nodeSource = context.getSourceCode().getText(node); - const isDirectProp = DIRECT_PROPS_REGEX.test(nodeSource) || - DIRECT_NEXT_PROPS_REGEX.test(nodeSource) || - DIRECT_PREV_PROPS_REGEX.test(nodeSource); - const reportedNode = ( - !isDirectProp && !utils.inConstructor() && !inComponentWillReceiveProps(context) ? - node.parent.property : - node.property - ); + const reportedNode = node.property; usedPropTypes.push({ name, allNames, @@ -358,7 +313,8 @@ module.exports = function usedPropTypesInstructions(context, components, utils) } const propName = ast.getKeyValue(context, properties[k]); - let currentNode = node; + // Get parent names in the right hand side of `const {foo} = props.a.b` + let currentNode = (node.parent && node.parent.init) || {}; allNames = []; while (currentNode.property && currentNode.property.name !== 'props') { allNames.unshift(currentNode.property.name); @@ -436,19 +392,35 @@ module.exports = function usedPropTypesInstructions(context, components, utils) return { VariableDeclarator(node) { - const destructuring = node.init && node.id && node.id.type === 'ObjectPattern'; + // Only handles destructuring + if (node.id.type !== 'ObjectPattern') { + return; + } + // let {props: {firstname}} = this - const thisDestructuring = destructuring && node.init.type === 'ThisExpression'; - // let {firstname} = props - const statelessDestructuring = destructuring && isPropAttributeName(node) && ( - utils.getParentStatelessComponent() || - isInLifeCycleMethod(node, checkAsyncSafeLifeCycles) - ); + const propsProperty = node.id.properties.find(property => ( + property.key && + (property.key.name === 'props' || property.key.value === 'props') && + property.value.type === 'ObjectPattern' + )); + if (propsProperty && node.init.type === 'ThisExpression') { + markPropTypesAsUsed(propsProperty.value); + return; + } - if (!thisDestructuring && !statelessDestructuring) { + // let {firstname} = props + if ( + isCommonVariableNameForProps(node.init.name) && + (utils.getParentStatelessComponent() || isInLifeCycleMethod(node, checkAsyncSafeLifeCycles)) + ) { + markPropTypesAsUsed(node.id); return; } - markPropTypesAsUsed(node); + + // let {firstname} = this.props + if (isThisDotProps(node.init) && isInClassComponent(utils)) { + markPropTypesAsUsed(node.id); + } }, FunctionDeclaration: handleFunctionLikeExpressions, @@ -465,7 +437,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils) }, MemberExpression(node) { - if (isPropTypesUsage(node, context, utils, checkAsyncSafeLifeCycles)) { + if (isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles)) { markPropTypesAsUsed(node); } },