Skip to content

Commit 2f6f521

Browse files
committed
Fix: bugs about multiple statements in v-on
1 parent 1eef022 commit 2f6f521

File tree

8 files changed

+1512
-52
lines changed

8 files changed

+1512
-52
lines changed

docs/ast.md

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,35 @@ Some types are featured from [ESTree].
44

55
- [Program]
66
- [Node]
7+
- [Statement]
8+
- [BlockStatement]
79
- [Expression]
810
- [Literal]
911
- [Pattern]
1012

13+
You can use the type definition of this AST:
14+
15+
```ts
16+
import {AST} from "vue-eslint-parser"
17+
18+
export function create(context) {
19+
context.parserServices.registerTemplateBodyVisitor(context, {
20+
VElement(node: AST.VElement): void {
21+
//...
22+
}
23+
})
24+
25+
return {
26+
Program(node: AST.ESLintProgram): void {
27+
//...
28+
}
29+
}
30+
}
31+
```
32+
33+
`AST` has the types of ESLint's AST with the prefix `ESLint`.<br>
34+
See details: [../src/ast/nodes.ts](../src/ast/nodes.ts)
35+
1136
## Node
1237

1338
```js
@@ -70,14 +95,20 @@ interface VForExpression <: Expression {
7095
left: [ Pattern ]
7196
right: Expression
7297
}
98+
99+
interface VOnExpression <: Expression {
100+
type: "VOnExpression"
101+
body: [ Statement ]
102+
}
73103
```
74104

75105
- This is mustaches or directive values.
76106
- If syntax errors exist, `VExpressionContainer#expression` is `null`.
77107
- `Reference` is objects but not `Node`. Those are external references which are in the expression.
78-
- `VForExpression` is an expression node like [ForInStatement] but it has an array as `left` property and does not have `body` property. This is the value of `v-for` directives.
108+
- `VForExpression` is an expression node like [ForInStatement] but it has an array as `left` property and does not have `body` property. This is the value of [`v-for` directives].
109+
- `VOnExpression` is an expression node like [BlockStatement] but it does not have braces. This is the value of [`v-on` directives].
79110

80-
> Note: `vue-eslint-parser` transforms `v-for="(x, i) in list"` to `for(let [x, i] in list);` then gives the configured parser (`espree` by default) it. This implies that it needs the capability to parse ES2015 destructuring in order to parse `v-for` directives.
111+
> Note: `vue-eslint-parser` transforms `v-for="(x, i) in list"` to `for(let [x, i] in list);` then gives the configured parser (`espree` by default) it. This implies that it needs the capability to parse ES2015 destructuring in order to parse [`v-for` directives].
81112
82113
## VDirectiveKey
83114

