mirror of https://github.com/vuejs/core.git
fix(compiler-core): use ast-based check for function expressions when possible
close #11615
This commit is contained in:
parent
905c9f16e1
commit
5861229475
|
@ -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)', () => {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue