pve-eslint/eslint/tests/lib/linter/node-event-generator.js
Dominik Csapak 8f9d1d4df0 import 8.23.1 source
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2022-09-15 11:13:41 +02:00

472 lines
19 KiB
JavaScript

/**
* @fileoverview Tests for NodeEventGenerator.
* @author Toru Nagashima
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const assert = require("assert"),
sinon = require("sinon"),
espree = require("espree"),
vk = require("eslint-visitor-keys"),
Traverser = require("../../../lib/shared/traverser"),
EventGeneratorTester = require("../../../tools/internal-testers/event-generator-tester"),
createEmitter = require("../../../lib/linter/safe-emitter"),
NodeEventGenerator = require("../../../lib/linter/node-event-generator");
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
const ESPREE_CONFIG = {
ecmaVersion: 6,
comment: true,
tokens: true,
range: true,
loc: true
};
const STANDARD_ESQUERY_OPTION = { visitorKeys: vk.KEYS, fallback: Traverser.getKeys };
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("NodeEventGenerator", () => {
EventGeneratorTester.testEventGeneratorInterface(
new NodeEventGenerator(createEmitter(), STANDARD_ESQUERY_OPTION)
);
describe("entering a single AST node", () => {
let emitter, generator;
beforeEach(() => {
emitter = Object.create(createEmitter(), { emit: { value: sinon.spy() } });
["Foo", "Bar", "Foo > Bar", "Foo:exit"].forEach(selector => emitter.on(selector, () => {}));
generator = new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION);
});
it("should generate events for entering AST node.", () => {
const dummyNode = { type: "Foo", value: 1 };
generator.enterNode(dummyNode);
assert(emitter.emit.calledOnce);
assert(emitter.emit.calledWith("Foo", dummyNode));
});
it("should generate events for exiting AST node.", () => {
const dummyNode = { type: "Foo", value: 1 };
generator.leaveNode(dummyNode);
assert(emitter.emit.calledOnce);
assert(emitter.emit.calledWith("Foo:exit", dummyNode));
});
it("should generate events for AST queries", () => {
const dummyNode = { type: "Bar", parent: { type: "Foo" } };
generator.enterNode(dummyNode);
assert(emitter.emit.calledTwice);
assert(emitter.emit.calledWith("Foo > Bar", dummyNode));
});
});
describe("traversing the entire AST", () => {
/**
* Gets a list of emitted types/selectors from the generator, in emission order
* @param {ASTNode} ast The AST to traverse
* @param {Array<string>|Set<string>} possibleQueries Selectors to detect
* @returns {Array[]} A list of emissions, in the order that they were emitted. Each emission is a two-element
* array where the first element is a string, and the second element is the emitted AST node.
*/
function getEmissions(ast, possibleQueries) {
const emissions = [];
const emitter = Object.create(createEmitter(), {
emit: {
value: (selector, node) => emissions.push([selector, node])
}
});
possibleQueries.forEach(query => emitter.on(query, () => {}));
const generator = new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION);
Traverser.traverse(ast, {
enter(node, parent) {
node.parent = parent;
generator.enterNode(node);
},
leave(node) {
generator.leaveNode(node);
}
});
return emissions;
}
/**
* Creates a test case that asserts a particular sequence of generator emissions
* @param {string} sourceText The source text that should be parsed and traversed
* @param {string[]} possibleQueries A collection of selectors that rules are listening for
* @param {Array[]} expectedEmissions A function that accepts the AST and returns a list of the emissions that the
* generator is expected to produce, in order.
* Each element of this list is an array where the first element is a selector (string), and the second is an AST node
* This should only include emissions that appear in possibleQueries.
* @returns {void}
*/
function assertEmissions(sourceText, possibleQueries, expectedEmissions) {
it(possibleQueries.join("; "), () => {
const ast = espree.parse(sourceText, ESPREE_CONFIG);
const emissions = getEmissions(ast, possibleQueries)
.filter(emission => possibleQueries.includes(emission[0]));
assert.deepStrictEqual(emissions, expectedEmissions(ast));
});
}
assertEmissions(
"foo + bar;",
["Program", "Program:exit", "ExpressionStatement", "ExpressionStatement:exit", "BinaryExpression", "BinaryExpression:exit", "Identifier", "Identifier:exit"],
ast => [
["Program", ast], // entering program
["ExpressionStatement", ast.body[0]], // entering 'foo + bar;'
["BinaryExpression", ast.body[0].expression], // entering 'foo + bar'
["Identifier", ast.body[0].expression.left], // entering 'foo'
["Identifier:exit", ast.body[0].expression.left], // exiting 'foo'
["Identifier", ast.body[0].expression.right], // entering 'bar'
["Identifier:exit", ast.body[0].expression.right], // exiting 'bar'
["BinaryExpression:exit", ast.body[0].expression], // exiting 'foo + bar'
["ExpressionStatement:exit", ast.body[0]], // exiting 'foo + bar;'
["Program:exit", ast] // exiting program
]
);
assertEmissions(
"foo + 5",
[
"BinaryExpression > Identifier",
"BinaryExpression",
"BinaryExpression Literal:exit",
"BinaryExpression > Identifier:exit",
"BinaryExpression:exit"
],
ast => [
["BinaryExpression", ast.body[0].expression], // foo + 5
["BinaryExpression > Identifier", ast.body[0].expression.left], // foo
["BinaryExpression > Identifier:exit", ast.body[0].expression.left], // exiting foo
["BinaryExpression Literal:exit", ast.body[0].expression.right], // exiting 5
["BinaryExpression:exit", ast.body[0].expression] // exiting foo + 5
]
);
assertEmissions(
"foo + 5",
["BinaryExpression > *[name='foo']"],
ast => [["BinaryExpression > *[name='foo']", ast.body[0].expression.left]] // entering foo
);
assertEmissions(
"foo",
["*"],
ast => [
["*", ast], // Program
["*", ast.body[0]], // ExpressionStatement
["*", ast.body[0].expression] // Identifier
]
);
assertEmissions(
"foo",
["*:not(ExpressionStatement)"],
ast => [
["*:not(ExpressionStatement)", ast], // Program
["*:not(ExpressionStatement)", ast.body[0].expression] // Identifier
]
);
assertEmissions(
"foo()",
["CallExpression[callee.name='foo']"],
ast => [["CallExpression[callee.name='foo']", ast.body[0].expression]] // foo()
);
assertEmissions(
"foo()",
["CallExpression[callee.name='bar']"],
() => [] // (nothing emitted)
);
assertEmissions(
"foo + bar + baz",
[":not(*)"],
() => [] // (nothing emitted)
);
assertEmissions(
"foo + bar + baz",
[":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])"],
ast => [
[":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.left.left], // foo
[":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.left.right], // bar
[":matches(Identifier[name='foo'], Identifier[name='bar'], Identifier[name='baz'])", ast.body[0].expression.right] // baz
]
);
assertEmissions(
"foo + 5 + 6",
["Identifier, Literal[value=5]"],
ast => [
["Identifier, Literal[value=5]", ast.body[0].expression.left.left], // foo
["Identifier, Literal[value=5]", ast.body[0].expression.left.right] // 5
]
);
assertEmissions(
"[foo, 5, foo]",
["Identifier + Literal"],
ast => [["Identifier + Literal", ast.body[0].expression.elements[1]]] // 5
);
assertEmissions(
"[foo, {}, 5]",
["Identifier + Literal", "Identifier ~ Literal"],
ast => [["Identifier ~ Literal", ast.body[0].expression.elements[2]]] // 5
);
assertEmissions(
"foo; bar + baz; qux()",
[":expression", ":statement"],
ast => [
[":statement", ast.body[0]],
[":expression", ast.body[0].expression],
[":statement", ast.body[1]],
[":expression", ast.body[1].expression],
[":expression", ast.body[1].expression.left],
[":expression", ast.body[1].expression.right],
[":statement", ast.body[2]],
[":expression", ast.body[2].expression],
[":expression", ast.body[2].expression.callee]
]
);
assertEmissions(
"function foo(){} var x; (function (p){}); () => {};",
[":function", "ExpressionStatement > :function", "VariableDeclaration, :function[params.length=1]"],
ast => [
[":function", ast.body[0]], // function foo(){}
["VariableDeclaration, :function[params.length=1]", ast.body[1]], // var x;
[":function", ast.body[2].expression], // function (p){}
["ExpressionStatement > :function", ast.body[2].expression], // function (p){}
["VariableDeclaration, :function[params.length=1]", ast.body[2].expression], // function (p){}
[":function", ast.body[3].expression], // () => {}
["ExpressionStatement > :function", ast.body[3].expression] // () => {}
]
);
assertEmissions(
"foo;",
[
"*",
":not(*)",
"Identifier",
"ExpressionStatement > *",
"ExpressionStatement > Identifier",
"ExpressionStatement > [name='foo']",
"Identifier, ReturnStatement",
"FooStatement",
"[name = 'foo']",
"[name='foo']",
"[name ='foo']",
"Identifier[name='foo']",
"[name='foo'][name.length=3]",
":not(Program, ExpressionStatement)",
":not(Program, Identifier) > [name.length=3]"
],
ast => [
["*", ast], // Program
["*", ast.body[0]], // ExpressionStatement
// selectors for the 'foo' identifier, in order of increasing specificity
["*", ast.body[0].expression], // 0 identifiers, 0 pseudoclasses
["ExpressionStatement > *", ast.body[0].expression], // 0 pseudoclasses, 1 identifier
["Identifier", ast.body[0].expression], // 0 pseudoclasses, 1 identifier
[":not(Program, ExpressionStatement)", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
["ExpressionStatement > Identifier", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
["Identifier, ReturnStatement", ast.body[0].expression], // 0 pseudoclasses, 2 identifiers
["[name = 'foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
["[name ='foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
["[name='foo']", ast.body[0].expression], // 1 pseudoclass, 0 identifiers
["ExpressionStatement > [name='foo']", ast.body[0].expression], // 1 attribute, 1 identifier
["Identifier[name='foo']", ast.body[0].expression], // 1 attribute, 1 identifier
[":not(Program, Identifier) > [name.length=3]", ast.body[0].expression], // 1 attribute, 2 identifiers
["[name='foo'][name.length=3]", ast.body[0].expression] // 2 attributes, 0 identifiers
]
);
assertEmissions(
"foo(); bar; baz;",
["CallExpression, [name='bar']"],
ast => [
["CallExpression, [name='bar']", ast.body[0].expression],
["CallExpression, [name='bar']", ast.body[1].expression]
]
);
assertEmissions(
"foo; bar;",
["[name.length=3]:exit"],
ast => [
["[name.length=3]:exit", ast.body[0].expression],
["[name.length=3]:exit", ast.body[1].expression]
]
);
// https://github.com/eslint/eslint/issues/14799
assertEmissions(
"const {a = 1} = b;",
["Property > .key"],
ast => [
["Property > .key", ast.body[0].declarations[0].id.properties[0].key]
]
);
});
describe("traversing the entire non-standard AST", () => {
/**
* Gets a list of emitted types/selectors from the generator, in emission order
* @param {ASTNode} ast The AST to traverse
* @param {Record<string, string[]>} visitorKeys The custom visitor keys.
* @param {Array<string>|Set<string>} possibleQueries Selectors to detect
* @returns {Array[]} A list of emissions, in the order that they were emitted. Each emission is a two-element
* array where the first element is a string, and the second element is the emitted AST node.
*/
function getEmissions(ast, visitorKeys, possibleQueries) {
const emissions = [];
const emitter = Object.create(createEmitter(), {
emit: {
value: (selector, node) => emissions.push([selector, node])
}
});
possibleQueries.forEach(query => emitter.on(query, () => {}));
const generator = new NodeEventGenerator(emitter, { visitorKeys, fallback: Traverser.getKeys });
Traverser.traverse(ast, {
visitorKeys,
enter(node, parent) {
node.parent = parent;
generator.enterNode(node);
},
leave(node) {
generator.leaveNode(node);
}
});
return emissions;
}
/**
* Creates a test case that asserts a particular sequence of generator emissions
* @param {ASTNode} ast The AST to traverse
* @param {Record<string, string[]>} visitorKeys The custom visitor keys.
* @param {string[]} possibleQueries A collection of selectors that rules are listening for
* @param {Array[]} expectedEmissions A function that accepts the AST and returns a list of the emissions that the
* generator is expected to produce, in order.
* Each element of this list is an array where the first element is a selector (string), and the second is an AST node
* This should only include emissions that appear in possibleQueries.
* @returns {void}
*/
function assertEmissions(ast, visitorKeys, possibleQueries, expectedEmissions) {
it(possibleQueries.join("; "), () => {
const emissions = getEmissions(ast, visitorKeys, possibleQueries)
.filter(emission => possibleQueries.includes(emission[0]));
assert.deepStrictEqual(emissions, expectedEmissions(ast));
});
}
assertEmissions(
espree.parse("const foo = [<div/>, <div/>]", { ...ESPREE_CONFIG, ecmaFeatures: { jsx: true } }),
vk.KEYS,
["* ~ *"],
ast => [
["* ~ *", ast.body[0].declarations[0].init.elements[1]] // entering second JSXElement
]
);
assertEmissions(
{
// Parse `class A implements B {}` with typescript-eslint.
type: "Program",
errors: [],
comments: [],
sourceType: "module",
body: [
{
type: "ClassDeclaration",
id: {
type: "Identifier",
name: "A"
},
superClass: null,
implements: [
{
type: "ClassImplements",
id: {
type: "Identifier",
name: "B"
},
typeParameters: null
}
],
body: {
type: "ClassBody",
body: []
}
}
]
},
vk.unionWith({
// see https://github.com/typescript-eslint/typescript-eslint/blob/e4d737b47574ff2c53cabab22853035dfe48c1ed/packages/visitor-keys/src/visitor-keys.ts#L27
ClassDeclaration: [
"decorators",
"id",
"typeParameters",
"superClass",
"superTypeParameters",
"implements",
"body"
]
}),
[":first-child"],
ast => [
[":first-child", ast.body[0]], // entering first ClassDeclaration
[":first-child", ast.body[0].implements[0]] // entering first ClassImplements
]
);
});
describe("parsing an invalid selector", () => {
it("throws a useful error", () => {
const emitter = createEmitter();
emitter.on("Foo >", () => {});
assert.throws(
() => new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION),
/Syntax error in selector "Foo >" at position 5: Expected " ", "!", .*/u
);
});
});
});