Skip to content

Commit 66eaeeb

Browse files
authored
Migrate Go type switches to shared CFG switch model (Option B)
1 parent 587c4b3 commit 66eaeeb

3 files changed

Lines changed: 128 additions & 177 deletions

File tree

go/ql/lib/semmle/go/controlflow/ControlFlowGraphShared.qll

Lines changed: 63 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,19 @@ module GoCfg {
6868
or
6969
e.getParent*() = any(Go::ArrayTypeExpr ate).getLength()
7070
or
71-
// The body block of an expression switch is transparent: the shared
72-
// switch model wires control flow directly from the switch to its case
73-
// clauses (in control-flow order) and between cases, so the enclosing
74-
// block must not introduce its own nodes or default left-to-right
75-
// sequencing of the case clauses.
76-
e = any(Go::ExpressionSwitchStmt sw).getBody()
71+
// The body block of a switch (expression or type) is transparent: the
72+
// shared switch model wires control flow directly from the switch to its
73+
// case clauses (in control-flow order) and between cases, so the
74+
// enclosing block must not introduce its own nodes or default
75+
// left-to-right sequencing of the case clauses.
76+
e = any(Go::SwitchStmt sw).getBody()
77+
or
78+
// The test statement of a type switch (`y := x.(type)` or the bare
79+
// `x.(type)` expression statement) is transparent: the shared switch
80+
// model evaluates the underlying type-assertion expression directly as
81+
// the switch expression (see `Switch.getExpr`), so the wrapping
82+
// statement must not introduce its own assignment or expression nodes.
83+
e = any(Go::TypeSwitchStmt ts).getTest()
7784
}
7885

7986
AstNode getChild(AstNode n, int index) {
@@ -82,13 +89,18 @@ module GoCfg {
8289
not skipCfg(n) and
8390
result = n.getChild(index)
8491
or
85-
// The body block of an expression switch is transparent (see `skipCfg`),
86-
// so it is not itself a child and contributes no children. Expose the
87-
// case clauses directly as children of the switch instead, so that the
88-
// AST child chain stays connected for abrupt-completion propagation
89-
// (e.g. a panicking call in a case body reaching the enclosing
90-
// function's exceptional exit).
91-
result = n.(Go::ExpressionSwitchStmt).getBody().getChild(index)
92+
// The body block of a switch (expression or type) is transparent (see
93+
// `skipCfg`), so it is not itself a child and contributes no children.
94+
// Expose the case clauses directly as children of the switch instead,
95+
// so that the AST child chain stays connected for abrupt-completion
96+
// propagation (e.g. a panicking call in a case body reaching the
97+
// enclosing function's exceptional exit).
98+
result = n.(Go::SwitchStmt).getBody().getChild(index)
99+
or
100+
// The type-switch test statement is transparent (see `skipCfg`), so
101+
// expose the underlying type-assertion expression directly as a child
102+
// of the type switch, keeping the AST child chain connected.
103+
result = n.(Go::TypeSwitchStmt).getExpr() and index = 1
92104
) and
93105
not skipCfg(result)
94106
}
@@ -141,7 +153,17 @@ module GoCfg {
141153
}
142154
}
143155

144-
class ExprStmt = Go::ExprStmt;
156+
class ExprStmt extends Stmt instanceof Go::ExprStmt {
157+
// The `x.(type)` test statement of a type switch is transparent (see
158+
// `skipCfg`): the shared switch model evaluates the underlying
159+
// type-assertion expression directly as the switch expression. It must
160+
// therefore not be treated as an ordinary expression statement, whose
161+
// value would otherwise be propagated from the expression to the
162+
// statement (creating a spurious flow into the transparent wrapper).
163+
ExprStmt() { not this = any(Go::TypeSwitchStmt ts).getTest() }
164+
165+
Expr getExpr() { result = Go::ExprStmt.super.getExpr() }
166+
}
145167

