mirror of https://github.com/vuejs/core.git
fix(compiler-vapor): handle variable name substring edge cases (#13520)
This commit is contained in:
parent
66f16ee5db
commit
bb4ae25793
|
@ -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`] = `
|
exports[`cache multiple access > optional chaining 1`] = `
|
||||||
"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
|
"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||||
const t0 = _template("<div></div>", true)
|
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`] = `
|
exports[`compiler v-bind > .attr modifier 1`] = `
|
||||||
"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
|
"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||||
const t0 = _template("<div></div>", true)
|
const t0 = _template("<div></div>", true)
|
||||||
|
|
|
@ -785,6 +785,25 @@ describe('cache multiple access', () => {
|
||||||
expect(code).contains('_setProp(n0, "id", _obj[1][_ctx.baz] + _obj.bar)')
|
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', () => {
|
test('cache variable used in both property shorthand and normal binding', () => {
|
||||||
const { code } = compileWithVBind(`
|
const { code } = compileWithVBind(`
|
||||||
<div :style="{color}" :id="color"/>
|
<div :style="{color}" :id="color"/>
|
||||||
|
|
|
@ -283,7 +283,13 @@ export function processExpressions(
|
||||||
function analyzeExpressions(expressions: SimpleExpressionNode[]) {
|
function analyzeExpressions(expressions: SimpleExpressionNode[]) {
|
||||||
const seenVariable: Record<string, number> = Object.create(null)
|
const seenVariable: Record<string, number> = Object.create(null)
|
||||||
const variableToExpMap = new Map<string, Set<SimpleExpressionNode>>()
|
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 seenIdentifier = new Set<string>()
|
||||||
const updatedVariable = new Set<string>()
|
const updatedVariable = new Set<string>()
|
||||||
|
|
||||||
|
@ -291,6 +297,7 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
|
||||||
name: string,
|
name: string,
|
||||||
exp: SimpleExpressionNode,
|
exp: SimpleExpressionNode,
|
||||||
isIdentifier: boolean,
|
isIdentifier: boolean,
|
||||||
|
loc?: { start: number; end: number },
|
||||||
parentStack: Node[] = [],
|
parentStack: Node[] = [],
|
||||||
) => {
|
) => {
|
||||||
if (isIdentifier) seenIdentifier.add(name)
|
if (isIdentifier) seenIdentifier.add(name)
|
||||||
|
@ -299,7 +306,11 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
|
||||||
name,
|
name,
|
||||||
(variableToExpMap.get(name) || new Set()).add(exp),
|
(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 (
|
if (
|
||||||
parentStack.some(
|
parentStack.some(
|
||||||
p => p.type === 'UpdateExpression' || p.type === 'AssignmentExpression',
|
p => p.type === 'UpdateExpression' || p.type === 'AssignmentExpression',
|
||||||
|
@ -317,12 +328,27 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
|
||||||
|
|
||||||
walkIdentifiers(exp.ast, (currentNode, parent, parentStack) => {
|
walkIdentifiers(exp.ast, (currentNode, parent, parentStack) => {
|
||||||
if (parent && isMemberExpression(parent)) {
|
if (parent && isMemberExpression(parent)) {
|
||||||
const memberExp = extractMemberExpression(parent, name => {
|
const memberExp = extractMemberExpression(parent, id => {
|
||||||
registerVariable(name, exp, true)
|
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)) {
|
} 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,
|
context: CodegenContext,
|
||||||
seenVariable: Record<string, number>,
|
seenVariable: Record<string, number>,
|
||||||
variableToExpMap: Map<string, Set<SimpleExpressionNode>>,
|
variableToExpMap: Map<string, Set<SimpleExpressionNode>>,
|
||||||
expToVariableMap: Map<SimpleExpressionNode, string[]>,
|
expToVariableMap: Map<
|
||||||
|
SimpleExpressionNode,
|
||||||
|
Array<{ name: string; loc?: { start: number; end: number } }>
|
||||||
|
>,
|
||||||
seenIdentifier: Set<string>,
|
seenIdentifier: Set<string>,
|
||||||
updatedVariable: Set<string>,
|
updatedVariable: Set<string>,
|
||||||
): DeclarationValue[] {
|
): DeclarationValue[] {
|
||||||
const declarations: DeclarationValue[] = []
|
const declarations: DeclarationValue[] = []
|
||||||
|
const expToReplacementMap = new Map<
|
||||||
|
SimpleExpressionNode,
|
||||||
|
Array<{
|
||||||
|
name: string
|
||||||
|
locs: { start: number; end: number }[]
|
||||||
|
}>
|
||||||
|
>()
|
||||||
|
|
||||||
for (const [name, exps] of variableToExpMap) {
|
for (const [name, exps] of variableToExpMap) {
|
||||||
if (updatedVariable.has(name)) continue
|
if (updatedVariable.has(name)) continue
|
||||||
if (seenVariable[name] > 1 && exps.size > 0) {
|
if (seenVariable[name] > 1 && exps.size > 0) {
|
||||||
|
@ -356,12 +393,20 @@ function processRepeatedVariables(
|
||||||
// e.g., foo[baz] -> foo_baz.
|
// e.g., foo[baz] -> foo_baz.
|
||||||
// for identifiers, we don't need to replace the content - they will be
|
// for identifiers, we don't need to replace the content - they will be
|
||||||
// replaced during context.withId(..., ids)
|
// replaced during context.withId(..., ids)
|
||||||
const replaceRE = new RegExp(escapeRegExp(name), 'g')
|
|
||||||
exps.forEach(node => {
|
exps.forEach(node => {
|
||||||
if (node.ast) {
|
if (node.ast && varName !== name) {
|
||||||
node.content = node.content.replace(replaceRE, varName)
|
const replacements = expToReplacementMap.get(node) || []
|
||||||
// re-parse the expression
|
replacements.push({
|
||||||
node.ast = parseExp(context, node.content)
|
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
|
return declarations
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldDeclareVariable(
|
function shouldDeclareVariable(
|
||||||
name: string,
|
name: string,
|
||||||
expToVariableMap: Map<SimpleExpressionNode, string[]>,
|
expToVariableMap: Map<
|
||||||
|
SimpleExpressionNode,
|
||||||
|
Array<{ name: string; loc?: { start: number; end: number } }>
|
||||||
|
>,
|
||||||
exps: Set<SimpleExpressionNode>,
|
exps: Set<SimpleExpressionNode>,
|
||||||
): boolean {
|
): 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`
|
// assume name equals to `foo`
|
||||||
// if each expression only references `foo`, declaration is needed
|
// if each expression only references `foo`, declaration is needed
|
||||||
// to avoid reactivity tracking
|
// to avoid reactivity tracking
|
||||||
|
@ -439,12 +504,15 @@ function processRepeatedExpressions(
|
||||||
expressions: SimpleExpressionNode[],
|
expressions: SimpleExpressionNode[],
|
||||||
varDeclarations: DeclarationValue[],
|
varDeclarations: DeclarationValue[],
|
||||||
updatedVariable: Set<string>,
|
updatedVariable: Set<string>,
|
||||||
expToVariableMap: Map<SimpleExpressionNode, string[]>,
|
expToVariableMap: Map<
|
||||||
|
SimpleExpressionNode,
|
||||||
|
Array<{ name: string; loc?: { start: number; end: number } }>
|
||||||
|
>,
|
||||||
): DeclarationValue[] {
|
): DeclarationValue[] {
|
||||||
const declarations: DeclarationValue[] = []
|
const declarations: DeclarationValue[] = []
|
||||||
const seenExp = expressions.reduce(
|
const seenExp = expressions.reduce(
|
||||||
(acc, exp) => {
|
(acc, exp) => {
|
||||||
const variables = expToVariableMap.get(exp)
|
const variables = expToVariableMap.get(exp)!.map(v => v.name)
|
||||||
// only handle expressions that are not identifiers
|
// only handle expressions that are not identifiers
|
||||||
if (
|
if (
|
||||||
exp.ast &&
|
exp.ast &&
|
||||||
|
@ -572,12 +640,12 @@ function genVarName(exp: string): string {
|
||||||
|
|
||||||
function extractMemberExpression(
|
function extractMemberExpression(
|
||||||
exp: Node,
|
exp: Node,
|
||||||
onIdentifier: (name: string) => void,
|
onIdentifier: (id: Identifier) => void,
|
||||||
): string {
|
): string {
|
||||||
if (!exp) return ''
|
if (!exp) return ''
|
||||||
switch (exp.type) {
|
switch (exp.type) {
|
||||||
case 'Identifier': // foo[bar]
|
case 'Identifier': // foo[bar]
|
||||||
onIdentifier(exp.name)
|
onIdentifier(exp)
|
||||||
return exp.name
|
return exp.name
|
||||||
case 'StringLiteral': // foo['bar']
|
case 'StringLiteral': // foo['bar']
|
||||||
return exp.extra ? (exp.extra.raw as string) : exp.value
|
return exp.extra ? (exp.extra.raw as string) : exp.value
|
||||||
|
|
Loading…
Reference in New Issue