@@ -177,14 +208,17 @@ extend interface Program {
177208
This spec enhances [Program] nodes as it has the root node of `<template>`.
178209
This supports only HTML for now. However, I'm going to add other languages Vue.js supports. The AST of other languages may be different form to VElement.
179210

180-
[ESTree]: https://github.com/estree/estree
181-
[Program]: https://github.com/estree/estree/blob/master/es5.md#programs
182-
[Node]: https://github.com/estree/estree/blob/master/es5.md#node-objects
183-
[Expression]: https://github.com/estree/estree/blob/master/es5.md#expression
184-
[Literal]: https://github.com/estree/estree/blob/master/es5.md#literal
185-
[Pattern]: https://github.com/estree/estree/blob/master/es5.md#patterns
211+
[ESTree]: https://github.com/estree/estree
212+
[Program]: https://github.com/estree/estree/blob/master/es5.md#programs
213+
[Node]: https://github.com/estree/estree/blob/master/es5.md#node-objects
214+
[Statement]: https://github.com/estree/estree/blob/master/es5.md#statements
215+
[BlockStatement]: https://github.com/estree/estree/blob/master/es5.md#blockstatement
216+
[Expression]: https://github.com/estree/estree/blob/master/es5.md#expressions
217+
[Literal]: https://github.com/estree/estree/blob/master/es5.md#literal
218+
[Pattern]: https://github.com/estree/estree/blob/master/es5.md#patterns
186219
[Identifier]: https://github.com/estree/estree/blob/master/es5.md#identifier
187220
[ForInStatement]: https://github.com/estree/estree/blob/master/es5.md#forinstatement
188221

189-
[`v-for` directives]: https://vuejs.org/v2/guide/list.html#v-for
190-
[scope]: https://vuejs.org/v2/guide/components.html#Scoped-Slots
222+
[`v-for` directives]: https://vuejs.org/v2/api/#v-for
223+
[`v-on` directives]: https://vuejs.org/v2/api/#v-on
224+
[scope]: https://vuejs.org/v2/guide/components.html#Scoped-Slots

src/ast/nodes.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface HasParent {
2121
/**
2222
* The union type for all nodes.
2323
*/
24-
export type Node = ESLintNode | VNode | VForExpression
24+
export type Node = ESLintNode | VNode | VForExpression | VOnExpression
2525

2626
//------------------------------------------------------------------------------
2727
// Script
@@ -73,7 +73,7 @@ export interface ESLintEmptyStatement extends HasLocation, HasParent {
7373

7474
export interface ESLintBlockStatement extends HasLocation, HasParent {
7575
type: "BlockStatement"
76-
body: ESLintStatement
76+
body: ESLintStatement[]
7777
}
7878

7979
export interface ESLintExpressionStatement extends HasLocation, HasParent {
@@ -575,6 +575,15 @@ export interface VForExpression extends HasLocation, HasParent {
575575
right: ESLintExpression
576576
}
577577

578+
/**
579+
* The node of `v-on` directives.
580+
*/
581+
export interface VOnExpression extends HasLocation, HasParent {
582+
type: "VOnExpression"
583+
parent: VExpressionContainer
584+
body: ESLintStatement[]
585+
}
586+
578587
/**
579588
* The union type of any nodes.
580589
*/
@@ -607,7 +616,7 @@ export interface VText extends HasLocation, HasParent {
607616
export interface VExpressionContainer extends HasLocation, HasParent {
608617
type: "VExpressionContainer"
609618
parent: VDocumentFragment | VElement | VDirective
610-
expression: ESLintExpression | VForExpression | null
619+
expression: ESLintExpression | VForExpression | VOnExpression | null
611620
references: Reference[]
612621
}
613622

src/script/index.ts

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* @copyright 2017 Toru Nagashima. All rights reserved.
44
* See LICENSE file in root directory for full license.
55
*/
6-
import {traverseNodes, ESLintArrayPattern, ESLintExpression, ESLintExpressionStatement, ESLintExtendedProgram, ESLintForInStatement, ESLintForOfStatement, ESLintPattern, ESLintProgram, ESLintVariableDeclaration, Node, ParseError, Reference, Token, Variable, VElement, VExpressionContainer, VForExpression} from "../ast"
6+
import * as lodash from "lodash"
7+
import {traverseNodes, ESLintArrayPattern, ESLintBlockStatement, ESLintExpression, ESLintExpressionStatement, ESLintExtendedProgram, ESLintForInStatement, ESLintForOfStatement, ESLintPattern, ESLintProgram, ESLintVariableDeclaration, Node, ParseError, Reference, Token, Variable, VElement, VForExpression, VOnExpression} from "../ast"
78
import {debug} from "../common/debug"
89
import {LocationCalculator} from "../common/location-calculator"
910
import {analyzeExternalReferences, analyzeVariablesAndExternalReferences} from "./scope-analyzer"
@@ -12,6 +13,7 @@ import {analyzeExternalReferences, analyzeVariablesAndExternalReferences} from "
1213
// [2] = aliases.
1314
// [3] = all after the aliases.
1415
const ALIAS_PARENS = /^(\s*)\(([\s\S]+)\)(\s*(?:in|of)\b[\s\S]+)$/
16+
const DUMMY_PARENT: any = {}
1517

1618
/**
1719
* The interface of ESLint custom parsers.
@@ -86,6 +88,25 @@ function normalizeLeft(left: ESLintVariableDeclaration | ESLintPattern, replaced
8688
return [id]
8789
}
8890

91+
/**
92+
* Remove references by name.
93+
* @param references The array of references to remove.
94+
* @param name The name of target references.
95+
*/
96+
function removeByName(references: Reference[], name: string): void {
97+
let i = 0
98+
while (i < references.length) {
99+
const reference = references[i]
100+
101+
if (reference.id.name === name) {
102+
references.splice(i, 1)
103+
}
104+
else {
105+
i += 1
106+
}
107+
}
108+
}
109+
89110
/**
90111
* Parse the given source code.
91112
*
@@ -114,7 +135,7 @@ function parseScriptFragment(code: string, locationCalculator: LocationCalculato
114135
* The result of parsing expressions.
115136
*/
116137
export interface ExpressionParseResult {
117-
expression: ESLintExpression | VForExpression
138+
expression: ESLintExpression | VForExpression | VOnExpression
118139
tokens: Token[]
119140
comments: Token[]
120141
references: Reference[]
@@ -245,7 +266,7 @@ export function parseVForExpression(code: string, locationCalculator: LocationCa
245266
type: "VForExpression",
246267
range: [firstToken.range[0], lastToken.range[1]],
247268
loc: {start: firstToken.loc.start, end: lastToken.loc.end},
248-
parent: {} as VExpressionContainer,
269+
parent: DUMMY_PARENT,
249270
left,
250271
right,
251272
}
@@ -273,3 +294,49 @@ export function parseVForExpression(code: string, locationCalculator: LocationCa
273294

274295
return {expression, tokens, comments, references, variables}
275296
}
297+
298+
/**
299+
* Parse the source code of inline scripts.
300+
* @param code The source code of inline scripts.
301+
* @param locationCalculator The location calculator for the inline script.
302+
* @param parserOptions The parser options.
303+
* @returns The result of parsing.
304+
*/
305+
export function parseVOnExpression(code: string, locationCalculator: LocationCalculator, parserOptions: any): ExpressionParseResult {
306+
debug("[script] parse v-on expression: \"{%s}\"", code)
307+
308+
const ast = parseScriptFragment(
309+
`{${code}}`,
310+
locationCalculator.getSubCalculatorAfter(-1),
311+
parserOptions
312+
).ast
313+
const references = analyzeExternalReferences(ast, parserOptions)
314+
const block = ast.body[0] as ESLintBlockStatement
315+
const body = block.body
316+
const first = lodash.first(body)
317+
const last = lodash.last(body)
318+
const expression: VOnExpression = {
319+
type: "VOnExpression",
320+
range: [
321+
(first != null) ? first.range[0] : block.range[0] + 1,
322+
(last != null) ? last.range[1] : block.range[1] - 1,
323+
],
324+
loc: {
325+
start: (first != null) ? first.loc.start : locationCalculator.getLocation(1),
326+
end: (last != null) ? last.loc.end : locationCalculator.getLocation(code.length + 1),
327+
},
328+
parent: DUMMY_PARENT,
329+
body,
330+
}
331+
const tokens = ast.tokens || []
332+
const comments = ast.comments || []
333+
334+
// Remvoe braces.
335+
tokens.shift()
336+
tokens.pop()
337+
338+
// Remove $event: https://vuejs.org/v2/api/#v-on
339+
removeByName(references, "$event")
340+
341+
return {expression, tokens, comments, references, variables: []}
342+
}

src/template/index.ts

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as lodash from "lodash"
77
import {ParseError, Reference, Token, Variable, VAttribute, VDirective, VDirectiveKey, VDocumentFragment, VExpressionContainer, VIdentifier, VLiteral, VNode} from "../ast"
88
import {debug} from "../common/debug"
99
import {LocationCalculator} from "../common/location-calculator"
10-
import {ExpressionParseResult, parseExpression, parseVForExpression} from "../script"
10+
import {ExpressionParseResult, parseExpression, parseVForExpression, parseVOnExpression} from "../script"
1111

1212
/**
1313
* Extract the variable declarations of scope attributes.
@@ -22,25 +22,6 @@ function extractScopeVariables(references: Reference[], outVariables: Variable[]
2222
}
2323
}
2424

25-
/**
26-
* Remove references by name.
27-
* @param references The array of references to remove.
28-
* @param name The name of target references.
29-
*/
30-
function removeByName(references: Reference[], name: string): void {
31-
let i = 0
32-
while (i < references.length) {
33-
const reference = references[i]
34-
35-
if (reference.id.name === name) {
36-
references.splice(i, 1)
37-
}
38-
else {
39-
i += 1
40-
}
41-
}
42-
}
43-
4425
/**
4526
* Get the belonging document of the given node.
4627
* @param leafNode The node to get.
@@ -237,9 +218,9 @@ function insertError(document: VDocumentFragment | null, error: ParseError): voi
237218
* @param parserOptions The parser options to parse expressions.
238219
* @param globalLocationCalculator The location calculator to adjust the locations of nodes.
239220
* @param node The attribute node to replace. This function modifies this node directly.
240-
* @param vFor The flag which indicates that this directive is `v-for`.
221+
* @param directiveName The name of this directive.
241222
*/
242-
function parseAttributeValue(code: string, parserOptions: any, globalLocationCalculator: LocationCalculator, node: VLiteral, vFor: boolean): ExpressionParseResult {
223+
function parseAttributeValue(code: string, parserOptions: any, globalLocationCalculator: LocationCalculator, node: VLiteral, directiveName: string): ExpressionParseResult {
243224
if (node.value.trim() === "") {
244225
throw new ParseError(
245226
"Unexpected empty",
@@ -253,9 +234,11 @@ function parseAttributeValue(code: string, parserOptions: any, globalLocationCal
253234
const firstChar = code[node.range[0]]
254235
const quoted = (firstChar === "\"" || firstChar === "'")
255236
const locationCalculator = globalLocationCalculator.getSubCalculatorAfter(node.range[0] + (quoted ? 1 : 0))
256-
const result = vFor
257-
? parseVForExpression(node.value, locationCalculator, parserOptions)
258-
: parseExpression(node.value, locationCalculator, parserOptions)
237+
const result = (
238+
directiveName === "for" ? parseVForExpression(node.value, locationCalculator, parserOptions) :
239+
directiveName === "on" ? parseVOnExpression(node.value, locationCalculator, parserOptions) :
240+
/* otherwise */ parseExpression(node.value, locationCalculator, parserOptions)
241+
)
259242

260243
// Add the tokens of quotes.
261244
if (quoted) {
@@ -299,15 +282,7 @@ export function convertToDirective(code: string, parserOptions: any, locationCal
299282
const document = getOwnerDocument(node)
300283

301284
try {
302-
const vFor = directive.key.name === "for"
303-
const vOn = directive.key.name === "on"
304-
const ret = parseAttributeValue(code, parserOptions, locationCalculator, node.value, vFor)
305-
306-
// https://vuejs.org/v2/api/#v-on
307-
// $event is not external references.
308-
if (vOn) {
309-
removeByName(ret.references, "$event")
310-
}
285+
const ret = parseAttributeValue(code, parserOptions, locationCalculator, node.value, directive.key.name)
311286

312287
directive.value = {
313288
type: "VExpressionContainer",
@@ -359,7 +334,7 @@ export function defineScopeAttributeVariable(code: string, parserOptions: any, l
359334
}
360335

361336
try {
362-
const ret = parseAttributeValue(code, parserOptions, locationCalculator, node.value, false)
337+
const ret = parseAttributeValue(code, parserOptions, locationCalculator, node.value, "scope")
363338
extractScopeVariables(ret.references, node.parent.parent.variables)
364339
}
365340
catch (err) {

0 commit comments

Comments
 (0)