浏览代码

fix(compiler-vapor): avoid cache name collisions in expression dedupe (#14568)

edison 1 月之前
父节点
当前提交
9101686980

+ 39 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/expression.spec.ts.snap

@@ -217,6 +217,24 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: expression > cache expressions > repeated expressions do not collide with existing identifiers 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  const n2 = t0()
+  _renderEffect(() => {
+    const _foo_bar_1 = _ctx.foo + _ctx.bar
+    _setProp(n0, "id", _ctx.foo_bar)
+    _setProp(n1, "id", _foo_bar_1)
+    _setProp(n2, "id", _foo_bar_1)
+  })
+  return [n0, n1, n2]
+}"
+`;
+
 exports[`compiler: expression > cache expressions > repeated simple function calls 1`] = `
 "import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div>")
@@ -233,6 +251,27 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: expression > cache expressions > repeated simple function calls do not collide with repeated variables 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  const n2 = t0()
+  const n3 = t0()
+  _renderEffect(() => {
+    const _foo = _ctx.foo
+    const _foo_1 = _foo()
+    _setProp(n0, "id", _foo)
+    _setProp(n1, "id", _foo)
+    _setProp(n2, "id", _foo_1)
+    _setProp(n3, "id", _foo_1)
+  })
+  return [n0, n1, n2, n3]
+}"
+`;
+
 exports[`compiler: expression > cache expressions > repeated simple function calls with setup-const binding 1`] = `
 "
   const n0 = t0()

+ 27 - 0
packages/compiler-vapor/__tests__/transforms/expression.spec.ts

@@ -124,6 +124,33 @@ describe('compiler: expression', () => {
       expect(code).contains('const _foo = _ctx.foo()')
     })
 
