Skip to content

Commit f44bece

Browse files
committed
Implement multiple pattern case and fall-through pattern case support
1 parent c283894 commit f44bece

File tree

9 files changed

+161
-51
lines changed

9 files changed

+161
-51
lines changed

java/ql/lib/semmle/code/java/ControlFlowGraph.qll

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -489,14 +489,14 @@ private module ControlFlowGraphImpl {
489489
private Stmt getSwitchStatement(SwitchBlock switch, int i) { result.isNthChildOf(switch, i) }
490490

491491
/**
492-
* Holds if `last` is the last node in a pattern case `pc`'s succeeding bind-and-test operation,
492+
* Holds if `last` is the last node in any of pattern case `pc`'s succeeding bind-and-test operations,
493493
* immediately before either falling through to execute successor statements or execute a rule body
494494
* if present. `completion` is the completion kind of the last operation.
495495
*/
496496
private predicate lastPatternCaseMatchingOp(
497497
PatternCase pc, ControlFlowNode last, Completion completion
498498
) {
499-
last(pc.getPattern(), last, completion) and
499+
last(pc.getAPattern(), last, completion) and
500500
completion = NormalCompletion() and
501501
not exists(pc.getGuard())
502502
or
@@ -776,6 +776,19 @@ private module ControlFlowGraphImpl {
776776
last(try.getFinally(), last, NormalCompletion())
777777
}
778778

779+
private predicate isNextNormalSwitchStmt(SwitchBlock switch, Stmt pred, Stmt succ) {
780+
exists(int i, Stmt immediateSucc |
781+
getSwitchStatement(switch, i) = pred and
782+
getSwitchStatement(switch, i + 1) = immediateSucc and
783+
(
784+
succ = immediateSucc and
785+
not immediateSucc instanceof PatternCase
786+
or
787+
isNextNormalSwitchStmt(switch, immediateSucc, succ)
788+
)
789+
)
790+
}
791+
779792
/**
780793
* Bind `last` to a cfg node nested inside `n` (or, indeed, `n` itself) such
781794
* that `last` may be the last node during an execution of `n` and finish
@@ -927,9 +940,15 @@ private module ControlFlowGraphImpl {
927940
completion != anonymousBreakCompletion() and
928941
not completion instanceof NormalOrBooleanCompletion
929942
or
930-
// if the last case completes normally, then so does the switch
931-
last(switch.getStmt(strictcount(switch.getAStmt()) - 1), last, NormalCompletion()) and
932-
completion = NormalCompletion()
943+
// if a statement without a non-pattern-case successor completes normally (or for a pattern case
944+
// the guard succeeds) then the switch completes normally.
945+
exists(Stmt lastNormalStmt, Completion stmtCompletion |
946+
lastNormalStmt = getSwitchStatement(switch, _) and
947+
not isNextNormalSwitchStmt(switch, lastNormalStmt, _) and
948+
last(lastNormalStmt, last, stmtCompletion) and
949+
(stmtCompletion = NormalCompletion() or stmtCompletion = BooleanCompletion(true, _)) and
950+
completion = NormalCompletion()
951+
)
933952
or
934953
// if no default case exists, then normal completion of the expression may terminate the switch
935954
// Note this can't happen if there are pattern cases or a null literal, as
@@ -973,9 +992,9 @@ private module ControlFlowGraphImpl {
973992
)
974993
or
975994
// A pattern case statement can complete:
976-
// * On failure of its type test (boolean false)
995+
// * On failure of its final type test (boolean false)
977996
// * On failure of its guard test if any (boolean false)
978-
// * On completion of its variable declarations, if it is not a rule and has no guard (normal completion)
997+
// * On completion of one of its pattern variable declarations, if it is not a rule and has no guard (normal completion)
979998
// * On success of its guard test, if it is not a rule (boolean true)
980999
// (the latter two cases are accounted for by lastPatternCaseMatchingOp)
9811000
exists(PatternCase pc | n = pc |
@@ -1315,9 +1334,13 @@ private module ControlFlowGraphImpl {
13151334
// Note this includes non-rule case statements and the successful pattern match successor
13161335
// of a non-rule pattern case statement. Rule case statements do not complete normally
13171336
// (they always break or yield).
1318-
exists(int i |
1319-
last(getSwitchStatement(switch, i), n, completion) and
1320-
result = first(getSwitchStatement(switch, i + 1)) and
1337+
// Exception: falling through into a pattern case statement (which necessarily does not
1338+
// declare any named variables) must skip one or more such statements, otherwise we would
1339+
// incorrectly apply their type test and/or guard.
1340+
exists(Stmt pred, Stmt succ |
1341+
isNextNormalSwitchStmt(switch, pred, succ) and
1342+
last(pred, n, completion) and
1343+
result = first(succ) and
13211344
(completion = NormalCompletion() or completion = BooleanCompletion(true, _))
13221345
)
13231346
or
@@ -1328,16 +1351,19 @@ private module ControlFlowGraphImpl {
13281351
)
13291352
or
13301353
// Pattern cases have internal edges:
1331-
// * Type test success -true-> variable declarations
1354+
// * Type test success -true-> one of the possible sets of variable declarations
1355+
// n.b. for unnamed patterns (e.g. case A _, B _) this means that *one* of the
1356+
// type tests has succeeded. There aren't enough nodes in the AST to describe
1357+
// a sequential test in detail, so CFG consumers have to watch out for this case.
13321358
// * Variable declarations -normal-> guard evaluation
13331359
// * Variable declarations -normal-> rule execution (when there is no guard)
13341360
// * Guard success -true-> rule execution
13351361
exists(PatternCase pc |
13361362
n = pc and
13371363
completion = basicBooleanCompletion(true) and
1338-
result = first(pc.getPattern())
1364+
result = first(pc.getAPattern())
13391365
or
1340-
last(pc.getPattern(), n, completion) and
1366+
last(pc.getAPattern(), n, completion) and
13411367
completion = NormalCompletion() and
13421368
result = first(pc.getGuard())
13431369
or

java/ql/lib/semmle/code/java/Dependency.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ predicate depends(RefType t, RefType dep) {
8484
or
8585
// A type accessed in a pattern-switch case statement in `t`.
8686
exists(PatternCase pc | t = pc.getEnclosingCallable().getDeclaringType() |
87-
usesType(pc.getPattern().getAChildExpr*().getType(), dep)
87+
usesType(pc.getAPattern().getAChildExpr*().getType(), dep)
8888
)
8989
)
9090
}

java/ql/lib/semmle/code/java/DependencyCounts.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ predicate numDepends(RefType t, RefType dep, int value) {
107107
or
108108
// the type accessed in a pattern-switch case statement in `t`.
109109
exists(PatternCase pc | elem = pc and t = pc.getEnclosingCallable().getDeclaringType() |
110-
usesType(pc.getPattern().getAChildExpr*().getType(), dep)
110+
usesType(pc.getAPattern().getAChildExpr*().getType(), dep)
111111
)
112112
)
113113
}

java/ql/lib/semmle/code/java/Expr.qll

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,7 +1590,9 @@ class InstanceOfExpr extends Expr, @instanceofexpr {
15901590
* Note that this won't get anything when record pattern matching is used-- for more general patterns,
15911591
* use `getPattern`.
15921592
*/
1593-
LocalVariableDeclExpr getLocalVariableDeclExpr() { result = this.getPattern().asBindingPattern() }
1593+
LocalVariableDeclExpr getLocalVariableDeclExpr() {
1594+
result = this.getPattern().asBindingOrUnnamedPattern()
1595+
}
15941596

15951597
/**
15961598
* Gets the access to the type on the right-hand side of the `instanceof` operator.
@@ -1681,7 +1683,10 @@ class LocalVariableDeclExpr extends Expr, @localvariabledeclexpr {
16811683
or
16821684
exists(InstanceOfExpr ioe | this.getParent() = ioe | result.isNthChildOf(ioe, 1))
16831685
or
1684-
exists(PatternCase pc | this.getParent() = pc | result.isNthChildOf(pc, -2))
1686+
exists(PatternCase pc, int index, int typeAccessIdx | this.isNthChildOf(pc, index) |
1687+
(if index = 0 then typeAccessIdx = -4 else typeAccessIdx = (-3 - index)) and
1688+
result.isNthChildOf(pc, typeAccessIdx)
1689+
)
16851690
or
16861691
exists(RecordPatternExpr rpe, int index |
16871692
this.isNthChildOf(rpe, index) and result.isNthChildOf(rpe, -(index + 1))
@@ -1742,17 +1747,17 @@ class LocalVariableDeclExpr extends Expr, @localvariabledeclexpr {
17421747
or
17431748
exists(SwitchStmt switch |
17441749
result = switch.getExpr() and
1745-
this = switch.getAPatternCase().getPattern().asBindingPattern()
1750+
this = switch.getAPatternCase().getAPattern().asBindingOrUnnamedPattern()
17461751
)
17471752
or
17481753
exists(SwitchExpr switch |
17491754
result = switch.getExpr() and
1750-
this = switch.getAPatternCase().getPattern().asBindingPattern()
1755+
this = switch.getAPatternCase().getAPattern().asBindingOrUnnamedPattern()
17511756
)
17521757
or
17531758
exists(InstanceOfExpr ioe |
17541759
result = ioe.getExpr() and
1755-
this = ioe.getPattern().asBindingPattern()
1760+
this = ioe.getPattern().asBindingOrUnnamedPattern()
17561761
)
17571762
}
17581763

@@ -2676,9 +2681,9 @@ class NotNullExpr extends UnaryExpr, @notnullexpr {
26762681
}
26772682

26782683
/**
2679-
* A binding or record pattern.
2684+
* A binding, unnamed or record pattern.
26802685
*
2681-
* Note binding patterns are represented as `LocalVariableDeclExpr`s.
2686+
* Note binding and unnamed patterns are represented as `LocalVariableDeclExpr`s.
26822687
*/
26832688
class PatternExpr extends Expr {
26842689
PatternExpr() {
@@ -2691,9 +2696,14 @@ class PatternExpr extends Expr {
26912696
}
26922697

26932698
/**
2694-
* Gets this pattern cast to a binding pattern.
2699+
* Gets this pattern cast to a binding or unnamed pattern.
2700+
*/
2701+
LocalVariableDeclExpr asBindingOrUnnamedPattern() { result = this }
2702+
2703+
/**
2704+
* DEPRECATED: alias for `asBindingOrUnnamedPattern`.
26952705
*/
2696-
LocalVariableDeclExpr asBindingPattern() { result = this }
2706+
deprecated LocalVariableDeclExpr asBindingPattern() { result = this.asBindingOrUnnamedPattern() }
26972707

26982708
/**
26992709
* Gets this pattern cast to a record pattern.

java/ql/lib/semmle/code/java/PrettyPrintAst.qll

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ private class PpInstanceOfExpr extends PpAst, InstanceOfExpr {
386386
i = 3 and result = " " and this.getPattern() instanceof LocalVariableDeclExpr
387387
or
388388
i = 4 and
389-
result = this.getPattern().asBindingPattern().getName()
389+
result = this.getPattern().asBindingOrUnnamedPattern().getName()
390390
}
391391

392392
override PpAst getChild(int i) {
@@ -782,28 +782,53 @@ private class PpSwitchCase extends PpAst, SwitchCase {
782782
}
783783

784784
private class PpPatternCase extends PpAst, PatternCase {
785+
private TypeAccess getPatternTypeAccess(int n) {
786+
result = this.getPatternAtIndex(n).asBindingOrUnnamedPattern().getTypeAccess()
787+
}
788+
789+
private predicate isAnonymousPattern(int n) {
790+
this.getPatternAtIndex(n).asBindingOrUnnamedPattern().isAnonymous()
791+
}
792+
785793
override string getPart(int i) {
786-
i = 0 and result = "case "
787-
or
788-
i = 2 and this.getPattern() instanceof LocalVariableDeclExpr and result = " "
789-
or
790-
i = 3 and result = this.getPattern().asBindingPattern().getName()
791-
or
792-
i = 4 and result = ":" and not this.isRule()
793-
or
794-
i = 4 and result = " -> " and this.isRule()
794+
exists(int n, int base | exists(this.getPatternAtIndex(n)) and base = n * 4 |
795+
i = base and
796+
(if n = 0 then result = "case " else result = ", ")
797+
or
798+
i = base + 2 and
799+
this.getPatternAtIndex(n) instanceof LocalVariableDeclExpr and
800+
not this.isAnonymousPattern(n) and
801+
result = " "
802+
or
803+
i = base + 3 and
804+
(
805+
if this.isAnonymousPattern(n)
806+
then result = "_"
807+
else result = this.getPatternAtIndex(n).asBindingOrUnnamedPattern().getName()
808+
)
809+
)
795810
or
796-
i = 6 and result = ";" and exists(this.getRuleExpression())
811+
exists(int base | base = (max(int n | exists(this.getPatternAtIndex(n))) + 1) * 4 |
812+
i = base and result = ":" and not this.isRule()
813+
or
814+
i = base and result = " -> " and this.isRule()
815+
or
816+
i = base + 2 and result = ";" and exists(this.getRuleExpression())
817+
)
797818
}
798819

799820
override PpAst getChild(int i) {
800-
i = 1 and result = this.getPattern().asBindingPattern().getTypeAccess()
801-
or
802-
i = 1 and result = this.getPattern().asRecordPattern()
803-
or
804-
i = 5 and result = this.getRuleExpression()
821+
exists(int n, int base | exists(this.getPatternAtIndex(n)) and base = n * 4 |
822+
i = 1 and result = this.getPatternAtIndex(n).asBindingOrUnnamedPattern().getTypeAccess()
823+
or
824+
i = 1 and result = this.getPatternAtIndex(n).asRecordPattern()
825+
)
805826
or
806-
i = 5 and result = this.getRuleStatement()
827+
exists(int base | base = (max(int n | exists(this.getPatternAtIndex(n))) + 1) * 4 |
828+
i = base + 1 and result = this.getRuleExpression()
829+
or
830+
i = base + 1 and result = this.getRuleStatement()
831+
)
807832
}
808833
}
809834

java/ql/lib/semmle/code/java/PrintAst.qll

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,11 @@ private newtype TPrintAstNode =
117117
TElementNode(Element el) { shouldPrint(el, _) } or
118118
TForInitNode(ForStmt fs) { shouldPrint(fs, _) and exists(fs.getAnInit()) } or
119119
TLocalVarDeclNode(LocalVariableDeclExpr lvde) {
120-
shouldPrint(lvde, _) and lvde.getParent() instanceof SingleLocalVarDeclParent
120+
shouldPrint(lvde, _) and
121+
(
122+
lvde.getParent() instanceof SingleLocalVarDeclParent or
123+
lvde.getParent() instanceof PatternCase
124+
)
121125
} or
122126
TAnnotationsNode(Annotatable ann) {
123127
shouldPrint(ann, _) and
@@ -415,6 +419,23 @@ final class ForStmtNode extends ExprStmtNode {
415419
}
416420
}
417421

422+
/**
423+
* A node representing a `PatternCase`.
424+
*/
425+
final class PatternCaseNode extends ExprStmtNode {
426+
PatternCase pc;
427+
428+
PatternCaseNode() { pc = element }
429+
430+
override PrintAstNode getChild(int childIndex) {
431+
result = super.getChild(childIndex) and
432+
not result.(ElementNode).getElement() instanceof LocalVariableDeclExpr and
433+
not result.(ElementNode).getElement() instanceof TypeAccess
434+
or
435+
result = TLocalVarDeclNode(pc.getPatternAtIndex(childIndex))
436+
}
437+
}
438+
418439
/**
419440
* An element that can be the parent of up to one `LocalVariableDeclExpr` for which we want
420441
* to use a synthetic node to hold the variable declaration and its `TypeAccess`.
@@ -423,8 +444,7 @@ private class SingleLocalVarDeclParent extends ExprOrStmt {
423444
SingleLocalVarDeclParent() {
424445
this instanceof EnhancedForStmt or
425446
this instanceof CatchClause or
426-
this.(InstanceOfExpr).isPattern() or
427-
this instanceof PatternCase
447+
this.(InstanceOfExpr).isPattern()
428448
}
429449

430450
/** Gets the variable declaration that this element contains */
@@ -643,7 +663,11 @@ final class LocalVarDeclSynthNode extends PrintAstNode, TLocalVarDeclNode {
643663

644664
LocalVarDeclSynthNode() { this = TLocalVarDeclNode(lvde) }
645665

646-
override string toString() { result = "(Single Local Variable Declaration)" }
666+
override string toString() {
667+
if lvde.getParent() instanceof PatternCase
668+
then result = "(Pattern case declaration)"
669+
else result = "(Single Local Variable Declaration)"
670+
}
647671

648672
override ElementNode getChild(int childIndex) {
649673
childIndex = 0 and

java/ql/lib/semmle/code/java/Statement.qll

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,12 +539,19 @@ class ConstCase extends SwitchCase {
539539

540540
/** A pattern case of a `switch` statement */
541541
class PatternCase extends SwitchCase {
542-
PatternExpr pattern;
542+
PatternCase() { exists(PatternExpr pe | pe.isNthChildOf(this, _)) }
543543

544-
PatternCase() { pattern.isNthChildOf(this, 0) }
544+
/**
545+
* DEPRECATED: alias for getPatternAtIndex(0)
546+
*/
547+
deprecated PatternExpr getPattern() { result = this.getPatternAtIndex(0) }
548+
549+
/**
550+
* Gets this case's `n`th pattern.
551+
*/
552+
PatternExpr getPatternAtIndex(int n) { result.isNthChildOf(this, n) }
545553

546-
/** Gets this case's pattern. */
547-
PatternExpr getPattern() { result = pattern }
554+
PatternExpr getAPattern() { result = this.getPatternAtIndex(_) }
548555

549556
/** Gets the guard applicable to this pattern case, if any. */
550557
Expr getGuard() { result.isNthChildOf(this, -3) }

java/ql/lib/semmle/code/java/Variable.qll

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ class LocalVariableDecl extends @localvar, LocalScopeVariable {
5858
/** Gets the callable in which this declaration occurs. */
5959
Callable getEnclosingCallable() { result = this.getCallable() }
6060

61-
override string toString() { result = this.getType().getName() + " " + this.getName() }
61+
override string toString() {
62+
exists(string sourceName |
63+
if this.getName() = "" then sourceName = "_" else sourceName = this.getName()
64+
|
65+
result = this.getType().getName() + " " + sourceName
66+
)
67+
}
6268

6369
/** Gets the initializer expression of this local variable declaration. */
6470
override Expr getInitializer() { result = this.getDeclExpr().getInit() }

java/ql/lib/semmle/code/java/controlflow/Guards.qll

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,20 @@ private predicate isNonFallThroughPredecessor(SwitchCase sc, ControlFlowNode pre
115115
(
116116
pred.(Expr).getParent*() = sc.getSelectorExpr()
117117
or
118-
pred.(Expr).getParent*() = getClosestPrecedingPatternCase(sc).getGuard()
118+
// Ambiguous: in the case of `case String _ when x: case "SomeConstant":`, the guard `x`
119+
// passing edge will fall through into the constant case, and the guard failing edge
120+
// will test if the selector equals `"SomeConstant"` and if so branch to the same
121+
// case statement. Therefore don't label this a non-fall-through predecessor.
122+
exists(PatternCase previousPatternCase |
123+
previousPatternCase = getClosestPrecedingPatternCase(sc)
124+
|
125+
pred.(Expr).getParent*() = previousPatternCase.getGuard() and
126+
// Check there is any statement in between the previous pattern case and this one.
127+
not previousPatternCase.getIndex() = sc.getIndex() - 1
128+
)
119129
or
130+
// Unambigious: on the test-passing edge there must be at least one intervening
131+
// declaration node, including anonymous `_` declarations.
120132
pred = getClosestPrecedingPatternCase(sc)
121133
)
122134
}

0 commit comments

Comments
 (0)