| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- /**
- * @fileoverview Traverser for SourceCode objects.
- * @author Nicholas C. Zakas
- */
- "use strict";
- //------------------------------------------------------------------------------
- // Requirements
- //------------------------------------------------------------------------------
- const { parse, matches } = require("./esquery");
- const vk = require("eslint-visitor-keys");
- //-----------------------------------------------------------------------------
- // Typedefs
- //-----------------------------------------------------------------------------
- /**
- * @import { Language, SourceCode } from "@eslint/core";
- * @import { ESQueryOptions } from "esquery";
- * @import { ESQueryParsedSelector } from "./esquery.js";
- * @import { SourceCodeVisitor } from "./source-code-visitor.js";
- */
- //-----------------------------------------------------------------------------
- // Helpers
- //-----------------------------------------------------------------------------
- const STEP_KIND_VISIT = 1;
- const STEP_KIND_CALL = 2;
- /**
- * Compares two ESQuery selectors by specificity.
- * @param {ESQueryParsedSelector} a The first selector to compare.
- * @param {ESQueryParsedSelector} b The second selector to compare.
- * @returns {number} A negative number if `a` is less specific than `b` or they are equally specific and `a` <= `b` alphabetically, a positive number if `a` is more specific than `b`.
- */
- function compareSpecificity(a, b) {
- return a.compare(b);
- }
- /**
- * Helper to wrap ESQuery operations.
- */
- class ESQueryHelper {
- /**
- * Creates a new instance.
- * @param {SourceCodeVisitor} visitor The visitor containing the functions to call.
- * @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
- */
- constructor(visitor, esqueryOptions) {
- /**
- * The visitor to use during traversal.
- * @type {SourceCodeVisitor}
- */
- this.visitor = visitor;
- /**
- * The options for `esquery` to use during matching.
- * @type {ESQueryOptions}
- */
- this.esqueryOptions = esqueryOptions;
- /**
- * A map of node type to selectors targeting that node type on the
- * enter phase of traversal.
- * @type {Map<string, ESQueryParsedSelector[]>}
- */
- this.enterSelectorsByNodeType = new Map();
- /**
- * A map of node type to selectors targeting that node type on the
- * exit phase of traversal.
- * @type {Map<string, ESQueryParsedSelector[]>}
- */
- this.exitSelectorsByNodeType = new Map();
- /**
- * An array of selectors that match any node type on the
- * enter phase of traversal.
- * @type {ESQueryParsedSelector[]}
- */
- this.anyTypeEnterSelectors = [];
- /**
- * An array of selectors that match any node type on the
- * exit phase of traversal.
- * @type {ESQueryParsedSelector[]}
- */
- this.anyTypeExitSelectors = [];
- visitor.forEachName(rawSelector => {
- const selector = parse(rawSelector);
- /*
- * If this selector has identified specific node types,
- * add it to the map for these node types for faster lookup.
- */
- if (selector.nodeTypes) {
- const typeMap = selector.isExit
- ? this.exitSelectorsByNodeType
- : this.enterSelectorsByNodeType;
- selector.nodeTypes.forEach(nodeType => {
- if (!typeMap.has(nodeType)) {
- typeMap.set(nodeType, []);
- }
- typeMap.get(nodeType).push(selector);
- });
- return;
- }
- /*
- * Remaining selectors are added to the "any type" selectors
- * list for the appropriate phase of traversal. This ensures
- * that all selectors will still be applied even if no
- * specific node type is matched.
- */
- const selectors = selector.isExit
- ? this.anyTypeExitSelectors
- : this.anyTypeEnterSelectors;
- selectors.push(selector);
- });
- // sort all selectors by specificity for prioritizing call order
- this.anyTypeEnterSelectors.sort(compareSpecificity);
- this.anyTypeExitSelectors.sort(compareSpecificity);
- this.enterSelectorsByNodeType.forEach(selectorList =>
- selectorList.sort(compareSpecificity),
- );
- this.exitSelectorsByNodeType.forEach(selectorList =>
- selectorList.sort(compareSpecificity),
- );
- }
- /**
- * Checks if a node matches a given selector.
- * @param {ASTNode} node The node to check
- * @param {ASTNode[]} ancestry The ancestry of the node being checked.
- * @param {ESQueryParsedSelector} selector An AST selector descriptor
- * @returns {boolean} `true` if the selector matches the node, `false` otherwise
- */
- matches(node, ancestry, selector) {
- return matches(node, selector.root, ancestry, this.esqueryOptions);
- }
- /**
- * Calculates all appropriate selectors to a node, in specificity order
- * @param {ASTNode} node The node to check
- * @param {ASTNode[]} ancestry The ancestry of the node being checked.
- * @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
- * @returns {string[]} An array of selectors that match the node.
- */
- calculateSelectors(node, ancestry, isExit) {
- const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
- const selectors = [];
- /*
- * Get the selectors that may match this node. First, check
- * to see if the node type has specific selectors,
- * then gather the "any type" selectors.
- */
- const selectorsByNodeType =
- (isExit
- ? this.exitSelectorsByNodeType
- : this.enterSelectorsByNodeType
- ).get(node[nodeTypeKey]) || [];
- 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 selectorsByNodeTypeIndex = 0;
- let anyTypeSelectorsIndex = 0;
- while (
- selectorsByNodeTypeIndex < selectorsByNodeType.length ||
- anyTypeSelectorsIndex < anyTypeSelectors.length
- ) {
- /*
- * If we've already exhausted the selectors for this node type,
- * or if the next any type selector is more specific than the
- * next selector for this node type, apply the any type selector.
- */
- const hasMoreNodeTypeSelectors =
- selectorsByNodeTypeIndex < selectorsByNodeType.length;
- const hasMoreAnyTypeSelectors =
- anyTypeSelectorsIndex < anyTypeSelectors.length;
- const anyTypeSelector = anyTypeSelectors[anyTypeSelectorsIndex];
- const nodeTypeSelector =
- selectorsByNodeType[selectorsByNodeTypeIndex];
- // Only compare specificity if both selectors exist
- const isAnyTypeSelectorLessSpecific =
- hasMoreAnyTypeSelectors &&
- hasMoreNodeTypeSelectors &&
- anyTypeSelector.compare(nodeTypeSelector) < 0;
- if (!hasMoreNodeTypeSelectors || isAnyTypeSelectorLessSpecific) {
- anyTypeSelectorsIndex++;
- if (this.matches(node, ancestry, anyTypeSelector)) {
- selectors.push(anyTypeSelector.source);
- }
- } else {
- selectorsByNodeTypeIndex++;
- if (this.matches(node, ancestry, nodeTypeSelector)) {
- selectors.push(nodeTypeSelector.source);
- }
- }
- }
- return selectors;
- }
- }
- //------------------------------------------------------------------------------
- // Public Interface
- //------------------------------------------------------------------------------
- /**
- * Traverses source code and ensures that visitor methods are called when
- * entering and leaving each node.
- */
- class SourceCodeTraverser {
- /**
- * The language of the source code being traversed.
- * @type {Language}
- */
- #language;
- /**
- * Map of languages to instances of this class.
- * @type {WeakMap<Language, SourceCodeTraverser>}
- */
- static instances = new WeakMap();
- /**
- * Creates a new instance.
- * @param {Language} language The language of the source code being traversed.
- */
- constructor(language) {
- this.#language = language;
- }
- static getInstance(language) {
- if (!this.instances.has(language)) {
- this.instances.set(language, new this(language));
- }
- return this.instances.get(language);
- }
- /**
- * Traverses the given source code synchronously.
- * @param {SourceCode} sourceCode The source code to traverse.
- * @param {SourceCodeVisitor} visitor The emitter to use for events.
- * @param {Object} options Options for traversal.
- * @param {ReturnType<SourceCode["traverse"]>} options.steps The steps to take during traversal.
- * @returns {void}
- * @throws {Error} If an error occurs during traversal.
- */
- traverseSync(sourceCode, visitor, { steps } = {}) {
- const esquery = new ESQueryHelper(visitor, {
- visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys,
- fallback: vk.getKeys,
- matchClass: this.#language.matchesSelectorClass ?? (() => false),
- nodeTypeKey: this.#language.nodeTypeKey,
- });
- const currentAncestry = [];
- for (const step of steps ?? sourceCode.traverse()) {
- switch (step.kind) {
- case STEP_KIND_VISIT: {
- try {
- if (step.phase === 1) {
- esquery
- .calculateSelectors(
- step.target,
- currentAncestry,
- false,
- )
- .forEach(selector => {
- visitor.callSync(
- selector,
- ...(step.args ?? [step.target]),
- );
- });
- currentAncestry.unshift(step.target);
- } else {
- currentAncestry.shift();
- esquery
- .calculateSelectors(
- step.target,
- currentAncestry,
- true,
- )
- .forEach(selector => {
- visitor.callSync(
- selector,
- ...(step.args ?? [step.target]),
- );
- });
- }
- } catch (err) {
- err.currentNode = step.target;
- throw err;
- }
- break;
- }
- case STEP_KIND_CALL: {
- visitor.callSync(step.target, ...step.args);
- break;
- }
- default:
- throw new Error(
- `Invalid traversal step found: "${step.kind}".`,
- );
- }
- }
- }
- }
- module.exports = { SourceCodeTraverser };
|