Răsfoiți Sursa

fix(compiler-core): more robust member expression check when running in node

fix #4640
Evan You 4 ani în urmă
părinte
comite
d23fde3d3b

+ 57 - 35
packages/compiler-core/__tests__/utils.spec.ts

@@ -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', () => {

+ 4 - 1
packages/compiler-core/src/transforms/vModel.ts

@@ -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)
     )

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

@@ -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(`;`)
 

+ 39 - 2
packages/compiler-core/src/utils.ts

@@ -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,