Skip to content

Commit f11dadc

Browse files
committed
refactor(compiler): improve member expression check for v-on & v-model
1 parent 87c3d2e commit f11dadc

File tree

9 files changed

+63
-13
lines changed

9 files changed

+63
-13
lines changed

packages/compiler-core/__tests__/transforms/vModel.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,5 +351,17 @@ describe('compiler: transform v-model', () => {
351351
})
352352
)
353353
})
354+
355+
test('mal-formed expression', () => {
356+
const onError = jest.fn()
357+
parseWithVModel('<span v-model="a + b" />', { onError })
358+
359+
expect(onError).toHaveBeenCalledTimes(1)
360+
expect(onError).toHaveBeenCalledWith(
361+
expect.objectContaining({
362+
code: ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION
363+
})
364+
)
365+
})
354366
})
355367
})

packages/compiler-core/__tests__/transforms/vOn.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,34 @@ describe('compiler: transform v-on', () => {
175175
})
176176
})
177177

178+
test('should NOT wrap as function if expression is complex member expression', () => {
179+
const node = parseWithVOn(`<div @click="a['b' + c]"/>`)
180+
const props = (node.codegenNode as CallExpression)
181+
.arguments[1] as ObjectExpression
182+
expect(props.properties[0]).toMatchObject({
183+
key: { content: `onClick` },
184+
value: {
185+
type: NodeTypes.SIMPLE_EXPRESSION,
186+
content: `a['b' + c]`
187+
}
188+
})
189+
})
190+
191+
test('complex member expression w/ prefixIdentifiers: true', () => {
192+
const node = parseWithVOn(`<div @click="a['b' + c]"/>`, {
193+
prefixIdentifiers: true
194+
})
195+
const props = (node.codegenNode as CallExpression)
196+
.arguments[1] as ObjectExpression
197+
expect(props.properties[0]).toMatchObject({
198+
key: { content: `onClick` },
199+
value: {
200+
type: NodeTypes.COMPOUND_EXPRESSION,
201+
children: [{ content: `_ctx.a` }, `['b' + `, { content: `_ctx.c` }, `]`]
202+
}
203+
})
204+
})
205+
178206
test('function expression w/ prefixIdentifiers: true', () => {
179207
const node = parseWithVOn(`<div @click="e => foo(e)"/>`, {
180208
prefixIdentifiers: true

packages/compiler-core/src/ast.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,11 +519,12 @@ export function createInterpolation(
519519
}
520520

521521
export function createCompoundExpression(
522-
children: CompoundExpressionNode['children']
522+
children: CompoundExpressionNode['children'],
523+
loc: SourceLocation = locStub
523524
): CompoundExpressionNode {
524525
return {
525526
type: NodeTypes.COMPOUND_EXPRESSION,
526-
loc: locStub,
527+
loc,
527528
children
528529
}
529530
}

packages/compiler-core/src/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export const errorMessages: { [code: number]: string } = {
170170
`These children will be ignored.`,
171171
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
172172
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
173-
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model has invalid expression.`,
173+
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
174174

175175
// generic errors
176176
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,

packages/compiler-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
1414
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
1515
import { optimizeText } from './transforms/optimizeText'
1616
import { transformOnce } from './transforms/vOnce'
17+
import { transformModel } from './transforms/vModel'
1718

1819
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
1920

@@ -62,6 +63,7 @@ export function baseCompile(
6263
on: transformOn,
6364
bind: transformBind,
6465
once: transformOnce,
66+
model: transformModel,
6567
...(options.directiveTransforms || {}) // user transforms
6668
}
6769
})

packages/compiler-core/src/transforms/transformExpression.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export function processExpression(
203203

204204
let ret
205205
if (children.length) {
206-
ret = createCompoundExpression(children)
206+
ret = createCompoundExpression(children, node.loc)
207207
} else {
208208
ret = node
209209
}

packages/compiler-core/src/transforms/vModel.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@ import {
77
Property
88
} from '../ast'
99
import { createCompilerError, ErrorCodes } from '../errors'
10-
import { isEmptyExpression } from '../utils'
10+
import { isMemberExpression } from '../utils'
1111

1212
export const transformModel: DirectiveTransform = (dir, node, context) => {
1313
const { exp, arg } = dir
1414
if (!exp) {
15-
context.onError(createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION))
16-
15+
context.onError(
16+
createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION, dir.loc)
17+
)
1718
return createTransformProps()
1819
}
1920

20-
if (isEmptyExpression(exp)) {
21+
const expString =
22+
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc.source
23+
if (!isMemberExpression(expString)) {
2124
context.onError(
22-
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION)
25+
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
2326
)
24-
2527
return createTransformProps()
2628
}
2729

packages/compiler-core/src/transforms/vOn.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
import { capitalize } from '@vue/shared'
1111
import { createCompilerError, ErrorCodes } from '../errors'
1212
import { processExpression } from './transformExpression'
13+
import { isMemberExpression } from '../utils'
1314

1415
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
15-
const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/
1616

1717
// v-on without arg is handled directly in ./element.ts due to it affecting
1818
// codegen for the entire props object. This transform here is only for v-on
@@ -49,7 +49,7 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
4949
// skipped by transformExpression as a special case.
5050
let exp: ExpressionNode = dir.exp as SimpleExpressionNode
5151
const isInlineStatement = !(
52-
simplePathRE.test(exp.content) || fnExpRE.test(exp.content)
52+
isMemberExpression(exp.content) || fnExpRE.test(exp.content)
5353
)
5454
// process the expression since it's been skipped
5555
if (!__BROWSER__ && context.prefixIdentifiers) {

packages/compiler-core/src/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,13 @@ export const walkJS: typeof walk = (ast, walker) => {
6363
return walk(ast, walker)
6464
}
6565

66+
const nonIdentifierRE = /^\d|[^\$\w]/
6667
export const isSimpleIdentifier = (name: string): boolean =>
67-
!/^\d|[^\$\w]/.test(name)
68+
!nonIdentifierRE.test(name)
69+
70+
const memberExpRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])*$/
71+
export const isMemberExpression = (path: string): boolean =>
72+
memberExpRE.test(path)
6873

6974
export function getInnerRange(
7075
loc: SourceLocation,

0 commit comments

Comments
 (0)