Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor]: simplify utils/usedPropTypes.js #2294

Merged
merged 1 commit into from May 29, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
216 changes: 94 additions & 122 deletions lib/util/usedPropTypes.js
Expand Up @@ -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';
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 (
Expand All @@ -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':
Expand All @@ -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`);
Expand All @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
},
Expand Down