Skip to content

Commit

Permalink
[Refactor]: simplify utils/usedPropTypes.js
Browse files Browse the repository at this point in the history
  • Loading branch information
golopot committed May 29, 2019
1 parent f7a9fb5 commit 904b83a
Showing 1 changed file with 93 additions and 114 deletions.
207 changes: 93 additions & 114 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,6 +253,13 @@ 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'
) {
markPropTypesAsUsed(node.parent.id, allNames);
}
// Do not mark computed props as used.
type = name !== '__COMPUTED_PROP__' ? 'direct' : null;
} else if (
Expand All @@ -293,31 +285,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 +304,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 +320,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.init || {};
allNames = [];
while (currentNode.property && currentNode.property.name !== 'props') {
allNames.unshift(currentNode.property.name);
Expand Down Expand Up @@ -436,19 +399,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 +444,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

0 comments on commit 904b83a

Please sign in to comment.