123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- /**
- * @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 <div>Hello {this.props.name}</div>;
- // }
- // }
- MethodDefinition(node) {
- if (!node.static || node.kind !== 'get') {
- return;
- }
- if (!propsUtil.isDefaultPropsDeclaration(node)) {
- return;
- }
- // find component this propTypes/defaultProps belongs to
- const component = components.get(utils.getParentES6Component());
- if (!component) {
- return;
- }
- const returnStatement = utils.findReturnStatement(node);
- if (!returnStatement) {
- return;
- }
- const expression = resolveNodeValue(returnStatement.argument);
- if (!expression || expression.type !== 'ObjectExpression') {
- return;
- }
- addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
- },
- // e.g.:
- // class Greeting extends React.Component {
- // render() {
- // return (
- // <h1>Hello, {this.props.foo} {this.props.bar}</h1>
- // );
- // }
- // static defaultProps = {
- // foo: 'bar',
- // bar: 'baz'
- // };
- // }
- ClassProperty(node) {
- if (!(node.static && node.value)) {
- return;
- }
- const propName = astUtil.getPropertyName(node);
- const isDefaultProp = propName === 'defaultProps' || propName === 'getDefaultProps';
- if (!isDefaultProp) {
- return;
- }
- // find component this propTypes/defaultProps belongs to
- const component = components.get(utils.getParentES6Component());
- if (!component) {
- return;
- }
- const expression = resolveNodeValue(node.value);
- if (!expression || expression.type !== 'ObjectExpression') {
- return;
- }
- addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(expression));
- },
- // e.g.:
- // React.createClass({
- // render: function() {
- // return <div>{this.props.foo}</div>;
- // },
- // getDefaultProps: function() {
- // return {
- // foo: 'default'
- // };
- // }
- // });
- ObjectExpression(node) {
- // find component this propTypes/defaultProps belongs to
- const component = utils.isES5Component(node) && components.get(node);
- if (!component) {
- return;
- }
- // Search for the proptypes declaration
- node.properties.forEach((property) => {
- if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
- return;
- }
- const isDefaultProp = propsUtil.isDefaultPropsDeclaration(property);
- if (isDefaultProp && property.value.type === 'FunctionExpression') {
- const returnStatement = utils.findReturnStatement(property);
- if (!returnStatement || returnStatement.argument.type !== 'ObjectExpression') {
- return;
- }
- addDefaultPropsToComponent(component, getDefaultPropsFromObjectExpression(returnStatement.argument));
- }
- });
- }
- };
- };
|