├── .gitignore
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── SwiftLeakCheck
│ ├── BackwardCompatiblity.swift
│ ├── BaseSyntaxTreeLeakDetector.swift
│ ├── DirectoryScanner.swift
│ ├── Function.swift
│ ├── FunctionSignature.swift
│ ├── Graph.swift
│ ├── GraphBuilder.swift
│ ├── GraphLeakDetector.swift
│ ├── Leak.swift
│ ├── LeakDetector.swift
│ ├── NonEscapeRules
│ │ ├── CollectionRules.swift
│ │ ├── DispatchQueueRule.swift
│ │ ├── ExprSyntaxPredicate.swift
│ │ ├── NonEscapeRule.swift
│ │ ├── UIViewAnimationRule.swift
│ │ └── UIViewControllerAnimationRule.swift
│ ├── Scope.swift
│ ├── SourceFileScope.swift
│ ├── Stack.swift
│ ├── SwiftSyntax+Extensions.swift
│ ├── Symbol.swift
│ ├── SyntaxRetrieval.swift
│ ├── TypeDecl.swift
│ ├── TypeResolve.swift
│ ├── Utility.swift
│ └── Variable.swift
└── SwiftLeakChecker
│ └── main.swift
├── SwiftLeakCheck.xcodeproj
├── CYaml_Info.plist
├── Clang_C_Info.plist
├── GeneratedModuleMap
│ └── _CSwiftSyntax
│ │ └── module.modulemap
├── SWXMLHash_Info.plist
├── SourceKit_Info.plist
├── SourceKittenFramework_Info.plist
├── SwiftLeakCheckTests_Info.plist
├── SwiftLeakCheck_Info.plist
├── SwiftSyntax_Info.plist
├── Yams_Info.plist
├── _CSwiftSyntax_Info.plist
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── xcshareddata
│ └── xcschemes
│ ├── SwiftLeakCheck-Package.xcscheme
│ └── SwiftLeakChecker.xcscheme
├── Tests
└── SwiftLeakCheckTests
│ ├── LeakDetectorTests.swift
│ ├── Snippets
│ ├── DispatchQueue
│ ├── EscapingAttribute
│ ├── Extensions
│ ├── FuncResolve
│ ├── IfElse
│ ├── Leak1
│ ├── Leak2
│ ├── NestedClosure
│ ├── NonEscapingClosure
│ ├── TypeInfer
│ ├── TypeResolve
│ ├── UIViewAnimation
│ └── UIViewControllerAnimation
│ └── StackTests.swift
└── images
├── leakcheck_sample.png
└── leakcheck_sample_xcode.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | # /*.xcodeproj
5 | xcuserdata/
6 | DerivedData
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Grabtaxi Holdings PTE LTE (GRAB)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "SwiftSyntax",
6 | "repositoryURL": "https://github.com/apple/swift-syntax",
7 | "state": {
8 | "branch": null,
9 | "revision": "844574d683f53d0737a9c6d706c3ef31ed2955eb",
10 | "version": "0.50300.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 | //
4 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
5 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
6 | //
7 |
8 | import PackageDescription
9 |
10 | let package = Package(
11 | name: "SwiftLeakCheck",
12 | products: [
13 | .library(name: "SwiftLeakCheck", targets: ["SwiftLeakCheck"]),
14 | .executable(name: "SwiftLeakChecker", targets: ["SwiftLeakChecker"])
15 | ],
16 | dependencies: [
17 | // Dependencies declare other packages that this package depends on.
18 | // .package(url: /* package url */, from: "1.0.0"),
19 |
20 | .package(url: "https://github.com/apple/swift-syntax", .exact("0.50300.0")),
21 |
22 | // For Swift toolchain 5.2 and above
23 | // .package(name: "SwiftSyntax", url: "https://github.com/apple/swift-syntax", .exact("0.50300.0")),
24 |
25 | ],
26 | targets: [
27 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
28 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
29 | .target(
30 | name: "SwiftLeakChecker",
31 | dependencies: [
32 | "SwiftLeakCheck"
33 | ]
34 | ),
35 | .target(
36 | name: "SwiftLeakCheck",
37 | dependencies: [
38 | "SwiftSyntax"
39 | ]
40 | ),
41 | .testTarget(
42 | name: "SwiftLeakCheckTests",
43 | dependencies: ["SwiftLeakCheck"]
44 | )
45 | ]
46 | )
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Swift Leak Checker
2 |
3 | A framework, a command-line tool that can detect potential memory leak caused by strongly captured `self` in `escaping` closure
4 |
5 |
6 |
7 |
8 | # Example
9 |
10 | Some examples of memory leak that are detected by the tool:
11 |
12 | ```swift
13 | class X {
14 | private var handler: (() -> Void)!
15 | private var anotherHandler: (() -> Void)!
16 |
17 | func setup() {
18 | handler = {
19 | self.doSmth() // <- Leak
20 | }
21 |
22 | anotherHandler = { // Outer closure
23 | doSmth { [weak self] in // <- Leak
24 | // .....
25 | }
26 | }
27 | }
28 | }
29 | ```
30 |
31 | For first leak, `self` holds a strong reference to `handler`, and `handler` holds a strong reference to `self`, which completes a retain cycle.
32 |
33 | For second leak, although `self` is captured weakly by the inner closure, but `self` is still implicitly captured strongly by the outer closure, which leaks to the same problem as the first leak
34 |
35 |
36 | # Usage
37 |
38 | There're 2 ways to use this tool: the fastest way is to use the provided SwiftLeakChecker target and start detecting leaks in your code, or you can drop the SwiftLeakCheck framework in your code
39 | and start building your own tool
40 |
41 | ### SwiftLeakChecker
42 |
43 | There is a SwiftLeakChecker target that you can run directly from XCode or as a command line.
44 |
45 | **To run from XCode:**
46 |
47 | Edit the `SwiftLeakChecker` scheme and change the `/path/to/your/swift/file/or/folder` to an absolute path of a Swift file or directory. Then hit the `Run` button (or `CMD+R`)
48 |
49 |
50 |
51 |
52 | **To run from command line:**
53 |
54 | ```
55 | ./SwiftLeakChecker path/to/your/swift/file/or/folder
56 | ```
57 |
58 | ### Build your own tool
59 |
60 | The SwiftLeakChecker target is ready to be used as-is. But if you want to build your own tool, do more customisation etc.., then you can follow these steps.
61 |
62 | Note: Xcode 11 or later or a Swift 5.2 toolchain or later with the Swift Package Manager is required.
63 |
64 |
65 | Add this repository to the `Package.swift` manifest of your project:
66 |
67 | ```swift
68 | // swift-tools-version:4.2
69 | import PackageDescription
70 |
71 | let package = Package(
72 | name: "MyAwesomeLeakDetector",
73 | dependencies: [
74 | .package(url: "This repo .git url", .exact("package version")),
75 | ],
76 | targets: [
77 | .target(name: "MyAwesomeLeakDetector", dependencies: ["SwiftLeakCheck"]),
78 | ]
79 | )
80 | ```
81 |
82 | Then, import `SwiftLeakCheck` in your Swift code
83 |
84 | To create a leak detector and start detecting:
85 |
86 | ```swift
87 | import SwiftLeakCheck
88 |
89 | let url = URL(fileURLWithPath: "absolute/path/to/your/swift/file/or/folder")
90 | let detector = GraphLeakDetector()
91 | let leaks = detector.detect(url)
92 | leaks.forEach { leak in
93 | print("\(leak)")
94 | }
95 | ```
96 |
97 | # Leak object
98 |
99 | Each `Leak` object contains `line`, `column` and `reason` info.
100 | ```
101 | {
102 | "line":41,
103 | "column":7,
104 | "reason":"`self` is strongly captured here, from a potentially escaped closure."
105 | }
106 | ```
107 |
108 | # CI and Danger
109 |
110 | The image on top shows a leak issue that was reported by our tool running on Gitlab CI. We use [Danger](https://github.com/danger/danger) to report the `line` and `reason` of every issue detected.
111 |
112 |
113 | # How it works
114 |
115 | We use [SourceKit](http://jpsim.com/uncovering-sourcekit) to get the [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) representation of the source file, then we travel the AST to detect for potential memory leak.
116 | Currently we only check if `self` is captured strongly in an escaping closure, which is one specific case that causes memory leak
117 |
118 | To do that, 3 things are checked:
119 |
120 | **1. Check if a reference captures `self`**
121 |
122 | ```swift
123 | block { [weak self] in
124 | guard let strongSelf = self else { return }
125 | let x = SomeClass()
126 | strongSelf.doSmth { [weak strongSelf] in
127 | guard let innerSelf = strongSelf else { return }
128 | x.doSomething()
129 | }
130 | }
131 | ```
132 |
133 | In this example, `innerSelf` captures `self`, because it is originated from `strongSelf` which is originated from `self`
134 |
135 | `x` is also a reference but doesn't capture `self`
136 |
137 | **2. Check if a closure is non-escaping**
138 |
139 | We use as much information about the closure as possible to determine if it is non-escaping or not.
140 |
141 | In the example below, `block` is non-escaping because it's not marked as `@escaping` and it's non-optional
142 | ```
143 | func doSmth(block: () -> Void) {
144 | ...
145 | }
146 | ```
147 |
148 | Or if it's anonymous closure, it's non-escaping
149 | ```swift
150 | let value = {
151 | return self.doSmth()
152 | }()
153 | ```
154 |
155 | We can check more complicated case like this:
156 |
157 | ```swift
158 | func test() {
159 | let block = {
160 | self.xxx
161 | }
162 | doSmth(block)
163 | }
164 | func doSmth(_ block: () -> Void) {
165 | ....
166 | }
167 | ```
168 |
169 | In this case, `block` is passed to a function `doSmth` and is not marked as `@escaping`, hence it's non-escaping
170 |
171 | **3. Whether an escaping closure captures self stronlgy from outside**
172 |
173 | ```swift
174 | block { [weak self] in
175 | guard let strongSelf = self else { return }
176 | self?.doSmth {
177 | strongSelf.x += 1
178 | }
179 | }
180 | ```
181 |
182 | In this example, we know that:
183 | 1. `strongSelf` refers to `self`
184 | 2. `doSmth` is escaping (just for example)
185 | 3. `strongSelf` (in the inner closure) is defined from outside, and it captures `self` strongly
186 |
187 |
188 | # False-positive alarms
189 |
190 | ### If we can't determine if a closure is escaping or non-escaping, we will just treat it as escaping.
191 |
192 | It can happen when for eg, the closure is passed to a function that is defined in other source file.
193 | To overcome that, you can define custom rules which have logic to classify a closure as escaping or non-escaping.
194 |
195 |
196 | # Non-escaping rules
197 |
198 | By default, we already did most of the legworks trying to determine if a closure is non-escaping (See #2 of `How it works` section)
199 |
200 | But in some cases, there's just not enough information in the source file.
201 | For eg, we know that a closure passed to `DispatchQueue.main.async` will be executed and gone very soon, hence it's safe to treat it as non-escaping. But the `DispatchQueue` code is not defined in the current source file, thus we don't have any information about it.
202 |
203 | The solution for this is to define a non-escaping rule. A non-escaping rule is a piece of code that takes in a closure expression and tells us whether the closure is non-escaping or not.
204 | To define a non-escaping rule, extend from `BaseNonEscapeRule` and override `func isNonEscape(arg: FunctionCallArgumentSyntax,....) -> Bool`
205 |
206 | Here's a rule that matches `DispatchQueue.main.async` or `DispatchQueue.global(qos:).asyncAfter` :
207 |
208 | ```swift
209 | open class DispatchQueueRule: NonEscapeRule {
210 |
211 | open override isNonEscape(arg: FunctionCallArgumentSyntax?, funcCallExpr: FunctionCallExprSyntax,, graph: Graph) -> Bool {
212 | // Signature of `async` function
213 | let asyncSignature = FunctionSignature(name: "async", params: [
214 | FunctionParam(name: "execute", isClosure: true)
215 | ])
216 |
217 | // Predicate to match DispatchQueue.main
218 | let mainQueuePredicate = ExprSyntaxPredicate.memberAccess("main", base: ExprSyntaxPredicate.name("DispatchQueue"))
219 |
220 | let mainQueueAsyncPredicate = ExprSyntaxPredicate.funcCall(asyncSignature, base: mainQueuePredicate)
221 | if funcCallExpr.match(mainQueueAsyncPredicate) { // Matched DispatchQueue.main.async(...)
222 | return true
223 | }
224 |
225 | // Signature of `asyncAfter` function
226 | let asyncAfterSignature = FunctionSignature(name: "asyncAfter", params: [
227 | FunctionParam(name: "deadline"),
228 | FunctionParam(name: "execute", isClosure: true)
229 | ])
230 |
231 | // Predicate to match DispatchQueue.global(qos: ...) or DispatchQueue.global()
232 | let globalQueuePredicate = ExprSyntaxPredicate.funcCall(
233 | FunctionSignature(name: "global", params: [
234 | FunctionParam(name: "qos", canOmit: true)
235 | ]),
236 | base: ExprSyntaxPredicate.name("DispatchQueue")
237 | )
238 |
239 | let globalQueueAsyncAfterPredicate = ExprSyntaxPredicate.funcCall(asyncAfterSignature, base: globalQueuePredicate)
240 | if funcCallExpr.match(globalQueueAsyncAfterPredicate) {
241 | return true
242 | }
243 |
244 | // Doesn't match either function
245 | return false
246 | }
247 | }
248 | ```
249 |
250 | Here's another example of rule that matches `UIView.animate(withDurations: animations:)`:
251 |
252 | ```swift
253 | open class UIViewAnimationRule: BaseNonEscapeRule {
254 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?, funcCallExpr: FunctionCallExprSyntax, graph: Graph) -> Bool {
255 | let signature = FunctionSignature(name: "animate", params: [
256 | FunctionParam(name: "withDuration"),
257 | FunctionParam(name: "animations", isClosure: true)
258 | ])
259 |
260 | let predicate = ExprSyntaxPredicate.funcCall(signature, base: ExprSyntaxPredicate.name("UIView"))
261 | return funcCallExpr.match(predicate)
262 | }
263 | }
264 | ```
265 |
266 | After creating the non-escaping rule, pass it to the leak detector:
267 |
268 | ```swift
269 | let leakDetector = GraphLeakDetector(nonEscapingRules: [DispatchQueueRule(), UIViewAnimationRule()])
270 | ```
271 |
272 | # Predefined non-escaping rules
273 |
274 | There're some ready-to-be-used non-escaping rules:
275 |
276 | **1. DispatchQueueRule**
277 |
278 | We know that a closure passed to `DispatchQueue.main.async` or its variations is escaping, but the closure will be executed very soon and destroyed after that. So even if it holds a strong reference to `self`, the reference
279 | will be gone quickly. So it's actually ok to treat it as non-escaping
280 |
281 | **3. UIViewAnimationRule**
282 |
283 | UIView static animation functions. Similar to DispatchQueue, UIView animation closures are escaping but will be executed then destroyed quickly.
284 |
285 | **3. UIViewControllerAnimationRule**
286 |
287 | UIViewController's present/dismiss functions. Similar to UIView animation rule.
288 |
289 | **4. CollectionRules**
290 |
291 | Swift Collection map/flatMap/compactMap/sort/filter/forEach. All these Swift Collection functions take in a non-escaping closure
292 |
293 | # Write your own detector
294 |
295 | In case you want to make your own detector instead of using the provided GraphLeakDetector, create a class that extends from `BaseSyntaxTreeLeakDetector` and override the function
296 |
297 | ```swift
298 | class MyOwnLeakDetector: BaseSyntaxTreeLeakDetector {
299 | override func detect(_ sourceFileNode: SourceFileSyntax) -> [Leak] {
300 | // Your own implementation
301 | }
302 | }
303 |
304 | // Create an instance and start detecting leaks
305 | let detector = MyOwnLeakDetector()
306 | let url = URL(fileURLWithPath: "absolute/path/to/your/swift/file/or/folder")
307 | let leaks = detector.detect(url)
308 | ```
309 |
310 |
311 | ### Graph
312 |
313 | Graph is the brain of the tool. It processes the AST and give valuable information, such as where a reference is defined, or if a closure is escaping or not.
314 | You probably want to use it if you create your own detector:
315 |
316 | ```swift
317 | let graph = GraphBuilder.buildGraph(node: sourceFileNode)
318 | ```
319 |
320 |
321 | # Note
322 |
323 | 1. To check a source file, we use only the AST of that file, and not any other source file. So if you call a function that is defined elsewhere, that information is not available.
324 |
325 | 2. For non-escaping closure, there's no need to use `self.`. This can help to prevent false-positive
326 |
327 |
328 | # License
329 |
330 | This library is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
331 |
332 |
333 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/BackwardCompatiblity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tmp.swift
3 | // SwiftLeakCheck
4 | //
5 | // Created by Hoang Le Pham on 07/02/2021.
6 | //
7 |
8 | import SwiftSyntax
9 |
10 | // For backward-compatible with Swift compiler 4.2 type names
11 | public typealias FunctionCallArgumentListSyntax = TupleExprElementListSyntax
12 | public typealias FunctionCallArgumentSyntax = TupleExprElementSyntax
13 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/BaseSyntaxTreeLeakDetector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseSyntaxTreeLeakDetector.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 09/12/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | open class BaseSyntaxTreeLeakDetector: LeakDetector {
14 | public init() {}
15 |
16 | public func detect(content: String) throws -> [Leak] {
17 | let node = try SyntaxRetrieval.request(content: content)
18 | return detect(node)
19 | }
20 |
21 | open func detect(_ sourceFileNode: SourceFileSyntax) -> [Leak] {
22 | fatalError("Implemented by subclass")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/DirectoryScanner.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DirectoryScanner.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 09/12/2019.
9 | //
10 |
11 | import Foundation
12 |
13 | public final class DirectoryScanner {
14 | private let callback: (URL, inout Bool) -> Void
15 | private var shouldStop = false
16 |
17 | public init(callback: @escaping (URL, inout Bool) -> Void) {
18 | self.callback = callback
19 | }
20 |
21 | public func scan(url: URL) {
22 | if shouldStop {
23 | shouldStop = false // Clear
24 | return
25 | }
26 |
27 | let isDirectory = (try? url.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
28 | if !isDirectory {
29 | callback(url, &shouldStop)
30 | } else {
31 | let enumerator = FileManager.default.enumerator(
32 | at: url,
33 | includingPropertiesForKeys: nil,
34 | options: [.skipsSubdirectoryDescendants],
35 | errorHandler: nil
36 | )!
37 |
38 | for childPath in enumerator {
39 | if let url = childPath as? URL {
40 | scan(url: url)
41 | if shouldStop {
42 | return
43 | }
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/Function.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Function.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 18/11/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public typealias Function = FunctionDeclSyntax
14 |
15 | public extension Function {
16 | enum MatchResult: Equatable {
17 | public struct MappingInfo: Equatable {
18 | let argumentToParamMapping: [FunctionCallArgumentSyntax: FunctionParameterSyntax]
19 | let trailingClosureArgumentToParam: FunctionParameterSyntax?
20 | }
21 |
22 | case nameMismatch
23 | case argumentMismatch
24 | case matched(MappingInfo)
25 |
26 | var isMatched: Bool {
27 | switch self {
28 | case .nameMismatch,
29 | .argumentMismatch:
30 | return false
31 | case .matched:
32 | return true
33 | }
34 | }
35 | }
36 |
37 | func match(_ functionCallExpr: FunctionCallExprSyntax) -> MatchResult {
38 | let (signature, mapping) = FunctionSignature.from(functionDeclExpr: self)
39 | switch signature.match(functionCallExpr) {
40 | case .nameMismatch:
41 | return .nameMismatch
42 | case .argumentMismatch:
43 | return .argumentMismatch
44 | case .matched(let matchedInfo):
45 | return .matched(.init(
46 | argumentToParamMapping: matchedInfo.argumentToParamMapping.mapValues { mapping[$0]! },
47 | trailingClosureArgumentToParam: matchedInfo.trailingClosureArgumentToParam.flatMap { mapping[$0] }))
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/FunctionSignature.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FunctionSignature.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 15/12/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public struct FunctionSignature {
14 | public let funcName: String
15 | public let params: [FunctionParam]
16 |
17 | public init(name: String, params: [FunctionParam]) {
18 | self.funcName = name
19 | self.params = params
20 | }
21 |
22 | public static func from(functionDeclExpr: FunctionDeclSyntax) -> (FunctionSignature, [FunctionParam: FunctionParameterSyntax]) {
23 | let funcName = functionDeclExpr.identifier.text
24 | let params = functionDeclExpr.signature.input.parameterList.map { FunctionParam(param: $0) }
25 | let mapping = Dictionary(uniqueKeysWithValues: zip(params, functionDeclExpr.signature.input.parameterList))
26 | return (FunctionSignature(name: funcName, params: params), mapping)
27 | }
28 |
29 | public enum MatchResult: Equatable {
30 | public struct MappingInfo: Equatable {
31 | let argumentToParamMapping: [FunctionCallArgumentSyntax: FunctionParam]
32 | let trailingClosureArgumentToParam: FunctionParam?
33 | }
34 |
35 | case nameMismatch
36 | case argumentMismatch
37 | case matched(MappingInfo)
38 |
39 | var isMatched: Bool {
40 | switch self {
41 | case .nameMismatch,
42 | .argumentMismatch:
43 | return false
44 | case .matched:
45 | return true
46 | }
47 | }
48 | }
49 |
50 | public func match(_ functionCallExpr: FunctionCallExprSyntax) -> MatchResult {
51 | guard funcName == functionCallExpr.symbol?.text else {
52 | return .nameMismatch
53 | }
54 |
55 | print("Debug: \(functionCallExpr)")
56 |
57 | return match((ArgumentListWrapper(functionCallExpr.argumentList), functionCallExpr.trailingClosure))
58 | }
59 |
60 | private func match(_ rhs: (ArgumentListWrapper, ClosureExprSyntax?)) -> MatchResult {
61 | let (arguments, trailingClosure) = rhs
62 |
63 | guard params.count > 0 else {
64 | if arguments.count == 0 && trailingClosure == nil {
65 | return .matched(.init(argumentToParamMapping: [:], trailingClosureArgumentToParam: nil))
66 | } else {
67 | return .argumentMismatch
68 | }
69 | }
70 |
71 | let firstParam = params[0]
72 | if firstParam.canOmit {
73 | let matchResult = removingFirstParam().match(rhs)
74 | if matchResult.isMatched {
75 | return matchResult
76 | }
77 | }
78 |
79 | guard arguments.count > 0 else {
80 | // In order to match, trailingClosure must be firstParam, there're no more params
81 | guard let trailingClosure = trailingClosure else {
82 | return .argumentMismatch
83 | }
84 | if params.count > 1 {
85 | return .argumentMismatch
86 | }
87 | if isMatched(param: firstParam, trailingClosure: trailingClosure) {
88 | return .matched(.init(argumentToParamMapping: [:], trailingClosureArgumentToParam: firstParam))
89 | } else {
90 | return .argumentMismatch
91 | }
92 | }
93 |
94 | let firstArgument = arguments[0]
95 | guard isMatched(param: firstParam, argument: firstArgument) else {
96 | return .argumentMismatch
97 | }
98 |
99 | let matchResult = removingFirstParam().match((arguments.removingFirst(), trailingClosure))
100 | if case let .matched(matchInfo) = matchResult {
101 | var argumentToParamMapping = matchInfo.argumentToParamMapping
102 | argumentToParamMapping[firstArgument] = firstParam
103 | return .matched(.init(argumentToParamMapping: argumentToParamMapping, trailingClosureArgumentToParam: matchInfo.trailingClosureArgumentToParam))
104 | } else {
105 | return .argumentMismatch
106 | }
107 | }
108 |
109 | // TODO: type matching
110 | private func isMatched(param: FunctionParam, argument: FunctionCallArgumentSyntax) -> Bool {
111 | return param.name == argument.label?.text
112 | }
113 | private func isMatched(param: FunctionParam, trailingClosure: ClosureExprSyntax) -> Bool {
114 | return param.isClosure
115 | }
116 |
117 | private func removingFirstParam() -> FunctionSignature {
118 | return FunctionSignature(name: funcName, params: Array(params[1...]))
119 | }
120 | }
121 |
122 | public struct FunctionParam: Hashable {
123 | public let name: String?
124 | public let secondName: String? // This acts as a way to differentiate param when name is omitted. Don't remove this
125 | public let canOmit: Bool
126 | public let isClosure: Bool
127 |
128 | public init(name: String?,
129 | secondName: String? = nil,
130 | isClosure: Bool = false,
131 | canOmit: Bool = false) {
132 | assert(name != "_")
133 | self.name = name
134 | self.secondName = secondName
135 | self.isClosure = isClosure
136 | self.canOmit = canOmit
137 | }
138 |
139 | public init(param: FunctionParameterSyntax) {
140 | name = (param.firstName?.text == "_" ? nil : param.firstName?.text)
141 | secondName = param.secondName?.text
142 |
143 | isClosure = (param.type?.isClosure == true)
144 | canOmit = param.defaultArgument != nil
145 | }
146 | }
147 |
148 | private struct ArgumentListWrapper {
149 | let argumentList: FunctionCallArgumentListSyntax
150 | private let startIndex: Int
151 |
152 | init(_ argumentList: FunctionCallArgumentListSyntax) {
153 | self.init(argumentList, startIndex: 0)
154 | }
155 |
156 | private init(_ argumentList: FunctionCallArgumentListSyntax, startIndex: Int) {
157 | self.argumentList = argumentList
158 | self.startIndex = startIndex
159 | }
160 |
161 | func removingFirst() -> ArgumentListWrapper {
162 | return ArgumentListWrapper(argumentList, startIndex: startIndex + 1)
163 | }
164 |
165 | subscript(_ i: Int) -> FunctionCallArgumentSyntax {
166 | return argumentList[startIndex + i]
167 | }
168 |
169 | var count: Int {
170 | return argumentList.count - startIndex
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/Graph.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Graph.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 11/11/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public protocol Graph {
14 | var sourceFileScope: SourceFileScope { get }
15 |
16 | /// Return the corresponding scope of a node if the node is of scope-type (class, func, closure,...)
17 | /// or return the enclosing scope if the node is not scope-type
18 | /// - Parameter node: The node
19 | func scope(for node: Syntax) -> Scope
20 |
21 | /// Get the scope that encloses a given node
22 | /// Eg, Scopes that enclose a func could be class, enum,...
23 | /// Or scopes that enclose a statement could be func, closure,...
24 | /// If the node is not enclosed by a scope (eg, sourcefile node), return the scope of the node itself
25 | /// - Parameter node: A node
26 | /// - Returns: The scope that encloses the node
27 | func enclosingScope(for node: Syntax) -> Scope
28 |
29 | /// Return the TypeDecl that encloses a given node
30 | /// - Parameter node: given node
31 | func enclosingTypeDecl(for node: Syntax) -> TypeDecl?
32 |
33 | /// Find the nearest scope to a symbol, that can resolve the definition of that symbol
34 | /// Usually it is the enclosing scope of the symbol
35 | func closetScopeThatCanResolveSymbol(_ symbol: Symbol) -> Scope?
36 |
37 | func resolveExprType(_ expr: ExprSyntax) -> TypeResolve
38 | func resolveVariableType(_ variable: Variable) -> TypeResolve
39 | func resolveType(_ type: TypeSyntax) -> TypeResolve
40 | func getAllRelatedTypeDecls(from typeDecl: TypeDecl) -> [TypeDecl]
41 | func getAllRelatedTypeDecls(from typeResolve: TypeResolve) -> [TypeDecl]
42 |
43 | func resolveVariable(_ identifier: IdentifierExprSyntax) -> Variable?
44 | func getVariableReferences(variable: Variable) -> [IdentifierExprSyntax]
45 |
46 | func resolveFunction(_ funcCallExpr: FunctionCallExprSyntax) -> (Function, Function.MatchResult.MappingInfo)?
47 |
48 | func isClosureEscape(_ closure: ClosureExprSyntax, nonEscapeRules: [NonEscapeRule]) -> Bool
49 | func isCollection(_ node: ExprSyntax) -> Bool
50 | }
51 |
52 | final class GraphImpl: Graph {
53 | enum SymbolResolve {
54 | case variable(Variable)
55 | case function(Function)
56 | case typeDecl(TypeDecl)
57 |
58 | var variable: Variable? {
59 | switch self {
60 | case .variable(let variable): return variable
61 | default:
62 | return nil
63 | }
64 | }
65 | }
66 |
67 | private var mapScopeNodeToScope = [ScopeNode: Scope]()
68 | private var cachedSymbolResolved = [Symbol: SymbolResolve]()
69 | private var cachedReferencesToVariable = [Variable: [IdentifierExprSyntax]]()
70 | private var cachedVariableType = [Variable: TypeResolve]()
71 | private var cachedFunCallExprType = [FunctionCallExprSyntax: TypeResolve]()
72 | private var cachedClosureEscapeCheck = [ClosureExprSyntax: Bool]()
73 |
74 | let sourceFileScope: SourceFileScope
75 | init(sourceFileScope: SourceFileScope) {
76 | self.sourceFileScope = sourceFileScope
77 | buildScopeNodeToScopeMapping(root: sourceFileScope)
78 | }
79 |
80 | private func buildScopeNodeToScopeMapping(root: Scope) {
81 | mapScopeNodeToScope[root.scopeNode] = root
82 | root.childScopes.forEach { child in
83 | buildScopeNodeToScopeMapping(root: child)
84 | }
85 | }
86 | }
87 |
88 | // MARK: - Scope
89 | extension GraphImpl {
90 | func scope(for node: Syntax) -> Scope {
91 | guard let scopeNode = ScopeNode.from(node: node) else {
92 | return enclosingScope(for: node)
93 | }
94 |
95 | return scope(for: scopeNode)
96 | }
97 |
98 | func enclosingScope(for node: Syntax) -> Scope {
99 | guard let scopeNode = node.enclosingScopeNode else {
100 | let result = scope(for: node)
101 | assert(result == sourceFileScope)
102 | return result
103 | }
104 |
105 | return scope(for: scopeNode)
106 | }
107 |
108 | func enclosingTypeDecl(for node: Syntax) -> TypeDecl? {
109 | var scopeNode: ScopeNode! = node.enclosingScopeNode
110 | while scopeNode != nil {
111 | if scopeNode.type.isTypeDecl {
112 | return scope(for: scopeNode).typeDecl
113 | }
114 | scopeNode = scopeNode.enclosingScopeNode
115 | }
116 |
117 | return nil
118 | }
119 |
120 | func scope(for scopeNode: ScopeNode) -> Scope {
121 | guard let result = mapScopeNodeToScope[scopeNode] else {
122 | fatalError("Can't find the scope of node \(scopeNode)")
123 | }
124 | return result
125 | }
126 |
127 | func closetScopeThatCanResolveSymbol(_ symbol: Symbol) -> Scope? {
128 | let scope = enclosingScope(for: symbol.node)
129 | // Special case when node is a closure capture item, ie `{ [weak self] in`
130 | // We need to examine node wrt closure's parent
131 | if symbol.node.parent?.is(ClosureCaptureItemSyntax.self) == true {
132 | if let parentScope = scope.parent {
133 | return parentScope
134 | } else {
135 | fatalError("Can't happen")
136 | }
137 | }
138 |
139 | if symbol.node.hasAncestor({ $0.is(InheritedTypeSyntax.self) }) {
140 | return scope.parent
141 | }
142 |
143 | if symbol.node.hasAncestor({ $0.is(ExtensionDeclSyntax.self) && symbol.node.isDescendent(of: $0.as(ExtensionDeclSyntax.self)!.extendedType._syntaxNode) }) {
144 | return scope.parent
145 | }
146 |
147 | return scope
148 | }
149 | }
150 |
151 | // MARK: - Symbol resolve
152 | extension GraphImpl {
153 | enum ResolveSymbolOption: Equatable, CaseIterable {
154 | case function
155 | case variable
156 | case typeDecl
157 | }
158 |
159 | func _findSymbol(_ symbol: Symbol,
160 | options: [ResolveSymbolOption] = ResolveSymbolOption.allCases,
161 | onResult: (SymbolResolve) -> Bool) -> SymbolResolve? {
162 | var scope: Scope! = closetScopeThatCanResolveSymbol(symbol)
163 | while scope != nil {
164 | if let result = cachedSymbolResolved[symbol], onResult(result) {
165 | return result
166 | }
167 |
168 | if let result = _findSymbol(symbol, options: options, in: scope, onResult: onResult) {
169 | cachedSymbolResolved[symbol] = result
170 | return result
171 | }
172 |
173 | scope = scope?.parent
174 | }
175 |
176 | return nil
177 | }
178 |
179 | func _findSymbol(_ symbol: Symbol,
180 | options: [ResolveSymbolOption] = ResolveSymbolOption.allCases,
181 | in scope: Scope,
182 | onResult: (SymbolResolve) -> Bool) -> SymbolResolve? {
183 |
184 | let scopesWithRelatedTypeDecl: [Scope]
185 | if let typeDecl = scope.typeDecl {
186 | scopesWithRelatedTypeDecl = getAllRelatedTypeDecls(from: typeDecl).map { $0.scope }
187 | } else {
188 | scopesWithRelatedTypeDecl = [scope]
189 | }
190 |
191 | for scope in scopesWithRelatedTypeDecl {
192 | if options.contains(.variable) {
193 | if case let .identifier(node) = symbol, let variable = scope.getVariable(node) {
194 | let result: SymbolResolve = .variable(variable)
195 | if onResult(result) {
196 | cachedReferencesToVariable[variable] = (cachedReferencesToVariable[variable] ?? []) + [node]
197 | return result
198 | }
199 | }
200 | }
201 |
202 | if options.contains(.function) {
203 | for function in scope.getFunctionWithSymbol(symbol) {
204 | let result: SymbolResolve = .function(function)
205 | if onResult(result) {
206 | return result
207 | }
208 | }
209 | }
210 |
211 | if options.contains(.typeDecl) {
212 | let typeDecls = scope.getTypeDecl(name: symbol.name)
213 | for typeDecl in typeDecls {
214 | let result: SymbolResolve = .typeDecl(typeDecl)
215 | if onResult(result) {
216 | return result
217 | }
218 | }
219 | }
220 | }
221 |
222 | return nil
223 | }
224 | }
225 |
226 | // MARK: - Variable reference
227 | extension GraphImpl {
228 |
229 | @discardableResult
230 | func resolveVariable(_ identifier: IdentifierExprSyntax) -> Variable? {
231 | return _findSymbol(.identifier(identifier), options: [.variable]) { resolve -> Bool in
232 | if resolve.variable != nil {
233 | return true
234 | }
235 | return false
236 | }?.variable
237 | }
238 |
239 | func getVariableReferences(variable: Variable) -> [IdentifierExprSyntax] {
240 | return cachedReferencesToVariable[variable] ?? []
241 | }
242 |
243 | func couldReferenceSelf(_ node: ExprSyntax) -> Bool {
244 | if node.isCalledExpr() {
245 | return false
246 | }
247 |
248 | if let identifierNode = node.as(IdentifierExprSyntax.self) {
249 | guard let variable = resolveVariable(identifierNode) else {
250 | return identifierNode.identifier.text == "self"
251 | }
252 |
253 | switch variable.raw {
254 | case .param:
255 | return false
256 | case let .capture(capturedNode):
257 | return couldReferenceSelf(ExprSyntax(capturedNode))
258 | case let .binding(_, valueNode):
259 | if let valueNode = valueNode {
260 | return couldReferenceSelf(valueNode)
261 | }
262 | return false
263 | }
264 | }
265 |
266 | return false
267 | }
268 | }
269 |
270 | // MARK: - Function resolve
271 | extension GraphImpl {
272 | func resolveFunction(_ funcCallExpr: FunctionCallExprSyntax) -> (Function, Function.MatchResult.MappingInfo)? {
273 | if let identifier = funcCallExpr.calledExpression.as(IdentifierExprSyntax.self) { // doSmth(...) or A(...)
274 | return _findFunction(symbol: .identifier(identifier), funcCallExpr: funcCallExpr)
275 | }
276 |
277 | if let memberAccessExpr = funcCallExpr.calledExpression.as(MemberAccessExprSyntax.self) { // a.doSmth(...) or .doSmth(...)
278 | if let base = memberAccessExpr.base {
279 | if couldReferenceSelf(base) {
280 | return _findFunction(symbol: .token(memberAccessExpr.name), funcCallExpr: funcCallExpr)
281 | }
282 | let typeDecls = getAllRelatedTypeDecls(from: resolveExprType(base))
283 | return _findFunction(from: typeDecls, symbol: .token(memberAccessExpr.name), funcCallExpr: funcCallExpr)
284 | } else {
285 | // Base is omitted when the type can be inferred.
286 | // For eg, we can say: let s: String = .init(...)
287 | return nil
288 | }
289 | }
290 |
291 | if funcCallExpr.calledExpression.is(OptionalChainingExprSyntax.self) {
292 | // TODO
293 | return nil
294 | }
295 |
296 | // Unhandled case
297 | return nil
298 | }
299 |
300 | // TODO: Currently we only resolve to `func`. This could resole to `closure` as well
301 | private func _findFunction(symbol: Symbol, funcCallExpr: FunctionCallExprSyntax)
302 | -> (Function, Function.MatchResult.MappingInfo)? {
303 |
304 | var result: (Function, Function.MatchResult.MappingInfo)?
305 | _ = _findSymbol(symbol, options: [.function]) { resolve -> Bool in
306 | switch resolve {
307 | case .variable, .typeDecl: // This could be due to cache
308 | return false
309 | case .function(let function):
310 | let mustStop = enclosingScope(for: function._syntaxNode).type.isTypeDecl
311 |
312 | switch function.match(funcCallExpr) {
313 | case .argumentMismatch,
314 | .nameMismatch:
315 | return mustStop
316 | case .matched(let info):
317 | guard result == nil else {
318 | // Should not happenn
319 | assert(false, "ambiguous")
320 | return true // Exit
321 | }
322 | result = (function, info)
323 | #if DEBUG
324 | return mustStop // Continue to search to make sure no ambiguity
325 | #else
326 | return true
327 | #endif
328 | }
329 | }
330 | }
331 |
332 | return result
333 | }
334 |
335 | private func _findFunction(from typeDecls: [TypeDecl], symbol: Symbol, funcCallExpr: FunctionCallExprSyntax)
336 | -> (Function, Function.MatchResult.MappingInfo)? {
337 |
338 | for typeDecl in typeDecls {
339 | for function in typeDecl.scope.getFunctionWithSymbol(symbol) {
340 | if case let .matched(info) = function.match(funcCallExpr) {
341 | return (function, info)
342 | }
343 | }
344 | }
345 |
346 | return nil
347 | }
348 | }
349 |
350 | // MARK: Type resolve
351 | extension GraphImpl {
352 | func resolveVariableType(_ variable: Variable) -> TypeResolve {
353 | if let type = cachedVariableType[variable] {
354 | return type
355 | }
356 |
357 | let result = _resolveType(variable.typeInfo)
358 | cachedVariableType[variable] = result
359 | return result
360 | }
361 |
362 | func resolveExprType(_ expr: ExprSyntax) -> TypeResolve {
363 | if let optionalExpr = expr.as(OptionalChainingExprSyntax.self) {
364 | return .optional(base: resolveExprType(optionalExpr.expression))
365 | }
366 |
367 | if let identifierExpr = expr.as(IdentifierExprSyntax.self) {
368 | if let variable = resolveVariable(identifierExpr) {
369 | return resolveVariableType(variable)
370 | }
371 | if identifierExpr.identifier.text == "self" {
372 | return enclosingTypeDecl(for: expr._syntaxNode).flatMap { .type($0) } ?? .unknown
373 | }
374 | // May be global variable, or type like Int, String,...
375 | return .unknown
376 | }
377 |
378 | // if let memberAccessExpr = node.as(MemberAccessExprSyntax.self) {
379 | // guard let base = memberAccessExpr.base else {
380 | // fatalError("Is it possible that `base` is nil ?")
381 | // }
382 | //
383 | // }
384 |
385 | if let functionCallExpr = expr.as(FunctionCallExprSyntax.self) {
386 | let result = cachedFunCallExprType[functionCallExpr] ?? _resolveFunctionCallType(functionCallExpr: functionCallExpr)
387 | cachedFunCallExprType[functionCallExpr] = result
388 | return result
389 | }
390 |
391 | if let arrayExpr = expr.as(ArrayExprSyntax.self) {
392 | return .sequence(elementType: resolveExprType(arrayExpr.elements[0].expression))
393 | }
394 |
395 | if expr.is(DictionaryExprSyntax.self) {
396 | return .dict
397 | }
398 |
399 | if expr.is(IntegerLiteralExprSyntax.self) {
400 | return _getAllExtensions(name: ["Int"]).first.flatMap { .type($0) } ?? .name(["Int"])
401 | }
402 | if expr.is(StringLiteralExprSyntax.self) {
403 | return _getAllExtensions(name: ["String"]).first.flatMap { .type($0) } ?? .name(["String"])
404 | }
405 | if expr.is(FloatLiteralExprSyntax.self) {
406 | return _getAllExtensions(name: ["Float"]).first.flatMap { .type($0) } ?? .name(["Float"])
407 | }
408 | if expr.is(BooleanLiteralExprSyntax.self) {
409 | return _getAllExtensions(name: ["Bool"]).first.flatMap { .type($0) } ?? .name(["Bool"])
410 | }
411 |
412 | if let tupleExpr = expr.as(TupleExprSyntax.self) {
413 | if tupleExpr.elementList.count == 1, let range = tupleExpr.elementList[0].expression.rangeInfo {
414 | if let leftType = range.left.flatMap({ resolveExprType($0) })?.toNilIfUnknown {
415 | return .sequence(elementType: leftType)
416 | } else if let rightType = range.right.flatMap({ resolveExprType($0) })?.toNilIfUnknown {
417 | return .sequence(elementType: rightType)
418 | } else {
419 | return .unknown
420 | }
421 | }
422 |
423 | return .tuple(tupleExpr.elementList.map { resolveExprType($0.expression) })
424 | }
425 |
426 | if let subscriptExpr = expr.as(SubscriptExprSyntax.self) {
427 | let sequenceElementType = resolveExprType(subscriptExpr.calledExpression).sequenceElementType
428 | if sequenceElementType != .unknown {
429 | if subscriptExpr.argumentList.count == 1, let argument = subscriptExpr.argumentList.first?.expression {
430 | if argument.rangeInfo != nil {
431 | return .sequence(elementType: sequenceElementType)
432 | }
433 | if resolveExprType(argument).isInt {
434 | return sequenceElementType
435 | }
436 | }
437 | }
438 |
439 | return .unknown
440 | }
441 |
442 | return .unknown
443 | }
444 |
445 | private func _resolveType(_ typeInfo: TypeInfo) -> TypeResolve {
446 | switch typeInfo {
447 | case .exact(let type):
448 | return resolveType(type)
449 | case .inferedFromExpr(let expr):
450 | return resolveExprType(expr)
451 | case .inferedFromClosure(let closureExpr, let paramIndex, let paramCount):
452 | // let x: (X, Y) -> Z = { a,b in ...}
453 | if let closureVariable = enclosingScope(for: Syntax(closureExpr)).getVariableBindingTo(expr: ExprSyntax(closureExpr)) {
454 | switch closureVariable.typeInfo {
455 | case .exact(let type):
456 | guard let argumentsType = (type.as(FunctionTypeSyntax.self))?.arguments else {
457 | // Eg: let onFetchJobs: JobCardsFetcher.OnFetchJobs = { [weak self] jobs in ... }
458 | return .unknown
459 | }
460 | assert(argumentsType.count == paramCount)
461 | return resolveType(argumentsType[paramIndex].type)
462 | case .inferedFromClosure,
463 | .inferedFromExpr,
464 | .inferedFromSequence,
465 | .inferedFromTuple:
466 | assert(false, "Seems wrong")
467 | return .unknown
468 | }
469 | }
470 | // TODO: there's also this case
471 | // var b: ((X) -> Y)!
472 | // b = { x in ... }
473 | return .unknown
474 | case .inferedFromSequence(let sequenceExpr):
475 | let sequenceType = resolveExprType(sequenceExpr)
476 | return sequenceType.sequenceElementType
477 | case .inferedFromTuple(let tupleTypeInfo, let index):
478 | if case let .tuple(types) = _resolveType(tupleTypeInfo) {
479 | return types[index]
480 | }
481 | return .unknown
482 | }
483 | }
484 |
485 | func resolveType(_ type: TypeSyntax) -> TypeResolve {
486 | if type.isOptional {
487 | return .optional(base: resolveType(type.wrappedType))
488 | }
489 |
490 | if let arrayType = type.as(ArrayTypeSyntax.self) {
491 | return .sequence(elementType: resolveType(arrayType.elementType))
492 | }
493 |
494 | if type.is(DictionaryTypeSyntax.self) {
495 | return .dict
496 | }
497 |
498 | if let tupleType = type.as(TupleTypeSyntax.self) {
499 | return .tuple(tupleType.elements.map { resolveType($0.type) })
500 | }
501 |
502 | if let tokens = type.tokens, let typeDecl = resolveTypeDecl(tokens: tokens) {
503 | return .type(typeDecl)
504 | } else if let name = type.name {
505 | return .name(name)
506 | } else {
507 | return .unknown
508 | }
509 | }
510 |
511 | private func _resolveFunctionCallType(functionCallExpr: FunctionCallExprSyntax, ignoreOptional: Bool = false) -> TypeResolve {
512 |
513 | if let (function, _) = resolveFunction(functionCallExpr) {
514 | if let type = function.signature.output?.returnType {
515 | return resolveType(type)
516 | } else {
517 | return .unknown // Void
518 | }
519 | }
520 |
521 | var calledExpr = functionCallExpr.calledExpression
522 |
523 | if let optionalExpr = calledExpr.as(OptionalChainingExprSyntax.self) { // Must be optional closure
524 | if !ignoreOptional {
525 | return .optional(base: _resolveFunctionCallType(functionCallExpr: functionCallExpr, ignoreOptional: true))
526 | } else {
527 | calledExpr = optionalExpr.expression
528 | }
529 | }
530 |
531 | // [X]()
532 | if let arrayExpr = calledExpr.as(ArrayExprSyntax.self) {
533 | if let typeIdentifier = arrayExpr.elements[0].expression.as(IdentifierExprSyntax.self) {
534 | if let typeDecl = resolveTypeDecl(tokens: [typeIdentifier.identifier]) {
535 | return .sequence(elementType: .type(typeDecl))
536 | } else {
537 | return .sequence(elementType: .name([typeIdentifier.identifier.text]))
538 | }
539 | } else {
540 | return .sequence(elementType: resolveExprType(arrayExpr.elements[0].expression))
541 | }
542 | }
543 |
544 | // [X: Y]()
545 | if calledExpr.is(DictionaryExprSyntax.self) {
546 | return .dict
547 | }
548 |
549 | // doSmth() or A()
550 | if let identifierExpr = calledExpr.as(IdentifierExprSyntax.self) {
551 | let identifierResolve = _findSymbol(.identifier(identifierExpr)) { resolve in
552 | switch resolve {
553 | case .function(let function):
554 | return function.match(functionCallExpr).isMatched
555 | case .typeDecl:
556 | return true
557 | case .variable:
558 | return false
559 | }
560 | }
561 | if let identifierResolve = identifierResolve {
562 | switch identifierResolve {
563 | // doSmth()
564 | case .function(let function):
565 | let returnType = function.signature.output?.returnType
566 | return returnType.flatMap { resolveType($0) } ?? .unknown
567 | // A()
568 | case .typeDecl(let typeDecl):
569 | return .type(typeDecl)
570 | case .variable:
571 | break
572 | }
573 | }
574 | }
575 |
576 | // x.y()
577 | if let memberAccessExpr = calledExpr.as(MemberAccessExprSyntax.self) {
578 | if let base = memberAccessExpr.base {
579 | let baseType = resolveExprType(base)
580 | if _isCollection(baseType) {
581 | let funcName = memberAccessExpr.name.text
582 | if ["map", "flatMap", "compactMap", "enumerated"].contains(funcName) {
583 | return .sequence(elementType: .unknown)
584 | }
585 | if ["filter", "sorted"].contains(funcName) {
586 | return baseType
587 | }
588 | }
589 | } else {
590 | // Base is omitted when the type can be inferred.
591 | // For eg, we can say: let s: String = .init(...)
592 | return .unknown
593 | }
594 |
595 | }
596 |
597 | return .unknown
598 | }
599 | }
600 |
601 | // MARK: - TypeDecl resolve
602 | extension GraphImpl {
603 |
604 | func resolveTypeDecl(tokens: [TokenSyntax]) -> TypeDecl? {
605 | guard tokens.count > 0 else {
606 | return nil
607 | }
608 |
609 | return _resolveTypeDecl(token: tokens[0], onResult: { typeDecl in
610 | var currentScope = typeDecl.scope
611 | for token in tokens[1...] {
612 | if let scope = currentScope.getTypeDecl(name: token.text).first?.scope {
613 | currentScope = scope
614 | } else {
615 | return false
616 | }
617 | }
618 | return true
619 | })
620 | }
621 |
622 | private func _resolveTypeDecl(token: TokenSyntax, onResult: (TypeDecl) -> Bool) -> TypeDecl? {
623 | let result = _findSymbol(.token(token), options: [.typeDecl]) { resolve in
624 | if case let .typeDecl(typeDecl) = resolve {
625 | return onResult(typeDecl)
626 | }
627 | return false
628 | }
629 |
630 | if let result = result, case let .typeDecl(scope) = result {
631 | return scope
632 | }
633 |
634 | return nil
635 | }
636 |
637 | func getAllRelatedTypeDecls(from: TypeDecl) -> [TypeDecl] {
638 | var result: [TypeDecl] = _getAllExtensions(typeDecl: from)
639 | if !from.isExtension {
640 | result = [from] + result
641 | } else {
642 | if let originalDecl = resolveTypeDecl(tokens: from.tokens) {
643 | result = [originalDecl] + result
644 | }
645 | }
646 |
647 | return result + result.flatMap { typeDecl -> [TypeDecl] in
648 | guard let inheritanceTypes = typeDecl.inheritanceTypes else {
649 | return []
650 | }
651 |
652 | return inheritanceTypes
653 | .compactMap { resolveTypeDecl(tokens: $0.typeName.tokens ?? []) }
654 | .flatMap { getAllRelatedTypeDecls(from: $0) }
655 | }
656 | }
657 |
658 | func getAllRelatedTypeDecls(from: TypeResolve) -> [TypeDecl] {
659 | switch from.wrappedType {
660 | case .type(let typeDecl):
661 | return getAllRelatedTypeDecls(from: typeDecl)
662 | case .sequence:
663 | return _getAllExtensions(name: ["Array"]) + _getAllExtensions(name: ["Collection"])
664 | case .dict:
665 | return _getAllExtensions(name: ["Dictionary"]) + _getAllExtensions(name: ["Collection"])
666 | case .name, .tuple, .unknown:
667 | return []
668 | case .optional:
669 | // Can't happen
670 | return []
671 | }
672 | }
673 |
674 | private func _getAllExtensions(typeDecl: TypeDecl) -> [TypeDecl] {
675 | guard let name = _getTypeDeclFullPath(typeDecl)?.map({ $0.text }) else { return [] }
676 | return _getAllExtensions(name: name)
677 | }
678 |
679 | private func _getAllExtensions(name: [String]) -> [TypeDecl] {
680 | return sourceFileScope.childScopes
681 | .compactMap { $0.typeDecl }
682 | .filter { $0.isExtension && $0.name == name }
683 | }
684 |
685 | // For eg, type path for C in be example below is A.B.C
686 | // class A {
687 | // struct B {
688 | // enum C {
689 | // Returns nil if the type is nested inside non-type entity like function
690 | private func _getTypeDeclFullPath(_ typeDecl: TypeDecl) -> [TokenSyntax]? {
691 | let tokens = typeDecl.tokens
692 | if typeDecl.scope.parent?.type == .sourceFileNode {
693 | return tokens
694 | }
695 | if let parentTypeDecl = typeDecl.scope.parent?.typeDecl, let parentTokens = _getTypeDeclFullPath(parentTypeDecl) {
696 | return parentTokens + tokens
697 | }
698 | return nil
699 | }
700 | }
701 |
702 | // MARK: - Classification
703 | extension GraphImpl {
704 | func isClosureEscape(_ closure: ClosureExprSyntax, nonEscapeRules: [NonEscapeRule]) -> Bool {
705 | func _isClosureEscape(_ expr: ExprSyntax, isFuncParam: Bool) -> Bool {
706 | // check cache
707 | if let closureNode = expr.as(ClosureExprSyntax.self), let cachedResult = cachedClosureEscapeCheck[closureNode] {
708 | return cachedResult
709 | }
710 |
711 | // If it's a param, and it's inside an escaping closure, then it's also escaping
712 | // For eg:
713 | // func doSmth(block: @escaping () -> Void) {
714 | // someObject.callBlock {
715 | // block()
716 | // }
717 | // }
718 | // Here block is a param and it's used inside an escaping closure
719 | if isFuncParam {
720 | if let parentClosure = expr.getEnclosingClosureNode() {
721 | if isClosureEscape(parentClosure, nonEscapeRules: nonEscapeRules) {
722 | return true
723 | }
724 | }
725 | }
726 |
727 | // Function call expression: {...}()
728 | if expr.isCalledExpr() {
729 | return false // Not escape
730 | }
731 |
732 | // let x = closure
733 | // `x` may be used anywhere
734 | if let variable = enclosingScope(for: expr._syntaxNode).getVariableBindingTo(expr: expr) {
735 | let references = getVariableReferences(variable: variable)
736 | for reference in references {
737 | if _isClosureEscape(ExprSyntax(reference), isFuncParam: isFuncParam) == true {
738 | return true // Escape
739 | }
740 | }
741 | }
742 |
743 | // Used as argument in function call: doSmth(a, b, c: {...}) or doSmth(a, b) {...}
744 | if let (functionCall, argument) = expr.getEnclosingFunctionCallExpression() {
745 | if let (function, matchedInfo) = resolveFunction(functionCall) {
746 | let param: FunctionParameterSyntax!
747 | if let argument = argument {
748 | param = matchedInfo.argumentToParamMapping[argument]
749 | } else {
750 | param = matchedInfo.trailingClosureArgumentToParam
751 | }
752 | guard param != nil else { fatalError("Something wrong") }
753 |
754 | // If the param is marked as `@escaping`, we still need to check with the non-escaping rules
755 | // If the param is not marked as `@escaping`, and it's optional, we don't know anything about it
756 | // If the param is not marked as `@escaping`, and it's not optional, we know it's non-escaping for sure
757 | if !param.isEscaping && param.type?.isOptional != true {
758 | return false
759 | }
760 |
761 | // get the `.function` scope where we define this func
762 | let scope = self.scope(for: function._syntaxNode)
763 | assert(scope.type.isFunction)
764 |
765 | guard let variableForParam = scope.variables.first(where: { $0.raw.token == (param.secondName ?? param.firstName) }) else {
766 | fatalError("Can't find the Variable that wrap the param")
767 | }
768 | let references = getVariableReferences(variable: variableForParam)
769 | for referennce in references {
770 | if _isClosureEscape(ExprSyntax(referennce), isFuncParam: true) == true {
771 | return true
772 | }
773 | }
774 | return false
775 | } else {
776 | // Can't resolve the function
777 | // Use custom rules
778 | for rule in nonEscapeRules {
779 | if rule.isNonEscape(closureNode: expr, graph: self) {
780 | return false
781 | }
782 | }
783 |
784 | // Still can't figure out using custom rules, assume closure is escaping
785 | return true
786 | }
787 | }
788 |
789 | return false // It's unlikely the closure is escaping
790 | }
791 |
792 | let result = _isClosureEscape(ExprSyntax(closure), isFuncParam: false)
793 | cachedClosureEscapeCheck[closure] = result
794 | return result
795 | }
796 |
797 | func isCollection(_ node: ExprSyntax) -> Bool {
798 | let type = resolveExprType(node)
799 | return _isCollection(type)
800 | }
801 |
802 | private func _isCollection(_ type: TypeResolve) -> Bool {
803 | let isCollectionTypeName: ([String]) -> Bool = { (name: [String]) in
804 | return name == ["Array"] || name == ["Dictionary"] || name == ["Set"]
805 | }
806 |
807 | switch type {
808 | case .tuple,
809 | .unknown:
810 | return false
811 | case .sequence,
812 | .dict:
813 | return true
814 | case .optional(let base):
815 | return _isCollection(base)
816 | case .name(let name):
817 | return isCollectionTypeName(name)
818 | case .type(let typeDecl):
819 | let allTypeDecls = getAllRelatedTypeDecls(from: typeDecl)
820 | for typeDecl in allTypeDecls {
821 | if isCollectionTypeName(typeDecl.name) {
822 | return true
823 | }
824 |
825 | for inherritedName in (typeDecl.inheritanceTypes?.map { $0.typeName.name ?? [""] } ?? []) {
826 | // If it extends any of the collection types or implements Collection protocol
827 | if isCollectionTypeName(inherritedName) || inherritedName == ["Collection"] {
828 | return true
829 | }
830 | }
831 | }
832 |
833 | return false
834 | }
835 | }
836 | }
837 |
838 | private extension Scope {
839 | func getVariableBindingTo(expr: ExprSyntax) -> Variable? {
840 | return variables.first(where: { variable -> Bool in
841 | switch variable.raw {
842 | case .param, .capture: return false
843 | case let .binding(_, valueNode):
844 | return valueNode != nil ? valueNode! == expr : false
845 | }
846 | })
847 | }
848 | }
849 |
850 | private extension TypeResolve {
851 | var toNilIfUnknown: TypeResolve? {
852 | switch self {
853 | case .unknown: return nil
854 | default: return self
855 | }
856 | }
857 | }
858 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/GraphBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GraphBuilder.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 29/10/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | final class GraphBuilder {
14 | static func buildGraph(node: SourceFileSyntax) -> GraphImpl {
15 | // First round: build the graph
16 | let visitor = GraphBuilderVistor()
17 | visitor.walk(node)
18 |
19 | let graph = GraphImpl(sourceFileScope: visitor.sourceFileScope)
20 |
21 | // Second round: resolve the references
22 | ReferenceBuilderVisitor(graph: graph).walk(node)
23 |
24 | return graph
25 | }
26 | }
27 |
28 | class BaseGraphVistor: SyntaxAnyVisitor {
29 | override func visit(_ node: UnknownDeclSyntax) -> SyntaxVisitorContinueKind {
30 | return .skipChildren
31 | }
32 |
33 | override func visit(_ node: UnknownExprSyntax) -> SyntaxVisitorContinueKind {
34 | return .skipChildren
35 | }
36 |
37 | override func visit(_ node: UnknownStmtSyntax) -> SyntaxVisitorContinueKind {
38 | return .skipChildren
39 | }
40 |
41 | override func visit(_ node: UnknownTypeSyntax) -> SyntaxVisitorContinueKind {
42 | return .skipChildren
43 | }
44 |
45 | override func visit(_ node: UnknownPatternSyntax) -> SyntaxVisitorContinueKind {
46 | return .skipChildren
47 | }
48 | }
49 |
50 | fileprivate final class GraphBuilderVistor: BaseGraphVistor {
51 | fileprivate var sourceFileScope: SourceFileScope!
52 | private var stack = Stack()
53 |
54 | override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
55 | if let scopeNode = ScopeNode.from(node: node) {
56 | if case let .sourceFileNode(node) = scopeNode {
57 | assert(stack.peek() == nil)
58 | sourceFileScope = SourceFileScope(node: node, parent: stack.peek())
59 | stack.push(sourceFileScope)
60 | } else {
61 | let scope = Scope(scopeNode: scopeNode, parent: stack.peek())
62 | stack.push(scope)
63 | }
64 | }
65 |
66 | #if DEBUG
67 | if node.is(ElseBlockSyntax.self) || node.is(ElseIfContinuationSyntax.self) {
68 | assertionFailure("Unhandled case")
69 | }
70 | #endif
71 |
72 |
73 | return super.visitAny(node)
74 | }
75 |
76 | override func visitAnyPost(_ node: Syntax) {
77 | if let scopeNode = ScopeNode.from(node: node) {
78 | assert(stack.peek()?.scopeNode == scopeNode)
79 | stack.pop()
80 | }
81 | super.visitAnyPost(node)
82 | }
83 |
84 | // Note: this is not necessarily in a func x(param...)
85 | // Take this example:
86 | // x.block { param in ... }
87 | // Swift treats `param` as ClosureParamSyntax , but if we put `param` in open and close parathenses,
88 | // Swift will treat it as FunctionParameterSyntax
89 | override func visit(_ node: FunctionParameterSyntax) -> SyntaxVisitorContinueKind {
90 |
91 | _ = super.visit(node)
92 |
93 | guard let scope = stack.peek(), scope.type.isFunction || scope.type == .enumCaseNode else {
94 | fatalError()
95 | }
96 | guard let name = node.secondName ?? node.firstName else {
97 | assert(scope.type == .enumCaseNode)
98 | return .visitChildren
99 | }
100 |
101 | guard name.tokenKind != .wildcardKeyword else {
102 | return .visitChildren
103 | }
104 |
105 | scope.addVariable(Variable.from(node, scope: scope))
106 | return .visitChildren
107 | }
108 |
109 | override func visit(_ node: ClosureCaptureItemSyntax) -> SyntaxVisitorContinueKind {
110 |
111 | _ = super.visit(node)
112 |
113 | guard let scope = stack.peek(), scope.isClosure else {
114 | fatalError()
115 | }
116 |
117 | Variable.from(node, scope: scope).flatMap { scope.addVariable($0) }
118 | return .visitChildren
119 | }
120 |
121 | override func visit(_ node: ClosureParamSyntax) -> SyntaxVisitorContinueKind {
122 |
123 | _ = super.visit(node)
124 |
125 | guard let scope = stack.peek(), scope.isClosure else {
126 | fatalError("A closure should be found for a ClosureParam node. Stack may have been corrupted")
127 | }
128 | scope.addVariable(Variable.from(node, scope: scope))
129 | return .visitChildren
130 | }
131 |
132 | override func visit(_ node: PatternBindingSyntax) -> SyntaxVisitorContinueKind {
133 |
134 | _ = super.visit(node)
135 |
136 | guard let scope = stack.peek() else {
137 | fatalError()
138 | }
139 |
140 | Variable.from(node, scope: scope).forEach {
141 | scope.addVariable($0)
142 | }
143 |
144 | return .visitChildren
145 | }
146 |
147 | override func visit(_ node: OptionalBindingConditionSyntax) -> SyntaxVisitorContinueKind {
148 |
149 | _ = super.visit(node)
150 |
151 | guard let scope = stack.peek() else {
152 | fatalError()
153 | }
154 |
155 | let isGuardCondition = node.isGuardCondition()
156 | assert(!isGuardCondition || scope.type == .guardNode)
157 | let scopeThatOwnVariable = (isGuardCondition ? scope.parent! : scope)
158 | if let variable = Variable.from(node, scope: scopeThatOwnVariable) {
159 | scopeThatOwnVariable.addVariable(variable)
160 | }
161 | return .visitChildren
162 | }
163 |
164 | override func visit(_ node: ForInStmtSyntax) -> SyntaxVisitorContinueKind {
165 |
166 | _ = super.visit(node)
167 |
168 | guard let scope = stack.peek(), scope.type == .forLoopNode else {
169 | fatalError()
170 | }
171 |
172 | Variable.from(node, scope: scope).forEach { variable in
173 | scope.addVariable(variable)
174 | }
175 |
176 | return .visitChildren
177 | }
178 | }
179 |
180 | /// Visit the tree and resolve references
181 | private final class ReferenceBuilderVisitor: BaseGraphVistor {
182 | private let graph: GraphImpl
183 | init(graph: GraphImpl) {
184 | self.graph = graph
185 | }
186 |
187 | override func visit(_ node: IdentifierExprSyntax) -> SyntaxVisitorContinueKind {
188 | graph.resolveVariable(node)
189 | return .visitChildren
190 | }
191 | }
192 |
193 | private extension Scope {
194 | var isClosure: Bool {
195 | return type == .closureNode
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/GraphLeakDetector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GraphLeakDetector.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 12/11/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public final class GraphLeakDetector: BaseSyntaxTreeLeakDetector {
14 |
15 | public var nonEscapeRules: [NonEscapeRule] = []
16 |
17 | override public func detect(_ sourceFileNode: SourceFileSyntax) -> [Leak] {
18 | var res: [Leak] = []
19 | let graph = GraphBuilder.buildGraph(node: sourceFileNode)
20 | let sourceLocationConverter = SourceLocationConverter(file: "", tree: sourceFileNode)
21 | let visitor = LeakSyntaxVisitor(graph: graph, nonEscapeRules: nonEscapeRules, sourceLocationConverter: sourceLocationConverter) { leak in
22 | res.append(leak)
23 | }
24 | visitor.walk(sourceFileNode)
25 | return res
26 | }
27 | }
28 |
29 | private final class LeakSyntaxVisitor: BaseGraphVistor {
30 | private let graph: GraphImpl
31 | private let sourceLocationConverter: SourceLocationConverter
32 | private let onLeakDetected: (Leak) -> Void
33 | private let nonEscapeRules: [NonEscapeRule]
34 |
35 | init(graph: GraphImpl,
36 | nonEscapeRules: [NonEscapeRule],
37 | sourceLocationConverter: SourceLocationConverter,
38 | onLeakDetected: @escaping (Leak) -> Void) {
39 | self.graph = graph
40 | self.sourceLocationConverter = sourceLocationConverter
41 | self.nonEscapeRules = nonEscapeRules
42 | self.onLeakDetected = onLeakDetected
43 | }
44 |
45 | override func visit(_ node: IdentifierExprSyntax) -> SyntaxVisitorContinueKind {
46 | detectLeak(node)
47 | return .skipChildren
48 | }
49 |
50 | private func detectLeak(_ node: IdentifierExprSyntax) {
51 | var leak: Leak?
52 | defer {
53 | if let leak = leak {
54 | onLeakDetected(leak)
55 | }
56 | }
57 |
58 | if node.getEnclosingClosureNode() == nil {
59 | // Not inside closure -> ignore
60 | return
61 | }
62 |
63 | if !graph.couldReferenceSelf(ExprSyntax(node)) {
64 | return
65 | }
66 |
67 | var currentScope: Scope! = graph.closetScopeThatCanResolveSymbol(.identifier(node))
68 | var isEscape = false
69 | while currentScope != nil {
70 | if let variable = currentScope.getVariable(node) {
71 | if !isEscape {
72 | // No leak
73 | return
74 | }
75 |
76 | switch variable.raw {
77 | case .param:
78 | fatalError("Can't happen since a param cannot reference `self`")
79 | case let .capture(capturedNode):
80 | if variable.isStrong && isEscape {
81 | leak = Leak(node: node, capturedNode: ExprSyntax(capturedNode), sourceLocationConverter: sourceLocationConverter)
82 | }
83 | case let .binding(_, valueNode):
84 | if let referenceNode = valueNode?.as(IdentifierExprSyntax.self) {
85 | if variable.isStrong && isEscape {
86 | leak = Leak(node: node, capturedNode: ExprSyntax(referenceNode), sourceLocationConverter: sourceLocationConverter)
87 | }
88 | } else {
89 | fatalError("Can't reference `self`")
90 | }
91 | }
92 |
93 | return
94 | }
95 |
96 | if case let .closureNode(closureNode) = currentScope.scopeNode {
97 | isEscape = graph.isClosureEscape(closureNode, nonEscapeRules: nonEscapeRules)
98 | }
99 |
100 | currentScope = currentScope.parent
101 | }
102 |
103 | if isEscape {
104 | leak = Leak(node: node, capturedNode: nil, sourceLocationConverter: sourceLocationConverter)
105 | return
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/Leak.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LeakDetection.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 27/10/2019.
9 | //
10 |
11 | import Foundation
12 | import SwiftSyntax
13 |
14 | open class Leak: CustomStringConvertible, Encodable {
15 | public let node: IdentifierExprSyntax
16 | public let capturedNode: ExprSyntax?
17 | public let sourceLocationConverter: SourceLocationConverter
18 |
19 | public private(set) lazy var line: Int = {
20 | return sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia).line ?? -1
21 | }()
22 |
23 | public private(set) lazy var column: Int = {
24 | return sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia).column ?? -1
25 | }()
26 |
27 | public init(node: IdentifierExprSyntax,
28 | capturedNode: ExprSyntax?,
29 | sourceLocationConverter: SourceLocationConverter) {
30 | self.node = node
31 | self.capturedNode = capturedNode
32 | self.sourceLocationConverter = sourceLocationConverter
33 | }
34 |
35 | private enum CodingKeys: CodingKey {
36 | case line
37 | case column
38 | case reason
39 | }
40 |
41 | open func encode(to encoder: Encoder) throws {
42 | var container = encoder.container(keyedBy: CodingKeys.self)
43 | try container.encode(line, forKey: .line)
44 | try container.encode(column, forKey: .column)
45 |
46 | let reason: String = {
47 | return "`self` is strongly captured here, from a potentially escaped closure."
48 | }()
49 | try container.encode(reason, forKey: .reason)
50 | }
51 |
52 | open var description: String {
53 | return """
54 | `self` is strongly captured at (line: \(line), column: \(column))"),
55 | from a potentially escaped closure.
56 | """
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/LeakDetector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LeakDetector.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 27/10/2019.
9 | //
10 |
11 | import SwiftSyntax
12 | import Foundation
13 |
14 | public protocol LeakDetector {
15 | func detect(content: String) throws -> [Leak]
16 | }
17 |
18 | extension LeakDetector {
19 | public func detect(_ filePath: String) throws -> [Leak] {
20 | return try detect(content: String(contentsOfFile: filePath))
21 | }
22 |
23 | public func detect(_ url: URL) throws -> [Leak] {
24 | return try detect(content: String(contentsOf: url))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/NonEscapeRules/CollectionRules.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionRules.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 29/10/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | /// Swift Collection functions like forEach, map, flatMap, sorted,....
14 | public enum CollectionRules {
15 |
16 | public private(set) static var rules: [NonEscapeRule] = {
17 | return [
18 | CollectionForEachRule(),
19 | CollectionCompactMapRule(),
20 | CollectionMapRule(),
21 | CollectionFilterRule(),
22 | CollectionSortRule(),
23 | CollectionFlatMapRule(),
24 | CollectionFirstWhereRule(),
25 | CollectionContainsRule(),
26 | CollectionMaxMinRule()
27 | ]
28 | }()
29 | }
30 |
31 | open class CollectionForEachRule: BaseNonEscapeRule {
32 | public let mustBeCollection: Bool
33 | private let signature = FunctionSignature(name: "forEach", params: [
34 | FunctionParam(name: nil, isClosure: true)
35 | ])
36 | public init(mustBeCollection: Bool = false) {
37 | self.mustBeCollection = mustBeCollection
38 | }
39 |
40 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
41 | funcCallExpr: FunctionCallExprSyntax,
42 | graph: Graph) -> Bool {
43 | return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in
44 | return !self.mustBeCollection || isCollection(expr, graph: graph)
45 | }))
46 | }
47 | }
48 |
49 | open class CollectionCompactMapRule: BaseNonEscapeRule {
50 | private let signature = FunctionSignature(name: "compactMap", params: [
51 | FunctionParam(name: nil, isClosure: true)
52 | ])
53 |
54 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
55 | funcCallExpr: FunctionCallExprSyntax,
56 | graph: Graph) -> Bool {
57 | return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in
58 | return isCollection(expr, graph: graph)
59 | }))
60 | }
61 | }
62 |
63 | open class CollectionMapRule: BaseNonEscapeRule {
64 | private let signature = FunctionSignature(name: "map", params: [
65 | FunctionParam(name: nil, isClosure: true)
66 | ])
67 |
68 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
69 | funcCallExpr: FunctionCallExprSyntax,
70 | graph: Graph) -> Bool {
71 | return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in
72 | return isCollection(expr, graph: graph) || isOptional(expr, graph: graph)
73 | }))
74 | }
75 | }
76 |
77 | open class CollectionFlatMapRule: BaseNonEscapeRule {
78 | private let signature = FunctionSignature(name: "flatMap", params: [
79 | FunctionParam(name: nil, isClosure: true)
80 | ])
81 |
82 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
83 | funcCallExpr: FunctionCallExprSyntax,
84 | graph: Graph) -> Bool {
85 | return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in
86 | return isCollection(expr, graph: graph) || isOptional(expr, graph: graph)
87 | }))
88 | }
89 | }
90 |
91 | open class CollectionFilterRule: BaseNonEscapeRule {
92 | private let signature = FunctionSignature(name: "filter", params: [
93 | FunctionParam(name: nil, isClosure: true)
94 | ])
95 |
96 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
97 | funcCallExpr: FunctionCallExprSyntax,
98 | graph: Graph) -> Bool {
99 | return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in
100 | return isCollection(expr, graph: graph)
101 | }))
102 | }
103 | }
104 |
105 | open class CollectionSortRule: BaseNonEscapeRule {
106 | private let sortSignature = FunctionSignature(name: "sort", params: [
107 | FunctionParam(name: "by", isClosure: true)
108 | ])
109 | private let sortedSignature = FunctionSignature(name: "sorted", params: [
110 | FunctionParam(name: "by", isClosure: true)
111 | ])
112 |
113 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
114 | funcCallExpr: FunctionCallExprSyntax,
115 | graph: Graph) -> Bool {
116 | return funcCallExpr.match(.funcCall(signature: sortSignature, base: .init { return isCollection($0, graph: graph) }))
117 | || funcCallExpr.match(.funcCall(signature: sortedSignature, base: .init { return isCollection($0, graph: graph) }))
118 | }
119 | }
120 |
121 | open class CollectionFirstWhereRule: BaseNonEscapeRule {
122 | private let firstWhereSignature = FunctionSignature(name: "first", params: [
123 | FunctionParam(name: "where", isClosure: true)
124 | ])
125 | private let firstIndexWhereSignature = FunctionSignature(name: "firstIndex", params: [
126 | FunctionParam(name: "where", isClosure: true)
127 | ])
128 |
129 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
130 | funcCallExpr: FunctionCallExprSyntax,
131 | graph: Graph) -> Bool {
132 | let base = ExprSyntaxPredicate { expr in
133 | return isCollection(expr, graph: graph)
134 | }
135 | return funcCallExpr.match(.funcCall(signature: firstWhereSignature, base: base))
136 | || funcCallExpr.match(.funcCall(signature: firstIndexWhereSignature, base: base))
137 | }
138 | }
139 |
140 | open class CollectionContainsRule: BaseNonEscapeRule {
141 | let signature = FunctionSignature(name: "contains", params: [
142 | FunctionParam(name: "where", isClosure: true)
143 | ])
144 |
145 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
146 | funcCallExpr: FunctionCallExprSyntax,
147 | graph: Graph) -> Bool {
148 | return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in
149 | return isCollection(expr, graph: graph) }))
150 | }
151 | }
152 |
153 | open class CollectionMaxMinRule: BaseNonEscapeRule {
154 | private let maxSignature = FunctionSignature(name: "max", params: [
155 | FunctionParam(name: "by", isClosure: true)
156 | ])
157 | private let minSignature = FunctionSignature(name: "min", params: [
158 | FunctionParam(name: "by", isClosure: true)
159 | ])
160 |
161 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
162 | funcCallExpr: FunctionCallExprSyntax,
163 | graph: Graph) -> Bool {
164 | return funcCallExpr.match(.funcCall(signature: maxSignature, base: .init { return isCollection($0, graph: graph) }))
165 | || funcCallExpr.match(.funcCall(signature: minSignature, base: .init { return isCollection($0, graph: graph) }))
166 | }
167 | }
168 |
169 | private func isCollection(_ expr: ExprSyntax?, graph: Graph) -> Bool {
170 | guard let expr = expr else {
171 | return false
172 | }
173 | return graph.isCollection(expr)
174 | }
175 |
176 | private func isOptional(_ expr: ExprSyntax?, graph: Graph) -> Bool {
177 | guard let expr = expr else {
178 | return false
179 | }
180 | return graph.resolveExprType(expr).isOptional
181 | }
182 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/NonEscapeRules/DispatchQueueRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DispatchQueueRule.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 28/10/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | open class DispatchQueueRule: BaseNonEscapeRule {
14 |
15 | private let signatures: [FunctionSignature] = [
16 | FunctionSignature(name: "async", params: [
17 | FunctionParam(name: "group", canOmit: true),
18 | FunctionParam(name: "qos", canOmit: true),
19 | FunctionParam(name: "flags", canOmit: true),
20 | FunctionParam(name: "execute", isClosure: true)
21 | ]),
22 | FunctionSignature(name: "async", params: [
23 | FunctionParam(name: "group", canOmit: true),
24 | FunctionParam(name: "execute")
25 | ]),
26 | FunctionSignature(name: "sync", params: [
27 | FunctionParam(name: "flags", canOmit: true),
28 | FunctionParam(name: "execute", isClosure: true)
29 | ]),
30 | FunctionSignature(name: "sync", params: [
31 | FunctionParam(name: "execute")
32 | ]),
33 | FunctionSignature(name: "asyncAfter", params: [
34 | FunctionParam(name: "deadline"),
35 | FunctionParam(name: "qos", canOmit: true),
36 | FunctionParam(name: "flags", canOmit: true),
37 | FunctionParam(name: "execute", isClosure: true)
38 | ]),
39 | FunctionSignature(name: "asyncAfter", params: [
40 | FunctionParam(name: "wallDeadline"),
41 | FunctionParam(name: "qos", canOmit: true),
42 | FunctionParam(name: "flags", canOmit: true),
43 | FunctionParam(name: "execute", isClosure: true)
44 | ])
45 | ]
46 |
47 | private let mainQueuePredicate: ExprSyntaxPredicate = .memberAccess("main", base: .name("DispatchQueue"))
48 | private let globalQueuePredicate: ExprSyntaxPredicate = .funcCall(
49 | signature: FunctionSignature(name: "global", params: [.init(name: "qos", canOmit: true)]),
50 | base: .name("DispatchQueue")
51 | )
52 |
53 |
54 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
55 | funcCallExpr: FunctionCallExprSyntax,
56 | graph: Graph) -> Bool {
57 |
58 | for signature in signatures {
59 | for queue in [mainQueuePredicate, globalQueuePredicate] {
60 | let predicate: ExprSyntaxPredicate = .funcCall(signature: signature, base: queue)
61 | if funcCallExpr.match(predicate) {
62 | return true
63 | }
64 | }
65 | }
66 |
67 | let isDispatchQueuePredicate: ExprSyntaxPredicate = .init { expr -> Bool in
68 | guard let expr = expr else { return false }
69 | let typeResolve = graph.resolveExprType(expr)
70 | switch typeResolve.wrappedType {
71 | case .name(let name):
72 | return self.isDispatchQueueType(name: name)
73 | case .type(let typeDecl):
74 | let allTypeDecls = graph.getAllRelatedTypeDecls(from: typeDecl)
75 | for typeDecl in allTypeDecls {
76 | if self.isDispatchQueueType(typeDecl: typeDecl) {
77 | return true
78 | }
79 | }
80 |
81 | return false
82 |
83 | case .dict,
84 | .sequence,
85 | .tuple,
86 | .optional, // Can't happen
87 | .unknown:
88 | return false
89 | }
90 | }
91 |
92 | for signature in signatures {
93 | let predicate: ExprSyntaxPredicate = .funcCall(signature: signature, base: isDispatchQueuePredicate)
94 | if funcCallExpr.match(predicate) {
95 | return true
96 | }
97 | }
98 |
99 | return false
100 | }
101 |
102 | private func isDispatchQueueType(name: [String]) -> Bool {
103 | return name == ["DispatchQueue"]
104 | }
105 |
106 | private func isDispatchQueueType(typeDecl: TypeDecl) -> Bool {
107 | if self.isDispatchQueueType(name: typeDecl.name) {
108 | return true
109 | }
110 | for inheritedType in (typeDecl.inheritanceTypes ?? []) {
111 | if self.isDispatchQueueType(name: inheritedType.typeName.name ?? []) {
112 | return true
113 | }
114 | }
115 | return false
116 | }
117 | }
118 |
119 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/NonEscapeRules/ExprSyntaxPredicate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExprSyntaxPredicate.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 26/12/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | open class ExprSyntaxPredicate {
14 | public let match: (ExprSyntax?) -> Bool
15 | public init(_ match: @escaping (ExprSyntax?) -> Bool) {
16 | self.match = match
17 | }
18 |
19 | public static let any: ExprSyntaxPredicate = .init { _ in true }
20 | }
21 |
22 | // MARK: - Identifier predicate
23 | extension ExprSyntaxPredicate {
24 | public static func name(_ text: String) -> ExprSyntaxPredicate {
25 | return .name({ $0 == text })
26 | }
27 |
28 | public static func name(_ namePredicate: @escaping (String) -> Bool) -> ExprSyntaxPredicate {
29 | return .init({ expr -> Bool in
30 | guard let identifierExpr = expr?.as(IdentifierExprSyntax.self) else {
31 | return false
32 | }
33 | return namePredicate(identifierExpr.identifier.text)
34 | })
35 | }
36 | }
37 |
38 | // MARK: - Function call predicate
39 | extension ExprSyntaxPredicate {
40 | public static func funcCall(name: String,
41 | base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate {
42 | return .funcCall(namePredicate: { $0 == name }, base: basePredicate)
43 | }
44 |
45 | public static func funcCall(namePredicate: @escaping (String) -> Bool,
46 | base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate {
47 | return .funcCall(predicate: { funcCallExpr -> Bool in
48 | guard let symbol = funcCallExpr.symbol else {
49 | return false
50 | }
51 | return namePredicate(symbol.text)
52 | && basePredicate.match(funcCallExpr.base)
53 | })
54 | }
55 |
56 | public static func funcCall(signature: FunctionSignature,
57 | base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate {
58 | return .funcCall(predicate: { funcCallExpr -> Bool in
59 | return signature.match(funcCallExpr).isMatched
60 | && basePredicate.match(funcCallExpr.base)
61 | })
62 | }
63 |
64 | public static func funcCall(predicate: @escaping (FunctionCallExprSyntax) -> Bool) -> ExprSyntaxPredicate {
65 | return .init({ expr -> Bool in
66 | guard let funcCallExpr = expr?.as(FunctionCallExprSyntax.self) else {
67 | return false
68 | }
69 | return predicate(funcCallExpr)
70 | })
71 | }
72 | }
73 |
74 | // MARK: - MemberAccess predicate
75 | extension ExprSyntaxPredicate {
76 | public static func memberAccess(_ memberPredicate: @escaping (String) -> Bool,
77 | base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate {
78 | return .init({ expr -> Bool in
79 | guard let memberAccessExpr = expr?.as(MemberAccessExprSyntax.self) else {
80 | return false
81 | }
82 | return memberPredicate(memberAccessExpr.name.text)
83 | && basePredicate.match(memberAccessExpr.base)
84 | })
85 | }
86 |
87 | public static func memberAccess(_ member: String, base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate {
88 | return .memberAccess({ $0 == member }, base: basePredicate)
89 | }
90 | }
91 |
92 | public extension ExprSyntax {
93 |
94 | func match(_ predicate: ExprSyntaxPredicate) -> Bool {
95 | return predicate.match(self)
96 | }
97 | }
98 |
99 | // Convenient
100 | public extension FunctionCallExprSyntax {
101 | func match(_ predicate: ExprSyntaxPredicate) -> Bool {
102 | return predicate.match(ExprSyntax(self))
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/NonEscapeRules/NonEscapeRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NonEscapeRule.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 28/10/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public protocol NonEscapeRule {
14 | func isNonEscape(closureNode: ExprSyntax, graph: Graph) -> Bool
15 | }
16 |
17 | open class BaseNonEscapeRule: NonEscapeRule {
18 | public init() {}
19 |
20 | public func isNonEscape(closureNode: ExprSyntax, graph: Graph) -> Bool {
21 | guard let (funcCallExpr, arg) = closureNode.getEnclosingFunctionCallExpression() else {
22 | return false
23 | }
24 |
25 | return isNonEscape(
26 | arg: arg,
27 | funcCallExpr: funcCallExpr,
28 | graph: graph
29 | )
30 | }
31 |
32 | /// Returns whether a given argument is escaping in a function call
33 | ///
34 | /// - Parameters:
35 | /// - arg: The closure argument, or nil if it's trailing closure
36 | /// - funcCallExpr: the source FunctionCallExprSyntax
37 | /// - graph: Source code graph. Use it to retrieve more info
38 | /// - Returns: true if the closure is non-escaping, false otherwise
39 | open func isNonEscape(arg: FunctionCallArgumentSyntax?,
40 | funcCallExpr: FunctionCallExprSyntax,
41 | graph: Graph) -> Bool {
42 | return false
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/NonEscapeRules/UIViewAnimationRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewAnimationRule.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 28/10/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | /// Eg, UIView.animate(..., animations: {...}) {
14 | /// .....
15 | /// }
16 | open class UIViewAnimationRule: BaseNonEscapeRule {
17 |
18 | private let signatures: [FunctionSignature] = [
19 | FunctionSignature(name: "animate", params: [
20 | FunctionParam(name: "withDuration"),
21 | FunctionParam(name: "animations", isClosure: true)
22 | ]),
23 | FunctionSignature(name: "animate", params: [
24 | FunctionParam(name: "withDuration"),
25 | FunctionParam(name: "animations", isClosure: true),
26 | FunctionParam(name: "completion", isClosure: true, canOmit: true)
27 | ]),
28 | FunctionSignature(name: "animate", params: [
29 | FunctionParam(name: "withDuration"),
30 | FunctionParam(name: "delay"),
31 | FunctionParam(name: "options", canOmit: true),
32 | FunctionParam(name: "animations", isClosure: true),
33 | FunctionParam(name: "completion", isClosure: true, canOmit: true)
34 | ]),
35 | FunctionSignature(name: "animate", params: [
36 | FunctionParam(name: "withDuration"),
37 | FunctionParam(name: "delay"),
38 | FunctionParam(name: "usingSpringWithDamping"),
39 | FunctionParam(name: "initialSpringVelocity"),
40 | FunctionParam(name: "options", canOmit: true),
41 | FunctionParam(name: "animations", isClosure: true),
42 | FunctionParam(name: "completion", isClosure: true, canOmit: true)
43 | ]),
44 | FunctionSignature(name: "transition", params: [
45 | FunctionParam(name: "from"),
46 | FunctionParam(name: "to"),
47 | FunctionParam(name: "duration"),
48 | FunctionParam(name: "options"),
49 | FunctionParam(name: "completion", isClosure: true, canOmit: true),
50 | ]),
51 | FunctionSignature( name: "transition", params: [
52 | FunctionParam(name: "with"),
53 | FunctionParam(name: "duration"),
54 | FunctionParam(name: "options"),
55 | FunctionParam(name: "animations", isClosure: true, canOmit: true),
56 | FunctionParam(name: "completion", isClosure: true, canOmit: true),
57 | ]),
58 | FunctionSignature(name: "animateKeyframes", params: [
59 | FunctionParam(name: "withDuration"),
60 | FunctionParam(name: "delay", canOmit: true),
61 | FunctionParam(name: "options", canOmit: true),
62 | FunctionParam(name: "animations", isClosure: true),
63 | FunctionParam(name: "completion", isClosure: true)
64 | ])
65 | ]
66 |
67 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
68 | funcCallExpr: FunctionCallExprSyntax,
69 | graph: Graph) -> Bool {
70 |
71 | // Check if base is `UIView`, if not we can end early without checking any of the signatures
72 | guard funcCallExpr.match(.funcCall(namePredicate: { _ in true }, base: .name("UIView"))) else {
73 | return false
74 | }
75 |
76 | // Now we can check each signature and ignore the base (already checked)
77 | for signature in signatures {
78 | if funcCallExpr.match(.funcCall(signature: signature, base: .any)) {
79 | return true
80 | }
81 | }
82 |
83 | return false
84 | }
85 | }
86 |
87 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/NonEscapeRules/UIViewControllerAnimationRule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewControllerAnimationRule.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 30/12/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | /// Eg, someViewController.present(vc, animated: true, completion: { ... })
14 | /// or someViewController.dismiss(animated: true) { ... }
15 | open class UIViewControllerAnimationRule: BaseNonEscapeRule {
16 |
17 | private let signatures: [FunctionSignature] = [
18 | FunctionSignature(name: "present", params: [
19 | FunctionParam(name: nil), // "viewControllerToPresent"
20 | FunctionParam(name: "animated"),
21 | FunctionParam(name: "completion", isClosure: true, canOmit: true)
22 | ]),
23 | FunctionSignature(name: "dismiss", params: [
24 | FunctionParam(name: "animated"),
25 | FunctionParam(name: "completion", isClosure: true, canOmit: true)
26 | ]),
27 | FunctionSignature(name: "transition", params: [
28 | FunctionParam(name: "from"),
29 | FunctionParam(name: "to"),
30 | FunctionParam(name: "duration"),
31 | FunctionParam(name: "options", canOmit: true),
32 | FunctionParam(name: "animations", isClosure: true),
33 | FunctionParam(name: "completion", isClosure: true, canOmit: true)
34 | ])
35 | ]
36 |
37 | open override func isNonEscape(arg: FunctionCallArgumentSyntax?,
38 | funcCallExpr: FunctionCallExprSyntax,
39 | graph: Graph) -> Bool {
40 |
41 | // Make sure the func is called from UIViewController
42 | guard isCalledFromUIViewController(funcCallExpr: funcCallExpr, graph: graph) else {
43 | return false
44 | }
45 |
46 | // Now we can check each signature and ignore the base that is already checked
47 | for signature in signatures {
48 | if funcCallExpr.match(.funcCall(signature: signature, base: .any)) {
49 | return true
50 | }
51 | }
52 |
53 | return false
54 | }
55 |
56 | open func isUIViewControllerType(name: [String]) -> Bool {
57 |
58 | let typeName = name.last ?? ""
59 |
60 | let candidates = [
61 | "UIViewController",
62 | "UITableViewController",
63 | "UICollectionViewController",
64 | "UIAlertController",
65 | "UIActivityViewController",
66 | "UINavigationController",
67 | "UITabBarController",
68 | "UIMenuController",
69 | "UISearchController"
70 | ]
71 |
72 | return candidates.contains(typeName) || typeName.hasSuffix("ViewController")
73 | }
74 |
75 | private func isUIViewControllerType(typeDecl: TypeDecl) -> Bool {
76 | if isUIViewControllerType(name: typeDecl.name) {
77 | return true
78 | }
79 |
80 | let inheritantTypes = (typeDecl.inheritanceTypes ?? []).map { $0.typeName }
81 | for inheritantType in inheritantTypes {
82 | if isUIViewControllerType(name: inheritantType.name ?? []) {
83 | return true
84 | }
85 | }
86 |
87 | return false
88 | }
89 |
90 | private func isCalledFromUIViewController(funcCallExpr: FunctionCallExprSyntax, graph: Graph) -> Bool {
91 | guard let base = funcCallExpr.base else {
92 | // No base, eg: doSmth()
93 | // class SomeClass {
94 | // func main() {
95 | // doSmth()
96 | // }
97 | // }
98 | // In this case, we find the TypeDecl where this func is called from (Eg, SomeClass)
99 | if let typeDecl = graph.enclosingTypeDecl(for: funcCallExpr._syntaxNode) {
100 | return isUIViewControllerType(typeDecl: typeDecl)
101 | } else {
102 | return false
103 | }
104 | }
105 |
106 | // Eg: base.doSmth()
107 | // We check if base is UIViewController
108 | let typeResolve = graph.resolveExprType(base)
109 | switch typeResolve.wrappedType {
110 | case .type(let typeDecl):
111 | let allTypeDecls = graph.getAllRelatedTypeDecls(from: typeDecl)
112 | for typeDecl in allTypeDecls {
113 | if isUIViewControllerType(typeDecl: typeDecl) {
114 | return true
115 | }
116 | }
117 | return false
118 | case .name(let name):
119 | return isUIViewControllerType(name: name)
120 | case .dict,
121 | .sequence,
122 | .tuple,
123 | .optional, // Can't happen
124 | .unknown:
125 | return false
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/Scope.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Scope.swift
3 | // LeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 27/10/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public enum ScopeNode: Hashable, CustomStringConvertible {
14 | case sourceFileNode(SourceFileSyntax)
15 | case classNode(ClassDeclSyntax)
16 | case structNode(StructDeclSyntax)
17 | case enumNode(EnumDeclSyntax)
18 | case enumCaseNode(EnumCaseDeclSyntax)
19 | case extensionNode(ExtensionDeclSyntax)
20 | case funcNode(FunctionDeclSyntax)
21 | case initialiseNode(InitializerDeclSyntax)
22 | case closureNode(ClosureExprSyntax)
23 | case ifBlockNode(CodeBlockSyntax, IfStmtSyntax) // If block in a `IfStmtSyntax`
24 | case elseBlockNode(CodeBlockSyntax, IfStmtSyntax) // Else block in a `IfStmtSyntax`
25 | case guardNode(GuardStmtSyntax)
26 | case forLoopNode(ForInStmtSyntax)
27 | case whileLoopNode(WhileStmtSyntax)
28 | case subscriptNode(SubscriptDeclSyntax)
29 | case accessorNode(AccessorDeclSyntax)
30 | case variableDeclNode(CodeBlockSyntax) // var x: Int { ... }
31 | case switchCaseNode(SwitchCaseSyntax)
32 |
33 | public static func from(node: Syntax) -> ScopeNode? {
34 | if let sourceFileNode = node.as(SourceFileSyntax.self) {
35 | return .sourceFileNode(sourceFileNode)
36 | }
37 |
38 | if let classNode = node.as(ClassDeclSyntax.self) {
39 | return .classNode(classNode)
40 | }
41 |
42 | if let structNode = node.as(StructDeclSyntax.self) {
43 | return .structNode(structNode)
44 | }
45 |
46 | if let enumNode = node.as(EnumDeclSyntax.self) {
47 | return .enumNode(enumNode)
48 | }
49 |
50 | if let enumCaseNode = node.as(EnumCaseDeclSyntax.self) {
51 | return .enumCaseNode(enumCaseNode)
52 | }
53 |
54 | if let extensionNode = node.as(ExtensionDeclSyntax.self) {
55 | return .extensionNode(extensionNode)
56 | }
57 |
58 | if let funcNode = node.as(FunctionDeclSyntax.self) {
59 | return .funcNode(funcNode)
60 | }
61 |
62 | if let initialiseNode = node.as(InitializerDeclSyntax.self) {
63 | return .initialiseNode(initialiseNode)
64 | }
65 |
66 | if let closureNode = node.as(ClosureExprSyntax.self) {
67 | return .closureNode(closureNode)
68 | }
69 |
70 | if let codeBlockNode = node.as(CodeBlockSyntax.self), codeBlockNode.parent?.is(IfStmtSyntax.self) == true {
71 | let parent = (codeBlockNode.parent?.as(IfStmtSyntax.self))!
72 | if codeBlockNode == parent.body {
73 | return .ifBlockNode(codeBlockNode, parent)
74 | } else if codeBlockNode == parent.elseBody?.as(CodeBlockSyntax.self) {
75 | return .elseBlockNode(codeBlockNode, parent)
76 | }
77 | return nil
78 | }
79 |
80 | if let guardNode = node.as(GuardStmtSyntax.self) {
81 | return .guardNode(guardNode)
82 | }
83 |
84 | if let forLoopNode = node.as(ForInStmtSyntax.self) {
85 | return .forLoopNode(forLoopNode)
86 | }
87 |
88 | if let whileLoopNode = node.as(WhileStmtSyntax.self) {
89 | return .whileLoopNode(whileLoopNode)
90 | }
91 |
92 | if let subscriptNode = node.as(SubscriptDeclSyntax.self) {
93 | return .subscriptNode(subscriptNode)
94 | }
95 |
96 | if let accessorNode = node.as(AccessorDeclSyntax.self) {
97 | return .accessorNode(accessorNode)
98 | }
99 |
100 | if let codeBlockNode = node.as(CodeBlockSyntax.self),
101 | codeBlockNode.parent?.is(PatternBindingSyntax.self) == true,
102 | codeBlockNode.parent?.parent?.is(PatternBindingListSyntax.self) == true,
103 | codeBlockNode.parent?.parent?.parent?.is(VariableDeclSyntax.self) == true {
104 | return .variableDeclNode(codeBlockNode)
105 | }
106 |
107 | if let switchCaseNode = node.as(SwitchCaseSyntax.self) {
108 | return .switchCaseNode(switchCaseNode)
109 | }
110 |
111 | return nil
112 | }
113 |
114 | public var node: Syntax {
115 | switch self {
116 | case .sourceFileNode(let node): return node._syntaxNode
117 | case .classNode(let node): return node._syntaxNode
118 | case .structNode(let node): return node._syntaxNode
119 | case .enumNode(let node): return node._syntaxNode
120 | case .enumCaseNode(let node): return node._syntaxNode
121 | case .extensionNode(let node): return node._syntaxNode
122 | case .funcNode(let node): return node._syntaxNode
123 | case .initialiseNode(let node): return node._syntaxNode
124 | case .closureNode(let node): return node._syntaxNode
125 | case .ifBlockNode(let node, _): return node._syntaxNode
126 | case .elseBlockNode(let node, _): return node._syntaxNode
127 | case .guardNode(let node): return node._syntaxNode
128 | case .forLoopNode(let node): return node._syntaxNode
129 | case .whileLoopNode(let node): return node._syntaxNode
130 | case .subscriptNode(let node): return node._syntaxNode
131 | case .accessorNode(let node): return node._syntaxNode
132 | case .variableDeclNode(let node): return node._syntaxNode
133 | case .switchCaseNode(let node): return node._syntaxNode
134 | }
135 | }
136 |
137 | public var type: ScopeType {
138 | switch self {
139 | case .sourceFileNode: return .sourceFileNode
140 | case .classNode: return .classNode
141 | case .structNode: return .structNode
142 | case .enumNode: return .enumNode
143 | case .enumCaseNode: return .enumCaseNode
144 | case .extensionNode: return .extensionNode
145 | case .funcNode: return .funcNode
146 | case .initialiseNode: return .initialiseNode
147 | case .closureNode: return .closureNode
148 | case .ifBlockNode, .elseBlockNode: return .ifElseNode
149 | case .guardNode: return .guardNode
150 | case .forLoopNode: return .forLoopNode
151 | case .whileLoopNode: return .whileLoopNode
152 | case .subscriptNode: return .subscriptNode
153 | case .accessorNode: return .accessorNode
154 | case .variableDeclNode: return .variableDeclNode
155 | case .switchCaseNode: return .switchCaseNode
156 | }
157 | }
158 |
159 | public var enclosingScopeNode: ScopeNode? {
160 | return node.enclosingScopeNode
161 | }
162 |
163 | public var description: String {
164 | return "\(node)"
165 | }
166 | }
167 |
168 | public enum ScopeType: Equatable {
169 | case sourceFileNode
170 | case classNode
171 | case structNode
172 | case enumNode
173 | case enumCaseNode
174 | case extensionNode
175 | case funcNode
176 | case initialiseNode
177 | case closureNode
178 | case ifElseNode
179 | case guardNode
180 | case forLoopNode
181 | case whileLoopNode
182 | case subscriptNode
183 | case accessorNode
184 | case variableDeclNode
185 | case switchCaseNode
186 |
187 | public var isTypeDecl: Bool {
188 | return self == .classNode
189 | || self == .structNode
190 | || self == .enumNode
191 | || self == .extensionNode
192 | }
193 |
194 | public var isFunction: Bool {
195 | return self == .funcNode
196 | || self == .initialiseNode
197 | || self == .closureNode
198 | || self == .subscriptNode
199 | }
200 |
201 | }
202 |
203 | open class Scope: Hashable, CustomStringConvertible {
204 | public let scopeNode: ScopeNode
205 | public let parent: Scope?
206 | public private(set) var variables = Stack()
207 | public private(set) var childScopes = [Scope]()
208 | public var type: ScopeType {
209 | return scopeNode.type
210 | }
211 |
212 | public var childFunctions: [Function] {
213 | return childScopes
214 | .compactMap { scope in
215 | if case let .funcNode(funcNode) = scope.scopeNode {
216 | return funcNode
217 | }
218 | return nil
219 | }
220 | }
221 |
222 | public var childTypeDecls: [TypeDecl] {
223 | return childScopes
224 | .compactMap { $0.typeDecl }
225 | }
226 |
227 | public var typeDecl: TypeDecl? {
228 | switch scopeNode {
229 | case .classNode(let node):
230 | return TypeDecl(tokens: [node.identifier], inheritanceTypes: node.inheritanceClause?.inheritedTypeCollection.map { $0 }, scope: self)
231 | case .structNode(let node):
232 | return TypeDecl(tokens: [node.identifier], inheritanceTypes: node.inheritanceClause?.inheritedTypeCollection.map { $0 }, scope: self)
233 | case .enumNode(let node):
234 | return TypeDecl(tokens: [node.identifier], inheritanceTypes: node.inheritanceClause?.inheritedTypeCollection.map { $0 }, scope: self)
235 | case .extensionNode(let node):
236 | return TypeDecl(tokens: node.extendedType.tokens!, inheritanceTypes: node.inheritanceClause?.inheritedTypeCollection.map { $0 }, scope: self)
237 | default:
238 | return nil
239 | }
240 | }
241 |
242 | // Whether a variable can be used before it's declared. This is true for node that defines type, such as class, struct, enum,....
243 | // Otherwise if a variable is inside func, or closure, or normal block (if, guard,..), it must be declared before being used
244 | public var canUseVariableOrFuncInAnyOrder: Bool {
245 | return type == .classNode
246 | || type == .structNode
247 | || type == .enumNode
248 | || type == .extensionNode
249 | || type == .sourceFileNode
250 | }
251 |
252 | public init(scopeNode: ScopeNode, parent: Scope?) {
253 | self.parent = parent
254 | self.scopeNode = scopeNode
255 | parent?.childScopes.append(self)
256 |
257 | if let parent = parent {
258 | assert(scopeNode.node.isDescendent(of: parent.scopeNode.node))
259 | }
260 | }
261 |
262 | func addVariable(_ variable: Variable) {
263 | assert(variable.scope == self)
264 | variables.push(variable)
265 | }
266 |
267 | func getVariable(_ node: IdentifierExprSyntax) -> Variable? {
268 | let name = node.identifier.text
269 | for variable in variables.filter({ $0.name == name }) {
270 | // Special case: guard let `x` = x else { ... }
271 | // or: let x = x.doSmth()
272 | // Here x on the right cannot be resolved to x on the left
273 | if case let .binding(_, valueNode) = variable.raw,
274 | valueNode != nil && node._syntaxNode.isDescendent(of: valueNode!._syntaxNode) {
275 | continue
276 | }
277 |
278 | if variable.raw.token.isBefore(node) {
279 | return variable
280 | } else if !canUseVariableOrFuncInAnyOrder {
281 | // Stop
282 | break
283 | }
284 | }
285 |
286 | return nil
287 | }
288 |
289 | func getFunctionWithSymbol(_ symbol: Symbol) -> [Function] {
290 | return childFunctions.filter { function in
291 | if function.identifier.isBefore(symbol.node) || canUseVariableOrFuncInAnyOrder {
292 | return function.identifier.text == symbol.name
293 | }
294 | return false
295 | }
296 | }
297 |
298 | func getTypeDecl(name: String) -> [TypeDecl] {
299 | return childTypeDecls
300 | .filter { typeDecl in
301 | return typeDecl.name == [name]
302 | }
303 | }
304 |
305 | open var description: String {
306 | return "\(scopeNode)"
307 | }
308 | }
309 |
310 | // MARK: - Hashable
311 | extension Scope {
312 | open func hash(into hasher: inout Hasher) {
313 | scopeNode.hash(into: &hasher)
314 | }
315 |
316 | public static func == (_ lhs: Scope, _ rhs: Scope) -> Bool {
317 | return lhs.scopeNode == rhs.scopeNode
318 | }
319 | }
320 |
321 | extension SyntaxProtocol {
322 | public var enclosingScopeNode: ScopeNode? {
323 | var parent = self.parent
324 | while parent != nil {
325 | if let scopeNode = ScopeNode.from(node: parent!) {
326 | return scopeNode
327 | }
328 | parent = parent?.parent
329 | }
330 | return nil
331 | }
332 | }
333 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/SourceFileScope.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SourceFileScope.swift
3 | // SwiftLeakCheck
4 | //
5 | // Created by Hoang Le Pham on 04/01/2020.
6 | //
7 |
8 | import SwiftSyntax
9 |
10 | public class SourceFileScope: Scope {
11 | let node: SourceFileSyntax
12 | init(node: SourceFileSyntax, parent: Scope?) {
13 | self.node = node
14 | super.init(scopeNode: .sourceFileNode(node), parent: parent)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/Stack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Stack.swift
3 | // LeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 27/10/2019.
9 | //
10 |
11 | public struct Stack {
12 | private var items: [T] = []
13 |
14 | public init() {}
15 |
16 | public init(items: [T]) {
17 | self.items = items
18 | }
19 |
20 | public mutating func push(_ item: T) {
21 | items.append(item)
22 | }
23 |
24 | @discardableResult
25 | public mutating func pop() -> T? {
26 | if !items.isEmpty {
27 | return items.removeLast()
28 | } else {
29 | return nil
30 | }
31 | }
32 |
33 | public mutating func reset() {
34 | items.removeAll()
35 | }
36 |
37 | public func peek() -> T? {
38 | return items.last
39 | }
40 | }
41 |
42 | extension Stack: Collection {
43 | public var startIndex: Int {
44 | return items.startIndex
45 | }
46 |
47 | public var endIndex: Int {
48 | return items.endIndex
49 | }
50 |
51 | public func index(after i: Int) -> Int {
52 | return items.index(after: i)
53 | }
54 |
55 | public subscript(_ index: Int) -> T {
56 | return items[items.count - index - 1]
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/SwiftSyntax+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftSyntax+Extensions.swift
3 | // LeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 27/10/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public extension SyntaxProtocol {
14 | func isBefore(_ node: SyntaxProtocol) -> Bool {
15 | return positionAfterSkippingLeadingTrivia.utf8Offset < node.positionAfterSkippingLeadingTrivia.utf8Offset
16 | }
17 |
18 | func getEnclosingNode(_ type: T.Type) -> T? {
19 | var parent = self.parent
20 | while parent != nil && parent!.is(type) == false {
21 | parent = parent?.parent
22 | if parent == nil { return nil }
23 | }
24 | return parent?.as(type)
25 | }
26 |
27 | func getEnclosingClosureNode() -> ClosureExprSyntax? {
28 | return getEnclosingNode(ClosureExprSyntax.self)
29 | }
30 | }
31 |
32 | extension Syntax {
33 | func isDescendent(of node: Syntax) -> Bool {
34 | return hasAncestor { $0 == node }
35 | }
36 |
37 | // TODO (Le): should we consider self as ancestor of self like this ?
38 | func hasAncestor(_ predicate: (Syntax) -> Bool) -> Bool {
39 | if predicate(self) { return true }
40 | var parent = self.parent
41 | while parent != nil {
42 | if predicate(parent!) {
43 | return true
44 | }
45 | parent = parent?.parent
46 | }
47 | return false
48 | }
49 | }
50 |
51 | public extension ExprSyntax {
52 | /// Returns the enclosing function call to which the current expr is passed as argument. We also return the corresponding
53 | /// argument of the current expr, or nil if current expr is trailing closure
54 | func getEnclosingFunctionCallExpression() -> (function: FunctionCallExprSyntax, argument: FunctionCallArgumentSyntax?)? {
55 | var function: FunctionCallExprSyntax?
56 | var argument: FunctionCallArgumentSyntax?
57 |
58 | if let parent = parent?.as(FunctionCallArgumentSyntax.self) { // Normal function argument
59 | assert(parent.parent?.is(FunctionCallArgumentListSyntax.self) == true)
60 | function = parent.parent?.parent?.as(FunctionCallExprSyntax.self)
61 | argument = parent
62 | } else if let parent = parent?.as(FunctionCallExprSyntax.self),
63 | self.is(ClosureExprSyntax.self),
64 | parent.trailingClosure == self.as(ClosureExprSyntax.self)
65 | { // Trailing closure
66 | function = parent
67 | }
68 |
69 | guard function != nil else {
70 | // Not function call
71 | return nil
72 | }
73 |
74 | return (function: function!, argument: argument)
75 | }
76 |
77 | func isCalledExpr() -> Bool {
78 | if let parentNode = parent?.as(FunctionCallExprSyntax.self) {
79 | if parentNode.calledExpression == self {
80 | return true
81 | }
82 | }
83 |
84 | return false
85 | }
86 |
87 | var rangeInfo: (left: ExprSyntax?, op: TokenSyntax, right: ExprSyntax?)? {
88 | if let expr = self.as(SequenceExprSyntax.self) {
89 | let elements = expr.elements
90 | guard elements.count == 3, let op = elements[1].rangeOperator else {
91 | return nil
92 | }
93 | return (left: elements[elements.startIndex], op: op, right: elements[elements.index(before: elements.endIndex)])
94 | }
95 |
96 | if let expr = self.as(PostfixUnaryExprSyntax.self) {
97 | if expr.operatorToken.isRangeOperator {
98 | return (left: nil, op: expr.operatorToken, right: expr.expression)
99 | } else {
100 | return nil
101 | }
102 | }
103 |
104 | if let expr = self.as(PrefixOperatorExprSyntax.self) {
105 | assert(expr.operatorToken != nil)
106 | if expr.operatorToken!.isRangeOperator {
107 | return (left: expr.postfixExpression, op: expr.operatorToken!, right: nil)
108 | } else {
109 | return nil
110 | }
111 | }
112 |
113 | return nil
114 | }
115 |
116 | private var rangeOperator: TokenSyntax? {
117 | guard let op = self.as(BinaryOperatorExprSyntax.self) else {
118 | return nil
119 | }
120 | return op.operatorToken.isRangeOperator ? op.operatorToken : nil
121 | }
122 | }
123 |
124 | public extension TokenSyntax {
125 | var isRangeOperator: Bool {
126 | return text == "..." || text == "..<"
127 | }
128 | }
129 |
130 | public extension TypeSyntax {
131 | var isOptional: Bool {
132 | return self.is(OptionalTypeSyntax.self) || self.is(ImplicitlyUnwrappedOptionalTypeSyntax.self)
133 | }
134 |
135 | var wrappedType: TypeSyntax {
136 | if let optionalType = self.as(OptionalTypeSyntax.self) {
137 | return optionalType.wrappedType
138 | }
139 | if let implicitOptionalType = self.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
140 | return implicitOptionalType.wrappedType
141 | }
142 | return self
143 | }
144 |
145 | var tokens: [TokenSyntax]? {
146 | if self == wrappedType {
147 | if let type = self.as(MemberTypeIdentifierSyntax.self) {
148 | if let base = type.baseType.tokens {
149 | return base + [type.name]
150 | }
151 | return nil
152 | }
153 | if let type = self.as(SimpleTypeIdentifierSyntax.self) {
154 | return [type.name]
155 | }
156 | return nil
157 | }
158 | return wrappedType.tokens
159 | }
160 |
161 | var name: [String]? {
162 | return tokens?.map { $0.text }
163 | }
164 |
165 | var isClosure: Bool {
166 | return wrappedType.is(FunctionTypeSyntax.self)
167 | || (wrappedType.as(AttributedTypeSyntax.self))?.baseType.isClosure == true
168 | || (wrappedType.as(TupleTypeSyntax.self)).flatMap { $0.elements.count == 1 && $0.elements[$0.elements.startIndex].type.isClosure } == true
169 | }
170 | }
171 |
172 | /// `gurad let a = b, ... `: `let a = b` is a OptionalBindingConditionSyntax
173 | public extension OptionalBindingConditionSyntax {
174 | func isGuardCondition() -> Bool {
175 | return parent?.is(ConditionElementSyntax.self) == true
176 | && parent?.parent?.is(ConditionElementListSyntax.self) == true
177 | && parent?.parent?.parent?.is(GuardStmtSyntax.self) == true
178 | }
179 | }
180 |
181 | public extension FunctionCallExprSyntax {
182 | var base: ExprSyntax? {
183 | return calledExpression.baseAndSymbol?.base
184 | }
185 |
186 | var symbol: TokenSyntax? {
187 | return calledExpression.baseAndSymbol?.symbol
188 | }
189 | }
190 |
191 | // Only used for the FunctionCallExprSyntax extension above
192 | private extension ExprSyntax {
193 | var baseAndSymbol: (base: ExprSyntax?, symbol: TokenSyntax)? {
194 | // base.symbol()
195 | if let memberAccessExpr = self.as(MemberAccessExprSyntax.self) {
196 | return (base: memberAccessExpr.base, symbol: memberAccessExpr.name)
197 | }
198 |
199 | // symbol()
200 | if let identifier = self.as(IdentifierExprSyntax.self) {
201 | return (base: nil, symbol: identifier.identifier)
202 | }
203 |
204 | // expr?.()
205 | if let optionalChainingExpr = self.as(OptionalChainingExprSyntax.self) {
206 | return optionalChainingExpr.expression.baseAndSymbol
207 | }
208 |
209 | // expr()
210 | if let specializeExpr = self.as(SpecializeExprSyntax.self) {
211 | return specializeExpr.expression.baseAndSymbol
212 | }
213 |
214 | assert(false, "Unhandled case")
215 | return nil
216 | }
217 | }
218 |
219 | public extension FunctionParameterSyntax {
220 | var isEscaping: Bool {
221 | guard let attributedType = type?.as(AttributedTypeSyntax.self) else {
222 | return false
223 | }
224 |
225 | return attributedType.attributes?.contains(where: { $0.as(AttributeSyntax.self)?.attributeName.text == "escaping" }) == true
226 | }
227 | }
228 |
229 | /// Convenient
230 | extension ArrayElementListSyntax {
231 | subscript(_ i: Int) -> ArrayElementSyntax {
232 | let index = self.index(startIndex, offsetBy: i)
233 | return self[index]
234 | }
235 | }
236 |
237 | extension FunctionCallArgumentListSyntax {
238 | subscript(_ i: Int) -> FunctionCallArgumentSyntax {
239 | let index = self.index(startIndex, offsetBy: i)
240 | return self[index]
241 | }
242 | }
243 |
244 | extension ExprListSyntax {
245 | subscript(_ i: Int) -> ExprSyntax {
246 | let index = self.index(startIndex, offsetBy: i)
247 | return self[index]
248 | }
249 | }
250 |
251 | extension PatternBindingListSyntax {
252 | subscript(_ i: Int) -> PatternBindingSyntax {
253 | let index = self.index(startIndex, offsetBy: i)
254 | return self[index]
255 | }
256 | }
257 |
258 | extension TupleTypeElementListSyntax {
259 | subscript(_ i: Int) -> TupleTypeElementSyntax {
260 | let index = self.index(startIndex, offsetBy: i)
261 | return self[index]
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/Symbol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Symbol.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 03/01/2020.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public enum Symbol: Hashable {
14 | case token(TokenSyntax)
15 | case identifier(IdentifierExprSyntax)
16 |
17 | var node: Syntax {
18 | switch self {
19 | case .token(let node): return node._syntaxNode
20 | case .identifier(let node): return node._syntaxNode
21 | }
22 | }
23 |
24 | var name: String {
25 | switch self {
26 | case .token(let node): return node.text
27 | case .identifier(let node): return node.identifier.text
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/SyntaxRetrieval.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SyntaxRetrieval.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 09/12/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public enum SyntaxRetrieval {
14 | public static func request(content: String) throws -> SourceFileSyntax {
15 | return try SyntaxParser.parse(
16 | source: content
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/TypeDecl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TypeDecl.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 04/01/2020.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | // Class, struct, enum or extension
14 | public struct TypeDecl: Equatable {
15 | /// The name of the class/struct/enum/extension.
16 | /// For class/struct/enum, it's 1 element
17 | /// For extension, it could be multiple. Eg, extension X.Y.Z {...}
18 | public let tokens: [TokenSyntax]
19 |
20 | public let inheritanceTypes: [InheritedTypeSyntax]?
21 |
22 | // Must be class/struct/enum/extension
23 | public let scope: Scope
24 |
25 | public var name: [String] {
26 | return tokens.map { $0.text }
27 | }
28 |
29 | public var isExtension: Bool {
30 | return scope.type == .extensionNode
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/TypeResolve.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TypeResolve.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 03/01/2020.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public indirect enum TypeResolve: Equatable {
14 | case optional(base: TypeResolve)
15 | case sequence(elementType: TypeResolve)
16 | case dict
17 | case tuple([TypeResolve])
18 | case name([String])
19 | case type(TypeDecl)
20 | case unknown
21 |
22 | public var isOptional: Bool {
23 | return self != self.wrappedType
24 | }
25 |
26 | public var wrappedType: TypeResolve {
27 | switch self {
28 | case .optional(let base):
29 | return base.wrappedType
30 | case .sequence,
31 | .dict,
32 | .tuple,
33 | .name,
34 | .type,
35 | .unknown:
36 | return self
37 | }
38 | }
39 |
40 | public var name: [String]? {
41 | switch self {
42 | case .optional(let base):
43 | return base.name
44 | case .name(let tokens):
45 | return tokens
46 | case .type(let typeDecl):
47 | return typeDecl.name
48 | case .sequence,
49 | .dict,
50 | .tuple,
51 | .unknown:
52 | return nil
53 | }
54 | }
55 |
56 | public var sequenceElementType: TypeResolve {
57 | switch self {
58 | case .optional(let base):
59 | return base.sequenceElementType
60 | case .sequence(let elementType):
61 | return elementType
62 | case .dict,
63 | .tuple,
64 | .name,
65 | .type,
66 | .unknown:
67 | return .unknown
68 | }
69 | }
70 | }
71 |
72 | internal extension TypeResolve {
73 | var isInt: Bool {
74 | return name == ["Int"]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/Utility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utility.swift
3 | // SwiftLeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 05/12/2019.
9 | //
10 |
11 | import Foundation
12 |
13 | extension Collection where Index == Int {
14 | subscript (safe index: Int) -> Element? {
15 | if index < 0 || index >= count {
16 | return nil
17 | }
18 | return self[index]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakCheck/Variable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Variable.swift
3 | // LeakCheck
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 27/10/2019.
9 | //
10 |
11 | import SwiftSyntax
12 |
13 | public enum RawVariable {
14 | case capture(capturedNode: IdentifierExprSyntax)
15 | case param(token: TokenSyntax)
16 | case binding(token: TokenSyntax, valueNode: ExprSyntax?)
17 |
18 | var token: TokenSyntax {
19 | switch self {
20 | case .capture(let capturedNode): return capturedNode.identifier
21 | case .binding(let token, _): return token
22 | case .param(let token): return token
23 | }
24 | }
25 | }
26 |
27 | indirect enum TypeInfo {
28 | case exact(TypeSyntax)
29 | case inferedFromExpr(ExprSyntax)
30 | case inferedFromSequence(ExprSyntax)
31 | case inferedFromTuple(tupleType: TypeInfo, index: Int)
32 | case inferedFromClosure(ClosureExprSyntax, paramIndex: Int, paramCount: Int)
33 | }
34 |
35 | // Represent a variable declaration. Eg
36 | // var a = 1
37 | // let b = c // b is the Variable, c is not (c is a reference)
38 | // block { [unowned x] in // x is a Variable
39 | // func doSmth(a: Int, b: String) // a, b are Variables
40 | public class Variable: Hashable, CustomStringConvertible {
41 | public let raw: RawVariable
42 | public var name: String { return raw.token.text }
43 | let typeInfo: TypeInfo
44 | public let memoryAttribute: MemoryAttribute?
45 | public let scope: Scope
46 |
47 | var valueNode: ExprSyntax? {
48 | switch raw {
49 | case .binding(_, let valueNode): return valueNode
50 | case .param, .capture: return nil
51 | }
52 | }
53 |
54 | var capturedNode: IdentifierExprSyntax? {
55 | switch raw {
56 | case .capture(let capturedNode): return capturedNode
57 | case .binding, .param: return nil
58 | }
59 | }
60 |
61 | public var isStrong: Bool {
62 | return memoryAttribute?.isStrong ?? true
63 | }
64 |
65 | public var description: String {
66 | return "\(raw)"
67 | }
68 |
69 | private init(raw: RawVariable,
70 | typeInfo: TypeInfo,
71 | scope: Scope,
72 | memoryAttribute: MemoryAttribute? = nil) {
73 | self.raw = raw
74 | self.typeInfo = typeInfo
75 | self.scope = scope
76 | self.memoryAttribute = memoryAttribute
77 | }
78 |
79 | public static func from(_ node: ClosureCaptureItemSyntax, scope: Scope) -> Variable? {
80 | assert(scope.scopeNode == node.enclosingScopeNode)
81 |
82 | guard let identifierExpr = node.expression.as(IdentifierExprSyntax.self) else {
83 | // There're cases such as { [loggedInState.services] in ... }, which probably we don't need to care about
84 | return nil
85 | }
86 |
87 | let memoryAttribute: MemoryAttribute? = {
88 | guard let specifier = node.specifier?.first else {
89 | return nil
90 | }
91 |
92 | assert(node.specifier!.count <= 1, "Unhandled case")
93 |
94 | guard let memoryAttribute = MemoryAttribute.from(specifier.text) else {
95 | fatalError("Unhandled specifier \(specifier.text)")
96 | }
97 | return memoryAttribute
98 | }()
99 |
100 | return Variable(
101 | raw: .capture(capturedNode: identifierExpr),
102 | typeInfo: .inferedFromExpr(ExprSyntax(identifierExpr)),
103 | scope: scope,
104 | memoryAttribute: memoryAttribute
105 | )
106 | }
107 |
108 | public static func from(_ node: ClosureParamSyntax, scope: Scope) -> Variable {
109 | guard let closure = node.getEnclosingClosureNode() else {
110 | fatalError()
111 | }
112 | assert(scope.scopeNode == .closureNode(closure))
113 |
114 | return Variable(
115 | raw: .param(token: node.name),
116 | typeInfo: .inferedFromClosure(closure, paramIndex: node.indexInParent, paramCount: node.parent!.children.count),
117 | scope: scope
118 | )
119 | }
120 |
121 | public static func from(_ node: FunctionParameterSyntax, scope: Scope) -> Variable {
122 | assert(node.enclosingScopeNode == scope.scopeNode)
123 |
124 | guard let token = node.secondName ?? node.firstName else {
125 | fatalError()
126 | }
127 |
128 | assert(token.tokenKind != .wildcardKeyword, "Unhandled case")
129 | assert(node.attributes == nil, "Unhandled case")
130 |
131 | guard let type = node.type else {
132 | // Type is omited, must be used in closure signature
133 | guard case let .closureNode(closureNode) = scope.scopeNode else {
134 | fatalError("Only closure can omit the param type")
135 | }
136 | return Variable(
137 | raw: .param(token: token),
138 | typeInfo: .inferedFromClosure(closureNode, paramIndex: node.indexInParent, paramCount: node.parent!.children.count),
139 | scope: scope
140 | )
141 | }
142 |
143 | return Variable(raw: .param(token: token), typeInfo: .exact(type), scope: scope)
144 | }
145 |
146 | public static func from(_ node: PatternBindingSyntax, scope: Scope) -> [Variable] {
147 | guard let parent = node.parent?.as(PatternBindingListSyntax.self) else {
148 | fatalError()
149 | }
150 |
151 | assert(parent.parent?.is(VariableDeclSyntax.self) == true, "Unhandled case")
152 |
153 | func _typeFromNode(_ node: PatternBindingSyntax) -> TypeInfo {
154 | // var a: Int
155 | if let typeAnnotation = node.typeAnnotation {
156 | return .exact(typeAnnotation.type)
157 | }
158 | // var a = value
159 | if let value = node.initializer?.value {
160 | return .inferedFromExpr(value)
161 | }
162 | // var a, b, .... = value
163 | let indexOfNextNode = node.indexInParent + 1
164 | return _typeFromNode(parent[indexOfNextNode])
165 | }
166 |
167 | let type = _typeFromNode(node)
168 |
169 | if let identifier = node.pattern.as(IdentifierPatternSyntax.self) {
170 | let memoryAttribute: MemoryAttribute? = {
171 | if let modifier = node.parent?.parent?.as(VariableDeclSyntax.self)!.modifiers?.first {
172 | return MemoryAttribute.from(modifier.name.text)
173 | }
174 | return nil
175 | }()
176 |
177 | return [
178 | Variable(
179 | raw: .binding(token: identifier.identifier, valueNode: node.initializer?.value),
180 | typeInfo: type,
181 | scope: scope,
182 | memoryAttribute: memoryAttribute
183 | )
184 | ]
185 | }
186 |
187 | if let tuple = node.pattern.as(TuplePatternSyntax.self) {
188 | return extractVariablesFromTuple(tuple, tupleType: type, tupleValue: node.initializer?.value, scope: scope)
189 | }
190 |
191 | return []
192 | }
193 |
194 | public static func from(_ node: OptionalBindingConditionSyntax, scope: Scope) -> Variable? {
195 | if let left = node.pattern.as(IdentifierPatternSyntax.self) {
196 | let right = node.initializer.value
197 | let type: TypeInfo
198 | if let typeAnnotation = node.typeAnnotation {
199 | type = .exact(typeAnnotation.type)
200 | } else {
201 | type = .inferedFromExpr(right)
202 | }
203 |
204 | return Variable(
205 | raw: .binding(token: left.identifier, valueNode: right),
206 | typeInfo: type,
207 | scope: scope,
208 | memoryAttribute: .strong
209 | )
210 | }
211 |
212 | return nil
213 | }
214 |
215 | public static func from(_ node: ForInStmtSyntax, scope: Scope) -> [Variable] {
216 | func _variablesFromPattern(_ pattern: PatternSyntax) -> [Variable] {
217 | if let identifierPattern = pattern.as(IdentifierPatternSyntax.self) {
218 | return [
219 | Variable(
220 | raw: .binding(token: identifierPattern.identifier, valueNode: nil),
221 | typeInfo: .inferedFromSequence(node.sequenceExpr),
222 | scope: scope
223 | )
224 | ]
225 | }
226 |
227 | if let tuplePattern = pattern.as(TuplePatternSyntax.self) {
228 | return extractVariablesFromTuple(
229 | tuplePattern,
230 | tupleType: .inferedFromSequence(node.sequenceExpr),
231 | tupleValue: nil,
232 | scope: scope
233 | )
234 | }
235 |
236 | if pattern.is(WildcardPatternSyntax.self) {
237 | return []
238 | }
239 |
240 | if let valueBindingPattern = pattern.as(ValueBindingPatternSyntax.self) {
241 | return _variablesFromPattern(valueBindingPattern.valuePattern)
242 | }
243 |
244 | assert(false, "Unhandled pattern in for statement: \(pattern)")
245 | return []
246 | }
247 |
248 | return _variablesFromPattern(node.pattern)
249 | }
250 |
251 | private static func extractVariablesFromTuple(_ tuplePattern: TuplePatternSyntax,
252 | tupleType: TypeInfo,
253 | tupleValue: ExprSyntax?,
254 | scope: Scope) -> [Variable] {
255 | return tuplePattern.elements.enumerated().flatMap { (index, element) -> [Variable] in
256 |
257 | let elementType: TypeInfo = .inferedFromTuple(tupleType: tupleType, index: index)
258 | let elementValue: ExprSyntax? = {
259 | if let tupleValue = tupleValue?.as(TupleExprSyntax.self) {
260 | return tupleValue.elementList[index].expression
261 | }
262 | return nil
263 | }()
264 |
265 | if let identifierPattern = element.pattern.as(IdentifierPatternSyntax.self) {
266 | return [
267 | Variable(
268 | raw: .binding(token: identifierPattern.identifier, valueNode: elementValue),
269 | typeInfo: elementType,
270 | scope: scope
271 | )
272 | ]
273 | }
274 |
275 | if let childTuplePattern = element.pattern.as(TuplePatternSyntax.self) {
276 | return extractVariablesFromTuple(
277 | childTuplePattern,
278 | tupleType: elementType,
279 | tupleValue: elementValue,
280 | scope: scope
281 | )
282 | }
283 |
284 | if element.pattern.is(WildcardPatternSyntax.self) {
285 | return []
286 | }
287 |
288 | assertionFailure("I don't think there's any other kind")
289 | return []
290 | }
291 | }
292 | }
293 |
294 | // MARK: - Hashable
295 | public extension Variable {
296 | static func == (_ lhs: Variable, _ rhs: Variable) -> Bool {
297 | return lhs.raw.token == rhs.raw.token
298 | }
299 |
300 | func hash(into hasher: inout Hasher) {
301 | hasher.combine(raw.token)
302 | }
303 | }
304 |
305 | public enum MemoryAttribute: Hashable {
306 | case weak
307 | case unowned
308 | case strong
309 |
310 | public var isStrong: Bool {
311 | switch self {
312 | case .weak,
313 | .unowned:
314 | return false
315 | case .strong:
316 | return true
317 | }
318 | }
319 |
320 | public static func from(_ text: String) -> MemoryAttribute? {
321 | switch text {
322 | case "weak":
323 | return .weak
324 | case "unowned":
325 | return .unowned
326 | default:
327 | return nil
328 | }
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/Sources/SwiftLeakChecker/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
3 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
4 | //
5 |
6 | import Foundation
7 | import SwiftLeakCheck
8 |
9 | enum CommandLineError: Error, LocalizedError {
10 | case missingFileName
11 |
12 | var errorDescription: String? {
13 | switch self {
14 | case .missingFileName:
15 | return "Missing file or directory name"
16 | }
17 | }
18 | }
19 |
20 | do {
21 | let arguments = CommandLine.arguments
22 | guard arguments.count > 1 else {
23 | throw CommandLineError.missingFileName
24 | }
25 |
26 | let path = arguments[1]
27 | let url = URL(fileURLWithPath: path)
28 | let dirScanner = DirectoryScanner(callback: { fileUrl, shouldStop in
29 | do {
30 | guard fileUrl.pathExtension == "swift" else {
31 | return
32 | }
33 |
34 | print("Scan \(fileUrl)")
35 |
36 | let leakDetector = GraphLeakDetector()
37 | leakDetector.nonEscapeRules = [
38 | UIViewAnimationRule(),
39 | UIViewControllerAnimationRule(),
40 | DispatchQueueRule()
41 | ] + CollectionRules.rules
42 |
43 | let startDate = Date()
44 | let leaks = try leakDetector.detect(fileUrl)
45 | let endDate = Date()
46 |
47 | print("Finished in \(endDate.timeIntervalSince(startDate)) seconds")
48 |
49 | leaks.forEach { leak in
50 | print(leak.description)
51 | }
52 | } catch {}
53 | })
54 |
55 | dirScanner.scan(url: url)
56 |
57 | } catch {
58 | print("\(error.localizedDescription)")
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/CYaml_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/Clang_C_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/GeneratedModuleMap/_CSwiftSyntax/module.modulemap:
--------------------------------------------------------------------------------
1 | module _CSwiftSyntax {
2 | umbrella "/Users/hoang.le/Projects/SwiftLeakCheck/.build/checkouts/swift-syntax/Sources/_CSwiftSyntax/include"
3 | export *
4 | }
5 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/SWXMLHash_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/SourceKit_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/SourceKittenFramework_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/SwiftLeakCheckTests_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | BNDL
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/SwiftLeakCheck_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/SwiftSyntax_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/Yams_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/_CSwiftSyntax_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundleDevelopmentRegion
5 | en
6 | CFBundleExecutable
7 | $(EXECUTABLE_NAME)
8 | CFBundleIdentifier
9 | $(PRODUCT_BUNDLE_IDENTIFIER)
10 | CFBundleInfoDictionaryVersion
11 | 6.0
12 | CFBundleName
13 | $(PRODUCT_NAME)
14 | CFBundlePackageType
15 | FMWK
16 | CFBundleShortVersionString
17 | 1.0
18 | CFBundleSignature
19 | ????
20 | CFBundleVersion
21 | $(CURRENT_PROJECT_VERSION)
22 | NSPrincipalClass
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/xcshareddata/xcschemes/SwiftLeakCheck-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
67 |
68 |
74 |
75 |
77 |
78 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/SwiftLeakCheck.xcodeproj/xcshareddata/xcschemes/SwiftLeakChecker.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/LeakDetectorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LeakDetectorTests.swift
3 | // SwiftLeakCheckTests
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 27/10/2019.
9 | //
10 |
11 | import XCTest
12 | import SwiftLeakCheck
13 |
14 | final class LeakDetectorTests: XCTestCase {
15 | func testLeak1() {
16 | verify(fileName: "Leak1")
17 | }
18 |
19 | func testLeak2() {
20 | verify(fileName: "Leak2")
21 | }
22 |
23 | func testNestedClosure() {
24 | verify(fileName: "NestedClosure")
25 | }
26 |
27 | func testNonEscapingClosure() {
28 | verify(fileName: "NonEscapingClosure")
29 | }
30 |
31 | func testUIViewAnimation() {
32 | verify(fileName: "UIViewAnimation")
33 | }
34 |
35 | func testUIViewControllerAnimation() {
36 | verify(fileName: "UIViewControllerAnimation")
37 | }
38 |
39 | func testEscapingAttribute() {
40 | verify(fileName: "EscapingAttribute")
41 | }
42 |
43 | func testIfElse() {
44 | verify(fileName: "IfElse")
45 | }
46 |
47 | func testFuncResolve() {
48 | verify(fileName: "FuncResolve")
49 | }
50 |
51 | func testTypeInfer() {
52 | verify(fileName: "TypeInfer")
53 | }
54 |
55 | func testTypeResolve() {
56 | verify(fileName: "TypeResolve")
57 | }
58 |
59 | func testDispatchQueue() {
60 | verify(fileName: "DispatchQueue")
61 | }
62 |
63 | func testExtensions() {
64 | verify(fileName: "Extensions")
65 | }
66 |
67 | private func verify(fileName: String, extension: String? = nil) {
68 | do {
69 | guard let url = bundle.url(forResource: fileName, withExtension: `extension`) else {
70 | XCTFail("File \(fileName + (`extension`.flatMap { ".\($0)" } ?? "")) doesn't exist")
71 | return
72 | }
73 |
74 | let content = try String(contentsOf: url)
75 | verify(content: content)
76 | } catch {
77 | XCTFail(error.localizedDescription)
78 | }
79 | }
80 |
81 | private func verify(content: String) {
82 | let lines = content.components(separatedBy: "\n")
83 | let expectedLeakAtLines = lines.enumerated().compactMap { (lineNumber, line) -> Int? in
84 | if line.trimmingCharacters(in: .whitespacesAndNewlines).hasSuffix("// Leak") {
85 | return lineNumber + 1
86 | }
87 | return nil
88 | }
89 |
90 | do {
91 | let leakDetector = GraphLeakDetector()
92 | leakDetector.nonEscapeRules = [
93 | UIViewAnimationRule(),
94 | UIViewControllerAnimationRule(),
95 | DispatchQueueRule()
96 | ] + CollectionRules.rules
97 |
98 | let leaks = try leakDetector.detect(content: content)
99 | let leakAtLines = leaks.map { $0.line }
100 | let leakAtLinesUnique = NSOrderedSet(array: leakAtLines).array as! [Int]
101 | XCTAssertEqual(leakAtLinesUnique, expectedLeakAtLines)
102 | } catch {
103 | XCTFail(error.localizedDescription)
104 | }
105 | }
106 |
107 | private lazy var bundle: Bundle = {
108 | return Bundle(for: type(of: self))
109 | }()
110 | }
111 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/DispatchQueue:
--------------------------------------------------------------------------------
1 | class X {
2 |
3 | private let queue: DispatchQueue!
4 |
5 | func main() {
6 | DispatchQueue.main.async {
7 | self.doSmth()
8 | }
9 |
10 | DispatchQueue.main.async(execute: {
11 | self.doSmth()
12 | })
13 |
14 | DispatchQueue.main.sync {
15 | self.doSmth()
16 | }
17 |
18 | DispatchQueue.main.sync(execute: {
19 | self.doSmth()
20 | })
21 |
22 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
23 | self.doSmth()
24 | }
25 |
26 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
27 | self.doSmth()
28 | })
29 |
30 | DispatchQueue.global().async {
31 | self.doSmth()
32 | }
33 |
34 | DispatchQueue.global().async(execute: {
35 | self.doSmth()
36 | })
37 |
38 | DispatchQueue.global(qos: .background).sync {
39 | self.doSmth()
40 | }
41 |
42 | queue.async {
43 | self.doSmth()
44 | }
45 | }
46 |
47 | func doSmth() {}
48 | }
49 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/EscapingAttribute:
--------------------------------------------------------------------------------
1 | class X {
2 | func main() {
3 | doSmth1 {
4 | self.x // Leak
5 | }
6 |
7 | doSmth2 {
8 | self.x // Leak
9 | }
10 |
11 | doSmth3 {
12 | self.x
13 | }
14 | }
15 |
16 | func doSmth1(block: @escaping () -> Void) {
17 | someObject.callBlock1(block)
18 | }
19 |
20 | func doSmth2(block: (() -> Void)?) {
21 | someObject.callBlock2(block)
22 | }
23 |
24 | func doSmth3(block: () -> Void) {
25 | someObject.callBlock3(block)
26 | }
27 |
28 | var x = 1
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/Extensions:
--------------------------------------------------------------------------------
1 | class A {
2 | class B {
3 | var x = 1
4 | func main() {
5 | // This should be resolved to the `func doSmth` defined in extension A.B
6 | doSmth {
7 | self.x
8 | }
9 | }
10 | }
11 |
12 | func doSmth(block: @escaping () -> Void) {
13 | someObject.callBlock(block)
14 | }
15 | }
16 |
17 | extension A.B {
18 | func doSmth(block: () -> Void) {
19 | block()
20 | }
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/FuncResolve:
--------------------------------------------------------------------------------
1 | class X {
2 | func main() {
3 | doSmth {
4 | self.x
5 | }
6 | }
7 |
8 | func doSmth(block: @escaping () -> Void) {
9 | callBlock1(block)
10 | callBlock2(block: block)
11 | }
12 |
13 | func callBlock1(_ block: @escaping () -> Void) {
14 | UIView.animate(withDuration: 1.0, animations: block)
15 | }
16 |
17 | func callBlock2(block b: @escaping () -> Void) {
18 | UIView.animate(withDuration: 1.0, animations: b)
19 | }
20 |
21 | var x = 1
22 | }
23 |
24 | class Y {
25 | let x = 1
26 | func main() {
27 | _ = getArray()
28 | .filter { num in return self.x < num }
29 | .map { num in self.x + num }
30 | .sorted { (a, b) in a + self.x > b }
31 | .flatMap { _ in self.getArray() }
32 | .customMapping { _ in self.x + 1 }
33 | .map { _ in self.x + 1 }
34 | }
35 |
36 | func getArray() -> [Int] {
37 | return [1, 2, 3]
38 | }
39 | }
40 |
41 | extension Array {
42 | func customMapping(_ block: (Element) -> T) -> [Element] {
43 | return map(block)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/IfElse:
--------------------------------------------------------------------------------
1 | class X {
2 | func ifElse() {
3 | Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
4 | if someCondition {
5 | guard let self = self else { return }
6 | self.doSmth()
7 | } else {
8 | block {
9 | self?.doSmth()
10 | }
11 | }
12 | }
13 | }
14 |
15 | func ifElseIf() {
16 | Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
17 | if someCondition {
18 | guard let self = self else { return }
19 | self.doSmth()
20 | } else if someOtherCondition {
21 | block {
22 | self?.doSmth()
23 | }
24 | }
25 | }
26 | }
27 |
28 | func doSmth() {}
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/Leak1:
--------------------------------------------------------------------------------
1 | class AClass {
2 | var x = 1
3 |
4 | func main() {
5 |
6 | let block = { [weak self] in
7 | guard let strongSelf = self else { return }
8 | strongSelf.x = strongSelf.x + 1
9 | weak var strongSelf2 = strongSelf
10 | someObject.doSmth {
11 | guard let `self` = self else { return }
12 | self.x = 1
13 | guard let strongSelf2 = strongSelf2 else { return }
14 | strongSelf2.x = 1
15 | let strongSelf3 = strongSelf // Leak
16 | strongSelf3.x = 1
17 | strongSelf.x = 1// Leak
18 | let _ = strongSelf // Leak
19 | }
20 | }
21 |
22 | // So this `block` will be evaluated as escaping
23 | someObject.doSmth(block)
24 |
25 | doSmth(closure: {
26 | self.x // Leak
27 | }, output: self)
28 |
29 | doSmth { [unowned self] in
30 | self.x
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/Leak2:
--------------------------------------------------------------------------------
1 | class X {
2 |
3 | var x = 1
4 |
5 | func main() {
6 | doSmth(weak: self, onNext: { strongSelf in
7 | self.x // Leak
8 |
9 | strongSelf.x
10 |
11 | strongSelf.x = self.x + 1 // Leak
12 |
13 | strongSelf.doSmth { [weak self] in // Leak
14 | guard let `self` = self else { return }
15 | self.x
16 | }
17 | })
18 |
19 | doSmth(weak: self, onNext: { strongSelf in
20 | switch self.status { // Leak
21 | case .ready:
22 | self.doSmth() // Leak
23 | default: break
24 | }
25 | })
26 | }
27 |
28 | func doSmth(weak myself: X, onNext: @escaping (X) -> Void) {
29 | someObject.block { [weak mySelf] in
30 | guard let strongSelf = mySelf else { return }
31 | onNext(strongSelf)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/NestedClosure:
--------------------------------------------------------------------------------
1 | class X {
2 | let x = 1
3 | func nonEscapingClosure() {
4 | let block: ([Int], B) -> Void = { a, b in
5 | a.map { val in
6 | self.x
7 | }
8 | b.map {
9 | // It captures `self` from outer closure `block`, but `block` is non-escaping
10 | self.x
11 | }
12 | }
13 |
14 | block()
15 |
16 | _ = { [weak self] (a: [Int], b: B) in
17 | guard let strongSelf = self else { return }
18 | a.map {
19 | self?.x
20 | strongSelf.x
21 | }
22 | b.map {
23 | self?.x
24 | strongSelf.x // Leak
25 | }
26 | }
27 | }
28 |
29 | func escapingClosure() {
30 | doSmth { (a: [Int], b: B) in
31 | a.map { val in
32 | self.x // Leak
33 | }
34 |
35 | b.map { [weak self] _ in // Leak
36 | guard let `self` = self else { return }
37 | self.x
38 | }
39 | }
40 |
41 | doSmth { [weak self] (a: [Int], b: B) in
42 | guard let strongSelf = self else { return }
43 | a.map { val in
44 | self?.x
45 | strongSelf.x
46 | }
47 |
48 | b.map {
49 | self?.x
50 | strongSelf.x // Leak
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/NonEscapingClosure:
--------------------------------------------------------------------------------
1 | class X {
2 | func nonEscapeClosure() {
3 | let block = {
4 | self.doSmth()
5 | }
6 | block()
7 | }
8 |
9 | func anonymousClosure() {
10 | _ = {
11 | self.doSmth()
12 | }()
13 | }
14 |
15 | func doSmth() {}
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/TypeInfer:
--------------------------------------------------------------------------------
1 | class X {
2 | let x: Int = 1
3 |
4 | func typeInferFromClosureParams() {
5 | let block: ([Int], String?, C) -> Void = { [weak self] a, b, c in
6 | guard let strongSelf = self else { return }
7 | a.map { val in
8 | strongSelf.x
9 | }
10 | b.map {
11 | strongSelf.x
12 | }
13 | c.map {
14 | strongSelf.x // Leak
15 | }
16 | }
17 | }
18 |
19 | func typeInferFromTupleClosure() {
20 | let (a, b) = (
21 | closure1: {
22 | self.x
23 | },
24 | closure2: {
25 | self.x // Leak
26 | }
27 | )
28 |
29 | a()
30 |
31 | b()
32 | doSmthWithCallback(b)
33 | }
34 |
35 | func typeInferFromTuple() {
36 | var (a, b) = ([1, 2], [1: "x", 2: "y"])
37 | a.sort(by: { (val1, val2) in
38 | return val1 + self.x > val2
39 | })
40 |
41 | b.filter { (key, val) in
42 | return key > self.x
43 | }
44 | }
45 |
46 | func explicitType() {
47 | var a: [Int]!
48 | a.map {
49 | self.x
50 | }
51 | }
52 |
53 | func range() {
54 | _ = (0...10).filter { val in
55 | val > self.x
56 | }
57 |
58 | _ = (0...).filter { val in
59 | val > self.x
60 | }
61 |
62 | _ = (0..<10).filter { val in
63 | val > self.x
64 | }
65 |
66 | var a: [Int]!
67 | a[..<5].filter { val in
68 | val > self.x
69 | }
70 | }
71 |
72 | func nestedArray() {
73 | var arr: [[Int]]!
74 | for a in arr {
75 | a.map { val in
76 | self.x + val
77 | }
78 | }
79 | arr.map { a in
80 | a.map { val in
81 | self.x + val
82 | }
83 | }
84 |
85 | var arr2 = [[1,2], [3, 4]]
86 | for a in arr2 {
87 | a.map { val in
88 | self.x + val
89 | }
90 | }
91 | arr2.map { a in
92 | a.map { val in
93 | self.x + val
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/TypeResolve:
--------------------------------------------------------------------------------
1 | class AClass {
2 | func main() {
3 | let nonEscapeBlock = {
4 | self.x += 1
5 | }
6 | let b = BClass()
7 | b.c(nonEscapeBlock)
8 | b.doNonEscapeBlock(nonEscapeBlock)
9 | b.doAnotherNonEscapeBlock(nonEscapeBlock)
10 |
11 | let escapeBlock = {
12 | // self.x += 1 // FIXME: Leak
13 | }
14 |
15 | b.doSmth(escapeBlock)
16 | }
17 |
18 | var x = 1
19 | var y = [1, 2, 3]
20 | }
21 |
22 | class BClass: CClass {
23 | var completion: (() -> Void)?
24 |
25 | func doSmth(_ completion: @escaping () -> Void) {
26 | self.completion = completion
27 | }
28 |
29 | func doNonEscapeBlock(_ block: () -> Void) {
30 | block()
31 | }
32 |
33 | func doAnotherNonEscapeBlock(_ block: () -> Void) {
34 | _ = AClass().y.map { val in
35 | block()
36 | return val
37 | }
38 | }
39 | }
40 |
41 | class CClass {
42 | func c(_ completion: @escaping () -> Void) {
43 | self.completion = completion
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/UIViewAnimation:
--------------------------------------------------------------------------------
1 | class X {
2 | private func main() {
3 | animateSmth {
4 | self.doSmth()
5 | }
6 |
7 | UIView.animate(withDuration: voiceDuration) {
8 | self.doSmth()
9 | }
10 |
11 | UIView.animate(withDuration: 0.3, animations: {
12 | self.doSmth()
13 | }) { _ in
14 | self.doSmth()
15 | }
16 |
17 | UIView.animate(withDuration: 0.3, animations: {
18 | self.doSmth()
19 | }, completion: { _ in
20 | self.doSmth()
21 | })
22 |
23 | UIView.animate(withDuration: Double(duration), delay: 0, options: .curveEaseInOut, animations: {
24 | self.doSmth()
25 | })
26 |
27 | UIView.animate(withDuration: Double(duration), delay: 0, options: .curveEaseInOut, animations: {
28 | self.doSmth()
29 | }) { _ in
30 | self.doSmth()
31 | }
32 |
33 | UIView.animate(withDuration: Double(duration), delay: 0, options: .curveEaseInOut, animations: {
34 | self.doSmth()
35 | }, completion: { _ in
36 | self.doSmth()
37 | })
38 |
39 | UIView.transition(from: view1, to: view2, duration: 1.0, options: []) { _ in
40 | self.doSmth()
41 | }
42 |
43 | animateIfNeeded(animated: animated, updateBlock: {
44 | self.doSmth()
45 | })
46 |
47 | stopActionAnimation {
48 | UIView.animate(withDuration: 0.2, delay: 0, options: [.transitionCrossDissolve, .beginFromCurrentState], animations: {
49 | self.doSmth()
50 | }) { (_) in
51 | self.doSmth()
52 | }
53 | }
54 | }
55 |
56 | private func animateSmth(_ completion: @escaping () -> Void) {
57 | UIView.animate(withDuration: 0.5,
58 | delay: 0,
59 | usingSpringWithDamping: 0.7,
60 | initialSpringVelocity: 0.5,
61 | options: [.curveEaseInOut],
62 | animations: { self.doSmth() },
63 | completion: { _ in completion() })
64 | }
65 |
66 | private func stopActionAnimation(_ completion: (() -> Void)? = nil) {
67 | dismissActionAnimation(completion)
68 | }
69 |
70 | private func dismissActionAnimation(_ completion: (() -> Void)? = nil) {
71 | UIView.animate(withDuration: actionAnimationOut / 2, delay: actionDelayOut, options: .beginFromCurrentState, animations: {
72 | self.doSmth()
73 | }) { (_) in
74 | self.doSmth()
75 | completion?()
76 | }
77 | }
78 |
79 | private func doSmth() {}
80 | }
81 |
82 | extension X {
83 | private func animateIfNeeded(animated: Bool, updateBlock: @escaping () -> Void) {
84 | if animated {
85 | UIView.transition(with: view1, duration: 1.0, options: []) {
86 | updateBlock()
87 | self.doSmth()
88 | }
89 | } else {
90 | updateBlock()
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/Snippets/UIViewControllerAnimation:
--------------------------------------------------------------------------------
1 | class SomeController: UIViewController {
2 | func main() {
3 | let vc = UIViewController(nibName: nil, bundle: nil)
4 |
5 | present(vc, animated: true, completion: {
6 | self.doSmth()
7 | })
8 |
9 | weak var weakSelf = self
10 | weakSelf?.present(vc, animated: true) {
11 | self.doSmth()
12 | }
13 |
14 | presentSmth {
15 | self.doSmth()
16 | }
17 | }
18 |
19 | func presentSmth(completion: @escaping () -> Void) {
20 | let vc = UIViewController(nibName: nil, bundle: nil)
21 | present(vc, animated: true, completion: {
22 | completion()
23 | })
24 | }
25 |
26 | func doSmth() {}
27 | }
28 |
29 | extension SomeController {
30 | @objc fileprivate func dismissSmth() {
31 | self.dismiss(animated: true) {
32 | self.doSmth()
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/SwiftLeakCheckTests/StackTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StackTests.swift
3 | // SwiftLeakCheckTests
4 | //
5 | // Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
6 | // Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
7 | //
8 | // Created by Hoang Le Pham on 27/10/2019.
9 | //
10 |
11 | import XCTest
12 | @testable import SwiftLeakCheck
13 |
14 | final class StackTests: XCTestCase {
15 | func testEnumerationOrder() {
16 | var stack = Stack()
17 | stack.push(5)
18 | stack.push(4)
19 | stack.push(3)
20 | stack.push(2)
21 | stack.push(1)
22 |
23 | let a = [1, 2, 3, 4, 5]
24 |
25 | // Map
26 | XCTAssertEqual(stack.map { $0 }, a)
27 |
28 | // Loop
29 | var arr1 = [Int]()
30 | stack.forEach { arr1.append($0) }
31 | XCTAssertEqual(arr1, a)
32 |
33 | var arr2 = [Int]()
34 | for num in stack {
35 | arr2.append(num)
36 | }
37 | XCTAssertEqual(arr2, a)
38 | }
39 |
40 | func testPushPopPeek() {
41 | var stack = Stack()
42 | stack.push(5)
43 | XCTAssertEqual(stack.peek(), 5)
44 | stack.push(4)
45 | XCTAssertEqual(stack.pop(), 4)
46 | XCTAssertEqual(stack.peek(), 5)
47 | stack.push(3)
48 | stack.push(2)
49 | XCTAssertEqual(stack.pop(), 2)
50 | stack.push(1)
51 |
52 | XCTAssertEqual(stack.map { $0 }, [1, 3, 5])
53 | }
54 |
55 | func testPopEmpty() {
56 | var stack = Stack()
57 | stack.push(1)
58 | XCTAssertEqual(stack.pop(), 1)
59 | XCTAssertEqual(stack.pop(), nil)
60 | }
61 |
62 | func testReset() {
63 | var stack = Stack()
64 | stack.push(5)
65 | stack.push(4)
66 | stack.reset()
67 |
68 | XCTAssertEqual(stack.map { $0 }, [])
69 | }
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/images/leakcheck_sample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grab/swift-leak-check/a65212337318eeb1ff0a4a0262b4133b871592ac/images/leakcheck_sample.png
--------------------------------------------------------------------------------
/images/leakcheck_sample_xcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grab/swift-leak-check/a65212337318eeb1ff0a4a0262b4133b871592ac/images/leakcheck_sample_xcode.png
--------------------------------------------------------------------------------