text: /**
   * @fileoverview Common defaultProps detection functionality.
   */
  
  'use strict';
  
  const astUtil = require('./ast');
  const propsUtil = require('./props');
  const variableUtil = require('./variable');
  const propWrapperUtil = require('./propWrapper');
  const {fromEntries} = require('./fromentries');
  
  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: Inferno.PropTypes.string.isRequired,
        //   bar: Inferno.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 = Inferno.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 Inferno.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 Inferno.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.:
      // Inferno.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));
          }
        });
      }
    };
  };
  