diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 5c1a8d78aa1..adb5c215590 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -942,7 +942,9 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser }); // only run code path analyzer if the top level node is "Program", skip otherwise - const eventGenerator = nodeQueue[0].node.type === "Program" ? new CodePathAnalyzer(new NodeEventGenerator(emitter)) : new NodeEventGenerator(emitter); + const eventGenerator = nodeQueue[0].node.type === "Program" + ? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys })) + : new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }); nodeQueue.forEach(traversalInfo => { currentNode = traversalInfo.node; diff --git a/lib/linter/node-event-generator.js b/lib/linter/node-event-generator.js index 6f3b2513998..0b4e50fc4b7 100644 --- a/lib/linter/node-event-generator.js +++ b/lib/linter/node-event-generator.js @@ -208,10 +208,12 @@ class NodeEventGenerator { * 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`.) + * @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes. * @returns {NodeEventGenerator} new instance */ - constructor(emitter) { + constructor(emitter, esqueryOptions) { this.emitter = emitter; + this.esqueryOptions = esqueryOptions; this.currentAncestry = []; this.enterSelectorsByNodeType = new Map(); this.exitSelectorsByNodeType = new Map(); @@ -250,7 +252,7 @@ class NodeEventGenerator { * @returns {void} */ applySelector(node, selector) { - if (esquery.matches(node, selector.parsedSelector, this.currentAncestry)) { + if (esquery.matches(node, selector.parsedSelector, this.currentAncestry, this.esqueryOptions)) { this.emitter.emit(selector.rawSelector, node); } } diff --git a/package.json b/package.json index f44e214811f..b831d7c059e 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", - "esquery": "^1.2.0", + "esquery": "^1.4.0", "esutils": "^2.0.2", "file-entry-cache": "^6.0.0", "functional-red-black-tree": "^1.0.1", diff --git a/tests/lib/linter/code-path-analysis/code-path-analyzer.js b/tests/lib/linter/code-path-analysis/code-path-analyzer.js index c5d0ecf267f..b2356f24691 100644 --- a/tests/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/tests/lib/linter/code-path-analysis/code-path-analyzer.js @@ -12,6 +12,7 @@ const assert = require("assert"), fs = require("fs"), path = require("path"), + vk = require("eslint-visitor-keys"), { Linter } = require("../../../../lib/linter"), EventGeneratorTester = require("../../../../tools/internal-testers/event-generator-tester"), createEmitter = require("../../../../lib/linter/safe-emitter"), @@ -19,12 +20,15 @@ const assert = require("assert"), CodePath = require("../../../../lib/linter/code-path-analysis/code-path"), CodePathAnalyzer = require("../../../../lib/linter/code-path-analysis/code-path-analyzer"), CodePathSegment = require("../../../../lib/linter/code-path-analysis/code-path-segment"), - NodeEventGenerator = require("../../../../lib/linter/node-event-generator"); + NodeEventGenerator = require("../../../../lib/linter/node-event-generator"), + Traverser = require("../../../lib/shared/traverser"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ +const STANDARD_ESQUERY_OPTION = { visitorKeys: vk.KEYS, fallback: Traverser.getKeys }; + const expectedPattern = /\/\*expected\s+((?:.|[\r\n])+?)\s*\*\//gu; const lineEndingPattern = /\r?\n/gu; const linter = new Linter(); @@ -54,7 +58,7 @@ function getExpectedDotArrows(source) { describe("CodePathAnalyzer", () => { EventGeneratorTester.testEventGeneratorInterface( - new CodePathAnalyzer(new NodeEventGenerator(createEmitter())) + new CodePathAnalyzer(new NodeEventGenerator(createEmitter(), STANDARD_ESQUERY_OPTION)) ); describe("interface of code paths", () => { diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 093ebcfaf70..f999dd3ad9d 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -5293,9 +5293,11 @@ var a = "test2"; let types = []; let sourceCode; let scopeManager; + let firstChildNodes = []; beforeEach(() => { types = []; + firstChildNodes = []; linter.defineRule("collect-node-types", () => ({ "*"(node) { types.push(node.type); @@ -5306,12 +5308,18 @@ var a = "test2"; return {}; }); + linter.defineRule("esquery-option", () => ({ + ":first-child"(node) { + firstChildNodes.push(node); + } + })); linter.defineParser("enhanced-parser2", testParsers.enhancedParser2); linter.verify("@foo class A {}", { parser: "enhanced-parser2", rules: { "collect-node-types": "error", - "save-scope-manager": "error" + "save-scope-manager": "error", + "esquery-option": "error" } }); @@ -5351,6 +5359,13 @@ var a = "test2"; ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] ); }); + + it("esquery should use the visitorKeys (so 'visitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { + assert.deepStrictEqual( + firstChildNodes, + [sourceCode.ast.body[0], sourceCode.ast.body[0].experimentalDecorators[0]] + ); + }); }); describe("if a parser provides 'scope'", () => { diff --git a/tests/lib/linter/node-event-generator.js b/tests/lib/linter/node-event-generator.js index 8d12cbbdc9d..f9010c32138 100644 --- a/tests/lib/linter/node-event-generator.js +++ b/tests/lib/linter/node-event-generator.js @@ -11,6 +11,7 @@ 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"), @@ -28,9 +29,11 @@ const ESPREE_CONFIG = { loc: true }; +const STANDARD_ESQUERY_OPTION = { visitorKeys: vk.KEYS, fallback: Traverser.getKeys }; + describe("NodeEventGenerator", () => { EventGeneratorTester.testEventGeneratorInterface( - new NodeEventGenerator(createEmitter()) + new NodeEventGenerator(createEmitter(), STANDARD_ESQUERY_OPTION) ); describe("entering a single AST node", () => { @@ -40,7 +43,7 @@ describe("NodeEventGenerator", () => { emitter = Object.create(createEmitter(), { emit: { value: sinon.spy() } }); ["Foo", "Bar", "Foo > Bar", "Foo:exit"].forEach(selector => emitter.on(selector, () => {})); - generator = new NodeEventGenerator(emitter); + generator = new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION); }); it("should generate events for entering AST node.", () => { @@ -89,7 +92,7 @@ describe("NodeEventGenerator", () => { }); possibleQueries.forEach(query => emitter.on(query, () => {})); - const generator = new NodeEventGenerator(emitter); + const generator = new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION); Traverser.traverse(ast, { enter(node, parent) { @@ -308,13 +311,131 @@ describe("NodeEventGenerator", () => { ); }); + 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} visitorKeys The custom visitor keys. + * @param {Array|Set} 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} 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.indexOf(emission[0]) !== -1); + + assert.deepStrictEqual(emissions, expectedEmissions(ast)); + }); + } + + assertEmissions( + espree.parse("const foo = [
,
]", { ...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), + () => new NodeEventGenerator(emitter, STANDARD_ESQUERY_OPTION), /Syntax error in selector "Foo >" at position 5: Expected " ", "!", .*/u ); }); diff --git a/tests/lib/rules/no-restricted-syntax.js b/tests/lib/rules/no-restricted-syntax.js index cf8bc412366..d5fb36aa472 100644 --- a/tests/lib/rules/no-restricted-syntax.js +++ b/tests/lib/rules/no-restricted-syntax.js @@ -145,18 +145,26 @@ ruleTester.run("no-restricted-syntax", rule, { { messageId: "restrictedSyntax", data: { message: "Using '[optional=true]' is not allowed." }, type: "CallExpression" }, { messageId: "restrictedSyntax", data: { message: "Using '[optional=true]' is not allowed." }, type: "MemberExpression" } ] - } + }, + + // fix https://github.com/estools/esquery/issues/110 + { + code: "a?.b", + options: [":nth-child(1)"], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { messageId: "restrictedSyntax", data: { message: "Using ':nth-child(1)' is not allowed." }, type: "ExpressionStatement" } + ] + }, - /* - * TODO(mysticatea): fix https://github.com/estools/esquery/issues/110 - * { - * code: "a?.b", - * options: [":nth-child(1)"], - * parserOptions: { ecmaVersion: 2020 }, - * errors: [ - * { messageId: "restrictedSyntax", data: { message: "Using ':nth-child(1)' is not allowed." }, type: "ExpressionStatement" } - * ] - * } - */ + // https://github.com/eslint/eslint/issues/13639#issuecomment-683976062 + { + code: "const foo = [
,
]", + options: ["* ~ *"], + parserOptions: { ecmaVersion: 2020, ecmaFeatures: { jsx: true } }, + errors: [ + { messageId: "restrictedSyntax", data: { message: "Using '* ~ *' is not allowed." }, type: "JSXElement" } + ] + } ] });