]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(compiler): improve member expression check for v-on & v-model
authorEvan You <yyx990803@gmail.com>
Thu, 10 Oct 2019 15:15:24 +0000 (11:15 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 10 Oct 2019 15:15:24 +0000 (11:15 -0400)
packages/compiler-core/__tests__/transforms/vModel.spec.ts
packages/compiler-core/__tests__/transforms/vOn.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-core/src/transforms/vModel.ts
packages/compiler-core/src/transforms/vOn.ts
packages/compiler-core/src/utils.ts

index 818076a509215560a57b2db45bdcec1acd896ced..3eed83cce1628a8292f2bafd61af5ae9f6431088 100644 (file)
@@ -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
+        })
+      )
+    })
   })
 })
index 114fd529cfc85447efeac2102d3930881c9653ca..41bd6677043ce60cc47eea6aa805c346f486f57d 100644 (file)
@@ -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
index d422fc59f5f484eacd392d982d02625c462f6327..9da05bb89cfd2a306a66d7f109490291b34df2a1 100644 (file)
@@ -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
   }
 }
index 759f34579d13ba14d2dbce4a3ad3a6d0688f08f9..24b2a107d4fba8d96517dd3c8e8df7fa43a722bf 100644 (file)
@@ -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.`,
index e1c967fece1bda5e8f2d23e46aee9077515e7ce3..9e8343e68299fcff9f7655f887c04d180c8f1422 100644 (file)
@@ -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
     }
   })
index fa3ef6b35ddb66a9b9e27ea5a6aabf1b5980dfcb..e3c2c028b80d52f4b847e33c107058057367d4d6 100644 (file)
@@ -203,7 +203,7 @@ export function processExpression(
 
   let ret
   if (children.length) {
-    ret = createCompoundExpression(children)
+    ret = createCompoundExpression(children, node.loc)
   } else {
     ret = node
   }
index 3be84226932fd4850b3477e54b31bc6c04a88481..812bab7f6555cceb8d7daeff33314efe7b17ed8f 100644 (file)
@@ -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()
   }
 
index 521d37dfc22d6881f09b4d429c57329ac49ec170..d4a21ba98b72524125adfbab37b057493603fb95 100644 (file)
@@ -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) {
index f6f28c684fb280dff5f52daeb438a8fb9eb0a903..0f873e3ac3471940f5bcb553a6437d2e001f4dbc 100644 (file)
@@ -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,