UNPKG

eslint

Version:

An AST-based pattern checker for JavaScript.

379 lines (328 loc) • 11.1 kB
/** * @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; const emptyArray = Object.freeze([]); /** * 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; /** * The property name to use to read a node's type. * @type {string} */ this.nodeTypeKey = esqueryOptions?.nodeTypeKey || "type"; /** * 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) { if (selector.fastMatch) { return selector.fastMatch(node, ancestry, this.esqueryOptions); } return matches(node, selector.root, ancestry, this.esqueryOptions); } /** * Calculates all appropriate selectors for 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 * @param {ESQueryParsedSelector[]} out An array to which matching selectors are appended. * @returns {void} */ calculateSelectors(node, ancestry, isExit, out) { /* * 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[this.nodeTypeKey]) || emptyArray; 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++; /* * Selectors that consist solely of node type identifiers or * wildcards are guaranteed to match, so the `matches()` call * can be skipped for them. */ if ( anyTypeSelector.alwaysMatches || this.matches(node, ancestry, anyTypeSelector) ) { out.push(anyTypeSelector); } } else { selectorsByNodeTypeIndex++; if ( nodeTypeSelector.alwaysMatches || this.matches(node, ancestry, nodeTypeSelector) ) { out.push(nodeTypeSelector); } } } } } //------------------------------------------------------------------------------ // 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 = []; const matchedSelectors = []; const allSteps = steps ?? sourceCode.traverse(); /** * Applies a single traversal step. * @param {Object} step The step to apply. * @returns {void} * @throws {Error} If an error occurs while applying the step. */ function applyStep(step) { switch (step.kind) { case STEP_KIND_VISIT: { try { const isExit = step.phase !== 1; if (isExit) { currentAncestry.shift(); } matchedSelectors.length = 0; esquery.calculateSelectors( step.target, currentAncestry, isExit, matchedSelectors, ); const args = step.args; for (let i = 0; i < matchedSelectors.length; i++) { const source = matchedSelectors[i].source; if (!args) { visitor.callSync(source, step.target); } else if (args.length === 1) { visitor.callSync(source, args[0]); } else { visitor.callSync(source, ...args); } } if (!isExit) { currentAncestry.unshift(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}".`, ); } } /* * Iterating an array by index avoids the overhead of the iterator * protocol. Other iterables are iterated directly instead of being * converted into an array first, because the conversion is slower than * iterating them and would fully materialize a lazy iterable. */ if (Array.isArray(allSteps)) { for (let i = 0; i < allSteps.length; i++) { applyStep(allSteps[i]); } } else { for (const step of allSteps) { applyStep(step); } } } } module.exports = { SourceCodeTraverser };