├── .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 --------------------------------------------------------------------------------