refactor(compiler): improve member expression check for v-on & v-model
This commit is contained in:
parent
87c3d2edae
commit
f11dadc1d2
@ -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
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.`,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user