]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-core): improve member expression check
authorEvan You <yyx990803@gmail.com>
Wed, 9 Jun 2021 15:57:48 +0000 (11:57 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 9 Jun 2021 15:57:48 +0000 (11:57 -0400)
fix #3910

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

index 4fc4e8d16a529652d66c07541a2da6f1c749817d..f866276b2917b5a8213c89f1cdca44d4dd635dcc 100644 (file)
@@ -81,9 +81,25 @@ test('isMemberExpression', () => {
   expect(isMemberExpression('obj[arr[ret[bar]]]')).toBe(true)
   expect(isMemberExpression('obj[arr[ret[bar]]].baz')).toBe(true)
   expect(isMemberExpression('obj[1 + 1]')).toBe(true)
-  // should warning
+  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)
+
+  // 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)
 })
index 3151c958e312c91cac68780c0fae271d1c731377..011e4c620a9b72418c56a8635cd759dae7ce8a70 100644 (file)
@@ -41,7 +41,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
     bindingType &&
     bindingType !== BindingTypes.SETUP_CONST
 
-  if (!isMemberExpression(expString) && !maybeRef) {
+  if (!expString.trim() || (!isMemberExpression(expString) && !maybeRef)) {
     context.onError(
       createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
     )
index d69446d890908d53f59c2ce10b4ee34fc2628110..de2299a37e8a127aeb8df774bee50f5554a8fbf6 100644 (file)
@@ -56,14 +56,67 @@ const nonIdentifierRE = /^\d|[^\$\w]/
 export const isSimpleIdentifier = (name: string): boolean =>
   !nonIdentifierRE.test(name)
 
-const memberExpRE = /^[A-Za-z_$\xA0-\uFFFF][\w$\xA0-\uFFFF]*(?:\s*\.\s*[A-Za-z_$\xA0-\uFFFF][\w$\xA0-\uFFFF]*|\[(.+)\])*$/
+const enum MemberExpLexState {
+  inMemberExp,
+  inBrackets,
+  inString
+}
+
+const validFirstIdentCharRE = /[A-Za-z_$\xA0-\uFFFF]/
+const validIdentCharRE = /[\.\w$\xA0-\uFFFF]/
+const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g
+
+/**
+ * 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 isMemberExpression = (path: string): boolean => {
-  if (!path) return false
-  const matched = memberExpRE.exec(path.trim())
-  if (!matched) return false
-  if (!matched[1]) return true
-  if (!/[\[\]]/.test(matched[1])) return true
-  return isMemberExpression(matched[1].trim())
+  // remove whitespaces around . or [ first
+  path = path.trim().replace(whitespaceRE, s => s.trim())
+
+  let state = MemberExpLexState.inMemberExp
+  let prevState = MemberExpLexState.inMemberExp
+  let currentOpenBracketCount = 0
+  let currentStringType: "'" | '"' | '`' | null = null
+
+  for (let i = 0; i < path.length; i++) {
+    const char = path.charAt(i)
+    switch (state) {
+      case MemberExpLexState.inMemberExp:
+        if (char === '[') {
+          prevState = state
+          state = MemberExpLexState.inBrackets
+          currentOpenBracketCount++
+        } else if (
+          !(i === 0 ? validFirstIdentCharRE : validIdentCharRE).test(char)
+        ) {
+          return false
+        }
+        break
+      case MemberExpLexState.inBrackets:
+        if (char === `'` || char === `"` || char === '`') {
+          prevState = state
+          state = MemberExpLexState.inString
+          currentStringType = char
+        } else if (char === `[`) {
+          currentOpenBracketCount++
+        } else if (char === `]`) {
+          if (!--currentOpenBracketCount) {
+            state = prevState
+          }
+        }
+        break
+      case MemberExpLexState.inString:
+        if (char === currentStringType) {
+          state = prevState
+          currentStringType = null
+        }
+        break
+    }
+  }
+  return !currentOpenBracketCount
 }
 
 export function getInnerRange(