refactor(compiler): improve member expression check for v-on & v-model

This commit is contained in:
Evan You 2019-10-10 11:15:24 -04:00
parent 87c3d2edae
commit f11dadc1d2
9 changed files with 63 additions and 13 deletions

View File

@ -351,5 +351,17 @@ describe('compiler: transform v-model', () => {
}) })
) )
}) })
test('mal-formed expression', () => {
const onError = jest.fn()
parseWithVModel('<span v-model="a + b" />', { onError })
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION
})
)
})
}) })
}) })

View File

@ -175,6 +175,34 @@ describe('compiler: transform v-on', () => {
}) })
}) })
test('should NOT wrap as function if expression is complex member expression', () => {
const node = parseWithVOn(`<div @click="a['b' + c]"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `a['b' + c]`
}
})
})
test('complex member expression w/ prefixIdentifiers: true', () => {
const node = parseWithVOn(`<div @click="a['b' + c]"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
key: { content: `onClick` },
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: `_ctx.a` }, `['b' + `, { content: `_ctx.c` }, `]`]
}
})
})
test('function expression w/ prefixIdentifiers: true', () => { test('function expression w/ prefixIdentifiers: true', () => {
const node = parseWithVOn(`<div @click="e => foo(e)"/>`, { const node = parseWithVOn(`<div @click="e => foo(e)"/>`, {
prefixIdentifiers: true prefixIdentifiers: true

View File

@ -519,11 +519,12 @@ export function createInterpolation(
} }
export function createCompoundExpression( export function createCompoundExpression(
children: CompoundExpressionNode['children'] children: CompoundExpressionNode['children'],
loc: SourceLocation = locStub
): CompoundExpressionNode { ): CompoundExpressionNode {
return { return {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
loc: locStub, loc,
children children
} }
} }

View File

@ -170,7 +170,7 @@ export const errorMessages: { [code: number]: string } = {
`These children will be ignored.`, `These children will be ignored.`,
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`, [ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`, [ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model has invalid expression.`, [ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
// generic errors // generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`, [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,

View File

@ -14,6 +14,7 @@ import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot' import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
import { optimizeText } from './transforms/optimizeText' import { optimizeText } from './transforms/optimizeText'
import { transformOnce } from './transforms/vOnce' import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel'
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
@ -62,6 +63,7 @@ export function baseCompile(
on: transformOn, on: transformOn,
bind: transformBind, bind: transformBind,
once: transformOnce, once: transformOnce,
model: transformModel,
...(options.directiveTransforms || {}) // user transforms ...(options.directiveTransforms || {}) // user transforms
} }
}) })

View File

@ -203,7 +203,7 @@ export function processExpression(
let ret let ret
if (children.length) { if (children.length) {
ret = createCompoundExpression(children) ret = createCompoundExpression(children, node.loc)
} else { } else {
ret = node ret = node
} }

View File

@ -7,21 +7,23 @@ import {
Property Property
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { isEmptyExpression } from '../utils' import { isMemberExpression } from '../utils'
export const transformModel: DirectiveTransform = (dir, node, context) => { export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir const { exp, arg } = dir
if (!exp) { if (!exp) {
context.onError(createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION)) context.onError(
createCompilerError(ErrorCodes.X_V_MODEL_NO_EXPRESSION, dir.loc)
)
return createTransformProps() return createTransformProps()
} }
if (isEmptyExpression(exp)) { const expString =
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : exp.loc.source
if (!isMemberExpression(expString)) {
context.onError( context.onError(
createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION) createCompilerError(ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION, exp.loc)
) )
return createTransformProps() return createTransformProps()
} }

View File

@ -10,9 +10,9 @@ import {
import { capitalize } from '@vue/shared' import { capitalize } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { isMemberExpression } from '../utils'
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/ const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/
// v-on without arg is handled directly in ./element.ts due to it affecting // v-on without arg is handled directly in ./element.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-on // 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) => {
// skipped by transformExpression as a special case. // skipped by transformExpression as a special case.
let exp: ExpressionNode = dir.exp as SimpleExpressionNode let exp: ExpressionNode = dir.exp as SimpleExpressionNode
const isInlineStatement = !( const isInlineStatement = !(
simplePathRE.test(exp.content) || fnExpRE.test(exp.content) isMemberExpression(exp.content) || fnExpRE.test(exp.content)
) )
// process the expression since it's been skipped // process the expression since it's been skipped
if (!__BROWSER__ && context.prefixIdentifiers) { if (!__BROWSER__ && context.prefixIdentifiers) {

View File

@ -63,8 +63,13 @@ export const walkJS: typeof walk = (ast, walker) => {
return walk(ast, walker) return walk(ast, walker)
} }
const nonIdentifierRE = /^\d|[^\$\w]/
export const isSimpleIdentifier = (name: string): boolean => export const isSimpleIdentifier = (name: string): boolean =>
!/^\d|[^\$\w]/.test(name) !nonIdentifierRE.test(name)
const memberExpRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])*$/
export const isMemberExpression = (path: string): boolean =>
memberExpRE.test(path)
export function getInnerRange( export function getInnerRange(
loc: SourceLocation, loc: SourceLocation,