{"id":20294185,"url":"https://github.com/grab/swift-leak-check","last_synced_at":"2025-04-11T11:42:32.686Z","repository":{"id":54585559,"uuid":"234489876","full_name":"grab/swift-leak-check","owner":"grab","description":null,"archived":false,"fork":false,"pushed_at":"2021-02-10T09:58:11.000Z","size":2224,"stargazers_count":135,"open_issues_count":3,"forks_count":10,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-03-25T08:02:59.157Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/grab.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-01-17T06:55:19.000Z","updated_at":"2025-03-04T14:15:53.000Z","dependencies_parsed_at":"2022-08-13T20:31:24.617Z","dependency_job_id":null,"html_url":"https://github.com/grab/swift-leak-check","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grab%2Fswift-leak-check","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grab%2Fswift-leak-check/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grab%2Fswift-leak-check/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grab%2Fswift-leak-check/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/grab","download_url":"https://codeload.github.com/grab/swift-leak-check/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248385776,"owners_count":21094939,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T15:28:10.191Z","updated_at":"2025-04-11T11:42:32.663Z","avatar_url":"https://github.com/grab.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Swift Leak Checker\n\nA framework, a command-line tool that can detect potential memory leak caused by strongly captured `self` in `escaping` closure\n\n\u003cimg src=images/leakcheck_sample.png width=800/\u003e\n\n\n# Example\n\nSome examples of memory leak that are detected by the tool:\n\n```swift\nclass X {\n  private var handler: (() -\u003e Void)!\n  private var anotherHandler: (() -\u003e Void)!\n  \n  func setup() {\n    handler = {\n      self.doSmth() // \u003c- Leak\n    }\n    \n    anotherHandler = { // Outer closure\n      doSmth { [weak self] in // \u003c- Leak\n        // .....    \n      }\n    }\n  }\n}\n```\n\nFor first leak, `self` holds a strong reference to `handler`, and `handler` holds a strong reference to `self`, which completes a retain cycle.\n\nFor 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\n\n\n# Usage\n\nThere'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\nand start building your own tool\n\n### SwiftLeakChecker\n\nThere is a SwiftLeakChecker target that you can run directly from XCode or as a command line. \n\n**To run from XCode:**\n\nEdit 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`)\n\n\u003cimg src=\"images/leakcheck_sample_xcode.png\" width=800/\u003e\n\n\n**To run from command line:**\n\n```\n./SwiftLeakChecker path/to/your/swift/file/or/folder\n```\n\n### Build your own tool\n\nThe 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.\n\nNote: Xcode 11 or later or a Swift 5.2 toolchain or later with the Swift Package Manager is required.\n\n\nAdd this repository to the `Package.swift` manifest of your project:\n\n```swift\n// swift-tools-version:4.2\nimport PackageDescription\n\nlet package = Package(\n  name: \"MyAwesomeLeakDetector\",\n  dependencies: [\n    .package(url: \"This repo .git url\", .exact(\"package version\")),\n  ],\n  targets: [\n    .target(name: \"MyAwesomeLeakDetector\", dependencies: [\"SwiftLeakCheck\"]),\n  ]\n)\n```\n\nThen, import `SwiftLeakCheck` in your Swift code\n\nTo create a leak detector and start detecting: \n\n```swift\nimport SwiftLeakCheck\n\nlet url = URL(fileURLWithPath: \"absolute/path/to/your/swift/file/or/folder\")\nlet detector = GraphLeakDetector()\nlet leaks = detector.detect(url)\nleaks.forEach { leak in\n  print(\"\\(leak)\")\n}\n```\n\n# Leak object\n\nEach `Leak` object contains `line`, `column` and `reason` info.\n```\n{\n  \"line\":41,\n  \"column\":7,\n  \"reason\":\"`self` is strongly captured here, from a potentially escaped closure.\"\n}\n```\n\n# CI and Danger\n\nThe 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.\n\n\n# How it works\n\nWe 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. \nCurrently we only check if `self` is captured strongly in an escaping closure, which is one specific case that causes memory leak\n\nTo do that, 3 things are checked:\n\n**1. Check if a reference captures `self`**\n\n```swift\nblock { [weak self] in\n  guard let strongSelf = self else { return }\n  let x = SomeClass()\n  strongSelf.doSmth { [weak strongSelf] in\n    guard let innerSelf = strongSelf else { return }\n    x.doSomething()\n  }\n}\n```\n\nIn this example, `innerSelf` captures `self`, because it is originated from `strongSelf` which is originated from `self`\n\n`x` is also a reference but doesn't capture `self`\n\n**2. Check if a closure is non-escaping**\n\nWe use as much information about the closure as possible to determine if it is non-escaping or not.\n\nIn the example below, `block` is non-escaping because it's not marked as `@escaping` and it's non-optional\n```\nfunc doSmth(block: () -\u003e Void) {\n   ... \n}\n```\n\nOr if it's anonymous closure, it's non-escaping\n```swift\nlet value = {\n  return self.doSmth()\n}()\n```\n\nWe can check more complicated case like this:\n\n```swift\nfunc test() {\n  let block = {\n    self.xxx\n  }\n  doSmth(block)\n}\nfunc doSmth(_ block: () -\u003e Void) {\n  ....\n}\n```\n\nIn this case, `block` is passed to a function `doSmth` and is not marked as `@escaping`, hence it's non-escaping\n\n**3. Whether an escaping closure captures self stronlgy from outside**\n\n```swift\nblock { [weak self] in\n  guard let strongSelf = self else { return }\n  self?.doSmth {\n    strongSelf.x += 1\n  }\n}\n```\n\nIn this example, we know that:\n1. `strongSelf` refers to `self`\n2. `doSmth` is escaping (just for example)\n3. `strongSelf` (in the inner closure) is defined from outside, and it captures `self` strongly\n\n\n# False-positive alarms\n\n### If we can't determine if a closure is escaping or non-escaping, we will just treat it as escaping. \n\nIt can happen when for eg, the closure is passed to a function that is defined in other source file.\nTo overcome that, you can define custom rules which have logic to classify a closure as escaping or non-escaping.\n\n\n# Non-escaping rules\n\nBy default, we already did most of the legworks trying to determine if a closure is non-escaping (See #2 of `How it works` section)\n\nBut in some cases, there's just not enough information in the source file. \nFor 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.\n\nThe 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.\nTo define a non-escaping rule, extend from `BaseNonEscapeRule`  and override `func isNonEscape(arg: FunctionCallArgumentSyntax,....) -\u003e Bool`\n\nHere's a rule that matches `DispatchQueue.main.async` or `DispatchQueue.global(qos:).asyncAfter` :\n\n```swift\nopen class DispatchQueueRule: NonEscapeRule {\n  \n  open override isNonEscape(arg: FunctionCallArgumentSyntax?, funcCallExpr: FunctionCallExprSyntax,, graph: Graph) -\u003e Bool {\n    // Signature of `async` function\n    let asyncSignature = FunctionSignature(name: \"async\", params: [\n      FunctionParam(name: \"execute\", isClosure: true)\n    ])\n    \n    // Predicate to match DispatchQueue.main\n    let mainQueuePredicate = ExprSyntaxPredicate.memberAccess(\"main\", base: ExprSyntaxPredicate.name(\"DispatchQueue\"))\n    \n    let mainQueueAsyncPredicate = ExprSyntaxPredicate.funcCall(asyncSignature, base: mainQueuePredicate)\n    if funcCallExpr.match(mainQueueAsyncPredicate) { // Matched DispatchQueue.main.async(...)\n        return true\n    }\n    \n    // Signature of `asyncAfter` function\n    let asyncAfterSignature = FunctionSignature(name: \"asyncAfter\", params: [\n      FunctionParam(name: \"deadline\"),\n      FunctionParam(name: \"execute\", isClosure: true)\n    ]) \n    \n    // Predicate to match DispatchQueue.global(qos: ...) or DispatchQueue.global()\n    let globalQueuePredicate = ExprSyntaxPredicate.funcCall(\n      FunctionSignature(name: \"global\", params: [\n        FunctionParam(name: \"qos\", canOmit: true)\n        ]),\n      base: ExprSyntaxPredicate.name(\"DispatchQueue\")\n    )\n    \n    let globalQueueAsyncAfterPredicate = ExprSyntaxPredicate.funcCall(asyncAfterSignature, base: globalQueuePredicate)\n    if funcCallExpr.match(globalQueueAsyncAfterPredicate) {\n        return true\n    }\n    \n    // Doesn't match either function\n    return false\n  }\n}\n```\n\nHere's another example of rule that matches `UIView.animate(withDurations: animations:)`:\n\n```swift\nopen class UIViewAnimationRule: BaseNonEscapeRule {\n  open override func isNonEscape(arg: FunctionCallArgumentSyntax?, funcCallExpr: FunctionCallExprSyntax, graph: Graph) -\u003e Bool {\n    let signature = FunctionSignature(name: \"animate\", params: [\n      FunctionParam(name: \"withDuration\"),\n      FunctionParam(name: \"animations\", isClosure: true)\n      ])\n    \n    let predicate = ExprSyntaxPredicate.funcCall(signature, base: ExprSyntaxPredicate.name(\"UIView\"))\n    return funcCallExpr.match(predicate)\n  }\n}\n```\n\nAfter creating the non-escaping rule, pass it to the leak detector:\n\n```swift\nlet leakDetector = GraphLeakDetector(nonEscapingRules: [DispatchQueueRule(), UIViewAnimationRule()])\n```\n\n# Predefined non-escaping rules\n\nThere're some ready-to-be-used non-escaping rules:\n\n**1. DispatchQueueRule**\n\nWe 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\nwill be gone quickly. So it's actually ok to treat it as non-escaping\n\n**3. UIViewAnimationRule**\n\nUIView static animation functions. Similar to DispatchQueue, UIView animation closures are escaping but will be executed then destroyed quickly.\n\n**3. UIViewControllerAnimationRule**\n\nUIViewController's present/dismiss functions. Similar to UIView animation rule.\n\n**4. CollectionRules**\n\nSwift Collection map/flatMap/compactMap/sort/filter/forEach. All these Swift Collection functions take in a non-escaping closure\n\n# Write your own detector\n\nIn 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\n\n```swift\nclass MyOwnLeakDetector: BaseSyntaxTreeLeakDetector {\n  override func detect(_ sourceFileNode: SourceFileSyntax) -\u003e [Leak] {\n    // Your own implementation\n  }\n}\n\n// Create an instance and start detecting leaks\nlet detector = MyOwnLeakDetector()\nlet url = URL(fileURLWithPath: \"absolute/path/to/your/swift/file/or/folder\")\nlet leaks = detector.detect(url)\n```\n\n\n### Graph\n\nGraph 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. \nYou probably want to use it if you create your own detector:\n\n```swift\nlet graph = GraphBuilder.buildGraph(node: sourceFileNode)\n```\n\n\n# Note\n\n1. 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.\n\n2. For non-escaping closure, there's no need to use `self.`. This can help to prevent false-positive\n\n\n# License\n\nThis library is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrab%2Fswift-leak-check","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrab%2Fswift-leak-check","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrab%2Fswift-leak-check/lists"}