Skip to content

Commit

Permalink
Add defineDocumentVisitor to parserServices (#119)
Browse files Browse the repository at this point in the history
* Add defineDocumentVisitor to parserServices

* fix

* fix

* Changed getTemplateBodyTokenStore to work even if `<template>` is missing

* Add testcase

* fix
  • Loading branch information
ota-meshi authored Jul 28, 2021
1 parent 02b6d08 commit 86ec4d1
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ See also to [here](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0043-sf
- `getTemplateBodyTokenStore()` ... returns ESLint `TokenStore` to get the tokens of `<template>`.
- `getDocumentFragment()` ... returns the root `VDocumentFragment`.
- `defineCustomBlocksVisitor(context, customParser, rule, scriptVisitor)` ... returns ESLint visitor that parses and traverses the contents of the custom block.
- `defineDocumentVisitor(documentVisitor, options)` ... returns ESLint visitor to traverses the document.
- [ast.md](./docs/ast.md) is `<template>` AST specification.
- [mustache-interpolation-spacing.js](https://github.com/vuejs/eslint-plugin-vue/blob/b434ff99d37f35570fa351681e43ba2cf5746db3/lib/rules/mustache-interpolation-spacing.js) is an example.

Expand Down
76 changes: 72 additions & 4 deletions src/parser-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ export interface ParserServices {
options?: { templateBodyTriggerSelector: "Program" | "Program:exit" },
): object

/**
* Define handlers to traverse the document.
* @param documentVisitor The document handlers.
* @param options The options. This is optional.
*/
defineDocumentVisitor(
documentVisitor: { [key: string]: (...args: any) => void },
options?: { triggerSelector: "Program" | "Program:exit" },
): object

/**
* Define handlers to traverse custom blocks.
* @param context The rule context.
Expand Down Expand Up @@ -103,6 +113,8 @@ export function define(
const templateBodyEmitters = new Map<string, EventEmitter>()
const stores = new WeakMap<object, TokenStore>()

const documentEmitters = new Map<string, EventEmitter>()

const customBlocksEmitters = new Map<
| ESLintCustomBlockParser["parseForESLint"]
| ESLintCustomBlockParser["parse"],
Expand Down Expand Up @@ -180,6 +192,63 @@ export function define(
return scriptVisitor
},

/**
* Define handlers to traverse the document.
* @param documentVisitor The document handlers.
* @param options The options. This is optional.
*/
defineDocumentVisitor(
documentVisitor: { [key: string]: (...args: any) => void },
options?: { triggerSelector: "Program" | "Program:exit" },
): object {
const scriptVisitor: { [key: string]: (...args: any) => void } = {}
if (!document) {
return scriptVisitor
}

const documentTriggerSelector =
options?.triggerSelector ?? "Program:exit"

let emitter = documentEmitters.get(documentTriggerSelector)

// If this is the first time, initialize the intermediate event emitter.
if (emitter == null) {
emitter = new EventEmitter()
emitter.setMaxListeners(0)
documentEmitters.set(documentTriggerSelector, emitter)

const programExitHandler =
scriptVisitor[documentTriggerSelector]
scriptVisitor[documentTriggerSelector] = (node) => {
try {
if (typeof programExitHandler === "function") {
programExitHandler(node)
}

// Traverse document.
const generator = new NodeEventGenerator(emitter!, {
visitorKeys: KEYS,
fallback: getFallbackKeys,
})
traverseNodes(document, generator)
} finally {
// eslint-disable-next-line @mysticatea/ts/ban-ts-ignore
// @ts-ignore
scriptVisitor[documentTriggerSelector] =
programExitHandler
documentEmitters.delete(documentTriggerSelector)
}
}
}

// Register handlers into the intermediate event emitter.
for (const selector of Object.keys(documentVisitor)) {
emitter.on(selector, documentVisitor[selector])
}

return scriptVisitor
},

/**
* Define handlers to traverse custom blocks.
* @param context The rule context.
Expand Down Expand Up @@ -331,14 +400,13 @@ export function define(
* @returns The token store of template body.
*/
getTemplateBodyTokenStore(): TokenStore {
const ast = rootAST.templateBody
const key = ast || stores
const key = document || stores
let store = stores.get(key)

if (!store) {
store =
ast != null
? new TokenStore(ast.tokens, ast.comments)
document != null
? new TokenStore(document.tokens, document.comments)
: new TokenStore([], [])
stores.set(key, store)
}
Expand Down
92 changes: 92 additions & 0 deletions test/define-document-visitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @author Yosuke Ota <https://github.com/ota-meshi>
*/
"use strict"

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const assert = require("assert")
const path = require("path")
const eslint = require("eslint")
const Linter = eslint.Linter

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const PARSER_PATH = path.resolve(__dirname, "../src/index.ts")

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

describe("parserServices.defineDocumentVisitor tests", () => {
it("should be able to visit the document using defineDocumentVisitor.", () => {
const code = `
<template>
{{forbidden}}
{{foo()}}
{{ok}}
</template>
<style>
.ng {
font: v-bind(forbidden)
}
.call {
font: v-bind('foo()')
}
.ok {
font: v-bind(ok)
}
</style>`

const linter = new Linter()

linter.defineParser(PARSER_PATH, require(PARSER_PATH))
linter.defineRule("test-no-forbidden", {
create(context) {
return context.parserServices.defineDocumentVisitor({
'Identifier[name="forbidden"]'(node) {
context.report({
node,
message: 'no "forbidden"',
})
},
})
},
})
linter.defineRule("test-no-call", {
create(context) {
return context.parserServices.defineDocumentVisitor({
CallExpression(node) {
context.report({
node,
message: "no call",
})
},
})
},
})
const messages = linter.verify(code, {
parser: PARSER_PATH,
parserOptions: {
ecmaVersion: 2018,
},
rules: {
"test-no-forbidden": "error",
"test-no-call": "error",
},
})
assert.strictEqual(messages.length, 4)
assert.strictEqual(messages[0].message, 'no "forbidden"')
assert.strictEqual(messages[0].line, 3)
assert.strictEqual(messages[1].message, "no call")
assert.strictEqual(messages[1].line, 4)
assert.strictEqual(messages[2].message, 'no "forbidden"')
assert.strictEqual(messages[2].line, 9)
assert.strictEqual(messages[3].message, "no call")
assert.strictEqual(messages[3].line, 12)
})
})

0 comments on commit 86ec4d1

Please sign in to comment.