Skip to content

Commit 1134405

Browse files
authored
refactor(compiler-vapor): generate unique variable to prevent collisions with user variables (#13822)
1 parent 2b1fdaf commit 1134405

File tree

5 files changed

+326
-14
lines changed

5 files changed

+326
-14
lines changed

packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,86 @@ export function render(_ctx) {
304304
}"
305305
`;
306306
307+
exports[`compile > gen unique helper alias > should avoid conflicts with existing variable names 1`] = `
308+
"import { txt as _txt2, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
309+
const t0 = _template("<div> </div>", true)
310+
311+
export function render(_ctx, $props, $emit, $attrs, $slots) {
312+
const n0 = t0()
313+
const x0 = _txt2(n0)
314+
_renderEffect(() => _setText(x0, _toDisplayString(_ctx.foo)))
315+
return n0
316+
}"
317+
`;
318+
319+
exports[`compile > gen unique node variables > should avoid binding conflicts for node vars (n*/x*) 1`] = `
320+
"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
321+
const t0 = _template("<div> </div>")
322+
323+
export function render(_ctx, $props, $emit, $attrs, $slots) {
324+
const n1 = t0()
325+
const n3 = t0()
326+
const x1 = _txt(n1)
327+
const x3 = _txt(n3)
328+
_renderEffect(() => {
329+
const _foo = _ctx.foo
330+
_setText(x1, _toDisplayString(_foo))
331+
_setText(x3, _toDisplayString(_foo))
332+
})
333+
return [n1, n3]
334+
}"
335+
`;
336+
337+
exports[`compile > gen unique node variables > should bump old ref var (r*) on conflict 1`] = `
338+
"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue';
339+
const t0 = _template("<div></div>")
340+
341+
export function render(_ctx, $props, $emit, $attrs, $slots) {
342+
const _setTemplateRef = _createTemplateRefSetter()
343+
const n1 = t0()
344+
const n3 = t0()
345+
const n4 = t0()
346+
let r1
347+
let r3
348+
let r4
349+
_renderEffect(() => {
350+
const _bar = _ctx.bar
351+
r1 = _setTemplateRef(n1, _bar, r1)
352+
r3 = _setTemplateRef(n3, _bar, r3)
353+
r4 = _setTemplateRef(n4, _bar, r4)
354+
})
355+
return [n1, n3, n4]
356+
}"
357+
`;
358+
359+
exports[`compile > gen unique node variables > should bump placeholder var (p*) on conflict 1`] = `
360+
"import { child as _child, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
361+
const t0 = _template("<div><div><div><span></span></div></div></div>", true)
362+
363+
export function render(_ctx, $props, $emit, $attrs, $slots) {
364+
const n1 = t0()
365+
const p1 = _child(n1, 0)
366+
const p3 = _child(p1, 0)
367+
const n0 = _child(p3, 0)
368+
_renderEffect(() => _setProp(n0, "id", _ctx.foo))
369+
return n1
370+
}"
371+
`;
372+
373+
exports[`compile > gen unique node variables > should bump template var (t*) on conflict 1`] = `
374+
"import { template as _template } from 'vue';
375+
const t1 = _template("<div></div>")
376+
const t3 = _template("<span></span>")
377+
const t4 = _template("<p></p>")
378+
379+
export function render(_ctx, $props, $emit, $attrs, $slots) {
380+
const n0 = t1()
381+
const n1 = t3()
382+
const n2 = t4()
383+
return [n0, n1, n2]
384+
}"
385+
`;
386+
307387
exports[`compile > static + dynamic root 1`] = `
308388
"import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue';
309389
const t0 = _template(" ")

packages/compiler-vapor/__tests__/compile.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,4 +268,99 @@ describe('compile', () => {
268268
expect(code).matchSnapshot()
269269
})
270270
})
271+
272+
describe('gen unique helper alias', () => {
273+
test('should avoid conflicts with existing variable names', () => {
274+
const code = compile(`<div>{{ foo }}</div>`, {
275+
bindingMetadata: {
276+
_txt: BindingTypes.LITERAL_CONST,
277+
_txt1: BindingTypes.SETUP_REF,
278+
},
279+
})
280+
expect(code).matchSnapshot()
281+
expect(code).contains('txt as _txt2')
282+
expect(code).contains('const x0 = _txt2(n0)')
283+
})
284+
})
285+
286+
describe('gen unique node variables', () => {
287+
test('should avoid binding conflicts for node vars (n*/x*)', () => {
288+
const code = compile(`<div>{{ foo }}</div><div>{{ foo }}</div>`, {
289+
bindingMetadata: {
290+
n0: BindingTypes.SETUP_REACTIVE_CONST,
291+
x0: BindingTypes.SETUP_MAYBE_REF,
292+
n2: BindingTypes.SETUP_REACTIVE_CONST,
293+
x2: BindingTypes.SETUP_MAYBE_REF,
294+
},
295+
})
296+
297+
expect(code).matchSnapshot()
298+
expect(code).not.contains('const n0')
299+
expect(code).not.contains('const x0')
300+
expect(code).not.contains('const n2')
301+
expect(code).not.contains('const x2')
302+
expect(code).contains('const n1 = t0()')
303+
expect(code).contains('const n3 = t0()')
304+
expect(code).contains('const x1 = _txt(n1)')
305+
expect(code).contains('const x3 = _txt(n3)')
306+
})
307+
308+
test('should bump old ref var (r*) on conflict', () => {
309+
const code = compile(
310+
`<div :ref="bar" /><div :ref="bar" /><div :ref="bar" />`,
311+
{
312+
bindingMetadata: {
313+
r0: BindingTypes.SETUP_REF,
314+
r2: BindingTypes.SETUP_REF,
315+
bar: BindingTypes.SETUP_REF,
316+
},
317+
},
318+
)
319+
320+
expect(code).matchSnapshot()
321+
expect(code).not.contains('let r0')
322+
expect(code).not.contains('let r2')
323+
expect(code).contains('let r1')
324+
expect(code).contains('let r3')
325+
expect(code).contains('let r4')
326+
expect(code).contains('r1 = _setTemplateRef(n1, _bar, r1)')
327+
expect(code).contains('r3 = _setTemplateRef(n3, _bar, r3)')
328+
expect(code).contains('r4 = _setTemplateRef(n4, _bar, r4)')
329+
})
330+
331+
test('should bump template var (t*) on conflict', () => {
332+
const code = compile(`<div/><span/><p/>`, {
333+
bindingMetadata: {
334+
t0: BindingTypes.SETUP_REF,
335+
t2: BindingTypes.SETUP_REF,
336+
},
337+
})
338+
339+
expect(code).matchSnapshot()
340+
expect(code).not.contains('const t0 =')
341+
expect(code).not.contains('const t2 =')
342+
expect(code).contains('const t1 = _template("<div></div>")')
343+
expect(code).contains('const t3 = _template("<span></span>")')
344+
expect(code).contains('const t4 = _template("<p></p>")')
345+
})
346+
347+
test('should bump placeholder var (p*) on conflict', () => {
348+
const code = compile(
349+
`<div><div><div><span :id="foo" /></div></div></div>`,
350+
{
351+
bindingMetadata: {
352+
p0: BindingTypes.SETUP_REF,
353+
p2: BindingTypes.SETUP_REF,
354+
foo: BindingTypes.SETUP_REF,
355+
},
356+
},
357+
)
358+
359+
expect(code).matchSnapshot()
360+
expect(code).not.contains('const p0 = ')
361+
expect(code).not.contains('const p2 = ')
362+
expect(code).contains('const p1 = ')
363+
expect(code).contains('const p3 = ')
364+
})
365+
})
271366
})

