Skip to content

Ruby: Add CaseElseBranch AST node to distinguish else-branch from its body#21991

Open
Copilot wants to merge 4 commits into
mainfrom
copilot/change-ast-for-else-branches
Open

Ruby: Add CaseElseBranch AST node to distinguish else-branch from its body#21991
Copilot wants to merge 4 commits into
mainfrom
copilot/change-ast-for-else-branches

Conversation

Copilot AI commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

In Ruby's case expression, the else branch was directly represented as a StmtSequence, unlike WhenClause and InClause which both have a body distinct from the branch node itself. This inconsistency made it impossible to refer to the else-branch as a node separate from its contents.

Changes

New AST node: CaseElseBranch

  • CaseExpr.getElseBranch() now returns a CaseElseBranch instead of StmtSequence
  • CaseElseBranch.getBody() returns the StmtSequence body — mirrors WhenClause.getBody() / InClause.getBody()
case foo
when 1 then puts "one"
else puts "other"   # <- CaseElseBranch node; getBody() returns the StmtSequence
end

Implementation (synthesized node approach)

  • Added CaseElseBranchKind / TCaseElseBranchSynth to the synthesis system — a wrapper node injected between the CaseExpr and the existing Else/StmtSequence real node, preserving location from the underlying tree-sitter Else node
  • CaseWhenClause.getBranch and CaseMatch.getBranch now return the synthesized wrapper instead of the raw Else node for the else index
  • TestPatternDesugar (expr in pattern) updated to wrap its synthesized ElseSynth in a CaseElseBranchSynth

CFG

  • Added CaseElseBranchTree — transparent to control flow; delegates first/last to the body

Breaking change

  • CaseExpr.getElseBranch() return type changed from StmtSequence to CaseElseBranch; callers needing the body should call .getBody()

}
}

private class CaseElseBranchTree extends ControlFlowTree instanceof CaseElseBranch {
@aschackmull aschackmull marked this pull request as ready for review June 16, 2026 09:30
@aschackmull aschackmull requested a review from a team as a code owner June 16, 2026 09:30
Copilot AI review requested due to automatic review settings June 16, 2026 09:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request updates the Ruby AST model for case expressions to represent the else branch as a distinct CaseElseBranch node (separate from its body), aligning it more closely with WhenClause / InClause structure and enabling callers to refer to the else-branch node itself.

Changes:

  • Introduces CaseElseBranch as a new AST node and updates CaseExpr.getElseBranch() to return it (breaking change).
  • Implements CaseElseBranch via synthesized-node wrapping in the AST synthesis system, with corresponding internal AST updates.
  • Updates control-flow and dataflow handling plus library tests/expected outputs and adds a breaking change note.
Show a summary per file
File Description
ruby/ql/test/library-tests/modules/modules.expected Updates expected module-enclosing output for the new else-branch node.
ruby/ql/test/library-tests/modules/methods.expected Updates expected method-enclosing output for the new else-branch node.
ruby/ql/test/library-tests/ast/control/CaseExpr.ql Adjusts library-test query to reflect getElseBranch()’s new return type.
ruby/ql/test/library-tests/ast/AstDesugar.expected Updates desugaring AST expectations to include CaseElseBranch and its body.
ruby/ql/test/library-tests/ast/Ast.expected Updates AST expectations to show CaseElseBranch wrapping the else body.
ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPrivate.qll Updates case-branch last-eval logic to follow else branch via .getElseBranch().getBody().
ruby/ql/lib/codeql/ruby/controlflow/internal/ControlFlowGraphImpl.qll Adds a transparent CFG tree wrapper for CaseElseBranch delegating to its body.
ruby/ql/lib/codeql/ruby/ast/internal/Synthesis.qll Adds synth kind + synthesis wiring for CaseElseBranch and updates pattern desugar synthesis.
ruby/ql/lib/codeql/ruby/ast/internal/Control.qll Wraps case/in else branches with the synthesized CaseElseBranch node; adds implementation class.
ruby/ql/lib/codeql/ruby/ast/internal/AST.qll Registers TCaseElseBranchSynth in cached AST node kinds/classes.
ruby/ql/lib/codeql/ruby/ast/Control.qll Exposes public CaseElseBranch API and updates CaseExpr branch/else docs and return type.
ruby/ql/lib/change-notes/2026-06-15-case-else-branch.md Adds breaking-change release note for the getElseBranch() return type change.

Copilot's findings

  • Files reviewed: 12/12 changed files
  • Comments generated: 1

---
category: breaking
---
* The `else` branch of a `case` expression is no longer represented as a `StmtSequence` directly. Instead, a new `CaseElseBranch` AST node wraps the body (a `StmtSequence`). `CaseExpr.getElseBranch()` now returns a `CaseElseBranch`, and the body of the else branch can be accessed via `CaseElseBranch.getBody()`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants