diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap index 62a68ca42..4ea0db55f 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -113,6 +113,21 @@ export function render(_ctx) { }" `; +exports[`cache multiple access > object property name substring cases 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _p = _ctx.p + const _p_title = _p.title + _setProp(n0, "id", _p_title + _p.titles + _p_title) + }) + return n0 +}" +`; + exports[`cache multiple access > optional chaining 1`] = ` "import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; const t0 = _template("
", true) @@ -194,6 +209,20 @@ export function render(_ctx) { }" `; +exports[`cache multiple access > variable name substring edge cases 1`] = ` +"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx) { + const n0 = t0() + _renderEffect(() => { + const _title = _ctx.title + _setProp(n0, "id", _title + _ctx.titles + _title) + }) + return n0 +}" +`; + exports[`compiler v-bind > .attr modifier 1`] = ` "import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue'; const t0 = _template("
", true) diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index c062c96ba..e96186c27 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -785,6 +785,25 @@ describe('cache multiple access', () => { expect(code).contains('_setProp(n0, "id", _obj[1][_ctx.baz] + _obj.bar)') }) + test('variable name substring edge cases', () => { + const { code } = compileWithVBind( + `
`, + ) + expect(code).matchSnapshot() + expect(code).contains('const _title = _ctx.title') + expect(code).contains('_setProp(n0, "id", _title + _ctx.titles + _title)') + }) + + test('object property name substring cases', () => { + const { code } = compileWithVBind( + `
`, + ) + expect(code).matchSnapshot() + expect(code).contains('const _p = _ctx.p') + expect(code).contains('const _p_title = _p.title') + expect(code).contains('_setProp(n0, "id", _p_title + _p.titles + _p_title)') + }) + test('cache variable used in both property shorthand and normal binding', () => { const { code } = compileWithVBind(`
diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts index e2c3c0e17..a8fbc8f83 100644 --- a/packages/compiler-vapor/src/generators/expression.ts +++ b/packages/compiler-vapor/src/generators/expression.ts @@ -283,7 +283,13 @@ export function processExpressions( function analyzeExpressions(expressions: SimpleExpressionNode[]) { const seenVariable: Record = Object.create(null) const variableToExpMap = new Map>() - const expToVariableMap = new Map() + const expToVariableMap = new Map< + SimpleExpressionNode, + Array<{ + name: string + loc?: { start: number; end: number } + }> + >() const seenIdentifier = new Set() const updatedVariable = new Set() @@ -291,6 +297,7 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) { name: string, exp: SimpleExpressionNode, isIdentifier: boolean, + loc?: { start: number; end: number }, parentStack: Node[] = [], ) => { if (isIdentifier) seenIdentifier.add(name) @@ -299,7 +306,11 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) { name, (variableToExpMap.get(name) || new Set()).add(exp), ) - expToVariableMap.set(exp, (expToVariableMap.get(exp) || []).concat(name)) + + const variables = expToVariableMap.get(exp) || [] + variables.push({ name, loc }) + expToVariableMap.set(exp, variables) + if ( parentStack.some( p => p.type === 'UpdateExpression' || p.type === 'AssignmentExpression', @@ -317,12 +328,27 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) { walkIdentifiers(exp.ast, (currentNode, parent, parentStack) => { if (parent && isMemberExpression(parent)) { - const memberExp = extractMemberExpression(parent, name => { - registerVariable(name, exp, true) + const memberExp = extractMemberExpression(parent, id => { + registerVariable(id.name, exp, true, { + start: id.start!, + end: id.end!, + }) }) - registerVariable(memberExp, exp, false, parentStack) + registerVariable( + memberExp, + exp, + false, + { start: parent.start!, end: parent.end! }, + parentStack, + ) } else if (!parentStack.some(isMemberExpression)) { - registerVariable(currentNode.name, exp, true, parentStack) + registerVariable( + currentNode.name, + exp, + true, + { start: currentNode.start!, end: currentNode.end! }, + parentStack, + ) } }) } @@ -340,11 +366,22 @@ function processRepeatedVariables( context: CodegenContext, seenVariable: Record, variableToExpMap: Map>, - expToVariableMap: Map, + expToVariableMap: Map< + SimpleExpressionNode, + Array<{ name: string; loc?: { start: number; end: number } }> + >, seenIdentifier: Set, updatedVariable: Set, ): DeclarationValue[] { const declarations: DeclarationValue[] = [] + const expToReplacementMap = new Map< + SimpleExpressionNode, + Array<{ + name: string + locs: { start: number; end: number }[] + }> + >() + for (const [name, exps] of variableToExpMap) { if (updatedVariable.has(name)) continue if (seenVariable[name] > 1 && exps.size > 0) { @@ -356,12 +393,20 @@ function processRepeatedVariables( // e.g., foo[baz] -> foo_baz. // for identifiers, we don't need to replace the content - they will be // replaced during context.withId(..., ids) - const replaceRE = new RegExp(escapeRegExp(name), 'g') exps.forEach(node => { - if (node.ast) { - node.content = node.content.replace(replaceRE, varName) - // re-parse the expression - node.ast = parseExp(context, node.content) + if (node.ast && varName !== name) { + const replacements = expToReplacementMap.get(node) || [] + replacements.push({ + name: varName, + locs: expToVariableMap.get(node)!.reduce( + (locs, v) => { + if (v.name === name && v.loc) locs.push(v.loc) + return locs + }, + [] as { start: number; end: number }[], + ), + }) + expToReplacementMap.set(node, replacements) } }) @@ -384,15 +429,35 @@ function processRepeatedVariables( } } + for (const [exp, replacements] of expToReplacementMap) { + replacements + .flatMap(({ name, locs }) => + locs.map(({ start, end }) => ({ start, end, name })), + ) + .sort((a, b) => b.end - a.end) + .forEach(({ start, end, name }) => { + exp.content = + exp.content.slice(0, start - 1) + name + exp.content.slice(end - 1) + }) + + // re-parse the expression + exp.ast = parseExp(context, exp.content) + } + return declarations } function shouldDeclareVariable( name: string, - expToVariableMap: Map, + expToVariableMap: Map< + SimpleExpressionNode, + Array<{ name: string; loc?: { start: number; end: number } }> + >, exps: Set, ): boolean { - const vars = Array.from(exps, exp => expToVariableMap.get(exp)!) + const vars = Array.from(exps, exp => + expToVariableMap.get(exp)!.map(v => v.name), + ) // assume name equals to `foo` // if each expression only references `foo`, declaration is needed // to avoid reactivity tracking @@ -439,12 +504,15 @@ function processRepeatedExpressions( expressions: SimpleExpressionNode[], varDeclarations: DeclarationValue[], updatedVariable: Set, - expToVariableMap: Map, + expToVariableMap: Map< + SimpleExpressionNode, + Array<{ name: string; loc?: { start: number; end: number } }> + >, ): DeclarationValue[] { const declarations: DeclarationValue[] = [] const seenExp = expressions.reduce( (acc, exp) => { - const variables = expToVariableMap.get(exp) + const variables = expToVariableMap.get(exp)!.map(v => v.name) // only handle expressions that are not identifiers if ( exp.ast && @@ -572,12 +640,12 @@ function genVarName(exp: string): string { function extractMemberExpression( exp: Node, - onIdentifier: (name: string) => void, + onIdentifier: (id: Identifier) => void, ): string { if (!exp) return '' switch (exp.type) { case 'Identifier': // foo[bar] - onIdentifier(exp.name) + onIdentifier(exp) return exp.name case 'StringLiteral': // foo['bar'] return exp.extra ? (exp.extra.raw as string) : exp.value