text: /**
   * @fileoverview Utility class and functions for React components detection
   * @author Yannick Croissant
   */
  
  'use strict';
  
  const doctrine = require('doctrine');
  const arrayIncludes = require('array-includes');
  const values = require('object.values');
  
  const variableUtil = require('./variable');
  const pragmaUtil = require('./pragma');
  const astUtil = require('./ast');
  const propTypesUtil = require('./propTypes');
  const jsxUtil = require('./jsx');
  const usedPropTypesUtil = require('./usedPropTypes');
  const defaultPropsUtil = require('./defaultProps');
  const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
  const isCreateElement = require('./isCreateElement');
  const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport');
  
  function getId(node) {
    return node && node.range.join(':');
  }
  
  function usedPropTypesAreEquivalent(propA, propB) {
    if (propA.name === propB.name) {
      if (!propA.allNames && !propB.allNames) {
        return true;
      }
      if (Array.isArray(propA.allNames) && Array.isArray(propB.allNames) && propA.allNames.join('') === propB.allNames.join('')) {
        return true;
      }
      return false;
    }
    return false;
  }
  
  function mergeUsedPropTypes(propsList, newPropsList) {
    const propsToAdd = newPropsList.filter((newProp) => {
      const newPropIsAlreadyInTheList = propsList.some((prop) => usedPropTypesAreEquivalent(prop, newProp));
      return !newPropIsAlreadyInTheList;
    });
  
    return propsList.concat(propsToAdd);
  }
  
  const Lists = new WeakMap();
  
  /**
   * Components
   */
  class Components {
    constructor() {
      Lists.set(this, {});
    }
  
    /**
     * Add a node to the components list, or update it if it's already in the list
     *
     * @param {ASTNode} node The AST node being added.
     * @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
     * @returns {Object} Added component object
     */
    add(node, confidence) {
      const id = getId(node);
      const list = Lists.get(this);
      if (list[id]) {
        if (confidence === 0 || list[id].confidence === 0) {
          list[id].confidence = 0;
        } else {
          list[id].confidence = Math.max(list[id].confidence, confidence);
        }
        return list[id];
      }
      list[id] = {
        node,
        confidence,
      };
      return list[id];
    }
  
    /**
     * Find a component in the list using its node
     *
     * @param {ASTNode} node The AST node being searched.
     * @returns {Object} Component object, undefined if the component is not found or has confidence value of 0.
     */
    get(node) {
      const id = getId(node);
      const item = Lists.get(this)[id];
      if (item && item.confidence >= 1) {
        return item;
      }
      return null;
    }
  
    /**
     * Update a component in the list
     *
     * @param {ASTNode} node The AST node being updated.
     * @param {Object} props Additional properties to add to the component.
     */
    set(node, props) {
      const list = Lists.get(this);
      let component = list[getId(node)];
      while (!component) {
        node = node.parent;
        if (!node) {
          return;
        }
        component = list[getId(node)];
      }
  
      Object.assign(
        component,
        props,
        {
          usedPropTypes: mergeUsedPropTypes(
            component.usedPropTypes || [],
            props.usedPropTypes || []
          ),
        }
      );
    }
  
    /**
     * Return the components list
     * Components for which we are not confident are not returned
     *
     * @returns {Object} Components list
     */
    list() {
      const thisList = Lists.get(this);
      const list = {};
      const usedPropTypes = {};
  
      // Find props used in components for which we are not confident
      Object.keys(thisList).filter((i) => thisList[i].confidence < 2).forEach((i) => {
        let component = null;
        let node = null;
        node = thisList[i].node;
        while (!component && node.parent) {
          node = node.parent;
          // Stop moving up if we reach a decorator
          if (node.type === 'Decorator') {
            break;
          }
          component = this.get(node);
        }
        if (component) {
          const newUsedProps = (thisList[i].usedPropTypes || []).filter((propType) => !propType.node || propType.node.kind !== 'init');
  
          const componentId = getId(component.node);
  
          usedPropTypes[componentId] = mergeUsedPropTypes(usedPropTypes[componentId] || [], newUsedProps);
        }
      });
  
      // Assign used props in not confident components to the parent component
      Object.keys(thisList).filter((j) => thisList[j].confidence >= 2).forEach((j) => {
        const id = getId(thisList[j].node);
        list[j] = thisList[j];
        if (usedPropTypes[id]) {
          list[j].usedPropTypes = mergeUsedPropTypes(list[j].usedPropTypes || [], usedPropTypes[id]);
        }
      });
      return list;
    }
  
    /**
     * Return the length of the components list
     * Components for which we are not confident are not counted
     *
     * @returns {Number} Components list length
     */
    length() {
      const list = Lists.get(this);
      return Object.keys(list).filter((i) => list[i].confidence >= 2).length;
    }
  }
  
  function getWrapperFunctions(context, pragma) {
    const componentWrapperFunctions = context.settings.componentWrapperFunctions || [];
  
    // eslint-disable-next-line arrow-body-style
    return componentWrapperFunctions.map((wrapperFunction) => {
      return typeof wrapperFunction === 'string'
        ? { property: wrapperFunction }
        : Object.assign({}, wrapperFunction, {
          object: wrapperFunction.object === '<pragma>' ? pragma : wrapperFunction.object,
        });
    }).concat([
      { property: 'forwardRef', object: pragma },
      { property: 'memo', object: pragma },
    ]);
  }
  
  function componentRule(rule, context) {
    const createClass = pragmaUtil.getCreateClassFromContext(context);
    const pragma = pragmaUtil.getFromContext(context);
    const sourceCode = context.getSourceCode();
    const components = new Components();
    const wrapperFunctions = getWrapperFunctions(context, pragma);
  
    // Utilities for component detection
    const utils = {
  
      /**
       * Check if the node is a React ES5 component
       *
       * @param {ASTNode} node The AST node being checked.
       * @returns {Boolean} True if the node is a React ES5 component, false if not
       */
      isES5Component(node) {
        if (!node.parent) {
          return false;
        }
        return new RegExp(`^(${pragma}\\.)?${createClass}$`).test(sourceCode.getText(node.parent.callee));
      },
  
      /**
       * Check if the node is a React ES6 component
       *
       * @param {ASTNode} node The AST node being checked.
       * @returns {Boolean} True if the node is a React ES6 component, false if not
       */
      isES6Component(node) {
        if (utils.isExplicitComponent(node)) {
          return true;
        }
  
        if (!node.superClass) {
          return false;
        }
        return new RegExp(`^(${pragma}\\.)?(Pure)?Component$`).test(sourceCode.getText(node.superClass));
      },
  
      /**
       * Check if the node is explicitly declared as a descendant of a React Component
       *
       * @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression).
       * @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not
       */
      isExplicitComponent(node) {
        let comment;
        // Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
        // Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
        // eslint-disable-next-line no-warning-comments
        // FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented.
        try {
          comment = sourceCode.getJSDocComment(node);
        } catch (e) {
          comment = null;
        }
  
        if (comment === null) {
          return false;
        }
  
        const commentAst = doctrine.parse(comment.value, {
          unwrap: true,
          tags: ['extends', 'augments'],
        });
  
        const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent');
  
        return relevantTags.length > 0;
      },
  
      /**
       * Checks to see if our component extends React.PureComponent
       *
       * @param {ASTNode} node The AST node being checked.
       * @returns {Boolean} True if node extends React.PureComponent, false if not
       */
      isPureComponent(node) {
        if (node.superClass) {
          return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass));
        }
        return false;
      },
  
      /**
       * Check if variable is destructured from pragma import
       *
       * @param {string} variable The variable name to check
       * @returns {Boolean} True if createElement is destructured from the pragma
       */
      isDestructuredFromPragmaImport(variable) {
        return isDestructuredFromPragmaImport(variable, context);
      },
  
      /**
       * Checks to see if node is called within createElement from pragma
       *
       * @param {ASTNode} node The AST node being checked.
       * @returns {Boolean} True if createElement called from pragma
       */
      isCreateElement(node) {
        return isCreateElement(node, context);
      },
  
      /**
       * Check if we are in a class constructor
       * @return {boolean} true if we are in a class constructor, false if not
       */
      inConstructor() {
        let scope = context.getScope();
        while (scope) {
          if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
            return true;
          }
          scope = scope.upper;
        }
        return false;
      },
  
      /**
       * Determine if the node is MemberExpression of `this.state`
       * @param {Object} node The node to process
       * @returns {Boolean}
       */
      isStateMemberExpression(node) {
        return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state';
      },
  
      isReturningJSX(ASTNode, strict) {
        return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict, true);
      },
  
      isReturningJSXOrNull(ASTNode, strict) {
        return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict);
      },
  
      getPragmaComponentWrapper(node) {
        let isPragmaComponentWrapper;
        let currentNode = node;
        let prevNode;
        do {
          currentNode = currentNode.parent;
          isPragmaComponentWrapper = this.isPragmaComponentWrapper(currentNode);
          if (isPragmaComponentWrapper) {
            prevNode = currentNode;
          }
        } while (isPragmaComponentWrapper);
  
        return prevNode;
      },
  
      getComponentNameFromJSXElement(node) {
        if (node.type !== 'JSXElement') {
          return null;
        }
        if (node.openingElement && node.openingElement.name && node.openingElement.name.name) {
          return node.openingElement.name.name;
        }
        return null;
      },
  
      /**
       * Getting the first JSX element's name.
       * @param {object} node
       * @returns {string | null}
       */
      getNameOfWrappedComponent(node) {
        if (node.length < 1) {
          return null;
        }
        const body = node[0].body;
        if (!body) {
          return null;
        }
        if (body.type === 'JSXElement') {
          return this.getComponentNameFromJSXElement(body);
        }
        if (body.type === 'BlockStatement') {
          const jsxElement = body.body.find((item) => item.type === 'ReturnStatement');
          return jsxElement
            && jsxElement.argument
            && this.getComponentNameFromJSXElement(jsxElement.argument);
        }
        return null;
      },
  
      /**
       * Get the list of names of components created till now
       * @returns {string | boolean}
       */
      getDetectedComponents() {
        const list = components.list();
        return values(list).filter((val) => {
          if (val.node.type === 'ClassDeclaration') {
            return true;
          }
          if (
            val.node.type === 'ArrowFunctionExpression'
            && val.node.parent
            && val.node.parent.type === 'VariableDeclarator'
            && val.node.parent.id
          ) {
            return true;
          }
          return false;
        }).map((val) => {
          if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name;
          return val.node.id && val.node.id.name;
        });
      },
  
      /**
       * It will check wheater memo/forwardRef is wrapping existing component or
       * creating a new one.
       * @param {object} node
       * @returns {boolean}
       */
      nodeWrapsComponent(node) {
        const childComponent = this.getNameOfWrappedComponent(node.arguments);
        const componentList = this.getDetectedComponents();
        return !!childComponent && arrayIncludes(componentList, childComponent);
      },
  
      isPragmaComponentWrapper(node) {
        if (!node || node.type !== 'CallExpression') {
          return false;
        }
  
        return wrapperFunctions.some((wrapperFunction) => {
          if (node.callee.type === 'MemberExpression') {
            return wrapperFunction.object
              && wrapperFunction.object === node.callee.object.name
              && wrapperFunction.property === node.callee.property.name
              && !this.nodeWrapsComponent(node);
          }
          return wrapperFunction.property === node.callee.name
            && (!wrapperFunction.object
              // Functions coming from the current pragma need special handling
              || (wrapperFunction.object === pragma && this.isDestructuredFromPragmaImport(node.callee.name))
            );
        });
      },
  
      /**
       * Find a return statement in the current node
       *
       * @param {ASTNode} node The AST node being checked
       */
      findReturnStatement: astUtil.findReturnStatement,
  
      /**
       * Get the parent component node from the current scope
       *
       * @returns {ASTNode} component node, null if we are not in a component
       */
      getParentComponent() {
        return (
          utils.getParentES6Component()
          || utils.getParentES5Component()
          || utils.getParentStatelessComponent()
        );
      },
  
      /**
       * Get the parent ES5 component node from the current scope
       *
       * @returns {ASTNode} component node, null if we are not in a component
       */
      getParentES5Component() {
        let scope = context.getScope();
        while (scope) {
          const node = scope.block && scope.block.parent && scope.block.parent.parent;
          if (node && utils.isES5Component(node)) {
            return node;
          }
          scope = scope.upper;
        }
        return null;
      },
  
      /**
       * Get the parent ES6 component node from the current scope
       *
       * @returns {ASTNode} component node, null if we are not in a component
       */
      getParentES6Component() {
        let scope = context.getScope();
        while (scope && scope.type !== 'class') {
          scope = scope.upper;
        }
        const node = scope && scope.block;
        if (!node || !utils.isES6Component(node)) {
          return null;
        }
        return node;
      },
  
      /**
       * @param {ASTNode} node
       * @returns {boolean}
       */
      isInAllowedPositionForComponent(node) {
        switch (node.parent.type) {
          case 'VariableDeclarator':
          case 'AssignmentExpression':
          case 'Property':
          case 'ReturnStatement':
          case 'ExportDefaultDeclaration':
          case 'ArrowFunctionExpression': {
            return true;
          }
          case 'SequenceExpression': {
            return utils.isInAllowedPositionForComponent(node.parent)
              && node === node.parent.expressions[node.parent.expressions.length - 1];
          }
          default:
            return false;
        }
      },
  
      /**
       * Get node if node is a stateless component, or node.parent in cases like
       * `React.memo` or `React.forwardRef`. Otherwise returns `undefined`.
       * @param {ASTNode} node
       * @returns {ASTNode | undefined}
       */
      getStatelessComponent(node) {
        const parent = node.parent;
        if (
          node.type === 'FunctionDeclaration'
          && (!node.id || isFirstLetterCapitalized(node.id.name))
          && utils.isReturningJSXOrNull(node)
        ) {
          return node;
        }
  
        if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
          const isMethod = parent.type === 'Property' && parent.method;
          const isPropertyAssignment = parent.type === 'AssignmentExpression'
            && parent.left.type === 'MemberExpression';
          const isModuleExportsAssignment = isPropertyAssignment
            && parent.left.object.name === 'module'
            && parent.left.property.name === 'exports';
  
          if (node.parent.type === 'ExportDefaultDeclaration') {
            if (utils.isReturningJSX(node)) {
              return node;
            }
            return undefined;
          }
  
          if (node.parent.type === 'VariableDeclarator' && utils.isReturningJSXOrNull(node)) {
            if (isFirstLetterCapitalized(node.parent.id.name)) {
              return node;
            }
            return undefined;
          }
  
          // Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
          const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
          if (pragmaComponentWrapper) {
            return pragmaComponentWrapper;
          }
  
          if (!(utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node))) {
            return undefined;
          }
  
          if (utils.isParentComponentNotStatelessComponent(node)) {
            return undefined;
          }
  
          if (isMethod && !isFirstLetterCapitalized(node.parent.key.name)) {
            return utils.isReturningJSX(node) ? node : undefined;
          }
  
          if (node.id) {
            return isFirstLetterCapitalized(node.id.name) ? node : undefined;
          }
  
          if (
            isPropertyAssignment
            && !isModuleExportsAssignment
            && !isFirstLetterCapitalized(parent.left.property.name)
          ) {
            return undefined;
          }
  
          return node;
        }
  
        return undefined;
      },
  
      /**
       * Get the parent stateless component node from the current scope
       *
       * @returns {ASTNode} component node, null if we are not in a component
       */
      getParentStatelessComponent() {
        let scope = context.getScope();
        while (scope) {
          const node = scope.block;
          const statelessComponent = utils.getStatelessComponent(node);
          if (statelessComponent) {
            return statelessComponent;
          }
          scope = scope.upper;
        }
        return null;
      },
  
      /**
       * Get the related component from a node
       *
       * @param {ASTNode} node The AST node being checked (must be a MemberExpression).
       * @returns {ASTNode} component node, null if we cannot find the component
       */
      getRelatedComponent(node) {
        let i;
        let j;
        let k;
        let l;
        let componentNode;
        // Get the component path
        const componentPath = [];
        while (node) {
          if (node.property && node.property.type === 'Identifier') {
            componentPath.push(node.property.name);
          }
          if (node.object && node.object.type === 'Identifier') {
            componentPath.push(node.object.name);
          }
          node = node.object;
        }
        componentPath.reverse();
        const componentName = componentPath.slice(0, componentPath.length - 1).join('.');
  
        // Find the variable in the current scope
        const variableName = componentPath.shift();
        if (!variableName) {
          return null;
        }
        let variableInScope;
        const variables = variableUtil.variablesInScope(context);
        for (i = 0, j = variables.length; i < j; i++) {
          if (variables[i].name === variableName) {
            variableInScope = variables[i];
            break;
          }
        }
        if (!variableInScope) {
          return null;
        }
  
        // Try to find the component using variable references
        const refs = variableInScope.references;
        refs.some((ref) => {
          let refId = ref.identifier;
          if (refId.parent && refId.parent.type === 'MemberExpression') {
            refId = refId.parent;
          }
          if (sourceCode.getText(refId) !== componentName) {
            return false;
          }
          if (refId.type === 'MemberExpression') {
            componentNode = refId.parent.right;
          } else if (
            refId.parent
            && refId.parent.type === 'VariableDeclarator'
            && refId.parent.init
            && refId.parent.init.type !== 'Identifier'
          ) {
            componentNode = refId.parent.init;
          }
          return true;
        });
  
        if (componentNode) {
          // Return the component
          return components.add(componentNode, 1);
        }
  
        // Try to find the component using variable declarations
        const defs = variableInScope.defs;
        const defInScope = defs.find((def) => (
          def.type === 'ClassName'
          || def.type === 'FunctionName'
          || def.type === 'Variable'
        ));
        if (!defInScope || !defInScope.node) {
          return null;
        }
        componentNode = defInScope.node.init || defInScope.node;
  
        // Traverse the node properties to the component declaration
        for (i = 0, j = componentPath.length; i < j; i++) {
          if (!componentNode.properties) {
            continue; // eslint-disable-line no-continue
          }
          for (k = 0, l = componentNode.properties.length; k < l; k++) {
            if (componentNode.properties[k].key && componentNode.properties[k].key.name === componentPath[i]) {
              componentNode = componentNode.properties[k];
              break;
            }
          }
          if (!componentNode || !componentNode.value) {
            return null;
          }
          componentNode = componentNode.value;
        }
  
        // Return the component
        return components.add(componentNode, 1);
      },
  
      isParentComponentNotStatelessComponent(node) {
        return !!(
          node.parent
          && node.parent.key
          && node.parent.key.type === 'Identifier'
          // custom component functions must start with a capital letter (returns false otherwise)
          && node.parent.key.name.charAt(0) === node.parent.key.name.charAt(0).toLowerCase()
          // react render function cannot have params
          && !!(node.params || []).length
        );
      },
    };
  
    // Component detection instructions
    const detectionInstructions = {
      CallExpression(node) {
        if (!utils.isPragmaComponentWrapper(node)) {
          return;
        }
        if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
          components.add(node, 2);
        }
      },
  
      ClassExpression(node) {
        if (!utils.isES6Component(node)) {
          return;
        }
        components.add(node, 2);
      },
  
      ClassDeclaration(node) {
        if (!utils.isES6Component(node)) {
          return;
        }
        components.add(node, 2);
      },
  
      ClassProperty(node) {
        node = utils.getParentComponent();
        if (!node) {
          return;
        }
        components.add(node, 2);
      },
  
      ObjectExpression(node) {
        if (!utils.isES5Component(node)) {
          return;
        }
        components.add(node, 2);
      },
  
      FunctionExpression(node) {
        if (node.async) {
          components.add(node, 0);
          return;
        }
  
        const component = utils.getParentComponent();
        if (
          !component
          || (component.parent && component.parent.type === 'JSXExpressionContainer')
        ) {
          // Ban the node if we cannot find a parent component
          components.add(node, 0);
          return;
        }
        components.add(component, 1);
      },
  
      FunctionDeclaration(node) {
        if (node.async) {
          components.add(node, 0);
          return;
        }
  
        node = utils.getParentComponent();
        if (!node) {
          return;
        }
        components.add(node, 1);
      },
  
      ArrowFunctionExpression(node) {
        if (node.async) {
          components.add(node, 0);
          return;
        }
  
        const component = utils.getParentComponent();
  
        if (
          !component
          || (component.parent && component.parent.type === 'JSXExpressionContainer')
        ) {
          // Ban the node if we cannot find a parent component
          components.add(node, 0);
          return;
        }
        if (component.expression && utils.isReturningJSX(component)) {
          components.add(component, 2);
        } else {
          components.add(component, 1);
        }
      },
  
      ThisExpression(node) {
        const component = utils.getParentComponent();
        if (!component || !/Function/.test(component.type) || !node.parent.property) {
          return;
        }
        // Ban functions accessing a property on a ThisExpression
        components.add(node, 0);
      },
  
      ReturnStatement(node) {
        if (!utils.isReturningJSX(node)) {
          return;
        }
        node = utils.getParentComponent();
        if (!node) {
          const scope = context.getScope();
          components.add(scope.block, 1);
          return;
        }
        components.add(node, 2);
      },
    };
  
    // Update the provided rule instructions to add the component detection
    const ruleInstructions = rule(context, components, utils);
    const updatedRuleInstructions = Object.assign({}, ruleInstructions);
    const propTypesInstructions = propTypesUtil(context, components, utils);
    const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils);
    const defaultPropsInstructions = defaultPropsUtil(context, components, utils);
    const allKeys = new Set(Object.keys(detectionInstructions).concat(
      Object.keys(propTypesInstructions),
      Object.keys(usedPropTypesInstructions),
      Object.keys(defaultPropsInstructions)
    ));
  
    allKeys.forEach((instruction) => {
      updatedRuleInstructions[instruction] = (node) => {
        if (instruction in detectionInstructions) {
          detectionInstructions[instruction](node);
        }
        if (instruction in propTypesInstructions) {
          propTypesInstructions[instruction](node);
        }
        if (instruction in usedPropTypesInstructions) {
          usedPropTypesInstructions[instruction](node);
        }
        if (instruction in defaultPropsInstructions) {
          defaultPropsInstructions[instruction](node);
        }
        if (ruleInstructions[instruction]) {
          return ruleInstructions[instruction](node);
        }
      };
    });
  
    // Return the updated rule instructions
    return updatedRuleInstructions;
  }
  
  module.exports = Object.assign(Components, {
    detect(rule) {
      return componentRule.bind(this, rule);
    },
  });
  