fix(compiler-core): more robust member expression check when running in node
fix #4640
This commit is contained in:
parent
7c3c28eb03
commit
d23fde3d3b
@ -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)
|
||||
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(isMemberExpression(`a['foo' + bar[baz]["qux"]]`)).toBe(true)
|
||||
// strings
|
||||
expect(fn(`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)
|
||||
// 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(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)
|
||||
// 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', () => {
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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(`;`)
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user