mirror of
https://git.proxmox.com/git/pve-eslint
synced 2025-08-24 15:11:48 +00:00
472 lines
19 KiB
JavaScript
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
|
|
);
|
|
});
|
|
});
|
|
});
|