diff --git a/packages/compiler-core/__tests__/utils.spec.ts b/packages/compiler-core/__tests__/utils.spec.ts index 3514391b74f..48f1ea5b9ae 100644 --- a/packages/compiler-core/__tests__/utils.spec.ts +++ b/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', () => { diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts index d55c4c617e5..bfd51c60406 100644 --- a/packages/compiler-core/src/transforms/vModel.ts +++ b/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) ) diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index 1815e09bc9c..0a804021d15 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/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(`;`) diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index e3bc04bec69..ee7e6bb132d 100644 --- a/packages/compiler-core/src/utils.ts +++ b/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,