Skip to content

Commit 9bea7aa

Browse files
committed
fix: crashes when v-on expression is not a function (vuejs#2605)
1 parent 95ae73c commit 9bea7aa

File tree

4 files changed

+78
-10
lines changed

4 files changed

+78
-10
lines changed

packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ export function render(_ctx, _cache) {
209209
return (_openBlock(), _createBlock(\\"div\\", null, [
210210
_createVNode(\\"div\\", null, [
211211
_createVNode(\\"div\\", {
212-
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.foo(...args)))
212+
onClick: _cache[1] || (_cache[1] = (...args) => ( (() => { if (typeof _ctx.foo === 'function') { return _ctx.foo } else { console.warn('v-on expression is not a function.') return () => { } } })()(...args)))
213213
})
214214
])
215215
]))

packages/compiler-core/__tests__/transforms/vOn.spec.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
transform,
1111
VNodeCall
1212
} from '../../src'
13-
import { transformOn } from '../../src/transforms/vOn'
13+
import { transformOn, wrapExpressionContent } from '../../src/transforms/vOn'
1414
import { transformElement } from '../../src/transforms/transformElement'
1515
import { transformExpression } from '../../src/transforms/transformExpression'
1616

@@ -30,6 +30,21 @@ function parseWithVOn(template: string, options: CompilerOptions = {}) {
3030
}
3131

3232
describe('compiler: transform v-on', () => {
33+
test('wrapper', () => {
34+
const wrapedExp = wrapExpressionContent('onClick')
35+
expect(wrapedExp).toMatch(
36+
`
37+
(() => {
38+
if (typeof onClick === 'function') {
39+
return onClick
40+
} else {
41+
console.warn('v-on expression is not a function.')
42+
return () => { }
43+
}
44+
})()`.replace(/\s+/g, ' ')
45+
)
46+
})
47+
3348
test('basic', () => {
3449
const { node } = parseWithVOn(`<div v-on:click="onClick"/>`)
3550
expect((node.codegenNode as VNodeCall).props).toMatchObject({
@@ -476,7 +491,11 @@ describe('compiler: transform v-on', () => {
476491
index: 1,
477492
value: {
478493
type: NodeTypes.COMPOUND_EXPRESSION,
479-
children: [`(...args) => (`, { content: `_ctx.foo(...args)` }, `)`]
494+
children: [
495+
`(...args) => (`,
496+
{ content: wrapExpressionContent(`_ctx.foo`) },
497+
`)`
498+
]
480499
}
481500
})
482501
})

packages/compiler-core/src/errors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const enum ErrorCodes {
7070
X_V_FOR_TEMPLATE_KEY_PLACEMENT,
7171
X_V_BIND_NO_EXPRESSION,
7272
X_V_ON_NO_EXPRESSION,
73+
X_V_ON_EXPRESSION_NOT_FUNCTION,
7374
X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
7475
X_V_SLOT_MIXED_SLOT_USAGE,
7576
X_V_SLOT_DUPLICATE_SLOT_NAMES,
@@ -144,6 +145,7 @@ export const errorMessages: { [code: number]: string } = {
144145
[ErrorCodes.X_V_FOR_TEMPLATE_KEY_PLACEMENT]: `<template v-for> key should be placed on the <template> tag.`,
145146
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
146147
[ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
148+
[ErrorCodes.X_V_ON_EXPRESSION_NOT_FUNCTION]: `v-on expression is not a function.`,
147149
[ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
148150
[ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE]:
149151
`Mixed v-slot usage on both the component and nested <template>.` +

packages/compiler-core/src/transforms/vOn.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { DirectiveTransform, DirectiveTransformResult } from '../transform'
22
import {
3+
CompoundExpressionNode,
34
createCompoundExpression,
45
createObjectProperty,
56
createSimpleExpression,
67
DirectiveNode,
78
ElementTypes,
89
ExpressionNode,
910
NodeTypes,
10-
SimpleExpressionNode
11+
SimpleExpressionNode,
12+
SourceLocation
1113
} from '../ast'
12-
import { camelize, toHandlerKey } from '@vue/shared'
13-
import { createCompilerError, ErrorCodes } from '../errors'
14+
import { camelize, isString, NOOP, toHandlerKey } from '@vue/shared'
15+
import { createCompilerError, ErrorCodes, errorMessages } from '../errors'
1416
import { processExpression } from './transformExpression'
1517
import { validateBrowserExpression } from '../validateExpression'
1618
import { hasScopeRef, isMemberExpression } from '../utils'
@@ -28,6 +30,51 @@ export interface VOnDirectiveNode extends DirectiveNode {
2830
exp: SimpleExpressionNode | undefined
2931
}
3032

33+
export const unfoldExpression = (
34+
exp: CompoundExpressionNode
35+
): SimpleExpressionNode => {
36+
let loc: SourceLocation = {
37+
source: '',
38+
start: { line: 1, column: 1, offset: 0 },
39+
end: { line: 1, column: 1, offset: 0 }
40+
}
41+
let content = ''
42+
43+
for (let i = 0; i < exp.children!.length; i++) {
44+
let node = exp.children[i]
45+
if (isString(node)) {
46+
loc.source += node
47+
content += node
48+
} else {
49+
let simpleExp = node as SimpleExpressionNode
50+
!i && (loc.start = simpleExp.loc.start)
51+
loc.end = simpleExp.loc.end
52+
loc.source += simpleExp.loc.source
53+
content += simpleExp.content
54+
}
55+
}
56+
57+
return createSimpleExpression(content, false, loc)
58+
}
59+
60+
// Event expression wrap to make sure it is a function.
61+
// See issue #2605.
62+
export const wrapExpressionContent = (content: string): string => {
63+
return (
64+
`
65+
(() => {
66+
if (typeof ${content} === 'function') {
67+
return ${content}
68+
} else {
69+
console.warn('${
70+
errorMessages[ErrorCodes.X_V_ON_EXPRESSION_NOT_FUNCTION]
71+
}')
72+
return ${NOOP}
73+
}
74+
})()`.replace(/\s+/g, ' ') + `(...args)`
75+
)
76+
}
77+
3178
export const transformOn: DirectiveTransform = (
3279
dir,
3380
node,
@@ -102,11 +149,11 @@ export const transformOn: DirectiveTransform = (
102149
// below) so that it always accesses the latest value when called - thus
103150
// avoiding the need to be patched.
104151
if (shouldCache && isMemberExp) {
105-
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
106-
exp.content += `(...args)`
107-
} else {
108-
exp.children.push(`(...args)`)
152+
if (exp.type === NodeTypes.COMPOUND_EXPRESSION) {
153+
exp = unfoldExpression(exp)
109154
}
155+
156+
exp.content = wrapExpressionContent(exp.content)
110157
}
111158
}
112159

0 commit comments

Comments
 (0)