Add CaseElseBranch AST node for Ruby case else branches

This commit is contained in:
copilot-swe-agent[bot]
2026-06-15 11:42:13 +00:00
committed by GitHub
parent ab4f170780
commit 8cb4b9b118
7 changed files with 138 additions and 17 deletions

View File

@@ -0,0 +1,4 @@
---
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()`.

View File

@@ -377,18 +377,18 @@ class CaseExpr extends ControlExpr instanceof CaseExprImpl {
/**
* Gets the `n`th branch of this case expression, either a `WhenClause`, an
* `InClause`, or a `StmtSequence`.
* `InClause`, or a `CaseElseBranch`.
*/
final AstNode getBranch(int n) { result = super.getBranch(n) }
/**
* Gets a branch of this case expression, either a `WhenClause`, an
* `InClause`, or a `StmtSequence`.
* `InClause`, or a `CaseElseBranch`.
*/
final AstNode getABranch() { result = this.getBranch(_) }
/** Gets the `else` branch of this case expression, if any. */
final StmtSequence getElseBranch() { result = this.getABranch() }
final CaseElseBranch getElseBranch() { result = this.getABranch() }
/**
* Gets the number of branches of this case expression.
@@ -533,6 +533,30 @@ class InClause extends AstNode instanceof InClauseImpl {
}
}
/**
* An `else` branch of a `case` expression.
* ```rb
* case foo
* when 1 then puts "one"
* else puts "other"
* end
* ```
*/
class CaseElseBranch extends AstNode instanceof CaseElseBranchImpl {
final override string getAPrimaryQlClass() { result = "CaseElseBranch" }
/** Gets the body of this else branch. */
final Stmt getBody() { result = super.getBody() }
final override string toString() { result = "else ..." }
final override AstNode getAChild(string pred) {
result = AstNode.super.getAChild(pred)
or
pred = "getBody" and result = this.getBody()
}
}
/**
* A one-line pattern match using the `=>` operator. For example:
* ```rb

View File

