From: Evan You Date: Thu, 19 Nov 2020 00:38:18 +0000 (-0500) Subject: wip: properly handle assignment/update expressions in inline mode X-Git-Tag: v3.0.3~32 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=8567feb2aa91edc35143459fd2b0277572df4458;p=thirdparty%2Fvuejs%2Fcore.git wip: properly handle assignment/update expressions in inline mode --- diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index bb59f4940f..43d019b753 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -83,7 +83,11 @@ export const enum BindingTypes { /** * a const binding that may be a ref. */ - SETUP_CONST_REF = 'setup-const-ref', + SETUP_MAYBE_REF = 'setup-maybe-ref', + /** + * bindings that are guaranteed to be refs + */ + SETUP_REF = 'setup-ref', /** * declared by other options, e.g. computed, inject */ @@ -91,7 +95,7 @@ export const enum BindingTypes { } export interface BindingMetadata { - [key: string]: BindingTypes + [key: string]: BindingTypes | undefined } interface SharedTransformCodegenOptions { diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index e3989a807e..1d4a46a133 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -272,7 +272,8 @@ export function resolveComponentType( } const tagFromSetup = checkType(BindingTypes.SETUP_LET) || - checkType(BindingTypes.SETUP_CONST_REF) + checkType(BindingTypes.SETUP_REF) || + checkType(BindingTypes.SETUP_MAYBE_REF) if (tagFromSetup) { return context.inline ? // setup scope bindings that may be refs need to be unrefed diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 9c33cf67b6..edee2f52ec 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -21,14 +21,22 @@ import { isGloballyWhitelisted, makeMap, babelParserDefaultPlugins, - hasOwn + hasOwn, + isString } from '@vue/shared' import { createCompilerError, ErrorCodes } from '../errors' -import { Node, Function, Identifier, ObjectProperty } from '@babel/types' +import { + Node, + Function, + Identifier, + ObjectProperty, + AssignmentExpression, + UpdateExpression +} from '@babel/types' import { validateBrowserExpression } from '../validateExpression' import { parse } from '@babel/parser' import { walk } from 'estree-walker' -import { UNREF } from '../runtimeHelpers' +import { IS_REF, UNREF } from '../runtimeHelpers' import { BindingTypes } from '../options' const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') @@ -100,28 +108,81 @@ export function processExpression( } const { inline, bindingMetadata } = context - const prefix = (raw: string) => { + const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => { const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw] if (inline) { + const isAssignmentLVal = + parent && parent.type === 'AssignmentExpression' && parent.left === id + const isUpdateArg = + parent && parent.type === 'UpdateExpression' && parent.argument === id // setup inline mode if (type === BindingTypes.SETUP_CONST) { return raw + } else if (type === BindingTypes.SETUP_REF) { + return isAssignmentLVal || isUpdateArg + ? `${raw}.value` + : `${context.helperString(UNREF)}(${raw})` } else if ( - type === BindingTypes.SETUP_CONST_REF || + type === BindingTypes.SETUP_MAYBE_REF || type === BindingTypes.SETUP_LET ) { - return `${context.helperString(UNREF)}(${raw})` + if (isAssignmentLVal) { + if (type === BindingTypes.SETUP_MAYBE_REF) { + // const binding that may or may not be ref + // if it's not a ref, then the assignment doesn't make sense so + // just no-op it + // x = y ---> !isRef(x) ? null : x.value = y + return `!${context.helperString( + IS_REF + )}(${raw}) ? null : ${raw}.value` + } else { + // let binding. + // this is a bit more tricky as we need to cover the case where + // let is a local non-ref value, and we need to replicate the + // right hand side value. + // x = y --> isRef(x) ? x.value = y : x = y + const rVal = (parent as AssignmentExpression).right + const rExp = rawExp.slice(rVal.start! - 1, rVal.end! - 1) + const rExpString = stringifyExpression( + processExpression(createSimpleExpression(rExp, false), context) + ) + return `${context.helperString(IS_REF)}(${raw})${ + context.isTS ? ` //@ts-ignore\n` : `` + } ? ${raw}.value = ${rExpString} : ${raw}` + } + } else if (isUpdateArg) { + // make id replace parent in the code range so the raw update operator + // is removed + id!.start = parent!.start + id!.end = parent!.end + const { prefix: isPrefix, operator } = parent as UpdateExpression + const prefix = isPrefix ? operator : `` + const postfix = isPrefix ? `` : operator + if (type === BindingTypes.SETUP_MAYBE_REF) { + // const binding that may or may not be ref + // if it's not a ref, then the assignment doesn't make sense so + // just no-op it + // x++ ---> !isRef(x) ? null : x.value++ + return `!${context.helperString( + IS_REF + )}(${raw}) ? null : ${prefix}${raw}.value${postfix}` + } else { + // let binding. + // x++ --> isRef(a) ? a.value++ : a++ + return `${context.helperString(IS_REF)}(${raw})${ + context.isTS ? ` //@ts-ignore\n` : `` + } ? ${prefix}${raw}.value${postfix} : ${prefix}${raw}${postfix}` + } + } else { + return `${context.helperString(UNREF)}(${raw})` + } } else if (type === BindingTypes.PROPS) { // use __props which is generated by compileScript so in ts mode // it gets correct type return `__props.${raw}` } } else { - if ( - type === BindingTypes.SETUP_LET || - type === BindingTypes.SETUP_CONST || - type === BindingTypes.SETUP_CONST_REF - ) { + if (type && type.startsWith('setup')) { // setup bindings in non-inline mode return `$setup.${raw}` } else if (type) { @@ -149,7 +210,7 @@ export function processExpression( !isGloballyWhitelisted(rawExp) && !isLiteralWhitelisted(rawExp) ) { - node.content = prefix(rawExp) + node.content = rewriteIdentifier(rawExp) } else if (!context.identifiers[rawExp] && !bailConstant) { // mark node constant for hoisting unless it's referring a scope variable node.isConstant = true @@ -199,7 +260,7 @@ export function processExpression( // we rewrite the value node.prefix = `${node.name}: ` } - node.name = prefix(node.name) + node.name = rewriteIdentifier(node.name, parent, node) ids.push(node) } else if (!isStaticPropertyKey(node, parent)) { // The identifier is considered constant unless it's pointing to a @@ -373,3 +434,15 @@ function shouldPrefix(id: Identifier, parent: Node) { return true } + +function stringifyExpression(exp: ExpressionNode | string): string { + if (isString(exp)) { + return exp + } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { + return exp.content + } else { + return (exp.children as (ExpressionNode | string)[]) + .map(stringifyExpression) + .join('') + } +} diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts index 0274b642e5..7bdffd81c0 100644 --- a/packages/compiler-core/src/transforms/vModel.ts +++ b/packages/compiler-core/src/transforms/vModel.ts @@ -5,7 +5,8 @@ import { createCompoundExpression, NodeTypes, Property, - ElementTypes + ElementTypes, + ExpressionNode } from '../ast' import { createCompilerError, ErrorCodes } from '../errors' import { @@ -14,7 +15,8 @@ import { hasScopeRef, isStaticExp } from '../utils' -import { helperNameMap, IS_REF, UNREF } from '../runtimeHelpers' +import { IS_REF } from '../runtimeHelpers' +import { BindingTypes } from '../options' export const transformModel: DirectiveTransform = (dir, node, context) => { const { exp, arg } = dir @@ -31,10 +33,14 @@ export const transformModel: DirectiveTransform = (dir, node, context) => { // im SFC `, { inlineTemplate: true } ) + // known const ref: set value + expect(content).toMatch(`count.value = $event`) + // const but maybe ref: only assign after check + expect(content).toMatch(`_isRef(maybe) ? maybe.value = $event : null`) + // let: handle both cases + expect(content).toMatch( + `_isRef(lett) ? lett.value = $event : lett = $event` + ) + assertCode(content) + }) + + test('template assignment expression codegen', () => { + const { content } = compile( + ` + + `, + { inlineTemplate: true } + ) + // known const ref: set value + expect(content).toMatch(`count.value = 1`) + // const but maybe ref: only assign after check + expect(content).toMatch( + `!_isRef(maybe) ? null : maybe.value = _unref(count)` + ) + // let: handle both cases + expect(content).toMatch( + `_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)` + ) + assertCode(content) + }) + + test('template update expression codegen', () => { + const { content } = compile( + ` + + `, + { inlineTemplate: true } + ) + // known const ref: set value + expect(content).toMatch(`count.value++`) + expect(content).toMatch(`--count.value`) + // const but maybe ref: only assign after check + expect(content).toMatch(`!_isRef(maybe) ? null : maybe.value++`) + expect(content).toMatch(`!_isRef(maybe) ? null : --maybe.value`) + // let: handle both cases + expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`) + expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`) assertCode(content) }) }) @@ -381,9 +453,9 @@ const { props, emit } = defineOptions({ expect(content).toMatch(`let d`) assertCode(content) expect(bindings).toStrictEqual({ - foo: BindingTypes.SETUP_CONST_REF, - a: BindingTypes.SETUP_CONST_REF, - b: BindingTypes.SETUP_CONST_REF, + foo: BindingTypes.SETUP_REF, + a: BindingTypes.SETUP_REF, + b: BindingTypes.SETUP_REF, c: BindingTypes.SETUP_LET, d: BindingTypes.SETUP_LET }) @@ -403,9 +475,9 @@ const { props, emit } = defineOptions({ expect(content).toMatch(`return { a, b, c }`) assertCode(content) expect(bindings).toStrictEqual({ - a: BindingTypes.SETUP_CONST_REF, - b: BindingTypes.SETUP_CONST_REF, - c: BindingTypes.SETUP_CONST_REF + a: BindingTypes.SETUP_REF, + b: BindingTypes.SETUP_REF, + c: BindingTypes.SETUP_REF }) }) @@ -495,12 +567,12 @@ const { props, emit } = defineOptions({ ) expect(content).toMatch(`return { n, a, c, d, f, g }`) expect(bindings).toStrictEqual({ - n: BindingTypes.SETUP_CONST_REF, - a: BindingTypes.SETUP_CONST_REF, - c: BindingTypes.SETUP_CONST_REF, - d: BindingTypes.SETUP_CONST_REF, - f: BindingTypes.SETUP_CONST_REF, - g: BindingTypes.SETUP_CONST_REF + n: BindingTypes.SETUP_REF, + a: BindingTypes.SETUP_REF, + c: BindingTypes.SETUP_REF, + d: BindingTypes.SETUP_REF, + f: BindingTypes.SETUP_REF, + g: BindingTypes.SETUP_REF }) assertCode(content) }) @@ -519,10 +591,10 @@ const { props, emit } = defineOptions({ expect(content).toMatch(`console.log(n.value, a.value, b.value, c.value)`) expect(content).toMatch(`return { n, a, b, c }`) expect(bindings).toStrictEqual({ - n: BindingTypes.SETUP_CONST_REF, - a: BindingTypes.SETUP_CONST_REF, - b: BindingTypes.SETUP_CONST_REF, - c: BindingTypes.SETUP_CONST_REF + n: BindingTypes.SETUP_REF, + a: BindingTypes.SETUP_REF, + b: BindingTypes.SETUP_REF, + c: BindingTypes.SETUP_REF }) assertCode(content) }) @@ -542,9 +614,9 @@ const { props, emit } = defineOptions({ expect(content).toMatch(`\nconst e = _ref(__e);`) expect(content).toMatch(`return { b, d, e }`) expect(bindings).toStrictEqual({ - b: BindingTypes.SETUP_CONST_REF, - d: BindingTypes.SETUP_CONST_REF, - e: BindingTypes.SETUP_CONST_REF + b: BindingTypes.SETUP_REF, + d: BindingTypes.SETUP_REF, + e: BindingTypes.SETUP_REF }) assertCode(content) }) @@ -728,8 +800,8 @@ describe('SFC analyze `) expect(bindings).toStrictEqual({ - foo: BindingTypes.SETUP_CONST_REF, - bar: BindingTypes.SETUP_CONST_REF + foo: BindingTypes.SETUP_MAYBE_REF, + bar: BindingTypes.SETUP_MAYBE_REF }) }) @@ -748,8 +820,8 @@ describe('SFC analyze `) expect(bindings).toStrictEqual({ - foo: BindingTypes.SETUP_CONST_REF, - bar: BindingTypes.SETUP_CONST_REF + foo: BindingTypes.SETUP_MAYBE_REF, + bar: BindingTypes.SETUP_MAYBE_REF }) }) @@ -867,7 +939,7 @@ describe('SFC analyze `) + expect(bindings).toStrictEqual({ + r: BindingTypes.SETUP_CONST, + a: BindingTypes.SETUP_REF, + b: BindingTypes.SETUP_LET, + c: BindingTypes.SETUP_CONST, + d: BindingTypes.SETUP_MAYBE_REF, + e: BindingTypes.SETUP_LET, foo: BindingTypes.PROPS }) }) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index bdbd0b1d3e..4e1d1288df 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -20,7 +20,8 @@ import { Statement, Expression, LabeledStatement, - TSUnionType + TSUnionType, + CallExpression } from '@babel/types' import { walk } from 'estree-walker' import { RawSourceMap } from 'source-map' @@ -159,6 +160,7 @@ export function compileScript( source: string } > = Object.create(null) + const userImportAlias: Record = Object.create(null) const setupBindings: Record = Object.create(null) const refBindings: Record = Object.create(null) const refIdentifiers: Set = new Set() @@ -220,12 +222,22 @@ export function compileScript( ) } + function registerUserImport( + source: string, + local: string, + imported: string | false + ) { + if (source === 'vue' && imported) { + userImportAlias[imported] = local + } + userImports[local] = { + imported: imported || null, + source + } + } + function processDefineOptions(node: Node): boolean { - if ( - node.type === 'CallExpression' && - node.callee.type === 'Identifier' && - node.callee.name === DEFINE_OPTIONS - ) { + if (isCallOf(node, DEFINE_OPTIONS)) { if (hasOptionsCall) { error(`duplicate ${DEFINE_OPTIONS}() call`, node) } @@ -308,7 +320,7 @@ export function compileScript( if (id.name[0] === '$') { error(`ref variable identifiers cannot start with $.`, id) } - refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_CONST_REF + refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_REF refIdentifiers.add(id) } @@ -409,15 +421,11 @@ export function compileScript( if (node.type === 'ImportDeclaration') { // record imports for dedupe for (const specifier of node.specifiers) { - const name = specifier.local.name const imported = specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name - userImports[name] = { - imported: imported || null, - source: node.source.value - } + registerUserImport(node.source.value, specifier.local.name, imported) } } else if (node.type === 'ExportDefaultDeclaration') { // export default @@ -567,10 +575,7 @@ export function compileScript( error(`different imports aliased to same local name.`, specifier) } } else { - userImports[local] = { - imported: imported || null, - source: node.source.value - } + registerUserImport(source, local, imported) } } if (removed === node.specifiers.length) { @@ -605,7 +610,7 @@ export function compileScript( node.type === 'ClassDeclaration') && !node.declare ) { - walkDeclaration(node, setupBindings) + walkDeclaration(node, setupBindings, userImportAlias) } // Type declarations @@ -783,9 +788,10 @@ export function compileScript( Object.assign(bindingMetadata, analyzeBindingsFromOptions(optionsArg)) } for (const [key, { source }] of Object.entries(userImports)) { - bindingMetadata[key] = source.endsWith('.vue') - ? BindingTypes.SETUP_CONST - : BindingTypes.SETUP_CONST_REF + bindingMetadata[key] = + source.endsWith('.vue') || source === 'vue' + ? BindingTypes.SETUP_CONST + : BindingTypes.SETUP_MAYBE_REF } for (const key in setupBindings) { bindingMetadata[key] = setupBindings[key] @@ -941,32 +947,34 @@ export function compileScript( function walkDeclaration( node: Declaration, - bindings: Record + bindings: Record, + userImportAlias: Record ) { if (node.type === 'VariableDeclaration') { const isConst = node.kind === 'const' // export const foo = ... for (const { id, init } of node.declarations) { - const isUseOptionsCall = !!( - isConst && - init && - init.type === 'CallExpression' && - init.callee.type === 'Identifier' && - init.callee.name === DEFINE_OPTIONS - ) + const isUseOptionsCall = !!(isConst && isCallOf(init, DEFINE_OPTIONS)) if (id.type === 'Identifier') { - bindings[id.name] = + let bindingType + if ( // if a declaration is a const literal, we can mark it so that // the generated render fn code doesn't need to unref() it isUseOptionsCall || (isConst && - init!.type !== 'Identifier' && // const a = b - init!.type !== 'CallExpression' && // const a = ref() - init!.type !== 'MemberExpression') // const a = b.c - ? BindingTypes.SETUP_CONST - : isConst - ? BindingTypes.SETUP_CONST_REF - : BindingTypes.SETUP_LET + canNeverBeRef(init!, userImportAlias['reactive'] || 'reactive')) + ) { + bindingType = BindingTypes.SETUP_CONST + } else if (isConst) { + if (isCallOf(init, userImportAlias['ref'] || 'ref')) { + bindingType = BindingTypes.SETUP_REF + } else { + bindingType = BindingTypes.SETUP_MAYBE_REF + } + } else { + bindingType = BindingTypes.SETUP_LET + } + bindings[id.name] = bindingType } else if (id.type === 'ObjectPattern') { walkObjectPattern(id, bindings, isConst, isUseOptionsCall) } else if (id.type === 'ArrayPattern') { @@ -998,7 +1006,7 @@ function walkObjectPattern( bindings[p.key.name] = isUseOptionsCall ? BindingTypes.SETUP_CONST : isConst - ? BindingTypes.SETUP_CONST_REF + ? BindingTypes.SETUP_MAYBE_REF : BindingTypes.SETUP_LET } else { walkPattern(p.value, bindings, isConst, isUseOptionsCall) @@ -1035,7 +1043,7 @@ function walkPattern( bindings[node.name] = isUseOptionsCall ? BindingTypes.SETUP_CONST : isConst - ? BindingTypes.SETUP_CONST_REF + ? BindingTypes.SETUP_MAYBE_REF : BindingTypes.SETUP_LET } else if (node.type === 'RestElement') { // argument can only be identifer when destructuring @@ -1051,7 +1059,7 @@ function walkPattern( bindings[node.left.name] = isUseOptionsCall ? BindingTypes.SETUP_CONST : isConst - ? BindingTypes.SETUP_CONST_REF + ? BindingTypes.SETUP_MAYBE_REF : BindingTypes.SETUP_LET } else { walkPattern(node.left, bindings, isConst) @@ -1419,6 +1427,43 @@ function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] { return [] } +function isCallOf(node: Node | null, name: string): node is CallExpression { + return !!( + node && + node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.name === name + ) +} + +function canNeverBeRef(node: Node, userReactiveImport: string): boolean { + if (isCallOf(node, userReactiveImport)) { + return true + } + switch (node.type) { + case 'UnaryExpression': + case 'BinaryExpression': + case 'ArrayExpression': + case 'ObjectExpression': + case 'FunctionExpression': + case 'ArrowFunctionExpression': + case 'UpdateExpression': + case 'ClassExpression': + case 'TaggedTemplateExpression': + return true + case 'SequenceExpression': + return canNeverBeRef( + node.expressions[node.expressions.length - 1], + userReactiveImport + ) + default: + if (node.type.endsWith('Literal')) { + return true + } + return false + } +} + /** * Analyze bindings in normal `