diff --git a/.eslintrc.yml b/.eslintrc.yml index 0f676d30..6119e624 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -15,7 +15,7 @@ overrides: "@typescript-eslint/no-duplicate-imports": error - files: "typings/**" rules: - node/no-missing-import: + '@mysticatea/node/no-missing-import': - error - allowModules: - estree diff --git a/package.json b/package.json index b419f10d..ecb9e13f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "eslint-scope": "^5.0.0", "eslint-visitor-keys": "^1.1.0", "espree": "^6.2.1", - "esquery": "^1.0.1", + "esquery": "^1.4.0", "lodash": "^4.17.15" }, "devDependencies": { diff --git a/src/ast/traverse.ts b/src/ast/traverse.ts index ecccfc22..a0f7b91d 100644 --- a/src/ast/traverse.ts +++ b/src/ast/traverse.ts @@ -11,7 +11,7 @@ import type { Node } from "./nodes" // Helpers //------------------------------------------------------------------------------ -const KEYS = Evk.unionWith({ +export const KEYS = Evk.unionWith({ VAttribute: ["key", "value"], VDirectiveKey: ["name", "argument", "modifiers"], VDocumentFragment: ["children"], diff --git a/src/external/node-event-generator.ts b/src/external/node-event-generator.ts index c4221247..d02be8a5 100644 --- a/src/external/node-event-generator.ts +++ b/src/external/node-event-generator.ts @@ -2,7 +2,7 @@ * This file is copied from `eslint/lib/util/node-event-generator.js` */ import EventEmitter from "events" -import esquery, {Selector} from "esquery" +import esquery, {ESQueryOptions, Selector} from "esquery" import union from "lodash/union" import intersection from "lodash/intersection" import memoize from "lodash/memoize" @@ -187,6 +187,7 @@ const parseSelector = memoize<(rawSelector: string) => NodeSelector>(rawSelector */ export default class NodeEventGenerator { emitter: EventEmitter + esqueryOptions: ESQueryOptions private currentAncestry: Node[] private enterSelectorsByNodeType: Map @@ -198,8 +199,9 @@ export default class NodeEventGenerator { * @param emitter - An event emitter which is the destination of events. This emitter must already * have registered listeners for all of the events that it needs to listen for. */ - constructor(emitter: EventEmitter) { + constructor(emitter: EventEmitter, esqueryOptions: ESQueryOptions) { this.emitter = emitter + this.esqueryOptions = esqueryOptions this.currentAncestry = [] this.enterSelectorsByNodeType = new Map() this.exitSelectorsByNodeType = new Map() @@ -260,7 +262,7 @@ export default class NodeEventGenerator { * @param selector An AST selector descriptor */ private applySelector(node: Node, selector: NodeSelector): void { - 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/src/parser-services.ts b/src/parser-services.ts index f7b9edf8..0e086d85 100644 --- a/src/parser-services.ts +++ b/src/parser-services.ts @@ -13,7 +13,7 @@ import type { VDocumentFragment, VAttribute, } from "./ast" -import { traverseNodes } from "./ast" +import { getFallbackKeys, KEYS, traverseNodes } from "./ast/traverse" import type { LocationCalculator } from "./common/location-calculator" import type { CustomBlockContext, @@ -149,6 +149,10 @@ export function define( // Traverse template body. const generator = new NodeEventGenerator( emitter as EventEmitter, + { + visitorKeys: KEYS, + fallback: getFallbackKeys, + }, ) traverseNodes( rootAST.templateBody as VElement, @@ -273,7 +277,10 @@ export function define( } // Traverse custom block. - const generator = new NodeEventGenerator(emitter) + const generator = new NodeEventGenerator(emitter, { + visitorKeys: parsedResult.visitorKeys, + fallback: getFallbackKeys, + }) traverseNodes(parsedResult.ast, { visitorKeys: parsedResult.visitorKeys, enterNode(n) { diff --git a/test/define-custom-blocks-visitor.js b/test/define-custom-blocks-visitor.js index 6d9ea10a..cbb88ce3 100644 --- a/test/define-custom-blocks-visitor.js +++ b/test/define-custom-blocks-visitor.js @@ -96,6 +96,18 @@ const noProgramExitRule = { } }, } +const siblingSelectorRule = { + create(context) { + return { + "* ~ *"(node) { + context.report({ + node, + message: "* ~ *", + }) + }, + } + }, +} function createLinter(target = "json") { const linter = new Linter() @@ -417,6 +429,36 @@ describe("parserServices.defineCustomBlocksVisitor tests", () => { ) }) + it("should work even if used sibling selector.", () => { + const code = ` + +[42, 42] + +` + const linter = createLinter() + linter.defineRule("test-for-sibling-selector", (context) => + context.parserServices.defineCustomBlocksVisitor( + context, + jsonParser, + { + target: "json", + create: siblingSelectorRule.create, + } + ) + ) + const messages = linter.verify(code, { + ...LINTER_CONFIG, + rules: { + "test-for-sibling-selector": "error", + }, + }) + + assert.strictEqual(messages.length, 1) + assert.strictEqual(messages[0].message, "* ~ *") + assert.strictEqual(messages[0].line, 3) + assert.strictEqual(messages[0].column, 6) + }) + describe("API tests", () => { it("should work getAncestors().", () => { const code = ` diff --git a/test/index.js b/test/index.js index 95a48edd..4ea35b5e 100644 --- a/test/index.js +++ b/test/index.js @@ -614,5 +614,33 @@ describe("Basic tests", () => { assert.strictEqual(messages2.length, 1) assert.strictEqual(messages2[0].message, "OK") }) + + it("should work even if used sibling selector.", () => { + const code = "" + const config = { + parser: PARSER_PATH, + rules: { + "test-rule": "error", + }, + } + const linter = new Linter() + + linter.defineParser(PARSER_PATH, require(PARSER_PATH)) + linter.defineRule("test-rule", (context) => + context.parserServices.defineTemplateBodyVisitor({ + "* ~ *"(node) { + context.report({ node, message: "OK" }) + }, + }) + ) + + const messages1 = linter.verify(code, config) + const messages2 = linter.verify(linter.getSourceCode(), config) + + assert.strictEqual(messages1.length, 1) + assert.strictEqual(messages1[0].message, "OK") + assert.strictEqual(messages2.length, 1) + assert.strictEqual(messages2[0].message, "OK") + }) }) }) diff --git a/typings/esquery/index.d.ts b/typings/esquery/index.d.ts index a6f4fee7..2e4d0fac 100644 --- a/typings/esquery/index.d.ts +++ b/typings/esquery/index.d.ts @@ -4,7 +4,32 @@ * See LICENSE file in root directory for full license. */ -export type Selector = AdjacentSelector | AttributeSelector | ChildSelector | ClassSelector | CompoundSelector | DescendantSelector | FieldSelector | HasSelector | IdentifierSelector | MatchesSelector | NotSelector | NthChildSelector | NthLastChildSelector | SiblingSelector | WildcardSelector +import type { Node } from "../../src/ast" +// eslint-disable-next-line @mysticatea/node/no-missing-import +import type { VisitorKeys } from "../eslint-visitor-keys" + +export type Selector = + | AdjacentSelector + | AttributeSelector + | ChildSelector + | ClassSelector + | CompoundSelector + | DescendantSelector + | FieldSelector + | HasSelector + | IdentifierSelector + | MatchesSelector + | NotSelector + | NthChildSelector + | NthLastChildSelector + | SiblingSelector + | WildcardSelector + +export type TraverseOptionFallback = (node: Node) => readonly string[] +export interface ESQueryOptions { + visitorKeys?: VisitorKeys + fallback?: TraverseOptionFallback +} export interface AdjacentSelector { type: "adjacent" @@ -16,7 +41,7 @@ export interface AttributeSelector { type: "attribute" name: string operator: string | null | undefined - value: { type: string, value: any } + value: { type: string; value: any } } export interface ChildSelector { @@ -68,13 +93,13 @@ export interface NotSelector { export interface NthChildSelector { type: "nth-child" right: Selector - index: { type: string, value: any } + index: { type: string; value: any } } export interface NthLastChildSelector { type: "nth-last-child" right: Selector - index: { type: string, value: any } + index: { type: string; value: any } } export interface SiblingSelector { @@ -89,6 +114,11 @@ export interface WildcardSelector { declare const esquery: { parse(query: string): Selector - matches(node: object, selector: Selector, ancestry: object[]): boolean + matches( + node: object, + selector: Selector, + ancestry: object[], + options?: ESQueryOptions, + ): boolean } export default esquery