Skip to content

Commit f7ca8e5

Browse files
authored
Merge pull request #14224 from rdmarsh2/rdmarsh2/swift/nil-coalescing-cfg
Swift: CFG and data flow for nil coalescing operator
2 parents 3703c56 + 06da5fd commit f7ca8e5

File tree

12 files changed

+246
-8
lines changed

12 files changed

+246
-8
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
5+
* The nil-coalescing operator `??` is now supported by the CFG construction and dataflow libraries.

swift/ql/lib/codeql/swift/controlflow/internal/ControlFlowElements.qll

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ newtype TControlFlowElement =
1212
TPropertyObserverElement(Accessor observer, AssignExpr assign) {
1313
isPropertyObserverElement(observer, assign)
1414
} or
15-
TKeyPathElement(KeyPathExpr expr)
15+
TKeyPathElement(KeyPathExpr expr) or
16+
TNilCoalescingTestElement(NilCoalescingExpr expr)
1617

1718
predicate isLValue(Expr e) { any(AssignExpr assign).getDest() = e }
1819

@@ -204,3 +205,15 @@ class ClosureElement extends ControlFlowElement, TClosureElement {
204205

205206
override string toString() { result = expr.toString() }
206207
}
208+
209+
class NilCoalescingElement extends ControlFlowElement, TNilCoalescingTestElement {
210+
NilCoalescingExpr expr;
211+
212+
NilCoalescingElement() { this = TNilCoalescingTestElement(expr) }
213+
214+
override Location getLocation() { result = expr.getLocation() }
215+
216+
NilCoalescingExpr getAst() { result = expr }
217+
218+
override string toString() { result = "emptiness test for " + expr.toString() }
219+
}

swift/ql/lib/codeql/swift/controlflow/internal/ControlFlowGraphImpl.qll

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,7 @@ module Exprs {
11811181
}
11821182

11831183
/**
1184-
* An autoclosure expression that is generated as part of a logical operation.
1184+
* An autoclosure expression that is generated as part of a logical operation or nil coalescing expression.
11851185
*
11861186
* This is needed because the Swift AST for `b1 && b2` is really syntactic sugar a function call:
11871187
* ```swift
@@ -1190,10 +1190,13 @@ module Exprs {
11901190
* So the `true` edge from `b1` cannot just go to `b2` since this is an implicit autoclosure.
11911191
* To handle this dig into the autoclosure when it's an operand of a logical operator.
11921192
*/
1193-
private class LogicalAutoClosureTree extends AstPreOrderTree {
1193+
private class ShortCircuitingAutoClosureTree extends AstPreOrderTree {
11941194
override AutoClosureExpr ast;
11951195

1196-
LogicalAutoClosureTree() { ast = any(LogicalOperation op).getAnOperand() }
1196+
ShortCircuitingAutoClosureTree() {
1197+
ast = any(LogicalOperation op).getAnOperand() or
1198+
ast = any(NilCoalescingExpr expr).getAnOperand()
1199+
}
11971200

11981201
override predicate last(ControlFlowElement last, Completion c) {
11991202
exists(Completion completion | astLast(ast.getReturn(), last, completion) |
@@ -1219,7 +1222,7 @@ module Exprs {
12191222
private class AutoClosureTree extends AstLeafTree {
12201223
override AutoClosureExpr ast;
12211224

1222-
AutoClosureTree() { not this instanceof LogicalAutoClosureTree }
1225+
AutoClosureTree() { not this instanceof ShortCircuitingAutoClosureTree }
12231226
}
12241227
}
12251228

@@ -1559,7 +1562,9 @@ module Exprs {
15591562
// This one is handled in `LogicalNotTree`.
15601563
not ast instanceof UnaryLogicalOperation and
15611564
// These are handled in `LogicalOrTree` and `LogicalAndTree`.
1562-
not ast instanceof BinaryLogicalOperation
1565+
not ast instanceof BinaryLogicalOperation and
1566+
// This one is handled in `NilCoalescingTree`
1567+
not ast instanceof NilCoalescingExpr
15631568
}
15641569

15651570
final override ControlFlowElement getChildElement(int i) {
@@ -1583,6 +1588,38 @@ module Exprs {
15831588
}
15841589
}
15851590

1591+
private class NilCoalescingTestTree extends LeafTree, NilCoalescingElement { }
1592+
1593+
private class NilCoalescingTree extends AstPostOrderTree {
1594+
override NilCoalescingExpr ast;
1595+
1596+
final override predicate propagatesAbnormal(ControlFlowElement child) {
1597+
child.asAstNode() = ast.getAnOperand().getFullyConverted()
1598+
}
1599+
1600+
final override predicate first(ControlFlowElement first) {
1601+
astFirst(ast.getLeftOperand().getFullyConverted(), first)
1602+
}
1603+
1604+
final override predicate succ(ControlFlowElement pred, ControlFlowElement succ, Completion c) {
1605+
astLast(ast.getLeftOperand().getFullyConverted(), pred, c) and
1606+
c instanceof NormalCompletion and
1607+
succ.(NilCoalescingTestTree).getAst() = ast
1608+
or
1609+
pred.(NilCoalescingTestTree).getAst() = ast and
1610+
exists(EmptinessCompletion ec | c = ec | ec.isEmpty()) and
1611+
astFirst(ast.getRightOperand().getFullyConverted(), succ)
1612+
or
1613+
pred.(NilCoalescingTestTree).getAst() = ast and
1614+
exists(EmptinessCompletion ec | c = ec | not ec.isEmpty()) and
1615+
succ.asAstNode() = ast
1616+
or
1617+
astLast(ast.getRightOperand().getFullyConverted(), pred, c) and
1618+
c instanceof NormalCompletion and
1619+
succ.asAstNode() = ast
1620+
}
1621+
}
1622+
15861623
private class LogicalAndTree extends AstPostOrderTree {
15871624
override LogicalAndExpr ast;
15881625

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Provides a class for the Swift nil-coalesing expr (`??`)
3+
*/
4+
5+
private import codeql.swift.elements.expr.Expr
6+
private import codeql.swift.elements.expr.BinaryExpr
7+
8+
/**
9+
* A Swift nil-coalesing expr (`??`).
10+
*/
11+
class NilCoalescingExpr extends BinaryExpr {
12+
NilCoalescingExpr() { this.getStaticTarget().getName() = "??(_:_:)" }
13+
}

swift/ql/lib/swift.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import codeql.swift.elements
44
import codeql.swift.elements.expr.ArithmeticOperation
55
import codeql.swift.elements.expr.BitwiseOperation
66
import codeql.swift.elements.expr.LogicalOperation
7+
import codeql.swift.elements.expr.NilCoalescingExpr
78
import codeql.swift.elements.expr.InitializerLookupExpr
89
import codeql.swift.elements.expr.MethodCallExpr
910
import codeql.swift.elements.expr.InitializerCallExpr

swift/ql/test/library-tests/ast/PrintAst.expected

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3300,6 +3300,44 @@ cfg.swift:
33003300
# 526| Type = Int
33013301
# 533| [ConcreteVarDecl] i
33023302
# 533| Type = Int
3303+
# 538| [NamedFunction] testNilCoalescing(x:)
3304+
# 538| InterfaceType = (Int?) -> Int
3305+
# 538| getParam(0): [ParamDecl] x
3306+
# 538| Type = Int?
3307+
# 538| getBody(): [BraceStmt] { ... }
3308+
# 539| getElement(0): [ReturnStmt] return ...
3309+
# 539| getResult(): [BinaryExpr] ... ??(_:_:) ...
3310+
# 539| getFunction(): [DeclRefExpr] ??(_:_:)
3311+
# 539| getArgument(0): [Argument] : x
3312+
# 539| getExpr(): [DeclRefExpr] x
3313+
# 539| getArgument(1): [Argument] : { ... }
3314+
# 539| getExpr(): [AutoClosureExpr] { ... }
3315+
# 539| getBody(): [BraceStmt] { ... }
3316+
# 539| getElement(0): [ReturnStmt] return ...
3317+
# 539| getResult(): [IntegerLiteralExpr] 0
3318+
# 542| [NamedFunction] testNilCoalescing2(x:)
3319+
# 542| InterfaceType = (Bool?) -> Int
3320+
# 542| getParam(0): [ParamDecl] x
3321+
# 542| Type = Bool?
3322+
# 542| getBody(): [BraceStmt] { ... }
3323+
# 543| getElement(0): [IfStmt] if ... then { ... } else { ... }
3324+
# 543| getCondition(): [StmtCondition] StmtCondition
3325+
# 543| getElement(0): [ConditionElement] ... ??(_:_:) ...
3326+
# 543| getBoolean(): [BinaryExpr] ... ??(_:_:) ...
3327+
# 543| getFunction(): [DeclRefExpr] ??(_:_:)
3328+
# 543| getArgument(0): [Argument] : x
3329+
# 543| getExpr(): [DeclRefExpr] x
3330+
# 543| getArgument(1): [Argument] : { ... }
3331+
# 543| getExpr(): [AutoClosureExpr] { ... }
3332+
# 543| getBody(): [BraceStmt] { ... }
3333+
# 543| getElement(0): [ReturnStmt] return ...
3334+
# 543| getResult(): [BooleanLiteralExpr] false
3335+
# 543| getThen(): [BraceStmt] { ... }
3336+
# 544| getElement(0): [ReturnStmt] return ...
3337+
# 544| getResult(): [IntegerLiteralExpr] 1
3338+
# 545| getElse(): [BraceStmt] { ... }
3339+
# 546| getElement(0): [ReturnStmt] return ...
3340+
# 546| getResult(): [IntegerLiteralExpr] 0
33033341
declarations.swift:
33043342
# 1| [StructDecl] Foo
33053343
# 2| getMember(0): [PatternBindingDecl] var ... = ...

swift/ql/test/library-tests/ast/cfg.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,3 +534,15 @@ func testAsyncFor () async {
534534
print(i)
535535
}
536536
}
537+
538+
func testNilCoalescing(x: Int?) -> Int {
539+
return x ?? 0
540+
}
541+
542+
func testNilCoalescing2(x: Bool?) -> Int {
543+
if x ?? false {
544+
return 1
545+
} else {
546+
return 0
547+
}
548+
}

swift/ql/test/library-tests/controlflow/graph/Cfg.expected

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6180,3 +6180,99 @@ cfg.swift:
61806180

61816181
# 534| i
61826182
#-----| -> (Any) ...
6183+
6184+
# 538| enter testNilCoalescing(x:)
6185+
#-----| -> testNilCoalescing(x:)
6186+
6187+
# 538| exit testNilCoalescing(x:)
6188+
6189+
# 538| exit testNilCoalescing(x:) (abnormal)
6190+
#-----| -> exit testNilCoalescing(x:)
6191+
6192+
# 538| exit testNilCoalescing(x:) (normal)
6193+
#-----| -> exit testNilCoalescing(x:)
6194+
6195+
# 538| testNilCoalescing(x:)
6196+
#-----| -> x
6197+
6198+
# 538| x
6199+
#-----| -> x
6200+
6201+
# 539| return ...
6202+
#-----| return -> exit testNilCoalescing(x:) (normal)
6203+
6204+
# 539| x
6205+
#-----| -> emptiness test for ... ??(_:_:) ...
6206+
6207+
# 539| ... ??(_:_:) ...
6208+
#-----| exception -> exit testNilCoalescing(x:) (abnormal)
6209+
#-----| -> return ...
6210+
6211+
# 539| emptiness test for ... ??(_:_:) ...
6212+
#-----| non-empty -> ... ??(_:_:) ...
6213+
#-----| empty -> { ... }
6214+
6215+
# 539| 0
6216+
#-----| -> return ...
6217+
6218+
# 539| return ...
6219+
#-----| -> ... ??(_:_:) ...
6220+
6221+
# 539| { ... }
6222+
#-----| -> 0
6223+
6224+
# 542| enter testNilCoalescing2(x:)
6225+
#-----| -> testNilCoalescing2(x:)
6226+
6227+
# 542| exit testNilCoalescing2(x:)
6228+
6229+
# 542| exit testNilCoalescing2(x:) (abnormal)
6230+
#-----| -> exit testNilCoalescing2(x:)
6231+
6232+
# 542| exit testNilCoalescing2(x:) (normal)
6233+
#-----| -> exit testNilCoalescing2(x:)
6234+
6235+
# 542| testNilCoalescing2(x:)
6236+
#-----| -> x
6237+
6238+
# 542| x
6239+
#-----| -> if ... then { ... } else { ... }
6240+
6241+
# 543| if ... then { ... } else { ... }
6242+
#-----| -> StmtCondition
6243+
6244+
# 543| x
6245+
#-----| -> emptiness test for ... ??(_:_:) ...
6246+
6247+
# 543| ... ??(_:_:) ...
6248+
#-----| exception -> exit testNilCoalescing2(x:) (abnormal)
6249+
#-----| true -> 1
6250+
#-----| false -> 0
6251+
6252+
# 543| StmtCondition
6253+
#-----| -> x
6254+
6255+
# 543| emptiness test for ... ??(_:_:) ...
6256+
#-----| non-empty -> ... ??(_:_:) ...
6257+
#-----| empty -> { ... }
6258+
6259+
# 543| false
6260+
#-----| -> return ...
6261+
6262+
# 543| return ...
6263+
#-----| -> ... ??(_:_:) ...
6264+
6265+
# 543| { ... }
6266+
#-----| -> false
6267+
6268+
# 544| return ...
6269+
#-----| return -> exit testNilCoalescing2(x:) (normal)
6270+
6271+
# 544| 1
6272+
#-----| -> return ...
6273+
6274+
# 546| return ...
6275+
#-----| return -> exit testNilCoalescing2(x:) (normal)
6276+
6277+
# 546| 0
6278+
#-----| -> return ...

swift/ql/test/library-tests/controlflow/graph/cfg.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,3 +534,15 @@ func testAsyncFor () async {
534534
print(i)
535535
}
536536
}
537+
538+
func testNilCoalescing(x: Int?) -> Int {
539+
return x ?? 0
540+
}
541+
542+
func testNilCoalescing2(x: Bool?) -> Int {
543+
if x ?? false {
544+
return 1
545+
} else {
546+
return 0
547+
}
548+
}

swift/ql/test/library-tests/dataflow/dataflow/DataFlow.expected

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ edges
145145
| test.swift:270:15:270:22 | call to source() | test.swift:270:15:270:31 | call to signum() |
146146
| test.swift:271:15:271:16 | ...? | test.swift:271:15:271:25 | call to signum() |
147147
| test.swift:271:15:271:25 | call to signum() | test.swift:271:15:271:25 | OptionalEvaluationExpr |
148+
| test.swift:275:20:275:27 | call to source() | test.swift:275:15:275:27 | ... ??(_:_:) ... |
149+
| test.swift:277:20:277:27 | call to source() | test.swift:277:15:277:27 | ... ??(_:_:) ... |
148150
| test.swift:279:26:279:26 | x [some:0] | test.swift:279:26:279:27 | ...! |
149151
| test.swift:279:26:279:27 | ...! | test.swift:279:15:279:31 | ... ? ... : ... |
150152
| test.swift:280:26:280:26 | x [some:0] | test.swift:280:26:280:27 | ...! |
@@ -713,6 +715,9 @@ nodes
713715
| test.swift:271:15:271:25 | call to signum() | semmle.label | call to signum() |
714716
| test.swift:274:15:274:20 | ... ??(_:_:) ... | semmle.label | ... ??(_:_:) ... |
715717
| test.swift:275:15:275:27 | ... ??(_:_:) ... | semmle.label | ... ??(_:_:) ... |
718+
| test.swift:275:20:275:27 | call to source() | semmle.label | call to source() |
719+
| test.swift:277:15:277:27 | ... ??(_:_:) ... | semmle.label | ... ??(_:_:) ... |
720+
| test.swift:277:20:277:27 | call to source() | semmle.label | call to source() |
716721
| test.swift:279:15:279:31 | ... ? ... : ... | semmle.label | ... ? ... : ... |
717722
| test.swift:279:26:279:26 | x [some:0] | semmle.label | x [some:0] |
718723
| test.swift:279:26:279:27 | ...! | semmle.label | ...! |
@@ -1262,6 +1267,8 @@ subpaths
12621267
| test.swift:271:15:271:25 | OptionalEvaluationExpr | test.swift:259:12:259:19 | call to source() | test.swift:271:15:271:25 | OptionalEvaluationExpr | result |
12631268
| test.swift:274:15:274:20 | ... ??(_:_:) ... | test.swift:259:12:259:19 | call to source() | test.swift:274:15:274:20 | ... ??(_:_:) ... | result |
12641269
| test.swift:275:15:275:27 | ... ??(_:_:) ... | test.swift:259:12:259:19 | call to source() | test.swift:275:15:275:27 | ... ??(_:_:) ... | result |
1270+
| test.swift:275:15:275:27 | ... ??(_:_:) ... | test.swift:275:20:275:27 | call to source() | test.swift:275:15:275:27 | ... ??(_:_:) ... | result |
1271+
| test.swift:277:15:277:27 | ... ??(_:_:) ... | test.swift:277:20:277:27 | call to source() | test.swift:277:15:277:27 | ... ??(_:_:) ... | result |
12651272
| test.swift:279:15:279:31 | ... ? ... : ... | test.swift:259:12:259:19 | call to source() | test.swift:279:15:279:31 | ... ? ... : ... | result |
12661273
| test.swift:280:15:280:38 | ... ? ... : ... | test.swift:259:12:259:19 | call to source() | test.swift:280:15:280:38 | ... ? ... : ... | result |
12671274
| test.swift:280:15:280:38 | ... ? ... : ... | test.swift:280:31:280:38 | call to source() | test.swift:280:15:280:38 | ... ? ... : ... | result |

swift/ql/test/library-tests/dataflow/dataflow/LocalFlow.expected

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,19 @@
247247
| test.swift:272:15:272:25 | call to signum() | test.swift:272:15:272:25 | OptionalEvaluationExpr |
248248
| test.swift:274:15:274:15 | x | test.swift:274:15:274:20 | ... ??(_:_:) ... |
249249
| test.swift:274:15:274:15 | x | test.swift:275:15:275:15 | x |
250+
| test.swift:274:20:274:20 | 0 | test.swift:274:15:274:20 | ... ??(_:_:) ... |
250251
| test.swift:274:20:274:20 | { ... } | test.swift:274:15:274:20 | ... ??(_:_:) ... |
251252
| test.swift:275:15:275:15 | x | test.swift:275:15:275:27 | ... ??(_:_:) ... |
252253
| test.swift:275:15:275:15 | x | test.swift:279:15:279:15 | x |
254+
| test.swift:275:20:275:27 | call to source() | test.swift:275:15:275:27 | ... ??(_:_:) ... |
253255
| test.swift:275:20:275:27 | { ... } | test.swift:275:15:275:27 | ... ??(_:_:) ... |
254256
| test.swift:276:15:276:15 | y | test.swift:276:15:276:20 | ... ??(_:_:) ... |
255257
| test.swift:276:15:276:15 | y | test.swift:277:15:277:15 | y |
258+
| test.swift:276:20:276:20 | 0 | test.swift:276:15:276:20 | ... ??(_:_:) ... |
256259
| test.swift:276:20:276:20 | { ... } | test.swift:276:15:276:20 | ... ??(_:_:) ... |
257260
| test.swift:277:15:277:15 | y | test.swift:277:15:277:27 | ... ??(_:_:) ... |
258261
| test.swift:277:15:277:15 | y | test.swift:281:15:281:15 | y |
262+
| test.swift:277:20:277:27 | call to source() | test.swift:277:15:277:27 | ... ??(_:_:) ... |
259263
| test.swift:277:20:277:27 | { ... } | test.swift:277:15:277:27 | ... ??(_:_:) ... |
260264
| test.swift:279:15:279:15 | x | test.swift:279:26:279:26 | x |
261265
| test.swift:279:15:279:15 | x | test.swift:280:15:280:15 | x |

swift/ql/test/library-tests/dataflow/dataflow/test.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,9 @@ func test_optionals(y: Int?) {
272272
sink(opt: y?.signum())
273273

274274
sink(arg: x ?? 0) // $ flow=259
275-
sink(arg: x ?? source()) // $ flow=259 MISSING: flow=276
275+
sink(arg: x ?? source()) // $ flow=259 flow=275
276276
sink(arg: y ?? 0)
277-
sink(arg: y ?? source()) // $ MISSING: flow=278
277+
sink(arg: y ?? source()) // $ flow=277
278278

279279
sink(arg: x != nil ? x! : 0) // $ flow=259
280280
sink(arg: x != nil ? x! : source()) // $ flow=259 flow=280

0 commit comments

Comments
 (0)