packages/compiler-vapor/src/generate.ts

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,35 @@ import {
1818
genCall,
1919
} from './generators/utils'
2020
import { setTemplateRefIdent } from './generators/templateRef'
21+
import { buildNextIdMap, getNextId } from './transform'
2122

2223
export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'>
2324

25+
const idWithTrailingDigitsRE = /^([A-Za-z_$][\w$]*)(\d+)$/
26+
2427
export class CodegenContext {
2528
options: Required<CodegenOptions>
2629

27-
helpers: Set<string> = new Set<string>([])
30+
bindingNames: Set<string> = new Set<string>()
31+
32+
helpers: Map<string, string> = new Map()
2833

29-
helper = (name: CoreHelper | VaporHelper) => {
30-
this.helpers.add(name)
31-
return `_${name}`
34+
helper = (name: CoreHelper | VaporHelper): string => {
35+
if (this.helpers.has(name)) {
36+
return this.helpers.get(name)!
37+
}
38+
39+
const base = `_${name}`
40+
if (this.bindingNames.size === 0 || !this.bindingNames.has(base)) {
41+
this.helpers.set(name, base)
42+
return base
43+
}
44+
45+
const map = this.nextIdMap.get(base)
46+
// start from 1 because "base" (no suffix) is already taken.
47+
const alias = `${base}${getNextId(map, 1)}`
48+
this.helpers.set(name, alias)
49+
return alias
3250
}
3351

3452
delegates: Set<string> = new Set<string>()
@@ -68,6 +86,55 @@ export class CodegenContext {
6886
return [this.scopeLevel++, () => this.scopeLevel--] as const
6987
}
7088

89+
private templateVars: Map<number, string> = new Map()
90+
private nextIdMap: Map<string, Map<number, number>> = new Map()
91+
private lastIdMap: Map<string, number> = new Map()
92+
private lastTIndex: number = -1
93+
private initNextIdMap(): void {
94+
if (this.bindingNames.size === 0) return
95+
96+
// build a map of binding names to their occupied ids
97+
const map = new Map<string, Set<number>>()
98+
for (const name of this.bindingNames) {
99+
const m = idWithTrailingDigitsRE.exec(name)
100+
if (!m) continue
101+
102+
const prefix = m[1]
103+
const num = Number(m[2])
104+
let set = map.get(prefix)
105+
if (!set) map.set(prefix, (set = new Set<number>()))
106+
set.add(num)
107+
}
108+
109+
for (const [prefix, nums] of map) {
110+
this.nextIdMap.set(prefix, buildNextIdMap(nums))
111+
}
112+
}
113+
114+
tName(i: number): string {
115+
let name = this.templateVars.get(i)
116+
if (name) return name
117+
118+
const map = this.nextIdMap.get('t')
119+
let lastId = this.lastIdMap.get('t') || -1
120+
for (let j = this.lastTIndex + 1; j <= i; j++) {
121+
this.templateVars.set(
122+
j,
123+
(name = `t${(lastId = getNextId(map, Math.max(j, lastId + 1)))}`),
124+
)
125+
}
126+
this.lastIdMap.set('t', lastId)
127+
this.lastTIndex = i
128+
return name!
129+
}
130+
131+
pName(i: number): string {
132+
const map = this.nextIdMap.get('p')
133+
let lastId = this.lastIdMap.get('p') || -1
134+
this.lastIdMap.set('p', (lastId = getNextId(map, Math.max(i, lastId + 1))))
135+
return `p${lastId}`
136+
}
137+
71138
constructor(
72139
public ir: RootIRNode,
73140
options: CodegenOptions,
@@ -90,6 +157,12 @@ export class CodegenContext {
90157
}
91158
this.options = extend(defaultOptions, options)
92159
this.block = ir.block
160+
this.bindingNames = new Set<string>(
161+
this.options.bindingMetadata
162+
? Object.keys(this.options.bindingMetadata)
163+
: [],
164+
)
165+
this.initNextIdMap()
93166
}
94167
}
95168

@@ -105,7 +178,6 @@ export function generate(
105178
): VaporCodegenResult {
106179
const [frag, push] = buildCodeFragment()
107180
const context = new CodegenContext(ir, options)
108-
const { helpers } = context
109181
const { inline, bindingMetadata } = options
110182
const functionName = 'render'
111183

@@ -156,7 +228,7 @@ export function generate(
156228
ast: ir,
157229
preamble,
158230
map: map && map.toJSON(),
159-
helpers,
231+
helpers: new Set<string>(Array.from(context.helpers.keys())),
160232
}
161233
}
162234

@@ -169,11 +241,11 @@ function genDelegates({ delegates, helper }: CodegenContext) {
169241
: ''
170242
}
171243

172-
function genHelperImports({ helpers, helper, options }: CodegenContext) {
244+
function genHelperImports({ helpers, options }: CodegenContext) {
173245
let imports = ''
174246
if (helpers.size) {
175-
imports += `import { ${[...helpers]
176-
.map(h => `${h} as _${h}`)
247+
imports += `import { ${Array.from(helpers)
248+
.map(([h, alias]) => `${h} as ${alias}`)
177249
.join(', ')} } from '${options.runtimeModuleName}';\n`
178250
}
179251
return imports

packages/compiler-vapor/src/generators/template.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
1111
export function genTemplates(
1212
templates: string[],
1313
rootIndex: number | undefined,
14-
{ helper }: CodegenContext,
14+
context: CodegenContext,
1515
): string {
1616
return templates
1717
.map(
1818
(template, i) =>
19-
`const t${i} = ${helper('template')}(${JSON.stringify(
19+
`const ${context.tName(i)} = ${context.helper('template')}(${JSON.stringify(
2020
template,
2121
)}${i === rootIndex ? ', true' : ''})\n`,
2222
)
@@ -31,7 +31,7 @@ export function genSelf(
3131
const { id, template, operation, hasDynamicChild } = dynamic
3232

3333
if (id !== undefined && template !== undefined) {
34-
push(NEWLINE, `const n${id} = t${template}()`)
34+
push(NEWLINE, `const n${id} = ${context.tName(template)}()`)
3535
push(...genDirectivesForElement(id, context))
3636
}
3737

@@ -90,7 +90,8 @@ export function genChildren(
9090
const logicalIndex = elementIndex - ifBranchCount + prependCount
9191
// p for "placeholder" variables that are meant for possible reuse by
9292
// other access paths
93-
const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
93+
const variable =
94+
id === undefined ? context.pName(context.block.tempId++) : `n${id}`
9495
pushBlock(NEWLINE, `const ${variable} = `)
9596

9697
if (prev) {

0 commit comments

Comments
 (0)