Skip to content

Conversation

wngtk
Copy link
Contributor

@wngtk wngtk commented Sep 24, 2025

My brother @PBK-B likes ​​to write​​ JSX in Vue. He ​​came across​​ this problem.

Variables declared under a switch-case clause are added _ctx. prefix when used.

<template>
  <p>{{ (() => { switch (true) { case true: let foo = 'Hello World'; return `${foo}`;} })()}}</p>
  <p>{{ (() => { switch (true) { case true: var foo = 'Hello World'; return `${foo}`;} })()}}</p>
  <p>{{ (() => { switch (true) { case true: var foo = 'Hello World'; } return `${foo}`; })()}}</p>
</template>

See Playground

Expected to show Hello World.

Summary by CodeRabbit

  • Bug Fixes

    • Fixes scoping of const declarations in switch cases so local variables are correctly recognized and emitted (including cases without braces), improving template expression handling and generated output accuracy.
  • Tests

    • Adds tests covering switch-case variable scoping (with and without braces) and validates emitted code and control-flow behavior.

Copy link

coderabbitai bot commented Sep 24, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds switch/case-aware traversal to babelUtils: imports SwitchCase/SwitchStatement, introduces walkSwitchStatement, extends walkBlockDeclarations to accept SwitchCase, and updates walkIdentifiers to record case-local declarations (respecting var vs non-var) plus improved handling for CatchClause and ForStatement. Adds tests for switch-case const scoping.

Changes

Cohort / File(s) Summary
Compiler core — AST traversal
packages/compiler-core/src/babelUtils.ts
Import SwitchCase/SwitchStatement; add walkSwitchStatement to collect identifiers from switch cases (respecting var vs non-var); extend walkBlockDeclarations signature to accept SwitchCase; update walkIdentifiers to detect SwitchStatement, and refine CatchClause/ForStatement handling to use existing scopeIds when present.
Tests — transform expressions
packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts
Add "switch case variable declarations" suite with three tests verifying: const in switch cases without braces are local (no _ctx.), behavior with braces preserved, and case test expressions scoped correctly.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Transformer as TemplateTransform
  participant WalkIDs as walkIdentifiers
  participant WalkSwitch as walkSwitchStatement
  participant WalkBlock as walkBlockDeclarations
  participant Codegen as Codegen

  Transformer->>WalkIDs: traverse AST
  WalkIDs->>WalkSwitch: encounter SwitchStatement (if no scopeIds)
  rect rgb(235,245,235)
    WalkSwitch->>WalkBlock: iterate cases, collect declarations (isVar? -> hoist)
    WalkBlock-->>WalkIDs: report identifiers (case-local vs hoisted)
  end
  WalkIDs-->>Codegen: supply scope id mappings
  Codegen->>Codegen: emit references (omit `_ctx.` for case-local non-var consts)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

ready to merge

Poem

I hop through switch-cased rows with care,
Sniffing consts hidden in the air.
Local footprints left, no _ctx. to trace,
My tiny paws keep the AST in place.
A rabbit's nod — the code's aware. 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely summarizes the primary change: preventing identifiers declared in switch-case clauses from being treated as references, which matches the modifications to babelUtils and the added tests for switch-case scoping. It is a single, specific sentence in conventional commit style and directly reflects the PR objective and changeset.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47c7af9 and b457df0.

