From 11344052caa6c21bee0356219dce921a076f9456 Mon Sep 17 00:00:00 2001 From: edison Date: Thu, 6 Nov 2025 15:19:45 +0800 Subject: [PATCH] refactor(compiler-vapor): generate unique variable to prevent collisions with user variables (#13822) --- .../__snapshots__/compile.spec.ts.snap | 80 ++++++++++++++++ .../compiler-vapor/__tests__/compile.spec.ts | 95 +++++++++++++++++++ packages/compiler-vapor/src/generate.ts | 90 ++++++++++++++++-- .../compiler-vapor/src/generators/template.ts | 9 +- packages/compiler-vapor/src/transform.ts | 66 ++++++++++++- 5 files changed, 326 insertions(+), 14 deletions(-) diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index c681811dd9..32f61085d1 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -304,6 +304,86 @@ export function render(_ctx) { }" `; +exports[`compile > gen unique helper alias > should avoid conflicts with existing variable names 1`] = ` +"import { txt as _txt2, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t0() + const x0 = _txt2(n0) + _renderEffect(() => _setText(x0, _toDisplayString(_ctx.foo))) + return n0 +}" +`; + +exports[`compile > gen unique node variables > should avoid binding conflicts for node vars (n*/x*) 1`] = ` +"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n1 = t0() + const n3 = t0() + const x1 = _txt(n1) + const x3 = _txt(n3) + _renderEffect(() => { + const _foo = _ctx.foo + _setText(x1, _toDisplayString(_foo)) + _setText(x3, _toDisplayString(_foo)) + }) + return [n1, n3] +}" +`; + +exports[`compile > gen unique node variables > should bump old ref var (r*) on conflict 1`] = ` +"import { createTemplateRefSetter as _createTemplateRefSetter, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const _setTemplateRef = _createTemplateRefSetter() + const n1 = t0() + const n3 = t0() + const n4 = t0() + let r1 + let r3 + let r4 + _renderEffect(() => { + const _bar = _ctx.bar + r1 = _setTemplateRef(n1, _bar, r1) + r3 = _setTemplateRef(n3, _bar, r3) + r4 = _setTemplateRef(n4, _bar, r4) + }) + return [n1, n3, n4] +}" +`; + +exports[`compile > gen unique node variables > should bump placeholder var (p*) on conflict 1`] = ` +"import { child as _child, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; +const t0 = _template("
", true) + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n1 = t0() + const p1 = _child(n1, 0) + const p3 = _child(p1, 0) + const n0 = _child(p3, 0) + _renderEffect(() => _setProp(n0, "id", _ctx.foo)) + return n1 +}" +`; + +exports[`compile > gen unique node variables > should bump template var (t*) on conflict 1`] = ` +"import { template as _template } from 'vue'; +const t1 = _template("
") +const t3 = _template("") +const t4 = _template("

") + +export function render(_ctx, $props, $emit, $attrs, $slots) { + const n0 = t1() + const n1 = t3() + const n2 = t4() + return [n0, n1, n2] +}" +`; + exports[`compile > static + dynamic root 1`] = ` "import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue'; const t0 = _template(" ") diff --git a/packages/compiler-vapor/__tests__/compile.spec.ts b/packages/compiler-vapor/__tests__/compile.spec.ts index 6de93bd320..32070300b9 100644 --- a/packages/compiler-vapor/__tests__/compile.spec.ts +++ b/packages/compiler-vapor/__tests__/compile.spec.ts @@ -268,4 +268,99 @@ describe('compile', () => { expect(code).matchSnapshot() }) }) + + describe('gen unique helper alias', () => { + test('should avoid conflicts with existing variable names', () => { + const code = compile(`
{{ foo }}
`, { + bindingMetadata: { + _txt: BindingTypes.LITERAL_CONST, + _txt1: BindingTypes.SETUP_REF, + }, + }) + expect(code).matchSnapshot() + expect(code).contains('txt as _txt2') + expect(code).contains('const x0 = _txt2(n0)') + }) + }) + + describe('gen unique node variables', () => { + test('should avoid binding conflicts for node vars (n*/x*)', () => { + const code = compile(`
{{ foo }}
{{ foo }}
`, { + bindingMetadata: { + n0: BindingTypes.SETUP_REACTIVE_CONST, + x0: BindingTypes.SETUP_MAYBE_REF, + n2: BindingTypes.SETUP_REACTIVE_CONST, + x2: BindingTypes.SETUP_MAYBE_REF, + }, + }) + + expect(code).matchSnapshot() + expect(code).not.contains('const n0') + expect(code).not.contains('const x0') + expect(code).not.contains('const n2') + expect(code).not.contains('const x2') + expect(code).contains('const n1 = t0()') + expect(code).contains('const n3 = t0()') + expect(code).contains('const x1 = _txt(n1)') + expect(code).contains('const x3 = _txt(n3)') + }) + + test('should bump old ref var (r*) on conflict', () => { + const code = compile( + `
`, + { + bindingMetadata: { + r0: BindingTypes.SETUP_REF, + r2: BindingTypes.SETUP_REF, + bar: BindingTypes.SETUP_REF, + }, + }, + ) + + expect(code).matchSnapshot() + expect(code).not.contains('let r0') + expect(code).not.contains('let r2') + expect(code).contains('let r1') + expect(code).contains('let r3') + expect(code).contains('let r4') + expect(code).contains('r1 = _setTemplateRef(n1, _bar, r1)') + expect(code).contains('r3 = _setTemplateRef(n3, _bar, r3)') + expect(code).contains('r4 = _setTemplateRef(n4, _bar, r4)') + }) + + test('should bump template var (t*) on conflict', () => { + const code = compile(`

`, { + bindingMetadata: { + t0: BindingTypes.SETUP_REF, + t2: BindingTypes.SETUP_REF, + }, + }) + + expect(code).matchSnapshot() + expect(code).not.contains('const t0 =') + expect(code).not.contains('const t2 =') + expect(code).contains('const t1 = _template("

")') + expect(code).contains('const t3 = _template("")') + expect(code).contains('const t4 = _template("

")') + }) + + test('should bump placeholder var (p*) on conflict', () => { + const code = compile( + `
`, + { + bindingMetadata: { + p0: BindingTypes.SETUP_REF, + p2: BindingTypes.SETUP_REF, + foo: BindingTypes.SETUP_REF, + }, + }, + ) + + expect(code).matchSnapshot() + expect(code).not.contains('const p0 = ') + expect(code).not.contains('const p2 = ') + expect(code).contains('const p1 = ') + expect(code).contains('const p3 = ') + }) + }) }) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 193a0f5da7..89af789f5f 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -18,17 +18,35 @@ import { genCall, } from './generators/utils' import { setTemplateRefIdent } from './generators/templateRef' +import { buildNextIdMap, getNextId } from './transform' export type CodegenOptions = Omit +const idWithTrailingDigitsRE = /^([A-Za-z_$][\w$]*)(\d+)$/ + export class CodegenContext { options: Required - helpers: Set = new Set([]) + bindingNames: Set = new Set() + + helpers: Map = new Map() - helper = (name: CoreHelper | VaporHelper) => { - this.helpers.add(name) - return `_${name}` + helper = (name: CoreHelper | VaporHelper): string => { + if (this.helpers.has(name)) { + return this.helpers.get(name)! + } + + const base = `_${name}` + if (this.bindingNames.size === 0 || !this.bindingNames.has(base)) { + this.helpers.set(name, base) + return base + } + + const map = this.nextIdMap.get(base) + // start from 1 because "base" (no suffix) is already taken. + const alias = `${base}${getNextId(map, 1)}` + this.helpers.set(name, alias) + return alias } delegates: Set = new Set() @@ -68,6 +86,55 @@ export class CodegenContext { return [this.scopeLevel++, () => this.scopeLevel--] as const } + private templateVars: Map = new Map() + private nextIdMap: Map> = new Map() + private lastIdMap: Map = new Map() + private lastTIndex: number = -1 + private initNextIdMap(): void { + if (this.bindingNames.size === 0) return + + // build a map of binding names to their occupied ids + const map = new Map>() + for (const name of this.bindingNames) { + const m = idWithTrailingDigitsRE.exec(name) + if (!m) continue + + const prefix = m[1] + const num = Number(m[2]) + let set = map.get(prefix) + if (!set) map.set(prefix, (set = new Set())) + set.add(num) + } + + for (const [prefix, nums] of map) { + this.nextIdMap.set(prefix, buildNextIdMap(nums)) + } + } + + tName(i: number): string { + let name = this.templateVars.get(i) + if (name) return name + + const map = this.nextIdMap.get('t') + let lastId = this.lastIdMap.get('t') || -1 + for (let j = this.lastTIndex + 1; j <= i; j++) { + this.templateVars.set( + j, + (name = `t${(lastId = getNextId(map, Math.max(j, lastId + 1)))}`), + ) + } + this.lastIdMap.set('t', lastId) + this.lastTIndex = i + return name! + } + + pName(i: number): string { + const map = this.nextIdMap.get('p') + let lastId = this.lastIdMap.get('p') || -1 + this.lastIdMap.set('p', (lastId = getNextId(map, Math.max(i, lastId + 1)))) + return `p${lastId}` + } + constructor( public ir: RootIRNode, options: CodegenOptions, @@ -90,6 +157,12 @@ export class CodegenContext { } this.options = extend(defaultOptions, options) this.block = ir.block + this.bindingNames = new Set( + this.options.bindingMetadata + ? Object.keys(this.options.bindingMetadata) + : [], + ) + this.initNextIdMap() } } @@ -105,7 +178,6 @@ export function generate( ): VaporCodegenResult { const [frag, push] = buildCodeFragment() const context = new CodegenContext(ir, options) - const { helpers } = context const { inline, bindingMetadata } = options const functionName = 'render' @@ -156,7 +228,7 @@ export function generate( ast: ir, preamble, map: map && map.toJSON(), - helpers, + helpers: new Set(Array.from(context.helpers.keys())), } } @@ -169,11 +241,11 @@ function genDelegates({ delegates, helper }: CodegenContext) { : '' } -function genHelperImports({ helpers, helper, options }: CodegenContext) { +function genHelperImports({ helpers, options }: CodegenContext) { let imports = '' if (helpers.size) { - imports += `import { ${[...helpers] - .map(h => `${h} as _${h}`) + imports += `import { ${Array.from(helpers) + .map(([h, alias]) => `${h} as ${alias}`) .join(', ')} } from '${options.runtimeModuleName}';\n` } return imports diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts index 45b3703a7f..1bf99ec383 100644 --- a/packages/compiler-vapor/src/generators/template.ts +++ b/packages/compiler-vapor/src/generators/template.ts @@ -11,12 +11,12 @@ import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils' export function genTemplates( templates: string[], rootIndex: number | undefined, - { helper }: CodegenContext, + context: CodegenContext, ): string { return templates .map( (template, i) => - `const t${i} = ${helper('template')}(${JSON.stringify( + `const ${context.tName(i)} = ${context.helper('template')}(${JSON.stringify( template, )}${i === rootIndex ? ', true' : ''})\n`, ) @@ -31,7 +31,7 @@ export function genSelf( const { id, template, operation, hasDynamicChild } = dynamic if (id !== undefined && template !== undefined) { - push(NEWLINE, `const n${id} = t${template}()`) + push(NEWLINE, `const n${id} = ${context.tName(template)}()`) push(...genDirectivesForElement(id, context)) } @@ -90,7 +90,8 @@ export function genChildren( const logicalIndex = elementIndex - ifBranchCount + prependCount // p for "placeholder" variables that are meant for possible reuse by // other access paths - const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}` + const variable = + id === undefined ? context.pName(context.block.tempId++) : `n${id}` pushBlock(NEWLINE, `const ${variable} = `) if (prev) { diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 6d2c1e8680..d5cec956fa 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -62,6 +62,8 @@ export type StructuralDirectiveTransform = ( export type TransformOptions = HackOptions +const generatedVarRE = /^[nxr](\d+)$/ + export class TransformContext { selfName: string | null = null parent: TransformContext | null = null @@ -86,6 +88,7 @@ export class TransformContext { slots: IRSlots[] = [] private globalId = 0 + private nextIdMap: Map | null = null constructor( public ir: RootIRNode, @@ -95,6 +98,7 @@ export class TransformContext { this.options = extend({}, defaultOptions, options) this.root = this as TransformContext if (options.filename) this.selfName = getSelfName(options.filename) + this.initNextIdMap() } enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void { @@ -117,7 +121,32 @@ export class TransformContext { } } - increaseId = (): number => this.globalId++ + increaseId = (): number => { + // allocate an id that won't conflict with user-defined bindings when used + // as generated identifiers with n/x/r prefixes (e.g., n1, x1, r1). + const id = getNextId(this.nextIdMap, this.globalId) + // advance next + this.globalId = getNextId(this.nextIdMap, id + 1) + return id + } + + private initNextIdMap(): void { + const binding = this.root.options.bindingMetadata + if (!binding) return + + const keys = Object.keys(binding) + if (keys.length === 0) return + + // extract numbers for specific literal prefixes + const numbers = new Set() + for (const name of keys) { + const m = generatedVarRE.exec(name) + if (m) numbers.add(Number(m[1])) + } + if (numbers.size === 0) return + + this.globalId = getNextId((this.nextIdMap = buildNextIdMap(numbers)), 0) + } reference(): number { if (this.dynamic.id !== undefined) return this.dynamic.id this.dynamic.flags |= DynamicFlag.REFERENCED @@ -296,3 +325,38 @@ export function createStructuralDirectiveTransform( } } } + +/** + * Build a "next-id" map from an occupied number set. + * For each consecutive range [start..end], map every v in the range to end + 1. + * Example: input [0, 1, 2, 4] => { 0: 3, 1: 3, 2: 3, 4: 5 }. + */ +export function buildNextIdMap(nums: Iterable): Map { + const map: Map = new Map() + const arr = Array.from(new Set(nums)).sort((a, b) => a - b) + if (arr.length === 0) return map + + for (let i = 0; i < arr.length; i++) { + let start = arr[i] + let end = start + while (i + 1 < arr.length && arr[i + 1] === end + 1) { + i++ + end = arr[i] + } + for (let v = start; v <= end; v++) map.set(v, end + 1) + } + return map +} + +/** + * Return the available id for n using a map built by buildNextIdMap: + * - If n is not occupied, return n. + * - If n is occupied, return the mapped value + */ +export function getNextId( + map: Map | null | undefined, + n: number, +): number { + if (map && map.has(n)) return map.get(n)! + return n +} -- 2.47.3