]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-core): use ast-based check for function expressions when possible
authorEvan You <evan@vuejs.org>
Thu, 15 Aug 2024 01:58:30 +0000 (09:58 +0800)
committerEvan You <evan@vuejs.org>
Thu, 15 Aug 2024 07:38:09 +0000 (15:38 +0800)
close #11615

packages/compiler-core/__tests__/transforms/vOn.spec.ts
packages/compiler-core/__tests__/utils.spec.ts
packages/compiler-core/src/transforms/vModel.ts
packages/compiler-core/src/transforms/vOn.ts
packages/compiler-core/src/utils.ts

index 27d5027533b37d67abc025512a0645f95e8ea08e..e29b2ed31424e2a98de7a1993b9be357de3f9f93 100644 (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)', () => {
index 506aa86982e9929fd0abf3f164c70b6a38bfb131..2d377a271ac85c7f394a6b6f24da62bbba68cda4 100644 (file)
@@ -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)
index 863b3a7d729b331212ce0eeec0cfd973b16824d5..8237c327a0cd1875c21c773508e495a4b67b3f49 100644 (file)
@@ -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),
     )
index a1631e10db30d3cc9cf225abe88f500a121ad8e2..ed809a2d79fbc7082eba5ed3eb8af5cc73847ff7 100644 (file)
@@ -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
index 561c63578644ea46bb4fea092eddb6c8d60ce962..5cf47f3fac31fae15ad941922d36e15fce986557 100644 (file)
@@ -39,7 +39,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 =>
@@ -77,15 +77,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[] = []
@@ -152,13 +157,20 @@ export const isMemberExpressionBrowser = (path: string): boolean => {
   return !currentOpenBracketCount && !currentOpenParensCount
 }
 
-export const isMemberExpressionNode = __BROWSER__
-  ? (NOOP as any as (path: string, context: TransformContext) => boolean)
-  : (path: string, context: TransformContext): boolean => {
+export const isMemberExpressionNode: (
+  exp: ExpressionNode,
+  context: TransformContext,
+) => boolean = __BROWSER__
+  ? (NOOP as any)
+  : (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' ||
@@ -170,9 +182,52 @@ export const isMemberExpressionNode = __BROWSER__
       }
     }
 
-export const isMemberExpression = __BROWSER__
-  ? isMemberExpressionBrowser
-  : isMemberExpressionNode
+export const isMemberExpression: (
+  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,