├── .circleci └── config.yml ├── .gitignore ├── .idea ├── MemoryLeakTestKit.iml ├── codeStyles │ └── Project.xml ├── misc.xml ├── modules.xml ├── runConfigurations │ ├── MemoryLeakTestKitTests.xml │ └── MemoryLeakTestKit_Package.xml ├── vcs.xml └── xcode.xml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── MemoryLeakTestKit.podspec ├── Package.swift ├── README.md ├── Sources └── MemoryLeakTestKit │ ├── Arrays.swift │ ├── CircularReferencePath.swift │ ├── IdentifiableReferencePath.swift │ ├── IdentifiableReferencePathComponent.swift │ ├── LeakedObject.swift │ ├── MemoryLeakDetector.swift │ ├── MemoryLeakReport.swift │ ├── NotNormalizedPathComponent.swift │ ├── ObjectTraverser.swift │ ├── PrefixedArray.generated.swift │ ├── PrefixedArray.swift.gyb │ ├── PrettyPrint.swift │ ├── Reference.swift │ ├── ReferenceID.swift │ ├── ReferencePath.swift │ ├── ReferencePathComponent.swift │ ├── ReferencePathNormalization.swift │ ├── Types.swift │ ├── Weak.swift │ └── WeakOrNotReference.swift ├── Tests ├── LinuxMain.swift └── MemoryLeakTestKitTests │ ├── MemoryLeakDetectorTests.swift │ ├── ReferenceIDTests.swift │ ├── ReferencePathNormalizationTests.swift │ └── XCTestManifests.swift └── bin ├── gyb ├── gyb ├── gyb.py └── gyb.pyc └── update /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: library/swift:4.2 6 | steps: 7 | - checkout 8 | - run: 9 | name: Test 10 | command: swift test 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /MemoryLeakTestKit.framework.zip 2 | .DS_Store 3 | /.build 4 | /Packages 5 | /*.xcodeproj 6 | 7 | # Created by https://www.gitignore.io/api/swift 8 | 9 | ### Swift ### 10 | # Xcode 11 | # 12 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 13 | 14 | ## Build generated 15 | build/ 16 | DerivedData/ 17 | 18 | ## Various settings 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | xcuserdata/ 28 | 29 | ## Other 30 | *.moved-aside 31 | *.xccheckout 32 | *.xcscmblueprint 33 | 34 | ## Obj-C/Swift specific 35 | *.hmap 36 | *.ipa 37 | *.dSYM.zip 38 | *.dSYM 39 | 40 | ## Playgrounds 41 | timeline.xctimeline 42 | playground.xcworkspace 43 | 44 | # Swift Package Manager 45 | # 46 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 47 | # Packages/ 48 | # Package.pins 49 | # Package.resolved 50 | .build/ 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # 58 | # Pods/ 59 | # 60 | # Add this line if you want to avoid checking in source code from the Xcode workspace 61 | # *.xcworkspace 62 | 63 | # Carthage 64 | # 65 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 66 | # Carthage/Checkouts 67 | 68 | Carthage/Build 69 | 70 | # fastlane 71 | # 72 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 73 | # screenshots whenever they are needed. 74 | # For more information about the recommended setup visit: 75 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 76 | 77 | fastlane/report.xml 78 | fastlane/Preview.html 79 | fastlane/screenshots/**/*.png 80 | fastlane/test_output 81 | 82 | # Code Injection 83 | # 84 | # After new code Injection tools there's a generated folder /iOSInjectionProject 85 | # https://github.com/johnno1962/injectionforxcode 86 | 87 | iOSInjectionProject/ 88 | 89 | 90 | # End of https://www.gitignore.io/api/swift 91 | 92 | # Created by https://www.gitignore.io/api/xcode 93 | 94 | ### Xcode ### 95 | # Xcode 96 | # 97 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 98 | 99 | ## User settings 100 | xcuserdata/ 101 | 102 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 103 | *.xcscmblueprint 104 | *.xccheckout 105 | 106 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 107 | build/ 108 | DerivedData/ 109 | *.moved-aside 110 | *.pbxuser 111 | !default.pbxuser 112 | *.mode1v3 113 | !default.mode1v3 114 | *.mode2v3 115 | !default.mode2v3 116 | *.perspectivev3 117 | !default.perspectivev3 118 | 119 | ### Xcode Patch ### 120 | *.xcodeproj/* 121 | !*.xcodeproj/project.pbxproj 122 | !*.xcodeproj/xcshareddata/ 123 | !*.xcworkspace/contents.xcworkspacedata 124 | /*.gcno 125 | 126 | 127 | # End of https://www.gitignore.io/api/xcode 128 | 129 | # Created by https://www.gitignore.io/api/appcode 130 | 131 | ### AppCode ### 132 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 133 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 134 | 135 | # User-specific stuff 136 | .idea/**/workspace.xml 137 | .idea/**/tasks.xml 138 | .idea/**/usage.statistics.xml 139 | .idea/**/dictionaries 140 | .idea/**/shelf 141 | 142 | # Generated files 143 | .idea/**/contentModel.xml 144 | 145 | # Sensitive or high-churn files 146 | .idea/**/dataSources/ 147 | .idea/**/dataSources.ids 148 | .idea/**/dataSources.local.xml 149 | .idea/**/sqlDataSources.xml 150 | .idea/**/dynamic.xml 151 | .idea/**/uiDesigner.xml 152 | .idea/**/dbnavigator.xml 153 | 154 | # Gradle 155 | .idea/**/gradle.xml 156 | .idea/**/libraries 157 | 158 | # Gradle and Maven with auto-import 159 | # When using Gradle or Maven with auto-import, you should exclude module files, 160 | # since they will be recreated, and may cause churn. Uncomment if using 161 | # auto-import. 162 | # .idea/modules.xml 163 | # .idea/*.iml 164 | # .idea/modules 165 | 166 | # CMake 167 | cmake-build-*/ 168 | 169 | # Mongo Explorer plugin 170 | .idea/**/mongoSettings.xml 171 | 172 | # File-based project format 173 | *.iws 174 | 175 | # IntelliJ 176 | out/ 177 | 178 | # mpeltonen/sbt-idea plugin 179 | .idea_modules/ 180 | 181 | # JIRA plugin 182 | atlassian-ide-plugin.xml 183 | 184 | # Cursive Clojure plugin 185 | .idea/replstate.xml 186 | 187 | # Crashlytics plugin (for Android Studio and IntelliJ) 188 | com_crashlytics_export_strings.xml 189 | crashlytics.properties 190 | crashlytics-build.properties 191 | fabric.properties 192 | 193 | # Editor-based Rest Client 194 | .idea/httpRequests 195 | 196 | # Android studio 3.1+ serialized cache file 197 | .idea/caches/build_file_checksums.ser 198 | 199 | ### AppCode Patch ### 200 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 201 | 202 | # *.iml 203 | # modules.xml 204 | # .idea/misc.xml 205 | # *.ipr 206 | 207 | # Sonarlint plugin 208 | .idea/sonarlint 209 | 210 | 211 | # End of https://www.gitignore.io/api/appcode 212 | 213 | # Created by https://www.gitignore.io/api/ruby 214 | 215 | ### Ruby ### 216 | *.gem 217 | *.rbc 218 | /.config 219 | /coverage/ 220 | /InstalledFiles 221 | /pkg/ 222 | /spec/reports/ 223 | /spec/examples.txt 224 | /test/tmp/ 225 | /test/version_tmp/ 226 | /tmp/ 227 | 228 | # Used by dotenv library to load environment variables. 229 | # .env 230 | 231 | ## Specific to RubyMotion: 232 | .dat* 233 | .repl_history 234 | build/ 235 | *.bridgesupport 236 | build-iPhoneOS/ 237 | build-iPhoneSimulator/ 238 | 239 | ## Specific to RubyMotion (use of CocoaPods): 240 | # 241 | # We recommend against adding the Pods directory to your .gitignore. However 242 | # you should judge for yourself, the pros and cons are mentioned at: 243 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 244 | # 245 | # vendor/Pods/ 246 | 247 | ## Documentation cache and generated files: 248 | /.yardoc/ 249 | /_yardoc/ 250 | /doc/ 251 | /rdoc/ 252 | 253 | ## Environment normalization: 254 | /.bundle/ 255 | /vendor/bundle 256 | /lib/bundler/man/ 257 | 258 | # for a library or gem, you might want to ignore these files since the code is 259 | # intended to run in multiple environments; otherwise, check them in: 260 | # Gemfile.lock 261 | # .ruby-version 262 | # .ruby-gemset 263 | 264 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 265 | .rvmrc 266 | 267 | 268 | # End of https://www.gitignore.io/api/ruby 269 | -------------------------------------------------------------------------------- /.idea/MemoryLeakTestKit.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/MemoryLeakTestKitTests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/runConfigurations/MemoryLeakTestKit_Package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/xcode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem "cocoapods" 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | activesupport (4.2.10) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.3) 11 | claide (1.0.2) 12 | cocoapods (1.5.3) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.2, < 2.0) 15 | cocoapods-core (= 1.5.3) 16 | cocoapods-deintegrate (>= 1.0.2, < 2.0) 17 | cocoapods-downloader (>= 1.2.0, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.3.0, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored2 (~> 3.1) 24 | escape (~> 0.0.4) 25 | fourflusher (~> 2.0.1) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.6.5) 28 | nap (~> 1.0) 29 | ruby-macho (~> 1.1) 30 | xcodeproj (>= 1.5.7, < 2.0) 31 | cocoapods-core (1.5.3) 32 | activesupport (>= 4.0.2, < 6) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | cocoapods-deintegrate (1.0.2) 36 | cocoapods-downloader (1.2.2) 37 | cocoapods-plugins (1.0.0) 38 | nap 39 | cocoapods-search (1.0.0) 40 | cocoapods-stats (1.0.0) 41 | cocoapods-trunk (1.3.1) 42 | nap (>= 0.8, < 2.0) 43 | netrc (~> 0.11) 44 | cocoapods-try (1.1.0) 45 | colored2 (3.1.2) 46 | concurrent-ruby (1.0.5) 47 | escape (0.0.4) 48 | fourflusher (2.0.1) 49 | fuzzy_match (2.0.4) 50 | gh_inspector (1.1.3) 51 | i18n (0.9.5) 52 | concurrent-ruby (~> 1.0) 53 | minitest (5.11.3) 54 | molinillo (0.6.6) 55 | nanaimo (0.2.6) 56 | nap (1.1.0) 57 | netrc (0.11.0) 58 | ruby-macho (1.3.1) 59 | thread_safe (0.3.6) 60 | tzinfo (1.2.5) 61 | thread_safe (~> 0.1) 62 | xcodeproj (1.7.0) 63 | CFPropertyList (>= 2.3.3, < 4.0) 64 | atomos (~> 0.1.3) 65 | claide (>= 1.0.2, < 2.0) 66 | colored2 (~> 3.1) 67 | nanaimo (~> 0.2.6) 68 | 69 | PLATFORMS 70 | ruby 71 | 72 | DEPENDENCIES 73 | cocoapods 74 | 75 | BUNDLED WITH 76 | 1.16.1 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Kuniwak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MemoryLeakTestKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "MemoryLeakTestKit" 3 | s.version = "0.0.1" 4 | s.summary = "A testing library to detect memory leaks for Swift." 5 | s.description = <<-DESC 6 | A testing library to detect memory leaks for Swift. This library can report many information such as leaked object's type/string representation/location/circular reference paths. 7 | DESC 8 | s.homepage = "https://github.com/Kuniwak/MemoryLeakTestKit" 9 | s.license = { :type => "MIT", :file => "LICENSE" } 10 | s.swift_version = "4.2" 11 | s.ios.deployment_target = "8.0" 12 | s.osx.deployment_target = "10.9" 13 | s.watchos.deployment_target = "2.0" 14 | s.tvos.deployment_target = "9.0" 15 | s.author = { "Kuniwak" => "orga.chem.job@gmail.com" } 16 | s.source = { :git => "https://github.com/Kuniwak/MemoryLeakTestKit.git", :tag => "#{s.version}" } 17 | s.source_files = "Sources/**/*.swift" 18 | s.exclude_files = "Sources/**/*.gyb" 19 | s.framework = "Foundation" 20 | end 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MemoryLeakTestKit", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "MemoryLeakTestKit", 12 | targets: ["MemoryLeakTestKit"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "MemoryLeakTestKit", 23 | dependencies: []), 24 | .testTarget( 25 | name: "MemoryLeakTestKitTests", 26 | dependencies: ["MemoryLeakTestKit"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MemoryLeakTestKit 2 | 3 | ![Swift 4.2 compatible](https://img.shields.io/badge/Swift%20version-4.2-green.svg) 4 | ![CocoaPods](https://img.shields.io/cocoapods/v/MemoryLeakTestKit.svg) 5 | ![Carthage](https://img.shields.io/badge/Carthage-compatible-green.svg) 6 | ![Swift Package Manager](https://img.shields.io/badge/SPM-compatible-green.svg) 7 | [![MIT license](https://img.shields.io/badge/lisence-MIT-yellow.svg)](https://github.com/Kuniwak/MemoryLeakTestKit/blob/master/LICENSE) 8 | 9 | A testing library to detect memory leaks for Swift. 10 | 11 | This library is under development. 12 | 13 | 14 | ## Supported Platforms 15 | 16 | | Platform | Build Status | 17 | |:---------|:-------------| 18 | | Linux | [![CircleCI](https://circleci.com/gh/Kuniwak/MemoryLeakTestKit/tree/master.svg?style=svg)](https://circleci.com/gh/Kuniwak/MemoryLeakTestKit/tree/master) | 19 | | iOS | [![Build Status](https://app.bitrise.io/app/457e68f44175b9c9/status.svg?token=AHKnQJD43MfDtDeh8-88Nw&branch=master)](https://app.bitrise.io/app/457e68f44175b9c9) | 20 | 21 | 22 | ## Usage 23 | 24 | ```swift 25 | import MemoryLeakTestKit 26 | 27 | 28 | let memoryLeaks = detectLeaks { 29 | // Create a instance 30 | return target 31 | } 32 | 33 | XCTAssertTrue( 34 | memoryLeaks.leakedObjects.isEmpty, 35 | memoryLeaks.prettyDescription 36 | ) 37 | ``` 38 | 39 | 40 | ## Example output 41 | 42 | ``` 43 | Summary: 44 | Found 2 leaked objects 45 | 46 | Leaked objects: 47 | 0: 48 | Description: Node 49 | Type: Node 50 | Location: (root).linkedNodes[0] 51 | Circular Paths: 52 | self.linkedNodes[1] === self 53 | 54 | 1: 55 | Description: Node 56 | Type: Node 57 | Location: (root) 58 | Circular Paths: 59 | self.linkedNodes[0].linkedNodes[0] === self 60 | ``` 61 | 62 | 63 | # License 64 | 65 | MIT 66 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/Arrays.swift: -------------------------------------------------------------------------------- 1 | public func intersperse( 2 | _ xs: CollectionT, 3 | _ y: T 4 | ) -> [T] where CollectionT.Element == T { 5 | guard let first = xs.first else { 6 | return [] 7 | } 8 | 9 | var result = [first] 10 | 11 | xs.dropFirst().forEach { x in 12 | result.append(y) 13 | result.append(x) 14 | } 15 | 16 | return result 17 | } -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/CircularReferencePath.swift: -------------------------------------------------------------------------------- 1 | public struct CircularReferencePath: Hashable { 2 | public let end: CircularPathEnd 3 | public let components: ArrayLongerThan1 4 | 5 | 6 | public var description: String { 7 | let accessors = self.components 8 | .map { $0.description } 9 | .joined(separator: "") 10 | 11 | return "self\(accessors) === self" 12 | } 13 | 14 | 15 | public init(end: CircularPathEnd, components: ArrayLongerThan1) { 16 | self.end = end 17 | self.components = components 18 | } 19 | 20 | 21 | public static func from(rootTypeName: TypeName, identifiablePath: IdentifiableReferencePath) -> Set { 22 | guard let idComponents = ArrayLongerThan1(identifiablePath.idComponents) else { 23 | return [] 24 | } 25 | 26 | let lastIdComponent = idComponents.last 27 | 28 | var result = Set() 29 | let idComponentsCount = idComponents.count 30 | 31 | if lastIdComponent.isIdentified(by: identifiablePath.rootID) { 32 | result.insert(CircularReferencePath( 33 | end: .root(rootTypeName), 34 | components: ReferencePathNormalization.normalize(idComponents.map { $0.noNormalizedComponent }) 35 | )) 36 | } 37 | 38 | result.formUnion( 39 | Set(idComponents 40 | .enumerated() 41 | .filter { indexAndIdComponent in 42 | let (_, idComponent) = indexAndIdComponent 43 | return idComponent == lastIdComponent 44 | } 45 | .compactMap { indexAndIdComponent -> ArrayLongerThan1? in 46 | let (circularStartIndex, _) = indexAndIdComponent 47 | let circularNextIndex = circularStartIndex + 1 48 | let circularIdComponents = idComponents[circularNextIndex..(circularIdComponents) 50 | } 51 | .map { circularComponents -> CircularReferencePath in 52 | return CircularReferencePath( 53 | end: .intermediate(lastIdComponent.typeName), 54 | components: ReferencePathNormalization.normalize(circularComponents.map { $0.noNormalizedComponent }) 55 | ) 56 | } 57 | ) 58 | ) 59 | 60 | return result 61 | } 62 | } 63 | 64 | 65 | 66 | public enum CircularPathEnd: Hashable { 67 | case root(TypeName) 68 | case intermediate(TypeName) 69 | } 70 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/IdentifiableReferencePath.swift: -------------------------------------------------------------------------------- 1 | public struct IdentifiableReferencePath: Hashable { 2 | public let rootID: ReferenceID 3 | public let idComponents: [IdentifiableReferencePathComponent] 4 | 5 | 6 | public var isRoot: Bool { 7 | return self.idComponents.isEmpty 8 | } 9 | 10 | 11 | public init(rootID: ReferenceID, idComponents: [IdentifiableReferencePathComponent]) { 12 | self.rootID = rootID 13 | self.idComponents = idComponents 14 | } 15 | 16 | 17 | public init( 18 | root: Any, componentAndValuePairs: Pairs 19 | ) where Pairs.Element == (component: NotNormalizedReferencePathComponent, value: Any) { 20 | self.init( 21 | rootID: ReferenceID(of: root), 22 | idComponents: componentAndValuePairs.map { pair in 23 | let (component: component, value: value) = pair 24 | return IdentifiableReferencePathComponent( 25 | id: ReferenceID(of: value), 26 | typeName: TypeName(of: value), 27 | noNormalizedComponent: component 28 | ) 29 | } 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/IdentifiableReferencePathComponent.swift: -------------------------------------------------------------------------------- 1 | public struct IdentifiableReferencePathComponent: Hashable { 2 | public let noNormalizedComponent: NotNormalizedReferencePathComponent 3 | public let typeName: TypeName 4 | private let id: ReferenceID 5 | 6 | 7 | public var hashValue: Int { 8 | return self.id.hashValue 9 | } 10 | 11 | 12 | public init(id: ReferenceID, typeName: TypeName, noNormalizedComponent: NotNormalizedReferencePathComponent) { 13 | self.id = id 14 | self.typeName = typeName 15 | self.noNormalizedComponent = noNormalizedComponent 16 | } 17 | 18 | 19 | public func isIdentified(by id: ReferenceID) -> Bool { 20 | return self.id == id 21 | } 22 | 23 | 24 | public static func ==(lhs: IdentifiableReferencePathComponent, rhs: IdentifiableReferencePathComponent) -> Bool { 25 | return lhs.id == rhs.id 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/LeakedObject.swift: -------------------------------------------------------------------------------- 1 | public struct LeakedObject: Hashable { 2 | public let objectDescription: String 3 | public let typeName: TypeName 4 | public let location: ReferencePath 5 | public let circularPaths: Set 6 | 7 | 8 | public init( 9 | objectDescription: String, 10 | typeName: TypeName, 11 | location: ReferencePath, 12 | circularPaths: Set 13 | ) { 14 | self.objectDescription = objectDescription 15 | self.typeName = typeName 16 | self.location = location 17 | self.circularPaths = circularPaths 18 | } 19 | 20 | 21 | public init?(reference: Reference) { 22 | guard !reference.isReleased else { 23 | return nil 24 | } 25 | 26 | self.init( 27 | objectDescription: reference.destinationObjectDescription, 28 | typeName: reference.destinationTypeName, 29 | location: ReferencePath(identifiablePath: reference.foundLocations.first), 30 | circularPaths: Set(reference.foundLocations.flatMap { identifiablePath in 31 | return CircularReferencePath.from( 32 | rootTypeName: reference.destinationTypeName, 33 | identifiablePath: identifiablePath 34 | ) 35 | }) 36 | ) 37 | } 38 | } 39 | 40 | 41 | 42 | extension LeakedObject: PrettyPrintable { 43 | public var descriptionLines: [IndentedLine] { 44 | let circularPathsDescription: [IndentedLine] 45 | 46 | if self.circularPaths.isEmpty { 47 | circularPathsDescription = lines(["No circular references found. There are 2 possible reasons:"]) 48 | + indent(lines([ 49 | "1. Some outer instances own it", 50 | "2. Anonymous instances that are on circular references end own it", 51 | ])) 52 | } 53 | else { 54 | circularPathsDescription = indent(lines(self.circularPaths.map { $0.description })) 55 | } 56 | 57 | return descriptionList([ 58 | (label: "Description", description: self.objectDescription), 59 | (label: "Type", description: self.typeName.text), 60 | (label: "Location", description: self.location.description), 61 | (label: "Circular Paths", description: "") 62 | ]) + circularPathsDescription 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/MemoryLeakDetector.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | 5 | public func detectLeaks(by build: () -> T) -> MemoryLeakReport { 6 | let releasedWeakMap = createWeakMap(from: build()) 7 | 8 | return MemoryLeakReport(references: releasedWeakMap.values) 9 | } 10 | 11 | 12 | 13 | public func detectLeaks(by build: (@escaping (T) -> Void) -> Void, _ callback: @escaping (MemoryLeakReport) -> Void) { 14 | build { target in 15 | let releasedWeakMap = createWeakMap(from: target) 16 | 17 | callback(MemoryLeakReport(references: releasedWeakMap.values)) 18 | } 19 | } 20 | 21 | 22 | 23 | public func createWeakMap(from target: T) -> [ReferenceID: Reference] { 24 | var result = [ 25 | ReferenceID(of: target): Reference( 26 | target, 27 | foundLocations: ArrayLongerThan1( 28 | prefix: IdentifiableReferencePath(root: target, componentAndValuePairs: []), [] 29 | ) 30 | ) 31 | ] 32 | 33 | traverseObjectWithPath( 34 | target, 35 | onEnter: { (_, value, path) in 36 | let childReferenceID = ReferenceID(of: value) 37 | 38 | if let visitedReference = result[childReferenceID] { 39 | visitedReference.found(location: .init( 40 | root: target, 41 | componentAndValuePairs: path 42 | )) 43 | } 44 | else { 45 | result[childReferenceID] = Reference( 46 | value, 47 | foundLocations: ArrayLongerThan1( 48 | prefix: IdentifiableReferencePath( 49 | root: target, 50 | componentAndValuePairs: path 51 | ), 52 | [] 53 | ) 54 | ) 55 | } 56 | }, 57 | onLeave: nil 58 | ) 59 | 60 | return result 61 | } 62 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/MemoryLeakReport.swift: -------------------------------------------------------------------------------- 1 | public struct MemoryLeakReport: Hashable { 2 | public let leakedObjects: Set 3 | 4 | 5 | public init(leakedObjects: Set) { 6 | self.leakedObjects = leakedObjects 7 | } 8 | 9 | 10 | public init(references: Seq) where Seq.Element == Reference { 11 | let leakedObjects = Set(references.compactMap { LeakedObject(reference: $0) }) 12 | self.init(leakedObjects: leakedObjects) 13 | } 14 | } 15 | 16 | 17 | 18 | extension MemoryLeakReport: PrettyPrintable { 19 | public var descriptionLines: [IndentedLine] { 20 | let leakedObjectsPart = sections(self.leakedObjects.map { $0.descriptionLines }) 21 | 22 | return sections([ 23 | (name: "Summary", body: lines([ 24 | "Found \(self.leakedObjects.count) leaked objects", 25 | ])), 26 | (name: "Leaked objects", body: leakedObjectsPart), 27 | ]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/NotNormalizedPathComponent.swift: -------------------------------------------------------------------------------- 1 | public enum NotNormalizedReferencePathComponent: Hashable { 2 | case label(String) 3 | case index(Int) 4 | case noLabel 5 | 6 | 7 | public init( 8 | isCollection: Bool, 9 | index: Int, 10 | label: String? 11 | ) { 12 | guard !isCollection else { 13 | self = .index(index) 14 | return 15 | } 16 | 17 | guard let label = label else { 18 | self = .noLabel 19 | return 20 | } 21 | 22 | self = .label(label) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/ObjectTraverser.swift: -------------------------------------------------------------------------------- 1 | public func traverseObjectWithPath( 2 | _ target: Any, 3 | onEnter: ((NotNormalizedReferencePathComponent, Any, [(component: NotNormalizedReferencePathComponent, value: Any)]) -> Void)?, 4 | onLeave: ((NotNormalizedReferencePathComponent, Any, [(component: NotNormalizedReferencePathComponent, value: Any)]) -> Void)? 5 | ) { 6 | var currentPath: [(component: NotNormalizedReferencePathComponent, value: Any)] = [] 7 | 8 | traverseObject( 9 | target, 10 | onEnter: { (component, value) in 11 | currentPath.append((component: component, value: value)) 12 | onEnter?(component, value, currentPath) 13 | }, 14 | onLeave: { (component, value) in 15 | onLeave?(component, value, currentPath) 16 | currentPath.removeLast() 17 | } 18 | ) 19 | } 20 | 21 | 22 | 23 | public func traverseObject( 24 | _ target: Any, 25 | onEnter: ((NotNormalizedReferencePathComponent, Any) -> Void)?, 26 | onLeave: ((NotNormalizedReferencePathComponent, Any) -> Void)? 27 | ) { 28 | var footprint = Set() 29 | 30 | traverseObjectRecursive( 31 | target, 32 | footprint: &footprint, 33 | onEnter: onEnter, 34 | onLeave: onLeave 35 | ) 36 | } 37 | 38 | 39 | 40 | private func traverseObjectRecursive( 41 | _ target: Any, 42 | footprint: inout Set, 43 | onEnter: ((NotNormalizedReferencePathComponent, Any) -> Void)?, 44 | onLeave: ((NotNormalizedReferencePathComponent, Any) -> Void)? 45 | ) { 46 | // NOTE: Avoid infinite recursions caused by circular references. 47 | let id = ReferenceID(of: target) 48 | if !footprint.contains(id) { 49 | footprint.insert(id) 50 | 51 | let mirror = Mirror(reflecting: target) 52 | mirror.children.enumerated().forEach { indexAndChild in 53 | let (index, (label: label, value: value)) = indexAndChild 54 | 55 | let component = NotNormalizedReferencePathComponent( 56 | isCollection: mirror.displayStyle == .collection, 57 | index: index, 58 | label: label 59 | ) 60 | 61 | onEnter?(component, value) 62 | 63 | traverseObjectRecursive( 64 | value, 65 | footprint: &footprint, 66 | onEnter: onEnter, 67 | onLeave: onLeave 68 | ) 69 | 70 | onLeave?(component, value) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/PrefixedArray.generated.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | public struct PrefixedArray { 4 | public let prefix: Element 5 | public let rest: RestElements 6 | 7 | 8 | public var first: Element { 9 | return self.prefix 10 | } 11 | 12 | 13 | public var firstAndRest: (Element, RestElements) { 14 | return (self.first, self.rest) 15 | } 16 | 17 | 18 | public init(prefix: Element, _ rest: RestElements) { 19 | self.prefix = prefix 20 | self.rest = rest 21 | } 22 | 23 | 24 | public func dropFirst() -> RestElements { 25 | return self.rest 26 | } 27 | } 28 | 29 | 30 | 31 | extension PrefixedArray: Equatable where Element: Equatable, RestElements: Equatable { 32 | public static func ==(lhs: PrefixedArray, rhs: PrefixedArray) -> Bool { 33 | return lhs.prefix == rhs.prefix 34 | && lhs.rest == rhs.rest 35 | } 36 | } 37 | 38 | 39 | 40 | extension PrefixedArray: Hashable where Element: Hashable, RestElements: Hashable { 41 | public func hash(into hasher: inout Hasher) { 42 | hasher.combine(self.prefix) 43 | hasher.combine(self.rest) 44 | } 45 | } 46 | 47 | 48 | 49 | extension PrefixedArray where RestElements == ArrayLongerThan0 { 50 | public func sequence() -> ArrayLongerThan1Sequence { 51 | return ArrayLongerThan1Sequence( 52 | prefix: self.prefix, 53 | rest: self.rest.sequence() 54 | ) 55 | } 56 | } 57 | extension PrefixedArray where RestElements == ArrayLongerThan1 { 58 | public func sequence() -> ArrayLongerThan2Sequence { 59 | return ArrayLongerThan2Sequence( 60 | prefix: self.prefix, 61 | rest: self.rest.sequence() 62 | ) 63 | } 64 | } 65 | 66 | 67 | 68 | public typealias ArrayLongerThan0Sequence = ArrayLongerThan0 69 | public typealias ArrayLongerThan1Sequence = PrefixedArraySequence> 70 | public typealias ArrayLongerThan2Sequence = PrefixedArraySequence> 71 | 72 | 73 | 74 | public struct PrefixedArraySequence: Sequence where RestElements.Element == E { 75 | public typealias Element = E 76 | 77 | 78 | public let prefix: Element 79 | public let rest: RestElements 80 | 81 | 82 | public init(prefix: Element, rest: RestElements) { 83 | self.prefix = prefix 84 | self.rest = rest 85 | } 86 | 87 | 88 | public func makeIterator() -> PrefixedArrayIterator { 89 | return PrefixedArrayIterator(iterate: self) 90 | } 91 | } 92 | 93 | 94 | 95 | public class PrefixedArrayIterator: IteratorProtocol { 96 | public typealias Element = RestElements.Element 97 | 98 | 99 | private var nextIterator: AnyIterator? = nil 100 | private let array: PrefixedArraySequence 101 | 102 | 103 | public init(iterate array: PrefixedArraySequence) { 104 | self.array = array 105 | } 106 | 107 | 108 | public func next() -> Element? { 109 | guard let nextIterator = self.nextIterator else { 110 | self.nextIterator = AnyIterator(self.array.rest.makeIterator()) 111 | return self.array.prefix 112 | } 113 | 114 | let result = nextIterator.next() 115 | self.nextIterator = nextIterator 116 | return result 117 | } 118 | } 119 | 120 | 121 | 122 | public struct PrefixedArrayEnd { 123 | private let array: AnyBidirectionalCollection 124 | 125 | 126 | public var startIndex: Int { 127 | return 0 128 | } 129 | 130 | 131 | public var endIndex: Int { 132 | return self.count - 1 133 | } 134 | 135 | 136 | public var count: Int { 137 | return self.array.count 138 | } 139 | 140 | 141 | public var first: Element? { 142 | return self.array.first 143 | } 144 | 145 | 146 | public var last: Element? { 147 | return self.array.last 148 | } 149 | 150 | 151 | public var firstAndRest: (Element, PrefixedArrayEnd)? { 152 | guard let first = self.first, let rest = self.dropFirst() else { 153 | return nil 154 | } 155 | 156 | return (first, rest) 157 | } 158 | 159 | 160 | public init( 161 | _ array: S 162 | ) where S.Element == Element { 163 | self.array = AnyBidirectionalCollection(array) 164 | } 165 | 166 | 167 | public init( 168 | prefix: Element, 169 | _ array: S 170 | ) where S.Element == Element { 171 | var newArray = Array(array) 172 | newArray.insert(prefix, at: 0) 173 | self.init(newArray) 174 | } 175 | 176 | 177 | public init( 178 | suffix: Element, 179 | _ array: S 180 | ) where S.Element == Element { 181 | guard let first = array.first else { 182 | self.init(prefix: suffix, []) 183 | return 184 | } 185 | 186 | var newArray = Array(array.dropFirst()) 187 | newArray.append(suffix) 188 | 189 | self.init(prefix: first, newArray) 190 | } 191 | 192 | 193 | public init(_ array: ArrayLongerThan0) { 194 | self = array 195 | } 196 | 197 | 198 | public init(_ array: ArrayLongerThan1) { 199 | self.init(array.relaxed()) 200 | } 201 | public init(_ array: ArrayLongerThan2) { 202 | self.init(array.relaxed()) 203 | } 204 | 205 | 206 | public init( 207 | suffix: Element, 208 | _ array: ArrayLongerThan0 209 | ) { 210 | self.init(suffix: suffix, array.relaxed()) 211 | } 212 | 213 | 214 | public subscript(index: Int) -> Element { 215 | let index = self.array.index(self.array.startIndex, offsetBy: index) 216 | return self.array[index] 217 | } 218 | 219 | 220 | public subscript(range: Range) -> ArrayLongerThan0 { 221 | let upperBound = self.array.index(self.array.startIndex, offsetBy: range.upperBound) 222 | let lowerBound = self.array.index(self.array.startIndex, offsetBy: range.lowerBound) 223 | 224 | return ArrayLongerThan0(self.array[lowerBound..(newArray) 232 | } 233 | 234 | 235 | public mutating func insert(contentsOf newElements: C, at i: Int) where C.Element == Element { 236 | var newArray = Array(self.array) 237 | newArray.insert(contentsOf: newElements, at: i) 238 | self = PrefixedArrayEnd(newArray) 239 | } 240 | 241 | 242 | public mutating func insert(contentsOf newElements: ArrayLongerThan0, at i: Int) { 243 | self.insert(contentsOf: newElements.relaxed(), at: i) 244 | } 245 | public mutating func insert(contentsOf newElements: ArrayLongerThan1, at i: Int) { 246 | self.insert(contentsOf: newElements.relaxed(), at: i) 247 | } 248 | public mutating func insert(contentsOf newElements: ArrayLongerThan2, at i: Int) { 249 | self.insert(contentsOf: newElements.relaxed(), at: i) 250 | } 251 | 252 | 253 | public mutating func append(_ newElement: Element) { 254 | var newArray = Array(self.array) 255 | newArray.append(newElement) 256 | self = PrefixedArrayEnd(newArray) 257 | } 258 | 259 | 260 | public mutating func append( 261 | contentsOf newElements: S 262 | ) where S.Element == Element { 263 | var newArray = Array(self.array) 264 | newArray.append(contentsOf: newElements) 265 | self = PrefixedArrayEnd(newArray) 266 | } 267 | 268 | 269 | public mutating func append(contentsOf newElements: ArrayLongerThan0) { 270 | self.append(contentsOf: newElements.relaxed()) 271 | } 272 | public mutating func append(contentsOf newElements: ArrayLongerThan1) { 273 | self.append(contentsOf: newElements.relaxed()) 274 | } 275 | public mutating func append(contentsOf newElements: ArrayLongerThan2) { 276 | self.append(contentsOf: newElements.relaxed()) 277 | } 278 | 279 | 280 | public func dropFirst() -> PrefixedArrayEnd? { 281 | guard !self.array.isEmpty else { 282 | return nil 283 | } 284 | 285 | return PrefixedArrayEnd(self.array.dropFirst()) 286 | } 287 | 288 | 289 | public func dropLast() -> AnyBidirectionalCollection? { 290 | guard !self.array.isEmpty else { 291 | return nil 292 | } 293 | 294 | return self.array.dropLast() 295 | } 296 | 297 | 298 | public func map(_ f: (Element) throws -> T) rethrows -> PrefixedArrayEnd { 299 | return PrefixedArrayEnd(try self.array.map(f)) 300 | } 301 | 302 | 303 | public func compactMap(_ f: (Element) throws -> T?) rethrows -> PrefixedArrayEnd { 304 | return PrefixedArrayEnd(try self.array.compactMap(f)) 305 | } 306 | 307 | 308 | public func flatMap(_ f: (Element) throws -> S) rethrows -> PrefixedArrayEnd where S.Element == T { 309 | return PrefixedArrayEnd(try self.array.flatMap(f)) 310 | } 311 | 312 | 313 | public func enumerated() -> PrefixedArrayEnd<(Int, Element)> { 314 | return PrefixedArrayEnd<(Int, Element)>(Array(self.array.enumerated())) 315 | } 316 | 317 | 318 | public func flatMap(_ f: (Element) throws -> ArrayLongerThan0) rethrows -> PrefixedArrayEnd { 319 | return try self.flatMap { try f($0).relaxed() } 320 | } 321 | public func flatMap(_ f: (Element) throws -> ArrayLongerThan1) rethrows -> PrefixedArrayEnd { 322 | return try self.flatMap { try f($0).relaxed() } 323 | } 324 | public func flatMap(_ f: (Element) throws -> ArrayLongerThan2) rethrows -> PrefixedArrayEnd { 325 | return try self.flatMap { try f($0).relaxed() } 326 | } 327 | 328 | 329 | public func filter(_ f: (Element) throws -> Bool) rethrows -> PrefixedArrayEnd { 330 | return PrefixedArrayEnd(try self.array.filter(f)) 331 | } 332 | 333 | 334 | public func relaxed() -> AnyBidirectionalCollection { 335 | return self.array 336 | } 337 | 338 | 339 | public func sequence() -> ArrayLongerThan0Sequence { 340 | return self 341 | } 342 | } 343 | 344 | 345 | 346 | extension PrefixedArrayEnd: Equatable where Element: Equatable { 347 | public static func ==(lhs: PrefixedArrayEnd, rhs: PrefixedArrayEnd) -> Bool { 348 | guard lhs.count == rhs.count else { return false } 349 | return zip(lhs.array, rhs.array).allSatisfy { $0.0 == $0.1 } 350 | } 351 | } 352 | 353 | 354 | 355 | extension PrefixedArrayEnd: Hashable where Element: Hashable { 356 | public func hash(into hasher: inout Hasher) { 357 | self.array.forEach { hasher.combine($0) } 358 | } 359 | } 360 | 361 | 362 | 363 | extension PrefixedArrayEnd: Sequence { 364 | public typealias Iterator = PrefixedArrayEndIterator 365 | 366 | 367 | public func makeIterator() -> PrefixedArrayEndIterator { 368 | return Iterator(self) 369 | } 370 | } 371 | 372 | 373 | 374 | public struct PrefixedArrayEndIterator: IteratorProtocol { 375 | private let array: PrefixedArrayEnd 376 | private var position: Int 377 | 378 | 379 | public init(_ array: PrefixedArrayEnd) { 380 | self.init(array, at: array.startIndex) 381 | } 382 | 383 | 384 | public init(_ array: PrefixedArrayEnd, at position: Int) { 385 | self.array = array 386 | self.position = position 387 | } 388 | 389 | 390 | public mutating func next() -> Element? { 391 | guard self.position <= self.array.endIndex else { 392 | return nil 393 | } 394 | 395 | let result = self.array[self.position] 396 | self.position += 1 397 | return result 398 | } 399 | } 400 | 401 | 402 | 403 | public typealias ArrayLongerThan0 = PrefixedArrayEnd 404 | public typealias ArrayLongerThan1 = PrefixedArray> 405 | public typealias ArrayLongerThan2 = PrefixedArray> 406 | 407 | 408 | 409 | extension PrefixedArray where RestElements == ArrayLongerThan0 { 410 | // ArrayLongerThan1 411 | public var count: Int { 412 | return self.rest.count + 1 413 | } 414 | 415 | 416 | // ArrayLongerThan1 417 | public var last: Element { 418 | guard let last = self.rest.last else { 419 | return self.first 420 | } 421 | return last 422 | } 423 | 424 | 425 | // ArrayLongerThan1 426 | public init?(_ array: C) where C.Element == Element { 427 | guard let first = array.first else { 428 | return nil 429 | } 430 | 431 | let restEnoughLength = PrefixedArrayEnd(array.dropFirst()) 432 | 433 | self.init(prefix: first, restEnoughLength) 434 | } 435 | 436 | 437 | // ArrayLongerThan1 438 | public init?(_ array: ArrayLongerThan0) { 439 | guard let (first, rest) = array.firstAndRest else { 440 | return nil 441 | } 442 | 443 | let restEnoughLength = ArrayLongerThan0(rest) 444 | 445 | self.init(prefix: first, restEnoughLength) 446 | } 447 | 448 | 449 | // ArrayLongerThan1 450 | public init(_ array: ArrayLongerThan1) { 451 | self = array 452 | } 453 | // ArrayLongerThan1 454 | public init(_ array: ArrayLongerThan2) { 455 | self.init(prefix: array.first, array.rest.relaxed()) 456 | } 457 | 458 | 459 | // ArrayLongerThan1 460 | public init(prefix: Element, _ array: ArrayLongerThan1) { 461 | self.init(prefix: prefix, array.relaxed()) 462 | } 463 | 464 | 465 | // ArrayLongerThan1 466 | public init(suffix: Element, _ array: ArrayLongerThan1) { 467 | self.init(suffix: suffix, array.relaxed()) 468 | } 469 | // ArrayLongerThan1 470 | public init(prefix: Element, _ array: ArrayLongerThan2) { 471 | self.init(prefix: prefix, array.relaxed()) 472 | } 473 | 474 | 475 | // ArrayLongerThan1 476 | public init(suffix: Element, _ array: ArrayLongerThan2) { 477 | self.init(suffix: suffix, array.relaxed()) 478 | } 479 | 480 | 481 | // ArrayLongerThan1 482 | public init(suffix: Element, _ array: ArrayLongerThan0) { 483 | guard let (first, rest) = array.firstAndRest else { 484 | self.init(prefix: suffix, PrefixedArrayEnd([])) 485 | return 486 | } 487 | 488 | self.init( 489 | prefix: first, 490 | ArrayLongerThan0(suffix: suffix, rest) 491 | ) 492 | } 493 | 494 | 495 | // ArrayLongerThan1 496 | public init(prefix: Element, _ array: C) where C.Element == Element { 497 | self.init(prefix: prefix, PrefixedArrayEnd(array)) 498 | } 499 | 500 | 501 | // ArrayLongerThan1 502 | public init(suffix: Element, _ array: C) where C.Element == Element { 503 | guard let first = array.first else { 504 | self.init(prefix: suffix, PrefixedArrayEnd([])) 505 | return 506 | } 507 | 508 | var newRest = Array(array.dropFirst()) 509 | newRest.append(suffix) 510 | 511 | self.init(prefix: first, PrefixedArrayEnd(newRest)) 512 | } 513 | 514 | 515 | // ArrayLongerThan1 516 | public subscript(index: Int) -> Element { 517 | guard index != 0 else { 518 | return self.first 519 | } 520 | return self.rest[index - 1] 521 | } 522 | 523 | 524 | // ArrayLongerThan1 525 | public subscript(range: Range) -> ArrayLongerThan0 { 526 | return self.relaxed()[range] 527 | } 528 | 529 | 530 | // ArrayLongerThan1 531 | public mutating func insert(_ newElement: Element, at i: Int) { 532 | guard i > 0 else { 533 | self = ArrayLongerThan1( 534 | prefix: newElement, 535 | self.relaxed() 536 | ) 537 | return 538 | } 539 | 540 | var newRest = self.rest 541 | newRest.insert(newElement, at: i - 1) 542 | 543 | self = ArrayLongerThan1( 544 | prefix: self.prefix, 545 | newRest 546 | ) 547 | } 548 | 549 | 550 | // ArrayLongerThan1 551 | public mutating func insert(contentsOf newElements: ArrayLongerThan0, at i: Int) { 552 | // TODO: Check the standard behavior to handle negative values. 553 | guard i > 0 else { 554 | guard let (first, rest) = newElements.firstAndRest else { return } 555 | 556 | self = ArrayLongerThan1( 557 | prefix: first, 558 | rest + ArrayLongerThan1(self) // NOTE: Avoid to use exceeded length types. 559 | ) 560 | return 561 | } 562 | 563 | var newRest = self.rest 564 | newRest.insert(contentsOf: newElements, at: i - 1) 565 | 566 | self = ArrayLongerThan1( 567 | prefix: self.prefix, 568 | newRest 569 | ) 570 | } 571 | // ArrayLongerThan1 572 | public mutating func insert(contentsOf newElements: ArrayLongerThan1, at i: Int) { 573 | // TODO: Check the standard behavior to handle negative values. 574 | guard i > 0 else { 575 | let (first, rest) = newElements.firstAndRest 576 | 577 | self = ArrayLongerThan1( 578 | prefix: first, 579 | rest + ArrayLongerThan0(self) // NOTE: Avoid to use exceeded length types. 580 | ) 581 | return 582 | } 583 | 584 | var newRest = self.rest 585 | newRest.insert(contentsOf: newElements, at: i - 1) 586 | 587 | self = ArrayLongerThan1( 588 | prefix: self.prefix, 589 | newRest 590 | ) 591 | } 592 | // ArrayLongerThan1 593 | public mutating func insert(contentsOf newElements: ArrayLongerThan2, at i: Int) { 594 | // TODO: Check the standard behavior to handle negative values. 595 | guard i > 0 else { 596 | let (first, rest) = newElements.firstAndRest 597 | 598 | self = ArrayLongerThan1( 599 | prefix: first, 600 | rest + ArrayLongerThan0(self) // NOTE: Avoid to use exceeded length types. 601 | ) 602 | return 603 | } 604 | 605 | var newRest = self.rest 606 | newRest.insert(contentsOf: newElements, at: i - 1) 607 | 608 | self = ArrayLongerThan1( 609 | prefix: self.prefix, 610 | newRest 611 | ) 612 | } 613 | 614 | 615 | // ArrayLongerThan1 616 | public mutating func append(_ newElement: Element) { 617 | var newRest = self.rest 618 | newRest.append(newElement) 619 | self = ArrayLongerThan1( 620 | prefix: self.prefix, 621 | newRest 622 | ) 623 | } 624 | 625 | 626 | // ArrayLongerThan1 627 | public mutating func append(contentsOf newElements: ArrayLongerThan0) { 628 | var newRest = self.rest 629 | newRest.append(contentsOf: newElements) 630 | self = ArrayLongerThan1( 631 | prefix: self.prefix, 632 | newRest 633 | ) 634 | } 635 | // ArrayLongerThan1 636 | public mutating func append(contentsOf newElements: ArrayLongerThan1) { 637 | var newRest = self.rest 638 | newRest.append(contentsOf: newElements) 639 | self = ArrayLongerThan1( 640 | prefix: self.prefix, 641 | newRest 642 | ) 643 | } 644 | // ArrayLongerThan1 645 | public mutating func append(contentsOf newElements: ArrayLongerThan2) { 646 | var newRest = self.rest 647 | newRest.append(contentsOf: newElements) 648 | self = ArrayLongerThan1( 649 | prefix: self.prefix, 650 | newRest 651 | ) 652 | } 653 | 654 | 655 | // ArrayLongerThan1 656 | public func dropLast() -> PrefixedArrayEnd { 657 | guard let rest = self.rest.dropLast() else { 658 | return PrefixedArrayEnd([]) 659 | } 660 | 661 | return PrefixedArrayEnd( 662 | prefix: self.first, 663 | rest 664 | ) 665 | } 666 | 667 | 668 | // ArrayLongerThan1 669 | public func map(_ f: (Element) throws -> T) rethrows -> ArrayLongerThan1 { 670 | return ArrayLongerThan1( 671 | prefix: try f(self.first), 672 | try self.rest.map(f) 673 | ) 674 | } 675 | 676 | 677 | // ArrayLongerThan1 678 | public func compactMap(_ f: (Element) throws -> T?) rethrows -> PrefixedArrayEnd { 679 | return try self.relaxed().compactMap(f) 680 | } 681 | 682 | 683 | // ArrayLongerThan1 684 | public func filter(_ f: (Element) throws -> Bool) rethrows -> PrefixedArrayEnd { 685 | return try self.relaxed().filter(f) 686 | } 687 | 688 | 689 | // ArrayLongerThan1 690 | public func flatMap(_ f: (Element) throws -> S) rethrows -> PrefixedArrayEnd where S.Element == T { 691 | return try self.relaxed().flatMap(f) 692 | } 693 | 694 | 695 | // ArrayLongerThan1 696 | public func enumerated() -> ArrayLongerThan1<(Int, Element)> { 697 | return ArrayLongerThan1<(Int, Element)>( 698 | prefix: (0, self.first), 699 | self.rest.enumerated().map { ($0.0 + 1, $0.1) } 700 | ) 701 | } 702 | 703 | 704 | // ArrayLongerThan1 705 | public func relaxed() -> ArrayLongerThan0 { 706 | return ArrayLongerThan0( 707 | prefix: self.prefix, 708 | self.rest.relaxed() 709 | ) 710 | } 711 | } 712 | extension PrefixedArray where RestElements == ArrayLongerThan1 { 713 | // ArrayLongerThan2 714 | public var count: Int { 715 | return self.rest.count + 1 716 | } 717 | 718 | 719 | // ArrayLongerThan2 720 | public var last: Element { 721 | return self.rest.last 722 | } 723 | 724 | 725 | // ArrayLongerThan2 726 | public init?(_ array: C) where C.Element == Element { 727 | guard let first = array.first else { 728 | return nil 729 | } 730 | 731 | guard let restEnoughLength = ArrayLongerThan1(array.dropFirst()) else { return nil } 732 | 733 | self.init(prefix: first, restEnoughLength) 734 | } 735 | 736 | 737 | // ArrayLongerThan2 738 | public init?(_ array: ArrayLongerThan0) { 739 | guard let (first, rest) = array.firstAndRest else { 740 | return nil 741 | } 742 | 743 | guard let restEnoughLength = ArrayLongerThan1(rest) else { 744 | return nil 745 | } 746 | 747 | self.init(prefix: first, restEnoughLength) 748 | } 749 | 750 | 751 | // ArrayLongerThan2 752 | public init?(_ array: ArrayLongerThan1) { 753 | guard let restEnoughLength = ArrayLongerThan1(array.rest) else { 754 | return nil 755 | } 756 | 757 | self.init(prefix: array.first, restEnoughLength) 758 | } 759 | // ArrayLongerThan2 760 | public init(_ array: ArrayLongerThan2) { 761 | self = array 762 | } 763 | 764 | 765 | // ArrayLongerThan2 766 | public init(prefix: Element, _ array: ArrayLongerThan2) { 767 | self.init(prefix: prefix, array.relaxed()) 768 | } 769 | 770 | 771 | // ArrayLongerThan2 772 | public init(suffix: Element, _ array: ArrayLongerThan2) { 773 | self.init(suffix: suffix, array.relaxed()) 774 | } 775 | 776 | 777 | public init(suffix: Element, _ array: ArrayLongerThan1) { 778 | self.init( 779 | prefix: array.first, 780 | ArrayLongerThan1(suffix: suffix, array.rest) 781 | ) 782 | } 783 | 784 | 785 | // ArrayLongerThan2 786 | public init?(prefix: Element, _ array: C) where C.Element == Element { 787 | guard let rest = ArrayLongerThan1(array) else { 788 | return nil 789 | } 790 | 791 | self.init(prefix: prefix, rest) 792 | } 793 | 794 | 795 | // ArrayLongerThan2 796 | public init?(suffix: Element, _ array: C) where C.Element == Element { 797 | guard let head = ArrayLongerThan1(array) else { 798 | return nil 799 | } 800 | 801 | self.init(suffix: suffix, head) 802 | } 803 | 804 | 805 | // ArrayLongerThan2 806 | public subscript(index: Int) -> Element { 807 | guard index != 0 else { 808 | return self.first 809 | } 810 | return self.rest[index - 1] 811 | } 812 | 813 | 814 | // ArrayLongerThan2 815 | public subscript(range: Range) -> ArrayLongerThan0 { 816 | return self.relaxed()[range] 817 | } 818 | 819 | 820 | // ArrayLongerThan2 821 | public mutating func insert(_ newElement: Element, at i: Int) { 822 | guard i > 0 else { 823 | self = ArrayLongerThan2( 824 | prefix: newElement, 825 | self.relaxed() 826 | ) 827 | return 828 | } 829 | 830 | var newRest = self.rest 831 | newRest.insert(newElement, at: i - 1) 832 | 833 | self = ArrayLongerThan2( 834 | prefix: self.prefix, 835 | newRest 836 | ) 837 | } 838 | 839 | 840 | // ArrayLongerThan2 841 | public mutating func insert(contentsOf newElements: ArrayLongerThan0, at i: Int) { 842 | // TODO: Check the standard behavior to handle negative values. 843 | guard i > 0 else { 844 | guard let (first, rest) = newElements.firstAndRest else { return } 845 | 846 | self = ArrayLongerThan2( 847 | prefix: first, 848 | rest + ArrayLongerThan2(self) // NOTE: Avoid to use exceeded length types. 849 | ) 850 | return 851 | } 852 | 853 | var newRest = self.rest 854 | newRest.insert(contentsOf: newElements, at: i - 1) 855 | 856 | self = ArrayLongerThan2( 857 | prefix: self.prefix, 858 | newRest 859 | ) 860 | } 861 | // ArrayLongerThan2 862 | public mutating func insert(contentsOf newElements: ArrayLongerThan1, at i: Int) { 863 | // TODO: Check the standard behavior to handle negative values. 864 | guard i > 0 else { 865 | let (first, rest) = newElements.firstAndRest 866 | 867 | self = ArrayLongerThan2( 868 | prefix: first, 869 | rest + ArrayLongerThan1(self) // NOTE: Avoid to use exceeded length types. 870 | ) 871 | return 872 | } 873 | 874 | var newRest = self.rest 875 | newRest.insert(contentsOf: newElements, at: i - 1) 876 | 877 | self = ArrayLongerThan2( 878 | prefix: self.prefix, 879 | newRest 880 | ) 881 | } 882 | // ArrayLongerThan2 883 | public mutating func insert(contentsOf newElements: ArrayLongerThan2, at i: Int) { 884 | // TODO: Check the standard behavior to handle negative values. 885 | guard i > 0 else { 886 | let (first, rest) = newElements.firstAndRest 887 | 888 | self = ArrayLongerThan2( 889 | prefix: first, 890 | rest + ArrayLongerThan0(self) // NOTE: Avoid to use exceeded length types. 891 | ) 892 | return 893 | } 894 | 895 | var newRest = self.rest 896 | newRest.insert(contentsOf: newElements, at: i - 1) 897 | 898 | self = ArrayLongerThan2( 899 | prefix: self.prefix, 900 | newRest 901 | ) 902 | } 903 | 904 | 905 | // ArrayLongerThan2 906 | public mutating func append(_ newElement: Element) { 907 | var newRest = self.rest 908 | newRest.append(newElement) 909 | self = ArrayLongerThan2( 910 | prefix: self.prefix, 911 | newRest 912 | ) 913 | } 914 | 915 | 916 | // ArrayLongerThan2 917 | public mutating func append(contentsOf newElements: ArrayLongerThan0) { 918 | var newRest = self.rest 919 | newRest.append(contentsOf: newElements) 920 | self = ArrayLongerThan2( 921 | prefix: self.prefix, 922 | newRest 923 | ) 924 | } 925 | // ArrayLongerThan2 926 | public mutating func append(contentsOf newElements: ArrayLongerThan1) { 927 | var newRest = self.rest 928 | newRest.append(contentsOf: newElements) 929 | self = ArrayLongerThan2( 930 | prefix: self.prefix, 931 | newRest 932 | ) 933 | } 934 | // ArrayLongerThan2 935 | public mutating func append(contentsOf newElements: ArrayLongerThan2) { 936 | var newRest = self.rest 937 | newRest.append(contentsOf: newElements) 938 | self = ArrayLongerThan2( 939 | prefix: self.prefix, 940 | newRest 941 | ) 942 | } 943 | 944 | 945 | // ArrayLongerThan2 946 | public func dropLast() -> ArrayLongerThan1 { 947 | return ArrayLongerThan1( 948 | prefix: self.first, 949 | rest.dropLast() 950 | ) 951 | } 952 | 953 | 954 | // ArrayLongerThan2 955 | public func map(_ f: (Element) throws -> T) rethrows -> ArrayLongerThan2 { 956 | return ArrayLongerThan2( 957 | prefix: try f(self.first), 958 | try self.rest.map(f) 959 | ) 960 | } 961 | 962 | 963 | // ArrayLongerThan2 964 | public func compactMap(_ f: (Element) throws -> T?) rethrows -> PrefixedArrayEnd { 965 | return try self.relaxed().compactMap(f) 966 | } 967 | 968 | 969 | // ArrayLongerThan2 970 | public func filter(_ f: (Element) throws -> Bool) rethrows -> PrefixedArrayEnd { 971 | return try self.relaxed().filter(f) 972 | } 973 | 974 | 975 | // ArrayLongerThan2 976 | public func flatMap(_ f: (Element) throws -> S) rethrows -> PrefixedArrayEnd where S.Element == T { 977 | return try self.relaxed().flatMap(f) 978 | } 979 | 980 | 981 | // ArrayLongerThan2 982 | public func enumerated() -> ArrayLongerThan2<(Int, Element)> { 983 | return ArrayLongerThan2<(Int, Element)>( 984 | prefix: (0, self.first), 985 | self.rest.enumerated().map { ($0.0 + 1, $0.1) } 986 | ) 987 | } 988 | 989 | 990 | // ArrayLongerThan2 991 | public func relaxed() -> ArrayLongerThan1 { 992 | return ArrayLongerThan1( 993 | prefix: self.prefix, 994 | self.rest.relaxed() 995 | ) 996 | } 997 | } 998 | 999 | 1000 | public func +(lhs: ArrayLongerThan0, rhs: PrefixedArrayEnd) -> ArrayLongerThan0 { 1001 | var result = lhs 1002 | result.append(contentsOf: rhs) 1003 | return result 1004 | } 1005 | public func +(lhs: ArrayLongerThan0, rhs: ArrayLongerThan1) -> ArrayLongerThan1 { 1006 | return ArrayLongerThan1(suffix: rhs.last, lhs + rhs.dropLast()) 1007 | } 1008 | public func +(lhs: ArrayLongerThan0, rhs: ArrayLongerThan2) -> ArrayLongerThan2 { 1009 | return ArrayLongerThan2(suffix: rhs.last, lhs + rhs.dropLast()) 1010 | } 1011 | public func +(lhs: ArrayLongerThan1, rhs: PrefixedArrayEnd) -> ArrayLongerThan1 { 1012 | var result = lhs 1013 | result.append(contentsOf: rhs) 1014 | return result 1015 | } 1016 | public func +(lhs: ArrayLongerThan1, rhs: ArrayLongerThan1) -> ArrayLongerThan2 { 1017 | return ArrayLongerThan2(suffix: rhs.last, lhs + rhs.dropLast()) 1018 | } 1019 | public func +(lhs: ArrayLongerThan2, rhs: PrefixedArrayEnd) -> ArrayLongerThan2 { 1020 | var result = lhs 1021 | result.append(contentsOf: rhs) 1022 | return result 1023 | } 1024 | 1025 | 1026 | 1027 | extension PrefixedArrayEnd where Element: Sequence { 1028 | public func joined() -> PrefixedArrayEnd { 1029 | return PrefixedArrayEnd(Array(self.array.joined())) 1030 | } 1031 | } 1032 | 1033 | 1034 | 1035 | extension PrefixedArray where Element: Sequence, RestElements == ArrayLongerThan0 { 1036 | public func joined() -> PrefixedArrayEnd { 1037 | return self.relaxed().joined() 1038 | } 1039 | } 1040 | extension PrefixedArray where Element: Sequence, RestElements == ArrayLongerThan1 { 1041 | public func joined() -> PrefixedArrayEnd { 1042 | return self.relaxed().joined() 1043 | } 1044 | } 1045 | 1046 | 1047 | 1048 | extension PrefixedArrayEnd where Element == String { 1049 | public func joined(separator: String) -> String { 1050 | return self.array.joined(separator: separator) 1051 | } 1052 | } 1053 | 1054 | 1055 | 1056 | extension PrefixedArray where Element == String, RestElements == ArrayLongerThan0 { 1057 | public func joined(separator: String) -> String { 1058 | return self.relaxed().joined(separator: separator) 1059 | } 1060 | } 1061 | extension PrefixedArray where Element == String, RestElements == ArrayLongerThan1 { 1062 | public func joined(separator: String) -> String { 1063 | return self.relaxed().joined(separator: separator) 1064 | } 1065 | } 1066 | 1067 | 1068 | func zip(_ a: ArrayLongerThan0, _ b: ArrayLongerThan0) -> ArrayLongerThan0<(A, B)> { 1069 | return ArrayLongerThan0<(A, B)>(Array(zip(a.relaxed(), b.relaxed()))) 1070 | } 1071 | 1072 | 1073 | func zip(_ a: ArrayLongerThan1, _ b: ArrayLongerThan1) -> ArrayLongerThan1<(A, B)> { 1074 | return ArrayLongerThan1<(A, B)>( 1075 | prefix: (a.first, b.first), 1076 | zip(a.rest, b.rest) 1077 | ) 1078 | } 1079 | func zip(_ a: ArrayLongerThan2, _ b: ArrayLongerThan2) -> ArrayLongerThan2<(A, B)> { 1080 | return ArrayLongerThan2<(A, B)>( 1081 | prefix: (a.first, b.first), 1082 | zip(a.rest, b.rest) 1083 | ) 1084 | } 1085 | 1086 | 1087 | 1088 | func zip(_ a: A, _ b: B, _ c: C) -> [(A.Element, B.Element, C.Element)] { 1089 | return zip(zip(a, b), c).map { ($0.0.0, $0.0.1, $0.1) } 1090 | } 1091 | 1092 | 1093 | 1094 | func zip(_ a: ArrayLongerThan0, _ b: ArrayLongerThan0, _ c: ArrayLongerThan0) -> ArrayLongerThan0<(A, B, C)> { 1095 | return ArrayLongerThan0<(A, B, C)>(Array(zip(a.relaxed(), b.relaxed(), c.relaxed()))) 1096 | } 1097 | 1098 | 1099 | 1100 | func zip(_ a: ArrayLongerThan1, _ b: ArrayLongerThan1, _ c: ArrayLongerThan1) -> ArrayLongerThan1<(A, B, C)> { 1101 | return ArrayLongerThan1<(A, B, C)>( 1102 | prefix: (a.first, b.first, c.first), 1103 | zip(a.rest, b.rest, c.rest) 1104 | ) 1105 | } 1106 | func zip(_ a: ArrayLongerThan2, _ b: ArrayLongerThan2, _ c: ArrayLongerThan2) -> ArrayLongerThan2<(A, B, C)> { 1107 | return ArrayLongerThan2<(A, B, C)>( 1108 | prefix: (a.first, b.first, c.first), 1109 | zip(a.rest, b.rest, c.rest) 1110 | ) 1111 | } 1112 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/PrefixedArray.swift.gyb: -------------------------------------------------------------------------------- 1 | %{ 2 | number_of_generated = 3 3 | }% 4 | 5 | 6 | public struct PrefixedArray { 7 | public let prefix: Element 8 | public let rest: RestElements 9 | 10 | 11 | public var first: Element { 12 | return self.prefix 13 | } 14 | 15 | 16 | public var firstAndRest: (Element, RestElements) { 17 | return (self.first, self.rest) 18 | } 19 | 20 | 21 | public init(prefix: Element, _ rest: RestElements) { 22 | self.prefix = prefix 23 | self.rest = rest 24 | } 25 | 26 | 27 | public func dropFirst() -> RestElements { 28 | return self.rest 29 | } 30 | } 31 | 32 | 33 | 34 | extension PrefixedArray: Equatable where Element: Equatable, RestElements: Equatable { 35 | public static func ==(lhs: PrefixedArray, rhs: PrefixedArray) -> Bool { 36 | return lhs.prefix == rhs.prefix 37 | && lhs.rest == rhs.rest 38 | } 39 | } 40 | 41 | 42 | 43 | extension PrefixedArray: Hashable where Element: Hashable, RestElements: Hashable { 44 | public func hash(into hasher: inout Hasher) { 45 | hasher.combine(self.prefix) 46 | hasher.combine(self.rest) 47 | } 48 | } 49 | 50 | 51 | 52 | % for i in range(1, number_of_generated): 53 | extension PrefixedArray where RestElements == ArrayLongerThan${i - 1} { 54 | public func sequence() -> ArrayLongerThan${i}Sequence { 55 | return ArrayLongerThan${i}Sequence( 56 | prefix: self.prefix, 57 | rest: self.rest.sequence() 58 | ) 59 | } 60 | } 61 | % end 62 | 63 | 64 | 65 | public typealias ArrayLongerThan0Sequence = ArrayLongerThan0 66 | % for i in range(1, number_of_generated): 67 | public typealias ArrayLongerThan${i}Sequence = PrefixedArraySequence> 68 | % end 69 | 70 | 71 | 72 | public struct PrefixedArraySequence: Sequence where RestElements.Element == E { 73 | public typealias Element = E 74 | 75 | 76 | public let prefix: Element 77 | public let rest: RestElements 78 | 79 | 80 | public init(prefix: Element, rest: RestElements) { 81 | self.prefix = prefix 82 | self.rest = rest 83 | } 84 | 85 | 86 | public func makeIterator() -> PrefixedArrayIterator { 87 | return PrefixedArrayIterator(iterate: self) 88 | } 89 | } 90 | 91 | 92 | 93 | public class PrefixedArrayIterator: IteratorProtocol { 94 | public typealias Element = RestElements.Element 95 | 96 | 97 | private var nextIterator: AnyIterator? = nil 98 | private let array: PrefixedArraySequence 99 | 100 | 101 | public init(iterate array: PrefixedArraySequence) { 102 | self.array = array 103 | } 104 | 105 | 106 | public func next() -> Element? { 107 | guard let nextIterator = self.nextIterator else { 108 | self.nextIterator = AnyIterator(self.array.rest.makeIterator()) 109 | return self.array.prefix 110 | } 111 | 112 | let result = nextIterator.next() 113 | self.nextIterator = nextIterator 114 | return result 115 | } 116 | } 117 | 118 | 119 | 120 | public struct PrefixedArrayEnd { 121 | private let array: AnyBidirectionalCollection 122 | 123 | 124 | public var startIndex: Int { 125 | return 0 126 | } 127 | 128 | 129 | public var endIndex: Int { 130 | return self.count - 1 131 | } 132 | 133 | 134 | public var count: Int { 135 | return self.array.count 136 | } 137 | 138 | 139 | public var first: Element? { 140 | return self.array.first 141 | } 142 | 143 | 144 | public var last: Element? { 145 | return self.array.last 146 | } 147 | 148 | 149 | public var firstAndRest: (Element, PrefixedArrayEnd)? { 150 | guard let first = self.first, let rest = self.dropFirst() else { 151 | return nil 152 | } 153 | 154 | return (first, rest) 155 | } 156 | 157 | 158 | public init( 159 | _ array: S 160 | ) where S.Element == Element { 161 | self.array = AnyBidirectionalCollection(array) 162 | } 163 | 164 | 165 | public init( 166 | prefix: Element, 167 | _ array: S 168 | ) where S.Element == Element { 169 | var newArray = Array(array) 170 | newArray.insert(prefix, at: 0) 171 | self.init(newArray) 172 | } 173 | 174 | 175 | public init( 176 | suffix: Element, 177 | _ array: S 178 | ) where S.Element == Element { 179 | guard let first = array.first else { 180 | self.init(prefix: suffix, []) 181 | return 182 | } 183 | 184 | var newArray = Array(array.dropFirst()) 185 | newArray.append(suffix) 186 | 187 | self.init(prefix: first, newArray) 188 | } 189 | 190 | 191 | public init(_ array: ArrayLongerThan0) { 192 | self = array 193 | } 194 | 195 | 196 | % for j in range(1, number_of_generated): 197 | public init(_ array: ArrayLongerThan${j}) { 198 | self.init(array.relaxed()) 199 | } 200 | % end 201 | 202 | 203 | public init( 204 | suffix: Element, 205 | _ array: ArrayLongerThan0 206 | ) { 207 | self.init(suffix: suffix, array.relaxed()) 208 | } 209 | 210 | 211 | public subscript(index: Int) -> Element { 212 | let index = self.array.index(self.array.startIndex, offsetBy: index) 213 | return self.array[index] 214 | } 215 | 216 | 217 | public subscript(range: Range) -> ArrayLongerThan0 { 218 | let upperBound = self.array.index(self.array.startIndex, offsetBy: range.upperBound) 219 | let lowerBound = self.array.index(self.array.startIndex, offsetBy: range.lowerBound) 220 | 221 | return ArrayLongerThan0(self.array[lowerBound..(newArray) 229 | } 230 | 231 | 232 | public mutating func insert(contentsOf newElements: C, at i: Int) where C.Element == Element { 233 | var newArray = Array(self.array) 234 | newArray.insert(contentsOf: newElements, at: i) 235 | self = PrefixedArrayEnd(newArray) 236 | } 237 | 238 | 239 | % for i in range(0, number_of_generated): 240 | public mutating func insert(contentsOf newElements: ArrayLongerThan${i}, at i: Int) { 241 | self.insert(contentsOf: newElements.relaxed(), at: i) 242 | } 243 | % end 244 | 245 | 246 | public mutating func append(_ newElement: Element) { 247 | var newArray = Array(self.array) 248 | newArray.append(newElement) 249 | self = PrefixedArrayEnd(newArray) 250 | } 251 | 252 | 253 | public mutating func append( 254 | contentsOf newElements: S 255 | ) where S.Element == Element { 256 | var newArray = Array(self.array) 257 | newArray.append(contentsOf: newElements) 258 | self = PrefixedArrayEnd(newArray) 259 | } 260 | 261 | 262 | % for i in range(0, number_of_generated): 263 | public mutating func append(contentsOf newElements: ArrayLongerThan${i}) { 264 | self.append(contentsOf: newElements.relaxed()) 265 | } 266 | % end 267 | 268 | 269 | public func dropFirst() -> PrefixedArrayEnd? { 270 | guard !self.array.isEmpty else { 271 | return nil 272 | } 273 | 274 | return PrefixedArrayEnd(self.array.dropFirst()) 275 | } 276 | 277 | 278 | public func dropLast() -> AnyBidirectionalCollection? { 279 | guard !self.array.isEmpty else { 280 | return nil 281 | } 282 | 283 | return self.array.dropLast() 284 | } 285 | 286 | 287 | public func map(_ f: (Element) throws -> T) rethrows -> PrefixedArrayEnd { 288 | return PrefixedArrayEnd(try self.array.map(f)) 289 | } 290 | 291 | 292 | public func compactMap(_ f: (Element) throws -> T?) rethrows -> PrefixedArrayEnd { 293 | return PrefixedArrayEnd(try self.array.compactMap(f)) 294 | } 295 | 296 | 297 | public func flatMap(_ f: (Element) throws -> S) rethrows -> PrefixedArrayEnd where S.Element == T { 298 | return PrefixedArrayEnd(try self.array.flatMap(f)) 299 | } 300 | 301 | 302 | public func enumerated() -> PrefixedArrayEnd<(Int, Element)> { 303 | return PrefixedArrayEnd<(Int, Element)>(Array(self.array.enumerated())) 304 | } 305 | 306 | 307 | % for i in range(0, number_of_generated): 308 | public func flatMap(_ f: (Element) throws -> ArrayLongerThan${i}) rethrows -> PrefixedArrayEnd { 309 | return try self.flatMap { try f($0).relaxed() } 310 | } 311 | % end 312 | 313 | 314 | public func filter(_ f: (Element) throws -> Bool) rethrows -> PrefixedArrayEnd { 315 | return PrefixedArrayEnd(try self.array.filter(f)) 316 | } 317 | 318 | 319 | public func relaxed() -> AnyBidirectionalCollection { 320 | return self.array 321 | } 322 | 323 | 324 | public func sequence() -> ArrayLongerThan0Sequence { 325 | return self 326 | } 327 | } 328 | 329 | 330 | 331 | extension PrefixedArrayEnd: Equatable where Element: Equatable { 332 | public static func ==(lhs: PrefixedArrayEnd, rhs: PrefixedArrayEnd) -> Bool { 333 | guard lhs.count == rhs.count else { return false } 334 | return zip(lhs.array, rhs.array).allSatisfy { $0.0 == $0.1 } 335 | } 336 | } 337 | 338 | 339 | 340 | extension PrefixedArrayEnd: Hashable where Element: Hashable { 341 | public func hash(into hasher: inout Hasher) { 342 | self.array.forEach { hasher.combine($0) } 343 | } 344 | } 345 | 346 | 347 | 348 | extension PrefixedArrayEnd: Sequence { 349 | public typealias Iterator = PrefixedArrayEndIterator 350 | 351 | 352 | public func makeIterator() -> PrefixedArrayEndIterator { 353 | return Iterator(self) 354 | } 355 | } 356 | 357 | 358 | 359 | public struct PrefixedArrayEndIterator: IteratorProtocol { 360 | private let array: PrefixedArrayEnd 361 | private var position: Int 362 | 363 | 364 | public init(_ array: PrefixedArrayEnd) { 365 | self.init(array, at: array.startIndex) 366 | } 367 | 368 | 369 | public init(_ array: PrefixedArrayEnd, at position: Int) { 370 | self.array = array 371 | self.position = position 372 | } 373 | 374 | 375 | public mutating func next() -> Element? { 376 | guard self.position <= self.array.endIndex else { 377 | return nil 378 | } 379 | 380 | let result = self.array[self.position] 381 | self.position += 1 382 | return result 383 | } 384 | } 385 | 386 | 387 | 388 | public typealias ArrayLongerThan0 = PrefixedArrayEnd 389 | % for i in range(1, number_of_generated): 390 | public typealias ArrayLongerThan${i} = PrefixedArray> 391 | % end 392 | 393 | 394 | 395 | % for i in range(1, number_of_generated): 396 | extension PrefixedArray where RestElements == ArrayLongerThan${i - 1} { 397 | // ArrayLongerThan${i} 398 | public var count: Int { 399 | return self.rest.count + 1 400 | } 401 | 402 | 403 | // ArrayLongerThan${i} 404 | public var last: Element { 405 | % if i > 1: 406 | return self.rest.last 407 | % else: 408 | guard let last = self.rest.last else { 409 | return self.first 410 | } 411 | return last 412 | % end 413 | } 414 | 415 | 416 | // ArrayLongerThan${i} 417 | public init?(_ array: C) where C.Element == Element { 418 | guard let first = array.first else { 419 | return nil 420 | } 421 | 422 | % if i == 1: 423 | let restEnoughLength = PrefixedArrayEnd(array.dropFirst()) 424 | % else: 425 | guard let restEnoughLength = ArrayLongerThan${i - 1}(array.dropFirst()) else { return nil } 426 | % end 427 | 428 | self.init(prefix: first, restEnoughLength) 429 | } 430 | 431 | 432 | // ArrayLongerThan${i} 433 | public init?(_ array: ArrayLongerThan0) { 434 | guard let (first, rest) = array.firstAndRest else { 435 | return nil 436 | } 437 | 438 | % if i == 1: 439 | let restEnoughLength = ArrayLongerThan0(rest) 440 | % else: 441 | guard let restEnoughLength = ArrayLongerThan${i - 1}(rest) else { 442 | return nil 443 | } 444 | % end 445 | 446 | self.init(prefix: first, restEnoughLength) 447 | } 448 | 449 | 450 | % for j in range(1, number_of_generated): 451 | % if j < i: 452 | // ArrayLongerThan${i} 453 | public init?(_ array: ArrayLongerThan${j}) { 454 | % if i == 1: 455 | let restEnoughLength = ArrayLongerThan0(array.rest) 456 | % else: 457 | guard let restEnoughLength = ArrayLongerThan${i - 1}(array.rest) else { 458 | return nil 459 | } 460 | % end 461 | 462 | self.init(prefix: array.first, restEnoughLength) 463 | } 464 | % elif j == i: 465 | // ArrayLongerThan${i} 466 | public init(_ array: ArrayLongerThan${j}) { 467 | self = array 468 | } 469 | % else: 470 | // ArrayLongerThan${i} 471 | public init(_ array: ArrayLongerThan${j}) { 472 | self.init(prefix: array.first, array.rest.relaxed()) 473 | } 474 | % end 475 | % end 476 | 477 | 478 | % for j in range(i, number_of_generated): 479 | // ArrayLongerThan${i} 480 | public init(prefix: Element, _ array: ArrayLongerThan${j}) { 481 | self.init(prefix: prefix, array.relaxed()) 482 | } 483 | 484 | 485 | // ArrayLongerThan${i} 486 | public init(suffix: Element, _ array: ArrayLongerThan${j}) { 487 | self.init(suffix: suffix, array.relaxed()) 488 | } 489 | % end 490 | 491 | 492 | % if i == 1: 493 | // ArrayLongerThan${i} 494 | public init(suffix: Element, _ array: ArrayLongerThan0) { 495 | guard let (first, rest) = array.firstAndRest else { 496 | self.init(prefix: suffix, PrefixedArrayEnd([])) 497 | return 498 | } 499 | 500 | self.init( 501 | prefix: first, 502 | ArrayLongerThan${i - 1}(suffix: suffix, rest) 503 | ) 504 | } 505 | % else: 506 | public init(suffix: Element, _ array: ArrayLongerThan${i - 1}) { 507 | self.init( 508 | prefix: array.first, 509 | ArrayLongerThan${i - 1}(suffix: suffix, array.rest) 510 | ) 511 | } 512 | % end 513 | 514 | 515 | % if i == 1: 516 | // ArrayLongerThan${i} 517 | public init(prefix: Element, _ array: C) where C.Element == Element { 518 | self.init(prefix: prefix, PrefixedArrayEnd(array)) 519 | } 520 | % else: 521 | // ArrayLongerThan${i} 522 | public init?(prefix: Element, _ array: C) where C.Element == Element { 523 | guard let rest = ArrayLongerThan${i - 1}(array) else { 524 | return nil 525 | } 526 | 527 | self.init(prefix: prefix, rest) 528 | } 529 | % end 530 | 531 | 532 | % if i == 1: 533 | // ArrayLongerThan${i} 534 | public init(suffix: Element, _ array: C) where C.Element == Element { 535 | guard let first = array.first else { 536 | self.init(prefix: suffix, PrefixedArrayEnd([])) 537 | return 538 | } 539 | 540 | var newRest = Array(array.dropFirst()) 541 | newRest.append(suffix) 542 | 543 | self.init(prefix: first, PrefixedArrayEnd(newRest)) 544 | } 545 | % else: 546 | // ArrayLongerThan${i} 547 | public init?(suffix: Element, _ array: C) where C.Element == Element { 548 | guard let head = ArrayLongerThan${i - 1}(array) else { 549 | return nil 550 | } 551 | 552 | self.init(suffix: suffix, head) 553 | } 554 | % end 555 | 556 | 557 | // ArrayLongerThan${i} 558 | public subscript(index: Int) -> Element { 559 | guard index != 0 else { 560 | return self.first 561 | } 562 | return self.rest[index - 1] 563 | } 564 | 565 | 566 | // ArrayLongerThan${i} 567 | public subscript(range: Range) -> ArrayLongerThan0 { 568 | return self.relaxed()[range] 569 | } 570 | 571 | 572 | // ArrayLongerThan${i} 573 | public mutating func insert(_ newElement: Element, at i: Int) { 574 | guard i > 0 else { 575 | self = ArrayLongerThan${i}( 576 | prefix: newElement, 577 | self.relaxed() 578 | ) 579 | return 580 | } 581 | 582 | var newRest = self.rest 583 | newRest.insert(newElement, at: i - 1) 584 | 585 | self = ArrayLongerThan${i}( 586 | prefix: self.prefix, 587 | newRest 588 | ) 589 | } 590 | 591 | 592 | % for j in range(0, number_of_generated): 593 | // ArrayLongerThan${i} 594 | public mutating func insert(contentsOf newElements: ArrayLongerThan${j}, at i: Int) { 595 | // TODO: Check the standard behavior to handle negative values. 596 | guard i > 0 else { 597 | % if j == 0: 598 | guard let (first, rest) = newElements.firstAndRest else { return } 599 | % else: 600 | let (first, rest) = newElements.firstAndRest 601 | %end 602 | 603 | self = ArrayLongerThan${i}( 604 | prefix: first, 605 | rest + ArrayLongerThan${max(i - j, 0)}(self) // NOTE: Avoid to use exceeded length types. 606 | ) 607 | return 608 | } 609 | 610 | var newRest = self.rest 611 | newRest.insert(contentsOf: newElements, at: i - 1) 612 | 613 | self = ArrayLongerThan${i}( 614 | prefix: self.prefix, 615 | newRest 616 | ) 617 | } 618 | % end 619 | 620 | 621 | // ArrayLongerThan${i} 622 | public mutating func append(_ newElement: Element) { 623 | var newRest = self.rest 624 | newRest.append(newElement) 625 | self = ArrayLongerThan${i}( 626 | prefix: self.prefix, 627 | newRest 628 | ) 629 | } 630 | 631 | 632 | % for j in range(0, number_of_generated): 633 | // ArrayLongerThan${i} 634 | public mutating func append(contentsOf newElements: ArrayLongerThan${j}) { 635 | var newRest = self.rest 636 | newRest.append(contentsOf: newElements) 637 | self = ArrayLongerThan${i}( 638 | prefix: self.prefix, 639 | newRest 640 | ) 641 | } 642 | % end 643 | 644 | 645 | % if i == 1: 646 | // ArrayLongerThan${i} 647 | public func dropLast() -> PrefixedArrayEnd { 648 | guard let rest = self.rest.dropLast() else { 649 | return PrefixedArrayEnd([]) 650 | } 651 | 652 | return PrefixedArrayEnd( 653 | prefix: self.first, 654 | rest 655 | ) 656 | } 657 | % else: 658 | // ArrayLongerThan${i} 659 | public func dropLast() -> ArrayLongerThan${i - 1} { 660 | return ArrayLongerThan${i - 1}( 661 | prefix: self.first, 662 | rest.dropLast() 663 | ) 664 | } 665 | % end 666 | 667 | 668 | // ArrayLongerThan${i} 669 | public func map(_ f: (Element) throws -> T) rethrows -> ArrayLongerThan${i} { 670 | return ArrayLongerThan${i}( 671 | prefix: try f(self.first), 672 | try self.rest.map(f) 673 | ) 674 | } 675 | 676 | 677 | // ArrayLongerThan${i} 678 | public func compactMap(_ f: (Element) throws -> T?) rethrows -> PrefixedArrayEnd { 679 | return try self.relaxed().compactMap(f) 680 | } 681 | 682 | 683 | // ArrayLongerThan${i} 684 | public func filter(_ f: (Element) throws -> Bool) rethrows -> PrefixedArrayEnd { 685 | return try self.relaxed().filter(f) 686 | } 687 | 688 | 689 | // ArrayLongerThan${i} 690 | public func flatMap(_ f: (Element) throws -> S) rethrows -> PrefixedArrayEnd where S.Element == T { 691 | return try self.relaxed().flatMap(f) 692 | } 693 | 694 | 695 | // ArrayLongerThan${i} 696 | public func enumerated() -> ArrayLongerThan${i}<(Int, Element)> { 697 | return ArrayLongerThan${i}<(Int, Element)>( 698 | prefix: (0, self.first), 699 | self.rest.enumerated().map { ($0.0 + 1, $0.1) } 700 | ) 701 | } 702 | 703 | 704 | // ArrayLongerThan${i} 705 | public func relaxed() -> ArrayLongerThan${i - 1} { 706 | return ArrayLongerThan${i - 1}( 707 | prefix: self.prefix, 708 | self.rest.relaxed() 709 | ) 710 | } 711 | } 712 | % end 713 | 714 | 715 | % for l in range(0, number_of_generated): 716 | public func +(lhs: ArrayLongerThan${l}, rhs: PrefixedArrayEnd) -> ArrayLongerThan${l} { 717 | var result = lhs 718 | result.append(contentsOf: rhs) 719 | return result 720 | } 721 | % for r in range(1, number_of_generated): 722 | % if l + r in range(0, number_of_generated): 723 | public func +(lhs: ArrayLongerThan${l}, rhs: ArrayLongerThan${r}) -> ArrayLongerThan${l + r} { 724 | return ArrayLongerThan${l + r}(suffix: rhs.last, lhs + rhs.dropLast()) 725 | } 726 | % end 727 | % end 728 | % end 729 | 730 | 731 | 732 | extension PrefixedArrayEnd where Element: Sequence { 733 | public func joined() -> PrefixedArrayEnd { 734 | return PrefixedArrayEnd(Array(self.array.joined())) 735 | } 736 | } 737 | 738 | 739 | 740 | % for i in range(1, number_of_generated): 741 | extension PrefixedArray where Element: Sequence, RestElements == ArrayLongerThan${i - 1} { 742 | public func joined() -> PrefixedArrayEnd { 743 | return self.relaxed().joined() 744 | } 745 | } 746 | % end 747 | 748 | 749 | 750 | extension PrefixedArrayEnd where Element == String { 751 | public func joined(separator: String) -> String { 752 | return self.array.joined(separator: separator) 753 | } 754 | } 755 | 756 | 757 | 758 | % for i in range(1, number_of_generated): 759 | extension PrefixedArray where Element == String, RestElements == ArrayLongerThan${i - 1} { 760 | public func joined(separator: String) -> String { 761 | return self.relaxed().joined(separator: separator) 762 | } 763 | } 764 | % end 765 | 766 | 767 | func zip(_ a: ArrayLongerThan0, _ b: ArrayLongerThan0) -> ArrayLongerThan0<(A, B)> { 768 | return ArrayLongerThan0<(A, B)>(Array(zip(a.relaxed(), b.relaxed()))) 769 | } 770 | 771 | 772 | % for i in range(1, number_of_generated): 773 | func zip(_ a: ArrayLongerThan${i}, _ b: ArrayLongerThan${i}) -> ArrayLongerThan${i}<(A, B)> { 774 | return ArrayLongerThan${i}<(A, B)>( 775 | prefix: (a.first, b.first), 776 | zip(a.rest, b.rest) 777 | ) 778 | } 779 | % end 780 | 781 | 782 | 783 | func zip(_ a: A, _ b: B, _ c: C) -> [(A.Element, B.Element, C.Element)] { 784 | return zip(zip(a, b), c).map { ($0.0.0, $0.0.1, $0.1) } 785 | } 786 | 787 | 788 | 789 | func zip(_ a: ArrayLongerThan0, _ b: ArrayLongerThan0, _ c: ArrayLongerThan0) -> ArrayLongerThan0<(A, B, C)> { 790 | return ArrayLongerThan0<(A, B, C)>(Array(zip(a.relaxed(), b.relaxed(), c.relaxed()))) 791 | } 792 | 793 | 794 | 795 | % for i in range(1, number_of_generated): 796 | func zip(_ a: ArrayLongerThan${i}, _ b: ArrayLongerThan${i}, _ c: ArrayLongerThan${i}) -> ArrayLongerThan${i}<(A, B, C)> { 797 | return ArrayLongerThan${i}<(A, B, C)>( 798 | prefix: (a.first, b.first, c.first), 799 | zip(a.rest, b.rest, c.rest) 800 | ) 801 | } 802 | % end 803 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/PrettyPrint.swift: -------------------------------------------------------------------------------- 1 | public protocol PrettyPrintable { 2 | var descriptionLines: [IndentedLine] { get } 3 | } 4 | 5 | 6 | 7 | extension PrettyPrintable { 8 | public var prettyDescription: String { 9 | return format(self.descriptionLines) 10 | } 11 | } 12 | 13 | 14 | 15 | public indirect enum IndentedLine: Equatable { 16 | case indent(IndentedLine) 17 | case content(String) 18 | 19 | 20 | public func format(indentWidth: Int) -> String { 21 | switch self { 22 | case .indent(let line): 23 | let indent = String(repeating: " ", count: indentWidth) 24 | return indent + line.format(indentWidth: indentWidth) 25 | case .content(let content): 26 | return content 27 | } 28 | } 29 | } 30 | 31 | 32 | 33 | public func format( 34 | _ lines: Lines, 35 | indentWidth: Int = 4 36 | ) -> String where Lines.Element == IndentedLine { 37 | return lines 38 | .map { line in line.format(indentWidth: indentWidth) } 39 | .joined(separator: "\n") 40 | } 41 | 42 | 43 | 44 | public func indent( 45 | _ lines: Lines 46 | ) -> [IndentedLine] where Lines.Element == IndentedLine { 47 | return lines.map { .indent($0) } 48 | } 49 | 50 | 51 | 52 | public func lines( 53 | _ lines: Lines 54 | ) -> [IndentedLine] where Lines.Element == String { 55 | return lines.map { .content($0) } 56 | } 57 | 58 | 59 | 60 | public func verticalPadding( 61 | _ lines: Lines 62 | ) -> [IndentedLine] where Lines.Element == IndentedLine { 63 | var result = [IndentedLine.content("")] 64 | result.append(contentsOf: lines) 65 | result.append(.content("")) 66 | return result 67 | } 68 | 69 | 70 | 71 | public func section( 72 | name: String, 73 | body: Lines 74 | ) -> [IndentedLine] where Lines.Element == IndentedLine { 75 | let bodyMustNotBeEmpty: [IndentedLine] 76 | 77 | if body.isEmpty { 78 | bodyMustNotBeEmpty = lines(["(empty)"]) 79 | } 80 | else { 81 | bodyMustNotBeEmpty = Array(body) 82 | } 83 | 84 | return [.content("\(name):")] + indent(bodyMustNotBeEmpty) 85 | } 86 | 87 | 88 | 89 | public func sections( 90 | _ sectionsInfo: Sections 91 | ) -> [IndentedLine] where Sections.Element == (name: String, body: Lines), Lines.Element == IndentedLine { 92 | let sectionLines = sectionsInfo.map { sectionInfo in 93 | return section(name: sectionInfo.name, body: sectionInfo.body) 94 | } 95 | 96 | let verticalSpace = [IndentedLine.content("")] 97 | 98 | return intersperse(sectionLines, verticalSpace).flatMap { $0 } 99 | } 100 | 101 | 102 | 103 | public func sections( 104 | _ sectionsInfo: Sections 105 | ) -> [IndentedLine] where Sections.Element == Lines, Lines.Element == IndentedLine { 106 | return sections(sectionsInfo 107 | .enumerated() 108 | .map { (name: "\($0.0)", body: $0.1) }) 109 | } 110 | 111 | 112 | 113 | public func descriptionList( 114 | _ items: Items 115 | ) -> [IndentedLine] where Items.Element == (label: String, description: String) { 116 | return items.map { item in .content("\(item.label): \(item.description)") } 117 | } -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/Reference.swift: -------------------------------------------------------------------------------- 1 | public class Reference { 2 | private let value: WeakOrNotReferenceType 3 | private let id: ReferenceID 4 | 5 | public let destinationTypeName: TypeName 6 | public let destinationObjectDescription: String 7 | public var foundLocations: ArrayLongerThan1 8 | 9 | 10 | public init( 11 | value: WeakOrNotReferenceType, 12 | id: ReferenceID, 13 | typeName: TypeName, 14 | description: String, 15 | foundLocations: ArrayLongerThan1 16 | ) { 17 | self.value = value 18 | self.id = id 19 | self.destinationTypeName = typeName 20 | self.destinationObjectDescription = description 21 | self.foundLocations = foundLocations 22 | } 23 | 24 | 25 | public convenience init(_ target: Any, foundLocations: ArrayLongerThan1) { 26 | self.init( 27 | value: WeakOrNotReferenceType(target), 28 | id: ReferenceID(of: target), 29 | typeName: TypeName(of: target), 30 | description: "\(target)", 31 | foundLocations: foundLocations 32 | ) 33 | } 34 | 35 | 36 | public var isReleased: Bool { 37 | guard let weak = self.value.weak else { 38 | return true 39 | } 40 | 41 | return weak.isReleased 42 | } 43 | 44 | 45 | public func found(location: IdentifiableReferencePath) { 46 | self.foundLocations.append(location) 47 | } 48 | } 49 | 50 | 51 | 52 | extension Reference: Hashable { 53 | public var hashValue: Int { 54 | return self.id.hashValue 55 | } 56 | 57 | 58 | public static func ==(lhs: Reference, rhs: Reference) -> Bool { 59 | return lhs.id == rhs.id 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/ReferenceID.swift: -------------------------------------------------------------------------------- 1 | public enum ReferenceID { 2 | case anyReferenceType(name: TypeName, objectIdentifier: ObjectIdentifier) 3 | case anyValueType(name: TypeName) 4 | case unknown(name: TypeName) 5 | 6 | 7 | public init(of target: T) { 8 | switch Mirror(reflecting: target).displayStyle { 9 | case .some(.class): 10 | let objectIdentifier = ObjectIdentifier(target as AnyObject) 11 | self = .anyReferenceType( 12 | name: TypeName(of: target), 13 | objectIdentifier: objectIdentifier 14 | ) 15 | 16 | case .some(.tuple), .some(.struct), .some(.collection), .some(.dictionary), 17 | .some(.enum), .some(.optional), .some(.set): 18 | self = .anyValueType(name: TypeName(of: target)) 19 | 20 | case .none: 21 | self = .unknown(name: TypeName(of: target)) 22 | } 23 | } 24 | } 25 | 26 | 27 | 28 | extension ReferenceID: Equatable { 29 | public static func ==(lhs: ReferenceID, rhs: ReferenceID) -> Bool { 30 | switch (lhs, rhs) { 31 | case (.anyReferenceType(name: _, objectIdentifier: let l), .anyReferenceType(name: _, objectIdentifier: let r)): 32 | return l == r 33 | 34 | default: 35 | // NOTE: Any values should not be identical. 36 | return false 37 | } 38 | } 39 | } 40 | 41 | 42 | 43 | extension ReferenceID: Hashable { 44 | public var hashValue: Int { 45 | switch self { 46 | case .anyReferenceType(name: _, objectIdentifier: let objectIdentifier): 47 | return objectIdentifier.hashValue 48 | 49 | case .anyValueType: 50 | // NOTE: Any values should not be identical. 51 | return 0 52 | 53 | case .unknown: 54 | return 1 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/ReferencePath.swift: -------------------------------------------------------------------------------- 1 | public struct ReferencePath: Hashable { 2 | public let components: [ReferencePathComponent] 3 | 4 | 5 | public var count: Int { 6 | return self.components.count 7 | } 8 | 9 | 10 | public var description: String { 11 | return "(root)" + self.components 12 | .map { $0.description } 13 | .joined(separator: "") 14 | } 15 | 16 | 17 | public init(components: [ReferencePathComponent]) { 18 | self.components = components 19 | } 20 | 21 | 22 | public init(identifiablePath: IdentifiableReferencePath) { 23 | let hints = identifiablePath 24 | .idComponents 25 | .map { ReferencePathNormalization.Hint($0.noNormalizedComponent) } 26 | 27 | self.init(components: ReferencePathNormalization.normalize(hints: hints)) 28 | } 29 | 30 | 31 | public static let root = ReferencePath(components: []) 32 | } 33 | 34 | 35 | 36 | extension ReferencePath: Comparable { 37 | public static func <(lhs: ReferencePath, rhs: ReferencePath) -> Bool { 38 | return lhs.count < rhs.count 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/ReferencePathComponent.swift: -------------------------------------------------------------------------------- 1 | public enum ReferencePathComponent: Hashable { 2 | case label(String) 3 | case index(Int) 4 | case noLabel 5 | 6 | 7 | public var description: String { 8 | switch self { 9 | case .label(let label): 10 | return ".\(label)" 11 | case .index(let index): 12 | return "[\(index)]" 13 | case .noLabel: 14 | return "[unknown accessor]" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/ReferencePathNormalization.swift: -------------------------------------------------------------------------------- 1 | // NOTE: Mirror.Child for values stored on lazy var become the following unreadable value: 2 | // (label: propertyName + ".storage", value: Optional.some(value)) 3 | public enum ReferencePathNormalization { 4 | public static let lazyStorageLabelSuffix = ".storage" 5 | public static let optionalSomeLabel = "some" 6 | 7 | 8 | public static func normalize( 9 | _ noNormalizedComponents: [NotNormalizedReferencePathComponent] 10 | ) -> [ReferencePathComponent] { 11 | return self.normalize(hints: noNormalizedComponents.map { Hint($0) }) 12 | } 13 | 14 | 15 | public static func normalize( 16 | _ noNormalizedComponents: ArrayLongerThan1 17 | ) -> ArrayLongerThan1 { 18 | return self.normalize(hints: noNormalizedComponents.map { Hint($0) }) 19 | } 20 | 21 | 22 | public static func normalize( 23 | _ noNormalizedComponents: ArrayLongerThan2 24 | ) -> ArrayLongerThan1 { 25 | return self.normalize(hints: noNormalizedComponents.map { Hint($0) }) 26 | } 27 | 28 | 29 | public static func normalize( 30 | hints: [Hint] 31 | ) -> [ReferencePathComponent] { 32 | guard let noEmptyHints = ArrayLongerThan1(hints) else { 33 | return [] 34 | } 35 | 36 | return Array(self.normalize(hints: noEmptyHints).sequence()) 37 | } 38 | 39 | 40 | public static func normalize( 41 | hints: ArrayLongerThan1 42 | ) -> ArrayLongerThan1 { 43 | guard let noEmptyHints = ArrayLongerThan2(hints) else { 44 | return ArrayLongerThan1(prefix: self.normalize(initialHint: hints.first, nextHintIfExists: nil), []) 45 | } 46 | 47 | return self.normalize(hints: noEmptyHints) 48 | } 49 | 50 | 51 | public static func normalize( 52 | hints: ArrayLongerThan2 53 | ) -> ArrayLongerThan1 { 54 | let prefix = self.contextDependedNormalize(currentHint: hints.prefix, nextHintIfExists: hints.rest.prefix) 55 | var suffixed = hints.map(Optional.some) 56 | suffixed.append(nil) 57 | 58 | // NOTE: Create a pair that represent a bidirectional contexts. 59 | // (prev, current, nextIfExists) 60 | // [0, 1] -> [(0, 1, nil)] 61 | // [0, 1, 2] -> [(0, 1, 2), (1, 2, nil)] 62 | let intermediates = zip(hints.relaxed().relaxed(), hints.dropFirst().relaxed(), suffixed.dropFirst().dropFirst()) 63 | .compactMap { slided -> ReferencePathComponent? in 64 | let (previousHint, currentHint, nextHintIfExists) = slided 65 | return self.normalize( 66 | previousHint: previousHint, 67 | currentHint: currentHint, 68 | nextHintIfExists: nextHintIfExists 69 | ) 70 | } 71 | 72 | return .init(prefix: prefix, intermediates) 73 | } 74 | 75 | 76 | public static func normalize(initialHint: Hint, nextHintIfExists: Hint?) -> ReferencePathComponent { 77 | return self.contextDependedNormalize(currentHint: initialHint, nextHintIfExists: nextHintIfExists) 78 | } 79 | 80 | 81 | public static func normalize(previousHint: Hint, currentHint: Hint, nextHintIfExists: Hint?) -> ReferencePathComponent? { 82 | switch (previousHint, currentHint, nextHintIfExists) { 83 | case (.hasLazyStorageSuffix, .isOptionalSome, _): 84 | return nil 85 | default: 86 | return self.contextDependedNormalize(currentHint: currentHint, nextHintIfExists: nextHintIfExists) 87 | } 88 | } 89 | 90 | 91 | // NOTE: This method can work properly only if the previous hint is not a lazy storage suffix. 92 | public static func contextDependedNormalize(currentHint: Hint, nextHintIfExists: Hint?) -> ReferencePathComponent { 93 | switch (currentHint, nextHintIfExists) { 94 | case (.hasLazyStorageSuffix(label: let label), .some(.isOptionalSome)): 95 | return .label(String(label.dropLast(self.lazyStorageLabelSuffix.count))) 96 | case (.hasLazyStorageSuffix(label: let label), _), (.none(.label(let label)), _): 97 | return .label(label) 98 | case (.none(.index(let index)), _): 99 | return .index(index) 100 | case (.none(.noLabel), _): 101 | return .noLabel 102 | case (.isOptionalSome, _): 103 | return .label(self.optionalSomeLabel) 104 | } 105 | } 106 | 107 | 108 | 109 | public enum Hint: Equatable { 110 | case none(NotNormalizedReferencePathComponent) 111 | case hasLazyStorageSuffix(label: String) 112 | case isOptionalSome 113 | 114 | 115 | public init(_ component: NotNormalizedReferencePathComponent) { 116 | switch component { 117 | case .label(let label): 118 | if label.hasSuffix(ReferencePathNormalization.lazyStorageLabelSuffix) { 119 | self = .hasLazyStorageSuffix(label: label) 120 | } 121 | else if (label == ReferencePathNormalization.optionalSomeLabel) { 122 | self = .isOptionalSome 123 | } 124 | else { 125 | self = .none(component) 126 | } 127 | case .noLabel, .index: 128 | self = .none(component) 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/Types.swift: -------------------------------------------------------------------------------- 1 | public struct TypeName: Hashable { 2 | public let text: String 3 | 4 | 5 | public init(text: String) { 6 | self.text = text 7 | } 8 | 9 | 10 | public init(of any: Any) { 11 | self.init(text: "\(Mirror(reflecting: any).subjectType)") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/Weak.swift: -------------------------------------------------------------------------------- 1 | public class Weak { 2 | public private(set) weak var value: T? 3 | 4 | 5 | public init(_ value: T) { 6 | self.value = value 7 | } 8 | 9 | 10 | public init() { 11 | self.value = nil 12 | } 13 | 14 | 15 | public var isReleased: Bool { 16 | return self.value == nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/MemoryLeakTestKit/WeakOrNotReference.swift: -------------------------------------------------------------------------------- 1 | public enum WeakOrNotReferenceType { 2 | case weak(Weak) 3 | case notReferenceType 4 | 5 | 6 | public var weak: Weak? { 7 | switch self { 8 | case .weak(let container): 9 | return container 10 | case .notReferenceType: 11 | return nil 12 | } 13 | } 14 | 15 | 16 | public init(_ target: T) { 17 | switch Mirror(reflecting: target).displayStyle { 18 | case .some(.class): 19 | self = .weak(Weak(target as AnyObject)) 20 | default: 21 | self = .notReferenceType 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import MemoryLeakTestKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += MemoryLeakTestKitTests.allTests() 7 | XCTMain(tests) -------------------------------------------------------------------------------- /Tests/MemoryLeakTestKitTests/MemoryLeakDetectorTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import MemoryLeakTestKit 4 | 5 | 6 | 7 | class MemoryLeakDetectorTests: XCTestCase { 8 | func testMemoryLeak() { 9 | typealias TestCase = ( 10 | build: () -> Any, 11 | expected: MemoryLeakReport 12 | ) 13 | 14 | let testCases: [UInt: TestCase] = [ 15 | #line: ( // No circulars 16 | build: { () -> Node in 17 | return Node(linkedNodes: []) 18 | }, 19 | expected: MemoryLeakReport( 20 | leakedObjects: [] 21 | ) 22 | ), 23 | #line: ( // Single direct circular 24 | build: { () -> Node in 25 | let node = Node(linkedNodes: []) 26 | node.linkedNodes = [node] 27 | return node 28 | }, 29 | expected: MemoryLeakReport( 30 | leakedObjects: [ 31 | LeakedObject( 32 | objectDescription: "Node", 33 | typeName: TypeName(text: "Node"), 34 | location: ReferencePath.root, 35 | circularPaths: [ 36 | CircularReferencePath( 37 | end: .root(TypeName(text: "Node")), 38 | components: ArrayLongerThan1([ 39 | .label("linkedNodes"), 40 | .index(0), 41 | ])! 42 | ), 43 | ] 44 | ), 45 | ] 46 | ) 47 | ), 48 | #line: ( // Single indirect circular 49 | build: { () -> Node in 50 | let indirectNode = Node(linkedNodes: []) 51 | let node = Node(linkedNodes: [indirectNode]) 52 | indirectNode.linkedNodes = [node] 53 | return node 54 | }, 55 | expected: MemoryLeakReport( 56 | leakedObjects: [ 57 | LeakedObject( 58 | objectDescription: "Node", 59 | typeName: TypeName(text: "Node"), 60 | location: ReferencePath.root, 61 | circularPaths: [ 62 | CircularReferencePath( 63 | end: .root(TypeName(text: "Node")), 64 | components: ArrayLongerThan1([ 65 | .label("linkedNodes"), 66 | .index(0), 67 | .label("linkedNodes"), 68 | .index(0), 69 | ])! 70 | ) 71 | ] 72 | ), 73 | LeakedObject( 74 | objectDescription: "Node", 75 | typeName: TypeName(text: "Node"), 76 | location: ReferencePath(components: [ 77 | .label("linkedNodes"), 78 | .index(0), 79 | ]), 80 | circularPaths: [] 81 | ), 82 | ] 83 | ) 84 | ), 85 | #line: ( // Both direct and indirect circulars 86 | build: { () -> Node in 87 | let indirectNode = Node(linkedNodes: []) 88 | let node = Node(linkedNodes: [indirectNode]) 89 | indirectNode.linkedNodes = [node, indirectNode] 90 | return node 91 | }, 92 | expected: MemoryLeakReport( 93 | leakedObjects: [ 94 | LeakedObject( 95 | objectDescription: "Node", 96 | typeName: TypeName(text: "Node"), 97 | location: ReferencePath.root, 98 | circularPaths: [ 99 | CircularReferencePath( 100 | end: .root(TypeName(text: "Node")), 101 | components: ArrayLongerThan1([ 102 | .label("linkedNodes"), 103 | .index(0), 104 | .label("linkedNodes"), 105 | .index(0), 106 | ])! 107 | ), 108 | ] 109 | ), 110 | LeakedObject( 111 | objectDescription: "Node", 112 | typeName: TypeName(text: "Node"), 113 | location: ReferencePath(components: [ 114 | .label("linkedNodes"), 115 | .index(0), 116 | ]), 117 | circularPaths: [ 118 | CircularReferencePath( 119 | end: .intermediate(TypeName(text: "Node")), 120 | components: ArrayLongerThan1([ 121 | .label("linkedNodes"), 122 | .index(1), 123 | ])! 124 | ), 125 | ] 126 | ), 127 | ] 128 | ) 129 | ), 130 | #line: ( // Lazy 131 | build: { () -> LazyCircularNode in 132 | let node = LazyCircularNode() 133 | _ = node.indirect 134 | return node 135 | }, 136 | expected: MemoryLeakReport( 137 | leakedObjects: [ 138 | LeakedObject( 139 | objectDescription: "LazyCircularNode", 140 | typeName: TypeName(text: "LazyCircularNode"), 141 | location: ReferencePath.root, 142 | circularPaths: [ 143 | CircularReferencePath( 144 | end: .root(TypeName(text: "LazyCircularNode")), 145 | components: ArrayLongerThan1([ 146 | .label("indirect"), 147 | .label("value"), 148 | ])! 149 | ), 150 | ] 151 | ), 152 | LeakedObject( 153 | objectDescription: "Indirect", 154 | typeName: TypeName(text: "Indirect"), 155 | location: ReferencePath(components: [ 156 | .label("indirect"), 157 | ]), 158 | circularPaths: [] 159 | ), 160 | ] 161 | ) 162 | ), 163 | #line: ( // No circular references but outer owner exists 164 | build: { () -> Node in 165 | let node = Node(linkedNodes: []) 166 | var anonymous: (() -> Void)? 167 | 168 | anonymous = { 169 | _ = node 170 | anonymous!() 171 | } 172 | 173 | return node 174 | }, 175 | expected: MemoryLeakReport( 176 | leakedObjects: [ 177 | LeakedObject( 178 | objectDescription: "Node", 179 | typeName: TypeName(text: "Node"), 180 | location: ReferencePath.root, 181 | circularPaths: [] 182 | ) 183 | ] 184 | ) 185 | ), 186 | #line: ( // Circular references but anonymous instances at end 187 | build: { () -> Node in 188 | let node = Node(linkedNodes: []) 189 | 190 | ReferenceOwner.global.own(node) 191 | 192 | return Node(linkedNodes: [node]) 193 | }, 194 | expected: MemoryLeakReport( 195 | leakedObjects: [ 196 | LeakedObject( 197 | objectDescription: "Node", 198 | typeName: TypeName(text: "Node"), 199 | location: ReferencePath(components: [ 200 | .label("linkedNodes"), 201 | .index(0), 202 | ]), 203 | circularPaths: [] 204 | ) 205 | ] 206 | ) 207 | ), 208 | ] 209 | 210 | testCases.forEach { tuple in 211 | let (line, (build: build, expected: expected)) = tuple 212 | 213 | let memoryLeakHints = detectLeaks(by: build) 214 | 215 | XCTAssertEqual( 216 | memoryLeakHints, expected, 217 | differenceMemoryLeakReport(between: expected, and: memoryLeakHints), 218 | line: line 219 | ) 220 | } 221 | } 222 | 223 | 224 | 225 | private final class Node: CustomStringConvertible { 226 | var linkedNodes: [Node] 227 | 228 | 229 | init(linkedNodes: [Node]) { 230 | self.linkedNodes = linkedNodes 231 | } 232 | 233 | 234 | var description: String { 235 | return "Node" 236 | } 237 | } 238 | 239 | 240 | 241 | private final class LazyCircularNode: CustomStringConvertible { 242 | lazy var indirect: Indirect = Indirect(value: self) 243 | 244 | 245 | final class Indirect: CustomStringConvertible { 246 | let value: LazyCircularNode 247 | 248 | 249 | init(value: LazyCircularNode) { 250 | self.value = value 251 | } 252 | 253 | 254 | var description: String { 255 | return "Indirect" 256 | } 257 | } 258 | 259 | 260 | var description: String { 261 | return "LazyCircularNode" 262 | } 263 | } 264 | 265 | 266 | 267 | public final class ReferenceOwner { 268 | private var owned: [Any] = [] 269 | private init() {} 270 | 271 | 272 | public func own(_ any: Any) { 273 | self.owned.append(any) 274 | } 275 | 276 | 277 | public static let global = ReferenceOwner() 278 | } 279 | 280 | 281 | 282 | private func differenceMemoryLeakReport(between a: MemoryLeakReport, and b: MemoryLeakReport) -> String { 283 | let missingLeakedObjects = sections(a.leakedObjects.subtracting(b.leakedObjects) 284 | .map { $0.descriptionLines }) 285 | 286 | let extraLeakedObjects = sections(b.leakedObjects.subtracting(a.leakedObjects) 287 | .map { $0.descriptionLines }) 288 | 289 | return format(verticalPadding(sections([ 290 | (name: "Missing leaked objects", body: missingLeakedObjects), 291 | (name: "Extra leaked objects", body: extraLeakedObjects), 292 | ]))) 293 | } 294 | 295 | 296 | static let allTests: [(String, (MemoryLeakDetectorTests) -> () throws -> Void)] = [ 297 | ("testMemoryLeak", testMemoryLeak), 298 | ] 299 | } -------------------------------------------------------------------------------- /Tests/MemoryLeakTestKitTests/ReferenceIDTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MemoryLeakTestKit 3 | 4 | 5 | 6 | class ReferenceIDTests: XCTestCase { 7 | func testEquatable() { 8 | typealias TestCase = ( 9 | ReferenceID, 10 | ReferenceID, 11 | expected: Bool 12 | ) 13 | 14 | let testCases: [UInt: TestCase] = [ 15 | #line: ( 16 | ReferenceID(of: 0), 17 | ReferenceID(of: 0), 18 | expected: false 19 | ), 20 | #line: { 21 | let object1 = NSObject() 22 | let object2 = NSObject() 23 | return ( 24 | ReferenceID(of: object1), 25 | ReferenceID(of: object2), 26 | expected: false 27 | ) 28 | }(), 29 | #line: { 30 | let object = NSObject() 31 | return ( 32 | ReferenceID(of: object), 33 | ReferenceID(of: object), 34 | expected: true 35 | ) 36 | }() 37 | ] 38 | 39 | testCases.forEach { tuple in 40 | let (line, (a, b, expected: expected)) = tuple 41 | 42 | XCTAssertEqual(a == b, expected, line: line) 43 | } 44 | } 45 | 46 | 47 | static let allTests: [(String, (ReferenceIDTests) -> () throws -> Void)] = [ 48 | ("testEquatable", testEquatable), 49 | ] 50 | } -------------------------------------------------------------------------------- /Tests/MemoryLeakTestKitTests/ReferencePathNormalizationTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MemoryLeakTestKit 3 | 4 | 5 | 6 | class ReferencePathNormalizationTests: XCTestCase { 7 | func testNormalize() { 8 | typealias TestCase = ( 9 | input: [NotNormalizedReferencePathComponent], 10 | expected: [ReferencePathComponent] 11 | ) 12 | 13 | let testCases: [UInt: TestCase] = [ 14 | #line: ( 15 | input: [], 16 | expected: [] 17 | ), 18 | #line: ( 19 | input: [ 20 | .noLabel, 21 | ], 22 | expected: [ 23 | .noLabel, 24 | ] 25 | ), 26 | #line: ( 27 | input: [ 28 | .label("label"), 29 | ], 30 | expected: [ 31 | .label("label"), 32 | ] 33 | ), 34 | #line: ( 35 | input: [ 36 | .index(0), 37 | ], 38 | expected: [ 39 | .index(0), 40 | ] 41 | ), 42 | #line: ( 43 | input: [ 44 | .label("label"), 45 | .index(1) 46 | ], 47 | expected: [ 48 | .label("label"), 49 | .index(1) 50 | ] 51 | ), 52 | #line: ( 53 | input: [ 54 | .label("notLazy"), 55 | .label("some") 56 | ], 57 | expected: [ 58 | .label("notLazy"), 59 | .label("some") 60 | ] 61 | ), 62 | #line: ( 63 | input: [ 64 | .label("some"), 65 | .label("some"), 66 | ], 67 | expected: [ 68 | .label("some"), 69 | .label("some"), 70 | ] 71 | ), 72 | 73 | 74 | // IMPORTANT CASES: 75 | #line: ( 76 | input: [ 77 | .label("fake.storage"), 78 | .label("some"), 79 | ], 80 | expected: [ 81 | .label("fake"), 82 | ] 83 | ), 84 | #line: ( 85 | input: [ 86 | .label("label"), 87 | .label("fake.storage"), 88 | .label("some"), 89 | ], 90 | expected: [ 91 | .label("label"), 92 | .label("fake"), 93 | ] 94 | ), 95 | #line: ( 96 | input: [ 97 | .label("fake.storage"), 98 | .label("some"), 99 | .label("label"), 100 | ], 101 | expected: [ 102 | .label("fake"), 103 | .label("label"), 104 | ] 105 | ), 106 | ] 107 | 108 | testCases.forEach { tuple in 109 | let (line, (input: input, expected: expected)) = tuple 110 | 111 | let actual = ReferencePathNormalization.normalize(input) 112 | 113 | XCTAssertEqual( 114 | actual, expected, 115 | difference(between: expected, and: actual), 116 | line: line 117 | ) 118 | } 119 | } 120 | 121 | 122 | func testHint() { 123 | typealias TestCase = ( 124 | input: NotNormalizedReferencePathComponent, 125 | expected: ReferencePathNormalization.Hint 126 | ) 127 | 128 | let testCases: [UInt: TestCase] = [ 129 | #line: ( 130 | input: .noLabel, 131 | expected: .none(.noLabel) 132 | ), 133 | #line: ( 134 | input: .index(0), 135 | expected: .none(.index(0)) 136 | ), 137 | #line: ( 138 | input: .label("label"), 139 | expected: .none(.label("label")) 140 | ), 141 | #line: ( 142 | input: .label("label.storage"), 143 | expected: .hasLazyStorageSuffix(label: "label.storage") 144 | ), 145 | #line: ( 146 | input: .label("some"), 147 | expected: .isOptionalSome 148 | ), 149 | #line: ( 150 | input: .label("some.storage"), 151 | expected: .hasLazyStorageSuffix(label: "some.storage") 152 | ), 153 | ] 154 | 155 | testCases.forEach { 156 | let (line, (input: input, expected: expected)) = $0 157 | 158 | let actual = ReferencePathNormalization.Hint(input) 159 | 160 | XCTAssertEqual(actual, expected, line: line) 161 | } 162 | } 163 | 164 | 165 | private func difference(between a: [ReferencePathComponent], and b: [ReferencePathComponent]) -> String { 166 | return format(verticalPadding(sections([ 167 | (name: "Expected", body: lines(a.map { $0.description })), 168 | (name: "Actual", body: lines(b.map { $0.description })), 169 | ]))) 170 | } 171 | 172 | 173 | static let allTests: [(String, (ReferencePathNormalizationTests) -> () throws -> Void)] = [ 174 | ("testNormalize", testNormalize), 175 | ("testHint", testHint), 176 | ] 177 | } -------------------------------------------------------------------------------- /Tests/MemoryLeakTestKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(MemoryLeakDetectorTests.allTests), 7 | testCase(ReferenceIDTests.allTests), 8 | testCase(ReferencePathNormalizationTests.allTests), 9 | ] 10 | } 11 | #endif -------------------------------------------------------------------------------- /bin/gyb/gyb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | import gyb 3 | gyb.main() 4 | -------------------------------------------------------------------------------- /bin/gyb/gyb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # GYB: Generate Your Boilerplate (improved names welcome; at least 3 | # this one's short). See -h output for instructions 4 | 5 | from __future__ import print_function 6 | 7 | import os 8 | import re 9 | import sys 10 | try: 11 | from cStringIO import StringIO 12 | except ImportError: 13 | from io import StringIO 14 | import textwrap 15 | import tokenize 16 | from bisect import bisect 17 | 18 | try: 19 | basestring 20 | except NameError: 21 | basestring = str 22 | 23 | 24 | def get_line_starts(s): 25 | """Return a list containing the start index of each line in s. 26 | 27 | The list also contains a sentinel index for the end of the string, 28 | so there will be one more element in the list than there are lines 29 | in the string 30 | """ 31 | starts = [0] 32 | 33 | for line in s.split('\n'): 34 | starts.append(starts[-1] + len(line) + 1) 35 | 36 | starts[-1] -= 1 37 | return starts 38 | 39 | 40 | def strip_trailing_nl(s): 41 | """If s ends with a newline, drop it; else return s intact""" 42 | return s[:-1] if s.endswith('\n') else s 43 | 44 | 45 | def split_lines(s): 46 | """Split s into a list of lines, each of which has a trailing newline 47 | 48 | If the lines are later concatenated, the result is s, possibly 49 | with a single appended newline. 50 | """ 51 | return [l + '\n' for l in s.split('\n')] 52 | 53 | 54 | # text on a line up to the first '$$', '${', or '%%' 55 | literalText = r'(?: [^$\n%] | \$(?![${]) | %(?!%) )*' 56 | 57 | # The part of an '%end' line that follows the '%' sign 58 | linesClose = r'[\ \t]* end [\ \t]* (?: \# .* )? $' 59 | 60 | # Note: Where "# Absorb" appears below, the regexp attempts to eat up 61 | # through the end of ${...} and %{...}% constructs. In reality we 62 | # handle this with the Python tokenizer, which avoids mis-detections 63 | # due to nesting, comments and strings. This extra absorption in the 64 | # regexp facilitates testing the regexp on its own, by preventing the 65 | # interior of some of these constructs from being treated as literal 66 | # text. 67 | tokenize_re = re.compile( 68 | r''' 69 | # %-lines and %{...}-blocks 70 | # \n? # absorb one preceding newline 71 | ^ 72 | (?: 73 | (?P 74 | (?P<_indent> [\ \t]* % (?! [{%] ) [\ \t]* ) (?! [\ \t] | ''' + 75 | linesClose + r''' ) .* 76 | ( \n (?P=_indent) (?! ''' + linesClose + r''' ) .* ) * 77 | ) 78 | | (?P [\ \t]* % [ \t]* ''' + linesClose + r''' ) 79 | | [\ \t]* (?P %\{ ) 80 | (?: [^}]| \} (?!%) )* \}% # Absorb 81 | ) 82 | \n? # absorb one trailing newline 83 | 84 | # Substitutions 85 | | (?P \$\{ ) 86 | [^}]* \} # Absorb 87 | 88 | # %% and $$ are literal % and $ respectively 89 | | (?P[$%]) (?P=symbol) 90 | 91 | # Literal text 92 | | (?P ''' + literalText + r''' 93 | (?: 94 | # newline that doesn't precede space+% 95 | (?: \n (?! [\ \t]* %[^%] ) ) 96 | ''' + literalText + r''' 97 | )* 98 | \n? 99 | ) 100 | ''', re.VERBOSE | re.MULTILINE) 101 | 102 | gyb_block_close = re.compile('\}%[ \t]*\n?') 103 | 104 | 105 | def token_pos_to_index(token_pos, start, line_starts): 106 | """Translate a tokenize (line, column) pair into an absolute 107 | position in source text given the position where we started 108 | tokenizing and a list that maps lines onto their starting 109 | character indexes. 110 | """ 111 | relative_token_line_plus1, token_col = token_pos 112 | 113 | # line number where we started tokenizing 114 | start_line_num = bisect(line_starts, start) - 1 115 | 116 | # line number of the token in the whole text 117 | abs_token_line = relative_token_line_plus1 - 1 + start_line_num 118 | 119 | # if found in the first line, adjust the end column to account 120 | # for the extra text 121 | if relative_token_line_plus1 == 1: 122 | token_col += start - line_starts[start_line_num] 123 | 124 | # Sometimes tokenizer errors report a line beyond the last one 125 | if abs_token_line >= len(line_starts): 126 | return line_starts[-1] 127 | 128 | return line_starts[abs_token_line] + token_col 129 | 130 | 131 | def tokenize_python_to_unmatched_close_curly(source_text, start, line_starts): 132 | """Apply Python's tokenize to source_text starting at index start 133 | while matching open and close curly braces. When an unmatched 134 | close curly brace is found, return its index. If not found, 135 | return len(source_text). If there's a tokenization error, return 136 | the position of the error. 137 | """ 138 | stream = StringIO(source_text) 139 | stream.seek(start) 140 | nesting = 0 141 | 142 | try: 143 | for kind, text, token_start, token_end, line_text \ 144 | in tokenize.generate_tokens(stream.readline): 145 | 146 | if text == '{': 147 | nesting += 1 148 | elif text == '}': 149 | nesting -= 1 150 | if nesting < 0: 151 | return token_pos_to_index(token_start, start, line_starts) 152 | 153 | except tokenize.TokenError as error: 154 | (message, error_pos) = error.args 155 | return token_pos_to_index(error_pos, start, line_starts) 156 | 157 | return len(source_text) 158 | 159 | 160 | def tokenize_template(template_text): 161 | r"""Given the text of a template, returns an iterator over 162 | (tokenType, token, match) tuples. 163 | 164 | **Note**: this is template syntax tokenization, not Python 165 | tokenization. 166 | 167 | When a non-literal token is matched, a client may call 168 | iter.send(pos) on the iterator to reset the position in 169 | template_text at which scanning will resume. 170 | 171 | This function provides a base level of tokenization which is 172 | then refined by ParseContext.token_generator. 173 | 174 | >>> from pprint import * 175 | >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( 176 | ... '%for x in range(10):\n% print x\n%end\njuicebox'))) 177 | [('gybLines', '%for x in range(10):\n% print x'), 178 | ('gybLinesClose', '%end'), 179 | ('literal', 'juicebox')] 180 | 181 | >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( 182 | ... '''Nothing 183 | ... % if x: 184 | ... % for i in range(3): 185 | ... ${i} 186 | ... % end 187 | ... % else: 188 | ... THIS SHOULD NOT APPEAR IN THE OUTPUT 189 | ... '''))) 190 | [('literal', 'Nothing\n'), 191 | ('gybLines', '% if x:\n% for i in range(3):'), 192 | ('substitutionOpen', '${'), 193 | ('literal', '\n'), 194 | ('gybLinesClose', '% end'), 195 | ('gybLines', '% else:'), 196 | ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n')] 197 | 198 | >>> for kind, text, _ in tokenize_template(''' 199 | ... This is $some$ literal stuff containing a ${substitution} 200 | ... followed by a %{...} block: 201 | ... %{ 202 | ... # Python code 203 | ... }% 204 | ... and here $${are} some %-lines: 205 | ... % x = 1 206 | ... % y = 2 207 | ... % if z == 3: 208 | ... % print '${hello}' 209 | ... % end 210 | ... % for x in zz: 211 | ... % print x 212 | ... % # different indentation 213 | ... % twice 214 | ... and some lines that literally start with a %% token 215 | ... %% first line 216 | ... %% second line 217 | ... '''): 218 | ... print((kind, text.strip().split('\n',1)[0])) 219 | ('literal', 'This is $some$ literal stuff containing a') 220 | ('substitutionOpen', '${') 221 | ('literal', 'followed by a %{...} block:') 222 | ('gybBlockOpen', '%{') 223 | ('literal', 'and here ${are} some %-lines:') 224 | ('gybLines', '% x = 1') 225 | ('gybLinesClose', '% end') 226 | ('gybLines', '% for x in zz:') 227 | ('gybLines', '% # different indentation') 228 | ('gybLines', '% twice') 229 | ('literal', 'and some lines that literally start with a % token') 230 | """ 231 | pos = 0 232 | end = len(template_text) 233 | 234 | saved_literal = [] 235 | literal_first_match = None 236 | 237 | while pos < end: 238 | m = tokenize_re.match(template_text, pos, end) 239 | 240 | # pull out the one matched key (ignoring internal patterns starting 241 | # with _) 242 | ((kind, text), ) = ( 243 | (kind, text) for (kind, text) in m.groupdict().items() 244 | if text is not None and kind[0] != '_') 245 | 246 | if kind in ('literal', 'symbol'): 247 | if len(saved_literal) == 0: 248 | literal_first_match = m 249 | # literals and symbols get batched together 250 | saved_literal.append(text) 251 | pos = None 252 | else: 253 | # found a non-literal. First yield any literal we've accumulated 254 | if saved_literal != []: 255 | yield 'literal', ''.join(saved_literal), literal_first_match 256 | saved_literal = [] 257 | 258 | # Then yield the thing we found. If we get a reply, it's 259 | # the place to resume tokenizing 260 | pos = yield kind, text, m 261 | 262 | # If we were not sent a new position by our client, resume 263 | # tokenizing at the end of this match. 264 | if pos is None: 265 | pos = m.end(0) 266 | else: 267 | # Client is not yet ready to process next token 268 | yield 269 | 270 | if saved_literal != []: 271 | yield 'literal', ''.join(saved_literal), literal_first_match 272 | 273 | 274 | def split_gyb_lines(source_lines): 275 | r"""Return a list of lines at which to split the incoming source 276 | 277 | These positions represent the beginnings of python line groups that 278 | will require a matching %end construct if they are to be closed. 279 | 280 | >>> src = split_lines('''\ 281 | ... if x: 282 | ... print x 283 | ... if y: # trailing comment 284 | ... print z 285 | ... if z: # another comment\ 286 | ... ''') 287 | >>> s = split_gyb_lines(src) 288 | >>> len(s) 289 | 2 290 | >>> src[s[0]] 291 | ' print z\n' 292 | >>> s[1] - len(src) 293 | 0 294 | 295 | >>> src = split_lines('''\ 296 | ... if x: 297 | ... if y: print 1 298 | ... if z: 299 | ... print 2 300 | ... pass\ 301 | ... ''') 302 | >>> s = split_gyb_lines(src) 303 | >>> len(s) 304 | 1 305 | >>> src[s[0]] 306 | ' if y: print 1\n' 307 | 308 | >>> src = split_lines('''\ 309 | ... if x: 310 | ... if y: 311 | ... print 1 312 | ... print 2 313 | ... ''') 314 | >>> s = split_gyb_lines(src) 315 | >>> len(s) 316 | 2 317 | >>> src[s[0]] 318 | ' if y:\n' 319 | >>> src[s[1]] 320 | ' print 1\n' 321 | """ 322 | last_token_text, last_token_kind = None, None 323 | unmatched_indents = [] 324 | 325 | dedents = 0 326 | try: 327 | for token_kind, token_text, token_start, \ 328 | (token_end_line, token_end_col), line_text \ 329 | in tokenize.generate_tokens(lambda i=iter(source_lines): 330 | next(i)): 331 | 332 | if token_kind in (tokenize.COMMENT, tokenize.ENDMARKER): 333 | continue 334 | 335 | if token_text == '\n' and last_token_text == ':': 336 | unmatched_indents.append(token_end_line) 337 | 338 | # The tokenizer appends dedents at EOF; don't consider 339 | # those as matching indentations. Instead just save them 340 | # up... 341 | if last_token_kind == tokenize.DEDENT: 342 | dedents += 1 343 | # And count them later, when we see something real. 344 | if token_kind != tokenize.DEDENT and dedents > 0: 345 | unmatched_indents = unmatched_indents[:-dedents] 346 | dedents = 0 347 | 348 | last_token_text, last_token_kind = token_text, token_kind 349 | 350 | except tokenize.TokenError: 351 | # Let the later compile() call report the error 352 | return [] 353 | 354 | if last_token_text == ':': 355 | unmatched_indents.append(len(source_lines)) 356 | 357 | return unmatched_indents 358 | 359 | 360 | def code_starts_with_dedent_keyword(source_lines): 361 | r"""Return True iff the incoming Python source_lines begin with "else", 362 | "elif", "except", or "finally". 363 | 364 | Initial comments and whitespace are ignored. 365 | 366 | >>> code_starts_with_dedent_keyword(split_lines('if x in y: pass')) 367 | False 368 | >>> code_starts_with_dedent_keyword(split_lines('except ifSomethingElse:')) 369 | True 370 | >>> code_starts_with_dedent_keyword( 371 | ... split_lines('\n# comment\nelse: # yes')) 372 | True 373 | """ 374 | token_text = None 375 | for token_kind, token_text, _, _, _ \ 376 | in tokenize.generate_tokens(lambda i=iter(source_lines): next(i)): 377 | 378 | if token_kind != tokenize.COMMENT and token_text.strip() != '': 379 | break 380 | 381 | return token_text in ('else', 'elif', 'except', 'finally') 382 | 383 | 384 | class ParseContext(object): 385 | 386 | """State carried through a parse of a template""" 387 | 388 | filename = '' 389 | template = '' 390 | line_starts = [] 391 | code_start_line = -1 392 | code_text = None 393 | tokens = None # The rest of the tokens 394 | close_lines = False 395 | 396 | def __init__(self, filename, template=None): 397 | self.filename = os.path.abspath(filename) 398 | if template is None: 399 | with open(filename) as f: 400 | self.template = f.read() 401 | else: 402 | self.template = template 403 | self.line_starts = get_line_starts(self.template) 404 | self.tokens = self.token_generator(tokenize_template(self.template)) 405 | self.next_token() 406 | 407 | def pos_to_line(self, pos): 408 | return bisect(self.line_starts, pos) - 1 409 | 410 | def token_generator(self, base_tokens): 411 | r"""Given an iterator over (kind, text, match) triples (see 412 | tokenize_template above), return a refined iterator over 413 | token_kinds. 414 | 415 | Among other adjustments to the elements found by base_tokens, 416 | this refined iterator tokenizes python code embedded in 417 | template text to help determine its true extent. The 418 | expression "base_tokens.send(pos)" is used to reset the index at 419 | which base_tokens resumes scanning the underlying text. 420 | 421 | >>> ctx = ParseContext('dummy', ''' 422 | ... %for x in y: 423 | ... % print x 424 | ... % end 425 | ... literally 426 | ... ''') 427 | >>> while ctx.token_kind: 428 | ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) 429 | ... ignored = ctx.next_token() 430 | ('literal', '\n') 431 | ('gybLinesOpen', 'for x in y:\n') 432 | ('gybLines', ' print x\n') 433 | ('gybLinesClose', '% end') 434 | ('literal', 'literally\n') 435 | 436 | >>> ctx = ParseContext('dummy', 437 | ... '''Nothing 438 | ... % if x: 439 | ... % for i in range(3): 440 | ... ${i} 441 | ... % end 442 | ... % else: 443 | ... THIS SHOULD NOT APPEAR IN THE OUTPUT 444 | ... ''') 445 | >>> while ctx.token_kind: 446 | ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) 447 | ... ignored = ctx.next_token() 448 | ('literal', 'Nothing\n') 449 | ('gybLinesOpen', 'if x:\n') 450 | ('gybLinesOpen', ' for i in range(3):\n') 451 | ('substitutionOpen', 'i') 452 | ('literal', '\n') 453 | ('gybLinesClose', '% end') 454 | ('gybLinesOpen', 'else:\n') 455 | ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n') 456 | 457 | >>> ctx = ParseContext('dummy', 458 | ... '''% for x in [1, 2, 3]: 459 | ... % if x == 1: 460 | ... literal1 461 | ... % elif x > 1: # add output line here to fix bug 462 | ... % if x == 2: 463 | ... literal2 464 | ... % end 465 | ... % end 466 | ... % end 467 | ... ''') 468 | >>> while ctx.token_kind: 469 | ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) 470 | ... ignored = ctx.next_token() 471 | ('gybLinesOpen', 'for x in [1, 2, 3]:\n') 472 | ('gybLinesOpen', ' if x == 1:\n') 473 | ('literal', 'literal1\n') 474 | ('gybLinesOpen', 'elif x > 1: # add output line here to fix bug\n') 475 | ('gybLinesOpen', ' if x == 2:\n') 476 | ('literal', 'literal2\n') 477 | ('gybLinesClose', '% end') 478 | ('gybLinesClose', '% end') 479 | ('gybLinesClose', '% end') 480 | """ 481 | for self.token_kind, self.token_text, self.token_match in base_tokens: 482 | kind = self.token_kind 483 | self.code_text = None 484 | 485 | # Do we need to close the current lines? 486 | self.close_lines = kind == 'gybLinesClose' 487 | 488 | # %{...}% and ${...} constructs 489 | if kind.endswith('Open'): 490 | 491 | # Tokenize text that follows as Python up to an unmatched '}' 492 | code_start = self.token_match.end(kind) 493 | self.code_start_line = self.pos_to_line(code_start) 494 | 495 | close_pos = tokenize_python_to_unmatched_close_curly( 496 | self.template, code_start, self.line_starts) 497 | self.code_text = self.template[code_start:close_pos] 498 | yield kind 499 | 500 | if (kind == 'gybBlockOpen'): 501 | # Absorb any '}% \n' 502 | m2 = gyb_block_close.match(self.template, close_pos) 503 | if not m2: 504 | raise ValueError("Invalid block closure") 505 | next_pos = m2.end(0) 506 | else: 507 | assert kind == 'substitutionOpen' 508 | # skip past the closing '}' 509 | next_pos = close_pos + 1 510 | 511 | # Resume tokenizing after the end of the code. 512 | base_tokens.send(next_pos) 513 | 514 | elif kind == 'gybLines': 515 | 516 | self.code_start_line = self.pos_to_line( 517 | self.token_match.start('gybLines')) 518 | indentation = self.token_match.group('_indent') 519 | 520 | # Strip off the leading indentation and %-sign 521 | source_lines = re.split( 522 | '^' + re.escape(indentation), 523 | self.token_match.group('gybLines') + '\n', 524 | flags=re.MULTILINE)[1:] 525 | 526 | if code_starts_with_dedent_keyword(source_lines): 527 | self.close_lines = True 528 | 529 | last_split = 0 530 | for line in split_gyb_lines(source_lines): 531 | self.token_kind = 'gybLinesOpen' 532 | self.code_text = ''.join(source_lines[last_split:line]) 533 | yield self.token_kind 534 | last_split = line 535 | self.code_start_line += line - last_split 536 | self.close_lines = False 537 | 538 | self.code_text = ''.join(source_lines[last_split:]) 539 | if self.code_text: 540 | self.token_kind = 'gybLines' 541 | yield self.token_kind 542 | else: 543 | yield self.token_kind 544 | 545 | def next_token(self): 546 | """Move to the next token""" 547 | for kind in self.tokens: 548 | return self.token_kind 549 | 550 | self.token_kind = None 551 | 552 | 553 | _default_line_directive = \ 554 | '// ###sourceLocation(file: "%(file)s", line: %(line)d)' 555 | 556 | 557 | class ExecutionContext(object): 558 | 559 | """State we pass around during execution of a template""" 560 | 561 | def __init__(self, line_directive=_default_line_directive, 562 | **local_bindings): 563 | self.local_bindings = local_bindings 564 | self.line_directive = line_directive 565 | self.local_bindings['__context__'] = self 566 | self.result_text = [] 567 | self.last_file_line = None 568 | 569 | def append_text(self, text, file, line): 570 | # see if we need to inject a line marker 571 | if self.line_directive: 572 | if (file, line) != self.last_file_line: 573 | # We can only insert the line directive at a line break 574 | if len(self.result_text) == 0 \ 575 | or self.result_text[-1].endswith('\n'): 576 | if sys.platform == 'win32': 577 | file = file.replace('\\', '/') 578 | substitutions = {'file': file, 'line': line + 1} 579 | format_str = self.line_directive + '\n' 580 | self.result_text.append(format_str % substitutions) 581 | # But if the new text contains any line breaks, we can create 582 | # one 583 | elif '\n' in text: 584 | i = text.find('\n') 585 | self.result_text.append(text[:i + 1]) 586 | # and try again 587 | self.append_text(text[i + 1:], file, line + 1) 588 | return 589 | 590 | self.result_text.append(text) 591 | self.last_file_line = (file, line + text.count('\n')) 592 | 593 | 594 | class ASTNode(object): 595 | 596 | """Abstract base class for template AST nodes""" 597 | 598 | def __init__(self): 599 | raise NotImplementedError("ASTNode.__init__ is not implemented.") 600 | 601 | def execute(self, context): 602 | raise NotImplementedError("ASTNode.execute is not implemented.") 603 | 604 | def __str__(self, indent=''): 605 | raise NotImplementedError("ASTNode.__str__ is not implemented.") 606 | 607 | def format_children(self, indent): 608 | if not self.children: 609 | return ' []' 610 | 611 | return '\n'.join( 612 | ['', indent + '['] + 613 | [x.__str__(indent + 4 * ' ') for x in self.children] + 614 | [indent + ']']) 615 | 616 | 617 | class Block(ASTNode): 618 | 619 | """A sequence of other AST nodes, to be executed in order""" 620 | 621 | children = [] 622 | 623 | def __init__(self, context): 624 | self.children = [] 625 | 626 | while context.token_kind and not context.close_lines: 627 | if context.token_kind == 'literal': 628 | node = Literal 629 | else: 630 | node = Code 631 | self.children.append(node(context)) 632 | 633 | def execute(self, context): 634 | for x in self.children: 635 | x.execute(context) 636 | 637 | def __str__(self, indent=''): 638 | return indent + 'Block:' + self.format_children(indent) 639 | 640 | 641 | class Literal(ASTNode): 642 | 643 | """An AST node that generates literal text""" 644 | 645 | def __init__(self, context): 646 | self.text = context.token_text 647 | start_position = context.token_match.start(context.token_kind) 648 | self.start_line_number = context.pos_to_line(start_position) 649 | self.filename = context.filename 650 | context.next_token() 651 | 652 | def execute(self, context): 653 | context.append_text(self.text, self.filename, self.start_line_number) 654 | 655 | def __str__(self, indent=''): 656 | return '\n'.join( 657 | [indent + x for x in ['Literal:'] + 658 | strip_trailing_nl(self.text).split('\n')]) 659 | 660 | 661 | class Code(ASTNode): 662 | 663 | """An AST node that is evaluated as Python""" 664 | 665 | code = None 666 | children = () 667 | kind = None 668 | 669 | def __init__(self, context): 670 | 671 | source = '' 672 | source_line_count = 0 673 | 674 | def accumulate_code(): 675 | s = source + (context.code_start_line - source_line_count) * '\n' \ 676 | + textwrap.dedent(context.code_text) 677 | line_count = context.code_start_line + \ 678 | context.code_text.count('\n') 679 | context.next_token() 680 | return s, line_count 681 | 682 | eval_exec = 'exec' 683 | if context.token_kind.startswith('substitution'): 684 | eval_exec = 'eval' 685 | source, source_line_count = accumulate_code() 686 | source = '(' + source.strip() + ')' 687 | 688 | else: 689 | while context.token_kind == 'gybLinesOpen': 690 | source, source_line_count = accumulate_code() 691 | source += ' __children__[%d].execute(__context__)\n' % len( 692 | self.children) 693 | source_line_count += 1 694 | 695 | self.children += (Block(context),) 696 | 697 | if context.token_kind == 'gybLinesClose': 698 | context.next_token() 699 | 700 | if context.token_kind == 'gybLines': 701 | source, source_line_count = accumulate_code() 702 | 703 | # Only handle a substitution as part of this code block if 704 | # we don't already have some %-lines. 705 | elif context.token_kind == 'gybBlockOpen': 706 | 707 | # Opening ${...} and %{...}% constructs 708 | source, source_line_count = accumulate_code() 709 | 710 | self.filename = context.filename 711 | self.start_line_number = context.code_start_line 712 | self.code = compile(source, context.filename, eval_exec) 713 | self.source = source 714 | 715 | def execute(self, context): 716 | # Save __children__ from the local bindings 717 | save_children = context.local_bindings.get('__children__') 718 | # Execute the code with our __children__ in scope 719 | context.local_bindings['__children__'] = self.children 720 | context.local_bindings['__file__'] = self.filename 721 | result = eval(self.code, context.local_bindings) 722 | 723 | if context.local_bindings['__children__'] is not self.children: 724 | raise ValueError("The code is not allowed to mutate __children__") 725 | # Restore the bindings 726 | context.local_bindings['__children__'] = save_children 727 | 728 | # If we got a result, the code was an expression, so append 729 | # its value 730 | if result is not None \ 731 | or (isinstance(result, basestring) and result != ''): 732 | from numbers import Number, Integral 733 | result_string = None 734 | if isinstance(result, Number) and not isinstance(result, Integral): 735 | result_string = repr(result) 736 | else: 737 | result_string = str(result) 738 | context.append_text( 739 | result_string, self.filename, self.start_line_number) 740 | 741 | def __str__(self, indent=''): 742 | source_lines = re.sub(r'^\n', '', strip_trailing_nl( 743 | self.source), flags=re.MULTILINE).split('\n') 744 | if len(source_lines) == 1: 745 | s = indent + 'Code: {' + source_lines[0] + '}' 746 | else: 747 | s = indent + 'Code:\n' + indent + '{\n' + '\n'.join( 748 | indent + 4 * ' ' + l for l in source_lines 749 | ) + '\n' + indent + '}' 750 | return s + self.format_children(indent) 751 | 752 | 753 | def expand(filename, line_directive=_default_line_directive, **local_bindings): 754 | r"""Return the contents of the givepn template file, executed with the given 755 | local bindings. 756 | 757 | >>> from tempfile import NamedTemporaryFile 758 | >>> # On Windows, the name of a NamedTemporaryFile cannot be used to open 759 | >>> # the file for a second time if delete=True. Therefore, we have to 760 | >>> # manually handle closing and deleting this file to allow us to open 761 | >>> # the file by its name across all platforms. 762 | >>> f = NamedTemporaryFile(delete=False) 763 | >>> f.write( 764 | ... r'''--- 765 | ... % for i in range(int(x)): 766 | ... a pox on ${i} for epoxy 767 | ... % end 768 | ... ${120 + 769 | ... 770 | ... 3} 771 | ... abc 772 | ... ${"w\nx\nX\ny"} 773 | ... z 774 | ... ''') 775 | >>> f.flush() 776 | >>> result = expand( 777 | ... f.name, 778 | ... line_directive='//#sourceLocation(file: "%(file)s", ' + \ 779 | ... 'line: %(line)d)', 780 | ... x=2 781 | ... ).replace( 782 | ... '"%s"' % f.name, '"dummy.file"') 783 | >>> print(result, end='') 784 | //#sourceLocation(file: "dummy.file", line: 1) 785 | --- 786 | //#sourceLocation(file: "dummy.file", line: 3) 787 | a pox on 0 for epoxy 788 | //#sourceLocation(file: "dummy.file", line: 3) 789 | a pox on 1 for epoxy 790 | //#sourceLocation(file: "dummy.file", line: 5) 791 | 123 792 | //#sourceLocation(file: "dummy.file", line: 8) 793 | abc 794 | w 795 | x 796 | X 797 | y 798 | //#sourceLocation(file: "dummy.file", line: 10) 799 | z 800 | >>> f.close() 801 | >>> os.remove(f.name) 802 | """ 803 | with open(filename) as f: 804 | t = parse_template(filename, f.read()) 805 | d = os.getcwd() 806 | os.chdir(os.path.dirname(os.path.abspath(filename))) 807 | try: 808 | return execute_template( 809 | t, line_directive=line_directive, **local_bindings) 810 | finally: 811 | os.chdir(d) 812 | 813 | 814 | def parse_template(filename, text=None): 815 | r"""Return an AST corresponding to the given template file. 816 | 817 | If text is supplied, it is assumed to be the contents of the file, 818 | as a string. 819 | 820 | >>> print(parse_template('dummy.file', text= 821 | ... '''% for x in [1, 2, 3]: 822 | ... % if x == 1: 823 | ... literal1 824 | ... % elif x > 1: # add output line after this line to fix bug 825 | ... % if x == 2: 826 | ... literal2 827 | ... % end 828 | ... % end 829 | ... % end 830 | ... ''')) 831 | Block: 832 | [ 833 | Code: 834 | { 835 | for x in [1, 2, 3]: 836 | __children__[0].execute(__context__) 837 | } 838 | [ 839 | Block: 840 | [ 841 | Code: 842 | { 843 | if x == 1: 844 | __children__[0].execute(__context__) 845 | elif x > 1: # add output line after this line to fix bug 846 | __children__[1].execute(__context__) 847 | } 848 | [ 849 | Block: 850 | [ 851 | Literal: 852 | literal1 853 | ] 854 | Block: 855 | [ 856 | Code: 857 | { 858 | if x == 2: 859 | __children__[0].execute(__context__) 860 | } 861 | [ 862 | Block: 863 | [ 864 | Literal: 865 | literal2 866 | ] 867 | ] 868 | ] 869 | ] 870 | ] 871 | ] 872 | ] 873 | 874 | >>> print(parse_template( 875 | ... 'dummy.file', 876 | ... text='%for x in range(10):\n% print(x)\n%end\njuicebox')) 877 | Block: 878 | [ 879 | Code: 880 | { 881 | for x in range(10): 882 | __children__[0].execute(__context__) 883 | } 884 | [ 885 | Block: 886 | [ 887 | Code: {print(x)} [] 888 | ] 889 | ] 890 | Literal: 891 | juicebox 892 | ] 893 | 894 | >>> print(parse_template('/dummy.file', text= 895 | ... '''Nothing 896 | ... % if x: 897 | ... % for i in range(3): 898 | ... ${i} 899 | ... % end 900 | ... % else: 901 | ... THIS SHOULD NOT APPEAR IN THE OUTPUT 902 | ... ''')) 903 | Block: 904 | [ 905 | Literal: 906 | Nothing 907 | Code: 908 | { 909 | if x: 910 | __children__[0].execute(__context__) 911 | else: 912 | __children__[1].execute(__context__) 913 | } 914 | [ 915 | Block: 916 | [ 917 | Code: 918 | { 919 | for i in range(3): 920 | __children__[0].execute(__context__) 921 | } 922 | [ 923 | Block: 924 | [ 925 | Code: {(i)} [] 926 | Literal: 927 | 928 | ] 929 | ] 930 | ] 931 | Block: 932 | [ 933 | Literal: 934 | THIS SHOULD NOT APPEAR IN THE OUTPUT 935 | ] 936 | ] 937 | ] 938 | 939 | >>> print(parse_template('dummy.file', text='''% 940 | ... %for x in y: 941 | ... % print(y) 942 | ... ''')) 943 | Block: 944 | [ 945 | Code: 946 | { 947 | for x in y: 948 | __children__[0].execute(__context__) 949 | } 950 | [ 951 | Block: 952 | [ 953 | Code: {print(y)} [] 954 | ] 955 | ] 956 | ] 957 | 958 | >>> print(parse_template('dummy.file', text='''% 959 | ... %if x: 960 | ... % print(y) 961 | ... AAAA 962 | ... %else: 963 | ... BBBB 964 | ... ''')) 965 | Block: 966 | [ 967 | Code: 968 | { 969 | if x: 970 | __children__[0].execute(__context__) 971 | else: 972 | __children__[1].execute(__context__) 973 | } 974 | [ 975 | Block: 976 | [ 977 | Code: {print(y)} [] 978 | Literal: 979 | AAAA 980 | ] 981 | Block: 982 | [ 983 | Literal: 984 | BBBB 985 | ] 986 | ] 987 | ] 988 | 989 | >>> print(parse_template('dummy.file', text='''% 990 | ... %if x: 991 | ... % print(y) 992 | ... AAAA 993 | ... %# This is a comment 994 | ... %else: 995 | ... BBBB 996 | ... ''')) 997 | Block: 998 | [ 999 | Code: 1000 | { 1001 | if x: 1002 | __children__[0].execute(__context__) 1003 | # This is a comment 1004 | else: 1005 | __children__[1].execute(__context__) 1006 | } 1007 | [ 1008 | Block: 1009 | [ 1010 | Code: {print(y)} [] 1011 | Literal: 1012 | AAAA 1013 | ] 1014 | Block: 1015 | [ 1016 | Literal: 1017 | BBBB 1018 | ] 1019 | ] 1020 | ] 1021 | 1022 | >>> print(parse_template('dummy.file', text='''\ 1023 | ... %for x in y: 1024 | ... AAAA 1025 | ... %if x: 1026 | ... BBBB 1027 | ... %end 1028 | ... CCCC 1029 | ... ''')) 1030 | Block: 1031 | [ 1032 | Code: 1033 | { 1034 | for x in y: 1035 | __children__[0].execute(__context__) 1036 | } 1037 | [ 1038 | Block: 1039 | [ 1040 | Literal: 1041 | AAAA 1042 | Code: 1043 | { 1044 | if x: 1045 | __children__[0].execute(__context__) 1046 | } 1047 | [ 1048 | Block: 1049 | [ 1050 | Literal: 1051 | BBBB 1052 | ] 1053 | ] 1054 | Literal: 1055 | CCCC 1056 | ] 1057 | ] 1058 | ] 1059 | """ 1060 | return Block(ParseContext(filename, text)) 1061 | 1062 | 1063 | def execute_template( 1064 | ast, line_directive=_default_line_directive, **local_bindings): 1065 | r"""Return the text generated by executing the given template AST. 1066 | 1067 | Keyword arguments become local variable bindings in the execution context 1068 | 1069 | >>> root_directory = os.path.abspath('/') 1070 | >>> file_name = root_directory + 'dummy.file' 1071 | >>> ast = parse_template(file_name, text= 1072 | ... '''Nothing 1073 | ... % if x: 1074 | ... % for i in range(3): 1075 | ... ${i} 1076 | ... % end 1077 | ... % else: 1078 | ... THIS SHOULD NOT APPEAR IN THE OUTPUT 1079 | ... ''') 1080 | >>> out = execute_template(ast, 1081 | ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', 1082 | ... x=1) 1083 | >>> out = out.replace(file_name, "DUMMY-FILE") 1084 | >>> print(out, end="") 1085 | //#sourceLocation(file: "DUMMY-FILE", line: 1) 1086 | Nothing 1087 | //#sourceLocation(file: "DUMMY-FILE", line: 4) 1088 | 0 1089 | //#sourceLocation(file: "DUMMY-FILE", line: 4) 1090 | 1 1091 | //#sourceLocation(file: "DUMMY-FILE", line: 4) 1092 | 2 1093 | 1094 | >>> ast = parse_template(file_name, text= 1095 | ... '''Nothing 1096 | ... % a = [] 1097 | ... % for x in range(3): 1098 | ... % a.append(x) 1099 | ... % end 1100 | ... ${a} 1101 | ... ''') 1102 | >>> out = execute_template(ast, 1103 | ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', 1104 | ... x=1) 1105 | >>> out = out.replace(file_name, "DUMMY-FILE") 1106 | >>> print(out, end="") 1107 | //#sourceLocation(file: "DUMMY-FILE", line: 1) 1108 | Nothing 1109 | //#sourceLocation(file: "DUMMY-FILE", line: 6) 1110 | [0, 1, 2] 1111 | 1112 | >>> ast = parse_template(file_name, text= 1113 | ... '''Nothing 1114 | ... % a = [] 1115 | ... % for x in range(3): 1116 | ... % a.append(x) 1117 | ... % end 1118 | ... ${a} 1119 | ... ''') 1120 | >>> out = execute_template(ast, 1121 | ... line_directive='#line %(line)d "%(file)s"', x=1) 1122 | >>> out = out.replace(file_name, "DUMMY-FILE") 1123 | >>> print(out, end="") 1124 | #line 1 "DUMMY-FILE" 1125 | Nothing 1126 | #line 6 "DUMMY-FILE" 1127 | [0, 1, 2] 1128 | """ 1129 | execution_context = ExecutionContext( 1130 | line_directive=line_directive, **local_bindings) 1131 | ast.execute(execution_context) 1132 | return ''.join(execution_context.result_text) 1133 | 1134 | 1135 | def main(): 1136 | """ 1137 | Lint this file. 1138 | >>> import sys 1139 | >>> gyb_path = os.path.realpath(__file__).replace('.pyc', '.py') 1140 | >>> sys.path.append(os.path.dirname(gyb_path)) 1141 | >>> import python_lint 1142 | >>> python_lint.lint([gyb_path], verbose=False) 1143 | 0 1144 | """ 1145 | 1146 | import argparse 1147 | import sys 1148 | 1149 | parser = argparse.ArgumentParser( 1150 | formatter_class=argparse.RawDescriptionHelpFormatter, 1151 | description='Generate Your Boilerplate!', epilog=''' 1152 | A GYB template consists of the following elements: 1153 | 1154 | - Literal text which is inserted directly into the output 1155 | 1156 | - %% or $$ in literal text, which insert literal '%' and '$' 1157 | symbols respectively. 1158 | 1159 | - Substitutions of the form ${}. The Python 1160 | expression is converted to a string and the result is inserted 1161 | into the output. 1162 | 1163 | - Python code delimited by %{...}%. Typically used to inject 1164 | definitions (functions, classes, variable bindings) into the 1165 | evaluation context of the template. Common indentation is 1166 | stripped, so you can add as much indentation to the beginning 1167 | of this code as you like 1168 | 1169 | - Lines beginning with optional whitespace followed by a single 1170 | '%' and Python code. %-lines allow you to nest other 1171 | constructs inside them. To close a level of nesting, use the 1172 | "%end" construct. 1173 | 1174 | - Lines beginning with optional whitespace and followed by a 1175 | single '%' and the token "end", which close open constructs in 1176 | %-lines. 1177 | 1178 | Example template: 1179 | 1180 | - Hello - 1181 | %{ 1182 | x = 42 1183 | def succ(a): 1184 | return a+1 1185 | }% 1186 | 1187 | I can assure you that ${x} < ${succ(x)} 1188 | 1189 | % if int(y) > 7: 1190 | % for i in range(3): 1191 | y is greater than seven! 1192 | % end 1193 | % else: 1194 | y is less than or equal to seven 1195 | % end 1196 | 1197 | - The End. - 1198 | 1199 | When run with "gyb -Dy=9", the output is 1200 | 1201 | - Hello - 1202 | 1203 | I can assure you that 42 < 43 1204 | 1205 | y is greater than seven! 1206 | y is greater than seven! 1207 | y is greater than seven! 1208 | 1209 | - The End. - 1210 | ''' 1211 | ) 1212 | parser.add_argument( 1213 | '-D', action='append', dest='defines', metavar='NAME=VALUE', 1214 | default=[], 1215 | help='''Bindings to be set in the template's execution context''') 1216 | 1217 | parser.add_argument( 1218 | 'file', type=argparse.FileType(), 1219 | help='Path to GYB template file (defaults to stdin)', nargs='?', 1220 | default=sys.stdin) 1221 | parser.add_argument( 1222 | '-o', dest='target', type=argparse.FileType('w'), 1223 | help='Output file (defaults to stdout)', default=sys.stdout) 1224 | parser.add_argument( 1225 | '--test', action='store_true', 1226 | default=False, help='Run a self-test') 1227 | parser.add_argument( 1228 | '--verbose-test', action='store_true', 1229 | default=False, help='Run a verbose self-test') 1230 | parser.add_argument( 1231 | '--dump', action='store_true', 1232 | default=False, help='Dump the parsed template to stdout') 1233 | parser.add_argument( 1234 | '--line-directive', 1235 | default=_default_line_directive, 1236 | help=''' 1237 | Line directive format string, which will be 1238 | provided 2 substitutions, `%%(line)d` and `%%(file)s`. 1239 | 1240 | Example: `// #sourceLocation(file: "%%(file)s", line: %%(line)d)` 1241 | ''') 1242 | 1243 | args = parser.parse_args(sys.argv[1:]) 1244 | 1245 | if args.test or args.verbose_test: 1246 | import doctest 1247 | selfmod = sys.modules[__name__] 1248 | if doctest.testmod(selfmod, verbose=args.verbose_test or None).failed: 1249 | sys.exit(1) 1250 | 1251 | bindings = dict(x.split('=', 1) for x in args.defines) 1252 | ast = parse_template(args.file.name, args.file.read()) 1253 | if args.dump: 1254 | print(ast) 1255 | # Allow the template to open files and import .py files relative to its own 1256 | # directory 1257 | os.chdir(os.path.dirname(os.path.abspath(args.file.name))) 1258 | sys.path = ['.'] + sys.path 1259 | 1260 | args.target.write(execute_template(ast, args.line_directive, **bindings)) 1261 | 1262 | 1263 | if __name__ == '__main__': 1264 | main() 1265 | -------------------------------------------------------------------------------- /bin/gyb/gyb.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuniwak/MemoryLeakTestKit/11a1ca08305f5da106327d6e42bce2de431bcf96/bin/gyb/gyb.pyc -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | set -o pipefail 3 | 4 | 5 | ROOT_DIR="$(cd "$(dirname "$0")/.."; pwd)" 6 | BIN_DIR="${ROOT_DIR}/bin" 7 | GYB="${BIN_DIR}/gyb/gyb" 8 | 9 | SOURCE_DIR="${ROOT_DIR}/Sources" 10 | 11 | 12 | for gyb_file in $(find "${SOURCE_DIR}" -name "*.gyb"); do 13 | out_file="$(dirname "${gyb_file}")/$(basename -s ".swift.gyb" "${gyb_file}").generated.swift" 14 | "${GYB}" "${gyb_file}" -o "${out_file}" --line-directive="" 15 | done 16 | --------------------------------------------------------------------------------