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