-
-
Notifications
You must be signed in to change notification settings - Fork 4.9k
refactor: SafeEmitter -> SourceCodeVisitor #19708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
1275b48
0c68faa
4acbab9
586c617
e074717
cce79ba
4306efe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ const vk = require("eslint-visitor-keys"); | |
| /** | ||
| * @import { ESQueryParsedSelector } from "./esquery.js"; | ||
| * @import { Language, SourceCode } from "@eslint/core"; | ||
| * @import { SourceCodeVisitor } from "./source-code-visitor.js"; | ||
| */ | ||
|
|
||
| //----------------------------------------------------------------------------- | ||
|
|
@@ -43,19 +44,17 @@ function compareSpecificity(a, b) { | |
| */ | ||
| class ESQueryHelper { | ||
| /** | ||
| * @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`.) | ||
| * Creates a new instance. | ||
| * @param {SourceCodeVisitor} visitor The visitor containing the functions to call. | ||
| * @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes. | ||
| * @returns {NodeEventGenerator} new instance | ||
| */ | ||
| constructor(emitter, esqueryOptions) { | ||
| constructor(visitor, esqueryOptions) { | ||
| /** | ||
| * The emitter to use during traversal. | ||
| * @type {SafeEmitter} | ||
| * @type {SourceCodeVisitor} | ||
| */ | ||
| this.emitter = emitter; | ||
| this.visitor = visitor; | ||
|
|
||
| /** | ||
| * The options for `esquery` to use during matching. | ||
|
|
@@ -91,7 +90,7 @@ class ESQueryHelper { | |
| */ | ||
| this.anyTypeExitSelectors = []; | ||
|
|
||
| emitter.eventNames().forEach(rawSelector => { | ||
| visitor.forEachName(rawSelector => { | ||
| const selector = parse(rawSelector); | ||
|
|
||
| /* | ||
|
|
@@ -141,12 +140,10 @@ class ESQueryHelper { | |
| * @param {ASTNode} node The node to check | ||
nzakas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * @param {ASTNode[]} ancestry The ancestry of the node being checked. | ||
| * @param {ESQueryParsedSelector} selector An AST selector descriptor | ||
| * @returns {void} | ||
| * @returns {boolean} `true` if the selector matches the node, `false` otherwise | ||
| */ | ||
| #applySelector(node, ancestry, selector) { | ||
| if (matches(node, selector.root, ancestry, this.esqueryOptions)) { | ||
| this.emitter.emit(selector.source, node); | ||
| } | ||
| matches(node, ancestry, selector) { | ||
| return matches(node, selector.root, ancestry, this.esqueryOptions); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -156,8 +153,9 @@ class ESQueryHelper { | |
| * @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited | ||
| * @returns {void} | ||
| */ | ||
| applySelectors(node, ancestry, isExit) { | ||
| calculateSelectors(node, ancestry, isExit) { | ||
nzakas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type"; | ||
| const selectors = []; | ||
|
|
||
| /* | ||
| * Get the selectors that may match this node. First, check | ||
|
|
@@ -189,27 +187,36 @@ class ESQueryHelper { | |
| * or if the next any type selector is more specific than the | ||
| * next selector for this node type, apply the any type selector. | ||
| */ | ||
| if ( | ||
| selectorsByNodeTypeIndex >= selectorsByNodeType.length || | ||
| (anyTypeSelectorsIndex < anyTypeSelectors.length && | ||
| anyTypeSelectors[anyTypeSelectorsIndex].compare( | ||
| selectorsByNodeType[selectorsByNodeTypeIndex], | ||
| ) < 0) | ||
| ) { | ||
| this.#applySelector( | ||
| node, | ||
| ancestry, | ||
| anyTypeSelectors[anyTypeSelectorsIndex++], | ||
| ); | ||
| 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 isAnyTypeSelectorMoreSpecific = | ||
| hasMoreAnyTypeSelectors && | ||
| hasMoreNodeTypeSelectors && | ||
| anyTypeSelector.compare(nodeTypeSelector) < 0; | ||
|
||
|
|
||
| if (!hasMoreNodeTypeSelectors || isAnyTypeSelectorMoreSpecific) { | ||
| anyTypeSelectorsIndex++; | ||
|
|
||
| if (this.matches(node, ancestry, anyTypeSelector)) { | ||
| selectors.push(anyTypeSelector.source); | ||
| } | ||
| } else { | ||
| // otherwise apply the node type selector | ||
| this.#applySelector( | ||
| node, | ||
| ancestry, | ||
| selectorsByNodeType[selectorsByNodeTypeIndex++], | ||
| ); | ||
| selectorsByNodeTypeIndex++; | ||
|
|
||
| if (this.matches(node, ancestry, nodeTypeSelector)) { | ||
| selectors.push(nodeTypeSelector.source); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return selectors; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -253,14 +260,14 @@ class SourceCodeTraverser { | |
| /** | ||
| * Traverses the given source code synchronously. | ||
| * @param {SourceCode} sourceCode The source code to traverse. | ||
| * @param {SafeEmitter} emitter The emitter to use for events. | ||
| * @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, emitter, { steps } = {}) { | ||
| const esquery = new ESQueryHelper(emitter, { | ||
| traverseSync(sourceCode, visitor, { steps } = {}) { | ||
| const esquery = new ESQueryHelper(visitor, { | ||
| visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys, | ||
| fallback: vk.getKeys, | ||
| matchClass: this.#language.matchesSelectorClass ?? (() => false), | ||
|
|
@@ -274,19 +281,27 @@ class SourceCodeTraverser { | |
| case STEP_KIND_VISIT: { | ||
| try { | ||
| if (step.phase === 1) { | ||
| esquery.applySelectors( | ||
| step.target, | ||
| currentAncestry, | ||
| false, | ||
| ); | ||
| esquery | ||
| .calculateSelectors( | ||
| step.target, | ||
| currentAncestry, | ||
| false, | ||
| ) | ||
| .forEach(selector => { | ||
| visitor.callSync(selector, step.target); | ||
| }); | ||
| currentAncestry.unshift(step.target); | ||
| } else { | ||
| currentAncestry.shift(); | ||
| esquery.applySelectors( | ||
| step.target, | ||
| currentAncestry, | ||
| true, | ||
| ); | ||
| esquery | ||
| .calculateSelectors( | ||
| step.target, | ||
| currentAncestry, | ||
| true, | ||
| ) | ||
| .forEach(selector => { | ||
| visitor.callSync(selector, step.target); | ||
| }); | ||
| } | ||
| } catch (err) { | ||
| err.currentNode = step.target; | ||
|
|
@@ -296,7 +311,7 @@ class SourceCodeTraverser { | |
| } | ||
|
|
||
| case STEP_KIND_CALL: { | ||
| emitter.emit(step.target, ...step.args); | ||
| visitor.callSync(step.target, ...step.args); | ||
| break; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| /** | ||
| * @fileoverview SourceCodeVisitor class | ||
| * @author Nicholas C. Zakas | ||
| */ | ||
|
|
||
| "use strict"; | ||
|
|
||
| //----------------------------------------------------------------------------- | ||
| // Helpers | ||
| //----------------------------------------------------------------------------- | ||
|
|
||
| const emptyArray = Object.freeze([]); | ||
|
|
||
| //------------------------------------------------------------------------------ | ||
| // Exports | ||
| //------------------------------------------------------------------------------ | ||
|
|
||
| /** | ||
| * A structure to hold a list of functions to call for a given name. | ||
| * This is used to allow multiple rules to register functions for a given name | ||
| * without having to know about each other. | ||
| */ | ||
| class SourceCodeVisitor { | ||
| /** | ||
| * The functions to call for a given name. | ||
| * @type {Map<string, Function[]>} | ||
| */ | ||
| #functions = new Map(); | ||
|
|
||
| /** | ||
| * Adds a function to the list of functions to call for a given name. | ||
| * @param {string} name The name of the function to call. | ||
| * @param {Function} func The function to call. | ||
| * @returns {void} | ||
| */ | ||
| add(name, func) { | ||
| if (this.#functions.has(name)) { | ||
| this.#functions.get(name).push(func); | ||
| } else { | ||
| this.#functions.set(name, [func]); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets the list of functions to call for a given name. | ||
| * @param {string} name The name of the function to call. | ||
| * @returns {Function[]} The list of functions to call. | ||
| */ | ||
| get(name) { | ||
| if (this.#functions.has(name)) { | ||
| return this.#functions.get(name); | ||
| } | ||
|
|
||
| return emptyArray; | ||
| } | ||
|
|
||
| /** | ||
| * Iterates over all names and calls the callback with the name. | ||
| * @param {(name:string) => void} callback The callback to call for each name. | ||
| * @returns {void} | ||
| */ | ||
| forEachName(callback) { | ||
| this.#functions.forEach((funcs, name) => { | ||
| callback(name); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Calls the functions for a given name with the given arguments. | ||
| * @param {string} name The name of the function to call. | ||
| * @param {any[]} args The arguments to pass to the function. | ||
| * @returns {void} | ||
| */ | ||
| callSync(name, ...args) { | ||
| if (this.#functions.has(name)) { | ||
| this.#functions.get(name).forEach(func => func(...args)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| module.exports = { SourceCodeVisitor }; |
Uh oh!
There was an error while loading. Please reload this page.