Jelajahi Sumber

refactor(compiler): improve member expression check for v-on & v-model

Evan You 6 tahun lalu
induk
melakukan
f11dadc1d2

+ 12 - 0
packages/compiler-core/__tests__/transforms/vModel.spec.ts

@@ -351,5 +351,17 @@ describe('compiler: transform v-model', () => {
         })
       )
     })
+
+    test('mal-formed expression', () => {
+      const onError = jest.fn()
+      parseWithVModel('<span v-model="a + b" />', { onError })
+
+      expect(onError).toHaveBeenCalledTimes(1)
+      expect(onError).toHaveBeenCalledWith(
+        expect.objectContaining({
+          code: ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION
+        })
+      )
+    })
   })
 })

+ 28 - 0
packages/compiler-core/__tests__/transforms/vOn.spec.ts

@@ -175,6 +175,34 @@ describe('compiler: transform v-on', () => {
     })
   })
 
+  test('should NOT wrap as function if expression is complex member expression', () => {
+    const node = parseWithVOn(`<div @click="a['b' + c]"/>`)
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
+    expect(props.properties[0]).toMatchObject({
+      key: { content: `onClick` },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: `a['b' + c]`
+      }
+    })
+  })
+
+  test('complex member expression w/ prefixIdentifiers: true', () => {
+    const node = parseWithVOn(`<div @click="a['b' + c]"/>`, {
+      prefixIdentifiers: true
+    })
+    const props = (node.codegenNode as CallExpression)
+      .arguments[1] as ObjectExpression
+    expect(props.properties[0]).toMatchObject({
+      key: { content: `onClick` },
+      value: {
+        type: NodeTypes.COMPOUND_EXPRESSION,
+        children: [{ content: `_ctx.a` }, `['b' + `, { content: `_ctx.c` }, `]`]
+      }
+    })
+  })
+
   test('function expression w/ prefixIdentifiers: true', () => {
     const node = parseWithVOn(`<div @click="e => foo(e)"/>`, {
       prefixIdentifiers: true

+ 3 - 2
packages/compiler-core/src/ast.ts

@@ -519,11 +519,12 @@ export function createInterpolation(
 }
 
 export function createCompoundExpression(
-  children: CompoundExpressionNode['children']
+  children: CompoundExpressionNode['children'],
+  loc: SourceLocation = locStub
 ): CompoundExpressionNode {
   return {
     type: NodeTypes.COMPOUND_EXPRESSION,
-    loc: locStub,
+    loc,
     children
   }
 }

+ 1 - 1
packages/compiler-core/src/errors.ts

@@ -170,7 +170,7 @@ export const errorMessages: { [code: number]: string } = {
     `These children will be ignored.`,
   [ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
   [ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
-  [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model has invalid expression.`,
+  [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
 
   // generic errors
   [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,

+ 2 - 0
packages/compiler-core/src/index.ts

@@ -14,6 +14,7 @@ import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
 import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
 import { optimizeText } from './transforms/optimizeText'
 import { transformOnce } from './transforms/vOnce'
+import { transformModel } from './transforms/vModel'
 
 export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
 
@@ -62,6 +63,7 @@ export function baseCompile(
       on: transformOn,
       bind: transformBind,
       once: transformOnce,
+      model: transformModel,
       ...(options.directiveTransforms || {}) // user transforms
     }
   })

+ 1 - 1
packages/compiler-core/src/transforms/transformExpression.ts

@@ -203,7 +203,7 @@ export function processExpression(
 
   let ret
   if (children.length) {
-    ret = createCompoundExpression(children)
+    ret = createCompoundExpression(children, node.loc)
   } else {
     ret = node
   }

+ 8 - 6
packages/compiler-core/src/transforms/vModel.ts

@@ -7,21 +7,23 @@ import {
   Property
 } from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
-import { isEmptyExpression } from '../utils'
+import { isMemberExpression } from '../utils'
 
 export const transformModel: DirectiveTransform = (dir, node, context) => {
   const { exp, arg } = dir
   if (!exp) {
-    context.onError(createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION))
-
+    context.onError(
+      createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION, dir.loc)
+    )
     return createTransformProps()
   }
 
-  if (isEmptyExpression(exp)) {
+  const expString =
+    exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc.source
+  if (!isMemberExpression(expString)) {
     context.onError(
-      createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION)
+      createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
     )
-
     return createTransformProps()
   }
 

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

@@ -10,9 +10,9 @@ import {
 import { capitalize } from '@vue/shared'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { processExpression } from './transformExpression'
+import { isMemberExpression } from '../utils'
 
 const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
-const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/
 
 // v-on without arg is handled directly in ./element.ts due to it affecting
 // codegen for the entire props object. This transform here is only for v-on
@@ -49,7 +49,7 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
     // skipped by transformExpression as a special case.
     let exp: ExpressionNode = dir.exp as SimpleExpressionNode
     const isInlineStatement = !(
-      simplePathRE.test(exp.content) || fnExpRE.test(exp.content)
+      isMemberExpression(exp.content) || fnExpRE.test(exp.content)
     )
     // process the expression since it's been skipped
     if (!__BROWSER__ && context.prefixIdentifiers) {

+ 6 - 1
packages/compiler-core/src/utils.ts

@@ -63,8 +63,13 @@ export const walkJS: typeof walk = (ast, walker) => {
   return walk(ast, walker)
 }
 
+const nonIdentifierRE = /^\d|[^\$\w]/
 export const isSimpleIdentifier = (name: string): boolean =>
-  !/^\d|[^\$\w]/.test(name)
+  !nonIdentifierRE.test(name)
+
+const memberExpRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])*$/
+export const isMemberExpression = (path: string): boolean =>
+  memberExpRE.test(path)
 
 export function getInnerRange(
   loc: SourceLocation,