text: /**
   * @file The event generator for AST nodes.
   * @author Toru Nagashima
   */
  
  'use strict';
  
  // ------------------------------------------------------------------------------
  // Requirements
  // ------------------------------------------------------------------------------
  
  const esquery = require('esquery');
  const lodash = require('lodash');
  
  // ------------------------------------------------------------------------------
  // Typedefs
  // ------------------------------------------------------------------------------
  
  /**
   * An object describing an AST selector
   * @typedef {object} ASTSelector
   * @property {string} rawSelector The string that was parsed into this selector
   * @property {boolean} isExit `true` if this should be emitted when exiting the node rather than when entering
   * @property {object} parsedSelector An object (from esquery) describing the matching behavior of the selector
   * @property {string[]|null} listenerTypes A list of node types that could possibly cause the selector to match,
   * or `null` if all node types could cause a match
   * @property {number} attributeCount The total number of classes, pseudo-classes, and attribute queries in this selector
   * @property {number} identifierCount The total number of identifier queries in this selector
   */
  
  // ------------------------------------------------------------------------------
  // Helpers
  // ------------------------------------------------------------------------------
  
  /**
   * Gets the possible types of a selector
   * @param {object} parsedSelector An object (from esquery) describing the matching behavior of the selector
   * @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
   */
  function getPossibleTypes(parsedSelector) {
    switch (parsedSelector.type) {
      case 'identifier':
        return [parsedSelector.value];
  
      case 'matches': {
        const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
  
        if (typesForComponents.every(Boolean)) {
          return lodash.union(...typesForComponents);
        }
  
        return null;
      }
  
      case 'compound': {
        const typesForComponents = parsedSelector.selectors
          .map(getPossibleTypes)
          .filter((typesForComponent) => typesForComponent);
  
        // If all of the components could match any type, then the compound could also match any type.
        if (typesForComponents.length === 0) {
          return null;
        }
  
        /*
         * If at least one of the components could only match a particular type, the compound could only match
         * the intersection of those types.
         */
        return lodash.intersection(...typesForComponents);
      }
  
      case 'child':
      case 'descendant':
      case 'sibling':
      case 'adjacent':
        return getPossibleTypes(parsedSelector.right);
  
      default:
        return null;
    }
  }
  
  /**
   * Counts the number of class, pseudo-class, and attribute queries in this selector
   * @param {object} parsedSelector An object (from esquery) describing the selector's matching behavior
   * @returns {number} The number of class, pseudo-class, and attribute queries in this selector
   */
  function countClassAttributes(parsedSelector) {
    switch (parsedSelector.type) {
      case 'child':
      case 'descendant':
      case 'sibling':
      case 'adjacent':
        return (
          countClassAttributes(parsedSelector.left) +
          countClassAttributes(parsedSelector.right)
        );
  
      case 'compound':
      case 'not':
      case 'matches':
        return parsedSelector.selectors.reduce(
          (sum, childSelector) => sum + countClassAttributes(childSelector),
          0
        );
  
      case 'attribute':
      case 'field':
      case 'nth-child':
      case 'nth-last-child':
        return 1;
  
      default:
        return 0;
    }
  }
  
  /**
   * Counts the number of identifier queries in this selector
   * @param {object} parsedSelector An object (from esquery) describing the selector's matching behavior
   * @returns {number} The number of identifier queries
   */
  function countIdentifiers(parsedSelector) {
    switch (parsedSelector.type) {
      case 'child':
      case 'descendant':
      case 'sibling':
      case 'adjacent':
        return (
          countIdentifiers(parsedSelector.left) +
          countIdentifiers(parsedSelector.right)
        );
  
      case 'compound':
      case 'not':
      case 'matches':
        return parsedSelector.selectors.reduce(
          (sum, childSelector) => sum + countIdentifiers(childSelector),
          0
        );
  
      case 'identifier':
        return 1;
  
      default:
        return 0;
    }
  }
  
  /**
   * Compares the specificity of two selector objects, with CSS-like rules.
   * @param {ASTSelector} selectorA An AST selector descriptor
   * @param {ASTSelector} selectorB Another AST selector descriptor
   * @returns {number}
   * a value less than 0 if selectorA is less specific than selectorB
   * a value greater than 0 if selectorA is more specific than selectorB
   * a value less than 0 if selectorA and selectorB have the same specificity, and selectorA <= selectorB alphabetically
   * a value greater than 0 if selectorA and selectorB have the same specificity, and selectorA > selectorB alphabetically
   */
  function compareSpecificity(selectorA, selectorB) {
    return (
      selectorA.attributeCount - selectorB.attributeCount ||
      selectorA.identifierCount - selectorB.identifierCount ||
      (selectorA.rawSelector <= selectorB.rawSelector ? -1 : 1)
    );
  }
  
  /**
   * Parses a raw selector string, and throws a useful error if parsing fails.
   * @param {string} rawSelector A raw AST selector
   * @returns {object} An object (from esquery) describing the matching behavior of this selector
   * @throws {Error} An error if the selector is invalid
   */
  function tryParseSelector(rawSelector) {
    try {
      return esquery.parse(rawSelector.replace(/:exit$/u, ''));
    } catch (error) {
      if (
        error.location &&
        error.location.start &&
        typeof error.location.start.offset === 'number'
      ) {
        throw new SyntaxError(
          `Syntax error in selector "${rawSelector}" at position ${error.location.start.offset}: ${error.message}`
        );
      }
  
      throw error;
    }
  }
  
  /**
   * Parses a raw selector string, and returns the parsed selector along with specificity and type information.
   * @param {string} rawSelector A raw AST selector
   * @returns {ASTSelector} A selector descriptor
   */
  const parseSelector = lodash.memoize((rawSelector) => {
    const parsedSelector = tryParseSelector(rawSelector);
  
    return {
      rawSelector,
      isExit: rawSelector.endsWith(':exit'),
      parsedSelector,
      listenerTypes: getPossibleTypes(parsedSelector),
      attributeCount: countClassAttributes(parsedSelector),
      identifierCount: countIdentifiers(parsedSelector),
    };
  });
  
  // ------------------------------------------------------------------------------
  // Public Interface
  // ------------------------------------------------------------------------------
  
  /**
   * The event generator for AST nodes.
   * This implements below interface.
   *
   * ```ts
   * interface EventGenerator {
   *     emitter: SafeEmitter;
   *     enterNode(node: ASTNode): void;
   *     leaveNode(node: ASTNode): void;
   * }
   * ```
   */
  class NodeEventGenerator {
    /**
     * @param {SafeEmitter} emitter
     * An SafeEmitter which is the destination of events. This emitter must already
     * have registered listeners for all of the events that it needs to listen for.
     * (See lib/linter/safe-emitter.js for more details on `SafeEmitter`.)
     * @returns {NodeEventGenerator} new instance
     */
    constructor(emitter) {
      this.emitter = emitter;
      this.currentAncestry = [];
      this.enterSelectorsByNodeType = new Map();
      this.exitSelectorsByNodeType = new Map();
      this.anyTypeEnterSelectors = [];
      this.anyTypeExitSelectors = [];
  
      for (const rawSelector of emitter.eventNames()) {
        const selector = parseSelector(rawSelector);
  
        if (selector.listenerTypes) {
          const typeMap = selector.isExit
            ? this.exitSelectorsByNodeType
            : this.enterSelectorsByNodeType;
  
          for (const nodeType of selector.listenerTypes) {
            if (!typeMap.has(nodeType)) {
              typeMap.set(nodeType, []);
            }
  
            typeMap.get(nodeType).push(selector);
          }
  
          continue;
        }
  
        const selectors = selector.isExit
          ? this.anyTypeExitSelectors
          : this.anyTypeEnterSelectors;
  
        selectors.push(selector);
      }
  
      this.anyTypeEnterSelectors.sort(compareSpecificity);
      this.anyTypeExitSelectors.sort(compareSpecificity);
      for (const selectorList of this.enterSelectorsByNodeType)
        selectorList.sort(compareSpecificity);
      for (const selectorList of this.exitSelectorsByNodeType)
        selectorList.sort(compareSpecificity);
    }
  
    /**
     * Checks a selector against a node, and emits it if it matches
     * @param {ASTNode} node The node to check
     * @param {ASTSelector} selector An AST selector descriptor
     * @returns {void}
     */
    applySelector(node, selector) {
      if (esquery.matches(node, selector.parsedSelector, this.currentAncestry)) {
        this.emitter.emit(selector.rawSelector, node);
      }
    }
  
    /**
     * Applies all appropriate selectors to a node, in specificity order
     * @param {ASTNode} node The node to check
     * @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
     * @returns {void}
     */
    applySelectors(node, isExit) {
      const selectorsByNodeType =
        (isExit
          ? this.exitSelectorsByNodeType
          : this.enterSelectorsByNodeType
        ).get(node.type) || [];
      const anyTypeSelectors = isExit
        ? this.anyTypeExitSelectors
        : this.anyTypeEnterSelectors;
  
      /*
       * SelectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
       * Iterate through each of them, applying selectors in the right order.
       */
      let selectorsByTypeIndex = 0;
      let anyTypeSelectorsIndex = 0;
  
      while (
        selectorsByTypeIndex < selectorsByNodeType.length ||
        anyTypeSelectorsIndex < anyTypeSelectors.length
      ) {
        if (
          selectorsByTypeIndex >= selectorsByNodeType.length ||
          (anyTypeSelectorsIndex < anyTypeSelectors.length &&
            compareSpecificity(
              anyTypeSelectors[anyTypeSelectorsIndex],
              selectorsByNodeType[selectorsByTypeIndex]
            ) < 0)
        ) {
          this.applySelector(node, anyTypeSelectors[anyTypeSelectorsIndex++]);
        } else {
          this.applySelector(node, selectorsByNodeType[selectorsByTypeIndex++]);
        }
      }
    }
  
    /**
     * Emits an event of entering AST node.
     * @param {ASTNode} node A node which was entered.
     * @returns {void}
     */
    enterNode(node) {
      if (node.parent) {
        this.currentAncestry.unshift(node.parent);
      }
  
      this.applySelectors(node, false);
    }
  
    /**
     * Emits an event of leaving AST node.
     * @param {ASTNode} node A node which was left.
     * @returns {void}
     */
    leaveNode(node) {
      this.applySelectors(node, true);
      this.currentAncestry.shift();
    }
  }
  
  module.exports = NodeEventGenerator;
  