diff --git a/lib/rules/default-props-match-prop-types.js b/lib/rules/default-props-match-prop-types.js index 79723f18a8..99ca165757 100644 --- a/lib/rules/default-props-match-prop-types.js +++ b/lib/rules/default-props-match-prop-types.js @@ -201,39 +201,6 @@ module.exports = { }); } - /** - * Extracts a DefaultProp from an ObjectExpression node. - * @param {ASTNode} objectExpression ObjectExpression node. - * @returns {Object|string} Object representation of a defaultProp, to be consumed by - * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps - * from this ObjectExpression can't be resolved. - */ - function getDefaultPropsFromObjectExpression(objectExpression) { - const hasSpread = objectExpression.properties.find(property => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement'); - - if (hasSpread) { - return 'unresolved'; - } - - return objectExpression.properties.map(defaultProp => ({ - name: defaultProp.key.name, - node: defaultProp - })); - } - - /** - * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is - * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations - * without risking false negatives. - * @param {Object} component The component to mark. - * @returns {void} - */ - function markDefaultPropsAsUnresolved(component) { - components.set(component.node, { - defaultProps: 'unresolved' - }); - } - /** * Adds propTypes to the component passed in. * @param {ASTNode} component The component to add the propTypes to. @@ -248,31 +215,6 @@ module.exports = { }); } - /** - * Adds defaultProps to the component passed in. - * @param {ASTNode} component The component to add the defaultProps to. - * @param {String[]|String} defaultProps defaultProps to add to the component or the string "unresolved" - * if this component has defaultProps that can't be resolved. - * @returns {void} - */ - function addDefaultPropsToComponent(component, defaultProps) { - // Early return if this component's defaultProps is already marked as "unresolved". - if (component.defaultProps === 'unresolved') { - return; - } - - if (defaultProps === 'unresolved') { - markDefaultPropsAsUnresolved(component); - return; - } - - const defaults = component.defaultProps || []; - - components.set(component.node, { - defaultProps: defaults.concat(defaultProps) - }); - } - /** * Tries to find a props type annotation in a stateless component. * @param {ASTNode} node The AST node to look for a props type annotation. @@ -323,8 +265,9 @@ module.exports = { return; } - defaultProps.forEach(defaultProp => { - const prop = propFromName(propTypes, defaultProp.name); + Object.keys(defaultProps).forEach(defaultPropName => { + const defaultProp = defaultProps[defaultPropName]; + const prop = propFromName(propTypes, defaultPropName); if (prop && (allowRequiredDefaults || !prop.isRequired)) { return; @@ -334,13 +277,13 @@ module.exports = { context.report( defaultProp.node, 'defaultProp "{{name}}" defined for isRequired propType.', - {name: defaultProp.name} + {name: defaultPropName} ); } else { context.report( defaultProp.node, 'defaultProp "{{name}}" has no corresponding propTypes declaration.', - {name: defaultProp.name} + {name: defaultPropName} ); } }); @@ -353,9 +296,8 @@ module.exports = { return { MemberExpression: function(node) { const isPropType = propsUtil.isPropTypesDeclaration(node); - const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node); - if (!isPropType && !isDefaultProp) { + if (!isPropType) { return; } @@ -376,21 +318,8 @@ module.exports = { // MyComponent.propTypes = myPropTypes; if (node.parent.type === 'AssignmentExpression') { const expression = resolveNodeValue(node.parent.right); - if (!expression || expression.type !== 'ObjectExpression') { - // If a value can't be found, we mark the defaultProps declaration as "unresolved", because - // we should ignore this component and not report any errors for it, to avoid false-positives - // with e.g. external defaultProps declarations. - if (isDefaultProp) { - markDefaultPropsAsUnresolved(component); - } - - return; - } - - if (isPropType) { + if (expression && expression.type === 'ObjectExpression') { addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); - } else { - addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); } return; @@ -400,20 +329,11 @@ module.exports = { // MyComponent.propTypes.baz = React.PropTypes.string; if (node.parent.type === 'MemberExpression' && node.parent.parent && node.parent.parent.type === 'AssignmentExpression') { - if (isPropType) { - addPropTypesToComponent(component, [{ - name: node.parent.property.name, - isRequired: propsUtil.isRequiredPropType(node.parent.parent.right), - node: node.parent.parent - }]); - } else { - addDefaultPropsToComponent(component, [{ - name: node.parent.property.name, - node: node.parent.parent - }]); - } - - return; + addPropTypesToComponent(component, [{ + name: node.parent.property.name, + isRequired: propsUtil.isRequiredPropType(node.parent.parent.right), + node: node.parent.parent + }]); } }, @@ -439,9 +359,8 @@ module.exports = { } const isPropType = propsUtil.isPropTypesDeclaration(node); - const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node); - if (!isPropType && !isDefaultProp) { + if (!isPropType) { return; } @@ -461,11 +380,7 @@ module.exports = { return; } - if (isPropType) { - addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); - } else { - addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); - } + addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); }, // e.g.: @@ -496,9 +411,8 @@ module.exports = { const propName = astUtil.getPropertyName(node); const isPropType = propName === 'propTypes'; - const isDefaultProp = propName === 'defaultProps' || propName === 'getDefaultProps'; - if (!isPropType && !isDefaultProp) { + if (!isPropType) { return; } @@ -513,11 +427,7 @@ module.exports = { return; } - if (isPropType) { - addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); - } else { - addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); - } + addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); }, // e.g.: @@ -548,25 +458,11 @@ module.exports = { } const isPropType = propsUtil.isPropTypesDeclaration(property); - const isDefaultProp = propsUtil.isDefaultPropsDeclaration(property); - - if (!isPropType && !isDefaultProp) { - return; - } if (isPropType && property.value.type === 'ObjectExpression') { addPropTypesToComponent(component, getPropTypesFromObjectExpression(property.value)); return; } - - if (isDefaultProp && property.value.type === 'FunctionExpression') { - const returnStatement = utils.findReturnStatement(property); - if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') { - return; - } - - addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument)); - } }); }, diff --git a/lib/rules/require-default-props.js b/lib/rules/require-default-props.js index 0704e8e323..8b21a9841c 100644 --- a/lib/rules/require-default-props.js +++ b/lib/rules/require-default-props.js @@ -176,36 +176,6 @@ module.exports = { }); } - /** - * Extracts a DefaultProp from an ObjectExpression node. - * @param {ASTNode} objectExpression ObjectExpression node. - * @returns {Object|string} Object representation of a defaultProp, to be consumed by - * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps - * from this ObjectExpression can't be resolved. - */ - function getDefaultPropsFromObjectExpression(objectExpression) { - const hasSpread = objectExpression.properties.find(property => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement'); - - if (hasSpread) { - return 'unresolved'; - } - - return objectExpression.properties.map(property => sourceCode.getText(property.key).replace(QUOTES_REGEX, '')); - } - - /** - * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is - * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations - * without risking false negatives. - * @param {Object} component The component to mark. - * @returns {void} - */ - function markDefaultPropsAsUnresolved(component) { - components.set(component.node, { - defaultProps: 'unresolved' - }); - } - /** * Adds propTypes to the component passed in. * @param {ASTNode} component The component to add the propTypes to. @@ -220,35 +190,6 @@ module.exports = { }); } - /** - * Adds defaultProps to the component passed in. - * @param {ASTNode} component The component to add the defaultProps to. - * @param {String[]|String} defaultProps defaultProps to add to the component or the string "unresolved" - * if this component has defaultProps that can't be resolved. - * @returns {void} - */ - function addDefaultPropsToComponent(component, defaultProps) { - // Early return if this component's defaultProps is already marked as "unresolved". - if (component.defaultProps === 'unresolved') { - return; - } - - if (defaultProps === 'unresolved') { - markDefaultPropsAsUnresolved(component); - return; - } - - const defaults = component.defaultProps || {}; - - defaultProps.forEach(defaultProp => { - defaults[defaultProp] = true; - }); - - components.set(component.node, { - defaultProps: defaults - }); - } - /** * Tries to find a props type annotation in a stateless component. * @param {ASTNode} node The AST node to look for a props type annotation. @@ -370,9 +311,8 @@ module.exports = { return { MemberExpression: function(node) { const isPropType = propsUtil.isPropTypesDeclaration(node); - const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node); - if (!isPropType && !isDefaultProp) { + if (!isPropType) { return; } @@ -394,39 +334,21 @@ module.exports = { if (node.parent.type === 'AssignmentExpression') { const expression = resolveNodeValue(node.parent.right); if (!expression || expression.type !== 'ObjectExpression') { - // If a value can't be found, we mark the defaultProps declaration as "unresolved", because - // we should ignore this component and not report any errors for it, to avoid false-positives - // with e.g. external defaultProps declarations. - if (isDefaultProp) { - markDefaultPropsAsUnresolved(component); - } - return; } - if (isPropType) { - addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); - } else { - addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); - } - + addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); return; } // e.g.: // MyComponent.propTypes.baz = PropTypes.string; if (node.parent.type === 'MemberExpression' && node.parent.parent.type === 'AssignmentExpression') { - if (isPropType) { - addPropTypesToComponent(component, [{ - name: node.parent.property.name, - isRequired: propsUtil.isRequiredPropType(node.parent.parent.right), - node: node.parent.parent - }]); - } else { - addDefaultPropsToComponent(component, [node.parent.property.name]); - } - - return; + addPropTypesToComponent(component, [{ + name: node.parent.property.name, + isRequired: propsUtil.isRequiredPropType(node.parent.parent.right), + node: node.parent.parent + }]); } }, @@ -452,9 +374,8 @@ module.exports = { } const isPropType = propsUtil.isPropTypesDeclaration(node); - const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node); - if (!isPropType && !isDefaultProp) { + if (!isPropType) { return; } @@ -474,11 +395,7 @@ module.exports = { return; } - if (isPropType) { - addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); - } else { - addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); - } + addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); }, // e.g.: @@ -508,9 +425,8 @@ module.exports = { } const isPropType = astUtil.getPropertyName(node) === 'propTypes'; - const isDefaultProp = astUtil.getPropertyName(node) === 'defaultProps' || astUtil.getPropertyName(node) === 'getDefaultProps'; - if (!isPropType && !isDefaultProp) { + if (!isPropType) { return; } @@ -525,11 +441,7 @@ module.exports = { return; } - if (isPropType) { - addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); - } else { - addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); - } + addPropTypesToComponent(component, getPropTypesFromObjectExpression(expression)); }, // e.g.: @@ -560,25 +472,11 @@ module.exports = { } const isPropType = propsUtil.isPropTypesDeclaration(property); - const isDefaultProp = propsUtil.isDefaultPropsDeclaration(property); - - if (!isPropType && !isDefaultProp) { - return; - } if (isPropType && property.value.type === 'ObjectExpression') { addPropTypesToComponent(component, getPropTypesFromObjectExpression(property.value)); return; } - - if (isDefaultProp && property.value.type === 'FunctionExpression') { - const returnStatement = utils.findReturnStatement(property); - if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') { - return; - } - - addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument)); - } }); }, diff --git a/lib/util/Components.js b/lib/util/Components.js index 1859600f35..8ab2aff43f 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -11,6 +11,7 @@ const variableUtil = require('./variable'); const pragmaUtil = require('./pragma'); const astUtil = require('./ast'); const propTypes = require('./propTypes'); +const defaultProps = require('./defaultProps'); function getId(node) { return node && node.range.join(':'); @@ -696,7 +697,9 @@ 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 defaultPropsInstructions = defaultProps(context, components, utils); + const allKeys = new Set(Object.keys(detectionInstructions).concat(Object.keys(propTypesInstructions)) + .concat(Object.keys(defaultPropsInstructions))); allKeys.forEach(instruction => { updatedRuleInstructions[instruction] = function(node) { if (instruction in detectionInstructions) { @@ -705,6 +708,9 @@ function componentRule(rule, context) { if (instruction in propTypesInstructions) { propTypesInstructions[instruction](node); } + if (instruction in defaultPropsInstructions) { + defaultPropsInstructions[instruction](node); + } return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : void 0; }; }); diff --git a/lib/util/defaultProps.js b/lib/util/defaultProps.js new file mode 100644 index 0000000000..f60f0fc908 --- /dev/null +++ b/lib/util/defaultProps.js @@ -0,0 +1,263 @@ +/** + * @fileoverview Common defaultProps detection functionality. + */ +'use strict'; + +const astUtil = require('./ast'); +const propsUtil = require('./props'); +const variableUtil = require('./variable'); + +const QUOTES_REGEX = /^["']|["']$/g; + +module.exports = function defaultPropsInstructions(context, components, utils) { + const sourceCode = context.getSourceCode(); + const propWrapperFunctions = new Set(context.settings.propWrapperFunctions || []); + + /** + * Try to resolve the node passed in to a variable in the current scope. If the node passed in is not + * an Identifier, then the node is simply returned. + * @param {ASTNode} node The node to resolve. + * @returns {ASTNode|null} Return null if the value could not be resolved, ASTNode otherwise. + */ + function resolveNodeValue(node) { + if (node.type === 'Identifier') { + return variableUtil.findVariableByName(context, node.name); + } + if ( + node.type === 'CallExpression' && + propWrapperFunctions.has(node.callee.name) && + node.arguments && node.arguments[0] + ) { + return resolveNodeValue(node.arguments[0]); + } + return node; + } + + /** + * Extracts a DefaultProp from an ObjectExpression node. + * @param {ASTNode} objectExpression ObjectExpression node. + * @returns {Object|string} Object representation of a defaultProp, to be consumed by + * `addDefaultPropsToComponent`, or string "unresolved", if the defaultProps + * from this ObjectExpression can't be resolved. + */ + function getDefaultPropsFromObjectExpression(objectExpression) { + const hasSpread = objectExpression.properties.find(property => property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement'); + + if (hasSpread) { + return 'unresolved'; + } + + return objectExpression.properties.map(defaultProp => ({ + name: sourceCode.getText(defaultProp.key).replace(QUOTES_REGEX, ''), + node: defaultProp + })); + } + + /** + * Marks a component's DefaultProps declaration as "unresolved". A component's DefaultProps is + * marked as "unresolved" if we cannot safely infer the values of its defaultProps declarations + * without risking false negatives. + * @param {Object} component The component to mark. + * @returns {void} + */ + function markDefaultPropsAsUnresolved(component) { + components.set(component.node, { + defaultProps: 'unresolved' + }); + } + + /** + * Adds defaultProps to the component passed in. + * @param {ASTNode} component The component to add the defaultProps to. + * @param {String[]|String} defaultProps defaultProps to add to the component or the string "unresolved" + * if this component has defaultProps that can't be resolved. + * @returns {void} + */ + function addDefaultPropsToComponent(component, defaultProps) { + // Early return if this component's defaultProps is already marked as "unresolved". + if (component.defaultProps === 'unresolved') { + return; + } + + if (defaultProps === 'unresolved') { + markDefaultPropsAsUnresolved(component); + return; + } + + const defaults = component.defaultProps || {}; + const newDefaultProps = defaultProps.reduce((props, x) => { + props[x.name] = x; + return props; + }, Object.assign({}, defaults)); + components.set(component.node, { + defaultProps: newDefaultProps + }); + } + + return { + MemberExpression: function(node) { + const isDefaultProp = propsUtil.isDefaultPropsDeclaration(node); + + if (!isDefaultProp) { + return; + } + + // find component this defaultProps belongs to + const component = utils.getRelatedComponent(node); + if (!component) { + return; + } + + // e.g.: + // MyComponent.propTypes = { + // foo: React.PropTypes.string.isRequired, + // bar: React.PropTypes.string + // }; + // + // or: + // + // MyComponent.propTypes = myPropTypes; + if (node.parent.type === 'AssignmentExpression') { + const expression = resolveNodeValue(node.parent.right); + if (!expression || expression.type !== 'ObjectExpression') { + // If a value can't be found, we mark the defaultProps declaration as "unresolved", because + // we should ignore this component and not report any errors for it, to avoid false-positives + // with e.g. external defaultProps declarations. + if (isDefaultProp) { + markDefaultPropsAsUnresolved(component); + } + + return; + } + + addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression)); + + return; + } + + // e.g.: + // MyComponent.propTypes.baz = React.PropTypes.string; + if (node.parent.type === 'MemberExpression' && node.parent.parent && + node.parent.parent.type === 'AssignmentExpression') { + addDefaultPropsToComponent(component, [{ + name: node.parent.property.name, + node: node.parent.parent + }]); + } + }, + + // e.g.: + // class Hello extends React.Component { + // static get defaultProps() { + // return { + // name: 'Dean' + // }; + // } + // render() { + // return