/** * @fileoverview Common defaultProps detection functionality. */ 'use strict'; const fromEntries = require('object.fromentries'); const astUtil = require('./ast'); const propsUtil = require('./props'); const variableUtil = require('./variable'); const propWrapperUtil = require('./propWrapper'); const QUOTES_REGEX = /^["']|["']$/g; module.exports = function defaultPropsInstructions(context, components, utils) { const sourceCode = context.getSourceCode(); /** * 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' && propWrapperUtil.isPropWrapperFunction(context, 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 {Object[]|'unresolved'} 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 = Object.assign( {}, defaults, fromEntries(defaultProps.map((prop) => [prop.name, prop])) ); components.set(component.node, { defaultProps: newDefaultProps }); } return { MemberExpression(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