]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(compiler-vapor): generate unique variable to prevent collisions with user... minor
authoredison <daiwei521@126.com>
Thu, 6 Nov 2025 07:19:45 +0000 (15:19 +0800)
committerGitHub <noreply@github.com>
Thu, 6 Nov 2025 07:19:45 +0000 (15:19 +0800)
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/compile.spec.ts
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/generators/template.ts
packages/compiler-vapor/src/transform.ts

index c681811dd93747930f47eea3b717d1212597ccf4..32f61085d1bf69a21b1f9a3e142b496fdb5d4acc 100644 (file)
@@ -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("<div> </div>", 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("<div> </div>")
+
+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("<div></div>")
+
+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("<div><div><div><span></span></div></div></div>", 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("<div></div>")
+const t3 = _template("<span></span>")
+const t4 = _template("<p></p>")
+
+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(" ")
index 6de93bd320c2c86a8d9dd4163b0b7246f0cc6e2a..32070300b9c4cd69395e2a24d60fd874740c9699 100644 (file)
@@ -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(`<div>{{ foo }}</div>`, {
+        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(`<div>{{ foo }}</div><div>{{ foo }}</div>`, {
+        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(
+        `<div :ref="bar" /><div :ref="bar" /><div :ref="bar" />`,
+        {
+          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(`<div/><span/><p/>`, {
+        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("<div></div>")')
+      expect(code).contains('const t3 = _template("<span></span>")')
+      expect(code).contains('const t4 = _template("<p></p>")')
+    })
+
+    test('should bump placeholder var (p*) on conflict', () => {
+      const code = compile(
+        `<div><div><div><span :id="foo" /></div></div></div>`,
+        {
+          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 = ')
+    })
+  })
 })
index 193a0f5da777be3f443ac9aa2480870aad6a3d34..89af789f5f9336d4cfced58e8162300d6e75f653 100644 (file)
@@ -18,17 +18,35 @@ import {
   genCall,
 } from './generators/utils'
 import { setTemplateRefIdent } from './generators/templateRef'
+import { buildNextIdMap, getNextId } from './transform'
 
 export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'>
 
+const idWithTrailingDigitsRE = /^([A-Za-z_$][\w$]*)(\d+)$/
+
 export class CodegenContext {
   options: Required<CodegenOptions>
 
-  helpers: Set<string> = new Set<string>([])
+  bindingNames: Set<string> = new Set<string>()
+
+  helpers: Map<string, string> = 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<string> = new Set<string>()
@@ -68,6 +86,55 @@ export class CodegenContext {
     return [this.scopeLevel++, () => this.scopeLevel--] as const
   }
 
+  private templateVars: Map<number, string> = new Map()
+  private nextIdMap: Map<string, Map<number, number>> = new Map()
+  private lastIdMap: Map<string, number> = 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<string, Set<number>>()
+    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<number>()))
+      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<string>(
+      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<string>(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
index 45b3703a7f94e92614f8e40bbec130225f206bc9..1bf99ec3834a3a86d39cc81665257195d07d863c 100644 (file)
@@ -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) {
index 6d2c1e86801da9f5d0e0570e9ec654557c9bf933..d5cec956fa68a6370caf958312f71959b6583cc5 100644 (file)
@@ -62,6 +62,8 @@ export type StructuralDirectiveTransform = (
 
 export type TransformOptions = HackOptions<BaseTransformOptions>
 
+const generatedVarRE = /^[nxr](\d+)$/
+
 export class TransformContext<T extends AllNode = AllNode> {
   selfName: string | null = null
   parent: TransformContext<RootNode | ElementNode> | null = null
@@ -86,6 +88,7 @@ export class TransformContext<T extends AllNode = AllNode> {
   slots: IRSlots[] = []
 
   private globalId = 0
+  private nextIdMap: Map<number, number> | null = null
 
   constructor(
     public ir: RootIRNode,
@@ -95,6 +98,7 @@ export class TransformContext<T extends AllNode = AllNode> {
     this.options = extend({}, defaultOptions, options)
     this.root = this as TransformContext<RootNode>
     if (options.filename) this.selfName = getSelfName(options.filename)
+    this.initNextIdMap()
   }
 
   enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void {
@@ -117,7 +121,32 @@ export class TransformContext<T extends AllNode = AllNode> {
     }
   }
 
-  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<number>()
+    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<number>): Map<number, number> {
+  const map: Map<number, number> = 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<number, number> | null | undefined,
+  n: number,
+): number {
+  if (map && map.has(n)) return map.get(n)!
+  return n
+}