Skip to content

Commit 86ec4d1

Browse files
authored
Add defineDocumentVisitor to parserServices (#119)
* Add defineDocumentVisitor to parserServices * fix * fix * Changed getTemplateBodyTokenStore to work even if `<template>` is missing * Add testcase * fix
1 parent 02b6d08 commit 86ec4d1

File tree

3 files changed

+165
-4
lines changed

3 files changed

+165
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ See also to [here](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0043-sf
203203
- `getTemplateBodyTokenStore()` ... returns ESLint `TokenStore` to get the tokens of `<template>`.
204204
- `getDocumentFragment()` ... returns the root `VDocumentFragment`.
205205
- `defineCustomBlocksVisitor(context, customParser, rule, scriptVisitor)` ... returns ESLint visitor that parses and traverses the contents of the custom block.
206+
- `defineDocumentVisitor(documentVisitor, options)` ... returns ESLint visitor to traverses the document.
206207
- [ast.md](./docs/ast.md) is `<template>` AST specification.
207208
- [mustache-interpolation-spacing.js](https://github.com/vuejs/eslint-plugin-vue/blob/b434ff99d37f35570fa351681e43ba2cf5746db3/lib/rules/mustache-interpolation-spacing.js) is an example.
208209

src/parser-services.ts

+72-4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ export interface ParserServices {
5656
options?: { templateBodyTriggerSelector: "Program" | "Program:exit" },
5757
): object
5858

59+
/**
60+
* Define handlers to traverse the document.
61+
* @param documentVisitor The document handlers.
62+
* @param options The options. This is optional.
63+
*/
64+
defineDocumentVisitor(
65+
documentVisitor: { [key: string]: (...args: any) => void },
66+
options?: { triggerSelector: "Program" | "Program:exit" },
67+
): object
68+
5969
/**
6070
* Define handlers to traverse custom blocks.
6171
* @param context The rule context.
@@ -103,6 +113,8 @@ export function define(
103113
const templateBodyEmitters = new Map<string, EventEmitter>()
104114
const stores = new WeakMap<object, TokenStore>()
105115

116+
const documentEmitters = new Map<string, EventEmitter>()
117+
106118
const customBlocksEmitters = new Map<
107119
| ESLintCustomBlockParser["parseForESLint"]
108120
| ESLintCustomBlockParser["parse"],
@@ -180,6 +192,63 @@ export function define(
180192
return scriptVisitor
181193
},
182194

195+
/**
196+
* Define handlers to traverse the document.
197+
* @param documentVisitor The document handlers.
198+
* @param options The options. This is optional.
199+
*/
200+
defineDocumentVisitor(
201+
documentVisitor: { [key: string]: (...args: any) => void },
202+
options?: { triggerSelector: "Program" | "Program:exit" },
203+
): object {
204+
const scriptVisitor: { [key: string]: (...args: any) => void } = {}
205+
if (!document) {
206+
return scriptVisitor
207+
}
208+
209+
const documentTriggerSelector =
210+
options?.triggerSelector ?? "Program:exit"
211+
212+
let emitter = documentEmitters.get(documentTriggerSelector)
213+
214+
// If this is the first time, initialize the intermediate event emitter.
215+
if (emitter == null) {
216+
emitter = new EventEmitter()
217+
emitter.setMaxListeners(0)
218+
documentEmitters.set(documentTriggerSelector, emitter)
219+
220+
const programExitHandler =
221+
scriptVisitor[documentTriggerSelector]
222+
scriptVisitor[documentTriggerSelector] = (node) => {
223+
try {
224+
if (typeof programExitHandler === "function") {
225+
programExitHandler(node)
226+
}
227+
228+
// Traverse document.
229+
const generator = new NodeEventGenerator(emitter!, {
230+
visitorKeys: KEYS,
231+
fallback: getFallbackKeys,
232+
})
233+
traverseNodes(document, generator)
234+
} finally {
235+
// eslint-disable-next-line @mysticatea/ts/ban-ts-ignore
236+
// @ts-ignore
237+
scriptVisitor[documentTriggerSelector] =
238+
programExitHandler
239+
documentEmitters.delete(documentTriggerSelector)
240+
}
241+
}
242+
}
243+
244+
// Register handlers into the intermediate event emitter.
245+
for (const selector of Object.keys(documentVisitor)) {
246+
emitter.on(selector, documentVisitor[selector])
247+
}
248+
249+
return scriptVisitor
250+
},
251+
183252
/**
184253
* Define handlers to traverse custom blocks.
185254
* @param context The rule context.
@@ -331,14 +400,13 @@ export function define(
331400
* @returns The token store of template body.
332401
*/
333402
getTemplateBodyTokenStore(): TokenStore {
334-
const ast = rootAST.templateBody
335-
const key = ast || stores
403+
const key = document || stores
336404
let store = stores.get(key)
337405

338406
if (!store) {
339407
store =
340-
ast != null
341-
? new TokenStore(ast.tokens, ast.comments)
408+
document != null
409+
? new TokenStore(document.tokens, document.comments)
342410
: new TokenStore([], [])
343411
stores.set(key, store)
344412
}

test/define-document-visitor.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* @author Yosuke Ota <https://github.com/ota-meshi>
3+
*/
4+
"use strict"
5+
6+
//------------------------------------------------------------------------------
7+
// Requirements
8+
//------------------------------------------------------------------------------
9+
10+
const assert = require("assert")
11+
const path = require("path")
12+
const eslint = require("eslint")
13+
const Linter = eslint.Linter
14+
15+
//------------------------------------------------------------------------------
16+
// Helpers
17+
//------------------------------------------------------------------------------
18+
19+
const PARSER_PATH = path.resolve(__dirname, "../src/index.ts")
20+
21+
//------------------------------------------------------------------------------
22+
// Tests
23+
//------------------------------------------------------------------------------
24+
25+
describe("parserServices.defineDocumentVisitor tests", () => {
26+
it("should be able to visit the document using defineDocumentVisitor.", () => {
27+
const code = `
28+
<template>
29+
{{forbidden}}
30+
{{foo()}}
31+
{{ok}}
32+
</template>
33+
<style>
34+
.ng {
35+
font: v-bind(forbidden)
36+
}
37+
.call {
38+
font: v-bind('foo()')
39+
}
40+
.ok {
41+
font: v-bind(ok)
42+
}
43+
</style>`
44+
45+
const linter = new Linter()
46+
47+
linter.defineParser(PARSER_PATH, require(PARSER_PATH))
48+
linter.defineRule("test-no-forbidden", {
49+
create(context) {
50+
return context.parserServices.defineDocumentVisitor({
51+
'Identifier[name="forbidden"]'(node) {
52+
context.report({
53+
node,
54+
message: 'no "forbidden"',
55+
})
56+
},
57+
})
58+
},
59+
})
60+
linter.defineRule("test-no-call", {
61+
create(context) {
62+
return context.parserServices.defineDocumentVisitor({
63+
CallExpression(node) {
64+
context.report({
65+
node,
66+
message: "no call",
67+
})
68+
},
69+
})
70+
},
71+
})
72+
const messages = linter.verify(code, {
73+
parser: PARSER_PATH,
74+
parserOptions: {
75+
ecmaVersion: 2018,
76+
},
77+
rules: {
78+
"test-no-forbidden": "error",
79+
"test-no-call": "error",
80+
},
81+
})
82+
assert.strictEqual(messages.length, 4)
83+
assert.strictEqual(messages[0].message, 'no "forbidden"')
84+
assert.strictEqual(messages[0].line, 3)
85+
assert.strictEqual(messages[1].message, "no call")
86+
assert.strictEqual(messages[1].line, 4)
87+
assert.strictEqual(messages[2].message, 'no "forbidden"')
88+
assert.strictEqual(messages[2].line, 9)
89+
assert.strictEqual(messages[3].message, "no call")
90+
assert.strictEqual(messages[3].line, 12)
91+
})
92+
})

0 commit comments

Comments
 (0)