From df22824e492e4cbce7afd92f075e719d42cfa94c Mon Sep 17 00:00:00 2001 From: dimohamdy Date: Fri, 4 Aug 2023 21:47:29 +0200 Subject: [PATCH] init --- MemoryLeak.xcodeproj/project.pbxproj | 187 +++- .../xcshareddata/swiftpm/Package.resolved | 14 + .../xcschemes/MemoryLeak.xcscheme | 84 ++ .../xcschemes/xcschememanagement.plist | 8 + .../SwiftLeakCheck/BackwardCompatiblity.swift | 12 + .../BaseSyntaxTreeLeakDetector.swift | 24 + .../SwiftLeakCheck/DirectoryScanner.swift | 48 + .../Sources/SwiftLeakCheck/Function.swift | 50 + .../SwiftLeakCheck/FunctionSignature.swift | 172 ++++ MemoryLeak/Sources/SwiftLeakCheck/Graph.swift | 857 ++++++++++++++++++ .../Sources/SwiftLeakCheck/GraphBuilder.swift | 199 ++++ .../SwiftLeakCheck/GraphLeakDetector.swift | 109 +++ MemoryLeak/Sources/SwiftLeakCheck/Leak.swift | 62 ++ .../Sources/SwiftLeakCheck/LeakDetector.swift | 26 + .../NonEscapeRules/CollectionRules.swift | 181 ++++ .../NonEscapeRules/DispatchQueueRule.swift | 118 +++ .../NonEscapeRules/ExprSyntaxPredicate.swift | 104 +++ .../NonEscapeRules/NonEscapeRule.swift | 44 + .../NonEscapeRules/UIViewAnimationRule.swift | 86 ++ .../UIViewControllerAnimationRule.swift | 128 +++ MemoryLeak/Sources/SwiftLeakCheck/Scope.swift | 332 +++++++ .../SwiftLeakCheck/SourceFileScope.swift | 16 + MemoryLeak/Sources/SwiftLeakCheck/Stack.swift | 58 ++ .../SwiftSyntax+Extensions.swift | 263 ++++++ .../Sources/SwiftLeakCheck/Symbol.swift | 30 + .../SwiftLeakCheck/SyntaxRetrieval.swift | 20 + .../Sources/SwiftLeakCheck/TypeDecl.swift | 32 + .../Sources/SwiftLeakCheck/TypeResolve.swift | 76 ++ .../Sources/SwiftLeakCheck/Utility.swift | 20 + .../Sources/SwiftLeakCheck/Variable.swift | 329 +++++++ .../Sources/SwiftLeakChecker/main.swift | 55 ++ MemoryLeak/main.swift | 11 - 32 files changed, 3740 insertions(+), 15 deletions(-) create mode 100644 MemoryLeak.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 MemoryLeak.xcodeproj/xcshareddata/xcschemes/MemoryLeak.xcscheme create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/BackwardCompatiblity.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/BaseSyntaxTreeLeakDetector.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/DirectoryScanner.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/Function.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/FunctionSignature.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/Graph.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/GraphBuilder.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/GraphLeakDetector.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/Leak.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/LeakDetector.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/CollectionRules.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/DispatchQueueRule.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/ExprSyntaxPredicate.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/NonEscapeRule.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/UIViewAnimationRule.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/UIViewControllerAnimationRule.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/Scope.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/SourceFileScope.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/Stack.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/SwiftSyntax+Extensions.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/Symbol.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/SyntaxRetrieval.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/TypeDecl.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/TypeResolve.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/Utility.swift create mode 100644 MemoryLeak/Sources/SwiftLeakCheck/Variable.swift create mode 100644 MemoryLeak/Sources/SwiftLeakChecker/main.swift delete mode 100644 MemoryLeak/main.swift diff --git a/MemoryLeak.xcodeproj/project.pbxproj b/MemoryLeak.xcodeproj/project.pbxproj index 04c14fb..fb546c2 100644 --- a/MemoryLeak.xcodeproj/project.pbxproj +++ b/MemoryLeak.xcodeproj/project.pbxproj @@ -7,7 +7,36 @@ objects = { /* Begin PBXBuildFile section */ - 33971E252A7C690900058593 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E242A7C690900058593 /* main.swift */; }; + 33971E4A2A7C692300058593 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E2D2A7C692300058593 /* main.swift */; }; + 33971E4B2A7C692300058593 /* Scope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E2F2A7C692300058593 /* Scope.swift */; }; + 33971E4C2A7C692300058593 /* GraphLeakDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E302A7C692300058593 /* GraphLeakDetector.swift */; }; + 33971E4D2A7C692300058593 /* Leak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E312A7C692300058593 /* Leak.swift */; }; + 33971E4E2A7C692300058593 /* DirectoryScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E322A7C692300058593 /* DirectoryScanner.swift */; }; + 33971E4F2A7C692300058593 /* Stack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E332A7C692300058593 /* Stack.swift */; }; + 33971E502A7C692300058593 /* BackwardCompatiblity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E342A7C692300058593 /* BackwardCompatiblity.swift */; }; + 33971E512A7C692300058593 /* SwiftSyntax+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E352A7C692300058593 /* SwiftSyntax+Extensions.swift */; }; + 33971E522A7C692300058593 /* FunctionSignature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E362A7C692300058593 /* FunctionSignature.swift */; }; + 33971E532A7C692300058593 /* GraphBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E372A7C692300058593 /* GraphBuilder.swift */; }; + 33971E542A7C692300058593 /* TypeResolve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E382A7C692300058593 /* TypeResolve.swift */; }; + 33971E552A7C692300058593 /* SourceFileScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E392A7C692300058593 /* SourceFileScope.swift */; }; + 33971E562A7C692300058593 /* Graph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E3A2A7C692300058593 /* Graph.swift */; }; + 33971E572A7C692300058593 /* SyntaxRetrieval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E3B2A7C692300058593 /* SyntaxRetrieval.swift */; }; + 33971E582A7C692300058593 /* UIViewControllerAnimationRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E3D2A7C692300058593 /* UIViewControllerAnimationRule.swift */; }; + 33971E592A7C692300058593 /* UIViewAnimationRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E3E2A7C692300058593 /* UIViewAnimationRule.swift */; }; + 33971E5A2A7C692300058593 /* CollectionRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E3F2A7C692300058593 /* CollectionRules.swift */; }; + 33971E5B2A7C692300058593 /* NonEscapeRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E402A7C692300058593 /* NonEscapeRule.swift */; }; + 33971E5C2A7C692300058593 /* ExprSyntaxPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E412A7C692300058593 /* ExprSyntaxPredicate.swift */; }; + 33971E5D2A7C692300058593 /* DispatchQueueRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E422A7C692300058593 /* DispatchQueueRule.swift */; }; + 33971E5E2A7C692300058593 /* BaseSyntaxTreeLeakDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E432A7C692300058593 /* BaseSyntaxTreeLeakDetector.swift */; }; + 33971E5F2A7C692300058593 /* TypeDecl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E442A7C692300058593 /* TypeDecl.swift */; }; + 33971E602A7C692300058593 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E452A7C692300058593 /* Utility.swift */; }; + 33971E612A7C692300058593 /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E462A7C692300058593 /* Variable.swift */; }; + 33971E622A7C692300058593 /* Function.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E472A7C692300058593 /* Function.swift */; }; + 33971E632A7C692300058593 /* LeakDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E482A7C692300058593 /* LeakDetector.swift */; }; + 33971E642A7C692300058593 /* Symbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33971E492A7C692300058593 /* Symbol.swift */; }; + 33971E672A7C696E00058593 /* SwiftParser in Frameworks */ = {isa = PBXBuildFile; productRef = 33971E662A7C696E00058593 /* SwiftParser */; }; + 33971E692A7C696E00058593 /* SwiftSyntax in Frameworks */ = {isa = PBXBuildFile; productRef = 33971E682A7C696E00058593 /* SwiftSyntax */; }; + 33971E6B2A7C696E00058593 /* SwiftSyntaxParser in Frameworks */ = {isa = PBXBuildFile; productRef = 33971E6A2A7C696E00058593 /* SwiftSyntaxParser */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -24,7 +53,33 @@ /* Begin PBXFileReference section */ 33971E212A7C690900058593 /* MemoryLeak */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = MemoryLeak; sourceTree = BUILT_PRODUCTS_DIR; }; - 33971E242A7C690900058593 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 33971E2D2A7C692300058593 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 33971E2F2A7C692300058593 /* Scope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scope.swift; sourceTree = ""; }; + 33971E302A7C692300058593 /* GraphLeakDetector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphLeakDetector.swift; sourceTree = ""; }; + 33971E312A7C692300058593 /* Leak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Leak.swift; sourceTree = ""; }; + 33971E322A7C692300058593 /* DirectoryScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryScanner.swift; sourceTree = ""; }; + 33971E332A7C692300058593 /* Stack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stack.swift; sourceTree = ""; }; + 33971E342A7C692300058593 /* BackwardCompatiblity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackwardCompatiblity.swift; sourceTree = ""; }; + 33971E352A7C692300058593 /* SwiftSyntax+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftSyntax+Extensions.swift"; sourceTree = ""; }; + 33971E362A7C692300058593 /* FunctionSignature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionSignature.swift; sourceTree = ""; }; + 33971E372A7C692300058593 /* GraphBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphBuilder.swift; sourceTree = ""; }; + 33971E382A7C692300058593 /* TypeResolve.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypeResolve.swift; sourceTree = ""; }; + 33971E392A7C692300058593 /* SourceFileScope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SourceFileScope.swift; sourceTree = ""; }; + 33971E3A2A7C692300058593 /* Graph.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Graph.swift; sourceTree = ""; }; + 33971E3B2A7C692300058593 /* SyntaxRetrieval.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyntaxRetrieval.swift; sourceTree = ""; }; + 33971E3D2A7C692300058593 /* UIViewControllerAnimationRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewControllerAnimationRule.swift; sourceTree = ""; }; + 33971E3E2A7C692300058593 /* UIViewAnimationRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewAnimationRule.swift; sourceTree = ""; }; + 33971E3F2A7C692300058593 /* CollectionRules.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionRules.swift; sourceTree = ""; }; + 33971E402A7C692300058593 /* NonEscapeRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonEscapeRule.swift; sourceTree = ""; }; + 33971E412A7C692300058593 /* ExprSyntaxPredicate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExprSyntaxPredicate.swift; sourceTree = ""; }; + 33971E422A7C692300058593 /* DispatchQueueRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatchQueueRule.swift; sourceTree = ""; }; + 33971E432A7C692300058593 /* BaseSyntaxTreeLeakDetector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseSyntaxTreeLeakDetector.swift; sourceTree = ""; }; + 33971E442A7C692300058593 /* TypeDecl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypeDecl.swift; sourceTree = ""; }; + 33971E452A7C692300058593 /* Utility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = ""; }; + 33971E462A7C692300058593 /* Variable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = ""; }; + 33971E472A7C692300058593 /* Function.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Function.swift; sourceTree = ""; }; + 33971E482A7C692300058593 /* LeakDetector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeakDetector.swift; sourceTree = ""; }; + 33971E492A7C692300058593 /* Symbol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Symbol.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -32,6 +87,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 33971E6B2A7C696E00058593 /* SwiftSyntaxParser in Frameworks */, + 33971E672A7C696E00058593 /* SwiftParser in Frameworks */, + 33971E692A7C696E00058593 /* SwiftSyntax in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -57,11 +115,69 @@ 33971E232A7C690900058593 /* MemoryLeak */ = { isa = PBXGroup; children = ( - 33971E242A7C690900058593 /* main.swift */, + 33971E2B2A7C692300058593 /* Sources */, ); path = MemoryLeak; sourceTree = ""; }; + 33971E2B2A7C692300058593 /* Sources */ = { + isa = PBXGroup; + children = ( + 33971E2C2A7C692300058593 /* SwiftLeakChecker */, + 33971E2E2A7C692300058593 /* SwiftLeakCheck */, + ); + path = Sources; + sourceTree = ""; + }; + 33971E2C2A7C692300058593 /* SwiftLeakChecker */ = { + isa = PBXGroup; + children = ( + 33971E2D2A7C692300058593 /* main.swift */, + ); + path = SwiftLeakChecker; + sourceTree = ""; + }; + 33971E2E2A7C692300058593 /* SwiftLeakCheck */ = { + isa = PBXGroup; + children = ( + 33971E2F2A7C692300058593 /* Scope.swift */, + 33971E302A7C692300058593 /* GraphLeakDetector.swift */, + 33971E312A7C692300058593 /* Leak.swift */, + 33971E322A7C692300058593 /* DirectoryScanner.swift */, + 33971E332A7C692300058593 /* Stack.swift */, + 33971E342A7C692300058593 /* BackwardCompatiblity.swift */, + 33971E352A7C692300058593 /* SwiftSyntax+Extensions.swift */, + 33971E362A7C692300058593 /* FunctionSignature.swift */, + 33971E372A7C692300058593 /* GraphBuilder.swift */, + 33971E382A7C692300058593 /* TypeResolve.swift */, + 33971E392A7C692300058593 /* SourceFileScope.swift */, + 33971E3A2A7C692300058593 /* Graph.swift */, + 33971E3B2A7C692300058593 /* SyntaxRetrieval.swift */, + 33971E3C2A7C692300058593 /* NonEscapeRules */, + 33971E432A7C692300058593 /* BaseSyntaxTreeLeakDetector.swift */, + 33971E442A7C692300058593 /* TypeDecl.swift */, + 33971E452A7C692300058593 /* Utility.swift */, + 33971E462A7C692300058593 /* Variable.swift */, + 33971E472A7C692300058593 /* Function.swift */, + 33971E482A7C692300058593 /* LeakDetector.swift */, + 33971E492A7C692300058593 /* Symbol.swift */, + ); + path = SwiftLeakCheck; + sourceTree = ""; + }; + 33971E3C2A7C692300058593 /* NonEscapeRules */ = { + isa = PBXGroup; + children = ( + 33971E3D2A7C692300058593 /* UIViewControllerAnimationRule.swift */, + 33971E3E2A7C692300058593 /* UIViewAnimationRule.swift */, + 33971E3F2A7C692300058593 /* CollectionRules.swift */, + 33971E402A7C692300058593 /* NonEscapeRule.swift */, + 33971E412A7C692300058593 /* ExprSyntaxPredicate.swift */, + 33971E422A7C692300058593 /* DispatchQueueRule.swift */, + ); + path = NonEscapeRules; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -78,6 +194,11 @@ dependencies = ( ); name = MemoryLeak; + packageProductDependencies = ( + 33971E662A7C696E00058593 /* SwiftParser */, + 33971E682A7C696E00058593 /* SwiftSyntax */, + 33971E6A2A7C696E00058593 /* SwiftSyntaxParser */, + ); productName = MemoryLeak; productReference = 33971E212A7C690900058593 /* MemoryLeak */; productType = "com.apple.product-type.tool"; @@ -106,6 +227,9 @@ Base, ); mainGroup = 33971E182A7C690900058593; + packageReferences = ( + 33971E652A7C696E00058593 /* XCRemoteSwiftPackageReference "swift-syntax" */, + ); productRefGroup = 33971E222A7C690900058593 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -120,7 +244,33 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 33971E252A7C690900058593 /* main.swift in Sources */, + 33971E512A7C692300058593 /* SwiftSyntax+Extensions.swift in Sources */, + 33971E572A7C692300058593 /* SyntaxRetrieval.swift in Sources */, + 33971E4D2A7C692300058593 /* Leak.swift in Sources */, + 33971E562A7C692300058593 /* Graph.swift in Sources */, + 33971E542A7C692300058593 /* TypeResolve.swift in Sources */, + 33971E4A2A7C692300058593 /* main.swift in Sources */, + 33971E5E2A7C692300058593 /* BaseSyntaxTreeLeakDetector.swift in Sources */, + 33971E4F2A7C692300058593 /* Stack.swift in Sources */, + 33971E552A7C692300058593 /* SourceFileScope.swift in Sources */, + 33971E522A7C692300058593 /* FunctionSignature.swift in Sources */, + 33971E5A2A7C692300058593 /* CollectionRules.swift in Sources */, + 33971E532A7C692300058593 /* GraphBuilder.swift in Sources */, + 33971E4E2A7C692300058593 /* DirectoryScanner.swift in Sources */, + 33971E582A7C692300058593 /* UIViewControllerAnimationRule.swift in Sources */, + 33971E4B2A7C692300058593 /* Scope.swift in Sources */, + 33971E622A7C692300058593 /* Function.swift in Sources */, + 33971E592A7C692300058593 /* UIViewAnimationRule.swift in Sources */, + 33971E5C2A7C692300058593 /* ExprSyntaxPredicate.swift in Sources */, + 33971E632A7C692300058593 /* LeakDetector.swift in Sources */, + 33971E502A7C692300058593 /* BackwardCompatiblity.swift in Sources */, + 33971E5D2A7C692300058593 /* DispatchQueueRule.swift in Sources */, + 33971E602A7C692300058593 /* Utility.swift in Sources */, + 33971E5F2A7C692300058593 /* TypeDecl.swift in Sources */, + 33971E4C2A7C692300058593 /* GraphLeakDetector.swift in Sources */, + 33971E642A7C692300058593 /* Symbol.swift in Sources */, + 33971E5B2A7C692300058593 /* NonEscapeRule.swift in Sources */, + 33971E612A7C692300058593 /* Variable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -280,6 +430,35 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 33971E652A7C696E00058593 /* XCRemoteSwiftPackageReference "swift-syntax" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-syntax.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 508.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 33971E662A7C696E00058593 /* SwiftParser */ = { + isa = XCSwiftPackageProductDependency; + package = 33971E652A7C696E00058593 /* XCRemoteSwiftPackageReference "swift-syntax" */; + productName = SwiftParser; + }; + 33971E682A7C696E00058593 /* SwiftSyntax */ = { + isa = XCSwiftPackageProductDependency; + package = 33971E652A7C696E00058593 /* XCRemoteSwiftPackageReference "swift-syntax" */; + productName = SwiftSyntax; + }; + 33971E6A2A7C696E00058593 /* SwiftSyntaxParser */ = { + isa = XCSwiftPackageProductDependency; + package = 33971E652A7C696E00058593 /* XCRemoteSwiftPackageReference "swift-syntax" */; + productName = SwiftSyntaxParser; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33971E192A7C690900058593 /* Project object */; } diff --git a/MemoryLeak.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MemoryLeak.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..d16d523 --- /dev/null +++ b/MemoryLeak.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "2c49d66d34dfd6f8130afdba889de77504b58ec0", + "version" : "508.0.1" + } + } + ], + "version" : 2 +} diff --git a/MemoryLeak.xcodeproj/xcshareddata/xcschemes/MemoryLeak.xcscheme b/MemoryLeak.xcodeproj/xcshareddata/xcschemes/MemoryLeak.xcscheme new file mode 100644 index 0000000..137412c --- /dev/null +++ b/MemoryLeak.xcodeproj/xcshareddata/xcschemes/MemoryLeak.xcscheme @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MemoryLeak.xcodeproj/xcuserdata/dabdelaziz.xcuserdatad/xcschemes/xcschememanagement.plist b/MemoryLeak.xcodeproj/xcuserdata/dabdelaziz.xcuserdatad/xcschemes/xcschememanagement.plist index c225f4d..9df7e9c 100644 --- a/MemoryLeak.xcodeproj/xcuserdata/dabdelaziz.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/MemoryLeak.xcodeproj/xcuserdata/dabdelaziz.xcuserdatad/xcschemes/xcschememanagement.plist @@ -10,5 +10,13 @@ 0 + SuppressBuildableAutocreation + + 33971E202A7C690900058593 + + primary + + + diff --git a/MemoryLeak/Sources/SwiftLeakCheck/BackwardCompatiblity.swift b/MemoryLeak/Sources/SwiftLeakCheck/BackwardCompatiblity.swift new file mode 100644 index 0000000..5b5043e --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/BackwardCompatiblity.swift @@ -0,0 +1,12 @@ +// +// Tmp.swift +// SwiftLeakCheck +// +// Created by Hoang Le Pham on 07/02/2021. +// + +import SwiftSyntax + +// For backward-compatible with Swift compiler 4.2 type names +public typealias FunctionCallArgumentListSyntax = TupleExprElementListSyntax +public typealias FunctionCallArgumentSyntax = TupleExprElementSyntax diff --git a/MemoryLeak/Sources/SwiftLeakCheck/BaseSyntaxTreeLeakDetector.swift b/MemoryLeak/Sources/SwiftLeakCheck/BaseSyntaxTreeLeakDetector.swift new file mode 100644 index 0000000..498934b --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/BaseSyntaxTreeLeakDetector.swift @@ -0,0 +1,24 @@ +// +// BaseSyntaxTreeLeakDetector.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 09/12/2019. +// + +import SwiftSyntax + +open class BaseSyntaxTreeLeakDetector: LeakDetector { + public init() {} + + public func detect(content: String) throws -> [Leak] { + let node = try SyntaxRetrieval.request(content: content) + return detect(node) + } + + open func detect(_ sourceFileNode: SourceFileSyntax) -> [Leak] { + fatalError("Implemented by subclass") + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/DirectoryScanner.swift b/MemoryLeak/Sources/SwiftLeakCheck/DirectoryScanner.swift new file mode 100644 index 0000000..0cef78f --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/DirectoryScanner.swift @@ -0,0 +1,48 @@ +// +// DirectoryScanner.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 09/12/2019. +// + +import Foundation + +public final class DirectoryScanner { + private let callback: (URL, inout Bool) -> Void + private var shouldStop = false + + public init(callback: @escaping (URL, inout Bool) -> Void) { + self.callback = callback + } + + public func scan(url: URL) { + if shouldStop { + shouldStop = false // Clear + return + } + + let isDirectory = (try? url.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false + if !isDirectory { + callback(url, &shouldStop) + } else { + let enumerator = FileManager.default.enumerator( + at: url, + includingPropertiesForKeys: nil, + options: [.skipsSubdirectoryDescendants], + errorHandler: nil + )! + + for childPath in enumerator { + if let url = childPath as? URL { + scan(url: url) + if shouldStop { + return + } + } + } + } + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/Function.swift b/MemoryLeak/Sources/SwiftLeakCheck/Function.swift new file mode 100644 index 0000000..85fe7e8 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/Function.swift @@ -0,0 +1,50 @@ +// +// Function.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 18/11/2019. +// + +import SwiftSyntax + +public typealias Function = FunctionDeclSyntax + +public extension Function { + enum MatchResult: Equatable { + public struct MappingInfo: Equatable { + let argumentToParamMapping: [FunctionCallArgumentSyntax: FunctionParameterSyntax] + let trailingClosureArgumentToParam: FunctionParameterSyntax? + } + + case nameMismatch + case argumentMismatch + case matched(MappingInfo) + + var isMatched: Bool { + switch self { + case .nameMismatch, + .argumentMismatch: + return false + case .matched: + return true + } + } + } + + func match(_ functionCallExpr: FunctionCallExprSyntax) -> MatchResult { + let (signature, mapping) = FunctionSignature.from(functionDeclExpr: self) + switch signature.match(functionCallExpr) { + case .nameMismatch: + return .nameMismatch + case .argumentMismatch: + return .argumentMismatch + case .matched(let matchedInfo): + return .matched(.init( + argumentToParamMapping: matchedInfo.argumentToParamMapping.mapValues { mapping[$0]! }, + trailingClosureArgumentToParam: matchedInfo.trailingClosureArgumentToParam.flatMap { mapping[$0] })) + } + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/FunctionSignature.swift b/MemoryLeak/Sources/SwiftLeakCheck/FunctionSignature.swift new file mode 100644 index 0000000..8e869bf --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/FunctionSignature.swift @@ -0,0 +1,172 @@ +// +// FunctionSignature.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 15/12/2019. +// + +import SwiftSyntax + +public struct FunctionSignature { + public let funcName: String + public let params: [FunctionParam] + + public init(name: String, params: [FunctionParam]) { + self.funcName = name + self.params = params + } + + public static func from(functionDeclExpr: FunctionDeclSyntax) -> (FunctionSignature, [FunctionParam: FunctionParameterSyntax]) { + let funcName = functionDeclExpr.identifier.text + let params = functionDeclExpr.signature.input.parameterList.map { FunctionParam(param: $0) } + let mapping = Dictionary(uniqueKeysWithValues: zip(params, functionDeclExpr.signature.input.parameterList)) + return (FunctionSignature(name: funcName, params: params), mapping) + } + + public enum MatchResult: Equatable { + public struct MappingInfo: Equatable { + let argumentToParamMapping: [FunctionCallArgumentSyntax: FunctionParam] + let trailingClosureArgumentToParam: FunctionParam? + } + + case nameMismatch + case argumentMismatch + case matched(MappingInfo) + + var isMatched: Bool { + switch self { + case .nameMismatch, + .argumentMismatch: + return false + case .matched: + return true + } + } + } + + public func match(_ functionCallExpr: FunctionCallExprSyntax) -> MatchResult { + guard funcName == functionCallExpr.symbol?.text else { + return .nameMismatch + } + + //print("Debug: \(functionCallExpr)") + + return match((ArgumentListWrapper(functionCallExpr.argumentList), functionCallExpr.trailingClosure)) + } + + private func match(_ rhs: (ArgumentListWrapper, ClosureExprSyntax?)) -> MatchResult { + let (arguments, trailingClosure) = rhs + + guard params.count > 0 else { + if arguments.count == 0 && trailingClosure == nil { + return .matched(.init(argumentToParamMapping: [:], trailingClosureArgumentToParam: nil)) + } else { + return .argumentMismatch + } + } + + let firstParam = params[0] + if firstParam.canOmit { + let matchResult = removingFirstParam().match(rhs) + if matchResult.isMatched { + return matchResult + } + } + + guard arguments.count > 0 else { + // In order to match, trailingClosure must be firstParam, there're no more params + guard let trailingClosure = trailingClosure else { + return .argumentMismatch + } + if params.count > 1 { + return .argumentMismatch + } + if isMatched(param: firstParam, trailingClosure: trailingClosure) { + return .matched(.init(argumentToParamMapping: [:], trailingClosureArgumentToParam: firstParam)) + } else { + return .argumentMismatch + } + } + + let firstArgument = arguments[0] + guard isMatched(param: firstParam, argument: firstArgument) else { + return .argumentMismatch + } + + let matchResult = removingFirstParam().match((arguments.removingFirst(), trailingClosure)) + if case let .matched(matchInfo) = matchResult { + var argumentToParamMapping = matchInfo.argumentToParamMapping + argumentToParamMapping[firstArgument] = firstParam + return .matched(.init(argumentToParamMapping: argumentToParamMapping, trailingClosureArgumentToParam: matchInfo.trailingClosureArgumentToParam)) + } else { + return .argumentMismatch + } + } + + // TODO: type matching + private func isMatched(param: FunctionParam, argument: FunctionCallArgumentSyntax) -> Bool { + return param.name == argument.label?.text + } + private func isMatched(param: FunctionParam, trailingClosure: ClosureExprSyntax) -> Bool { + return param.isClosure + } + + private func removingFirstParam() -> FunctionSignature { + return FunctionSignature(name: funcName, params: Array(params[1...])) + } +} + +public struct FunctionParam: Hashable { + public let name: String? + public let secondName: String? // This acts as a way to differentiate param when name is omitted. Don't remove this + public let canOmit: Bool + public let isClosure: Bool + + public init(name: String?, + secondName: String? = nil, + isClosure: Bool = false, + canOmit: Bool = false) { + assert(name != "_") + self.name = name + self.secondName = secondName + self.isClosure = isClosure + self.canOmit = canOmit + } + + public init(param: FunctionParameterSyntax) { + name = (param.firstName?.text == "_" ? nil : param.firstName?.text) + secondName = param.secondName?.text + + isClosure = (param.type?.isClosure == true) + canOmit = param.defaultArgument != nil + } +} + +private struct ArgumentListWrapper { + let argumentList: FunctionCallArgumentListSyntax + private let startIndex: Int + + init(_ argumentList: FunctionCallArgumentListSyntax) { + self.init(argumentList, startIndex: 0) + } + + private init(_ argumentList: FunctionCallArgumentListSyntax, startIndex: Int) { + self.argumentList = argumentList + self.startIndex = startIndex + } + + func removingFirst() -> ArgumentListWrapper { + return ArgumentListWrapper(argumentList, startIndex: startIndex + 1) + } + + subscript(_ i: Int) -> FunctionCallArgumentSyntax { + return argumentList[startIndex + i] + } + + var count: Int { + return argumentList.count - startIndex + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/Graph.swift b/MemoryLeak/Sources/SwiftLeakCheck/Graph.swift new file mode 100644 index 0000000..b34048b --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/Graph.swift @@ -0,0 +1,857 @@ +// +// Graph.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 11/11/2019. +// + +import SwiftSyntax + +public protocol Graph { + var sourceFileScope: SourceFileScope { get } + + /// Return the corresponding scope of a node if the node is of scope-type (class, func, closure,...) + /// or return the enclosing scope if the node is not scope-type + /// - Parameter node: The node + func scope(for node: Syntax) -> Scope + + /// Get the scope that encloses a given node + /// Eg, Scopes that enclose a func could be class, enum,... + /// Or scopes that enclose a statement could be func, closure,... + /// If the node is not enclosed by a scope (eg, sourcefile node), return the scope of the node itself + /// - Parameter node: A node + /// - Returns: The scope that encloses the node + func enclosingScope(for node: Syntax) -> Scope + + /// Return the TypeDecl that encloses a given node + /// - Parameter node: given node + func enclosingTypeDecl(for node: Syntax) -> TypeDecl? + + /// Find the nearest scope to a symbol, that can resolve the definition of that symbol + /// Usually it is the enclosing scope of the symbol + func closetScopeThatCanResolveSymbol(_ symbol: Symbol) -> Scope? + + func resolveExprType(_ expr: ExprSyntax) -> TypeResolve + func resolveVariableType(_ variable: Variable) -> TypeResolve + func resolveType(_ type: TypeSyntax) -> TypeResolve + func getAllRelatedTypeDecls(from typeDecl: TypeDecl) -> [TypeDecl] + func getAllRelatedTypeDecls(from typeResolve: TypeResolve) -> [TypeDecl] + + func resolveVariable(_ identifier: IdentifierExprSyntax) -> Variable? + func getVariableReferences(variable: Variable) -> [IdentifierExprSyntax] + + func resolveFunction(_ funcCallExpr: FunctionCallExprSyntax) -> (Function, Function.MatchResult.MappingInfo)? + + func isClosureEscape(_ closure: ClosureExprSyntax, nonEscapeRules: [NonEscapeRule]) -> Bool + func isCollection(_ node: ExprSyntax) -> Bool +} + +final class GraphImpl: Graph { + enum SymbolResolve { + case variable(Variable) + case function(Function) + case typeDecl(TypeDecl) + + var variable: Variable? { + switch self { + case .variable(let variable): return variable + default: + return nil + } + } + } + + private var mapScopeNodeToScope = [ScopeNode: Scope]() + private var cachedSymbolResolved = [Symbol: SymbolResolve]() + private var cachedReferencesToVariable = [Variable: [IdentifierExprSyntax]]() + private var cachedVariableType = [Variable: TypeResolve]() + private var cachedFunCallExprType = [FunctionCallExprSyntax: TypeResolve]() + private var cachedClosureEscapeCheck = [ClosureExprSyntax: Bool]() + + let sourceFileScope: SourceFileScope + init(sourceFileScope: SourceFileScope) { + self.sourceFileScope = sourceFileScope + buildScopeNodeToScopeMapping(root: sourceFileScope) + } + + private func buildScopeNodeToScopeMapping(root: Scope) { + mapScopeNodeToScope[root.scopeNode] = root + root.childScopes.forEach { child in + buildScopeNodeToScopeMapping(root: child) + } + } +} + +// MARK: - Scope +extension GraphImpl { + func scope(for node: Syntax) -> Scope { + guard let scopeNode = ScopeNode.from(node: node) else { + return enclosingScope(for: node) + } + + return scope(for: scopeNode) + } + + func enclosingScope(for node: Syntax) -> Scope { + guard let scopeNode = node.enclosingScopeNode else { + let result = scope(for: node) + assert(result == sourceFileScope) + return result + } + + return scope(for: scopeNode) + } + + func enclosingTypeDecl(for node: Syntax) -> TypeDecl? { + var scopeNode: ScopeNode! = node.enclosingScopeNode + while scopeNode != nil { + if scopeNode.type.isTypeDecl { + return scope(for: scopeNode).typeDecl + } + scopeNode = scopeNode.enclosingScopeNode + } + + return nil + } + + func scope(for scopeNode: ScopeNode) -> Scope { + guard let result = mapScopeNodeToScope[scopeNode] else { + fatalError("Can't find the scope of node \(scopeNode)") + } + return result + } + + func closetScopeThatCanResolveSymbol(_ symbol: Symbol) -> Scope? { + let scope = enclosingScope(for: symbol.node) + // Special case when node is a closure capture item, ie `{ [weak self] in` + // We need to examine node wrt closure's parent + if symbol.node.parent?.is(ClosureCaptureItemSyntax.self) == true { + if let parentScope = scope.parent { + return parentScope + } else { + fatalError("Can't happen") + } + } + + if symbol.node.hasAncestor({ $0.is(InheritedTypeSyntax.self) }) { + return scope.parent + } + + if symbol.node.hasAncestor({ $0.is(ExtensionDeclSyntax.self) && symbol.node.isDescendent(of: $0.as(ExtensionDeclSyntax.self)!.extendedType._syntaxNode) }) { + return scope.parent + } + + return scope + } +} + +// MARK: - Symbol resolve +extension GraphImpl { + enum ResolveSymbolOption: Equatable, CaseIterable { + case function + case variable + case typeDecl + } + + func _findSymbol(_ symbol: Symbol, + options: [ResolveSymbolOption] = ResolveSymbolOption.allCases, + onResult: (SymbolResolve) -> Bool) -> SymbolResolve? { + var scope: Scope! = closetScopeThatCanResolveSymbol(symbol) + while scope != nil { + if let result = cachedSymbolResolved[symbol], onResult(result) { + return result + } + + if let result = _findSymbol(symbol, options: options, in: scope, onResult: onResult) { + cachedSymbolResolved[symbol] = result + return result + } + + scope = scope?.parent + } + + return nil + } + + func _findSymbol(_ symbol: Symbol, + options: [ResolveSymbolOption] = ResolveSymbolOption.allCases, + in scope: Scope, + onResult: (SymbolResolve) -> Bool) -> SymbolResolve? { + + let scopesWithRelatedTypeDecl: [Scope] + if let typeDecl = scope.typeDecl { + scopesWithRelatedTypeDecl = getAllRelatedTypeDecls(from: typeDecl).map { $0.scope } + } else { + scopesWithRelatedTypeDecl = [scope] + } + + for scope in scopesWithRelatedTypeDecl { + if options.contains(.variable) { + if case let .identifier(node) = symbol, let variable = scope.getVariable(node) { + let result: SymbolResolve = .variable(variable) + if onResult(result) { + cachedReferencesToVariable[variable] = (cachedReferencesToVariable[variable] ?? []) + [node] + return result + } + } + } + + if options.contains(.function) { + for function in scope.getFunctionWithSymbol(symbol) { + let result: SymbolResolve = .function(function) + if onResult(result) { + return result + } + } + } + + if options.contains(.typeDecl) { + let typeDecls = scope.getTypeDecl(name: symbol.name) + for typeDecl in typeDecls { + let result: SymbolResolve = .typeDecl(typeDecl) + if onResult(result) { + return result + } + } + } + } + + return nil + } +} + +// MARK: - Variable reference +extension GraphImpl { + + @discardableResult + func resolveVariable(_ identifier: IdentifierExprSyntax) -> Variable? { + return _findSymbol(.identifier(identifier), options: [.variable]) { resolve -> Bool in + if resolve.variable != nil { + return true + } + return false + }?.variable + } + + func getVariableReferences(variable: Variable) -> [IdentifierExprSyntax] { + return cachedReferencesToVariable[variable] ?? [] + } + + func couldReferenceSelf(_ node: ExprSyntax) -> Bool { + if node.isCalledExpr() { + return false + } + + if let identifierNode = node.as(IdentifierExprSyntax.self) { + guard let variable = resolveVariable(identifierNode) else { + return identifierNode.identifier.text == "self" + } + + switch variable.raw { + case .param: + return false + case let .capture(capturedNode): + return couldReferenceSelf(ExprSyntax(capturedNode)) + case let .binding(_, valueNode): + if let valueNode = valueNode { + return couldReferenceSelf(valueNode) + } + return false + } + } + + return false + } +} + +// MARK: - Function resolve +extension GraphImpl { + func resolveFunction(_ funcCallExpr: FunctionCallExprSyntax) -> (Function, Function.MatchResult.MappingInfo)? { + if let identifier = funcCallExpr.calledExpression.as(IdentifierExprSyntax.self) { // doSmth(...) or A(...) + return _findFunction(symbol: .identifier(identifier), funcCallExpr: funcCallExpr) + } + + if let memberAccessExpr = funcCallExpr.calledExpression.as(MemberAccessExprSyntax.self) { // a.doSmth(...) or .doSmth(...) + if let base = memberAccessExpr.base { + if couldReferenceSelf(base) { + return _findFunction(symbol: .token(memberAccessExpr.name), funcCallExpr: funcCallExpr) + } + let typeDecls = getAllRelatedTypeDecls(from: resolveExprType(base)) + return _findFunction(from: typeDecls, symbol: .token(memberAccessExpr.name), funcCallExpr: funcCallExpr) + } else { + // Base is omitted when the type can be inferred. + // For eg, we can say: let s: String = .init(...) + return nil + } + } + + if funcCallExpr.calledExpression.is(OptionalChainingExprSyntax.self) { + // TODO + return nil + } + + // Unhandled case + return nil + } + + // TODO: Currently we only resolve to `func`. This could resole to `closure` as well + private func _findFunction(symbol: Symbol, funcCallExpr: FunctionCallExprSyntax) + -> (Function, Function.MatchResult.MappingInfo)? { + + var result: (Function, Function.MatchResult.MappingInfo)? + _ = _findSymbol(symbol, options: [.function]) { resolve -> Bool in + switch resolve { + case .variable, .typeDecl: // This could be due to cache + return false + case .function(let function): + let mustStop = enclosingScope(for: function._syntaxNode).type.isTypeDecl + + switch function.match(funcCallExpr) { + case .argumentMismatch, + .nameMismatch: + return mustStop + case .matched(let info): + guard result == nil else { + // Should not happenn + assert(false, "ambiguous") + return true // Exit + } + result = (function, info) + #if DEBUG + return mustStop // Continue to search to make sure no ambiguity + #else + return true + #endif + } + } + } + + return result + } + + private func _findFunction(from typeDecls: [TypeDecl], symbol: Symbol, funcCallExpr: FunctionCallExprSyntax) + -> (Function, Function.MatchResult.MappingInfo)? { + + for typeDecl in typeDecls { + for function in typeDecl.scope.getFunctionWithSymbol(symbol) { + if case let .matched(info) = function.match(funcCallExpr) { + return (function, info) + } + } + } + + return nil + } +} + +// MARK: Type resolve +extension GraphImpl { + func resolveVariableType(_ variable: Variable) -> TypeResolve { + if let type = cachedVariableType[variable] { + return type + } + + let result = _resolveType(variable.typeInfo) + cachedVariableType[variable] = result + return result + } + + func resolveExprType(_ expr: ExprSyntax) -> TypeResolve { + if let optionalExpr = expr.as(OptionalChainingExprSyntax.self) { + return .optional(base: resolveExprType(optionalExpr.expression)) + } + + if let identifierExpr = expr.as(IdentifierExprSyntax.self) { + if let variable = resolveVariable(identifierExpr) { + return resolveVariableType(variable) + } + if identifierExpr.identifier.text == "self" { + return enclosingTypeDecl(for: expr._syntaxNode).flatMap { .type($0) } ?? .unknown + } + // May be global variable, or type like Int, String,... + return .unknown + } + +// if let memberAccessExpr = node.as(MemberAccessExprSyntax.self) { +// guard let base = memberAccessExpr.base else { +// fatalError("Is it possible that `base` is nil ?") +// } +// +// } + + if let functionCallExpr = expr.as(FunctionCallExprSyntax.self) { + let result = cachedFunCallExprType[functionCallExpr] ?? _resolveFunctionCallType(functionCallExpr: functionCallExpr) + cachedFunCallExprType[functionCallExpr] = result + return result + } + + if let arrayExpr = expr.as(ArrayExprSyntax.self) { + return .sequence(elementType: resolveExprType(arrayExpr.elements[0].expression)) + } + + if expr.is(DictionaryExprSyntax.self) { + return .dict + } + + if expr.is(IntegerLiteralExprSyntax.self) { + return _getAllExtensions(name: ["Int"]).first.flatMap { .type($0) } ?? .name(["Int"]) + } + if expr.is(StringLiteralExprSyntax.self) { + return _getAllExtensions(name: ["String"]).first.flatMap { .type($0) } ?? .name(["String"]) + } + if expr.is(FloatLiteralExprSyntax.self) { + return _getAllExtensions(name: ["Float"]).first.flatMap { .type($0) } ?? .name(["Float"]) + } + if expr.is(BooleanLiteralExprSyntax.self) { + return _getAllExtensions(name: ["Bool"]).first.flatMap { .type($0) } ?? .name(["Bool"]) + } + + if let tupleExpr = expr.as(TupleExprSyntax.self) { + if tupleExpr.elementList.count == 1, let range = tupleExpr.elementList[0].expression.rangeInfo { + if let leftType = range.left.flatMap({ resolveExprType($0) })?.toNilIfUnknown { + return .sequence(elementType: leftType) + } else if let rightType = range.right.flatMap({ resolveExprType($0) })?.toNilIfUnknown { + return .sequence(elementType: rightType) + } else { + return .unknown + } + } + + return .tuple(tupleExpr.elementList.map { resolveExprType($0.expression) }) + } + + if let subscriptExpr = expr.as(SubscriptExprSyntax.self) { + let sequenceElementType = resolveExprType(subscriptExpr.calledExpression).sequenceElementType + if sequenceElementType != .unknown { + if subscriptExpr.argumentList.count == 1, let argument = subscriptExpr.argumentList.first?.expression { + if argument.rangeInfo != nil { + return .sequence(elementType: sequenceElementType) + } + if resolveExprType(argument).isInt { + return sequenceElementType + } + } + } + + return .unknown + } + + return .unknown + } + + private func _resolveType(_ typeInfo: TypeInfo) -> TypeResolve { + switch typeInfo { + case .exact(let type): + return resolveType(type) + case .inferedFromExpr(let expr): + return resolveExprType(expr) + case .inferedFromClosure(let closureExpr, let paramIndex, let paramCount): + // let x: (X, Y) -> Z = { a,b in ...} + if let closureVariable = enclosingScope(for: Syntax(closureExpr)).getVariableBindingTo(expr: ExprSyntax(closureExpr)) { + switch closureVariable.typeInfo { + case .exact(let type): + guard let argumentsType = (type.as(FunctionTypeSyntax.self))?.arguments else { + // Eg: let onFetchJobs: JobCardsFetcher.OnFetchJobs = { [weak self] jobs in ... } + return .unknown + } + assert(argumentsType.count == paramCount) + return resolveType(argumentsType[paramIndex].type) + case .inferedFromClosure, + .inferedFromExpr, + .inferedFromSequence, + .inferedFromTuple: + assert(false, "Seems wrong") + return .unknown + } + } + // TODO: there's also this case + // var b: ((X) -> Y)! + // b = { x in ... } + return .unknown + case .inferedFromSequence(let sequenceExpr): + let sequenceType = resolveExprType(sequenceExpr) + return sequenceType.sequenceElementType + case .inferedFromTuple(let tupleTypeInfo, let index): + if case let .tuple(types) = _resolveType(tupleTypeInfo) { + return types[index] + } + return .unknown + } + } + + func resolveType(_ type: TypeSyntax) -> TypeResolve { + if type.isOptional { + return .optional(base: resolveType(type.wrappedType)) + } + + if let arrayType = type.as(ArrayTypeSyntax.self) { + return .sequence(elementType: resolveType(arrayType.elementType)) + } + + if type.is(DictionaryTypeSyntax.self) { + return .dict + } + + if let tupleType = type.as(TupleTypeSyntax.self) { + return .tuple(tupleType.elements.map { resolveType($0.type) }) + } + + if let tokens = type.tokens, let typeDecl = resolveTypeDecl(tokens: tokens) { + return .type(typeDecl) + } else if let name = type.name { + return .name(name) + } else { + return .unknown + } + } + + private func _resolveFunctionCallType(functionCallExpr: FunctionCallExprSyntax, ignoreOptional: Bool = false) -> TypeResolve { + + if let (function, _) = resolveFunction(functionCallExpr) { + if let type = function.signature.output?.returnType { + return resolveType(type) + } else { + return .unknown // Void + } + } + + var calledExpr = functionCallExpr.calledExpression + + if let optionalExpr = calledExpr.as(OptionalChainingExprSyntax.self) { // Must be optional closure + if !ignoreOptional { + return .optional(base: _resolveFunctionCallType(functionCallExpr: functionCallExpr, ignoreOptional: true)) + } else { + calledExpr = optionalExpr.expression + } + } + + // [X]() + if let arrayExpr = calledExpr.as(ArrayExprSyntax.self) { + if let typeIdentifier = arrayExpr.elements[0].expression.as(IdentifierExprSyntax.self) { + if let typeDecl = resolveTypeDecl(tokens: [typeIdentifier.identifier]) { + return .sequence(elementType: .type(typeDecl)) + } else { + return .sequence(elementType: .name([typeIdentifier.identifier.text])) + } + } else { + return .sequence(elementType: resolveExprType(arrayExpr.elements[0].expression)) + } + } + + // [X: Y]() + if calledExpr.is(DictionaryExprSyntax.self) { + return .dict + } + + // doSmth() or A() + if let identifierExpr = calledExpr.as(IdentifierExprSyntax.self) { + let identifierResolve = _findSymbol(.identifier(identifierExpr)) { resolve in + switch resolve { + case .function(let function): + return function.match(functionCallExpr).isMatched + case .typeDecl: + return true + case .variable: + return false + } + } + if let identifierResolve = identifierResolve { + switch identifierResolve { + // doSmth() + case .function(let function): + let returnType = function.signature.output?.returnType + return returnType.flatMap { resolveType($0) } ?? .unknown + // A() + case .typeDecl(let typeDecl): + return .type(typeDecl) + case .variable: + break + } + } + } + + // x.y() + if let memberAccessExpr = calledExpr.as(MemberAccessExprSyntax.self) { + if let base = memberAccessExpr.base { + let baseType = resolveExprType(base) + if _isCollection(baseType) { + let funcName = memberAccessExpr.name.text + if ["map", "flatMap", "compactMap", "enumerated"].contains(funcName) { + return .sequence(elementType: .unknown) + } + if ["filter", "sorted"].contains(funcName) { + return baseType + } + } + } else { + // Base is omitted when the type can be inferred. + // For eg, we can say: let s: String = .init(...) + return .unknown + } + + } + + return .unknown + } +} + +// MARK: - TypeDecl resolve +extension GraphImpl { + + func resolveTypeDecl(tokens: [TokenSyntax]) -> TypeDecl? { + guard tokens.count > 0 else { + return nil + } + + return _resolveTypeDecl(token: tokens[0], onResult: { typeDecl in + var currentScope = typeDecl.scope + for token in tokens[1...] { + if let scope = currentScope.getTypeDecl(name: token.text).first?.scope { + currentScope = scope + } else { + return false + } + } + return true + }) + } + + private func _resolveTypeDecl(token: TokenSyntax, onResult: (TypeDecl) -> Bool) -> TypeDecl? { + let result = _findSymbol(.token(token), options: [.typeDecl]) { resolve in + if case let .typeDecl(typeDecl) = resolve { + return onResult(typeDecl) + } + return false + } + + if let result = result, case let .typeDecl(scope) = result { + return scope + } + + return nil + } + + func getAllRelatedTypeDecls(from: TypeDecl) -> [TypeDecl] { + var result: [TypeDecl] = _getAllExtensions(typeDecl: from) + if !from.isExtension { + result = [from] + result + } else { + if let originalDecl = resolveTypeDecl(tokens: from.tokens) { + result = [originalDecl] + result + } + } + + return result + result.flatMap { typeDecl -> [TypeDecl] in + guard let inheritanceTypes = typeDecl.inheritanceTypes else { + return [] + } + + return inheritanceTypes + .compactMap { resolveTypeDecl(tokens: $0.typeName.tokens ?? []) } + .flatMap { getAllRelatedTypeDecls(from: $0) } + } + } + + func getAllRelatedTypeDecls(from: TypeResolve) -> [TypeDecl] { + switch from.wrappedType { + case .type(let typeDecl): + return getAllRelatedTypeDecls(from: typeDecl) + case .sequence: + return _getAllExtensions(name: ["Array"]) + _getAllExtensions(name: ["Collection"]) + case .dict: + return _getAllExtensions(name: ["Dictionary"]) + _getAllExtensions(name: ["Collection"]) + case .name, .tuple, .unknown: + return [] + case .optional: + // Can't happen + return [] + } + } + + private func _getAllExtensions(typeDecl: TypeDecl) -> [TypeDecl] { + guard let name = _getTypeDeclFullPath(typeDecl)?.map({ $0.text }) else { return [] } + return _getAllExtensions(name: name) + } + + private func _getAllExtensions(name: [String]) -> [TypeDecl] { + return sourceFileScope.childScopes + .compactMap { $0.typeDecl } + .filter { $0.isExtension && $0.name == name } + } + + // For eg, type path for C in be example below is A.B.C + // class A { + // struct B { + // enum C { + // Returns nil if the type is nested inside non-type entity like function + private func _getTypeDeclFullPath(_ typeDecl: TypeDecl) -> [TokenSyntax]? { + let tokens = typeDecl.tokens + if typeDecl.scope.parent?.type == .sourceFileNode { + return tokens + } + if let parentTypeDecl = typeDecl.scope.parent?.typeDecl, let parentTokens = _getTypeDeclFullPath(parentTypeDecl) { + return parentTokens + tokens + } + return nil + } +} + +// MARK: - Classification +extension GraphImpl { + func isClosureEscape(_ closure: ClosureExprSyntax, nonEscapeRules: [NonEscapeRule]) -> Bool { + func _isClosureEscape(_ expr: ExprSyntax, isFuncParam: Bool) -> Bool { + // check cache + if let closureNode = expr.as(ClosureExprSyntax.self), let cachedResult = cachedClosureEscapeCheck[closureNode] { + return cachedResult + } + + // If it's a param, and it's inside an escaping closure, then it's also escaping + // For eg: + // func doSmth(block: @escaping () -> Void) { + // someObject.callBlock { + // block() + // } + // } + // Here block is a param and it's used inside an escaping closure + if isFuncParam { + if let parentClosure = expr.getEnclosingClosureNode() { + if isClosureEscape(parentClosure, nonEscapeRules: nonEscapeRules) { + return true + } + } + } + + // Function call expression: {...}() + if expr.isCalledExpr() { + return false // Not escape + } + + // let x = closure + // `x` may be used anywhere + if let variable = enclosingScope(for: expr._syntaxNode).getVariableBindingTo(expr: expr) { + let references = getVariableReferences(variable: variable) + for reference in references { + if _isClosureEscape(ExprSyntax(reference), isFuncParam: isFuncParam) == true { + return true // Escape + } + } + } + + // Used as argument in function call: doSmth(a, b, c: {...}) or doSmth(a, b) {...} + if let (functionCall, argument) = expr.getEnclosingFunctionCallExpression() { + if let (function, matchedInfo) = resolveFunction(functionCall) { + let param: FunctionParameterSyntax! + if let argument = argument { + param = matchedInfo.argumentToParamMapping[argument] + } else { + param = matchedInfo.trailingClosureArgumentToParam + } + guard param != nil else { fatalError("Something wrong") } + + // If the param is marked as `@escaping`, we still need to check with the non-escaping rules + // If the param is not marked as `@escaping`, and it's optional, we don't know anything about it + // If the param is not marked as `@escaping`, and it's not optional, we know it's non-escaping for sure + if !param.isEscaping && param.type?.isOptional != true { + return false + } + + // get the `.function` scope where we define this func + let scope = self.scope(for: function._syntaxNode) + assert(scope.type.isFunction) + + guard let variableForParam = scope.variables.first(where: { $0.raw.token == (param.secondName ?? param.firstName) }) else { + fatalError("Can't find the Variable that wrap the param") + } + let references = getVariableReferences(variable: variableForParam) + for referennce in references { + if _isClosureEscape(ExprSyntax(referennce), isFuncParam: true) == true { + return true + } + } + return false + } else { + // Can't resolve the function + // Use custom rules + for rule in nonEscapeRules { + if rule.isNonEscape(closureNode: expr, graph: self) { + return false + } + } + + // Still can't figure out using custom rules, assume closure is escaping + return true + } + } + + return false // It's unlikely the closure is escaping + } + + let result = _isClosureEscape(ExprSyntax(closure), isFuncParam: false) + cachedClosureEscapeCheck[closure] = result + return result + } + + func isCollection(_ node: ExprSyntax) -> Bool { + let type = resolveExprType(node) + return _isCollection(type) + } + + private func _isCollection(_ type: TypeResolve) -> Bool { + let isCollectionTypeName: ([String]) -> Bool = { (name: [String]) in + return name == ["Array"] || name == ["Dictionary"] || name == ["Set"] + } + + switch type { + case .tuple, + .unknown: + return false + case .sequence, + .dict: + return true + case .optional(let base): + return _isCollection(base) + case .name(let name): + return isCollectionTypeName(name) + case .type(let typeDecl): + let allTypeDecls = getAllRelatedTypeDecls(from: typeDecl) + for typeDecl in allTypeDecls { + if isCollectionTypeName(typeDecl.name) { + return true + } + + for inherritedName in (typeDecl.inheritanceTypes?.map { $0.typeName.name ?? [""] } ?? []) { + // If it extends any of the collection types or implements Collection protocol + if isCollectionTypeName(inherritedName) || inherritedName == ["Collection"] { + return true + } + } + } + + return false + } + } +} + +private extension Scope { + func getVariableBindingTo(expr: ExprSyntax) -> Variable? { + return variables.first(where: { variable -> Bool in + switch variable.raw { + case .param, .capture: return false + case let .binding(_, valueNode): + return valueNode != nil ? valueNode! == expr : false + } + }) + } +} + +private extension TypeResolve { + var toNilIfUnknown: TypeResolve? { + switch self { + case .unknown: return nil + default: return self + } + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/GraphBuilder.swift b/MemoryLeak/Sources/SwiftLeakCheck/GraphBuilder.swift new file mode 100644 index 0000000..2a91ff1 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/GraphBuilder.swift @@ -0,0 +1,199 @@ +// +// GraphBuilder.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 29/10/2019. +// + +import SwiftSyntax + +final class GraphBuilder { + static func buildGraph(node: SourceFileSyntax) -> GraphImpl { + // First round: build the graph + let visitor = GraphBuilderVistor() + visitor.walk(node) + + let graph = GraphImpl(sourceFileScope: visitor.sourceFileScope) + + // Second round: resolve the references + ReferenceBuilderVisitor(graph: graph).walk(node) + + return graph + } +} + +class BaseGraphVistor: SyntaxAnyVisitor { + override func visit(_ node: MissingDeclSyntax) -> SyntaxVisitorContinueKind { + return .skipChildren + } + + override func visit(_ node: MissingExprSyntax) -> SyntaxVisitorContinueKind { + return .skipChildren + } + + override func visit(_ node: MissingStmtSyntax) -> SyntaxVisitorContinueKind { + return .skipChildren + } + + override func visit(_ node: MissingTypeSyntax) -> SyntaxVisitorContinueKind { + return .skipChildren + } + + override func visit(_ node: MissingPatternSyntax) -> SyntaxVisitorContinueKind { + return .skipChildren + } +} + +fileprivate final class GraphBuilderVistor: BaseGraphVistor { + fileprivate var sourceFileScope: SourceFileScope! + private var stack = Stack() + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + if let scopeNode = ScopeNode.from(node: node) { + if case let .sourceFileNode(node) = scopeNode { + assert(stack.peek() == nil) + sourceFileScope = SourceFileScope(node: node, parent: stack.peek()) + stack.push(sourceFileScope) + } else { + let scope = Scope(scopeNode: scopeNode, parent: stack.peek()) + stack.push(scope) + } + } + + /* + #if DEBUG + if node.is(ElseBlockSyntax.self) || node.is(ElseIfContinuationSyntax.self) { + assertionFailure("Unhandled case") + } + #endif + */ + + return super.visitAny(node) + } + + override func visitAnyPost(_ node: Syntax) { + if let scopeNode = ScopeNode.from(node: node) { + assert(stack.peek()?.scopeNode == scopeNode) + stack.pop() + } + super.visitAnyPost(node) + } + + // Note: this is not necessarily in a func x(param...) + // Take this example: + // x.block { param in ... } + // Swift treats `param` as ClosureParamSyntax , but if we put `param` in open and close parathenses, + // Swift will treat it as FunctionParameterSyntax + override func visit(_ node: FunctionParameterSyntax) -> SyntaxVisitorContinueKind { + + _ = super.visit(node) + + guard let scope = stack.peek(), scope.type.isFunction || scope.type == .enumCaseNode else { + fatalError() + } + guard let name = node.secondName ?? node.firstName else { + assert(scope.type == .enumCaseNode) + return .visitChildren + } + + guard name.tokenKind != .wildcardKeyword else { + return .visitChildren + } + + scope.addVariable(Variable.from(node, scope: scope)) + return .visitChildren + } + + override func visit(_ node: ClosureCaptureItemSyntax) -> SyntaxVisitorContinueKind { + + _ = super.visit(node) + + guard let scope = stack.peek(), scope.isClosure else { + fatalError() + } + + Variable.from(node, scope: scope).flatMap { scope.addVariable($0) } + return .visitChildren + } + + override func visit(_ node: ClosureParamSyntax) -> SyntaxVisitorContinueKind { + + _ = super.visit(node) + + guard let scope = stack.peek(), scope.isClosure else { + fatalError("A closure should be found for a ClosureParam node. Stack may have been corrupted") + } + scope.addVariable(Variable.from(node, scope: scope)) + return .visitChildren + } + + override func visit(_ node: PatternBindingSyntax) -> SyntaxVisitorContinueKind { + + _ = super.visit(node) + + guard let scope = stack.peek() else { + fatalError() + } + + Variable.from(node, scope: scope).forEach { + scope.addVariable($0) + } + + return .visitChildren + } + + override func visit(_ node: OptionalBindingConditionSyntax) -> SyntaxVisitorContinueKind { + + _ = super.visit(node) + + guard let scope = stack.peek() else { + fatalError() + } + + let isGuardCondition = node.isGuardCondition() + assert(!isGuardCondition || scope.type == .guardNode) + let scopeThatOwnVariable = (isGuardCondition ? scope.parent! : scope) + if let variable = Variable.from(node, scope: scopeThatOwnVariable) { + scopeThatOwnVariable.addVariable(variable) + } + return .visitChildren + } + + override func visit(_ node: ForInStmtSyntax) -> SyntaxVisitorContinueKind { + + _ = super.visit(node) + + guard let scope = stack.peek(), scope.type == .forLoopNode else { + fatalError() + } + + Variable.from(node, scope: scope).forEach { variable in + scope.addVariable(variable) + } + + return .visitChildren + } +} + +/// Visit the tree and resolve references +private final class ReferenceBuilderVisitor: BaseGraphVistor { + private let graph: GraphImpl + init(graph: GraphImpl) { + self.graph = graph + super.init(viewMode: .sourceAccurate) + } + + override func visit(_ node: IdentifierExprSyntax) -> SyntaxVisitorContinueKind { + graph.resolveVariable(node) + return .visitChildren + } +} + +private extension Scope { + var isClosure: Bool { + return type == .closureNode + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/GraphLeakDetector.swift b/MemoryLeak/Sources/SwiftLeakCheck/GraphLeakDetector.swift new file mode 100644 index 0000000..3e94510 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/GraphLeakDetector.swift @@ -0,0 +1,109 @@ +// +// GraphLeakDetector.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 12/11/2019. +// + +import SwiftSyntax + +public final class GraphLeakDetector: BaseSyntaxTreeLeakDetector { + + public var nonEscapeRules: [NonEscapeRule] = [] + + override public func detect(_ sourceFileNode: SourceFileSyntax) -> [Leak] { + var res: [Leak] = [] + let graph = GraphBuilder.buildGraph(node: sourceFileNode) + let sourceLocationConverter = SourceLocationConverter(file: "", tree: sourceFileNode) + let visitor = LeakSyntaxVisitor(graph: graph, nonEscapeRules: nonEscapeRules, sourceLocationConverter: sourceLocationConverter) { leak in + res.append(leak) + } + visitor.walk(sourceFileNode) + return res + } +} + +private final class LeakSyntaxVisitor: BaseGraphVistor { + private let graph: GraphImpl + private let sourceLocationConverter: SourceLocationConverter + private let onLeakDetected: (Leak) -> Void + private let nonEscapeRules: [NonEscapeRule] + + init(graph: GraphImpl, + nonEscapeRules: [NonEscapeRule], + sourceLocationConverter: SourceLocationConverter, + onLeakDetected: @escaping (Leak) -> Void) { + self.graph = graph + self.sourceLocationConverter = sourceLocationConverter + self.nonEscapeRules = nonEscapeRules + self.onLeakDetected = onLeakDetected + super.init(viewMode: .sourceAccurate) + } + + override func visit(_ node: IdentifierExprSyntax) -> SyntaxVisitorContinueKind { + detectLeak(node) + return .skipChildren + } + + private func detectLeak(_ node: IdentifierExprSyntax) { + var leak: Leak? + defer { + if let leak = leak { + onLeakDetected(leak) + } + } + + if node.getEnclosingClosureNode() == nil { + // Not inside closure -> ignore + return + } + + if !graph.couldReferenceSelf(ExprSyntax(node)) { + return + } + + var currentScope: Scope! = graph.closetScopeThatCanResolveSymbol(.identifier(node)) + var isEscape = false + while currentScope != nil { + if let variable = currentScope.getVariable(node) { + if !isEscape { + // No leak + return + } + + switch variable.raw { + case .param: + fatalError("Can't happen since a param cannot reference `self`") + case let .capture(capturedNode): + if variable.isStrong && isEscape { + leak = Leak(node: node, capturedNode: ExprSyntax(capturedNode), sourceLocationConverter: sourceLocationConverter) + } + case let .binding(_, valueNode): + if let referenceNode = valueNode?.as(IdentifierExprSyntax.self) { + if variable.isStrong && isEscape { + leak = Leak(node: node, capturedNode: ExprSyntax(referenceNode), sourceLocationConverter: sourceLocationConverter) + } + } else { + fatalError("Can't reference `self`") + } + } + + return + } + + if case let .closureNode(closureNode) = currentScope.scopeNode { + isEscape = graph.isClosureEscape(closureNode, nonEscapeRules: nonEscapeRules) + } + + currentScope = currentScope.parent + } + + if isEscape { + leak = Leak(node: node, capturedNode: nil, sourceLocationConverter: sourceLocationConverter) + return + } + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/Leak.swift b/MemoryLeak/Sources/SwiftLeakCheck/Leak.swift new file mode 100644 index 0000000..3983207 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/Leak.swift @@ -0,0 +1,62 @@ +// +// LeakDetection.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 27/10/2019. +// + +import Foundation +import SwiftSyntax + +open class Leak: CustomStringConvertible, Encodable { + public let node: IdentifierExprSyntax + public let capturedNode: ExprSyntax? + public let sourceLocationConverter: SourceLocationConverter + + public private(set) lazy var line: Int = { + return sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia).line ?? -1 + }() + + public private(set) lazy var column: Int = { + return sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia).column ?? -1 + }() + + public init(node: IdentifierExprSyntax, + capturedNode: ExprSyntax?, + sourceLocationConverter: SourceLocationConverter) { + self.node = node + self.capturedNode = capturedNode + self.sourceLocationConverter = sourceLocationConverter + } + + private enum CodingKeys: CodingKey { + case line + case column + case reason + } + + open func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(line, forKey: .line) + try container.encode(column, forKey: .column) + + let reason: String = { + return "`self` is strongly captured here, from a potentially escaped closure." + }() + try container.encode(reason, forKey: .reason) + } + + open var description: String { + return """ + `self` is strongly captured at (line: \(line), column: \(column))"), + from a potentially escaped closure. + """ + } + + open func xcodeWarning(path: String) ->String { + return "\(path):\(line):\(column): warning: `self` is strongly captured" + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/LeakDetector.swift b/MemoryLeak/Sources/SwiftLeakCheck/LeakDetector.swift new file mode 100644 index 0000000..703d8a3 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/LeakDetector.swift @@ -0,0 +1,26 @@ +// +// LeakDetector.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 27/10/2019. +// + +import SwiftSyntax +import Foundation + +public protocol LeakDetector { + func detect(content: String) throws -> [Leak] +} + +extension LeakDetector { + public func detect(_ filePath: String) throws -> [Leak] { + return try detect(content: String(contentsOfFile: filePath)) + } + + public func detect(_ url: URL) throws -> [Leak] { + return try detect(content: String(contentsOf: url)) + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/CollectionRules.swift b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/CollectionRules.swift new file mode 100644 index 0000000..4ae2109 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/CollectionRules.swift @@ -0,0 +1,181 @@ +// +// CollectionRules.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 29/10/2019. +// + +import SwiftSyntax + +/// Swift Collection functions like forEach, map, flatMap, sorted,.... +public enum CollectionRules { + + public private(set) static var rules: [NonEscapeRule] = { + return [ + CollectionForEachRule(), + CollectionCompactMapRule(), + CollectionMapRule(), + CollectionFilterRule(), + CollectionSortRule(), + CollectionFlatMapRule(), + CollectionFirstWhereRule(), + CollectionContainsRule(), + CollectionMaxMinRule() + ] + }() +} + +open class CollectionForEachRule: BaseNonEscapeRule { + public let mustBeCollection: Bool + private let signature = FunctionSignature(name: "forEach", params: [ + FunctionParam(name: nil, isClosure: true) + ]) + public init(mustBeCollection: Bool = false) { + self.mustBeCollection = mustBeCollection + } + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in + return !self.mustBeCollection || isCollection(expr, graph: graph) + })) + } +} + +open class CollectionCompactMapRule: BaseNonEscapeRule { + private let signature = FunctionSignature(name: "compactMap", params: [ + FunctionParam(name: nil, isClosure: true) + ]) + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in + return isCollection(expr, graph: graph) + })) + } +} + +open class CollectionMapRule: BaseNonEscapeRule { + private let signature = FunctionSignature(name: "map", params: [ + FunctionParam(name: nil, isClosure: true) + ]) + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in + return isCollection(expr, graph: graph) || isOptional(expr, graph: graph) + })) + } +} + +open class CollectionFlatMapRule: BaseNonEscapeRule { + private let signature = FunctionSignature(name: "flatMap", params: [ + FunctionParam(name: nil, isClosure: true) + ]) + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in + return isCollection(expr, graph: graph) || isOptional(expr, graph: graph) + })) + } +} + +open class CollectionFilterRule: BaseNonEscapeRule { + private let signature = FunctionSignature(name: "filter", params: [ + FunctionParam(name: nil, isClosure: true) + ]) + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in + return isCollection(expr, graph: graph) + })) + } +} + +open class CollectionSortRule: BaseNonEscapeRule { + private let sortSignature = FunctionSignature(name: "sort", params: [ + FunctionParam(name: "by", isClosure: true) + ]) + private let sortedSignature = FunctionSignature(name: "sorted", params: [ + FunctionParam(name: "by", isClosure: true) + ]) + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + return funcCallExpr.match(.funcCall(signature: sortSignature, base: .init { return isCollection($0, graph: graph) })) + || funcCallExpr.match(.funcCall(signature: sortedSignature, base: .init { return isCollection($0, graph: graph) })) + } +} + +open class CollectionFirstWhereRule: BaseNonEscapeRule { + private let firstWhereSignature = FunctionSignature(name: "first", params: [ + FunctionParam(name: "where", isClosure: true) + ]) + private let firstIndexWhereSignature = FunctionSignature(name: "firstIndex", params: [ + FunctionParam(name: "where", isClosure: true) + ]) + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + let base = ExprSyntaxPredicate { expr in + return isCollection(expr, graph: graph) + } + return funcCallExpr.match(.funcCall(signature: firstWhereSignature, base: base)) + || funcCallExpr.match(.funcCall(signature: firstIndexWhereSignature, base: base)) + } +} + +open class CollectionContainsRule: BaseNonEscapeRule { + let signature = FunctionSignature(name: "contains", params: [ + FunctionParam(name: "where", isClosure: true) + ]) + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + return funcCallExpr.match(.funcCall(signature: signature, base: .init { expr in + return isCollection(expr, graph: graph) })) + } +} + +open class CollectionMaxMinRule: BaseNonEscapeRule { + private let maxSignature = FunctionSignature(name: "max", params: [ + FunctionParam(name: "by", isClosure: true) + ]) + private let minSignature = FunctionSignature(name: "min", params: [ + FunctionParam(name: "by", isClosure: true) + ]) + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + return funcCallExpr.match(.funcCall(signature: maxSignature, base: .init { return isCollection($0, graph: graph) })) + || funcCallExpr.match(.funcCall(signature: minSignature, base: .init { return isCollection($0, graph: graph) })) + } +} + +private func isCollection(_ expr: ExprSyntax?, graph: Graph) -> Bool { + guard let expr = expr else { + return false + } + return graph.isCollection(expr) +} + +private func isOptional(_ expr: ExprSyntax?, graph: Graph) -> Bool { + guard let expr = expr else { + return false + } + return graph.resolveExprType(expr).isOptional +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/DispatchQueueRule.swift b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/DispatchQueueRule.swift new file mode 100644 index 0000000..3bdd59f --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/DispatchQueueRule.swift @@ -0,0 +1,118 @@ +// +// DispatchQueueRule.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 28/10/2019. +// + +import SwiftSyntax + +open class DispatchQueueRule: BaseNonEscapeRule { + + private let signatures: [FunctionSignature] = [ + FunctionSignature(name: "async", params: [ + FunctionParam(name: "group", canOmit: true), + FunctionParam(name: "qos", canOmit: true), + FunctionParam(name: "flags", canOmit: true), + FunctionParam(name: "execute", isClosure: true) + ]), + FunctionSignature(name: "async", params: [ + FunctionParam(name: "group", canOmit: true), + FunctionParam(name: "execute") + ]), + FunctionSignature(name: "sync", params: [ + FunctionParam(name: "flags", canOmit: true), + FunctionParam(name: "execute", isClosure: true) + ]), + FunctionSignature(name: "sync", params: [ + FunctionParam(name: "execute") + ]), + FunctionSignature(name: "asyncAfter", params: [ + FunctionParam(name: "deadline"), + FunctionParam(name: "qos", canOmit: true), + FunctionParam(name: "flags", canOmit: true), + FunctionParam(name: "execute", isClosure: true) + ]), + FunctionSignature(name: "asyncAfter", params: [ + FunctionParam(name: "wallDeadline"), + FunctionParam(name: "qos", canOmit: true), + FunctionParam(name: "flags", canOmit: true), + FunctionParam(name: "execute", isClosure: true) + ]) + ] + + private let mainQueuePredicate: ExprSyntaxPredicate = .memberAccess("main", base: .name("DispatchQueue")) + private let globalQueuePredicate: ExprSyntaxPredicate = .funcCall( + signature: FunctionSignature(name: "global", params: [.init(name: "qos", canOmit: true)]), + base: .name("DispatchQueue") + ) + + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + + for signature in signatures { + for queue in [mainQueuePredicate, globalQueuePredicate] { + let predicate: ExprSyntaxPredicate = .funcCall(signature: signature, base: queue) + if funcCallExpr.match(predicate) { + return true + } + } + } + + let isDispatchQueuePredicate: ExprSyntaxPredicate = .init { expr -> Bool in + guard let expr = expr else { return false } + let typeResolve = graph.resolveExprType(expr) + switch typeResolve.wrappedType { + case .name(let name): + return self.isDispatchQueueType(name: name) + case .type(let typeDecl): + let allTypeDecls = graph.getAllRelatedTypeDecls(from: typeDecl) + for typeDecl in allTypeDecls { + if self.isDispatchQueueType(typeDecl: typeDecl) { + return true + } + } + + return false + + case .dict, + .sequence, + .tuple, + .optional, // Can't happen + .unknown: + return false + } + } + + for signature in signatures { + let predicate: ExprSyntaxPredicate = .funcCall(signature: signature, base: isDispatchQueuePredicate) + if funcCallExpr.match(predicate) { + return true + } + } + + return false + } + + private func isDispatchQueueType(name: [String]) -> Bool { + return name == ["DispatchQueue"] + } + + private func isDispatchQueueType(typeDecl: TypeDecl) -> Bool { + if self.isDispatchQueueType(name: typeDecl.name) { + return true + } + for inheritedType in (typeDecl.inheritanceTypes ?? []) { + if self.isDispatchQueueType(name: inheritedType.typeName.name ?? []) { + return true + } + } + return false + } +} + diff --git a/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/ExprSyntaxPredicate.swift b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/ExprSyntaxPredicate.swift new file mode 100644 index 0000000..3a039e4 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/ExprSyntaxPredicate.swift @@ -0,0 +1,104 @@ +// +// ExprSyntaxPredicate.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 26/12/2019. +// + +import SwiftSyntax + +open class ExprSyntaxPredicate { + public let match: (ExprSyntax?) -> Bool + public init(_ match: @escaping (ExprSyntax?) -> Bool) { + self.match = match + } + + public static let any: ExprSyntaxPredicate = .init { _ in true } +} + +// MARK: - Identifier predicate +extension ExprSyntaxPredicate { + public static func name(_ text: String) -> ExprSyntaxPredicate { + return .name({ $0 == text }) + } + + public static func name(_ namePredicate: @escaping (String) -> Bool) -> ExprSyntaxPredicate { + return .init({ expr -> Bool in + guard let identifierExpr = expr?.as(IdentifierExprSyntax.self) else { + return false + } + return namePredicate(identifierExpr.identifier.text) + }) + } +} + +// MARK: - Function call predicate +extension ExprSyntaxPredicate { + public static func funcCall(name: String, + base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate { + return .funcCall(namePredicate: { $0 == name }, base: basePredicate) + } + + public static func funcCall(namePredicate: @escaping (String) -> Bool, + base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate { + return .funcCall(predicate: { funcCallExpr -> Bool in + guard let symbol = funcCallExpr.symbol else { + return false + } + return namePredicate(symbol.text) + && basePredicate.match(funcCallExpr.base) + }) + } + + public static func funcCall(signature: FunctionSignature, + base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate { + return .funcCall(predicate: { funcCallExpr -> Bool in + return signature.match(funcCallExpr).isMatched + && basePredicate.match(funcCallExpr.base) + }) + } + + public static func funcCall(predicate: @escaping (FunctionCallExprSyntax) -> Bool) -> ExprSyntaxPredicate { + return .init({ expr -> Bool in + guard let funcCallExpr = expr?.as(FunctionCallExprSyntax.self) else { + return false + } + return predicate(funcCallExpr) + }) + } +} + +// MARK: - MemberAccess predicate +extension ExprSyntaxPredicate { + public static func memberAccess(_ memberPredicate: @escaping (String) -> Bool, + base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate { + return .init({ expr -> Bool in + guard let memberAccessExpr = expr?.as(MemberAccessExprSyntax.self) else { + return false + } + return memberPredicate(memberAccessExpr.name.text) + && basePredicate.match(memberAccessExpr.base) + }) + } + + public static func memberAccess(_ member: String, base basePredicate: ExprSyntaxPredicate) -> ExprSyntaxPredicate { + return .memberAccess({ $0 == member }, base: basePredicate) + } +} + +public extension ExprSyntax { + + func match(_ predicate: ExprSyntaxPredicate) -> Bool { + return predicate.match(self) + } +} + +// Convenient +public extension FunctionCallExprSyntax { + func match(_ predicate: ExprSyntaxPredicate) -> Bool { + return predicate.match(ExprSyntax(self)) + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/NonEscapeRule.swift b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/NonEscapeRule.swift new file mode 100644 index 0000000..4de2b90 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/NonEscapeRule.swift @@ -0,0 +1,44 @@ +// +// NonEscapeRule.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 28/10/2019. +// + +import SwiftSyntax + +public protocol NonEscapeRule { + func isNonEscape(closureNode: ExprSyntax, graph: Graph) -> Bool +} + +open class BaseNonEscapeRule: NonEscapeRule { + public init() {} + + public func isNonEscape(closureNode: ExprSyntax, graph: Graph) -> Bool { + guard let (funcCallExpr, arg) = closureNode.getEnclosingFunctionCallExpression() else { + return false + } + + return isNonEscape( + arg: arg, + funcCallExpr: funcCallExpr, + graph: graph + ) + } + + /// Returns whether a given argument is escaping in a function call + /// + /// - Parameters: + /// - arg: The closure argument, or nil if it's trailing closure + /// - funcCallExpr: the source FunctionCallExprSyntax + /// - graph: Source code graph. Use it to retrieve more info + /// - Returns: true if the closure is non-escaping, false otherwise + open func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + return false + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/UIViewAnimationRule.swift b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/UIViewAnimationRule.swift new file mode 100644 index 0000000..005dda7 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/UIViewAnimationRule.swift @@ -0,0 +1,86 @@ +// +// UIViewAnimationRule.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 28/10/2019. +// + +import SwiftSyntax + +/// Eg, UIView.animate(..., animations: {...}) { +/// ..... +/// } +open class UIViewAnimationRule: BaseNonEscapeRule { + + private let signatures: [FunctionSignature] = [ + FunctionSignature(name: "animate", params: [ + FunctionParam(name: "withDuration"), + FunctionParam(name: "animations", isClosure: true) + ]), + FunctionSignature(name: "animate", params: [ + FunctionParam(name: "withDuration"), + FunctionParam(name: "animations", isClosure: true), + FunctionParam(name: "completion", isClosure: true, canOmit: true) + ]), + FunctionSignature(name: "animate", params: [ + FunctionParam(name: "withDuration"), + FunctionParam(name: "delay"), + FunctionParam(name: "options", canOmit: true), + FunctionParam(name: "animations", isClosure: true), + FunctionParam(name: "completion", isClosure: true, canOmit: true) + ]), + FunctionSignature(name: "animate", params: [ + FunctionParam(name: "withDuration"), + FunctionParam(name: "delay"), + FunctionParam(name: "usingSpringWithDamping"), + FunctionParam(name: "initialSpringVelocity"), + FunctionParam(name: "options", canOmit: true), + FunctionParam(name: "animations", isClosure: true), + FunctionParam(name: "completion", isClosure: true, canOmit: true) + ]), + FunctionSignature(name: "transition", params: [ + FunctionParam(name: "from"), + FunctionParam(name: "to"), + FunctionParam(name: "duration"), + FunctionParam(name: "options"), + FunctionParam(name: "completion", isClosure: true, canOmit: true), + ]), + FunctionSignature( name: "transition", params: [ + FunctionParam(name: "with"), + FunctionParam(name: "duration"), + FunctionParam(name: "options"), + FunctionParam(name: "animations", isClosure: true, canOmit: true), + FunctionParam(name: "completion", isClosure: true, canOmit: true), + ]), + FunctionSignature(name: "animateKeyframes", params: [ + FunctionParam(name: "withDuration"), + FunctionParam(name: "delay", canOmit: true), + FunctionParam(name: "options", canOmit: true), + FunctionParam(name: "animations", isClosure: true), + FunctionParam(name: "completion", isClosure: true) + ]) + ] + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + + // Check if base is `UIView`, if not we can end early without checking any of the signatures + guard funcCallExpr.match(.funcCall(namePredicate: { _ in true }, base: .name("UIView"))) else { + return false + } + + // Now we can check each signature and ignore the base (already checked) + for signature in signatures { + if funcCallExpr.match(.funcCall(signature: signature, base: .any)) { + return true + } + } + + return false + } +} + diff --git a/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/UIViewControllerAnimationRule.swift b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/UIViewControllerAnimationRule.swift new file mode 100644 index 0000000..0320aa7 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/NonEscapeRules/UIViewControllerAnimationRule.swift @@ -0,0 +1,128 @@ +// +// UIViewControllerAnimationRule.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 30/12/2019. +// + +import SwiftSyntax + +/// Eg, someViewController.present(vc, animated: true, completion: { ... }) +/// or someViewController.dismiss(animated: true) { ... } +open class UIViewControllerAnimationRule: BaseNonEscapeRule { + + private let signatures: [FunctionSignature] = [ + FunctionSignature(name: "present", params: [ + FunctionParam(name: nil), // "viewControllerToPresent" + FunctionParam(name: "animated"), + FunctionParam(name: "completion", isClosure: true, canOmit: true) + ]), + FunctionSignature(name: "dismiss", params: [ + FunctionParam(name: "animated"), + FunctionParam(name: "completion", isClosure: true, canOmit: true) + ]), + FunctionSignature(name: "transition", params: [ + FunctionParam(name: "from"), + FunctionParam(name: "to"), + FunctionParam(name: "duration"), + FunctionParam(name: "options", canOmit: true), + FunctionParam(name: "animations", isClosure: true), + FunctionParam(name: "completion", isClosure: true, canOmit: true) + ]) + ] + + open override func isNonEscape(arg: FunctionCallArgumentSyntax?, + funcCallExpr: FunctionCallExprSyntax, + graph: Graph) -> Bool { + + // Make sure the func is called from UIViewController + guard isCalledFromUIViewController(funcCallExpr: funcCallExpr, graph: graph) else { + return false + } + + // Now we can check each signature and ignore the base that is already checked + for signature in signatures { + if funcCallExpr.match(.funcCall(signature: signature, base: .any)) { + return true + } + } + + return false + } + + open func isUIViewControllerType(name: [String]) -> Bool { + + let typeName = name.last ?? "" + + let candidates = [ + "UIViewController", + "UITableViewController", + "UICollectionViewController", + "UIAlertController", + "UIActivityViewController", + "UINavigationController", + "UITabBarController", + "UIMenuController", + "UISearchController" + ] + + return candidates.contains(typeName) || typeName.hasSuffix("ViewController") + } + + private func isUIViewControllerType(typeDecl: TypeDecl) -> Bool { + if isUIViewControllerType(name: typeDecl.name) { + return true + } + + let inheritantTypes = (typeDecl.inheritanceTypes ?? []).map { $0.typeName } + for inheritantType in inheritantTypes { + if isUIViewControllerType(name: inheritantType.name ?? []) { + return true + } + } + + return false + } + + private func isCalledFromUIViewController(funcCallExpr: FunctionCallExprSyntax, graph: Graph) -> Bool { + guard let base = funcCallExpr.base else { + // No base, eg: doSmth() + // class SomeClass { + // func main() { + // doSmth() + // } + // } + // In this case, we find the TypeDecl where this func is called from (Eg, SomeClass) + if let typeDecl = graph.enclosingTypeDecl(for: funcCallExpr._syntaxNode) { + return isUIViewControllerType(typeDecl: typeDecl) + } else { + return false + } + } + + // Eg: base.doSmth() + // We check if base is UIViewController + let typeResolve = graph.resolveExprType(base) + switch typeResolve.wrappedType { + case .type(let typeDecl): + let allTypeDecls = graph.getAllRelatedTypeDecls(from: typeDecl) + for typeDecl in allTypeDecls { + if isUIViewControllerType(typeDecl: typeDecl) { + return true + } + } + return false + case .name(let name): + return isUIViewControllerType(name: name) + case .dict, + .sequence, + .tuple, + .optional, // Can't happen + .unknown: + return false + } + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/Scope.swift b/MemoryLeak/Sources/SwiftLeakCheck/Scope.swift new file mode 100644 index 0000000..2c41ea2 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/Scope.swift @@ -0,0 +1,332 @@ +// +// Scope.swift +// LeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 27/10/2019. +// + +import SwiftSyntax + +public enum ScopeNode: Hashable, CustomStringConvertible { + case sourceFileNode(SourceFileSyntax) + case classNode(ClassDeclSyntax) + case structNode(StructDeclSyntax) + case enumNode(EnumDeclSyntax) + case enumCaseNode(EnumCaseDeclSyntax) + case extensionNode(ExtensionDeclSyntax) + case funcNode(FunctionDeclSyntax) + case initialiseNode(InitializerDeclSyntax) + case closureNode(ClosureExprSyntax) + case ifBlockNode(CodeBlockSyntax, IfStmtSyntax) // If block in a `IfStmtSyntax` + case elseBlockNode(CodeBlockSyntax, IfStmtSyntax) // Else block in a `IfStmtSyntax` + case guardNode(GuardStmtSyntax) + case forLoopNode(ForInStmtSyntax) + case whileLoopNode(WhileStmtSyntax) + case subscriptNode(SubscriptDeclSyntax) + case accessorNode(AccessorDeclSyntax) + case variableDeclNode(CodeBlockSyntax) // var x: Int { ... } + case switchCaseNode(SwitchCaseSyntax) + + public static func from(node: Syntax) -> ScopeNode? { + if let sourceFileNode = node.as(SourceFileSyntax.self) { + return .sourceFileNode(sourceFileNode) + } + + if let classNode = node.as(ClassDeclSyntax.self) { + return .classNode(classNode) + } + + if let structNode = node.as(StructDeclSyntax.self) { + return .structNode(structNode) + } + + if let enumNode = node.as(EnumDeclSyntax.self) { + return .enumNode(enumNode) + } + + if let enumCaseNode = node.as(EnumCaseDeclSyntax.self) { + return .enumCaseNode(enumCaseNode) + } + + if let extensionNode = node.as(ExtensionDeclSyntax.self) { + return .extensionNode(extensionNode) + } + + if let funcNode = node.as(FunctionDeclSyntax.self) { + return .funcNode(funcNode) + } + + if let initialiseNode = node.as(InitializerDeclSyntax.self) { + return .initialiseNode(initialiseNode) + } + + if let closureNode = node.as(ClosureExprSyntax.self) { + return .closureNode(closureNode) + } + + if let codeBlockNode = node.as(CodeBlockSyntax.self), codeBlockNode.parent?.is(IfStmtSyntax.self) == true { + let parent = (codeBlockNode.parent?.as(IfStmtSyntax.self))! + if codeBlockNode == parent.body { + return .ifBlockNode(codeBlockNode, parent) + } else if codeBlockNode == parent.elseBody?.as(CodeBlockSyntax.self) { + return .elseBlockNode(codeBlockNode, parent) + } + return nil + } + + if let guardNode = node.as(GuardStmtSyntax.self) { + return .guardNode(guardNode) + } + + if let forLoopNode = node.as(ForInStmtSyntax.self) { + return .forLoopNode(forLoopNode) + } + + if let whileLoopNode = node.as(WhileStmtSyntax.self) { + return .whileLoopNode(whileLoopNode) + } + + if let subscriptNode = node.as(SubscriptDeclSyntax.self) { + return .subscriptNode(subscriptNode) + } + + if let accessorNode = node.as(AccessorDeclSyntax.self) { + return .accessorNode(accessorNode) + } + + if let codeBlockNode = node.as(CodeBlockSyntax.self), + codeBlockNode.parent?.is(PatternBindingSyntax.self) == true, + codeBlockNode.parent?.parent?.is(PatternBindingListSyntax.self) == true, + codeBlockNode.parent?.parent?.parent?.is(VariableDeclSyntax.self) == true { + return .variableDeclNode(codeBlockNode) + } + + if let switchCaseNode = node.as(SwitchCaseSyntax.self) { + return .switchCaseNode(switchCaseNode) + } + + return nil + } + + public var node: Syntax { + switch self { + case .sourceFileNode(let node): return node._syntaxNode + case .classNode(let node): return node._syntaxNode + case .structNode(let node): return node._syntaxNode + case .enumNode(let node): return node._syntaxNode + case .enumCaseNode(let node): return node._syntaxNode + case .extensionNode(let node): return node._syntaxNode + case .funcNode(let node): return node._syntaxNode + case .initialiseNode(let node): return node._syntaxNode + case .closureNode(let node): return node._syntaxNode + case .ifBlockNode(let node, _): return node._syntaxNode + case .elseBlockNode(let node, _): return node._syntaxNode + case .guardNode(let node): return node._syntaxNode + case .forLoopNode(let node): return node._syntaxNode + case .whileLoopNode(let node): return node._syntaxNode + case .subscriptNode(let node): return node._syntaxNode + case .accessorNode(let node): return node._syntaxNode + case .variableDeclNode(let node): return node._syntaxNode + case .switchCaseNode(let node): return node._syntaxNode + } + } + + public var type: ScopeType { + switch self { + case .sourceFileNode: return .sourceFileNode + case .classNode: return .classNode + case .structNode: return .structNode + case .enumNode: return .enumNode + case .enumCaseNode: return .enumCaseNode + case .extensionNode: return .extensionNode + case .funcNode: return .funcNode + case .initialiseNode: return .initialiseNode + case .closureNode: return .closureNode + case .ifBlockNode, .elseBlockNode: return .ifElseNode + case .guardNode: return .guardNode + case .forLoopNode: return .forLoopNode + case .whileLoopNode: return .whileLoopNode + case .subscriptNode: return .subscriptNode + case .accessorNode: return .accessorNode + case .variableDeclNode: return .variableDeclNode + case .switchCaseNode: return .switchCaseNode + } + } + + public var enclosingScopeNode: ScopeNode? { + return node.enclosingScopeNode + } + + public var description: String { + return "\(node)" + } +} + +public enum ScopeType: Equatable { + case sourceFileNode + case classNode + case structNode + case enumNode + case enumCaseNode + case extensionNode + case funcNode + case initialiseNode + case closureNode + case ifElseNode + case guardNode + case forLoopNode + case whileLoopNode + case subscriptNode + case accessorNode + case variableDeclNode + case switchCaseNode + + public var isTypeDecl: Bool { + return self == .classNode + || self == .structNode + || self == .enumNode + || self == .extensionNode + } + + public var isFunction: Bool { + return self == .funcNode + || self == .initialiseNode + || self == .closureNode + || self == .subscriptNode + } + +} + +open class Scope: Hashable, CustomStringConvertible { + public let scopeNode: ScopeNode + public let parent: Scope? + public private(set) var variables = Stack() + public private(set) var childScopes = [Scope]() + public var type: ScopeType { + return scopeNode.type + } + + public var childFunctions: [Function] { + return childScopes + .compactMap { scope in + if case let .funcNode(funcNode) = scope.scopeNode { + return funcNode + } + return nil + } + } + + public var childTypeDecls: [TypeDecl] { + return childScopes + .compactMap { $0.typeDecl } + } + + public var typeDecl: TypeDecl? { + switch scopeNode { + case .classNode(let node): + return TypeDecl(tokens: [node.identifier], inheritanceTypes: node.inheritanceClause?.inheritedTypeCollection.map { $0 }, scope: self) + case .structNode(let node): + return TypeDecl(tokens: [node.identifier], inheritanceTypes: node.inheritanceClause?.inheritedTypeCollection.map { $0 }, scope: self) + case .enumNode(let node): + return TypeDecl(tokens: [node.identifier], inheritanceTypes: node.inheritanceClause?.inheritedTypeCollection.map { $0 }, scope: self) + case .extensionNode(let node): + return TypeDecl(tokens: node.extendedType.tokens!, inheritanceTypes: node.inheritanceClause?.inheritedTypeCollection.map { $0 }, scope: self) + default: + return nil + } + } + + // Whether a variable can be used before it's declared. This is true for node that defines type, such as class, struct, enum,.... + // Otherwise if a variable is inside func, or closure, or normal block (if, guard,..), it must be declared before being used + public var canUseVariableOrFuncInAnyOrder: Bool { + return type == .classNode + || type == .structNode + || type == .enumNode + || type == .extensionNode + || type == .sourceFileNode + } + + public init(scopeNode: ScopeNode, parent: Scope?) { + self.parent = parent + self.scopeNode = scopeNode + parent?.childScopes.append(self) + + if let parent = parent { + assert(scopeNode.node.isDescendent(of: parent.scopeNode.node)) + } + } + + func addVariable(_ variable: Variable) { + assert(variable.scope == self) + variables.push(variable) + } + + func getVariable(_ node: IdentifierExprSyntax) -> Variable? { + let name = node.identifier.text + for variable in variables.filter({ $0.name == name }) { + // Special case: guard let `x` = x else { ... } + // or: let x = x.doSmth() + // Here x on the right cannot be resolved to x on the left + if case let .binding(_, valueNode) = variable.raw, + valueNode != nil && node._syntaxNode.isDescendent(of: valueNode!._syntaxNode) { + continue + } + + if variable.raw.token.isBefore(node) { + return variable + } else if !canUseVariableOrFuncInAnyOrder { + // Stop + break + } + } + + return nil + } + + func getFunctionWithSymbol(_ symbol: Symbol) -> [Function] { + return childFunctions.filter { function in + if function.identifier.isBefore(symbol.node) || canUseVariableOrFuncInAnyOrder { + return function.identifier.text == symbol.name + } + return false + } + } + + func getTypeDecl(name: String) -> [TypeDecl] { + return childTypeDecls + .filter { typeDecl in + return typeDecl.name == [name] + } + } + + open var description: String { + return "\(scopeNode)" + } +} + +// MARK: - Hashable +extension Scope { + public func hash(into hasher: inout Hasher) { + scopeNode.hash(into: &hasher) + } + + public static func == (_ lhs: Scope, _ rhs: Scope) -> Bool { + return lhs.scopeNode == rhs.scopeNode + } +} + +extension SyntaxProtocol { + public var enclosingScopeNode: ScopeNode? { + var parent = self.parent + while parent != nil { + if let scopeNode = ScopeNode.from(node: parent!) { + return scopeNode + } + parent = parent?.parent + } + return nil + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/SourceFileScope.swift b/MemoryLeak/Sources/SwiftLeakCheck/SourceFileScope.swift new file mode 100644 index 0000000..16d1f79 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/SourceFileScope.swift @@ -0,0 +1,16 @@ +// +// SourceFileScope.swift +// SwiftLeakCheck +// +// Created by Hoang Le Pham on 04/01/2020. +// + +import SwiftSyntax + +public class SourceFileScope: Scope { + let node: SourceFileSyntax + init(node: SourceFileSyntax, parent: Scope?) { + self.node = node + super.init(scopeNode: .sourceFileNode(node), parent: parent) + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/Stack.swift b/MemoryLeak/Sources/SwiftLeakCheck/Stack.swift new file mode 100644 index 0000000..e77de87 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/Stack.swift @@ -0,0 +1,58 @@ +// +// Stack.swift +// LeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 27/10/2019. +// + +public struct Stack { + private var items: [T] = [] + + public init() {} + + public init(items: [T]) { + self.items = items + } + + public mutating func push(_ item: T) { + items.append(item) + } + + @discardableResult + public mutating func pop() -> T? { + if !items.isEmpty { + return items.removeLast() + } else { + return nil + } + } + + public mutating func reset() { + items.removeAll() + } + + public func peek() -> T? { + return items.last + } +} + +extension Stack: Collection { + public var startIndex: Int { + return items.startIndex + } + + public var endIndex: Int { + return items.endIndex + } + + public func index(after i: Int) -> Int { + return items.index(after: i) + } + + public subscript(_ index: Int) -> T { + return items[items.count - index - 1] + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/SwiftSyntax+Extensions.swift b/MemoryLeak/Sources/SwiftLeakCheck/SwiftSyntax+Extensions.swift new file mode 100644 index 0000000..7fe5234 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/SwiftSyntax+Extensions.swift @@ -0,0 +1,263 @@ +// +// SwiftSyntax+Extensions.swift +// LeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 27/10/2019. +// + +import SwiftSyntax + +public extension SyntaxProtocol { + func isBefore(_ node: SyntaxProtocol) -> Bool { + return positionAfterSkippingLeadingTrivia.utf8Offset < node.positionAfterSkippingLeadingTrivia.utf8Offset + } + + func getEnclosingNode(_ type: T.Type) -> T? { + var parent = self.parent + while parent != nil && parent!.is(type) == false { + parent = parent?.parent + if parent == nil { return nil } + } + return parent?.as(type) + } + + func getEnclosingClosureNode() -> ClosureExprSyntax? { + return getEnclosingNode(ClosureExprSyntax.self) + } +} + +extension Syntax { + func isDescendent(of node: Syntax) -> Bool { + return hasAncestor { $0 == node } + } + + // TODO (Le): should we consider self as ancestor of self like this ? + func hasAncestor(_ predicate: (Syntax) -> Bool) -> Bool { + if predicate(self) { return true } + var parent = self.parent + while parent != nil { + if predicate(parent!) { + return true + } + parent = parent?.parent + } + return false + } +} + +public extension ExprSyntax { + /// Returns the enclosing function call to which the current expr is passed as argument. We also return the corresponding + /// argument of the current expr, or nil if current expr is trailing closure + func getEnclosingFunctionCallExpression() -> (function: FunctionCallExprSyntax, argument: FunctionCallArgumentSyntax?)? { + var function: FunctionCallExprSyntax? + var argument: FunctionCallArgumentSyntax? + + if let parent = parent?.as(FunctionCallArgumentSyntax.self) { // Normal function argument + assert(parent.parent?.is(FunctionCallArgumentListSyntax.self) == true) + function = parent.parent?.parent?.as(FunctionCallExprSyntax.self) + argument = parent + } else if let parent = parent?.as(FunctionCallExprSyntax.self), + self.is(ClosureExprSyntax.self), + parent.trailingClosure == self.as(ClosureExprSyntax.self) + { // Trailing closure + function = parent + } + + guard function != nil else { + // Not function call + return nil + } + + return (function: function!, argument: argument) + } + + func isCalledExpr() -> Bool { + if let parentNode = parent?.as(FunctionCallExprSyntax.self) { + if parentNode.calledExpression == self { + return true + } + } + + return false + } + + var rangeInfo: (left: ExprSyntax?, op: TokenSyntax, right: ExprSyntax?)? { + if let expr = self.as(SequenceExprSyntax.self) { + let elements = expr.elements + guard elements.count == 3, let op = elements[1].rangeOperator else { + return nil + } + return (left: elements[elements.startIndex], op: op, right: elements[elements.index(before: elements.endIndex)]) + } + + if let expr = self.as(PostfixUnaryExprSyntax.self) { + if expr.operatorToken.isRangeOperator { + return (left: nil, op: expr.operatorToken, right: expr.expression) + } else { + return nil + } + } + + if let expr = self.as(PrefixOperatorExprSyntax.self) { + assert(expr.operatorToken != nil) + if expr.operatorToken!.isRangeOperator { + return (left: expr.postfixExpression, op: expr.operatorToken!, right: nil) + } else { + return nil + } + } + + return nil + } + + private var rangeOperator: TokenSyntax? { + guard let op = self.as(BinaryOperatorExprSyntax.self) else { + return nil + } + return op.operatorToken.isRangeOperator ? op.operatorToken : nil + } +} + +public extension TokenSyntax { + var isRangeOperator: Bool { + return text == "..." || text == "..<" + } +} + +public extension TypeSyntax { + var isOptional: Bool { + return self.is(OptionalTypeSyntax.self) || self.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) + } + + var wrappedType: TypeSyntax { + if let optionalType = self.as(OptionalTypeSyntax.self) { + return optionalType.wrappedType + } + if let implicitOptionalType = self.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) { + return implicitOptionalType.wrappedType + } + return self + } + + var tokens: [TokenSyntax]? { + if self == wrappedType { + if let type = self.as(MemberTypeIdentifierSyntax.self) { + if let base = type.baseType.tokens { + return base + [type.name] + } + return nil + } + if let type = self.as(SimpleTypeIdentifierSyntax.self) { + return [type.name] + } + return nil + } + return wrappedType.tokens + } + + var name: [String]? { + return tokens?.map { $0.text } + } + + var isClosure: Bool { + return wrappedType.is(FunctionTypeSyntax.self) + || (wrappedType.as(AttributedTypeSyntax.self))?.baseType.isClosure == true + || (wrappedType.as(TupleTypeSyntax.self)).flatMap { $0.elements.count == 1 && $0.elements[$0.elements.startIndex].type.isClosure } == true + } +} + +/// `gurad let a = b, ... `: `let a = b` is a OptionalBindingConditionSyntax +public extension OptionalBindingConditionSyntax { + func isGuardCondition() -> Bool { + return parent?.is(ConditionElementSyntax.self) == true + && parent?.parent?.is(ConditionElementListSyntax.self) == true + && parent?.parent?.parent?.is(GuardStmtSyntax.self) == true + } +} + +public extension FunctionCallExprSyntax { + var base: ExprSyntax? { + return calledExpression.baseAndSymbol?.base + } + + var symbol: TokenSyntax? { + return calledExpression.baseAndSymbol?.symbol + } +} + +// Only used for the FunctionCallExprSyntax extension above +private extension ExprSyntax { + var baseAndSymbol: (base: ExprSyntax?, symbol: TokenSyntax)? { + // base.symbol() + if let memberAccessExpr = self.as(MemberAccessExprSyntax.self) { + return (base: memberAccessExpr.base, symbol: memberAccessExpr.name) + } + + // symbol() + if let identifier = self.as(IdentifierExprSyntax.self) { + return (base: nil, symbol: identifier.identifier) + } + + // expr?.() + if let optionalChainingExpr = self.as(OptionalChainingExprSyntax.self) { + return optionalChainingExpr.expression.baseAndSymbol + } + + // expr() + if let specializeExpr = self.as(SpecializeExprSyntax.self) { + return specializeExpr.expression.baseAndSymbol + } + + assert(false, "Unhandled case") + return nil + } +} + +public extension FunctionParameterSyntax { + var isEscaping: Bool { + guard let attributedType = type?.as(AttributedTypeSyntax.self) else { + return false + } + + return attributedType.attributes?.contains(where: { $0.as(AttributeSyntax.self)?.attributeName.text == "escaping" }) == true + } +} + +/// Convenient +extension ArrayElementListSyntax { + subscript(_ i: Int) -> ArrayElementSyntax { + let index = self.index(startIndex, offsetBy: i) + return self[index] + } +} + +extension FunctionCallArgumentListSyntax { + subscript(_ i: Int) -> FunctionCallArgumentSyntax { + let index = self.index(startIndex, offsetBy: i) + return self[index] + } +} + +extension ExprListSyntax { + subscript(_ i: Int) -> ExprSyntax { + let index = self.index(startIndex, offsetBy: i) + return self[index] + } +} + +extension PatternBindingListSyntax { + subscript(_ i: Int) -> PatternBindingSyntax { + let index = self.index(startIndex, offsetBy: i) + return self[index] + } +} + +extension TupleTypeElementListSyntax { + subscript(_ i: Int) -> TupleTypeElementSyntax { + let index = self.index(startIndex, offsetBy: i) + return self[index] + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/Symbol.swift b/MemoryLeak/Sources/SwiftLeakCheck/Symbol.swift new file mode 100644 index 0000000..80b6327 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/Symbol.swift @@ -0,0 +1,30 @@ +// +// Symbol.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 03/01/2020. +// + +import SwiftSyntax + +public enum Symbol: Hashable { + case token(TokenSyntax) + case identifier(IdentifierExprSyntax) + + var node: Syntax { + switch self { + case .token(let node): return node._syntaxNode + case .identifier(let node): return node._syntaxNode + } + } + + var name: String { + switch self { + case .token(let node): return node.text + case .identifier(let node): return node.identifier.text + } + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/SyntaxRetrieval.swift b/MemoryLeak/Sources/SwiftLeakCheck/SyntaxRetrieval.swift new file mode 100644 index 0000000..f1f95fc --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/SyntaxRetrieval.swift @@ -0,0 +1,20 @@ +// +// SyntaxRetrieval.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 09/12/2019. +// + +import SwiftSyntax +import SwiftSyntaxParser + +public enum SyntaxRetrieval { + public static func request(content: String) throws -> SourceFileSyntax { + return try SyntaxParser.parse( + source: content + ) + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/TypeDecl.swift b/MemoryLeak/Sources/SwiftLeakCheck/TypeDecl.swift new file mode 100644 index 0000000..5197f8c --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/TypeDecl.swift @@ -0,0 +1,32 @@ +// +// TypeDecl.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 04/01/2020. +// + +import SwiftSyntax + +// Class, struct, enum or extension +public struct TypeDecl: Equatable { + /// The name of the class/struct/enum/extension. + /// For class/struct/enum, it's 1 element + /// For extension, it could be multiple. Eg, extension X.Y.Z {...} + public let tokens: [TokenSyntax] + + public let inheritanceTypes: [InheritedTypeSyntax]? + + // Must be class/struct/enum/extension + public let scope: Scope + + public var name: [String] { + return tokens.map { $0.text } + } + + public var isExtension: Bool { + return scope.type == .extensionNode + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/TypeResolve.swift b/MemoryLeak/Sources/SwiftLeakCheck/TypeResolve.swift new file mode 100644 index 0000000..32e60c3 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/TypeResolve.swift @@ -0,0 +1,76 @@ +// +// TypeResolve.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 03/01/2020. +// + +import SwiftSyntax + +public indirect enum TypeResolve: Equatable { + case optional(base: TypeResolve) + case sequence(elementType: TypeResolve) + case dict + case tuple([TypeResolve]) + case name([String]) + case type(TypeDecl) + case unknown + + public var isOptional: Bool { + return self != self.wrappedType + } + + public var wrappedType: TypeResolve { + switch self { + case .optional(let base): + return base.wrappedType + case .sequence, + .dict, + .tuple, + .name, + .type, + .unknown: + return self + } + } + + public var name: [String]? { + switch self { + case .optional(let base): + return base.name + case .name(let tokens): + return tokens + case .type(let typeDecl): + return typeDecl.name + case .sequence, + .dict, + .tuple, + .unknown: + return nil + } + } + + public var sequenceElementType: TypeResolve { + switch self { + case .optional(let base): + return base.sequenceElementType + case .sequence(let elementType): + return elementType + case .dict, + .tuple, + .name, + .type, + .unknown: + return .unknown + } + } +} + +internal extension TypeResolve { + var isInt: Bool { + return name == ["Int"] + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/Utility.swift b/MemoryLeak/Sources/SwiftLeakCheck/Utility.swift new file mode 100644 index 0000000..8dc7e23 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/Utility.swift @@ -0,0 +1,20 @@ +// +// Utility.swift +// SwiftLeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 05/12/2019. +// + +import Foundation + +extension Collection where Index == Int { + subscript (safe index: Int) -> Element? { + if index < 0 || index >= count { + return nil + } + return self[index] + } +} diff --git a/MemoryLeak/Sources/SwiftLeakCheck/Variable.swift b/MemoryLeak/Sources/SwiftLeakCheck/Variable.swift new file mode 100644 index 0000000..c6a0c5f --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakCheck/Variable.swift @@ -0,0 +1,329 @@ +// +// Variable.swift +// LeakCheck +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// +// Created by Hoang Le Pham on 27/10/2019. +// + +import SwiftSyntax + +public enum RawVariable { + case capture(capturedNode: IdentifierExprSyntax) + case param(token: TokenSyntax) + case binding(token: TokenSyntax, valueNode: ExprSyntax?) + + var token: TokenSyntax { + switch self { + case .capture(let capturedNode): return capturedNode.identifier + case .binding(let token, _): return token + case .param(let token): return token + } + } +} + +indirect enum TypeInfo { + case exact(TypeSyntax) + case inferedFromExpr(ExprSyntax) + case inferedFromSequence(ExprSyntax) + case inferedFromTuple(tupleType: TypeInfo, index: Int) + case inferedFromClosure(ClosureExprSyntax, paramIndex: Int, paramCount: Int) +} + +// Represent a variable declaration. Eg +// var a = 1 +// let b = c // b is the Variable, c is not (c is a reference) +// block { [unowned x] in // x is a Variable +// func doSmth(a: Int, b: String) // a, b are Variables +public class Variable: Hashable, CustomStringConvertible { + public let raw: RawVariable + public var name: String { return raw.token.text } + let typeInfo: TypeInfo + public let memoryAttribute: MemoryAttribute? + public let scope: Scope + + var valueNode: ExprSyntax? { + switch raw { + case .binding(_, let valueNode): return valueNode + case .param, .capture: return nil + } + } + + var capturedNode: IdentifierExprSyntax? { + switch raw { + case .capture(let capturedNode): return capturedNode + case .binding, .param: return nil + } + } + + public var isStrong: Bool { + return memoryAttribute?.isStrong ?? true + } + + public var description: String { + return "\(raw)" + } + + private init(raw: RawVariable, + typeInfo: TypeInfo, + scope: Scope, + memoryAttribute: MemoryAttribute? = nil) { + self.raw = raw + self.typeInfo = typeInfo + self.scope = scope + self.memoryAttribute = memoryAttribute + } + + public static func from(_ node: ClosureCaptureItemSyntax, scope: Scope) -> Variable? { + assert(scope.scopeNode == node.enclosingScopeNode) + + guard let identifierExpr = node.expression.as(IdentifierExprSyntax.self) else { + // There're cases such as { [loggedInState.services] in ... }, which probably we don't need to care about + return nil + } + + let memoryAttribute: MemoryAttribute? = { + guard let specifier = node.specifier?.first else { + return nil + } + + assert(node.specifier!.count <= 1, "Unhandled case") + + guard let memoryAttribute = MemoryAttribute.from(specifier.text) else { + fatalError("Unhandled specifier \(specifier.text)") + } + return memoryAttribute + }() + + return Variable( + raw: .capture(capturedNode: identifierExpr), + typeInfo: .inferedFromExpr(ExprSyntax(identifierExpr)), + scope: scope, + memoryAttribute: memoryAttribute + ) + } + + public static func from(_ node: ClosureParamSyntax, scope: Scope) -> Variable { + guard let closure = node.getEnclosingClosureNode() else { + fatalError() + } + assert(scope.scopeNode == .closureNode(closure)) + + return Variable( + raw: .param(token: node.name), + typeInfo: .inferedFromClosure(closure, paramIndex: node.indexInParent, paramCount: node.parent!.children.count), + scope: scope + ) + } + + public static func from(_ node: FunctionParameterSyntax, scope: Scope) -> Variable { + assert(node.enclosingScopeNode == scope.scopeNode) + + guard let token = node.secondName ?? node.firstName else { + fatalError() + } + + assert(token.tokenKind != .wildcardKeyword, "Unhandled case") + assert(node.attributes == nil, "Unhandled case") + + guard let type = node.type else { + // Type is omited, must be used in closure signature + guard case let .closureNode(closureNode) = scope.scopeNode else { + fatalError("Only closure can omit the param type") + } + return Variable( + raw: .param(token: token), + typeInfo: .inferedFromClosure(closureNode, paramIndex: node.indexInParent, paramCount: node.parent!.children.count), + scope: scope + ) + } + + return Variable(raw: .param(token: token), typeInfo: .exact(type), scope: scope) + } + + public static func from(_ node: PatternBindingSyntax, scope: Scope) -> [Variable] { + guard let parent = node.parent?.as(PatternBindingListSyntax.self) else { + fatalError() + } + + assert(parent.parent?.is(VariableDeclSyntax.self) == true, "Unhandled case") + + func _typeFromNode(_ node: PatternBindingSyntax) -> TypeInfo { + // var a: Int + if let typeAnnotation = node.typeAnnotation { + return .exact(typeAnnotation.type) + } + // var a = value + if let value = node.initializer?.value { + return .inferedFromExpr(value) + } + // var a, b, .... = value + let indexOfNextNode = node.indexInParent + 1 + return _typeFromNode(parent[indexOfNextNode]) + } + + let type = _typeFromNode(node) + + if let identifier = node.pattern.as(IdentifierPatternSyntax.self) { + let memoryAttribute: MemoryAttribute? = { + if let modifier = node.parent?.parent?.as(VariableDeclSyntax.self)!.modifiers?.first { + return MemoryAttribute.from(modifier.name.text) + } + return nil + }() + + return [ + Variable( + raw: .binding(token: identifier.identifier, valueNode: node.initializer?.value), + typeInfo: type, + scope: scope, + memoryAttribute: memoryAttribute + ) + ] + } + + if let tuple = node.pattern.as(TuplePatternSyntax.self) { + return extractVariablesFromTuple(tuple, tupleType: type, tupleValue: node.initializer?.value, scope: scope) + } + + return [] + } + + public static func from(_ node: OptionalBindingConditionSyntax, scope: Scope) -> Variable? { + if let left = node.pattern.as(IdentifierPatternSyntax.self), let right = node.initializer?.value { + let type: TypeInfo + if let typeAnnotation = node.typeAnnotation { + type = .exact(typeAnnotation.type) + } else { + type = .inferedFromExpr(right) + } + + return Variable( + raw: .binding(token: left.identifier, valueNode: right), + typeInfo: type, + scope: scope, + memoryAttribute: .strong + ) + } + + return nil + } + + public static func from(_ node: ForInStmtSyntax, scope: Scope) -> [Variable] { + func _variablesFromPattern(_ pattern: PatternSyntax) -> [Variable] { + if let identifierPattern = pattern.as(IdentifierPatternSyntax.self) { + return [ + Variable( + raw: .binding(token: identifierPattern.identifier, valueNode: nil), + typeInfo: .inferedFromSequence(node.sequenceExpr), + scope: scope + ) + ] + } + + if let tuplePattern = pattern.as(TuplePatternSyntax.self) { + return extractVariablesFromTuple( + tuplePattern, + tupleType: .inferedFromSequence(node.sequenceExpr), + tupleValue: nil, + scope: scope + ) + } + + if pattern.is(WildcardPatternSyntax.self) { + return [] + } + + if let valueBindingPattern = pattern.as(ValueBindingPatternSyntax.self) { + return _variablesFromPattern(valueBindingPattern.valuePattern) + } + + assert(false, "Unhandled pattern in for statement: \(pattern)") + return [] + } + + return _variablesFromPattern(node.pattern) + } + + private static func extractVariablesFromTuple(_ tuplePattern: TuplePatternSyntax, + tupleType: TypeInfo, + tupleValue: ExprSyntax?, + scope: Scope) -> [Variable] { + return tuplePattern.elements.enumerated().flatMap { (index, element) -> [Variable] in + + let elementType: TypeInfo = .inferedFromTuple(tupleType: tupleType, index: index) + let elementValue: ExprSyntax? = { + if let tupleValue = tupleValue?.as(TupleExprSyntax.self) { + return tupleValue.elementList[index].expression + } + return nil + }() + + if let identifierPattern = element.pattern.as(IdentifierPatternSyntax.self) { + return [ + Variable( + raw: .binding(token: identifierPattern.identifier, valueNode: elementValue), + typeInfo: elementType, + scope: scope + ) + ] + } + + if let childTuplePattern = element.pattern.as(TuplePatternSyntax.self) { + return extractVariablesFromTuple( + childTuplePattern, + tupleType: elementType, + tupleValue: elementValue, + scope: scope + ) + } + + if element.pattern.is(WildcardPatternSyntax.self) { + return [] + } + + assertionFailure("I don't think there's any other kind") + return [] + } + } +} + +// MARK: - Hashable +public extension Variable { + static func == (_ lhs: Variable, _ rhs: Variable) -> Bool { + return lhs.raw.token == rhs.raw.token + } + + func hash(into hasher: inout Hasher) { + hasher.combine(raw.token) + } +} + +public enum MemoryAttribute: Hashable { + case weak + case unowned + case strong + + public var isStrong: Bool { + switch self { + case .weak, + .unowned: + return false + case .strong: + return true + } + } + + public static func from(_ text: String) -> MemoryAttribute? { + switch text { + case "weak": + return .weak + case "unowned": + return .unowned + default: + return nil + } + } +} diff --git a/MemoryLeak/Sources/SwiftLeakChecker/main.swift b/MemoryLeak/Sources/SwiftLeakChecker/main.swift new file mode 100644 index 0000000..65cd247 --- /dev/null +++ b/MemoryLeak/Sources/SwiftLeakChecker/main.swift @@ -0,0 +1,55 @@ +// +// Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. +// Use of this source code is governed by an MIT-style license that can be found in the LICENSE file +// + +import Foundation + +enum CommandLineError: Error, LocalizedError { + case missingFileName + + var errorDescription: String? { + switch self { + case .missingFileName: + return "Missing file or directory name" + } + } +} + +do { + let arguments = CommandLine.arguments + guard arguments.count > 1 else { + throw CommandLineError.missingFileName + } + + let path = arguments[1] + let url = URL(fileURLWithPath: path) + let dirScanner = DirectoryScanner(callback: { fileUrl, shouldStop in + do { + guard fileUrl.pathExtension == "swift" else { + return + } + + let leakDetector = GraphLeakDetector() + leakDetector.nonEscapeRules = [ + UIViewAnimationRule(), + UIViewControllerAnimationRule(), + DispatchQueueRule() + ] + CollectionRules.rules + + let startDate = Date() + let leaks = try leakDetector.detect(fileUrl) + let endDate = Date() + + leaks.forEach { leak in + print(leak.xcodeWarning(path: fileUrl.path)) + } + } catch {} + }) + + dirScanner.scan(url: url) + +} catch { + print("\(error.localizedDescription)") +} + diff --git a/MemoryLeak/main.swift b/MemoryLeak/main.swift deleted file mode 100644 index b448361..0000000 --- a/MemoryLeak/main.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// main.swift -// MemoryLeak -// -// Created by Dimo Abdelaziz on 04/08/2023. -// - -import Foundation - -print("Hello, World!") -