+    test('repeated simple function calls do not collide with repeated variables', () => {
+      const { code } = compileWithExpression(`
+        <div :id="foo"></div>
+        <div :id="foo"></div>
+        <div :id="foo()"></div>
+        <div :id="foo()"></div>
+      `)
+      expect(code).matchSnapshot()
+      expect(code).contains('const _foo = _ctx.foo')
+      expect(code).toMatch(/const _foo_1 = .*foo\(\)/)
+      expect(code).contains('_setProp(n0, "id", _foo)')
+      expect(code).contains('_setProp(n2, "id", _foo_1)')
+    })
+
+    test('repeated expressions do not collide with existing identifiers', () => {
+      const { code } = compileWithExpression(`
+        <div :id="foo_bar"></div>
+        <div :id="foo + bar"></div>
+        <div :id="foo + bar"></div>
+      `)
+      expect(code).matchSnapshot()
+      expect(code).contains('_setProp(n0, "id", _ctx.foo_bar)')
+      expect(code).contains('const _foo_bar_1 = _ctx.foo + _ctx.bar')
+      expect(code).contains('_setProp(n1, "id", _foo_bar_1)')
+      expect(code).contains('_setProp(n2, "id", _foo_bar_1)')
+    })
+
     test('repeated simple function calls with setup-const binding', () => {
       const { code } = compileWithExpression(
         `

+ 57 - 34
packages/compiler-vapor/src/generators/expression.ts

@@ -252,6 +252,7 @@ export function processExpressions(
     seenIdentifier,
     updatedVariable,
   } = analyzeExpressions(expressions)
+  const reservedNames = new Set<string>(seenIdentifier)
 
   // process repeated identifiers and member expressions
   // e.g., `foo[baz]` will be transformed into `foo_baz`
@@ -262,6 +263,7 @@ export function processExpressions(
     expToVariableMap,
     seenIdentifier,
     updatedVariable,
+    reservedNames,
   )
 
   // process duplicate expressions after identifier and member expression handling.
@@ -272,6 +274,7 @@ export function processExpressions(
     varDeclarations,
     updatedVariable,
     expToVariableMap,
+    reservedNames,
   )
 
   return genDeclarations(
@@ -387,6 +390,7 @@ function processRepeatedVariables(
   >,
   seenIdentifier: Set<string>,
   updatedVariable: Set<string>,
+  reservedNames: Set<string>,
 ): DeclarationValue[] {
   const declarations: DeclarationValue[] = []
   const expToReplacementMap = new Map<
@@ -404,7 +408,9 @@ function processRepeatedVariables(
     if (isGloballyAllowed(name)) continue
     if (seenVariable[name] > 1 && exps.size > 0) {
       const isIdentifier = seenIdentifier.has(name)
-      const varName = isIdentifier ? name : genVarName(name)
+      const varName = isIdentifier
+        ? name
+        : getUniqueDeclarationName(genVarName(name), reservedNames)
 
       // replaces all non-identifiers with the new name. if node content
       // includes only one member expression, it will become an identifier,
@@ -526,6 +532,7 @@ function processRepeatedExpressions(
     SimpleExpressionNode,
     Array<{ name: string; loc?: { start: number; end: number } }>
   >,
+  reservedNames: Set<string>,
 ): DeclarationValue[] {
   const declarations: DeclarationValue[] = []
   const seenExp = expressions.reduce(
@@ -554,41 +561,43 @@ function processRepeatedExpressions(
   Object.entries(seenExp).forEach(([content, count]) => {
     if (count > 1) {
       // foo + baz -> foo_baz
-      const varName = genVarName(content)
-      if (!declarations.some(d => d.name === varName)) {
-        // if foo and baz have no other references, we don't need to declare separate variables
-        // instead of:
-        // const foo = _ctx.foo
-        // const baz = _ctx.baz
-        // const foo_baz = foo + baz
-        // we can generate:
-        // const foo_baz = _ctx.foo + _ctx.baz
-        const delVars: Record<string, string> = {}
-        for (let i = varDeclarations.length - 1; i >= 0; i--) {
-          const item = varDeclarations[i]
-          if (!item.exps || !item.seenCount) continue
-
-          const shouldRemove = [...item.exps].every(
-            node => node.content === content && item.seenCount === count,
-          )
-          if (shouldRemove) {
-            delVars[item.name] = item.rawName!
-            varDeclarations.splice(i, 1)
-          }
-        }
-        const value = extend(
-          {},
-          expressions.find(exp => exp.content === content)!,
+      // if foo and baz have no other references, we don't need to declare separate variables
+      // instead of:
+      // const foo = _ctx.foo
+      // const baz = _ctx.baz
+      // const foo_baz = foo + baz
+      // we can generate:
+      // const foo_baz = _ctx.foo + _ctx.baz
+      const delVars: Record<string, string> = {}
+      for (let i = varDeclarations.length - 1; i >= 0; i--) {
+        const item = varDeclarations[i]
+        if (!item.exps || !item.seenCount) continue
+
+        const shouldRemove = [...item.exps].every(
+          node => node.content === content && item.seenCount === count,
         )
-        Object.keys(delVars).forEach(name => {
-          value.content = value.content.replace(name, delVars[name])
-          if (value.ast) value.ast = parseExp(context, value.content)
-        })
-        declarations.push({
-          name: varName,
-          value: value,
-        })
+        if (shouldRemove) {
+          delVars[item.name] = item.rawName!
+          reservedNames.delete(item.name)
+          varDeclarations.splice(i, 1)
+        }
       }
+      const value = extend(
+        {},
+        expressions.find(exp => exp.content === content)!,
+      )
+      Object.keys(delVars).forEach(name => {
+        value.content = value.content.replace(name, delVars[name])
+        if (value.ast) value.ast = parseExp(context, value.content)
+      })
+      const varName = getUniqueDeclarationName(
+        genVarName(content),
+        reservedNames,
+      )
+      declarations.push({
+        name: varName,
+        value,
+      })
 
       // assume content equals to `foo + baz`
       expressions.forEach(exp => {
@@ -674,6 +683,20 @@ export function genVarName(exp: string): string {
     .replace(/_+$/, '')}`
 }
 
+function getUniqueDeclarationName(
+  baseName: string,
+  reservedNames: Set<string>,
+): string {
+  const normalizedBase = baseName || 'exp'
+  let name = normalizedBase
+  let i = 1
+  while (reservedNames.has(name)) {
+    name = `${normalizedBase}_${i++}`
+  }
+  reservedNames.add(name)
+  return name
+}
+
 function extractMemberExpression(
   exp: Node,
   onIdentifier: (id: Identifier) => void,