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 { Position } from '../src/ast'
|
||||||
import {
|
import {
|
||||||
getInnerRange,
|
getInnerRange,
|
||||||
advancePositionWithClone,
|
advancePositionWithClone,
|
||||||
isMemberExpression,
|
isMemberExpressionNode,
|
||||||
|
isMemberExpressionBrowser,
|
||||||
toValidAssetId
|
toValidAssetId
|
||||||
} from '../src/utils'
|
} from '../src/utils'
|
||||||
|
|
||||||
@ -73,40 +75,60 @@ describe('getInnerRange', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('isMemberExpression', () => {
|
describe('isMemberExpression', () => {
|
||||||
// should work
|
function commonAssertions(fn: (str: string) => boolean) {
|
||||||
expect(isMemberExpression('obj.foo')).toBe(true)
|
// should work
|
||||||
expect(isMemberExpression('obj[foo]')).toBe(true)
|
expect(fn('obj.foo')).toBe(true)
|
||||||
expect(isMemberExpression('obj[arr[0]]')).toBe(true)
|
expect(fn('obj[foo]')).toBe(true)
|
||||||
expect(isMemberExpression('obj[arr[ret.bar]]')).toBe(true)
|
expect(fn('obj[arr[0]]')).toBe(true)
|
||||||
expect(isMemberExpression('obj[arr[ret[bar]]]')).toBe(true)
|
expect(fn('obj[arr[ret.bar]]')).toBe(true)
|
||||||
expect(isMemberExpression('obj[arr[ret[bar]]].baz')).toBe(true)
|
expect(fn('obj[arr[ret[bar]]]')).toBe(true)
|
||||||
expect(isMemberExpression('obj[1 + 1]')).toBe(true)
|
expect(fn('obj[arr[ret[bar]]].baz')).toBe(true)
|
||||||
expect(isMemberExpression(`obj[x[0]]`)).toBe(true)
|
expect(fn('obj[1 + 1]')).toBe(true)
|
||||||
expect(isMemberExpression('obj[1][2]')).toBe(true)
|
expect(fn(`obj[x[0]]`)).toBe(true)
|
||||||
expect(isMemberExpression('obj[1][2].foo[3].bar.baz')).toBe(true)
|
expect(fn('obj[1][2]')).toBe(true)
|
||||||
expect(isMemberExpression(`a[b[c.d]][0]`)).toBe(true)
|
expect(fn('obj[1][2].foo[3].bar.baz')).toBe(true)
|
||||||
expect(isMemberExpression('obj?.foo')).toBe(true)
|
expect(fn(`a[b[c.d]][0]`)).toBe(true)
|
||||||
expect(isMemberExpression('foo().test')).toBe(true)
|
expect(fn('obj?.foo')).toBe(true)
|
||||||
|
expect(fn('foo().test')).toBe(true)
|
||||||
|
|
||||||
// strings
|
// strings
|
||||||
expect(isMemberExpression(`a['foo' + bar[baz]["qux"]]`)).toBe(true)
|
expect(fn(`a['foo' + bar[baz]["qux"]]`)).toBe(true)
|
||||||
|
|
||||||
// multiline whitespaces
|
// multiline whitespaces
|
||||||
expect(isMemberExpression('obj \n .foo \n [bar \n + baz]')).toBe(true)
|
expect(fn('obj \n .foo \n [bar \n + baz]')).toBe(true)
|
||||||
expect(isMemberExpression(`\n model\n.\nfoo \n`)).toBe(true)
|
expect(fn(`\n model\n.\nfoo \n`)).toBe(true)
|
||||||
|
|
||||||
// should fail
|
// should fail
|
||||||
expect(isMemberExpression('a \n b')).toBe(false)
|
expect(fn('a \n b')).toBe(false)
|
||||||
expect(isMemberExpression('obj[foo')).toBe(false)
|
expect(fn('obj[foo')).toBe(false)
|
||||||
expect(isMemberExpression('objfoo]')).toBe(false)
|
expect(fn('objfoo]')).toBe(false)
|
||||||
expect(isMemberExpression('obj[arr[0]')).toBe(false)
|
expect(fn('obj[arr[0]')).toBe(false)
|
||||||
expect(isMemberExpression('obj[arr0]]')).toBe(false)
|
expect(fn('obj[arr0]]')).toBe(false)
|
||||||
expect(isMemberExpression('123[a]')).toBe(false)
|
expect(fn('123[a]')).toBe(false)
|
||||||
expect(isMemberExpression('a + b')).toBe(false)
|
expect(fn('a + b')).toBe(false)
|
||||||
expect(isMemberExpression('foo()')).toBe(false)
|
expect(fn('foo()')).toBe(false)
|
||||||
expect(isMemberExpression('a?b:c')).toBe(false)
|
expect(fn('a?b:c')).toBe(false)
|
||||||
expect(isMemberExpression(`state['text'] = $event`)).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', () => {
|
test('toValidAssetId', () => {
|
||||||
|
@ -41,7 +41,10 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
|
|||||||
bindingType &&
|
bindingType &&
|
||||||
bindingType !== BindingTypes.SETUP_CONST
|
bindingType !== BindingTypes.SETUP_CONST
|
||||||
|
|
||||||
if (!expString.trim() || (!isMemberExpression(expString) && !maybeRef)) {
|
if (
|
||||||
|
!expString.trim() ||
|
||||||
|
(!isMemberExpression(expString, context) && !maybeRef)
|
||||||
|
) {
|
||||||
context.onError(
|
context.onError(
|
||||||
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
|
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
|
let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
|
||||||
if (exp) {
|
if (exp) {
|
||||||
const isMemberExp = isMemberExpression(exp.content)
|
const isMemberExp = isMemberExpression(exp.content, context)
|
||||||
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
|
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
|
||||||
const hasMultipleStatements = exp.content.includes(`;`)
|
const hasMultipleStatements = exp.content.includes(`;`)
|
||||||
|
|
||||||
|
@ -42,8 +42,16 @@ import {
|
|||||||
WITH_MEMO,
|
WITH_MEMO,
|
||||||
OPEN_BLOCK
|
OPEN_BLOCK
|
||||||
} from './runtimeHelpers'
|
} 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 { PropsExpression } from './transforms/transformElement'
|
||||||
|
import { parseExpression } from '@babel/parser'
|
||||||
|
import { Expression } from '@babel/types'
|
||||||
|
|
||||||
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
|
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
|
||||||
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
|
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
|
* 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.
|
* 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
|
// remove whitespaces around . or [ first
|
||||||
path = path.trim().replace(whitespaceRE, s => s.trim())
|
path = path.trim().replace(whitespaceRE, s => s.trim())
|
||||||
|
|
||||||
@ -153,6 +161,35 @@ export const isMemberExpression = (path: string): boolean => {
|
|||||||
return !currentOpenBracketCount && !currentOpenParensCount
|
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(
|
export function getInnerRange(
|
||||||
loc: SourceLocation,
|
loc: SourceLocation,
|
||||||
offset: number,
|
offset: number,
|
||||||
|
Loading…
Reference in New Issue
Block a user