]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-core): more robust member expression check when running in node
authorEvan You <yyx990803@gmail.com>
Tue, 21 Sep 2021 16:19:27 +0000 (12:19 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 21 Sep 2021 16:19:27 +0000 (12:19 -0400)
fix #4640

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 3514391b74ff59fc689782a0c58a20b74fe2e629..48f1ea5b9ae6eaf799f1abc0d0d5aa92e906e047 100644 (file)
@@ -1,8 +1,10 @@
+import { TransformContext } from '../src'
 import { Position } from '../src/ast'
 import {
   getInnerRange,
   advancePositionWithClone,
-  isMemberExpression,
+  isMemberExpressionNode,
+  isMemberExpressionBrowser,
   toValidAssetId
 } from '../src/utils'
 
@@ -73,40 +75,60 @@ describe('getInnerRange', () => {
   })
 })
 
-test('isMemberExpression', () => {
-  // should work
-  expect(isMemberExpression('obj.foo')).toBe(true)
-  expect(isMemberExpression('obj[foo]')).toBe(true)
-  expect(isMemberExpression('obj[arr[0]]')).toBe(true)
-  expect(isMemberExpression('obj[arr[ret.bar]]')).toBe(true)
-  expect(isMemberExpression('obj[arr[ret[bar]]]')).toBe(true)
-  expect(isMemberExpression('obj[arr[ret[bar]]].baz')).toBe(true)
-  expect(isMemberExpression('obj[1 + 1]')).toBe(true)
-  expect(isMemberExpression(`obj[x[0]]`)).toBe(true)
-  expect(isMemberExpression('obj[1][2]')).toBe(true)
-  expect(isMemberExpression('obj[1][2].foo[3].bar.baz')).toBe(true)
-  expect(isMemberExpression(`a[b[c.d]][0]`)).toBe(true)
-  expect(isMemberExpression('obj?.foo')).toBe(true)
-  expect(isMemberExpression('foo().test')).toBe(true)
-
-  // strings
-  expect(isMemberExpression(`a['foo' + bar[baz]["qux"]]`)).toBe(true)
-
-  // multiline whitespaces
-  expect(isMemberExpression('obj \n .foo \n [bar \n + baz]')).toBe(true)
-  expect(isMemberExpression(`\n model\n.\nfoo \n`)).toBe(true)
-
-  // should fail
-  expect(isMemberExpression('a \n b')).toBe(false)
-  expect(isMemberExpression('obj[foo')).toBe(false)
-  expect(isMemberExpression('objfoo]')).toBe(false)
-  expect(isMemberExpression('obj[arr[0]')).toBe(false)
-  expect(isMemberExpression('obj[arr0]]')).toBe(false)
-  expect(isMemberExpression('123[a]')).toBe(false)
-  expect(isMemberExpression('a + b')).toBe(false)
-  expect(isMemberExpression('foo()')).toBe(false)
-  expect(isMemberExpression('a?b:c')).toBe(false)
-  expect(isMemberExpression(`state['text'] = $event`)).toBe(false)
+describe('isMemberExpression', () => {
+  function commonAssertions(fn: (str: string) => boolean) {
+    // should work
+    expect(fn('obj.foo')).toBe(true)
+    expect(fn('obj[foo]')).toBe(true)
+    expect(fn('obj[arr[0]]')).toBe(true)
+    expect(fn('obj[arr[ret.bar]]')).toBe(true)
+    expect(fn('obj[arr[ret[bar]]]')).toBe(true)
+    expect(fn('obj[arr[ret[bar]]].baz')).toBe(true)
+    expect(fn('obj[1 + 1]')).toBe(true)
+    expect(fn(`obj[x[0]]`)).toBe(true)
+    expect(fn('obj[1][2]')).toBe(true)
+    expect(fn('obj[1][2].foo[3].bar.baz')).toBe(true)
+    expect(fn(`a[b[c.d]][0]`)).toBe(true)
+    expect(fn('obj?.foo')).toBe(true)
+    expect(fn('foo().test')).toBe(true)
+
+    // strings
+    expect(fn(`a['foo' + bar[baz]["qux"]]`)).toBe(true)
+
+    // multiline whitespaces
+    expect(fn('obj \n .foo \n [bar \n + baz]')).toBe(true)
+    expect(fn(`\n model\n.\nfoo \n`)).toBe(true)
+
+    // should fail
+    expect(fn('a \n b')).toBe(false)
+    expect(fn('obj[foo')).toBe(false)
+    expect(fn('objfoo]')).toBe(false)
+    expect(fn('obj[arr[0]')).toBe(false)
+    expect(fn('obj[arr0]]')).toBe(false)
+    expect(fn('123[a]')).toBe(false)
+    expect(fn('a + b')).toBe(false)
+    expect(fn('foo()')).toBe(false)
+    expect(fn('a?b:c')).toBe(false)
+    expect(fn(`state['text'] = $event`)).toBe(false)
+  }
+
+  test('browser', () => {
+    commonAssertions(isMemberExpressionBrowser)
+  })
+
+  test('node', () => {
+    const ctx = { expressionPlugins: ['typescript'] } as any as TransformContext
+    const fn = (str: string) => isMemberExpressionNode(str, ctx)
+    commonAssertions(fn)
+
+    // TS-specific checks
+    expect(fn('foo as string')).toBe(true)
+    expect(fn(`foo.bar as string`)).toBe(true)
+    expect(fn(`foo['bar'] as string`)).toBe(true)
+    expect(fn(`foo[bar as string]`)).toBe(true)
+    expect(fn(`foo() as string`)).toBe(false)
+    expect(fn(`a + b as string`)).toBe(false)
+  })
 })
 
 test('toValidAssetId', () => {
index d55c4c617e5ffbf971dc48f9eef8f7253a50f0a7..bfd51c604063a269112001381877ad955e97ddc0 100644 (file)
@@ -41,7 +41,10 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
     bindingType &&
     bindingType !== BindingTypes.SETUP_CONST
 
-  if (!expString.trim() || (!isMemberExpression(expString) && !maybeRef)) {
+  if (
+    !expString.trim() ||
+    (!isMemberExpression(expString, context) && !maybeRef)
+  ) {
     context.onError(
       createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
     )
index 1815e09bc9c13f5454b19080e740caa9dbca81ae..0a804021d15540fb833a760f23384d9481349c17 100644 (file)
@@ -73,7 +73,7 @@ export const transformOn: DirectiveTransform = (
   }
   let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
   if (exp) {
-    const isMemberExp = isMemberExpression(exp.content)
+    const isMemberExp = isMemberExpression(exp.content, context)
     const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
     const hasMultipleStatements = exp.content.includes(`;`)
 
index e3bc04bec69262aa2efc69841ae5824dbd4412cb..ee7e6bb132dad2a638cc5e722c6b5aaaf84c9693 100644 (file)
@@ -42,8 +42,16 @@ import {
   WITH_MEMO,
   OPEN_BLOCK
 } from './runtimeHelpers'
-import { isString, isObject, hyphenate, extend } from '@vue/shared'
+import {
+  isString,
+  isObject,
+  hyphenate,
+  extend,
+  babelParserDefaultPlugins
+} from '@vue/shared'
 import { PropsExpression } from './transforms/transformElement'
+import { parseExpression } from '@babel/parser'
+import { Expression } from '@babel/types'
 
 export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
   p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
@@ -84,7 +92,7 @@ const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g
  * 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 isMemberExpression = (path: string): boolean => {
+export const isMemberExpressionBrowser = (path: string): boolean => {
   // remove whitespaces around . or [ first
   path = path.trim().replace(whitespaceRE, s => s.trim())
 
@@ -153,6 +161,35 @@ export const isMemberExpression = (path: string): boolean => {
   return !currentOpenBracketCount && !currentOpenParensCount
 }
 
+export const isMemberExpressionNode = (
+  path: string,
+  context: TransformContext
+): boolean => {
+  path = path.trim()
+  if (!validFirstIdentCharRE.test(path[0])) {
+    return false
+  }
+  try {
+    let ret: Expression = parseExpression(path, {
+      plugins: [...context.expressionPlugins, ...babelParserDefaultPlugins]
+    })
+    if (ret.type === 'TSAsExpression' || ret.type === 'TSTypeAssertion') {
+      ret = ret.expression
+    }
+    return (
+      ret.type === 'MemberExpression' ||
+      ret.type === 'OptionalMemberExpression' ||
+      ret.type === 'Identifier'
+    )
+  } catch (e) {
+    return false
+  }
+}
+
+export const isMemberExpression = __BROWSER__
+  ? isMemberExpressionBrowser
+  : isMemberExpressionNode
+
 export function getInnerRange(
   loc: SourceLocation,
   offset: number,