]> git.proxmox.com Git - pve-eslint.git/blob - eslint/tests/lib/linter/node-event-generator.js
import 8.3.0 source
[pve-eslint.git] / eslint / tests / lib / linter / node-event-generator.js
1 /**
2 * @fileoverview Tests for NodeEventGenerator.
3 * @author Toru Nagashima
4 */
5 "use strict";
6
7 //------------------------------------------------------------------------------
8 // Requirements
9 //------------------------------------------------------------------------------
10
11 const assert = require("assert"),
12 sinon = require("sinon"),
13 espree = require("espree"),
14 vk = require("eslint-visitor-keys"),
15 Traverser = require("../../../lib/shared/traverser"),
16 EventGeneratorTester = require("../../../tools/internal-testers/event-generator-tester"),
17 createEmitter = require("../../../lib/linter/safe-emitter"),
18 NodeEventGenerator = require("../../../lib/linter/node-event-generator");
19
20
21 //------------------------------------------------------------------------------
22 // Constants
23 //------------------------------------------------------------------------------
24
25 const ESPREE_CONFIG = {
26 ecmaVersion: 6,
27 comment: true,
28 tokens: true,
29 range: true,
30 loc: true
31 };
32
33 const STANDARD_ESQUERY_OPTION = { visitorKeys: vk.KEYS, fallback: Traverser.getKeys };
34
35 //------------------------------------------------------------------------------
36 // Tests
37 //------------------------------------------------------------------------------
38
39 describe("NodeEventGenerator", () => {
40 EventGeneratorTester.testEventGeneratorInterface(
41 new NodeEventGenerator(createEmitter(), STANDARD_ESQUERY_OPTION)
42 );
43
44 describe("entering a single AST node", () => {
45 let emitter, generator;
46
47 beforeEach(() => {
48 emitter = Object.create(createEmitter(), { emit: { value: sinon.spy() } });
49
50 ["Foo", "Bar", "Foo > Bar", "Foo:exit"].forEach(selector => emitter.on(selector, () => {}));
51 generator = new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION);
52 });
53
54 it("should generate events for entering AST node.", () => {
55 const dummyNode = { type: "Foo", value: 1 };
56
57 generator.enterNode(dummyNode);
58
59 assert(emitter.emit.calledOnce);
60 assert(emitter.emit.calledWith("Foo", dummyNode));
61 });
62
63 it("should generate events for exitting AST node.", () => {
64 const dummyNode = { type: "Foo", value: 1 };
65
66 generator.leaveNode(dummyNode);
67
68 assert(emitter.emit.calledOnce);
69 assert(emitter.emit.calledWith("Foo:exit", dummyNode));
70 });
71
72 it("should generate events for AST queries", () => {
73 const dummyNode = { type: "Bar", parent: { type: "Foo" } };
74
75 generator.enterNode(dummyNode);
76
77 assert(emitter.emit.calledTwice);
78 assert(emitter.emit.calledWith("Foo > Bar", dummyNode));
79 });
80 });
81
82 describe("traversing the entire AST", () => {
83
84 /**
85 * Gets a list of emitted types/selectors from the generator, in emission order
86 * @param {ASTNode} ast The AST to traverse
87 * @param {Array<string>|Set<string>} possibleQueries Selectors to detect
88 * @returns {Array[]} A list of emissions, in the order that they were emitted. Each emission is a two-element
89 * array where the first element is a string, and the second element is the emitted AST node.
90 */
91 function getEmissions(ast, possibleQueries) {
92 const emissions = [];
93 const emitter = Object.create(createEmitter(), {
94 emit: {
95 value: (selector, node) => emissions.push([selector, node])
96 }
97 });
98
99 possibleQueries.forEach(query => emitter.on(query, () => {}));
100 const generator = new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION);
101
102 Traverser.traverse(ast, {
103 enter(node, parent) {
104 node.parent = parent;
105 generator.enterNode(node);
106 },
107 leave(node) {
108 generator.leaveNode(node);
109 }
110 });
111
112 return emissions;
113 }
114
115 /**
116 * Creates a test case that asserts a particular sequence of generator emissions
117 * @param {string} sourceText The source text that should be parsed and traversed
118 * @param {string[]} possibleQueries A collection of selectors that rules are listening for
119 * @param {Array[]} expectedEmissions A function that accepts the AST and returns a list of the emissions that the
120 * generator is expected to produce, in order.
121 * Each element of this list is an array where the first element is a selector (string), and the second is an AST node
122 * This should only include emissions that appear in possibleQueries.
123 * @returns {void}
124 */
125 function assertEmissions(sourceText, possibleQueries, expectedEmissions) {
126 it(possibleQueries.join("; "), () => {
127 const ast = espree.parse(sourceText, ESPREE_CONFIG);
128 const emissions = getEmissions(ast, possibleQueries)
129 .filter(emission => possibleQueries.indexOf(emission[0]) !== -1);
130
131 assert.deepStrictEqual(emissions, expectedEmissions(ast));
132 });
133 }
134
135 assertEmissions(
136 "foo + bar;",
137 ["Program", "Program:exit", "ExpressionStatement", "ExpressionStatement:exit", "BinaryExpression", "BinaryExpression:exit", "Identifier", "Identifier:exit"],
138 ast => [
139 ["Program", ast], // entering program
140 ["ExpressionStatement", ast.body[0]], // entering 'foo + bar;'
141 ["BinaryExpression", ast.body[0].expression], // entering 'foo + bar'
142 ["Identifier", ast.body[0].expression.left], // entering 'foo'
143 ["Identifier:exit", ast.body[0].expression.left], // exiting 'foo'
144 ["Identifier", ast.body[0].expression.right], // entering 'bar'
145 ["Identifier:exit", ast.body[0].expression.right], // exiting 'bar'
146 ["BinaryExpression:exit", ast.body[0].expression], // exiting 'foo + bar'
147 ["ExpressionStatement:exit", ast.body[0]], // exiting 'foo + bar;'
148 ["Program:exit", ast] // exiting program
149 ]
150 );
151
152 assertEmissions(
153 "foo + 5",
154 [
155 "BinaryExpression > Identifier",
156 "BinaryExpression",
157 "BinaryExpression Literal:exit",
158 "BinaryExpression > Identifier:exit",
159 "BinaryExpression:exit"
160 ],
161 ast => [
162 ["BinaryExpression", ast.body[0].expression], // foo + 5
163 ["BinaryExpression > Identifier", ast.body[0].expression.left], // foo
164 ["BinaryExpression > Identifier:exit", ast.body[0].expression.left], // exiting foo
165 ["BinaryExpression Literal:exit", ast.body[0].expression.right], // exiting 5
166 ["BinaryExpression:exit", ast.body[0].expression] // exiting foo + 5
167 ]
168 );
169
170 assertEmissions(
171 "foo + 5",
172 ["BinaryExpression > *[name='foo']"],
173 ast => [["BinaryExpression > *[name='foo']", ast.body[0].expression.left]] // entering foo
174 );
175
176 assertEmissions(
177 "foo",
178 ["*"],
179 ast => [
180 ["*", ast], // Program
181 ["*", ast.body[0]], // ExpressionStatement
182 ["*", ast.body[0].expression] // Identifier
183 ]
184 );
185
186 assertEmissions(
187 "foo",
188 ["*:not(ExpressionStatement)"],
189 ast => [
190 ["*:not(ExpressionStatement)", ast], // Program
191 ["*:not(ExpressionStatement)", ast.body[0].expression] // Identifier
192 ]
193 );
194
195 assertEmissions(
196 "foo()",
197 ["CallExpression[callee.name='foo']"],
198 ast => [["CallExpression[callee.name='foo']", ast.body[0].expression]] // foo()
199 );
200
201 assertEmissions(
202 "foo()",
203 ["CallExpression[callee.name='bar']"],
204 () => [] // (nothing emitted)
205 );
206
207 assertEmissions(
208 "foo + bar + baz",
209 [":not(*)"],
210 () => [] // (nothing emitted)
211 );
212
213 assertEmissions(
214 "foo + bar + baz",
215 [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])"],
216 ast => [
217 [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.left.left], // foo
218 [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.left.right], // bar
219 [":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.right] // baz
220 ]
221 );
222
223 assertEmissions(
224 "foo + 5 + 6",
225 ["Identifier, Literal[value=5]"],
226 ast => [
227 ["Identifier, Literal[value=5]", ast.body[0].expression.left.left], // foo
228 ["Identifier, Literal[value=5]", ast.body[0].expression.left.right] // 5
229 ]
230 );
231
232 assertEmissions(
233 "[foo, 5, foo]",
234 ["Identifier + Literal"],
235 ast => [["Identifier + Literal", ast.body[0].expression.elements[1]]] // 5
236 );
237
238 assertEmissions(
239 "[foo, {}, 5]",
240 ["Identifier + Literal", "Identifier ~ Literal"],
241 ast => [["Identifier ~ Literal", ast.body[0].expression.elements[2]]] // 5
242 );
243
244 assertEmissions(
245 "foo; bar + baz; qux()",
246 [":expression", ":statement"],
247 ast => [
248 [":statement", ast.body[0]],
249 [":expression", ast.body[0].expression],
250 [":statement", ast.body[1]],
251 [":expression", ast.body[1].expression],
252 [":expression", ast.body[1].expression.left],
253 [":expression", ast.body[1].expression.right],
254 [":statement", ast.body[2]],
255 [":expression", ast.body[2].expression],
256 [":expression", ast.body[2].expression.callee]
257 ]
258 );
259
260 assertEmissions(
261 "function foo(){} var x; (function (p){}); () => {};",
262 [":function", "ExpressionStatement > :function", "VariableDeclaration, :function[params.length=1]"],
263 ast => [
264 [":function", ast.body[0]], // function foo(){}
265 ["VariableDeclaration, :function[params.length=1]", ast.body[1]], // var x;
266 [":function", ast.body[2].expression], // function (p){}
267 ["ExpressionStatement > :function", ast.body[2].expression], // function (p){}
268 ["VariableDeclaration, :function[params.length=1]", ast.body[2].expression], // function (p){}
269 [":function", ast.body[3].expression], // () => {}
270 ["ExpressionStatement > :function", ast.body[3].expression] // () => {}
271 ]
272 );
273
274 assertEmissions(
275 "foo;",
276 [
277 "*",
278 ":not(*)",
279 "Identifier",
280 "ExpressionStatement > *",
281 "ExpressionStatement > Identifier",
282 "ExpressionStatement > [name='foo']",
283 "Identifier, ReturnStatement",
284 "FooStatement",
285 "[name = 'foo']",
286 "[name='foo']",
287 "[name ='foo']",
288 "Identifier[name='foo']",
289 "[name='foo'][name.length=3]",
290 ":not(Program, ExpressionStatement)",
291 ":not(Program, Identifier) > [name.length=3]"
292 ],
293 ast => [
294 ["*", ast], // Program
295 ["*", ast.body[0]], // ExpressionStatement
296
297 // selectors for the 'foo' identifier, in order of increasing specificity
298 ["*", ast.body[0].expression], // 0 identifiers, 0 pseudoclasses
299 ["ExpressionStatement > *", ast.body[0].expression], // 0 pseudoclasses, 1 identifier
300 ["Identifier", ast.body[0].expression], // 0 pseudoclasses, 1 identifier
301 [":not(Program, ExpressionStatement)", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
302 ["ExpressionStatement > Identifier", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
303 ["Identifier, ReturnStatement", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
304 ["[name = 'foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
305 ["[name ='foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
306 ["[name='foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
307 ["ExpressionStatement > [name='foo']", ast.body[0].expression], // 1 attribute, 1 identifier
308 ["Identifier[name='foo']", ast.body[0].expression], // 1 attribute, 1 identifier
309 [":not(Program, Identifier) > [name.length=3]", ast.body[0].expression], // 1 attribute, 2 identifiers
310 ["[name='foo'][name.length=3]", ast.body[0].expression] // 2 attributes, 0 identifiers
311 ]
312 );
313
314 assertEmissions(
315 "foo(); bar; baz;",
316 ["CallExpression, [name='bar']"],
317 ast => [
318 ["CallExpression, [name='bar']", ast.body[0].expression],
319 ["CallExpression, [name='bar']", ast.body[1].expression]
320 ]
321 );
322
323 assertEmissions(
324 "foo; bar;",
325 ["[name.length=3]:exit"],
326 ast => [
327 ["[name.length=3]:exit", ast.body[0].expression],
328 ["[name.length=3]:exit", ast.body[1].expression]
329 ]
330 );
331
332 // https://github.com/eslint/eslint/issues/14799
333 assertEmissions(
334 "const {a = 1} = b;",
335 ["Property > .key"],
336 ast => [
337 ["Property > .key", ast.body[0].declarations[0].id.properties[0].key]
338 ]
339 );
340 });
341
342 describe("traversing the entire non-standard AST", () => {
343
344 /**
345 * Gets a list of emitted types/selectors from the generator, in emission order
346 * @param {ASTNode} ast The AST to traverse
347 * @param {Record<string, string[]>} visitorKeys The custom visitor keys.
348 * @param {Array<string>|Set<string>} possibleQueries Selectors to detect
349 * @returns {Array[]} A list of emissions, in the order that they were emitted. Each emission is a two-element
350 * array where the first element is a string, and the second element is the emitted AST node.
351 */
352 function getEmissions(ast, visitorKeys, possibleQueries) {
353 const emissions = [];
354 const emitter = Object.create(createEmitter(), {
355 emit: {
356 value: (selector, node) => emissions.push([selector, node])
357 }
358 });
359
360 possibleQueries.forEach(query => emitter.on(query, () => {}));
361 const generator = new NodeEventGenerator(emitter, { visitorKeys, fallback: Traverser.getKeys });
362
363 Traverser.traverse(ast, {
364 visitorKeys,
365 enter(node, parent) {
366 node.parent = parent;
367 generator.enterNode(node);
368 },
369 leave(node) {
370 generator.leaveNode(node);
371 }
372 });
373
374 return emissions;
375 }
376
377 /**
378 * Creates a test case that asserts a particular sequence of generator emissions
379 * @param {ASTNode} ast The AST to traverse
380 * @param {Record<string, string[]>} visitorKeys The custom visitor keys.
381 * @param {string[]} possibleQueries A collection of selectors that rules are listening for
382 * @param {Array[]} expectedEmissions A function that accepts the AST and returns a list of the emissions that the
383 * generator is expected to produce, in order.
384 * Each element of this list is an array where the first element is a selector (string), and the second is an AST node
385 * This should only include emissions that appear in possibleQueries.
386 * @returns {void}
387 */
388 function assertEmissions(ast, visitorKeys, possibleQueries, expectedEmissions) {
389 it(possibleQueries.join("; "), () => {
390 const emissions = getEmissions(ast, visitorKeys, possibleQueries)
391 .filter(emission => possibleQueries.indexOf(emission[0]) !== -1);
392
393 assert.deepStrictEqual(emissions, expectedEmissions(ast));
394 });
395 }
396
397 assertEmissions(
398 espree.parse("const foo = [<div/>, <div/>]", { ...ESPREE_CONFIG, ecmaFeatures: { jsx: true } }),
399 vk.KEYS,
400 ["* ~ *"],
401 ast => [
402 ["* ~ *", ast.body[0].declarations[0].init.elements[1]] // entering second JSXElement
403 ]
404 );
405
406 assertEmissions(
407 {
408
409 // Parse `class A implements B {}` with typescript-eslint.
410 type: "Program",
411 errors: [],
412 comments: [],
413 sourceType: "module",
414 body: [
415 {
416 type: "ClassDeclaration",
417 id: {
418 type: "Identifier",
419 name: "A"
420 },
421 superClass: null,
422 implements: [
423 {
424 type: "ClassImplements",
425 id: {
426 type: "Identifier",
427 name: "B"
428 },
429 typeParameters: null
430 }
431 ],
432 body: {
433 type: "ClassBody",
434 body: []
435 }
436 }
437 ]
438 },
439 vk.unionWith({
440
441 // see https://github.com/typescript-eslint/typescript-eslint/blob/e4d737b47574ff2c53cabab22853035dfe48c1ed/packages/visitor-keys/src/visitor-keys.ts#L27
442 ClassDeclaration: [
443 "decorators",
444 "id",
445 "typeParameters",
446 "superClass",
447 "superTypeParameters",
448 "implements",
449 "body"
450 ]
451 }),
452 [":first-child"],
453 ast => [
454 [":first-child", ast.body[0]], // entering first ClassDeclaration
455 [":first-child", ast.body[0].implements[0]] // entering first ClassImplements
456 ]
457 );
458 });
459
460 describe("parsing an invalid selector", () => {
461 it("throws a useful error", () => {
462 const emitter = createEmitter();
463
464 emitter.on("Foo >", () => {});
465 assert.throws(
466 () => new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION),
467 /Syntax error in selector "Foo >" at position 5: Expected " ", "!", .*/u
468 );
469 });
470 });
471 });