Skip to content

Commit f4fa40e

Browse files
nzakasmdjermanovic
andauthored
refactor: NodeEventGenerator -> SourceCodeTraverser (#19679)
* refactor: NodeEventGenerator -> SourceCodeTraverser refs #18787 * Clean up * Remove NodeEventGenerator * Fix CodePathAnalyzer tests * move currentancestry property to variable * Update lib/linter/source-code-traverser.js Co-authored-by: Milos Djermanovic <[email protected]> * Update tests/lib/linter/source-code-traverser.js Co-authored-by: Milos Djermanovic <[email protected]> * Don't call SourceCode#traverse() twice * Add tests for nodeTypeKey * Add missing tests * Ensure that AST traversal happens before rule instantiation * Remove extra file --------- Co-authored-by: Milos Djermanovic <[email protected]>
1 parent 81c3c93 commit f4fa40e

File tree

4 files changed

+458
-227
lines changed

4 files changed

+458
-227
lines changed

lib/linter/linter.js

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ const path = require("node:path"),
2727
{ SourceCode } = require("../languages/js/source-code"),
2828
applyDisableDirectives = require("./apply-disable-directives"),
2929
{ ConfigCommentParser } = require("@eslint/plugin-kit"),
30-
NodeEventGenerator = require("./node-event-generator"),
3130
createReportTranslator = require("./report-translator"),
3231
Rules = require("./rules"),
3332
createEmitter = require("./safe-emitter"),
@@ -66,8 +65,7 @@ const { ProcessorService } = require("../services/processor-service");
6665
const { containsDifferentProperty } = require("../shared/option-utils");
6766
const { Config } = require("../config/config");
6867
const { WarningService } = require("../services/warning-service");
69-
const STEP_KIND_VISIT = 1;
70-
const STEP_KIND_CALL = 2;
68+
const { SourceCodeTraverser } = require("./source-code-traverser");
7169

7270
//------------------------------------------------------------------------------
7371
// Typedefs
@@ -1179,9 +1177,6 @@ function runRules(
11791177
) {
11801178
const emitter = createEmitter();
11811179

1182-
// must happen first to assign all node.parent properties
1183-
const eventQueue = sourceCode.traverse();
1184-
11851180
/*
11861181
* Create a frozen object with the ruleContext properties and methods that are shared by all rules.
11871182
* All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
@@ -1201,6 +1196,7 @@ function runRules(
12011196
});
12021197

12031198
const lintingProblems = [];
1199+
const steps = sourceCode.traverse();
12041200

12051201
Object.keys(configuredRules).forEach(ruleId => {
12061202
const severity = Config.getRuleNumericSeverity(configuredRules[ruleId]);
@@ -1347,40 +1343,9 @@ function runRules(
13471343
});
13481344
});
13491345

1350-
const eventGenerator = new NodeEventGenerator(emitter, {
1351-
visitorKeys: sourceCode.visitorKeys ?? language.visitorKeys,
1352-
fallback: Traverser.getKeys,
1353-
matchClass: language.matchesSelectorClass ?? (() => false),
1354-
nodeTypeKey: language.nodeTypeKey,
1355-
});
1356-
1357-
for (const step of eventQueue) {
1358-
switch (step.kind) {
1359-
case STEP_KIND_VISIT: {
1360-
try {
1361-
if (step.phase === 1) {
1362-
eventGenerator.enterNode(step.target);
1363-
} else {
1364-
eventGenerator.leaveNode(step.target);
1365-
}
1366-
} catch (err) {
1367-
err.currentNode = step.target;
1368-
throw err;
1369-
}
1370-
break;
1371-
}
1372-
1373-
case STEP_KIND_CALL: {
1374-
emitter.emit(step.target, ...step.args);
1375-
break;
1376-
}
1346+
const traverser = SourceCodeTraverser.getInstance(language);
13771347

1378-
default:
1379-
throw new Error(
1380-
`Invalid traversal step found: "${step.type}".`,
1381-
);
1382-
}
1383-
}
1348+
traverser.traverseSync(sourceCode, emitter, { steps });
13841349

13851350
return lintingProblems;
13861351
}
Lines changed: 143 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
2-
* @fileoverview The event generator for AST nodes.
3-
* @author Toru Nagashima
2+
* @fileoverview Traverser for SourceCode objects.
3+
* @author Nicholas C. Zakas
44
*/
55

66
"use strict";
@@ -10,19 +10,24 @@
1010
//------------------------------------------------------------------------------
1111

1212
const { parse, matches } = require("./esquery");
13+
const vk = require("eslint-visitor-keys");
1314

1415
//-----------------------------------------------------------------------------
1516
// Typedefs
1617
//-----------------------------------------------------------------------------
1718

1819
/**
1920
* @import { ESQueryParsedSelector } from "./esquery.js";
21+
* @import { Language, SourceCode } from "@eslint/core";
2022
*/
2123

2224
//-----------------------------------------------------------------------------
2325
// Helpers
2426
//-----------------------------------------------------------------------------
2527

28+
const STEP_KIND_VISIT = 1;
29+
const STEP_KIND_CALL = 2;
30+
2631
/**
2732
* Compares two ESQuery selectors by specificity.
2833
* @param {ESQueryParsedSelector} a The first selector to compare.
@@ -33,69 +38,10 @@ function compareSpecificity(a, b) {
3338
return a.compare(b);
3439
}
3540

36-
//------------------------------------------------------------------------------
37-
// Public Interface
38-
//------------------------------------------------------------------------------
39-
4041
/**
41-
* The event generator for AST nodes.
42-
* This implements below interface.
43-
*
44-
* ```ts
45-
* interface EventGenerator {
46-
* emitter: SafeEmitter;
47-
* enterNode(node: ASTNode): void;
48-
* leaveNode(node: ASTNode): void;
49-
* }
50-
* ```
42+
* Helper to wrap ESQuery operations.
5143
*/
52-
class NodeEventGenerator {
53-
/**
54-
* The emitter to use during traversal.
55-
* @type {SafeEmitter}
56-
*/
57-
emitter;
58-
59-
/**
60-
* The options for `esquery` to use during matching.
61-
* @type {ESQueryOptions}
62-
*/
63-
esqueryOptions;
64-
65-
/**
66-
* The ancestry of the currently visited node.
67-
* @type {ASTNode[]}
68-
*/
69-
currentAncestry = [];
70-
71-
/**
72-
* A map of node type to selectors targeting that node type on the
73-
* enter phase of traversal.
74-
* @type {Map<string, ESQueryParsedSelector[]>}
75-
*/
76-
enterSelectorsByNodeType = new Map();
77-
78-
/**
79-
* A map of node type to selectors targeting that node type on the
80-
* exit phase of traversal.
81-
* @type {Map<string, ESQueryParsedSelector[]>}
82-
*/
83-
exitSelectorsByNodeType = new Map();
84-
85-
/**
86-
* An array of selectors that match any node type on the
87-
* enter phase of traversal.
88-
* @type {ESQueryParsedSelector[]}
89-
*/
90-
anyTypeEnterSelectors = [];
91-
92-
/**
93-
* An array of selectors that match any node type on the
94-
* exit phase of traversal.
95-
* @type {ESQueryParsedSelector[]}
96-
*/
97-
anyTypeExitSelectors = [];
98-
44+
class ESQueryHelper {
9945
/**
10046
* @param {SafeEmitter} emitter
10147
* An SafeEmitter which is the destination of events. This emitter must already
@@ -105,9 +51,46 @@ class NodeEventGenerator {
10551
* @returns {NodeEventGenerator} new instance
10652
*/
10753
constructor(emitter, esqueryOptions) {
54+
/**
55+
* The emitter to use during traversal.
56+
* @type {SafeEmitter}
57+
*/
10858
this.emitter = emitter;
59+
60+
/**
61+
* The options for `esquery` to use during matching.
62+
* @type {ESQueryOptions}
63+
*/
10964
this.esqueryOptions = esqueryOptions;
11065

66+
/**
67+
* A map of node type to selectors targeting that node type on the
68+
* enter phase of traversal.
69+
* @type {Map<string, ESQueryParsedSelector[]>}
70+
*/
71+
this.enterSelectorsByNodeType = new Map();
72+
73+
/**
74+
* A map of node type to selectors targeting that node type on the
75+
* exit phase of traversal.
76+
* @type {Map<string, ESQueryParsedSelector[]>}
77+
*/
78+
this.exitSelectorsByNodeType = new Map();
79+
80+
/**
81+
* An array of selectors that match any node type on the
82+
* enter phase of traversal.
83+
* @type {ESQueryParsedSelector[]}
84+
*/
85+
this.anyTypeEnterSelectors = [];
86+
87+
/**
88+
* An array of selectors that match any node type on the
89+
* exit phase of traversal.
90+
* @type {ESQueryParsedSelector[]}
91+
*/
92+
this.anyTypeExitSelectors = [];
93+
11194
emitter.eventNames().forEach(rawSelector => {
11295
const selector = parse(rawSelector);
11396

@@ -156,29 +139,24 @@ class NodeEventGenerator {
156139
/**
157140
* Checks a selector against a node, and emits it if it matches
158141
* @param {ASTNode} node The node to check
142+
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
159143
* @param {ESQueryParsedSelector} selector An AST selector descriptor
160144
* @returns {void}
161145
*/
162-
applySelector(node, selector) {
163-
if (
164-
matches(
165-
node,
166-
selector.root,
167-
this.currentAncestry,
168-
this.esqueryOptions,
169-
)
170-
) {
146+
#applySelector(node, ancestry, selector) {
147+
if (matches(node, selector.root, ancestry, this.esqueryOptions)) {
171148
this.emitter.emit(selector.source, node);
172149
}
173150
}
174151

175152
/**
176153
* Applies all appropriate selectors to a node, in specificity order
177154
* @param {ASTNode} node The node to check
155+
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
178156
* @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
179157
* @returns {void}
180158
*/
181-
applySelectors(node, isExit) {
159+
applySelectors(node, ancestry, isExit) {
182160
const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
183161

184162
/*
@@ -218,39 +196,117 @@ class NodeEventGenerator {
218196
selectorsByNodeType[selectorsByNodeTypeIndex],
219197
) < 0)
220198
) {
221-
this.applySelector(
199+
this.#applySelector(
222200
node,
201+
ancestry,
223202
anyTypeSelectors[anyTypeSelectorsIndex++],
224203
);
225204
} else {
226205
// otherwise apply the node type selector
227-
this.applySelector(
206+
this.#applySelector(
228207
node,
208+
ancestry,
229209
selectorsByNodeType[selectorsByNodeTypeIndex++],
230210
);
231211
}
232212
}
233213
}
214+
}
215+
216+
//------------------------------------------------------------------------------
217+
// Public Interface
218+
//------------------------------------------------------------------------------
234219

220+
/**
221+
* Traverses source code and ensures that visitor methods are called when
222+
* entering and leaving each node.
223+
*/
224+
class SourceCodeTraverser {
235225
/**
236-
* Emits an event of entering AST node.
237-
* @param {ASTNode} node A node which was entered.
238-
* @returns {void}
226+
* The language of the source code being traversed.
227+
* @type {Language}
228+
*/
229+
#language;
230+
231+
/**
232+
* Map of languages to instances of this class.
233+
* @type {WeakMap<Language, SourceCodeTraverser>}
234+
*/
235+
static instances = new WeakMap();
236+
237+
/**
238+
* Creates a new instance.
239+
* @param {Language} language The language of the source code being traversed.
239240
*/
240-
enterNode(node) {
241-
this.applySelectors(node, false);
242-
this.currentAncestry.unshift(node);
241+
constructor(language) {
242+
this.#language = language;
243+
}
244+
245+
static getInstance(language) {
246+
if (!this.instances.has(language)) {
247+
this.instances.set(language, new this(language));
248+
}
249+
250+
return this.instances.get(language);
243251
}
244252

245253
/**
246-
* Emits an event of leaving AST node.
247-
* @param {ASTNode} node A node which was left.
254+
* Traverses the given source code synchronously.
255+
* @param {SourceCode} sourceCode The source code to traverse.
256+
* @param {SafeEmitter} emitter The emitter to use for events.
257+
* @param {Object} options Options for traversal.
258+
* @param {ReturnType<SourceCode["traverse"]>} options.steps The steps to take during traversal.
248259
* @returns {void}
260+
* @throws {Error} If an error occurs during traversal.
249261
*/
250-
leaveNode(node) {
251-
this.currentAncestry.shift();
252-
this.applySelectors(node, true);
262+
traverseSync(sourceCode, emitter, { steps } = {}) {
263+
const esquery = new ESQueryHelper(emitter, {
264+
visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys,
265+
fallback: vk.getKeys,
266+
matchClass: this.#language.matchesSelectorClass ?? (() => false),
267+
nodeTypeKey: this.#language.nodeTypeKey,
268+
});
269+
270+
const currentAncestry = [];
271+
272+
for (const step of steps ?? sourceCode.traverse()) {
273+
switch (step.kind) {
274+
case STEP_KIND_VISIT: {
275+
try {
276+
if (step.phase === 1) {
277+
esquery.applySelectors(
278+
step.target,
279+
currentAncestry,
280+
false,
281+
);
282+
currentAncestry.unshift(step.target);
283+
} else {
284+
currentAncestry.shift();
285+
esquery.applySelectors(
286+
step.target,
287+
currentAncestry,
288+
true,
289+
);
290+
}
291+
} catch (err) {
292+
err.currentNode = step.target;
293+
throw err;
294+
}
295+
break;
296+
}
297+
298+
case STEP_KIND_CALL: {
299+
emitter.emit(step.target, ...step.args);
300+
break;
301+
}
302+
303+
default:
304+
throw new Error(
305+
`Invalid traversal step found: "${step.kind}".`,
306+
);
307+
}
308+
}
253309
}
254310
}
255311

256-
module.exports = NodeEventGenerator;
312+
module.exports = { SourceCodeTraverser };

0 commit comments

Comments
 (0)