146168
class IfStmt extends Stmt {
147169
IfStmt() { this instanceof Go::IfStmt }
@@ -234,11 +256,19 @@ module GoCfg {
234256
}
235257

236258
class Switch extends AstNode {
237-
Switch() { this instanceof Go::ExpressionSwitchStmt }
259+
Switch() { this instanceof Go::SwitchStmt }
238260

239-
Expr getExpr() { result = this.(Go::ExpressionSwitchStmt).getExpr() }
261+
Expr getExpr() {
262+
result = this.(Go::ExpressionSwitchStmt).getExpr()
263+
or
264+
// For a type switch the "switch expression" is the type-assertion
265+
// expression `x.(type)`; evaluating it directly (rather than the
266+
// wrapping `y := x.(type)` statement) lets the shared switch model
267+
// drive the per-case type tests.
268+
result = this.(Go::TypeSwitchStmt).getExpr()
269+
}
240270

241-
Case getCase(int index) { result = this.(Go::ExpressionSwitchStmt).getCase(index) }
271+
Case getCase(int index) { result = this.(Go::SwitchStmt).getCase(index) }
242272

243273
Stmt getStmt(int index) {
244274
// Go nests each case clause's body statements under the clause rather
@@ -256,7 +286,7 @@ module GoCfg {
256286
}
257287

258288
class Case extends AstNode {
259-
Case() { this = any(Go::ExpressionSwitchStmt sw).getACase() }
289+
Case() { this = any(Go::SwitchStmt sw).getACase() }
260290

261291
AstNode getPattern(int index) { result = this.(Go::CaseClause).getExpr(index) }
262292

@@ -270,7 +300,7 @@ module GoCfg {
270300
}
271301

272302
/** Gets the initializer of `switch` statement `switch`, if any. */
273-
AstNode getSwitchInit(Switch switch) { result = switch.(Go::ExpressionSwitchStmt).getInit() }
303+
AstNode getSwitchInit(Switch switch) { result = switch.(Go::SwitchStmt).getInit() }
274304

275305
/**
276306
* Go has no implicit fall-through between case clauses; a case that runs to
@@ -287,12 +317,10 @@ module GoCfg {
287317

288318
/**
289319
* Holds if `s` is the flattened body element at position (`caseIdx`,
290-
* `inner`) of expression switch `sw`: either the `caseIdx`-th case clause
291-
* itself (with `inner` = -1) or its `inner`-th body statement.
320+
* `inner`) of switch `sw`: either the `caseIdx`-th case clause itself (with
321+
* `inner` = -1) or its `inner`-th body statement.
292322
*/
293-
private predicate switchFlatItem(
294-
Go::ExpressionSwitchStmt sw, Go::Stmt s, int caseIdx, int inner
295-
) {
323+
private predicate switchFlatItem(Go::SwitchStmt sw, Go::Stmt s, int caseIdx, int inner) {
296324
s = sw.getCase(caseIdx) and inner = -1
297325
or
298326
s = sw.getCase(caseIdx).getStmt(inner)
@@ -469,6 +497,11 @@ module GoCfg {
469497
or
470498
notBlankIdent(n.(Go::RangeStmt).getValue()) and i = 1
471499
) and
500+
// The `y := x.(type)` test statement of a type switch is transparent
501+
// (see `skipCfg`): the per-case implicit variables are written at the
502+
// case match nodes (see `IR::TypeSwitchImplicitVariableInstruction`),
503+
// so the guard itself emits no assignment write node.
504+
not n = any(Go::TypeSwitchStmt ts).getTest() and
472505
tag = "assign:" + i.toString()
473506
)
474507
or
@@ -559,14 +592,6 @@ module GoCfg {
559592
not exists(n.(Go::SliceExpr).getMax()) and
560593
tag = "implicit-max"
561594
or
562-
// Type switch implicit variable
563-
exists(Go::TypeSwitchStmt ts, Go::DefineStmt ds |
564-
ds = ts.getAssign() and
565-
n.(Go::CaseClause) = ts.getACase() and
566-
exists(n.(Go::CaseClause).getImplicitlyDeclaredVariable()) and
567-
tag = "type-switch-var"
568-
)
569-
or
570595
// Literal element initialization
571596
n = any(Go::CompositeLit lit).getAnElement() and
572597
tag = "lit-init"
@@ -943,7 +968,6 @@ module GoCfg {
943968

944969
predicate step(PreControlFlowNode n1, PreControlFlowNode n2) {
945970
rangeLoop(n1, n2) or
946-
switchStmt(n1, n2) or
947971
selectStmt(n1, n2) or
948972
deferStmt(n1, n2) or
949973
goStmtStep(n1, n2) or
@@ -1007,7 +1031,12 @@ module GoCfg {
10071031
assgn instanceof Go::Assignment and not assgn instanceof Go::RecvStmt
10081032
or
10091033
assgn instanceof Go::ValueSpec
1010-
)
1034+
) and
1035+
// The `y := x.(type)` test statement of a type switch is transparent
1036+
// (see `skipCfg`); the shared switch model evaluates the underlying
1037+
// type-assertion expression directly, so this statement has no
1038+
// assignment flow of its own.
1039+
not assgn = any(Go::TypeSwitchStmt ts).getTest()
10111040
|
10121041
// Route through children (LHS names, RHS expressions)
10131042
childSequenceStep(assgn, n1, n2)
@@ -1429,119 +1458,6 @@ module GoCfg {
14291458
)
14301459
}
14311460

1432-
private predicate switchStmt(PreControlFlowNode n1, PreControlFlowNode n2) {
1433-
typeSwitch(n1, n2) or typeCaseClause(n1, n2)
1434-
}
1435-
1436-
private predicate switchCasesStartOrAfter(Go::TypeSwitchStmt sw, PreControlFlowNode n) {
1437-
n.isBefore(sw.getNonDefaultCase(0))
1438-
or
1439-
not exists(sw.getANonDefaultCase()) and n.isBefore(sw.getDefault())
1440-
or
1441-
not exists(sw.getACase()) and n.isAfter(sw)
1442-
}
1443-
1444-
private predicate typeSwitch(PreControlFlowNode n1, PreControlFlowNode n2) {
1445-
exists(Go::TypeSwitchStmt sw |
1446-
n1.isBefore(sw) and
1447-
(
1448-
n2.isBefore(sw.getInit())
1449-
or
1450-
not exists(sw.getInit()) and n2.isBefore(sw.getTest())
1451-
)
1452-
or
1453-
n1.isAfter(sw.getInit()) and n2.isBefore(sw.getTest())
1454-
or
1455-
n1.isAfter(sw.getTest()) and switchCasesStartOrAfter(sw, n2)
1456-
)
1457-
}
1458-
1459-
/**
1460-
* Holds if `cc` is a case clause of a type switch with an assignment that
1461-
* implicitly declares a variable whose type narrows to the case type. In
1462-
* this situation the CFG inserts a `type-switch-var` additional node
1463-
* between the case test and the case body, on which the IR layer
1464-
* materialises the implicit assignment to that variable.
1465-
*/
1466-
private predicate hasTypeSwitchVar(Go::CaseClause cc) {
1467-
exists(Go::TypeSwitchStmt ts |
1468-
ts.getACase() = cc and
1469-
exists(ts.getAssign()) and
1470-
exists(cc.getImplicitlyDeclaredVariable())
1471-
)
1472-
}
1473-
1474-
private predicate typeCaseClause(PreControlFlowNode n1, PreControlFlowNode n2) {
1475-
exists(Go::TypeSwitchStmt sw, Go::CaseClause cc, int i | cc = sw.getNonDefaultCase(i) |
1476-
n1.isBefore(cc) and n2.isBefore(cc.getExpr(0))
1477-
or
1478-
// A type switch is not boolean, so each case type test has a single
1479-
// "after" node from which control flows both to the case body (on a
1480-
// match) and on to the next test (on a mismatch).
1481-
exists(int j | n1.isAfter(cc.getExpr(j)) and n2.isBefore(cc.getExpr(j + 1)))
1482-
or
1483-
exists(int last | last = max(int j | exists(cc.getExpr(j))) |
1484-
n1.isAfter(cc.getExpr(last)) and
1485-
(
1486-
hasTypeSwitchVar(cc) and n2.isAdditional(cc, "type-switch-var")
1487-
or
1488-
not hasTypeSwitchVar(cc) and
1489-
(
1490-
n2.isBefore(cc.getStmt(0))
1491-
or
1492-
not exists(cc.getStmt(0)) and n2.isAfter(sw)
1493-
)
1494-
)
1495-
or
1496-
n1.isAfter(cc.getExpr(last)) and
1497-
(
1498-
n2.isBefore(sw.getNonDefaultCase(i + 1))
1499-
or
1500-
not exists(sw.getNonDefaultCase(i + 1)) and n2.isBefore(sw.getDefault())
1501-
or
1502-
not exists(sw.getNonDefaultCase(i + 1)) and
1503-
not exists(sw.getDefault()) and
1504-
n2.isAfter(sw)
1505-
)
1506-
)
1507-
)
1508-
or
1509-
exists(Go::TypeSwitchStmt sw, Go::CaseClause def | def = sw.getDefault() |
1510-
n1.isBefore(def) and
1511-
(
1512-
hasTypeSwitchVar(def) and n2.isAdditional(def, "type-switch-var")
1513-
or
1514-
not hasTypeSwitchVar(def) and
1515-
(
1516-
n2.isBefore(def.getStmt(0))
1517-
or
1518-
not exists(def.getStmt(0)) and n2.isAfter(sw)
1519-
)
1520-
)
1521-
)
1522-
or
1523-
exists(Go::TypeSwitchStmt sw, Go::CaseClause cc |
1524-
sw.getACase() = cc and
1525-
hasTypeSwitchVar(cc) and
1526-
n1.isAdditional(cc, "type-switch-var") and
1527-
(
1528-
n2.isBefore(cc.getStmt(0))
1529-
or
1530-
not exists(cc.getStmt(0)) and n2.isAfter(sw)
1531-
)
1532-
)
1533-
or
1534-
exists(Go::TypeSwitchStmt sw, Go::CaseClause cc | cc = sw.getACase() |
1535-
exists(int j | n1.isAfter(cc.getStmt(j)) and n2.isBefore(cc.getStmt(j + 1)))
1536-
or
1537-
exists(int last |
1538-
last = max(int j | exists(cc.getStmt(j))) and
1539-
n1.isAfter(cc.getStmt(last)) and
1540-
n2.isAfter(sw)
1541-
)
1542-
)
1543-
}
1544-
15451461
private predicate commClauseBodyStart(
15461462
Go::SelectStmt sel, Go::CommClause cc, PreControlFlowNode n
15471463
) {

go/ql/lib/semmle/go/controlflow/IR.qll

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module;
1414

1515
import go
1616
private import ControlFlowGraphShared
17+
private import codeql.controlflow.SuccessorType
1718

1819
/** Provides predicates and classes for working with IR constructs. */
1920
module IR {
@@ -43,6 +44,20 @@ module IR {
4344
e = any(ParenExpr pe | isInBooleanCondContext(pe)).getExpr()
4445
}
4546

47+
/**
48+
* Holds if `n` is the control-flow node representing a successful match of
49+
* the type-switch case clause `cc` that implicitly declares a variable.
50+
*
51+
* This node dominates the case body and is where the implicit per-case
52+
* variable declaration/assignment is materialised (see
53+
* `TypeSwitchImplicitVariableInstruction`).
54+
*/
55+
private predicate typeSwitchCaseMatch(ControlFlow::Node n, CaseClause cc) {
56+
cc = any(TypeSwitchStmt ts).getACase() and
57+
exists(cc.getImplicitlyDeclaredVariable()) and
58+
n.isAfterValue(cc, any(MatchingSuccessor t | t.getValue() = true))
59+
}
60+
4661
/**
4762
* An IR instruction.
4863
*/
@@ -56,6 +71,11 @@ module IR {
5671
or
5772
this.isAfterFalse(_)
5873
or
74+
// The successful-match node of a type-switch case that binds an implicit
75+
// variable hosts that variable's declaration/assignment (see
76+
// `TypeSwitchImplicitVariableInstruction`).
77+
typeSwitchCaseMatch(this, _)
78+
or
5979
// `NotExpr` and `LogicalBinaryExpr` are not in `postOrInOrder`, so they
6080
// have no `isIn` node. When such an expression is not in a conditional
6181
// context (so it has a single combined after-node rather than per-branch
@@ -1182,7 +1202,7 @@ module IR {
11821202
class TypeSwitchImplicitVariableInstruction extends Instruction {
11831203
CaseClause cc;
11841204

1185-
TypeSwitchImplicitVariableInstruction() { this.isAdditional(cc, "type-switch-var") }
1205+
TypeSwitchImplicitVariableInstruction() { typeSwitchCaseMatch(this, cc) }
11861206

11871207
override predicate writes(ValueEntity v, Instruction rhs) {
11881208
v = cc.getImplicitlyDeclaredVariable() and

0 commit comments

Comments
 (0)