Skip to content

Commit

Permalink
fix(compiler-core): more robust member expression check when running …
Browse files Browse the repository at this point in the history
…in node

fix #4640
  • Loading branch information
yyx990803 committed Sep 21, 2021
1 parent 7c3c28e commit d23fde3
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 39 deletions.
92 changes: 57 additions & 35 deletions 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'

Expand Down Expand Up @@ -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', () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/compiler-core/src/transforms/vModel.ts
Expand Up @@ -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)
)
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-core/src/transforms/vOn.ts
Expand Up @@ -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(`;`)

Expand Down
41 changes: 39 additions & 2 deletions packages/compiler-core/src/utils.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit d23fde3

Please sign in to comment.