feat(compiler): better warning for invalid expressions in function/browser mode

fix #1266
This commit is contained in:
Evan You 2020-06-11 16:31:51 -04:00
parent 10bb34bb86
commit e29f0b3fc2
7 changed files with 159 additions and 1 deletions

View File

@ -36,7 +36,9 @@ export function getBaseTransformPreset(
trackVForSlotScopes, trackVForSlotScopes,
transformExpression transformExpression
] ]
: []), : __BROWSER__ && __DEV__
? [transformExpression]
: []),
transformSlotOutlet, transformSlotOutlet,
transformElement, transformElement,
trackSlotScopes, trackSlotScopes,

View File

@ -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
} }

View File

@ -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

View File

@ -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 = {

View File

@ -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([

View 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
) {}

View 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
)
)
}
}