📒 Files selected for processing (2)
  • packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts (1 hunks)
  • packages/compiler-core/src/babelUtils.ts (5 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/compiler-core/src/babelUtils.ts (1)

195-220: Fix scoping: avoid collecting var in block scopes and use strict equality

  • Block-level traversal should skip var declarations; otherwise var becomes block-scoped (incorrect). Only collect var from loop initializers (function-scoped).
  • Use === instead of == for type checks.

Apply:

-export function walkBlockDeclarations(
-  block: BlockStatement | SwitchCase | Program,
-  onIdent: (node: Identifier) => void,
-): void {
-  const body = block.type === 'SwitchCase' ? block.consequent : block.body
-  for (const stmt of body) {
-    if (stmt.type === 'VariableDeclaration') {
-      if (stmt.declare) continue
-      for (const decl of stmt.declarations) {
-        for (const id of extractIdentifiers(decl.id)) {
-          onIdent(id)
-        }
-      }
-    } else if (
+export function walkBlockDeclarations(
+  block: BlockStatement | SwitchCase | Program,
+  onIdent: (node: Identifier) => void,
+): void {
+  const body = block.type === 'SwitchCase' ? block.consequent : block.body
+  for (const stmt of body) {
+    if (stmt.type === 'VariableDeclaration') {
+      if (stmt.declare) continue
+      // Only collect block-scoped bindings here; 'var' is function-scoped.
+      if (stmt.kind !== 'var') {
+        for (const decl of stmt.declarations) {
+          for (const id of extractIdentifiers(decl.id)) {
+            onIdent(id)
+          }
+        }
+      }
+    } else if (
       stmt.type === 'FunctionDeclaration' ||
       stmt.type === 'ClassDeclaration'
     ) {
       if (stmt.declare || !stmt.id) continue
       onIdent(stmt.id)
     } else if (isForStatement(stmt)) {
-      walkForStatement(stmt, true, onIdent)
-    } else if (stmt.type == 'SwitchStatement') {
-      walkSwitchStatement(stmt, true, onIdent)
+      // Capture loop initializer bindings; include 'var' so it leaks to the surrounding scope.
+      walkForStatement(stmt, true, onIdent)
+    } else if (stmt.type === 'SwitchStatement') {
+      // Delegate; inside this block we only want 'var' to bubble to this enclosing scope.
+      walkSwitchStatement(stmt, true, onIdent)
     }
   }
 }
🧹 Nitpick comments (1)
packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts (1)

720-765: Nice coverage for case bodies; please add var-in-switch and case-test reference checks

  • Add a test asserting var declared in a case is visible after the switch and not prefixed with _ctx.
  • Add a test asserting identifiers used in case <expr> are still prefixed with _ctx when not locally declared.

I can draft the exact tests to drop in this suite if you want.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b555f02 and 2f4cdd8.

📒 Files selected for processing (2)
  • packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts (1 hunks)
  • packages/compiler-core/src/babelUtils.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts (1)
packages/compiler-core/src/ast.ts (1)
  • InterpolationNode (249-252)
packages/compiler-core/src/babelUtils.ts (1)
packages/compiler-core/src/ast.ts (1)
  • BlockStatement (444-447)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed
🔇 Additional comments (2)
packages/compiler-core/src/babelUtils.ts (2)

13-15: LGTM on type imports

Adding SwitchCase and SwitchStatement types is correct.


85-88: Correct to introduce SwitchStatement handling here

This integrates switch traversal into identifier walking as needed for the fix.

@edison1105 edison1105 added scope: compiler 🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. labels Sep 24, 2025
Copy link

github-actions bot commented Sep 24, 2025

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 102 kB 38.6 kB 34.7 kB
vue.global.prod.js 160 kB 58.7 kB 52.3 kB

Usages

Name Size Gzip Brotli
createApp (CAPI only) 46.7 kB 18.3 kB 16.7 kB
createApp 54.7 kB 21.3 kB 19.5 kB
createSSRApp 58.9 kB 23 kB 21 kB
defineCustomElement 60 kB 23 kB 21 kB
overall 68.8 kB 26.5 kB 24.2 kB

Copy link

pkg-pr-new bot commented Sep 24, 2025

Open in StackBlitz

@vue/compiler-core

npm i https://pkg.pr.new/@vue/compiler-core@13923

@vue/compiler-dom

npm i https://pkg.pr.new/@vue/compiler-dom@13923

@vue/compiler-sfc

npm i https://pkg.pr.new/@vue/compiler-sfc@13923

@vue/compiler-ssr

npm i https://pkg.pr.new/@vue/compiler-ssr@13923

@vue/reactivity

npm i https://pkg.pr.new/@vue/reactivity@13923

@vue/runtime-core

npm i https://pkg.pr.new/@vue/runtime-core@13923

@vue/runtime-dom

npm i https://pkg.pr.new/@vue/runtime-dom@13923

@vue/server-renderer

npm i https://pkg.pr.new/@vue/server-renderer@13923

@vue/shared

npm i https://pkg.pr.new/@vue/shared@13923

vue

npm i https://pkg.pr.new/vue@13923

@vue/compat

npm i https://pkg.pr.new/@vue/compat@13923

commit: b457df0

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c60c2d5 and a543ec2.

📒 Files selected for processing (1)
  • packages/compiler-core/src/babelUtils.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/compiler-core/src/babelUtils.ts (1)
packages/compiler-core/src/ast.ts (1)
  • BlockStatement (444-447)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed
🔇 Additional comments (4)
packages/compiler-core/src/babelUtils.ts (4)

13-14: LGTM!

The import of SwitchCase and SwitchStatement types from @babel/types is correct and necessary for the new switch-case handling functionality.


200-203: LGTM!

The signature extension to include SwitchCase is correct, and the conditional body extraction logic properly handles the different node types.


85-92: Fix identifier scoping logic in switch statements.

The current implementation has critical scoping issues that cause local variables in switch cases to be incorrectly prefixed with _ctx. when referenced. Based on the past review comments from edison1105, this needs to follow the established pattern of checking for pre-computed scopeIds first.

Apply this diff to fix the scoping logic:

-      } else if (node.type === 'SwitchStatement') {
-        if (node.scopeIds) {
-          node.scopeIds.forEach(id => markKnownIds(id, knownIds))
-        }
-        // record switch case block-level local variables
-        walkSwitchStatement(node, false, id =>
-          markScopeIdentifier(node, id, knownIds),
-        )
+      } else if (node.type === 'SwitchStatement') {
+        if (node.scopeIds) {
+          node.scopeIds.forEach(id => markKnownIds(id, knownIds))
+        } else {
+          // record switch case block-level local variables
+          walkSwitchStatement(node, false, id =>
+            markScopeIdentifier(node, id, knownIds),
+          )
+        }

255-275: Critical: Fix switch statement scoping to avoid incorrect _ctx prefixing.

The current implementation has multiple severe scoping issues that will cause the exact bug this PR is meant to fix:

  1. Missing case test handling: The function doesn't collect identifiers from case test expressions (cs.test), but these should be treated as references, not local declarations
  2. Incorrect var scoping: var declarations inside case bodies are being treated as block-scoped instead of function-scoped
  3. Incomplete recursion: The function doesn't handle nested statements like function/class declarations or nested control structures

Based on the previous review feedback, apply this complete fix:

 function walkSwitchStatement(
   stmt: SwitchStatement,
   isVar: boolean,
   onIdent: (id: Identifier) => void,
 ) {
   for (const cs of stmt.cases) {
-    for (const stmt of cs.consequent) {
-      if (
-        stmt.type === 'VariableDeclaration' &&
-        (stmt.kind === 'var' ? isVar : !isVar)
-      ) {
-        for (const decl of stmt.declarations) {
-          for (const id of extractIdentifiers(decl.id)) {
-            onIdent(id)
+    // Only collect declarations from the case body; case tests are references.
+    for (const s of cs.consequent) {
+      if (s.type === 'VariableDeclaration') {
+        if (s.declare) continue
+        if (s.kind === 'var' ? isVar : !isVar) {
+          for (const decl of s.declarations) {
+            for (const id of extractIdentifiers(decl.id)) {
+              onIdent(id)
+            }
           }
         }
+      } else if (
+        s.type === 'FunctionDeclaration' ||
+        s.type === 'ClassDeclaration'
+      ) {
+        if (s.declare || !s.id) continue
+        onIdent(s.id)
+      } else if (isForStatement(s)) {
+        // Respect 'var' vs block-scoped loop initializers.
+        walkForStatement(s, isVar, onIdent)
+      } else if (s.type === 'SwitchStatement') {
+        // Nested switch
+        walkSwitchStatement(s, isVar, onIdent)
       }
     }
-    walkBlockDeclarations(cs, onIdent)
   }
 }

Please add tests to verify:

  1. var in switch case remains function-scoped: switch (0) { case 0: var x = 1; } console.log(x)x should not be prefixed with _ctx.
  2. Case test references are treated as references: switch (v) { case foo: break }foo should be _ctx.foo unless locally declared
  3. Block-scoped declarations work correctly: switch (0) { case 0: const y = 1; console.log(y); }y should not be prefixed with _ctx.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/compiler-core/src/babelUtils.ts (1)

199-224: Do not record var in block/switch scopes; fix walkBlockDeclarations

walkBlockDeclarations currently records function-scoped var and passes true to for/switch walkers — only block-scoped bindings should be recorded.

File: packages/compiler-core/src/babelUtils.ts (walkBlockDeclarations)

 export function walkBlockDeclarations(
   block: BlockStatement | SwitchCase | Program,
   onIdent: (node: Identifier) => void,
 ): void {
   const body = block.type === 'SwitchCase' ? block.consequent : block.body
   for (const stmt of body) {
     if (stmt.type === 'VariableDeclaration') {
       if (stmt.declare) continue
+      // Only block-scoped declarations here; 'var' is function-scoped.
+      if (stmt.kind === 'var') continue
       for (const decl of stmt.declarations) {
         for (const id of extractIdentifiers(decl.id)) {
           onIdent(id)
         }
       }
     } else if (
       stmt.type === 'FunctionDeclaration' ||
       stmt.type === 'ClassDeclaration'
     ) {
       if (stmt.declare || !stmt.id) continue
       onIdent(stmt.id)
     } else if (isForStatement(stmt)) {
-      walkForStatement(stmt, true, onIdent)
+      // Only collect block-scoped loop initializers here.
+      walkForStatement(stmt, false, onIdent)
     } else if (stmt.type === 'SwitchStatement') {
-      walkSwitchStatement(stmt, true, onIdent)
+      // Collect case-local block-scoped declarations.
+      walkSwitchStatement(stmt, false, onIdent)
     }
   }
 }

Tests to add:

  • switch (0) { case 0: var x = 1 } x → ensure no _ctx.x prefix.
  • switch (0) { case 0: for (var i = 0; i < 1; i++) {} } i → ensure no _ctx.i prefix.
🧹 Nitpick comments (2)
packages/compiler-core/src/babelUtils.ts (2)

85-92: Avoid re-walking when scopeIds already computed on SwitchStatement

Skip traversal if scopeIds exist to prevent duplicate marking and extra work.

Apply:

} else if (node.type === 'SwitchStatement') {
-  if (node.scopeIds) {
-    node.scopeIds.forEach(id => markKnownIds(id, knownIds))
-  }
-  // record switch case block-level local variables
-  walkSwitchStatement(node, false, id =>
-    markScopeIdentifier(node, id, knownIds),
-  )
+  if (node.scopeIds) {
+    node.scopeIds.forEach(id => markKnownIds(id, knownIds))
+  } else {
+    // record switch case block-level local variables
+    walkSwitchStatement(node, false, id =>
+      markScopeIdentifier(node, id, knownIds),
+    )
+  }
}

255-275: Simplify switch-case scanning and avoid misclassifying declarations

Only collect declarations from case bodies; don’t pre-scan, and rely on walkBlockDeclarations (which should ignore var) to handle functions/classes/let/const and nested structures.

Apply:

-function walkSwitchStatement(
-  stmt: SwitchStatement,
-  isVar: boolean,
-  onIdent: (id: Identifier) => void,
-) {
-  for (const cs of stmt.cases) {
-    for (const stmt of cs.consequent) {
-      if (
-        stmt.type === 'VariableDeclaration' &&
-        (stmt.kind === 'var' ? isVar : !isVar)
-      ) {
-        for (const decl of stmt.declarations) {
-          for (const id of extractIdentifiers(decl.id)) {
-            onIdent(id)
-          }
-        }
-      }
-    }
-    walkBlockDeclarations(cs, onIdent)
-  }
-}
+function walkSwitchStatement(
+  stmt: SwitchStatement,
+  _isVar: boolean,
+  onIdent: (id: Identifier) => void,
+) {
+  for (const cs of stmt.cases) {
+    // Case test expressions are references, not declarations.
+    walkBlockDeclarations(cs, onIdent)
+  }
+}

Also add tests to assert that case foo: treats foo as a reference (gets _ctx.foo unless locally declared).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a543ec2 and f5f832f.

📒 Files selected for processing (2)
  • packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts (1 hunks)
  • packages/compiler-core/src/babelUtils.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/compiler-core/tests/transforms/transformExpressions.spec.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/compiler-core/src/babelUtils.ts (1)
packages/compiler-core/src/ast.ts (1)
  • BlockStatement (444-447)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed
🔇 Additional comments (1)
packages/compiler-core/src/babelUtils.ts (1)

13-15: Type imports for SwitchCase/SwitchStatement: LGTM

Accurate additions for switch traversal utilities.

@wngtk wngtk requested a review from edison1105 September 24, 2025 06:43
@edison1105 edison1105 merged commit 5953c9f into vuejs:main Sep 24, 2025
12 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. scope: compiler
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants