]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): respect var hoisting in props destructure main 14933/head
authordaiwei <daiwei521@126.com>
Sat, 6 Jun 2026 07:06:18 +0000 (15:06 +0800)
committerdaiwei <daiwei521@126.com>
Sat, 6 Jun 2026 07:06:18 +0000 (15:06 +0800)
packages/compiler-sfc/__tests__/compileScript/__snapshots__/definePropsDestructure.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript/definePropsDestructure.spec.ts
packages/compiler-sfc/src/script/definePropsDestructure.ts

index d3d6a31c2f029067d915ce9160c152e431a89178..5e04c48d3880e868ef3974e2f2adb76d8b898f41 100644 (file)
@@ -388,6 +388,108 @@ return { rest }
 }"
 `;
 
+exports[`sfc reactive props destructure > var declaration shadowing before declaration in for loop 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*@__PURE__*/_defineComponent({
+  props: {
+    foo: { type: String, required: false, default: "a" }
+  },
+  setup(__props: any) {
+
+
+let bar: string | undefined;
+function init() {
+  bar = foo;
+  for (var foo = "b"; false;) {}
+}
+init();
+
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc reactive props destructure > var declaration shadowing before declaration in nested block 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*@__PURE__*/_defineComponent({
+  props: {
+    foo: { type: String, required: false, default: "a" }
+  },
+  setup(__props: any) {
+
+
+let bar: string | undefined;
+function init() {
+  bar = foo;
+  {
+    var foo = "b";
+  }
+}
+init();
+
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc reactive props destructure > var declaration shadowing does not cross function scope 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*@__PURE__*/_defineComponent({
+  props: {
+    foo: { type: String, required: false, default: "a" }
+  },
+  setup(__props: any) {
+
+
+let bar: string | undefined;
+function init() {
+  function nested() {
+    {
+      var foo = "b";
+    }
+    return foo;
+  }
+  bar = __props.foo;
+  nested();
+}
+init();
+
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc reactive props destructure > var declaration shadowing in nested block 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*@__PURE__*/_defineComponent({
+  props: {
+    foo: { type: String, required: false, default: "a" }
+  },
+  setup(__props: any) {
+
+
+let bar: string | undefined;
+function init() {
+  {
+    var foo = "b";
+  }
+  bar = foo;
+}
+init();
+
+return () => {}
+}
+
+})"
+`;
+
 exports[`sfc reactive props destructure > with TSInstantiationExpression 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 type Foo = <T extends string | number>(data: T) => void
index cfb73a2ab26b9ba27d1dadef35d1a2e3278619b6..70797e4cd63985442fb06db4e0c45a1768d31baa 100644 (file)
@@ -108,6 +108,79 @@ describe('sfc reactive props destructure', () => {
     assertCode(content)
   })
 
+  test('var declaration shadowing in nested block', () => {
+    const { content } = compile(`<script setup lang="ts">
+const { foo = "a" } = defineProps<{ foo?: string }>();
+let bar: string | undefined;
+function init() {
+  {
+    var foo = "b";
+  }
+  bar = foo;
+}
+init();
+</script>`)
+    expect(content).toMatch(`var foo = "b"`)
+    expect(content).toMatch(`bar = foo`)
+    expect(content).not.toMatch(`bar = __props.foo`)
+    assertCode(content)
+  })
+
+  test('var declaration shadowing before declaration in nested block', () => {
+    const { content } = compile(`<script setup lang="ts">
+const { foo = "a" } = defineProps<{ foo?: string }>();
+let bar: string | undefined;
+function init() {
+  bar = foo;
+  {
+    var foo = "b";
+  }
+}
+init();
+</script>`)
+    expect(content).toMatch(`bar = foo`)
+    expect(content).toMatch(`var foo = "b"`)
+    expect(content).not.toMatch(`bar = __props.foo`)
+    assertCode(content)
+  })
+
+  test('var declaration shadowing before declaration in for loop', () => {
+    const { content } = compile(`<script setup lang="ts">
+const { foo = "a" } = defineProps<{ foo?: string }>();
+let bar: string | undefined;
+function init() {
+  bar = foo;
+  for (var foo = "b"; false;) {}
+}
+init();
+</script>`)
+    expect(content).toMatch(`bar = foo`)
+    expect(content).toMatch(`for (var foo = "b"; false;)`)
+    expect(content).not.toMatch(`bar = __props.foo`)
+    assertCode(content)
+  })
+
+  test('var declaration shadowing does not cross function scope', () => {
+    const { content } = compile(`<script setup lang="ts">
+const { foo = "a" } = defineProps<{ foo?: string }>();
+let bar: string | undefined;
+function init() {
+  function nested() {
+    {
+      var foo = "b";
+    }
+    return foo;
+  }
+  bar = foo;
+  nested();
+}
+init();
+</script>`)
+    expect(content).toMatch(`return foo`)
+    expect(content).toMatch(`bar = __props.foo`)
+    assertCode(content)
+  })
+
   test('default values w/ array runtime declaration', () => {
     const { content } = compile(`
       <script setup>
index 0d18d01943d30746f15303ba8b776a9ee642c1d4..e1050c25573f0a265f00a3c03316bec9e019204b 100644 (file)
@@ -105,6 +105,7 @@ export function transformDestructuredProps(
 
   const rootScope: Scope = Object.create(null)
   const scopeStack: Scope[] = [rootScope]
+  const functionScopeStack: Scope[] = [rootScope]
   let currentScope: Scope = rootScope
   const excludedIds = new WeakSet<Identifier>()
   const parentStack: Node[] = []
@@ -116,19 +117,26 @@ export function transformDestructuredProps(
     propsLocalToPublicMap[local] = key
   }
 
-  function pushScope() {
-    scopeStack.push((currentScope = Object.create(currentScope)))
+  function pushScope(isFunctionScope = false) {
+    const scope = (currentScope = Object.create(currentScope))
+    scopeStack.push(scope)
+    if (isFunctionScope) {
+      functionScopeStack.push(scope)
+    }
   }
 
-  function popScope() {
+  function popScope(isFunctionScope = false) {
     scopeStack.pop()
+    if (isFunctionScope) {
+      functionScopeStack.pop()
+    }
     currentScope = scopeStack[scopeStack.length - 1] || null
   }
 
-  function registerLocalBinding(id: Identifier) {
+  function registerLocalBinding(id: Identifier, scope = currentScope) {
     excludedIds.add(id)
-    if (currentScope) {
-      currentScope[id.name] = false
+    if (scope) {
+      scope[id.name] = false
     } else {
       ctx.error(
         'registerBinding called without active scope, something is wrong.',
@@ -162,7 +170,13 @@ export function transformDestructuredProps(
     }
   }
 
-  function walkVariableDeclaration(stmt: VariableDeclaration, isRoot = false) {
+  function walkVariableDeclaration(
+    stmt: VariableDeclaration,
+    isRoot = false,
+    scope = stmt.kind === 'var'
+      ? functionScopeStack[functionScopeStack.length - 1]
+      : currentScope,
+  ) {
     if (stmt.declare) {
       return
     }
@@ -175,12 +189,42 @@ export function transformDestructuredProps(
           // are already passed in as knownProps
           excludedIds.add(id)
         } else {
-          registerLocalBinding(id)
+          registerLocalBinding(id, scope)
         }
       }
     }
   }
 
+  function walkFunctionScopeVarDeclarations(
+    scopeNode: Program | BlockStatement,
+    isRoot = false,
+  ) {
+    const scope = functionScopeStack[functionScopeStack.length - 1]
+    walk(scopeNode, {
+      enter(node: Node, parent: Node | null) {
+        if (
+          parent &&
+          parent.type.startsWith('TS') &&
+          !TS_NODE_TYPES.includes(parent.type)
+        ) {
+          return this.skip()
+        }
+
+        if (
+          isFunctionType(node) ||
+          node.type === 'ClassDeclaration' ||
+          node.type === 'ClassExpression'
+        ) {
+          return this.skip()
+        }
+
+        if (node.type === 'VariableDeclaration' && node.kind === 'var') {
+          walkVariableDeclaration(node, isRoot && parent === scopeNode, scope)
+        }
+      },
+    })
+  }
+
   function rewriteId(id: Identifier, parent: Node, parentStack: Node[]) {
     if (
       (parent.type === 'AssignmentExpression' && id === parent.left) ||
@@ -227,6 +271,7 @@ export function transformDestructuredProps(
 
   // check root scope first
   const ast = ctx.scriptSetupAst!
+  walkFunctionScopeVarDeclarations(ast, true)
   walkScope(ast, true)
   walk(ast, {
     enter(node: Node, parent: Node | null) {
@@ -246,9 +291,10 @@ export function transformDestructuredProps(
 
       // function scopes
       if (isFunctionType(node)) {
-        pushScope()
+        pushScope(true)
         walkFunctionParams(node, registerLocalBinding)
         if (node.body.type === 'BlockStatement') {
+          walkFunctionScopeVarDeclarations(node.body)
           walkScope(node.body)
         }
         return
@@ -301,9 +347,11 @@ export function transformDestructuredProps(
     },
     leave(node: Node, parent: Node | null) {
       parent && parentStack.pop()
-      if (
-        (node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
-        isFunctionType(node) ||
+      if (isFunctionType(node)) {
+        popScope(true)
+      } else if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
+        popScope()
+      } else if (
         node.type === 'CatchClause' ||
         node.type === 'ForOfStatement' ||
         node.type === 'ForInStatement' ||