diff --git a/packages/compiler-core/src/compile.ts b/packages/compiler-core/src/compile.ts index ae296139..e3b6260b 100644 --- a/packages/compiler-core/src/compile.ts +++ b/packages/compiler-core/src/compile.ts @@ -36,7 +36,9 @@ export function getBaseTransformPreset( trackVForSlotScopes, transformExpression ] - : []), + : __BROWSER__ && __DEV__ + ? [transformExpression] + : []), transformSlotOutlet, transformElement, trackSlotScopes, diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 9ff5b802..0d3f145e 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -25,6 +25,7 @@ import { import { isGloballyWhitelisted, makeMap } from '@vue/shared' import { createCompilerError, ErrorCodes } from '../errors' import { Node, Function, Identifier, ObjectProperty } from '@babel/types' +import { validateBrowserExpression } from '../validateExpression' const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') @@ -84,6 +85,12 @@ export function processExpression( // v-on handler values may contain multiple statements asRawStatements = false ): 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()) { return node } diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index a1869ba7..ee5c243a 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -41,6 +41,7 @@ import { FRAGMENT } from '../runtimeHelpers' import { processExpression } from './transformExpression' +import { validateBrowserExpression } from '../validateExpression' import { PatchFlags, PatchFlagNames } from '@vue/shared' export const transformFor = createStructuralDirectiveTransform( @@ -243,6 +244,9 @@ export function parseForExpression( context ) } + if (__DEV__ && __BROWSER__) { + validateBrowserExpression(result.source as SimpleExpressionNode, context) + } let valueContent = LHS.trim() .replace(stripParensRE, '') @@ -261,6 +265,13 @@ export function parseForExpression( if (!__BROWSER__ && context.prefixIdentifiers) { result.key = processExpression(result.key, context, true) } + if (__DEV__ && __BROWSER__) { + validateBrowserExpression( + result.key as SimpleExpressionNode, + context, + true + ) + } } if (iteratorMatch[2]) { @@ -280,6 +291,13 @@ export function parseForExpression( if (!__BROWSER__ && context.prefixIdentifiers) { 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) { result.value = processExpression(result.value, context, true) } + if (__DEV__ && __BROWSER__) { + validateBrowserExpression( + result.value as SimpleExpressionNode, + context, + true + ) + } } return result diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index 61095588..fc6ca5e1 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -22,6 +22,7 @@ import { } from '../ast' import { createCompilerError, ErrorCodes } from '../errors' import { processExpression } from './transformExpression' +import { validateBrowserExpression } from '../validateExpression' import { CREATE_BLOCK, FRAGMENT, @@ -93,6 +94,10 @@ export function processIf( dir.exp = processExpression(dir.exp as SimpleExpressionNode, context) } + if (__DEV__ && __BROWSER__ && dir.exp) { + validateBrowserExpression(dir.exp as SimpleExpressionNode, context) + } + if (dir.name === 'if') { const branch = createIfBranch(node, dir) const ifNode: IfNode = { diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index 019fcbd5..d2458410 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/packages/compiler-core/src/transforms/vOn.ts @@ -11,6 +11,7 @@ import { import { capitalize, camelize } from '@vue/shared' import { createCompilerError, ErrorCodes } from '../errors' import { processExpression } from './transformExpression' +import { validateBrowserExpression } from '../validateExpression' import { isMemberExpression, hasScopeRef } from '../utils' 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)) { // wrap inline statement in a function expression exp = createCompoundExpression([ diff --git a/packages/compiler-core/src/transforms/validateExpression.ts b/packages/compiler-core/src/transforms/validateExpression.ts new file mode 100644 index 00000000..49096254 --- /dev/null +++ b/packages/compiler-core/src/transforms/validateExpression.ts @@ -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 +) {} diff --git a/packages/compiler-core/src/validateExpression.ts b/packages/compiler-core/src/validateExpression.ts new file mode 100644 index 00000000..64f171fc --- /dev/null +++ b/packages/compiler-core/src/validateExpression.ts @@ -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 + ) + ) + } +}