feat(compiler): better warning for invalid expressions in function/browser mode
fix #1266
This commit is contained in:
parent
10bb34bb86
commit
e29f0b3fc2
@ -36,7 +36,9 @@ export function getBaseTransformPreset(
|
|||||||
trackVForSlotScopes,
|
trackVForSlotScopes,
|
||||||
transformExpression
|
transformExpression
|
||||||
]
|
]
|
||||||
: []),
|
: __BROWSER__ && __DEV__
|
||||||
|
? [transformExpression]
|
||||||
|
: []),
|
||||||
transformSlotOutlet,
|
transformSlotOutlet,
|
||||||
transformElement,
|
transformElement,
|
||||||
trackSlotScopes,
|
trackSlotScopes,
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
import { isGloballyWhitelisted, makeMap } from '@vue/shared'
|
import { isGloballyWhitelisted, makeMap } from '@vue/shared'
|
||||||
import { createCompilerError, ErrorCodes } from '../errors'
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
|
import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
|
||||||
|
import { validateBrowserExpression } from '../validateExpression'
|
||||||
|
|
||||||
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
|
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
|
||||||
|
|
||||||
@ -84,6 +85,12 @@ export function processExpression(
|
|||||||
// v-on handler values may contain multiple statements
|
// v-on handler values may contain multiple statements
|
||||||
asRawStatements = false
|
asRawStatements = false
|
||||||
): ExpressionNode {
|
): ExpressionNode {
|
||||||
|
if (__DEV__ && __BROWSER__) {
|
||||||
|
// simple in-browser validation (same logic in 2.x)
|
||||||
|
validateBrowserExpression(node, context, asParams, asRawStatements)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
if (!context.prefixIdentifiers || !node.content.trim()) {
|
if (!context.prefixIdentifiers || !node.content.trim()) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ import {
|
|||||||
FRAGMENT
|
FRAGMENT
|
||||||
} from '../runtimeHelpers'
|
} from '../runtimeHelpers'
|
||||||
import { processExpression } from './transformExpression'
|
import { processExpression } from './transformExpression'
|
||||||
|
import { validateBrowserExpression } from '../validateExpression'
|
||||||
import { PatchFlags, PatchFlagNames } from '@vue/shared'
|
import { PatchFlags, PatchFlagNames } from '@vue/shared'
|
||||||
|
|
||||||
export const transformFor = createStructuralDirectiveTransform(
|
export const transformFor = createStructuralDirectiveTransform(
|
||||||
@ -243,6 +244,9 @@ export function parseForExpression(
|
|||||||
context
|
context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (__DEV__ && __BROWSER__) {
|
||||||
|
validateBrowserExpression(result.source as SimpleExpressionNode, context)
|
||||||
|
}
|
||||||
|
|
||||||
let valueContent = LHS.trim()
|
let valueContent = LHS.trim()
|
||||||
.replace(stripParensRE, '')
|
.replace(stripParensRE, '')
|
||||||
@ -261,6 +265,13 @@ export function parseForExpression(
|
|||||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||||
result.key = processExpression(result.key, context, true)
|
result.key = processExpression(result.key, context, true)
|
||||||
}
|
}
|
||||||
|
if (__DEV__ && __BROWSER__) {
|
||||||
|
validateBrowserExpression(
|
||||||
|
result.key as SimpleExpressionNode,
|
||||||
|
context,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iteratorMatch[2]) {
|
if (iteratorMatch[2]) {
|
||||||
@ -280,6 +291,13 @@ export function parseForExpression(
|
|||||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||||
result.index = processExpression(result.index, context, true)
|
result.index = processExpression(result.index, context, true)
|
||||||
}
|
}
|
||||||
|
if (__DEV__ && __BROWSER__) {
|
||||||
|
validateBrowserExpression(
|
||||||
|
result.index as SimpleExpressionNode,
|
||||||
|
context,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,6 +307,13 @@ export function parseForExpression(
|
|||||||
if (!__BROWSER__ && context.prefixIdentifiers) {
|
if (!__BROWSER__ && context.prefixIdentifiers) {
|
||||||
result.value = processExpression(result.value, context, true)
|
result.value = processExpression(result.value, context, true)
|
||||||
}
|
}
|
||||||
|
if (__DEV__ && __BROWSER__) {
|
||||||
|
validateBrowserExpression(
|
||||||
|
result.value as SimpleExpressionNode,
|
||||||
|
context,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from '../ast'
|
} from '../ast'
|
||||||
import { createCompilerError, ErrorCodes } from '../errors'
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
import { processExpression } from './transformExpression'
|
import { processExpression } from './transformExpression'
|
||||||
|
import { validateBrowserExpression } from '../validateExpression'
|
||||||
import {
|
import {
|
||||||
CREATE_BLOCK,
|
CREATE_BLOCK,
|
||||||
FRAGMENT,
|
FRAGMENT,
|
||||||
@ -93,6 +94,10 @@ export function processIf(
|
|||||||
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
|
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (__DEV__ && __BROWSER__ && dir.exp) {
|
||||||
|
validateBrowserExpression(dir.exp as SimpleExpressionNode, context)
|
||||||
|
}
|
||||||
|
|
||||||
if (dir.name === 'if') {
|
if (dir.name === 'if') {
|
||||||
const branch = createIfBranch(node, dir)
|
const branch = createIfBranch(node, dir)
|
||||||
const ifNode: IfNode = {
|
const ifNode: IfNode = {
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
import { capitalize, camelize } from '@vue/shared'
|
import { capitalize, camelize } from '@vue/shared'
|
||||||
import { createCompilerError, ErrorCodes } from '../errors'
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
import { processExpression } from './transformExpression'
|
import { processExpression } from './transformExpression'
|
||||||
|
import { validateBrowserExpression } from '../validateExpression'
|
||||||
import { isMemberExpression, hasScopeRef } from '../utils'
|
import { isMemberExpression, hasScopeRef } from '../utils'
|
||||||
|
|
||||||
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
|
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
|
||||||
@ -89,6 +90,15 @@ export const transformOn: DirectiveTransform = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (__DEV__ && __BROWSER__) {
|
||||||
|
validateBrowserExpression(
|
||||||
|
exp as SimpleExpressionNode,
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
hasMultipleStatements
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isInlineStatement || (isCacheable && isMemberExp)) {
|
if (isInlineStatement || (isCacheable && isMemberExp)) {
|
||||||
// wrap inline statement in a function expression
|
// wrap inline statement in a function expression
|
||||||
exp = createCompoundExpression([
|
exp = createCompoundExpression([
|
||||||
|
49
packages/compiler-core/src/transforms/validateExpression.ts
Normal file
49
packages/compiler-core/src/transforms/validateExpression.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { NodeTransform, TransformContext } from '../transform'
|
||||||
|
import { NodeTypes, SimpleExpressionNode } from '../ast'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When using the runtime compiler in function mode, some expressions will
|
||||||
|
* become invalid (e.g. using keyworkds like `class` in expressions) so we need
|
||||||
|
* to detect them.
|
||||||
|
*
|
||||||
|
* This transform is browser-only and dev-only.
|
||||||
|
*/
|
||||||
|
export const validateExpression: NodeTransform = (node, context) => {
|
||||||
|
if (node.type === NodeTypes.INTERPOLATION) {
|
||||||
|
validateBrowserExpression(node.content as SimpleExpressionNode, context)
|
||||||
|
} else if (node.type === NodeTypes.ELEMENT) {
|
||||||
|
// handle directives on element
|
||||||
|
for (let i = 0; i < node.props.length; i++) {
|
||||||
|
const dir = node.props[i]
|
||||||
|
// do not process for v-on & v-for since they are special handled
|
||||||
|
if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
|
||||||
|
const exp = dir.exp
|
||||||
|
const arg = dir.arg
|
||||||
|
// do not process exp if this is v-on:arg - we need special handling
|
||||||
|
// for wrapping inline statements.
|
||||||
|
if (
|
||||||
|
exp &&
|
||||||
|
exp.type === NodeTypes.SIMPLE_EXPRESSION &&
|
||||||
|
!(dir.name === 'on' && arg)
|
||||||
|
) {
|
||||||
|
validateBrowserExpression(
|
||||||
|
exp,
|
||||||
|
context,
|
||||||
|
// slot args must be processed as function params
|
||||||
|
dir.name === 'slot'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (arg && arg.type === NodeTypes.SIMPLE_EXPRESSION && !arg.isStatic) {
|
||||||
|
validateBrowserExpression(arg, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateBrowserExpression(
|
||||||
|
node: SimpleExpressionNode,
|
||||||
|
context: TransformContext,
|
||||||
|
asParams = false,
|
||||||
|
asRawStatements = false
|
||||||
|
) {}
|
60
packages/compiler-core/src/validateExpression.ts
Normal file
60
packages/compiler-core/src/validateExpression.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// these keywords should not appear inside expressions, but operators like
|
||||||
|
|
||||||
|
import { SimpleExpressionNode } from './ast'
|
||||||
|
import { TransformContext } from './transform'
|
||||||
|
import { createCompilerError, ErrorCodes } from './errors'
|
||||||
|
|
||||||
|
// typeof, instanceof and in are allowed
|
||||||
|
const prohibitedKeywordRE = new RegExp(
|
||||||
|
'\\b' +
|
||||||
|
(
|
||||||
|
'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' +
|
||||||
|
'super,throw,while,yield,delete,export,import,return,switch,default,' +
|
||||||
|
'extends,finally,continue,debugger,function,arguments,typeof,void'
|
||||||
|
)
|
||||||
|
.split(',')
|
||||||
|
.join('\\b|\\b') +
|
||||||
|
'\\b'
|
||||||
|
)
|
||||||
|
|
||||||
|
// strip strings in expressions
|
||||||
|
const stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a non-prefixed expression.
|
||||||
|
* This is only called when using the in-browser runtime compiler since it
|
||||||
|
* doesn't prefix expressions.
|
||||||
|
*/
|
||||||
|
export function validateBrowserExpression(
|
||||||
|
node: SimpleExpressionNode,
|
||||||
|
context: TransformContext,
|
||||||
|
asParams = false,
|
||||||
|
asRawStatements = false
|
||||||
|
) {
|
||||||
|
const exp = node.content
|
||||||
|
try {
|
||||||
|
new Function(
|
||||||
|
asRawStatements
|
||||||
|
? ` ${exp} `
|
||||||
|
: `return ${asParams ? `(${exp}) => {}` : `(${exp})`}`
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
let message = e.message
|
||||||
|
const keywordMatch = exp
|
||||||
|
.replace(stripStringRE, '')
|
||||||
|
.match(prohibitedKeywordRE)
|
||||||
|
if (keywordMatch) {
|
||||||
|
message = `avoid using JavaScript keyword as property name: "${
|
||||||
|
keywordMatch[0]
|
||||||
|
}"`
|
||||||
|
}
|
||||||
|
context.onError(
|
||||||
|
createCompilerError(
|
||||||
|
ErrorCodes.X_INVALID_EXPRESSION,
|
||||||
|
node.loc,
|
||||||
|
undefined,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user