diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts
index 66097f29f0f..9fda0259585 100644
--- a/packages/compiler-core/__tests__/transforms/vOn.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vOn.spec.ts
@@ -285,6 +285,21 @@ describe('compiler: transform v-on', () => {
},
],
})
+
+ const { node: node2 } = parseWithVOn(
+ `
foo(e)"/>`,
+ )
+ expect((node2.codegenNode as VNodeCall).props).toMatchObject({
+ properties: [
+ {
+ key: { content: `onClick` },
+ value: {
+ type: NodeTypes.SIMPLE_EXPRESSION,
+ content: `(e: (number | string)[]) => foo(e)`,
+ },
+ },
+ ],
+ })
})
test('should NOT wrap as function if expression is already function expression (async)', () => {
diff --git a/packages/compiler-core/__tests__/utils.spec.ts b/packages/compiler-core/__tests__/utils.spec.ts
index 506aa86982e..2d377a271ac 100644
--- a/packages/compiler-core/__tests__/utils.spec.ts
+++ b/packages/compiler-core/__tests__/utils.spec.ts
@@ -1,5 +1,5 @@
-import type { TransformContext } from '../src'
-import type { Position } from '../src/ast'
+import type { ExpressionNode, TransformContext } from '../src'
+import { type Position, createSimpleExpression } from '../src/ast'
import {
advancePositionWithClone,
isMemberExpressionBrowser,
@@ -41,7 +41,8 @@ describe('advancePositionWithClone', () => {
})
describe('isMemberExpression', () => {
- function commonAssertions(fn: (str: string) => boolean) {
+ function commonAssertions(raw: (exp: ExpressionNode) => boolean) {
+ const fn = (str: string) => raw(createSimpleExpression(str))
// should work
expect(fn('obj.foo')).toBe(true)
expect(fn('obj[foo]')).toBe(true)
@@ -78,13 +79,16 @@ describe('isMemberExpression', () => {
test('browser', () => {
commonAssertions(isMemberExpressionBrowser)
- expect(isMemberExpressionBrowser('123[a]')).toBe(false)
+ expect(isMemberExpressionBrowser(createSimpleExpression('123[a]'))).toBe(
+ false,
+ )
})
test('node', () => {
const ctx = { expressionPlugins: ['typescript'] } as any as TransformContext
- const fn = (str: string) => isMemberExpressionNode(str, ctx)
- commonAssertions(fn)
+ const fn = (str: string) =>
+ isMemberExpressionNode(createSimpleExpression(str), ctx)
+ commonAssertions(exp => isMemberExpressionNode(exp, ctx))
// TS-specific checks
expect(fn('foo as string')).toBe(true)
diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts
index 88be6238a4d..60c7dc63bd1 100644
--- a/packages/compiler-core/src/transforms/vModel.ts
+++ b/packages/compiler-core/src/transforms/vModel.ts
@@ -55,10 +55,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
bindingType === BindingTypes.SETUP_REF ||
bindingType === BindingTypes.SETUP_MAYBE_REF)
- if (
- !expString.trim() ||
- (!isMemberExpression(expString, context) && !maybeRef)
- ) {
+ if (!expString.trim() || (!isMemberExpression(exp, context) && !maybeRef)) {
context.onError(
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc),
)
diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts
index a1631e10db3..ed809a2d79f 100644
--- a/packages/compiler-core/src/transforms/vOn.ts
+++ b/packages/compiler-core/src/transforms/vOn.ts
@@ -13,12 +13,9 @@ import { camelize, toHandlerKey } from '@vue/shared'
import { ErrorCodes, createCompilerError } from '../errors'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
-import { hasScopeRef, isMemberExpression } from '../utils'
+import { hasScopeRef, isFnExpression, isMemberExpression } from '../utils'
import { TO_HANDLER_KEY } from '../runtimeHelpers'
-const fnExpRE =
- /^\s*(async\s*)?(\([^)]*?\)|[\w$_]+)\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
-
export interface VOnDirectiveNode extends DirectiveNode {
// v-on without arg is handled directly in ./transformElements.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-on
@@ -84,8 +81,8 @@ export const transformOn: DirectiveTransform = (
}
let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
if (exp) {
- const isMemberExp = isMemberExpression(exp.content, context)
- const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
+ const isMemberExp = isMemberExpression(exp, context)
+ const isInlineStatement = !(isMemberExp || isFnExpression(exp, context))
const hasMultipleStatements = exp.content.includes(`;`)
// process the expression since it's been skipped
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index 54a3a845737..5b29a117c14 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -40,7 +40,7 @@ import {
import { NOOP, isObject, isString } from '@vue/shared'
import type { PropsExpression } from './transforms/transformElement'
import { parseExpression } from '@babel/parser'
-import type { Expression } from '@babel/types'
+import type { Expression, Node } from '@babel/types'
import { unwrapTSNode } from './babelUtils'
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
@@ -78,15 +78,20 @@ const validFirstIdentCharRE = /[A-Za-z_$\xA0-\uFFFF]/
const validIdentCharRE = /[\.\?\w$\xA0-\uFFFF]/
const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g
+const getExpSource = (exp: ExpressionNode): string =>
+ exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc.source
+
/**
* Simple lexer to check if an expression is a member expression. This is
* lax and only checks validity at the root level (i.e. does not validate exps
* inside square brackets), but it's ok since these are only used on template
* expressions and false positives are invalid expressions in the first place.
*/
-export const isMemberExpressionBrowser = (path: string): boolean => {
+export const isMemberExpressionBrowser = (exp: ExpressionNode): boolean => {
// remove whitespaces around . or [ first
- path = path.trim().replace(whitespaceRE, s => s.trim())
+ const path = getExpSource(exp)
+ .trim()
+ .replace(whitespaceRE, s => s.trim())
let state = MemberExpLexState.inMemberExp
let stateStack: MemberExpLexState[] = []
@@ -154,15 +159,19 @@ export const isMemberExpressionBrowser = (path: string): boolean => {
}
export const isMemberExpressionNode: (
- path: string,
+ exp: ExpressionNode,
context: TransformContext,
) => boolean = __BROWSER__
? (NOOP as any)
- : (path: string, context: TransformContext): boolean => {
+ : (exp, context) => {
try {
- let ret: Expression = parseExpression(path, {
- plugins: context.expressionPlugins,
- })
+ let ret: Node =
+ exp.ast ||
+ parseExpression(getExpSource(exp), {
+ plugins: context.expressionPlugins
+ ? [...context.expressionPlugins, 'typescript']
+ : ['typescript'],
+ })
ret = unwrapTSNode(ret) as Expression
return (
ret.type === 'MemberExpression' ||
@@ -175,10 +184,52 @@ export const isMemberExpressionNode: (
}
export const isMemberExpression: (
- path: string,
+ exp: ExpressionNode,
context: TransformContext,
) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode
+const fnExpRE =
+ /^\s*(async\s*)?(\([^)]*?\)|[\w$_]+)\s*(:[^=]+)?=>|^\s*(async\s+)?function(?:\s+[\w$]+)?\s*\(/
+
+export const isFnExpressionBrowser: (exp: ExpressionNode) => boolean = exp =>
+ fnExpRE.test(getExpSource(exp))
+
+export const isFnExpressionNode: (
+ exp: ExpressionNode,
+ context: TransformContext,
+) => boolean = __BROWSER__
+ ? (NOOP as any)
+ : (exp, context) => {
+ try {
+ let ret: Node =
+ exp.ast ||
+ parseExpression(getExpSource(exp), {
+ plugins: context.expressionPlugins
+ ? [...context.expressionPlugins, 'typescript']
+ : ['typescript'],
+ })
+ // parser may parse the exp as statements when it contains semicolons
+ if (ret.type === 'Program') {
+ ret = ret.body[0]
+ if (ret.type === 'ExpressionStatement') {
+ ret = ret.expression
+ }
+ }
+ ret = unwrapTSNode(ret) as Expression
+ return (
+ ret.type === 'FunctionExpression' ||
+ ret.type === 'ArrowFunctionExpression'
+ )
+ } catch (e) {
+ return false
+ }
+ }
+
+export const isFnExpression: (
+ exp: ExpressionNode,
+ context: TransformContext,
+) => boolean = __BROWSER__ ? isFnExpressionBrowser : isFnExpressionNode
+
export function advancePositionWithClone(
pos: Position,
source: string,