fix(compiler-vapor): handle variable name substring edge cases (#13520)
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details

This commit is contained in:
edison 2025-06-26 15:41:25 +08:00 committed by GitHub
parent 66f16ee5db
commit bb4ae25793
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 134 additions and 18 deletions

View File

@ -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("<div></div>", 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("<div></div>", 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("<div></div>", 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("<div></div>", true)

View File

@ -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(
`<div :id="title + titles + title"></div>`,
)
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(
`<div :id="p.title + p.titles + p.title"></div>`,
)
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(`
<div :style="{color}" :id="color"/>

View File

@ -283,7 +283,13 @@ export function processExpressions(
function analyzeExpressions(expressions: SimpleExpressionNode[]) {
const seenVariable: Record<string, number> = Object.create(null)
const variableToExpMap = new Map<string, Set<SimpleExpressionNode>>()
const expToVariableMap = new Map<SimpleExpressionNode, string[]>()
const expToVariableMap = new Map<
SimpleExpressionNode,
Array<{
name: string
loc?: { start: number; end: number }
}>
>()
const seenIdentifier = new Set<string>()
const updatedVariable = new Set<string>()
@ -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<string, number>,
variableToExpMap: Map<string, Set<SimpleExpressionNode>>,
expToVariableMap: Map<SimpleExpressionNode, string[]>,
expToVariableMap: Map<
SimpleExpressionNode,
Array<{ name: string; loc?: { start: number; end: number } }>
>,
seenIdentifier: Set<string>,
updatedVariable: Set<string>,
): 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<SimpleExpressionNode, string[]>,
expToVariableMap: Map<
SimpleExpressionNode,
Array<{ name: string; loc?: { start: number; end: number } }>
>,
exps: Set<SimpleExpressionNode>,
): 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<string>,
expToVariableMap: Map<SimpleExpressionNode, string[]>,
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