fix(compiler-core): use ast-based check for function expressions when possible

close #11615
This commit is contained in:
Evan You 2024-08-15 09:58:30 +08:00
parent 905c9f16e1
commit 5861229475
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
5 changed files with 89 additions and 25 deletions

View File

@ -285,6 +285,21 @@ describe('compiler: transform v-on', () => {
}, },
], ],
}) })
const { node: node2 } = parseWithVOn(
`<div @click="(e: (number | string)[]) => 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)', () => { test('should NOT wrap as function if expression is already function expression (async)', () => {

View File

@ -1,5 +1,5 @@
import type { TransformContext } from '../src' import type { ExpressionNode, TransformContext } from '../src'
import type { Position } from '../src/ast' import { type Position, createSimpleExpression } from '../src/ast'
import { import {
advancePositionWithClone, advancePositionWithClone,
isMemberExpressionBrowser, isMemberExpressionBrowser,
@ -41,7 +41,8 @@ describe('advancePositionWithClone', () => {
}) })
describe('isMemberExpression', () => { describe('isMemberExpression', () => {
function commonAssertions(fn: (str: string) => boolean) { function commonAssertions(raw: (exp: ExpressionNode) => boolean) {
const fn = (str: string) => raw(createSimpleExpression(str))
// should work // should work
expect(fn('obj.foo')).toBe(true) expect(fn('obj.foo')).toBe(true)
expect(fn('obj[foo]')).toBe(true) expect(fn('obj[foo]')).toBe(true)
@ -78,13 +79,16 @@ describe('isMemberExpression', () => {
test('browser', () => { test('browser', () => {
commonAssertions(isMemberExpressionBrowser) commonAssertions(isMemberExpressionBrowser)
expect(isMemberExpressionBrowser('123[a]')).toBe(false) expect(isMemberExpressionBrowser(createSimpleExpression('123[a]'))).toBe(
false,
)
}) })
test('node', () => { test('node', () => {
const ctx = { expressionPlugins: ['typescript'] } as any as TransformContext const ctx = { expressionPlugins: ['typescript'] } as any as TransformContext
const fn = (str: string) => isMemberExpressionNode(str, ctx) const fn = (str: string) =>
commonAssertions(fn) isMemberExpressionNode(createSimpleExpression(str), ctx)
commonAssertions(exp => isMemberExpressionNode(exp, ctx))
// TS-specific checks // TS-specific checks
expect(fn('foo as string')).toBe(true) expect(fn('foo as string')).toBe(true)

View File

@ -55,10 +55,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
bindingType === BindingTypes.SETUP_REF || bindingType === BindingTypes.SETUP_REF ||
bindingType === BindingTypes.SETUP_MAYBE_REF) bindingType === BindingTypes.SETUP_MAYBE_REF)
if ( if (!expString.trim() || (!isMemberExpression(exp, context) && !maybeRef)) {
!expString.trim() ||
(!isMemberExpression(expString, context) && !maybeRef)
) {
context.onError( context.onError(
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc), createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc),
) )

View File

@ -13,12 +13,9 @@ import { camelize, toHandlerKey } from '@vue/shared'
import { ErrorCodes, createCompilerError } from '../errors' import { ErrorCodes, createCompilerError } from '../errors'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { hasScopeRef, isMemberExpression } from '../utils' import { hasScopeRef, isFnExpression, isMemberExpression } from '../utils'
import { TO_HANDLER_KEY } from '../runtimeHelpers' 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 { export interface VOnDirectiveNode extends DirectiveNode {
// v-on without arg is handled directly in ./transformElements.ts due to it affecting // 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 // 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 let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
if (exp) { if (exp) {
const isMemberExp = isMemberExpression(exp.content, context) const isMemberExp = isMemberExpression(exp, context)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content)) const isInlineStatement = !(isMemberExp || isFnExpression(exp, context))
const hasMultipleStatements = exp.content.includes(`;`) const hasMultipleStatements = exp.content.includes(`;`)
// process the expression since it's been skipped // process the expression since it's been skipped

View File

@ -40,7 +40,7 @@ import {
import { NOOP, isObject, isString } from '@vue/shared' import { NOOP, isObject, isString } from '@vue/shared'
import type { PropsExpression } from './transforms/transformElement' import type { PropsExpression } from './transforms/transformElement'
import { parseExpression } from '@babel/parser' import { parseExpression } from '@babel/parser'
import type { Expression } from '@babel/types' import type { Expression, Node } from '@babel/types'
import { unwrapTSNode } from './babelUtils' import { unwrapTSNode } from './babelUtils'
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode => 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 validIdentCharRE = /[\.\?\w$\xA0-\uFFFF]/
const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g 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 * 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 * 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 * 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. * 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 // 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 state = MemberExpLexState.inMemberExp
let stateStack: MemberExpLexState[] = [] let stateStack: MemberExpLexState[] = []
@ -154,15 +159,19 @@ export const isMemberExpressionBrowser = (path: string): boolean => {
} }
export const isMemberExpressionNode: ( export const isMemberExpressionNode: (
path: string, exp: ExpressionNode,
context: TransformContext, context: TransformContext,
) => boolean = __BROWSER__ ) => boolean = __BROWSER__
? (NOOP as any) ? (NOOP as any)
: (path: string, context: TransformContext): boolean => { : (exp, context) => {
try { try {
let ret: Expression = parseExpression(path, { let ret: Node =
plugins: context.expressionPlugins, exp.ast ||
}) parseExpression(getExpSource(exp), {
plugins: context.expressionPlugins
? [...context.expressionPlugins, 'typescript']
: ['typescript'],
})
ret = unwrapTSNode(ret) as Expression ret = unwrapTSNode(ret) as Expression
return ( return (
ret.type === 'MemberExpression' || ret.type === 'MemberExpression' ||
@ -175,10 +184,52 @@ export const isMemberExpressionNode: (
} }
export const isMemberExpression: ( export const isMemberExpression: (
path: string, exp: ExpressionNode,
context: TransformContext, context: TransformContext,
) => boolean = __BROWSER__ ? isMemberExpressionBrowser : isMemberExpressionNode ) => 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( export function advancePositionWithClone(
pos: Position, pos: Position,
source: string, source: string,