Skip to content

Commit d23fde3

Browse files
committed
fix(compiler-core): more robust member expression check when running in node
fix #4640
1 parent 7c3c28e commit d23fde3

File tree

4 files changed

+101
-39
lines changed

4 files changed

+101
-39
lines changed

Diff for: packages/compiler-core/__tests__/utils.spec.ts

+57-35
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { TransformContext } from '../src'
12
import { Position } from '../src/ast'
23
import {
34
getInnerRange,
45
advancePositionWithClone,
5-
isMemberExpression,
6+
isMemberExpressionNode,
7+
isMemberExpressionBrowser,
68
toValidAssetId
79
} from '../src/utils'
810

@@ -73,40 +75,60 @@ describe('getInnerRange', () => {
7375
})
7476
})
7577

76-
test('isMemberExpression', () => {
77-
// should work
78-
expect(isMemberExpression('obj.foo')).toBe(true)
79-
expect(isMemberExpression('obj[foo]')).toBe(true)
80-
expect(isMemberExpression('obj[arr[0]]')).toBe(true)
81-
expect(isMemberExpression('obj[arr[ret.bar]]')).toBe(true)
82-
expect(isMemberExpression('obj[arr[ret[bar]]]')).toBe(true)
83-
expect(isMemberExpression('obj[arr[ret[bar]]].baz')).toBe(true)
84-
expect(isMemberExpression('obj[1 + 1]')).toBe(true)
85-
expect(isMemberExpression(`obj[x[0]]`)).toBe(true)
86-
expect(isMemberExpression('obj[1][2]')).toBe(true)
87-
expect(isMemberExpression('obj[1][2].foo[3].bar.baz')).toBe(true)
88-
expect(isMemberExpression(`a[b[c.d]][0]`)).toBe(true)
89-
expect(isMemberExpression('obj?.foo')).toBe(true)
90-
expect(isMemberExpression('foo().test')).toBe(true)
91-
92-
// strings
93-
expect(isMemberExpression(`a['foo' + bar[baz]["qux"]]`)).toBe(true)
94-
95-
// multiline whitespaces
96-
expect(isMemberExpression('obj \n .foo \n [bar \n + baz]')).toBe(true)
97-
expect(isMemberExpression(`\n model\n.\nfoo \n`)).toBe(true)
98-
99-
// should fail
100-
expect(isMemberExpression('a \n b')).toBe(false)
101-
expect(isMemberExpression('obj[foo')).toBe(false)
102-
expect(isMemberExpression('objfoo]')).toBe(false)
103-
expect(isMemberExpression('obj[arr[0]')).toBe(false)
104-
expect(isMemberExpression('obj[arr0]]')).toBe(false)
105-
expect(isMemberExpression('123[a]')).toBe(false)
106-
expect(isMemberExpression('a + b')).toBe(false)
107-
expect(isMemberExpression('foo()')).toBe(false)
108-
expect(isMemberExpression('a?b:c')).toBe(false)
109-
expect(isMemberExpression(`state['text'] = $event`)).toBe(false)
78+
describe('isMemberExpression', () => {
79+
function commonAssertions(fn: (str: string) => boolean) {
80+
// should work
81+
expect(fn('obj.foo')).toBe(true)
82+
expect(fn('obj[foo]')).toBe(true)
83+
expect(fn('obj[arr[0]]')).toBe(true)
84+
expect(fn('obj[arr[ret.bar]]')).toBe(true)
85+
expect(fn('obj[arr[ret[bar]]]')).toBe(true)
86+
expect(fn('obj[arr[ret[bar]]].baz')).toBe(true)
87+
expect(fn('obj[1 + 1]')).toBe(true)
88+
expect(fn(`obj[x[0]]`)).toBe(true)
89+
expect(fn('obj[1][2]')).toBe(true)
90+
expect(fn('obj[1][2].foo[3].bar.baz')).toBe(true)
91+
expect(fn(`a[b[c.d]][0]`)).toBe(true)
92+
expect(fn('obj?.foo')).toBe(true)
93+
expect(fn('foo().test')).toBe(true)
94+
95+
// strings
96+
expect(fn(`a['foo' + bar[baz]["qux"]]`)).toBe(true)
97+
98+
// multiline whitespaces
99+
expect(fn('obj \n .foo \n [bar \n + baz]')).toBe(true)
100+
expect(fn(`\n model\n.\nfoo \n`)).toBe(true)
101+
102+
// should fail
103+
expect(fn('a \n b')).toBe(false)
104+
expect(fn('obj[foo')).toBe(false)
105+
expect(fn('objfoo]')).toBe(false)
106+
expect(fn('obj[arr[0]')).toBe(false)
107+
expect(fn('obj[arr0]]')).toBe(false)
108+
expect(fn('123[a]')).toBe(false)
109+
expect(fn('a + b')).toBe(false)
110+
expect(fn('foo()')).toBe(false)
111+
expect(fn('a?b:c')).toBe(false)
112+
expect(fn(`state['text'] = $event`)).toBe(false)
113+
}
114+
115+
test('browser', () => {
116+
commonAssertions(isMemberExpressionBrowser)
117+
})
118+
119+
test('node', () => {
120+
const ctx = { expressionPlugins: ['typescript'] } as any as TransformContext
121+
const fn = (str: string) => isMemberExpressionNode(str, ctx)
122+
commonAssertions(fn)
123+
124+
// TS-specific checks
125+
expect(fn('foo as string')).toBe(true)
126+
expect(fn(`foo.bar as string`)).toBe(true)
127+
expect(fn(`foo['bar'] as string`)).toBe(true)
128+
expect(fn(`foo[bar as string]`)).toBe(true)
129+
expect(fn(`foo() as string`)).toBe(false)
130+
expect(fn(`a + b as string`)).toBe(false)
131+
})
110132
})
111133

112134
test('toValidAssetId', () => {

Diff for: packages/compiler-core/src/transforms/vModel.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
4141
bindingType &&
4242
bindingType !== BindingTypes.SETUP_CONST
4343

44-
if (!expString.trim() || (!isMemberExpression(expString) && !maybeRef)) {
44+
if (
45+
!expString.trim() ||
46+
(!isMemberExpression(expString, context) && !maybeRef)
47+
) {
4548
context.onError(
4649
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
4750
)

Diff for: packages/compiler-core/src/transforms/vOn.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const transformOn: DirectiveTransform = (
7373
}
7474
let shouldCache: boolean = context.cacheHandlers && !exp && !context.inVOnce
7575
if (exp) {
76-
const isMemberExp = isMemberExpression(exp.content)
76+
const isMemberExp = isMemberExpression(exp.content, context)
7777
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
7878
const hasMultipleStatements = exp.content.includes(`;`)
7979

Diff for: packages/compiler-core/src/utils.ts

+39-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,16 @@ import {
4242
WITH_MEMO,
4343
OPEN_BLOCK
4444
} from './runtimeHelpers'
45-
import { isString, isObject, hyphenate, extend } from '@vue/shared'
45+
import {
46+
isString,
47+
isObject,
48+
hyphenate,
49+
extend,
50+
babelParserDefaultPlugins
51+
} from '@vue/shared'
4652
import { PropsExpression } from './transforms/transformElement'
53+
import { parseExpression } from '@babel/parser'
54+
import { Expression } from '@babel/types'
4755

4856
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
4957
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
@@ -84,7 +92,7 @@ const whitespaceRE = /\s+[.[]\s*|\s*[.[]\s+/g
8492
* inside square brackets), but it's ok since these are only used on template
8593
* expressions and false positives are invalid expressions in the first place.
8694
*/
87-
export const isMemberExpression = (path: string): boolean => {
95+
export const isMemberExpressionBrowser = (path: string): boolean => {
8896
// remove whitespaces around . or [ first
8997
path = path.trim().replace(whitespaceRE, s => s.trim())
9098

@@ -153,6 +161,35 @@ export const isMemberExpression = (path: string): boolean => {
153161
return !currentOpenBracketCount && !currentOpenParensCount
154162
}
155163

164+
export const isMemberExpressionNode = (
165+
path: string,
166+
context: TransformContext
167+
): boolean => {
168+
path = path.trim()
169+
if (!validFirstIdentCharRE.test(path[0])) {
170+
return false
171+
}
172+
try {
173+
let ret: Expression = parseExpression(path, {
174+
plugins: [...context.expressionPlugins, ...babelParserDefaultPlugins]
175+
})
176+
if (ret.type === 'TSAsExpression' || ret.type === 'TSTypeAssertion') {
177+
ret = ret.expression
178+
}
179+
return (
180+
ret.type === 'MemberExpression' ||
181+
ret.type === 'OptionalMemberExpression' ||
182+
ret.type === 'Identifier'
183+
)
184+
} catch (e) {
185+
return false
186+
}
187+
}
188+
189+
export const isMemberExpression = __BROWSER__
190+
? isMemberExpressionBrowser
191+
: isMemberExpressionNode
192+
156193
export function getInnerRange(
157194
loc: SourceLocation,
158195
offset: number,

0 commit comments

Comments
 (0)