@@ -113,6 +113,9 @@ private module Cached {
TBraceBlockSynth(Ast::AstNode parent, int i) { mkSynthChild(BraceBlockKind(), parent, i) } or
TBraceBlockReal(Ruby::Block g) { not g.getParent() instanceof Ruby::Lambda } or
TBreakStmt(Ruby::Break g) or
TCaseElseBranchSynth(Ast::AstNode parent, int i) {
mkSynthChild(CaseElseBranchKind(), parent, i)
} or
TCaseEqExpr(Ruby::Binary g) { g instanceof @ruby_binary_equalequalequal } or
TCaseExpr(Ruby::Case g) or
TCaseMatchReal(Ruby::CaseMatch g) or
@@ -400,14 +403,15 @@ private module Cached {
class TAstNodeSynth =
TAddExprSynth or TAssignExprSynth or TBitwiseAndExprSynth or TBitwiseOrExprSynth or
TBitwiseXorExprSynth or TBraceBlockSynth or TBodyStmtSynth or TBooleanLiteralSynth or
TCaseMatchSynth or TClassVariableAccessSynth or TConstantReadAccessSynth or
TConstantWriteAccessSynth or TDivExprSynth or TElseSynth or TExponentExprSynth or
TGlobalVariableAccessSynth or TIfSynth or TInClauseSynth or TInstanceVariableAccessSynth or
TIntegerLiteralSynth or TLShiftExprSynth or TLocalVariableAccessSynth or
TLogicalAndExprSynth or TLogicalOrExprSynth or TMethodCallSynth or TModuloExprSynth or
TMulExprSynth or TNilLiteralSynth or TRShiftExprSynth or TRangeLiteralSynth or TSelfSynth or
TSimpleParameterSynth or TSplatExprSynth or THashSplatExprSynth or TStmtSequenceSynth or
TSubExprSynth or TPairSynth or TSimpleSymbolLiteralSynth;
TCaseElseBranchSynth or TCaseMatchSynth or TClassVariableAccessSynth or
TConstantReadAccessSynth or TConstantWriteAccessSynth or TDivExprSynth or TElseSynth or
TExponentExprSynth or TGlobalVariableAccessSynth or TIfSynth or TInClauseSynth or
TInstanceVariableAccessSynth or TIntegerLiteralSynth or TLShiftExprSynth or
TLocalVariableAccessSynth or TLogicalAndExprSynth or TLogicalOrExprSynth or
TMethodCallSynth or TModuloExprSynth or TMulExprSynth or TNilLiteralSynth or
TRShiftExprSynth or TRangeLiteralSynth or TSelfSynth or TSimpleParameterSynth or
TSplatExprSynth or THashSplatExprSynth or TStmtSequenceSynth or TSubExprSynth or
TPairSynth or TSimpleSymbolLiteralSynth;
/**
* Gets the underlying TreeSitter entity for a given AST node. This does not
@@ -598,6 +602,8 @@ private module Cached {
or
result = TBraceBlockSynth(parent, i)
or
result = TCaseElseBranchSynth(parent, i)
or
result = TCaseMatchSynth(parent, i)
or
result = TClassVariableAccessSynth(parent, i, _)
@@ -718,6 +724,8 @@ TAstNodeReal fromGenerated(Ruby::AstNode n) { n = toGenerated(result) }
class TCall = TMethodCall or TYieldCall;
class TCaseElseBranch = TCaseElseBranchSynth;
class TCaseMatch = TCaseMatchReal or TCaseMatchSynth;
class TCase = TCaseExpr or TCaseMatch;

View File

@@ -19,8 +19,11 @@ class CaseWhenClause extends CaseExprImpl, TCaseExpr {
final override Expr getValue() { toGenerated(result) = g.getValue() }
final override AstNode getBranch(int n) {
toGenerated(result) = g.getChild(n) or
toGenerated(result) = g.getChild(n)
// When branches map directly to WhenClause nodes
toGenerated(result) = g.getChild(n) and not g.getChild(n) instanceof Ruby::Else
or
// The else branch is wrapped in a synthesized CaseElseBranch node
g.getChild(n) instanceof Ruby::Else and result = getSynthChild(this, n)
}
}
@@ -34,7 +37,8 @@ class CaseMatch extends CaseExprImpl, TCaseMatchReal {
final override AstNode getBranch(int n) {
toGenerated(result) = g.getClauses(n)
or
n = count(g.getClauses(_)) and toGenerated(result) = g.getElse()
// The else branch is wrapped in a synthesized CaseElseBranch node
n = count(g.getClauses(_)) and exists(g.getElse()) and result = getSynthChild(this, n)
}
}
@@ -87,3 +91,9 @@ class InClauseSynth extends InClauseImpl, TInClauseSynth {
final override predicate hasUnlessCondition() { none() }
}
class CaseElseBranchImpl extends AstNode, TCaseElseBranch {
CaseElseBranchImpl() { this = TCaseElseBranchSynth(_, _) }
final Stmt getBody() { synthChild(this, 0, result) }
}

View File

@@ -22,6 +22,7 @@ newtype TSynthKind =
BodyStmtKind() or
BooleanLiteralKind(boolean value) { value = true or value = false } or
BraceBlockKind() or
CaseElseBranchKind() or
CaseMatchKind() or
ClassVariableAccessKind(ClassVariable v) or
DefinedExprKind() or
@@ -80,6 +81,8 @@ class SynthKind extends TSynthKind {
or
this = BraceBlockKind() and result = "BraceBlockKind"
or
this = CaseElseBranchKind() and result = "CaseElseBranchKind"
or
this = CaseMatchKind() and result = "CaseMatchKind"
or
this = ClassVariableAccessKind(_) and result = "ClassVariableAccessKind"
@@ -1840,7 +1843,7 @@ private module TestPatternDesugar {
or
child = SynthChild(InClauseKind()) and i = 1
or
child = SynthChild(ElseKind()) and i = 2
child = SynthChild(CaseElseBranchKind()) and i = 2
)
or
parent = TInClauseSynth(case, 1) and
@@ -1851,7 +1854,11 @@ private module TestPatternDesugar {
child = SynthChild(BooleanLiteralKind(true)) and i = 1
)
or
parent = TElseSynth(case, 2) and
parent = TCaseElseBranchSynth(case, 2) and
child = SynthChild(ElseKind()) and
i = 0
or
parent = TElseSynth(TCaseElseBranchSynth(case, 2), 0) and
child = SynthChild(BooleanLiteralKind(false)) and
i = 0
)
@@ -1994,3 +2001,61 @@ private module CallableBodySynthesis {
}
}
}
private module CaseElseBranchSynthesis {
pragma[nomagic]
private predicate caseElseBranchSynthesis(AstNode parent, int i, Child child) {
// Wrap the else branch of a real `case`/`when` expression
exists(Ruby::Case g, Ruby::Else elseNode, int elseIndex |
elseNode = g.getChild(elseIndex) and
(
// Create the CaseElseBranch wrapper node at the else index
parent = TCaseExpr(g) and
child = SynthChild(CaseElseBranchKind()) and
i = elseIndex
or
// The body of the CaseElseBranch is the Else node
parent = TCaseElseBranchSynth(TCaseExpr(g), elseIndex) and
child = RealChildRef(TElseReal(elseNode)) and
i = 0
)
)
or
// Wrap the else branch of a real `case`/`in` expression
exists(Ruby::CaseMatch g, Ruby::Else elseNode, int elseIndex |
elseNode = g.getElse() and
elseIndex = count(g.getClauses(_)) and
(
// Create the CaseElseBranch wrapper node at the else index
parent = TCaseMatchReal(g) and
child = SynthChild(CaseElseBranchKind()) and
i = elseIndex
or
// The body of the CaseElseBranch is the Else node
parent = TCaseElseBranchSynth(TCaseMatchReal(g), elseIndex) and
child = RealChildRef(TElseReal(elseNode)) and
i = 0
)
)
}
private class CaseElseBranchSynthesisImpl extends Synthesis {
final override predicate child(AstNode parent, int i, Child child) {
caseElseBranchSynthesis(parent, i, child)
}
final override predicate location(AstNode n, Location l) {
// Give the CaseElseBranch the location of the underlying Else node
exists(Ruby::Case g, int elseIndex |
n = TCaseElseBranchSynth(TCaseExpr(g), elseIndex) and
l = g.getChild(elseIndex).getLocation()
)
or
exists(Ruby::CaseMatch g, int elseIndex |
elseIndex = count(g.getClauses(_)) and
n = TCaseElseBranchSynth(TCaseMatchReal(g), elseIndex) and
l = g.getElse().getLocation()
)
}
}
}

View File

@@ -498,6 +498,16 @@ module Trees {
}
}
private class CaseElseBranchTree extends ControlFlowTree instanceof CaseElseBranch {
final override predicate propagatesAbnormal(AstNode child) { child = super.getBody() }
final override predicate first(AstNode first) { first(super.getBody(), first) }
final override predicate last(AstNode last, Completion c) { last(super.getBody(), last, c) }
final override predicate succ(AstNode pred, AstNode succ, Completion c) { none() }
}
private class PatternVariableAccessTree extends LocalVariableAccessTree instanceof LocalVariableWriteAccess,
CasePattern
{

View File

@@ -4,7 +4,7 @@ query predicate caseValues(CaseExpr c, Expr value) { value = c.getValue() }
query predicate caseNoValues(CaseExpr c) { not exists(c.getValue()) }
query predicate caseElseBranches(CaseExpr c, StmtSequence elseBranch) {
query predicate caseElseBranches(CaseExpr c, CaseElseBranch elseBranch) {
elseBranch = c.getElseBranch()
}