Repository: uber/SwiftCodeSan Branch: master Commit: b586387590bd Files: 40 Total size: 185.8 KB Directory structure: gitextract__6ffq4gh/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── NOTICE.txt ├── Package.swift ├── README.md ├── Sources/ │ ├── SwiftCodeSan/ │ │ ├── Executor.swift │ │ └── main.swift │ └── SwiftCodeSanKit/ │ ├── Core/ │ │ ├── AccessLevelRewriter.swift │ │ ├── DeclMetaTypes.swift │ │ ├── DeclRemover.swift │ │ ├── DeclVisitor.swift │ │ ├── ImportRewriter.swift │ │ └── RefChecker.swift │ ├── FileParsers/ │ │ └── DeclParser.swift │ ├── FileUpdaters/ │ │ └── DeclUpdater.swift │ ├── Operations/ │ │ ├── RemoveDeadDecls.swift │ │ ├── RemoveUnusedImports.swift │ │ └── UpdateAccessLevels.swift │ └── Utils/ │ ├── Extensions/ │ │ ├── FileManagerExtensions.swift │ │ ├── SequenceExtensions.swift │ │ ├── StringExtensions.swift │ │ ├── SyntaxExtensions.swift │ │ └── SyntaxParserExtensions.swift │ ├── Logger.swift │ └── Scanner.swift ├── Tests/ │ ├── SwiftCodeSanTestCase.swift │ └── TestClasses/ │ ├── Fixtures/ │ │ ├── test0.swift │ │ ├── test1.swift │ │ ├── test2.swift │ │ ├── test3.swift │ │ ├── test4.swift │ │ ├── test5.swift │ │ ├── test6.swift │ │ ├── test7.swift │ │ └── test8.swift │ └── SwiftCodeSanTests.swift └── install-script.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [master] pull_request: branches: [master] jobs: macos: runs-on: macOS-latest steps: - name: Checkout uses: actions/checkout@v1 - name: Build run: swift build -v - name: Test run: swift test -v -c release ================================================ FILE: .gitignore ================================================ ## Xcode projects *.xcodeproj *.xcworkspace ## Build generated build/ DerivedData/ ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other .DS_Store *.moved-aside *.xccheckout *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. Packages/ Package.pins Package.resolved .build/ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mobile-open-source@uber.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mobile-open-source@uber.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: NOTICE.txt ================================================ SwiftMockGen depends on the following libraries: Swift Package Manager (https://github.com/apple/swift-package-manager) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.2 import PackageDescription let package = Package( name: "SwiftCodeSan", platforms: [ .macOS(.v10_15), ], products: [ .executable(name: "SwiftCodeSan", targets: ["SwiftCodeSan"]), .library(name: "SwiftCodeSanKit", targets: ["SwiftCodeSanKit"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "1.0.2")), .package(name: "SwiftSyntax", url: "https://github.com/apple/swift-syntax.git", .branch("swift-5.6-RELEASE")) ], targets: [ .target( name: "SwiftCodeSan", dependencies: [ "SwiftCodeSanKit", .product(name: "ArgumentParser", package: "swift-argument-parser"), ]), .target( name: "SwiftCodeSanKit", dependencies: [ .product(name: "SwiftSyntax", package: "SwiftSyntax"), .product(name: "SwiftSyntaxParser", package: "SwiftSyntax"), ] ), .testTarget( name: "SwiftCodeSanTests", dependencies: [ "SwiftCodeSanKit", ], path: "Tests" ) ] ) ================================================ FILE: README.md ================================================ # ![](Images/logo.png) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2964/badge)](https://bestpractices.coreinfrastructure.org/projects/2964) [![Build Status](https://github.com/uber/SwiftCodeSan/workflows/CI/badge.svg)](https://github.com/uber/SwiftCodeSan/actions) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fuber%2FSwiftCodeSan.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fuber%2FSwiftCodeSan?ref=badge_shield) # Welcome to SwiftCodeSan **SwiftCodeSan** is a tool that "sanitizes" code written in Swift. It has support for removing dead code (unreferenced decls) and unused imports, and narrowing access levels (public to internal), which will not only help clean up the codebase but also reduce the build time and the binary size. It uses `SwiftSyntax` for parsing and uses concurrency for faster performance. Unlike other tools, `SwiftCodeSan` does not involve compiling; it handles reference checks directly. This eliminates the need to compile the entire project before running an analysis, which can take a long time for codebases like Uber's (~3M LoC). Main objectives of `SwiftCodeSan` are accuracy, performance, and ease of use. It's a lightweight commandline tool, which uses the `SwiftCodeSanKit` framework underneath. It can be used as a standalone tool or integrated into other tools such as a linter. Try `SwiftCodeSan` and clean up your codebase, and see an improvement in the code quality and the build time. ## Motivation Main objectives of `SwiftCodeSan` are accuracy, performance, flexibility, and ease of use. There aren't many 3rd party tools that perform fast on a large codebase containing, for example, over 3M LoC. They require building the entire projects on Xcode (for index stores), and take several hours to run analyses. The results contain false postives and negatives. They don't provide support to modify files directly with the results, and lack features such as finding unused imports or redundant access levels. `SwiftCodeSan` was built for scalability and performance so running analyses takes a few minutes instead of hours. Since it does not require compiling the codebase, it can also run on code being developed with any IDEs (not just Xcode). It's a lightweight commandline tool, and uses a minimal set of frameworks necessary (see the Used Libraries below) to keep the code lean and efficient. It provides an input option to directly modify files with results, and features other than removing dead code, such as updating access levels and removing unused import statments. ## Disclaimer This project may contain unstable APIs which may not be ready for general use. Support and/or new releases may be limited. ## System Requirements * Swift 5.3 or later * Xcode 12.0 or later * MacOS 10.15.4 or later * Support is included for the Swift Package Manager ## Build / Install Option 1: Clone and build ``` $ git clone https://github.com/uber/SwiftCodeSan.git $ cd SwiftCodeSan $ swift build -c release $ .build/release/SwiftCodeSan -h // see commandline input options below ``` Instead of calling the binary `SwiftCodeSan` built in `.build/release`, you can copy the executable into a directory that is part of your `PATH` environment variable and call `SwiftCodeSan`. Or use Xcode, via following. ``` $ swift package generate-xcodeproj ``` ## Run `SwiftCodeSan` is a commandline executable. To run it, pass in a list of the source file directories or file paths of a build target, and the destination filepath for the mock output. To see other arguments to the commandline, run `SwiftCodeSan --help`. ``` ./SwiftCodeSan --files-to-modules [file_to_module_list] --remove-deadcode --in-place ``` The `file_to_module_list` contains a map of source file paths to corresponding module names. Other input options are `--remove-unused-imports` and `--update-access-levels`. If `--in-place` is set, files will be modified directly. Use --help to see the complete list of argument options. ## Add SwiftCodeSanKit to your project Option 1: SPM ```swift dependencies: [ .package(url: "https://github.com/uber/SwiftCodeSan.git", from: "0.0.1"), ], targets: [ .target(name: "MyTarget", dependencies: ["SwiftCodeSanKit"]), ] ``` ## Distribution The `install-script.sh` will build and package up the `SwiftCodeSan` binary and other necessary resources in the same bundle. ``` $ ./install-script.sh -h // see input options $ ./install-script.sh -s [source dir] -t SwiftCodeSan -d [destination dir] -o [output filename] ``` This will create a tarball for distribution, which contains the `SwiftCodeSan` executable along with a necessary SwiftSyntax parser dylib (lib_InternalSwiftSyntaxParser.dylib). This allows running `SwiftCodeSan` without depending on where the dylib lives. ## Used libraries [SwiftSyntax](https://github.com/apple/swift-syntax) | [SPM](https://github.com/swift-package-manager) ## How to contribute to SwiftCodeSan See [CONTRIBUTING](CONTRIBUTING.md) for more info. ## Report any issues If you run into any problems, please file a git issue. Please include: * The OS version (e.g. macOS 10.15.6) * The Swift version installed on your machine (from `swift --version`) * The Xcode version * The specific release version of this source code (you can use `git tag` to get a list of all the release versions or `git log` to get a specific commit sha) * Any local changes on your machine ## License SwiftCodeSan is licensed under Apache License 2.0. See [LICENSE](LICENSE.txt) for more information. Copyright (C) 2017 Uber Technologies Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Sources/SwiftCodeSan/Executor.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import ArgumentParser import SwiftCodeSanKit struct Executor: ParsableCommand { static var configuration = CommandConfiguration(commandName: "SwiftCodeSan", abstract: "SwiftCodeSan: Code Sanitizer for Swift.") private enum Operation: EnumerableFlag { case removeDeadcode case removeUnusedImports case updateAccessLevels static func help(for value: Executor.Operation) -> ArgumentHelp? { switch value { case .removeDeadcode: return "If set, it will remove dead code and generate a report in the logfile. If an --in-place option is set, files will be modified directly." case .removeUnusedImports: return "If set, it will remove unused import statements and generate a report in the logfile. If an --in-place option is set, files will be modified directly." case .updateAccessLevels: return "If set, it will remove unnecessary public or open access levels from decls and generate a report in the logfile. If an --in-place option is set, files will be modified directly." } } } // MARK: - Private @Option(name: [.long, .customShort("v")], help: "The logging level to use. Default is set to 0 (info only). Set 1 for verbose, 2 for warning, and 3 for error.") private var loggingLevel: Int = 0 @Option(name: .customLong("logfile"), help: "Log file path containing the analysis results. If no value is given, it will be saved to a tmp file.", completion: .file()) private var logFilePath: String? @Option(name: [.customLong("files-to-modules"), .short], parsing: .upToNextOption, help: "File paths each containing a map of source files and corresponding module names.", completion: .file()) private var fileLists: [String] = [] @Option(name: .customLong("syslib-list"), parsing: .upToNextOption, help: "File paths each containing a list of (weak) system frameworks.", completion: .file()) private var syslibLists: [String] = [] @Option(name: .customLong("test-list"), parsing: .upToNextOption, help: "File paths each containing a list of test files.", completion: .file()) private var testFileLists: [String] = [] @Option(name: [.long, .short], help: "The root path. If given, it will be prepended to the source file paths.", completion: .file()) private var root: String? @Option(name: [.long, .customShort("j")], help: "Maximum number of threads to execute concurrently (default = number of cores on the running machine)") private var concurrencyLimit: Int? @Option(name: [.long, .short], parsing: .upToNextOption, help: "List of declarations to whitelist (separated by a comma or a space)", completion: .file()) private var whitelistDecls: [String] = [] @Option(name: .long, parsing: .upToNextOption, help: "List of declarations with given prefixes to whitelist (separated by a comma or a space)", completion: .file()) private var whitelistDeclsPrefix: [String] = [] @Option(name: .long, parsing: .upToNextOption, help: "List of declarations with given suffixes to whitelist (separated by a comma or a space).", completion: .file()) private var whitelistDeclsSuffix: [String] = [] @Option(name: .long, parsing: .upToNextOption, help: "List of declarations with given parent types to whitelist (separated by a comma or a space).", completion: .file()) private var whitelistParents: [String] = [] @Option(name: .long, parsing: .upToNextOption, help: "List of declarations in the given modules to whitelist (separated by a comma or a space).", completion: .file()) private var whitelistModules: [String] = [] @Option(name: .long, parsing: .upToNextOption, help: "List of declarations in the modules with given prefixes to whitelist (separated by a comma or a space).", completion: .file()) private var whitelistModulesPrefix: [String] = [] @Option(name: .long, parsing: .upToNextOption, help: "List of declarations in the modules with given suffixes to whitelist (separated by a comma or a space).", completion: .file()) private var whitelistModulesSuffix: [String] = [] @Option(name: .long, parsing: .upToNextOption, help: "List of member declarations with given names to whitelist (separated by a comma or a space).", completion: .file()) private var whitelistMembers: [String] = [] @Option(name: [.long, .short], help: "If set, files modified within the set number of days (leading up to today) will be whitelisted, i.e. all declarations in such files will be whitelisted.") private var thresholdDays: Int? @Flag private var operation: Operation @Option(name: .customLong("remove-annotation"), help: "If set, it will remove the annotation passed in from decls and generate a report in the logfile. If an --in-place option is set, files will be modified directly. ") private var deleteAnnotation: String? @Flag(name: [.customLong("in-place"), .short], help: "If set, given source files will be modified with results.") private var inplace: Bool = false @Flag(name: .customLong("in-place-tests"), help: "If set, given test files will be modified with results.") private var inplaceTests: Bool = false @Flag(name: .long, help: "If set, only top level decls will be parsed/used for analysis.") private var topDeclsOnly: Bool = false private func fullPath(_ path: String) -> String { if path.hasPrefix("/") { return path } if path.hasPrefix("~") { let home = FileManager.default.homeDirectoryForCurrentUser.path return path.replacingOccurrences(of: "~", with: home, range: path.range(of: "~")) } return FileManager.default.currentDirectoryPath + "/" + path } mutating func run() throws { minLogLevel = loggingLevel var filesToModules = [String: String]() fileLists.forEach { arg in let line = arg.components(separatedBy: ":") if let key = line.first, let val = line.last { filesToModules[key] = val } } let whitelist = Whitelist(thresholdDays: thresholdDays, decls: whitelistDecls, declsPrefix: whitelistDeclsPrefix, declsSuffix: whitelistDeclsSuffix, modules: [whitelistModules, syslibLists].compactMap{$0}.flatMap{$0}, modulesPrefix: whitelistModulesPrefix, modulesSuffix: whitelistModulesSuffix, inheritedTypes: whitelistParents, members: whitelistMembers) execute(with: filesToModules, nil, root, logFilePath, inplace, inplaceTests, topDeclsOnly, concurrencyLimit, whitelist, operation, deleteAnnotation) } private func execute(with filesToModules: [String: String], _ testfiles: [String]?, _ root: String?, _ logfile: String?, _ inplace: Bool, _ inplaceTests: Bool, _ topDeclsOnly: Bool, _ jobs: Int?, _ whitelist: Whitelist?, _ operation: Operation, _ deleteAnnotation: String?) { switch operation { case .removeUnusedImports: removeUnusedImports(fileToModuleMap: filesToModules, whitelist: whitelist, topDeclsOnly: topDeclsOnly, inplace: inplace, logFilePath: logfile, concurrencyLimit: jobs) case .removeDeadcode: removeDeadDecls(filesToModules: filesToModules, whitelist: whitelist, topDeclsOnly: topDeclsOnly, inplace: inplace, testFiles: testfiles, inplaceTests: inplaceTests, logFilePath: logfile, concurrencyLimit: jobs, onCompletion: {}) case .updateAccessLevels: updateAccessLevels(filesToModules: filesToModules, whitelist: whitelist, inplace: inplace, concurrencyLimit: jobs, onCompletion: {}) } } } ================================================ FILE: Sources/SwiftCodeSan/main.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import ArgumentParser import Foundation func main() { let inputs = Array(CommandLine.arguments.dropFirst()) print("Start...") Executor.main(inputs) print("Done.") } main() ================================================ FILE: Sources/SwiftCodeSanKit/Core/AccessLevelRewriter.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax /** Updates access levels in the source code */ public final class AccessLevelRewriter: SyntaxRewriter { var decls: [DeclMetadata] let path: String let module: String public init(_ path: String, module: String?, decls: [DeclMetadata]) { self.path = path self.module = module ?? "" self.decls = decls } private func updateModifiers(_ name: String, fullName: String, description: String, declType: DeclType, modifiers: ModifierListSyntax?) -> (ModifierListSyntax, Bool)? { let contains = decls.contains(where: { (d: DeclMetadata) -> Bool in return d.name == name && d.fullName == fullName && d.declDescription == description && d.declType == declType }) if contains { var isModified = false var list = [DeclModifierSyntax]() if let modifiers = modifiers { for modifier in modifiers { if modifier.name.text == String.public || modifier.name.text == String.open { let updatedAcl = modifier.name.withKind(.stringLiteral("")).withoutTrailingTrivia() let updatedModifier = SyntaxFactory.makeDeclModifier(name: updatedAcl, detailLeftParen: modifier.detailLeftParen, detail: modifier.detail, detailRightParen: modifier.detailRightParen) isModified = true list.append(updatedModifier) } else { if isModified, modifier.name.text == String.internal, modifier.detail?.text == "set" { let updatedAcl = modifier.name.withKind(.stringLiteral("")).withoutTrailingTrivia() let updatedModifier = SyntaxFactory.makeDeclModifier(name: updatedAcl, detailLeftParen: nil, detail: nil, detailRightParen: nil) list.append(updatedModifier) } else { list.append(modifier) } } } } return (SyntaxFactory.makeModifierList(list), isModified) } return nil } override public func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } override public func visit(_ node: EnumDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } override public func visit(_ node: StructDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } override public func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } override public func visit(_ node: ClassDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } override public func visit(_ node: FunctionDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } override public func visit(_ node: SubscriptDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } override public func visit(_ node: InitializerDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } override public func visit(_ node: VariableDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } override public func visit(_ node: TypealiasDeclSyntax) -> DeclSyntax { var mutableNode = node if let (updatedModifier, isModified) = updateModifiers(node.name, fullName: node.fullName, description: node.description, declType: node.declType, modifiers: node.modifiers) { if isModified { mutableNode.modifiers = updatedModifier } return DeclSyntax(mutableNode) } return super.visit(node) } } ================================================ FILE: Sources/SwiftCodeSanKit/Core/DeclMetaTypes.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax /** Decl metadata needed for decls in source code being parsed */ public typealias DeclMap = [String: [DeclMetadata]] public enum DeclType { case protocolType, classType, extensionType, structType, enumType case typealiasType, patType case varType, subscriptType, funcType, initType, operatorType, enumCaseType case other } extension DeclType { var isEncloserType: Bool { if self == .protocolType || self == .classType || self == .extensionType || self == .structType || self == .enumType { return true } return false } } public final class DeclMetadata: Hashable { let name: String var type: String let fullName: String let declType: DeclType var inheritedTypes: [String] let boundTypes: [String] let boundTypesAL: [String] var members: [DeclMetadata] = [] let path: String let module: String let imports: [String] var encloser: String var declDescription: String var annotated: Bool = false var isOverride: Bool var isExtensionMember: Bool = false var isPublicOrOpen: Bool var shouldExpose: Bool = false var visited: Bool = false var used: Bool = false public func hash(into hasher: inout Hasher) { hasher.combine(fullName) hasher.combine(declType) hasher.combine(encloser) hasher.combine(path) hasher.combine(module) } public static func == (lhs: DeclMetadata, rhs: DeclMetadata) -> Bool { if lhs.name == rhs.name, lhs.type == rhs.type, lhs.fullName == rhs.fullName, lhs.declType == rhs.declType, lhs.encloser == rhs.encloser, lhs.path == rhs.path, lhs.module == rhs.module { return true } return false } public init(path: String, module: String, imports: [String], encloser: String, name: String, type: String, fullName: String, description: String, declType: DeclType, inheritedTypes: [String], boundTypes: [String], boundTypesAL: [String], isPublicOrOpen: Bool, isOverride: Bool, annotated: Bool = false, used: Bool) { self.path = path self.module = module self.imports = imports self.encloser = encloser self.name = name self.type = type self.fullName = fullName self.declDescription = description self.declType = declType self.inheritedTypes = inheritedTypes self.boundTypes = boundTypes self.boundTypesAL = boundTypesAL self.annotated = annotated self.isPublicOrOpen = isPublicOrOpen self.isOverride = isOverride } } struct AnnotationMetadata { var module: String? var typeAliases: [String: String]? var varTypes: [String: String]? } public struct Whitelist { public let thresholdDays: Int? public let decls: [String]? public let declsPrefix: [String]? public let declsSuffix: [String]? public let modules: [String]? public let modulesPrefix: [String]? public let modulesSuffix: [String]? public let inheritedTypes: [String]? public let members: [String]? public init(thresholdDays: Int?, decls: [String]?, declsPrefix: [String]?, declsSuffix: [String]?, modules: [String]?, modulesPrefix: [String]?, modulesSuffix: [String]?, inheritedTypes: [String]?, members: [String]?) { self.thresholdDays = thresholdDays self.decls = decls self.declsPrefix = declsPrefix self.declsSuffix = declsSuffix self.modules = modules self.modulesPrefix = modulesPrefix self.modulesSuffix = modulesSuffix self.inheritedTypes = inheritedTypes self.members = members } func declWhitelisted(name: String, isMember: Bool, module: String?, parents: [String]?, path: String?) -> Bool { if let module = module { if let list = modules, list.contains(module) { return true } if let list = modulesPrefix { let moduleHasPrefix = !list.filter{module.hasPrefix($0)}.isEmpty if moduleHasPrefix { return true } } if let list = modulesSuffix { let moduleHasSuffix = !list.filter{module.hasSuffix($0)}.isEmpty if moduleHasSuffix { return true } } } if let parents = parents, let list = inheritedTypes { let inParentsList = !list.filter{ parents.contains($0) }.isEmpty if inParentsList { return true } } if isMember { if let list = members, list.contains(name) { return true } } else { if let list = decls, list.contains(name) { return true } if let list = declsPrefix { let declHasPrefix = !list.filter { name.hasPrefix($0) }.isEmpty if declHasPrefix { return true } } if let list = declsSuffix { let declHasSuffix = !list.filter { name.hasSuffix($0) }.isEmpty if declHasSuffix { return true } } } return false } } public func flatten(declMap: DeclMap) -> DeclMap { var flatDeclMap = DeclMap() for (k, vals) in declMap { for v in vals { if flatDeclMap[k] == nil { flatDeclMap[k] = [] } if flatDeclMap[k]?.contains(v) ?? false { } else { flatDeclMap[k]?.append(v) } for m in v.members { if flatDeclMap[m.name] == nil { flatDeclMap[m.name] = [] } flatDeclMap[m.name]?.append(m) } } } return flatDeclMap } ================================================ FILE: Sources/SwiftCodeSanKit/Core/DeclRemover.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax /** Removes unused decls */ public final class DeclRemover: SyntaxRewriter { let path: String let decls: [DeclMetadata] public init(_ path: String, decls: [DeclMetadata]) { self.path = path self.decls = decls } override public func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankExtensionDecl()) } return super.visit(node) } override public func visit(_ node: EnumDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankEnumDecl()) } return super.visit(node) } override public func visit(_ node: StructDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankStructDecl()) } return super.visit(node) } override public func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankProtocolDecl()) } return super.visit(node) } override public func visit(_ node: ClassDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankClassDecl()) } return super.visit(node) } override public func visit(_ node: FunctionDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankFunctionDecl()) } return super.visit(node) } override public func visit(_ node: SubscriptDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankSubscriptDecl()) } return super.visit(node) } override public func visit(_ node: InitializerDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankInitializerDecl()) } return super.visit(node) } override public func visit(_ node: VariableDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankVariableDecl()) } return super.visit(node) } override public func visit(_ node: TypealiasDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankTypealiasDecl()) } return super.visit(node) } override public func visit(_ node: AssociatedtypeDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankAssociatedtypeDecl()) } return super.visit(node) } override public func visit(_ node: EnumCaseDeclSyntax) -> DeclSyntax { if shouldRemove(node.name, fullName: node.fullName, description: node.description, declType: node.declType) { return DeclSyntax(SyntaxFactory.makeBlankEnumCaseDecl()) } return super.visit(node) } private func shouldRemove(_ name: String, fullName: String, description: String, declType: DeclType) -> Bool { let inList = decls.contains(where: { (d: DeclMetadata) -> Bool in return d.name == name && d.fullName == fullName && d.declDescription == description && d.declType == declType }) return inList } } ================================================ FILE: Sources/SwiftCodeSanKit/Core/DeclVisitor.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax /** Visit decls in source code being parsed */ final class DeclVisitor: SyntaxVisitor { var declMap = DeclMap() let path: String let module: String let topDeclsOnly: Bool let whitelistPath: Bool let whitelist: Whitelist? var importedModules = [String]() init(_ path: String, module: String?, topDeclsOnly: Bool, whitelistPath: Bool, whitelist: Whitelist?) { self.whitelist = whitelist self.whitelistPath = whitelistPath self.path = path self.module = module ?? "" self.topDeclsOnly = topDeclsOnly } override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { updateDecl(node, description: node.description, members: topDeclsOnly ? nil : node.members.members) return .skipChildren } override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { if node.attributesDescription.contains(String.propertyWrapper) { return .skipChildren } updateDecl(node, description: node.description, members: topDeclsOnly ? nil : node.members.members) return .visitChildren } override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { if node.attributesDescription.contains(String.propertyWrapper) { return .skipChildren } updateDecl(node, description: node.description, members: topDeclsOnly ? nil : node.members.members) return .skipChildren } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { updateDecl(node, description: node.description, members: topDeclsOnly ? nil : node.members.members) return .skipChildren } override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { updateDecl(node, description: node.description, members: topDeclsOnly ? nil : node.members.members) return .skipChildren } override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { importedModules.append(node.path.description.trimmed) return .visitChildren } override func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind { if let item = node.item.as(FunctionDeclSyntax.self) { updateDecl(item, description: item.description, members: nil) return .skipChildren } else if let _ = node.item.as(OperatorDeclSyntax.self) { return .skipChildren } else if let item = node.item.as(VariableDeclSyntax.self) { updateDecl(item, description: item.description, members: nil) return .skipChildren } else if let item = node.item.as(TypealiasDeclSyntax.self) { updateDecl(item, description: item.description, members: nil) return .skipChildren } return .visitChildren } private func memberDecls(_ decl: DeclSyntax, encloser: String, encloserDeclType: DeclType, encloserWhitelisted: Bool) -> [DeclMetadata] { let mdecls = decl.declMetadatas(path: path, module: module, encloser: encloser, description: decl.description, imports: importedModules) for mdecl in mdecls { if encloserDeclType == .extensionType { mdecl.isExtensionMember = true } let memberWhitelisted = whitelist?.declWhitelisted(name: mdecl.name, isMember: true, module: nil, parents: nil, path: mdecl.path) ?? false if encloserWhitelisted || memberWhitelisted || mdecl.declType == .initType || mdecl.declType == .subscriptType || mdecl.declType == .operatorType { if mdecl.isPublicOrOpen { mdecl.shouldExpose = true } mdecl.used = true } } return mdecls } private func updateDecl(_ item: DeclProtocol, description: String, members: MemberDeclListSyntax?) { let decls = item.declMetadatas(path: path, module: module, encloser: "", description: description, imports: importedModules) for decl in decls { var shouldWhitelist = (decl.declType == .operatorType) if !shouldWhitelist, !decl.name.isEmpty { if let whitelist = whitelist, whitelist.declWhitelisted(name: decl.name, isMember: false, module: module, parents: decl.inheritedTypes, path: decl.path) { // whitelisted so don't add to declMap shouldWhitelist = true } } if shouldWhitelist { if decl.isPublicOrOpen { decl.shouldExpose = true } decl.used = true } if let members = members { var list = [DeclMetadata]() for m in members { if let ifconfig = m.decl.as(IfConfigDeclSyntax.self) { for clause in ifconfig.clauses { if let clauseMembers = clause.elements.as(MemberDeclListSyntax.self) { for el in clauseMembers { let mdecls = memberDecls(el.decl, encloser: decl.name, encloserDeclType: decl.declType, encloserWhitelisted: shouldWhitelist) list.append(contentsOf: mdecls) } } } } else { let mdecls = memberDecls(m.decl, encloser: decl.name, encloserDeclType: decl.declType, encloserWhitelisted: shouldWhitelist) list.append(contentsOf: mdecls) } } decl.members = list } if !decl.name.isEmpty, declMap[decl.name] == nil { declMap[decl.name] = [] } declMap[decl.name]?.append(decl) } } } ================================================ FILE: Sources/SwiftCodeSanKit/Core/ImportRewriter.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax /** Updates import statements in source code */ public final class ImportRewriter: SyntaxRewriter { let unused: [String] public init(_ path: String, unusedModules: [String]?) { self.unused = unusedModules ?? [] } override public func visit(_ node: ImportDeclSyntax) -> DeclSyntax { var remove = false let str = node.path.description.trimmed if unused.contains(str) { remove = true } else { for t in node.path.tokens { if unused.contains(t.text) { remove = true } } } if remove { if let trivia = node.importTok.leadingTrivia { let t = SyntaxFactory.makeUnknown("", leadingTrivia: trivia, trailingTrivia: Trivia(pieces: [])) let ret = SyntaxFactory.makeImportDecl(attributes: nil, modifiers: nil, importTok: t, importKind: nil, path: SyntaxFactory.makeAccessPath([])) return DeclSyntax(ret) } else { let ret = SyntaxFactory.makeBlankImportDecl() return DeclSyntax(ret) } } return super.visit(node) } } ================================================ FILE: Sources/SwiftCodeSanKit/Core/RefChecker.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax /** Checks for referenced decls */ final class RefChecker: SyntaxVisitor { var imports = [String]() private var declMap = DeclMap() private var path: String private var module: String private var reflist = [String]() var refs: Set { return Set(reflist) } init(_ path: String, module: String, declMap: DeclMap) { self.path = path self.module = module self.declMap = declMap } override func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind { if node.item.is(ExprSyntax.self) || node.item.is(StmtSyntax.self) { reflist.append(contentsOf: node.item.refTypes(with: declMap)) return .skipChildren } return .visitChildren } override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) if node.isOverride { reflist.append(node.name) } return .visitChildren } override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) if node.isOverride { reflist.append(node.name) } return .visitChildren } override func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) return .visitChildren } override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) if node.isOverride { reflist.append(node.name) } return .visitChildren } override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) return .visitChildren } override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) return .visitChildren } override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) return .visitChildren } override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) reflist.append(node.name) return .visitChildren } override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) return .visitChildren } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) return .visitChildren } override func visit(_ node: TypealiasDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) return .visitChildren } override func visit(_ node: AssociatedtypeDeclSyntax) -> SyntaxVisitorContinueKind { reflist.append(contentsOf: node.refTypes(with: declMap)) return .visitChildren } override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { if node.attributes == nil, node.importKind == nil { let str = node.path.description.trimmed imports.append(str) } return .skipChildren } } ================================================ FILE: Sources/SwiftCodeSanKit/FileParsers/DeclParser.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax import SwiftSyntaxParser public class DeclParser { public init() {} func scanAndMapDecls(fileToModuleMap: [String: String], topDeclsOnly: Bool, whitelist: Whitelist?) -> DeclMap { var allDeclMap = DeclMap() scanDecls(fileToModuleMap: fileToModuleMap, topDeclsOnly: topDeclsOnly, whitelist: whitelist) { (filepath, subResults) in for (k, decls) in subResults { if allDeclMap[k] == nil { allDeclMap[k] = [] } for decl in decls { if allDeclMap[k]?.contains(decl) ?? false { // Already added, so do nothing } else { allDeclMap[k]?.append(decl) } } } } return allDeclMap } func scanAndMapDecls(fileToModuleMap: [String: String], topDeclsOnly: Bool) -> DeclMap { return scanAndMapDecls(fileToModuleMap: fileToModuleMap, topDeclsOnly: topDeclsOnly, whitelist: nil) } func scanDecls(fileToModuleMap: [String: String], topDeclsOnly: Bool, completion: @escaping (String, DeclMap) -> ()) { scan(fileToModuleMap) { (path: String, module: String, lock: NSLock?) in self.visitSrc(path: path, module: module, topDeclsOnly: topDeclsOnly, whitelist: nil, lock: lock, completion: completion) } } func scanDecls(fileToModuleMap: [String: String], topDeclsOnly: Bool, whitelist: Whitelist?, completion: @escaping (String, DeclMap) -> ()) { scan(fileToModuleMap) { (path: String, module: String, lock: NSLock?) in self.visitSrc(path: path, module: module, topDeclsOnly: topDeclsOnly, whitelist: whitelist, lock: lock, completion: completion) } } var wpaths = 0 var npaths = 0 private func visitSrc(path: String, module: String?, topDeclsOnly: Bool, whitelist: Whitelist?, lock: NSLock?, completion: @escaping (String, DeclMap) -> ()) { do { let node = try SyntaxParser.parse(path) let whitelistPath = FileManager.modifiedWithin(whitelist?.thresholdDays, at: path) if whitelistPath { wpaths += 1 } npaths += 1 let visitor = DeclVisitor(path, module: module, topDeclsOnly: topDeclsOnly, whitelistPath: whitelistPath, whitelist: whitelist) visitor.walk(node) lock?.lock() defer {lock?.unlock()} completion(path, visitor.declMap) } catch { fatalError(error.localizedDescription) } } func checkRefs(fileToModuleMap: [String: String], declMap: DeclMap, completion: @escaping (String, Set, [String]) -> ()) { scan(fileToModuleMap) { (path: String, module: String, lock: NSLock?) in self.referenceSrc(path: path, module: module, declMap: declMap, lock: lock, completion: completion) } } private func referenceSrc(path: String, module: String, declMap: DeclMap, lock: NSLock?, completion: @escaping (String, Set, [String]) -> ()) { do { let node = try SyntaxParser.parse(path) let visitor = RefChecker(path, module: module, declMap: declMap) visitor.walk(node) lock?.lock() completion(path, visitor.refs, visitor.imports) lock?.unlock() } catch { fatalError(error.localizedDescription) } } // MARK - input is dirs or filepaths func scanDecls(paths: [String], isDirs: Bool, topDeclsOnly: Bool, pathToModules: [String: String], whitelist: Whitelist?, completion: @escaping (String, DeclMap) -> ()) { if isDirs { scan(dirs: paths) { (path: String, lock: NSLock?) in self.visitSrc(path: path, module: pathToModules[path], topDeclsOnly: topDeclsOnly, whitelist: whitelist, lock: lock, completion: completion) } } else { scan(paths) { (path: String, lock: NSLock?) in self.visitSrc(path: path, module: pathToModules[path], topDeclsOnly: topDeclsOnly, whitelist: whitelist, lock: lock, completion: completion) } } } func checkRefs(paths: [String], isDirs: Bool, pathToModules: [String: String], declMap: DeclMap, completion: @escaping (String, Set, [String]) -> ()) { if isDirs { scan(dirs: paths) { (path: String, lock: NSLock?) in self.referenceSrc(path: path, module: pathToModules[path] ?? "", declMap: declMap, lock: lock, completion: completion) } } else { scan(paths) { (path: String, lock: NSLock?) in self.referenceSrc(path: path, module: pathToModules[path] ?? "", declMap: declMap, lock: lock, completion: completion) } } } } ================================================ FILE: Sources/SwiftCodeSanKit/FileUpdaters/DeclUpdater.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax import SwiftSyntaxParser final class DeclUpdater { func updateAccessLevels(filesToDecls: [String: [DeclMetadata]], filesToModules: [String: String], completion: @escaping (String, String) -> ()) { scan(filesToDecls) { (path, decls, lock) in do { let node = try SyntaxParser.parse(path) let rewriter = AccessLevelRewriter(path, module: filesToModules[path], decls: decls) let ret = rewriter.visit(node) lock?.lock() completion(path, ret.description) lock?.unlock() } catch { fatalError(error.localizedDescription) } } } func removeDeadDecls(filesToDecls: [String: [DeclMetadata]], completion: @escaping (String, String) -> ()) { scan(filesToDecls) { (path, decls, lock) in do { let node = try SyntaxParser.parse(path) let remover = DeclRemover(path, decls: decls) let ret = remover.visit(node) lock?.lock() completion(path, ret.description) lock?.unlock() } catch { fatalError(error.localizedDescription) } } } func removeUnusedImports(paths: [String], isDirs: Bool, unusedImports: [String: [String]], completion: @escaping (String, String) -> ()) { if isDirs { scan(dirs: paths) { (path, lock) in self.updateSrcs(path: path, module: "", lock: lock, unusedImports: unusedImports, completion: completion) } } else { scan(paths) { (path, lock) in self.updateSrcs(path: path, module: "", lock: lock, unusedImports: unusedImports, completion: completion) } } } func removeUnusedImports(fileToModuleMap: [String: String], unusedImports: [String: [String]], completion: @escaping (String, String) -> ()) { scan(fileToModuleMap) { (path, module, lock) in self.updateSrcs(path: path, module: module, lock: lock, unusedImports: unusedImports, completion: completion) } } private func updateSrcs(path: String, module: String, lock: NSLock?, unusedImports: [String: [String]], completion: @escaping (String, String) -> ()) { do { let node = try SyntaxParser.parse(path) let remover = ImportRewriter(path, unusedModules: unusedImports[path]) let ret = remover.visit(node) lock?.lock() completion(path, ret.description) lock?.unlock() } catch { fatalError(error.localizedDescription) } } } ================================================ FILE: Sources/SwiftCodeSanKit/Operations/RemoveDeadDecls.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation public func removeDeadDecls(filesToModules: [String: String], whitelist: Whitelist?, topDeclsOnly: Bool, inplace: Bool, testFiles: [String]?, inplaceTests: Bool, logFilePath: String? = nil, concurrencyLimit: Int? = nil, onCompletion: @escaping () -> ()) { log("Start of removing dead code: topDeclsOnly", topDeclsOnly) scanConcurrencyLimit = concurrencyLimit let p = DeclParser() var pathToDeclsUpdate = [String: [DeclMetadata]]() log("Scan and map top-level decls...") logTime() let declMap = p.scanAndMapDecls(fileToModuleMap: filesToModules, topDeclsOnly: false, whitelist: whitelist) logTime() print("WWW: ", p.npaths, p.wpaths) log("Check references, look up their source modules, and mark used...") let flatDeclMap = flatten(declMap: declMap) var nref = 0 p.checkRefs(fileToModuleMap: filesToModules, declMap: flatDeclMap) { (path, refs, imports) in if let refModule = filesToModules[path] { markUsed(refs, in: refModule, imports: imports, with: flatDeclMap, updateMembers: true) } log("#Checked refs", counter: &nref, interval: 1000, timed: true) } logTime() repeat { log("Look up interface members and mark used if any...") shouldRetry = false markInterfaceMembersUsed(declMap: declMap) logTime() } while shouldRetry var i = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count log("Mark bound types used...") markBoundTypesUsed(declMap: flatDeclMap) resetVisited(declMap: declMap) var j = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count logTime() while i != j { log("#Remaining decls to mark used", j-i, j, i) log("Repeat: Look up interface members and mark used if any...") markInterfaceMembersUsed(declMap: declMap) i = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count logTime() log("Repeat: Mark bound types used...") markBoundTypesUsed(declMap: flatDeclMap) resetVisited(declMap: flatDeclMap) j = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count logTime() } log("Filter out used decls...") for (_, decls) in flatDeclMap { for decl in decls { if !decl.used { if pathToDeclsUpdate[decl.path] == nil { pathToDeclsUpdate[decl.path] = [] } pathToDeclsUpdate[decl.path]?.append(decl) } } } let totalUnused = pathToDeclsUpdate.values.flatMap{$0}.count let totalUsed = flatDeclMap.values.flatMap{$0}.filter{$0.used}.count logTime() log("#Total top-level decls: ", declMap.values.flatMap{$0}.count, "#Total decls", flatDeclMap.values.flatMap{$0}.count, "#Decls Unused", totalUnused, "#Decls Used", totalUsed, "#Files to update", pathToDeclsUpdate.count) if let logfile = logFilePath { log("Save results to", logfile) let ret = pathToDeclsUpdate.map { arg in let vals = arg.value.map{ ObjectIdentifier($0).debugDescription + ", " + $0.fullName + ", " + $0.encloser } let valStr = vals.joined(separator: "\n") return "\(arg.key)\n--- \(valStr)\n" }.joined(separator: "\n") try? ret.write(toFile: logfile, atomically: true, encoding: .utf8) logTime() } if inplace { log("Remove unused decls from files...", pathToDeclsUpdate.count) let updater = DeclUpdater() updater.removeDeadDecls(filesToDecls: pathToDeclsUpdate) { (path, content) in try? content.write(toFile: path, atomically: true, encoding: .utf8) } logTime() } logTotalElapsed("Done") onCompletion() } // MARK - private functions private func markBoundTypesUsed(declMap: DeclMap) { for (k, decls) in declMap { if !k.isEmpty { // Empty means expr or stmt for decl in decls { if decl.used { decl.visited = true markBoundTypesUsed(decl, level: 0, declMap: declMap) } } } } } private func markBoundTypesUsed(_ decl: DeclMetadata, level: Int, declMap: DeclMap) { for boundType in decl.boundTypes { if !boundType.isEmpty { var bases: [String]? var leaf: String? if boundType.contains(".") { bases = boundType.components(separatedBy: ".") leaf = bases?.removeLast() } let key = leaf ?? boundType if let boundDecls = declMap[key] { for boundDecl in boundDecls { if boundDecl.visited, boundDecl.used { continue } boundDecl.visited = true if decl.module == boundDecl.module || decl.imports.contains(boundDecl.module) { boundDecl.used = true markBoundTypesUsed(boundDecl, level: level + 1, declMap: declMap) } } } } } } var shouldRetry = false private func markInterfaceMembersUsed(declMap: DeclMap) { var ndecls = 0 scan(declMap) { (key, vals, lock) in for cur in vals { var members = [DeclMetadata]() var interfaceMembers = [DeclMetadata]() var userDefinedTypes = [String]() var stdlibTypes = [String]() let level = 0 markBoundMembersUsed(key: cur, declMap: declMap, level: level, members: &members, interfaceMembers: &interfaceMembers, userDefinedTypes: &userDefinedTypes, stdlibTypes: &stdlibTypes) log("#Marked used members", counter: &ndecls, interval: 10000, timed: true) } } } private func markBoundMembersUsed(key cur: DeclMetadata, declMap: DeclMap, level: Int, members: inout [DeclMetadata], interfaceMembers: inout [DeclMetadata], userDefinedTypes: inout [String], stdlibTypes: inout [String]) { // First resolve inheritance (loop up protocol conformance, subclassing, and update member ALs) var parents = cur.inheritedTypes let curIsExtension = cur.declType == .extensionType if curIsExtension { parents.append(cur.name) } resolveInheritance(key: cur, inheritedTypes: parents, declMap: declMap, level: level, members: &members, interfaceMembers: &interfaceMembers, userDefinedTypes: &userDefinedTypes, stdlibTypes: &stdlibTypes) let stdTypes = stdlibTypes.filter{!userDefinedTypes.contains($0)} for member in members { let matchingMembers = interfaceMembers.filter {$0.name == member.name} for matched in matchingMembers { if matched.used { member.used = true } else if member.used || member.isOverride { if !matched.used { matched.used = true shouldRetry = true } } else if !stdTypes.isEmpty { if !matched.used { matched.used = true shouldRetry = true } member.used = true } } if matchingMembers.isEmpty, member.isOverride { // This might be a member overriding stdlib api member.used = true } } // For the following decl types, check bound types and update member ALs. if cur.declType == .extensionType || cur.declType == .enumType { if !cur.used { for m in cur.members { if m.used { cur.used = true break } } } if !cur.used { let boundTypes = cur.boundTypes.filter{!cur.inheritedTypes.contains($0)} for boundType in boundTypes { if boundType.isEmpty { continue } if cur.name != boundType, let _ = declMap[boundType] { // even if boundtype is used, cur might not be used } else if cur.inheritedTypes.contains(boundType) { // If parent is not in declMap, assume it's in stdlib. for member in cur.members { member.used = true cur.used = true } if cur.used { break } } } } } } private func resolveInheritance(key cur: DeclMetadata, inheritedTypes: [String]?, declMap: DeclMap, level: Int, members: inout [DeclMetadata], interfaceMembers: inout [DeclMetadata], userDefinedTypes: inout [String], stdlibTypes: inout [String]) { let parents = inheritedTypes ?? cur.inheritedTypes for parent in parents { if parent.isEmpty { continue } if let parentDecls = declMap[parent] { for parentDecl in parentDecls { if parentDecl.name.isEmpty { continue } if parentDecl.declType == .protocolType || parentDecl.declType == .classType || parentDecl.declType == .typealiasType { if parentDecl.declType == .protocolType { interfaceMembers.append(contentsOf: parentDecl.members) } else if parentDecl.declType == .classType, cur.declType == .classType { interfaceMembers.append(contentsOf: parentDecl.members) } userDefinedTypes.append(parentDecl.name) members.append(contentsOf: cur.members) let optionalInitialTypes = parentDecl.declType == .typealiasType ? parentDecl.boundTypes : nil resolveInheritance(key: parentDecl, inheritedTypes: optionalInitialTypes, declMap: declMap, level: level+1, members: &members, interfaceMembers: &interfaceMembers, userDefinedTypes: &userDefinedTypes, stdlibTypes: &stdlibTypes) } else if parentDecl.declType == .extensionType { // Parent could be a user defined type or a stdlib type. Add to a list for now and filter out below. stdlibTypes.append(parentDecl.name) members.append(contentsOf: cur.members) } } } else { // If parent is not in declMap, assume it's in stdlib. stdlibTypes.append(parent) } } for stdlibType in stdlibTypes { if userDefinedTypes.contains(stdlibType) { continue } interfaceMembers.append(contentsOf: cur.members) members.append(contentsOf: cur.members) break } } private func accessMembers(_ bases: [String], _ i: Int, _ refModule: String, _ imports: [String], declMap: DeclMap) -> Bool { let j = i + 1 if j < bases.count { let cur = bases[i] let next = bases[j] if let prefixDecls = declMap[cur] { for prefixDecl in prefixDecls { var list: [DeclMetadata]? if prefixDecl.declType == .funcType || prefixDecl.declType == .operatorType || // This is handled here but shouldn't be member-accessed prefixDecl.declType == .varType { if let typeDecls = declMap[prefixDecl.type] { for t in typeDecls { list = t.members.filter{$0.name == next} } } } else { list = prefixDecl.members.filter{$0.name == next} } if let list = list, !list.isEmpty { let accessed = accessMembers(bases, i + 1, refModule, imports, declMap: declMap) if accessed, (refModule == prefixDecl.module || imports.contains(prefixDecl.module)) { for member in list { member.used = true } } } else { return false } } } } return true } private func markUsed(_ refs: Set, in refModule: String, imports: [String], with declMap: DeclMap, updateMembers: Bool) { // Leaf level node checks for r in refs { var bases: [String]? var leaf: String? if r.contains(".") { bases = r.components(separatedBy: ".") } // First, traverse member access, and update visibility along the way var accessedMembers = false if let bases = bases { accessedMembers = accessMembers(bases, 0, refModule, imports, declMap: declMap) } if accessedMembers { continue } leaf = bases?.removeLast() let refKey = leaf ?? r // If above fails (e.g. encloser type is not found), or non-member access, try following if let refDecls = declMap[refKey] { for refDecl in refDecls { if refModule == refDecl.module || imports.contains(refDecl.module) || refDecl.isOverride { refDecl.used = true } } } } } private func resetVisited(declMap: DeclMap) { for (_, decls) in declMap { for decl in decls { decl.visited = false } } } ================================================ FILE: Sources/SwiftCodeSanKit/Operations/RemoveUnusedImports.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation public func removeUnusedImports(fileToModuleMap: [String: String], whitelist: Whitelist?, topDeclsOnly: Bool, inplace: Bool, logFilePath: String? = nil, concurrencyLimit: Int? = nil) { scanConcurrencyLimit = concurrencyLimit let p = DeclParser() log("Scan all decls and generate a decl map...") logTime() let allDeclMap = p.scanAndMapDecls(fileToModuleMap: fileToModuleMap, topDeclsOnly: topDeclsOnly) logTime() log("#Decls", allDeclMap.keys.count) var unusedImports = [String: [String]]() let whitelistModulesBlock = { (module: String) -> Bool in let moduleComps = module.components(separatedBy: ".").filter {!$0.isEmpty} for comp in moduleComps { if let list = whitelist?.modules, list.contains(comp) { return true } if let list = whitelist?.modulesSuffix { for suffix in list { if comp.hasSuffix(suffix) { return true } } } if let list = whitelist?.modulesPrefix { for prefix in list { if comp.hasPrefix(prefix) { return true } } } } return false } log("Check referenced decls and compare their source modules against imported modules to filter out unused imports...") var total = 0 p.checkRefs(fileToModuleMap: fileToModuleMap, declMap: allDeclMap) { (filepath, refs, imports) in var usedImportsInFile = [String: Bool]() for i in imports { usedImportsInFile[i] = whitelistModulesBlock(i) } for r in refs { if let refDecls = allDeclMap[r] { for refDecl in refDecls { let m = refDecl.module if imports.contains(m) { usedImportsInFile[m] = true } else { let refinedImports = imports.filter {$0.contains(".")} for item in refinedImports { let comps = item.components(separatedBy: ".") if comps.contains(m) { usedImportsInFile[item] = true } } } } } else if imports.contains(r) { // Sometimes a module name can be used in code, e.g. CoreFoundation.Foo usedImportsInFile[r] = true } } var unusedListInFile = [String]() for (module, used) in usedImportsInFile { if !used { total += 1 unusedListInFile.append(module) } } if !unusedListInFile.isEmpty { unusedImports[filepath] = Set(unusedListInFile).compactMap{$0} } // log(total, interval: 200) } logTime() log("#Unused imports", total) if let op = logFilePath { log("Save results...") var totalUnused = 0 var ret = unusedImports.map { (path, unusedlist) -> String in totalUnused += unusedlist.count return path + "\n" + String(unusedlist.count) + "\n" + unusedlist.joined(separator: ", ") } assert(total == totalUnused) ret.append("Total unused: \(totalUnused)") let retStr = ret.joined(separator: "\n\n") let declstr = allDeclMap.map{ (k, v) -> String in let t = """ \(k): \(v.map { $0.path }.joined(separator: ", ")) """ return t }.joined(separator: "\n") try? retStr.write(toFile: op, atomically: true, encoding: .utf8) try? declstr.write(toFile: op+"-decls", atomically: true, encoding: .utf8) } if inplace { log("Remove unused imports from files...", unusedImports.keys.count) let updater = DeclUpdater() updater.removeUnusedImports(fileToModuleMap: fileToModuleMap, unusedImports: unusedImports) { (path, result) in try? result.write(toFile: path, atomically: true, encoding: .utf8) } } logTime() logTotalElapsed("Done") } ================================================ FILE: Sources/SwiftCodeSanKit/Operations/UpdateAccessLevels.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation public func updateAccessLevels(filesToModules: [String: String], whitelist: Whitelist?, inplace: Bool, logFilePath: String? = nil, concurrencyLimit: Int? = nil, onCompletion: @escaping () -> ()) { scanConcurrencyLimit = concurrencyLimit let p = DeclParser() var pathToDeclsUpdate = [String: [DeclMetadata]]() log("Scan and map top-level decls...") logTime() let declMap = p.scanAndMapDecls(fileToModuleMap: filesToModules, topDeclsOnly: false, whitelist: whitelist) logTime() log("Check references, look up their source modules, and mark visibility...") p.checkRefs(fileToModuleMap: filesToModules, declMap: declMap) { (path, refs, imports) in if let refModule = filesToModules[path] { markVisiblity(refs, in: refModule, imports: imports, with: declMap, updateMembers: true) } } logTime() log("Update ALs (access levels) of top-level decls and their bound types...") updateBoundTypeALs(declMap: declMap) resetVisited(declMap: declMap) log("Update ALs of member decls of interfaces (protocol / base class)...") updateMemberALs(declMap: declMap) logTime() log("Flatten decls, and check references again, for member decls...") var nref = 0 let flatDeclMap = flatten(declMap: declMap) p.checkRefs(fileToModuleMap: filesToModules, declMap: flatDeclMap) { (path, refs, imports) in if let refModule = filesToModules[path] { markVisiblity(refs, in: refModule, imports: imports, with: flatDeclMap, updateMembers: false) } log(counter: &nref, interval: 1000) } log(nref) logTime() log("Update ALs of all decls and their bound types...") updateBoundTypeALs(declMap: flatDeclMap) resetVisited(declMap: flatDeclMap) var i = -1 var j = 0 while i != j { log("If bound types are modified, update their member ALs as well...") updateMemberALs(declMap: declMap) i = flatDeclMap.values.flatMap{$0}.filter{$0.shouldExpose}.count log("Again, update ALs of of all decls and their bound types...") updateBoundTypeALs(declMap: flatDeclMap) resetVisited(declMap: flatDeclMap) j = flatDeclMap.values.flatMap{$0}.filter{$0.shouldExpose}.count log("#Remaining decls to update", i-j, i, j) } log("Save decls to update per files...") for (_, decls) in flatDeclMap { for decl in decls { if decl.isPublicOrOpen, !decl.shouldExpose { if pathToDeclsUpdate[decl.path] == nil { pathToDeclsUpdate[decl.path] = [] } pathToDeclsUpdate[decl.path]?.append(decl) } } } if let logfile = logFilePath { log("Save results to", logfile) let ret = pathToDeclsUpdate.map {"\($0.key): \($0.value.map{$0.name + ", " + $0.encloser}.joined(separator: "\n"))"}.joined(separator: "\n") try? ret.write(toFile: logfile, atomically: true, encoding: .utf8) } if inplace { log("Update decl ALs in files...") let updater = DeclUpdater() updater.updateAccessLevels(filesToDecls: pathToDeclsUpdate, filesToModules: filesToModules) { (path, content) in try? content.write(toFile: path, atomically: true, encoding: .utf8) } } logTime() let total = pathToDeclsUpdate.values.flatMap{$0}.count log("#Total top-level decls: ", declMap.count, "#Total decls", flatDeclMap.count, "#Decls updated", total, "#Files updated", pathToDeclsUpdate.count) logTotalElapsed("Done") onCompletion() } // MARK - private functions private func updateBoundTypeALs(declMap: DeclMap) { for (k, decls) in declMap { if !k.isEmpty { // Empty means expr or stmt for decl in decls { if (decl.isPublicOrOpen && decl.shouldExpose) || decl.declType == .extensionType || decl.isExtensionMember { decl.visited = true updateBoundTypeALs(decl, level: 0, declMap: declMap) } } } } } private func updateBoundTypeALs(_ decl: DeclMetadata, level: Int, declMap: DeclMap) { for boundType in decl.boundTypesAL { if !boundType.isEmpty { var bases: [String]? var leaf: String? if boundType.contains(".") { bases = boundType.components(separatedBy: ".") leaf = bases?.removeLast() } let key = leaf ?? boundType if let boundDecls = declMap[key] { for boundDecl in boundDecls { if boundDecl.visited, boundDecl.shouldExpose { continue } boundDecl.visited = true if decl.module == boundDecl.module || decl.imports.contains(boundDecl.module) { boundDecl.shouldExpose = true updateBoundTypeALs(boundDecl, level: level + 1, declMap: declMap) } } } } } } private func updateMemberALs(declMap: DeclMap) { for (_, vals) in declMap { for cur in vals { var members = [DeclMetadata]() var interfaceMembers = [DeclMetadata]() let level = 0 updateBoundMemberALs(key: cur, declMap: declMap, level: level, members: &members, interfaceMembers: &interfaceMembers) } } } private func updateBoundMemberALs(key cur: DeclMetadata, declMap: DeclMap, level: Int, members: inout [DeclMetadata], interfaceMembers: inout [DeclMetadata]) { // First resolve inheritance (loop up protocol conformance, subclassing, and update member ALs) var parents = cur.inheritedTypes let curIsExtension = cur.declType == .extensionType if curIsExtension { parents.append(cur.name) } resolveInheritance(key: cur, inheritedTypes: parents, declMap: declMap, level: level, members: &members, interfaceMembers: &interfaceMembers) let interfaceMemberNames = interfaceMembers.map{$0.name} for member in members { if interfaceMemberNames.contains(member.name) { if member.isPublicOrOpen || (curIsExtension && cur.isPublicOrOpen) { member.shouldExpose = true // If encloser is extension, it should be also exposed since its member is public/exposed if curIsExtension, !cur.shouldExpose { cur.shouldExpose = true } } } else if member.isPublicOrOpen, member.isOverride { // This might be a member overriding stdlib api member.shouldExpose = true } } // For the following decl types, check bound types and update member ALs. if cur.declType == .extensionType || cur.declType == .enumType { var visitedCurrent = false let boundTypesAL = cur.boundTypesAL.filter{!cur.inheritedTypes.contains($0)} for boundType in boundTypesAL { if !boundType.isEmpty, cur.name != boundType, let boundTypeVals = declMap[boundType] { for boundDecl in boundTypeVals { if !visitedCurrent, boundDecl.isPublicOrOpen, boundDecl.shouldExpose { for member in cur.members { if member.isPublicOrOpen { member.shouldExpose = true } } visitedCurrent = true } } } else if !visitedCurrent, cur.inheritedTypes.contains(boundType) { // If parent is not in declMap, assume it's in stdlib. for member in cur.members { if member.isPublicOrOpen { member.shouldExpose = true } } visitedCurrent = true } } if visitedCurrent, !cur.shouldExpose { cur.shouldExpose = true } } } private func resolveInheritance(key cur: DeclMetadata, inheritedTypes: [String]?, declMap: DeclMap, level: Int, members: inout [DeclMetadata], interfaceMembers: inout [DeclMetadata]) { let parents = inheritedTypes ?? cur.inheritedTypes var stdlibTypes = [String]() var userDefinedTypes = [String]() for parent in parents { if parent.isEmpty { continue } if let parentDecls = declMap[parent] { for parentDecl in parentDecls { if parentDecl.name.isEmpty { continue } if parentDecl.declType == .protocolType || parentDecl.declType == .classType || parentDecl.declType == .typealiasType { if parentDecl.isPublicOrOpen, parentDecl.shouldExpose { if parentDecl.declType == .protocolType { interfaceMembers.append(contentsOf: parentDecl.members) } else if parentDecl.declType == .classType, cur.declType == .classType { interfaceMembers.append(contentsOf: parentDecl.members) } } userDefinedTypes.append(parentDecl.name) members.append(contentsOf: cur.members) let optionalInitialTypes = parentDecl.declType == .typealiasType ? parentDecl.boundTypesAL : nil resolveInheritance(key: parentDecl, inheritedTypes: optionalInitialTypes, declMap: declMap, level: level+1, members: &members, interfaceMembers: &interfaceMembers) } else if parentDecl.declType == .extensionType { // Parent could be a user defined type or a stdlib type. Add to a list for now and filter out below. stdlibTypes.append(parentDecl.name) } } } else { // If parent is not in declMap, assume it's in stdlib. stdlibTypes.append(parent) } } for stdlibType in stdlibTypes { if userDefinedTypes.contains(stdlibType) { continue } for member in cur.members { if member.isPublicOrOpen { interfaceMembers.append(member) members.append(member) } } break } } private func traverseMembers(_ bases: [String], _ i: Int, _ refModule: String, _ imports: [String], declMap: DeclMap) -> Bool { let j = i + 1 if j < bases.count { let cur = bases[i] let next = bases[j] if let prefixDecls = declMap[cur] { for prefixDecl in prefixDecls { var list: [DeclMetadata]? if prefixDecl.declType == .funcType || prefixDecl.declType == .operatorType || // This is handled here but shouldn't be member-accessed prefixDecl.declType == .varType { if let typeDecls = declMap[prefixDecl.type] { for t in typeDecls { list = t.members.filter{$0.name == next} } } } else { list = prefixDecl.members.filter{$0.name == next} } if let list = list, !list.isEmpty { let checked = traverseMembers(bases, i + 1, refModule, imports, declMap: declMap) if checked, refModule != prefixDecl.module, imports.contains(prefixDecl.module) { for member in list { member.shouldExpose = true } } } else { return false } } } } return true } private func markVisiblity(_ refs: Set, in refModule: String, imports: [String], with declMap: DeclMap, updateMembers: Bool) { // Leaf level node checks for r in refs { var bases: [String]? var leaf: String? if r.contains(".") { bases = r.components(separatedBy: ".") } // First, traverse member access, and update visibility along the way var accessedMembers = false if let bases = bases { accessedMembers = traverseMembers(bases, 0, refModule, imports, declMap: declMap) } if accessedMembers { continue } leaf = bases?.removeLast() let refKey = leaf ?? r // If above fails (e.g. encloser type is not found), or non-member access, try following if let refDecls = declMap[refKey] { for refDecl in refDecls { if true || refDecl.isPublicOrOpen || refDecl.declType == .extensionType || refDecl.isExtensionMember { // multi modules w/ same decls (foo): // 1. shadowing: if ref'd, it uses a decl in the same module even if the others are imported. // - if foo from another module should be called, it's required to use qualifier X.foo // 2. if not decl's in the same module as ref, uses corresponding modules, so need to look up imports // 3. if foo inits are the same for multi-modules: // - need qualifier X.foo if refModule == refDecl.module { // r is either declared internally // so r should not be public, so add [decl.path: r] to pathToUpdateDecls } else { // look up imports and check decl.module is in the imports, then decl.shouldBePublic = true, so do nothing. if imports.contains(refDecl.module) { if !refDecl.encloser.isEmpty { // If it has an encloser (part of a class, protocol, etc), // check if the encloser is in ref'd. // Encloser type might not be listed, leakdetect.inst.accumulatedLeaksStream refDecl.shouldExpose = true } else { // then r in decl.module should remain public refDecl.shouldExpose = true } } else { // r must be part of stdlib, handled in updateMemberALs above. } } } } } } } private func shouldMatchACLForMembers(_ declType: DeclType) -> Bool { return declType == .protocolType || declType == .extensionType || declType == .enumType } private func resetVisited(declMap: DeclMap) { for (_, decls) in declMap { for decl in decls { decl.visited = false } } } ================================================ FILE: Sources/SwiftCodeSanKit/Utils/Extensions/FileManagerExtensions.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation extension FileManager { static func modifiedWithin(_ delta: Int?, at path: String) -> Bool { if let fileAttrs = try? FileManager.default.attributesOfItem(atPath: path), let modifiedDate = fileAttrs[FileAttributeKey.creationDate] as? Date { let now = Date() let days = Int(floor(modifiedDate.distance(to: now) / 60 / 60 / 24)) if let delta = delta, days < delta { return true } } return false } } ================================================ FILE: Sources/SwiftCodeSanKit/Utils/Extensions/SequenceExtensions.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation public extension Sequence { func compactMap(path: KeyPath) -> [T] { return compactMap { (element) -> T? in element[keyPath: path] } } func map(path: KeyPath) -> [T] { return map { (element) -> T in element[keyPath: path] } } func filter(path: KeyPath) -> [Element] { return filter { (element) -> Bool in element[keyPath: path] } } func sorted(path: KeyPath) -> [Element] where T: Comparable { return sorted { (lhs, rhs) -> Bool in lhs[keyPath: path] < rhs[keyPath: path] } } func sorted(path: KeyPath, fallback: KeyPath) -> [Element] where T: Comparable, U: Comparable { return sorted { (lhs, rhs) -> Bool in if lhs[keyPath: path] == rhs[keyPath: path] { return lhs[keyPath: fallback] < rhs[keyPath: fallback] } return lhs[keyPath: path] < rhs[keyPath: path] } } } ================================================ FILE: Sources/SwiftCodeSanKit/Utils/Extensions/StringExtensions.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation var alphanumericSet = CharacterSet.alphanumerics extension String { static public let `final` = "final" static let override = "override" static let unknownVal = "Unknown" static let prefix = "prefix" static let `public` = "public" static let `open` = "open" static let `internal` = "internal" static let `required` = "required" static let `convenience` = "convenience" static let moduleColon = "module:" static let typealiasColon = "typealias:" static let rxColon = "rx:" static let varColon = "var:" static let annotationArgDelimiter = ";" static let transparent = "@_transparent" static let propertyWrapper = "propertyWrapper" var raw: String { if hasPrefix("`"), hasSuffix("`") { var val = dropFirst() val = val.dropLast() return String(val) } return self } func arguments(with delimiter: String) -> [String: String]? { let argstr = self let args = argstr.components(separatedBy: delimiter) var argsMap = [String: String]() for item in args { let keyVal = item.components(separatedBy: "=").map{$0.trimmed} if let k = keyVal.first { if k.contains(":") { break } if let v = keyVal.last { argsMap[k] = v } } } return !argsMap.isEmpty ? argsMap : nil } public var trimmed: String { return self.trimmingCharacters(in: .whitespaces) } var isAlphanumeric: Bool { let ret = self.unicodeScalars.filter {alphanumericSet.contains($0) || $0 == "_"} return !ret.isEmpty } var isPublicOrOpen: Bool { return self == .public || self == .open } } ================================================ FILE: Sources/SwiftCodeSanKit/Utils/Extensions/SyntaxExtensions.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax protocol DeclProtocol { var type: String { get } var name: String { get } var fullName: String { get } var declType: DeclType { get } var inheritedTypes: [String] { get } func refTypes(with declMap: DeclMap, filterKey: String?) -> [String] var refTypes: [String] { get } var boundTypes: [String] { get } var boundTypesAL: [String] { get } // Bound types for access levels var accessLevel: String { get } var isOverride: Bool { get } var isExprOrStmt: Bool { get } func declMetadatas(path: String, module: String, encloser: String, description: String, imports: [String]) -> [DeclMetadata] } extension DeclProtocol { func declMetadatas(path: String, module: String, encloser: String, description: String, imports: [String]) -> [DeclMetadata] { if let declSyntax = self as? DeclSyntax, let varSyntax = declSyntax.as(VariableDeclSyntax.self) { return varSyntax.declMetadatas(path: path, module: module, encloser: encloser, description: description, imports: imports) } let val = DeclMetadata(path: path, module: module, imports: imports, encloser: encloser, name: name, type: type, fullName: fullName, description: description, declType: declType, inheritedTypes: inheritedTypes, boundTypes: boundTypes, boundTypesAL: boundTypesAL, isPublicOrOpen: accessLevel.isPublicOrOpen, isOverride: isOverride, used: false) return [val] } func refTypes(with declMap: DeclMap, filterKey: String? = nil) -> [String] { return refTypes.filter { declMap[$0] != nil || $0.contains(".") || $0.hasSuffix("Strings") || $0.hasSuffix("Images") } } } extension Syntax: DeclProtocol { var name: String { return "" } var type: String { return "" } var fullName: String { return name } var accessLevel: String { return "" } var isOverride: Bool { return false } var isExprOrStmt: Bool { return false } var declType: DeclType { return .other } var inheritedTypes: [String] { return [] } var refTypes: [String] { return boundTypesAL } var boundTypes: [String] { return tokens.exprTokenList } var boundTypesAL: [String] { return boundTypes } } extension DeclSyntax: DeclProtocol { var name: String { if let d = self.as(FunctionDeclSyntax.self) { return d.name } else if let d = self.as(VariableDeclSyntax.self) { return d.name } else if let d = self.as(InitializerDeclSyntax.self) { return d.name } else if let d = self.as(SubscriptDeclSyntax.self) { return d.name } else if let d = self.as(ProtocolDeclSyntax.self) { return d.name } else if let d = self.as(ClassDeclSyntax.self) { return d.name } else if let d = self.as(ExtensionDeclSyntax.self) { return d.name } else if let d = self.as(StructDeclSyntax.self) { return d.name } else if let d = self.as(EnumDeclSyntax.self) { return d.name } else if let d = self.as(EnumCaseDeclSyntax.self) { return d.name } else if let d = self.as(TypealiasDeclSyntax.self) { return d.name } else if let d = self.as(AssociatedtypeDeclSyntax.self) { return d.name } else { return "" } } var type: String { if let d = self.as(FunctionDeclSyntax.self) { return d.type } else if let d = self.as(SubscriptDeclSyntax.self) { return d.type } else if let d = self.as(VariableDeclSyntax.self) { return d.type } return name } var fullName: String { if let d = self.as(FunctionDeclSyntax.self) { return d.fullName } else if let d = self.as(VariableDeclSyntax.self) { return d.fullName } else if let d = self.as(InitializerDeclSyntax.self) { return d.fullName } else if let d = self.as(SubscriptDeclSyntax.self) { return d.fullName } else if let d = self.as(ProtocolDeclSyntax.self) { return d.fullName } else if let d = self.as(ClassDeclSyntax.self) { return d.fullName } else if let d = self.as(StructDeclSyntax.self) { return d.fullName } else if let d = self.as(EnumDeclSyntax.self) { return d.fullName } else if let d = self.as(ExtensionDeclSyntax.self) { return d.fullName } else if let d = self.as(EnumCaseDeclSyntax.self) { return d.fullName } else if let d = self.as(TypealiasDeclSyntax.self) { return d.fullName } else if let d = self.as(AssociatedtypeDeclSyntax.self) { return d.fullName } return name } var declType: DeclType { if let d = self.as(FunctionDeclSyntax.self) { return d.declType } else if let d = self.as(VariableDeclSyntax.self) { return d.declType } else if let d = self.as(InitializerDeclSyntax.self) { return d.declType } else if let d = self.as(SubscriptDeclSyntax.self) { return d.declType } else if let d = self.as(ProtocolDeclSyntax.self) { return d.declType } else if let d = self.as(ClassDeclSyntax.self) { return d.declType } else if let d = self.as(ExtensionDeclSyntax.self) { return d.declType } else if let d = self.as(StructDeclSyntax.self) { return d.declType } else if let d = self.as(EnumDeclSyntax.self) { return d.declType } else if let d = self.as(EnumCaseDeclSyntax.self) { return d.declType } else if let d = self.as(TypealiasDeclSyntax.self) { return d.declType } else if let d = self.as(AssociatedtypeDeclSyntax.self) { return d.declType } return .other } var inheritedTypes: [String] { if let d = self.as(ProtocolDeclSyntax.self) { return d.inheritedTypes } else if let d = self.as(ClassDeclSyntax.self) { return d.inheritedTypes } else if let d = self.as(ExtensionDeclSyntax.self) { return d.inheritedTypes } else if let d = self.as(StructDeclSyntax.self) { return d.inheritedTypes } else if let d = self.as(EnumDeclSyntax.self) { return d.inheritedTypes } else if let d = self.as(TypealiasDeclSyntax.self) { return d.inheritedTypes } else if let d = self.as(AssociatedtypeDeclSyntax.self) { return d.inheritedTypes } return [] } func refTypes(with declMap: DeclMap, filterKey: String?) -> [String] { var list = refTypes if !declMap.isEmpty { list = list.filter{declMap[$0] != nil} } return list } var refTypes: [String] { if let d = self.as(FunctionDeclSyntax.self) { return d.refTypes } else if let d = self.as(VariableDeclSyntax.self) { return d.refTypes } else if let d = self.as(InitializerDeclSyntax.self) { return d.refTypes } else if let d = self.as(SubscriptDeclSyntax.self) { return d.refTypes } else if let d = self.as(ProtocolDeclSyntax.self) { return d.refTypes } else if let d = self.as(ClassDeclSyntax.self) { return d.refTypes } else if let d = self.as(ExtensionDeclSyntax.self) { return d.refTypes } else if let d = self.as(StructDeclSyntax.self) { return d.refTypes } else if let d = self.as(EnumDeclSyntax.self) { return d.refTypes } else if let d = self.as(EnumCaseDeclSyntax.self) { return d.refTypes } else if let d = self.as(TypealiasDeclSyntax.self) { return d.refTypes } else if let d = self.as(AssociatedtypeDeclSyntax.self) { return d.refTypes } return [] } var boundTypes: [String] { if let d = self.as(FunctionDeclSyntax.self) { return d.boundTypes } else if let d = self.as(VariableDeclSyntax.self) { return d.boundTypes } else if let d = self.as(InitializerDeclSyntax.self) { return d.boundTypes } else if let d = self.as(SubscriptDeclSyntax.self) { return d.boundTypes } else if let d = self.as(ProtocolDeclSyntax.self) { return d.boundTypes } else if let d = self.as(ClassDeclSyntax.self) { return d.boundTypes } else if let d = self.as(ExtensionDeclSyntax.self) { return d.boundTypes } else if let d = self.as(StructDeclSyntax.self) { return d.boundTypes } else if let d = self.as(EnumDeclSyntax.self) { return d.boundTypes } else if let d = self.as(EnumCaseDeclSyntax.self) { return d.boundTypes } else if let d = self.as(TypealiasDeclSyntax.self) { return d.boundTypes } else if let d = self.as(AssociatedtypeDeclSyntax.self) { return d.boundTypes } return [] } var boundTypesAL: [String] { if let d = self.as(FunctionDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(VariableDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(InitializerDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(SubscriptDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(ProtocolDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(ClassDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(ExtensionDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(StructDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(EnumDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(EnumCaseDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(TypealiasDeclSyntax.self) { return d.boundTypesAL } else if let d = self.as(AssociatedtypeDeclSyntax.self) { return d.boundTypesAL } return [] } var accessLevel: String { if let d = self.as(FunctionDeclSyntax.self) { return d.accessLevel } else if let d = self.as(VariableDeclSyntax.self) { return d.accessLevel } else if let d = self.as(InitializerDeclSyntax.self) { return d.accessLevel } else if let d = self.as(SubscriptDeclSyntax.self) { return d.accessLevel } else if let d = self.as(ProtocolDeclSyntax.self) { return d.accessLevel } else if let d = self.as(ClassDeclSyntax.self) { return d.accessLevel } else if let d = self.as(ExtensionDeclSyntax.self) { return d.accessLevel } else if let d = self.as(StructDeclSyntax.self) { return d.accessLevel } else if let d = self.as(EnumDeclSyntax.self) { return d.accessLevel } else if let d = self.as(EnumCaseDeclSyntax.self) { return d.accessLevel } else if let d = self.as(TypealiasDeclSyntax.self) { return d.accessLevel } else if let d = self.as(AssociatedtypeDeclSyntax.self) { return d.accessLevel } return "" } var isOverride: Bool { if let d = self.as(FunctionDeclSyntax.self) { return d.isOverride } else if let d = self.as(VariableDeclSyntax.self) { return d.isOverride } else if let d = self.as(InitializerDeclSyntax.self) { return d.isOverride } return false } var isExprOrStmt: Bool { _syntaxNode.is(StmtSyntax.self) || _syntaxNode.is(ExprSyntax.self) } } extension MemberDeclListItemSyntax: DeclProtocol { var refTypes: [String] { return decl.refTypes } var declType: DeclType { return decl.declType } var inheritedTypes: [String] { return decl.inheritedTypes } var boundTypes: [String] { return decl.boundTypes } var boundTypesAL: [String] { return decl.boundTypesAL } var accessLevel: String { return decl.accessLevel } var isOverride: Bool { return decl.isOverride } var isExprOrStmt: Bool { return decl.isExprOrStmt } var name: String { return decl.name } var type: String { return decl.type } var fullName: String { return decl.fullName } } extension MemberDeclListSyntax: DeclProtocol { var name: String { return "" } var type: String { return name } var fullName: String { return name } var declType: DeclType { return .other } var inheritedTypes: [String] { return [] } var accessLevel: String { return "" } var isOverride: Bool { return false } var isExprOrStmt: Bool { return false } var boundTypes: [String] { return self.map { $0.decl.boundTypes }.flatMap{$0} } var boundTypesAL: [String] { return self.map { $0.decl.boundTypesAL }.flatMap{$0} } var refTypes: [String] { return boundTypesAL } } extension ProtocolDeclSyntax: DeclProtocol { var fullName: String { return name } var type: String { return name } var isExprOrStmt: Bool { return false } var isOverride: Bool { return false } var name: String { return identifier.text.raw } var accessLevel: String { return self.modifiers?.acl ?? "" } var inheritedTypes: [String] { return [inheritanceClause?.tokens.exprTokenList, genericWhereClause?.tokens.exprTokenList, ].compactMap{$0}.flatMap{$0}.filter{$0 != name} } var boundTypes: [String] { return inheritedTypes } var boundTypesAL: [String] { return [boundTypes, members.members.boundTypesAL, ].compactMap{$0}.flatMap{$0} } var refTypes: [String] { return boundTypesAL } var declType: DeclType { return .protocolType } var isPrivate: Bool { return self.modifiers?.isPrivate ?? false } var attributesDescription: String { self.attributes?.trimmedDescription ?? "" } var offset: Int64 { return Int64(self.position.utf8Offset) } func annotationMetadata(with annotation: String) -> AnnotationMetadata? { return leadingTrivia?.annotationMetadata(with: annotation) } } extension ClassDeclSyntax: DeclProtocol { var fullName: String { return name } var type: String { return name } var isExprOrStmt: Bool { return false } var isOverride: Bool { return false } var name: String { return identifier.text.raw } var accessLevel: String { return self.modifiers?.acl ?? "" } var declType: DeclType { return .classType } var boundTypes: [String] { return inheritedTypes } var boundTypesAL: [String] { return boundTypes } var refTypes: [String] { return [boundTypesAL, members.members.boundTypesAL, ].compactMap{$0}.flatMap{$0} } var inheritedTypes: [String] { return [genericParameterClause?.genericParameterList.tokens.exprTokenList, genericWhereClause?.tokens.exprTokenList, inheritanceClause?.tokens.exprTokenList ].compactMap{$0}.flatMap{$0}.filter{$0 != name} } var attributesDescription: String { self.attributes?.trimmedDescription ?? "" } var offset: Int64 { return Int64(self.position.utf8Offset) } func annotationMetadata(with annotation: String) -> AnnotationMetadata? { return leadingTrivia?.annotationMetadata(with: annotation) } } extension ExtensionDeclSyntax: DeclProtocol { var fullName: String { return extendedType.description.trimmed + "_" + inheritedTypes.joined() } var type: String { return name } var isExprOrStmt: Bool { return false } var isOverride: Bool { return false } var name: String { let str = extendedType.description.trimmed.raw let comps = str.components(separatedBy: ".") if let last = comps.last, !last.isEmpty { return last } return str } var declType: DeclType { return .extensionType } var accessLevel: String { return self.modifiers?.acl ?? "" } var inheritedTypes: [String] { return [inheritanceClause?.tokens.exprTokenList, genericWhereClause?.tokens.exprTokenList, ].compactMap{$0}.flatMap{$0}.filter{$0 != name} } var boundTypes: [String] { var ret = inheritedTypes ret.append(name) return ret } var boundTypesAL: [String] { return [boundTypes, members.members.boundTypesAL, ].compactMap{$0}.flatMap{$0} } var refTypes: [String] { return boundTypesAL } func refTypes(with declMap: DeclMap, filterKey: String? = nil) -> [String] { var list = [extendedType.tokens.exprTokenList, refTypes ].compactMap{$0}.flatMap{$0} if !declMap.isEmpty { list = list.filter{declMap[$0] != nil} } return list } } extension EnumCaseDeclSyntax: DeclProtocol { var inheritedTypes: [String] { return [] } var type: String { return name } var isExprOrStmt: Bool { return false } var isOverride: Bool { return false } var name: String { let ret = elements.map{$0.identifier.text}.first ?? elements.description return ret.raw } var fullName: String { return self.elements.description } var accessLevel: String { return self.modifiers?.acl ?? "" } var declType: DeclType { return .enumCaseType } var boundTypes: [String] { let list = elements.compactMap{$0.associatedValue?.parameterList.compactMap{$0.type?.tokens.exprTokenList}.flatMap{$0}}.flatMap{$0} return list } var boundTypesAL: [String] { return boundTypes } var refTypes: [String] { return boundTypesAL } } extension EnumDeclSyntax: DeclProtocol { var fullName: String { return name + "_" + inheritedTypes.joined() } var type: String { return name } var isExprOrStmt: Bool { return false } var isOverride: Bool { return false } var attributesDescription: String { self.attributes?.description.trimmed ?? "" } var name: String { return identifier.text.trimmed.raw } var accessLevel: String { return self.modifiers?.acl ?? "" } var declType: DeclType { return .enumType } var inheritedTypes: [String] { return [inheritanceClause?.tokens.exprTokenList, genericParameters?.tokens.exprTokenList, genericWhereClause?.tokens.exprTokenList ].compactMap{$0}.flatMap{$0}.filter{ $0 != name } } var boundTypes: [String] { return inheritedTypes } var boundTypesAL: [String] { return [boundTypes, members.members.boundTypesAL ].compactMap{$0}.flatMap{$0} } var refTypes: [String] { return boundTypesAL } } extension StructDeclSyntax: DeclProtocol { var fullName: String { return name + "_" + inheritedTypes.joined() } var type: String { return name } var isExprOrStmt: Bool { return false } var isOverride: Bool { return false } var attributesDescription: String { self.attributes?.description.trimmed ?? "" } var name: String { return identifier.text.trimmed.raw } var declType: DeclType { return .structType } var accessLevel: String { return self.modifiers?.acl ?? "" } var inheritedTypes: [String] { return [inheritanceClause?.tokens.exprTokenList, genericParameterClause?.tokens.exprTokenList, genericWhereClause?.tokens.exprTokenList, ].compactMap{$0}.flatMap{$0}.filter{ $0 != name } } var boundTypes: [String] { return inheritedTypes } var boundTypesAL: [String] { return boundTypes } var refTypes: [String] { return [boundTypesAL, members.members.boundTypesAL, ].compactMap{$0}.flatMap{$0} } } extension AssociatedtypeDeclSyntax: DeclProtocol { var isExprOrStmt: Bool { return false } var isOverride: Bool { return false } var type: String { return name } var name: String { return identifier.text.trimmed.raw } var fullName: String { return name + "_" + boundTypes.joined() } var declType: DeclType { return .patType } var accessLevel: String { return self.modifiers?.acl ?? "" } var inheritedTypes: [String] { return [inheritanceClause?.tokens.exprTokenList, genericWhereClause?.tokens.exprTokenList, ].compactMap{$0}.flatMap{$0}.filter{$0 != name} } var boundTypes: [String] { return [inheritedTypes, initializer?.value.tokens.exprTokenList.filter{$0 != name} ].compactMap{$0}.flatMap{$0} } var boundTypesAL: [String] { return boundTypes } var refTypes: [String] { return boundTypesAL } } extension TypealiasDeclSyntax: DeclProtocol { var isExprOrStmt: Bool { return false } var isOverride: Bool { return false } var type: String { return name } var name: String { return identifier.text.trimmed.raw } var fullName: String { return name + "_" + boundTypes.joined() } var declType: DeclType { return .typealiasType } var accessLevel: String { return self.modifiers?.acl ?? "" } var inheritedTypes: [String] { return [genericParameterClause?.tokens.exprTokenList, genericWhereClause?.tokens.exprTokenList, ].compactMap{$0}.flatMap{$0}.filter{$0 != name} } var boundTypes: [String] { return [inheritedTypes, initializer?.value.tokens.exprTokenList.filter{$0 != name} ].compactMap{$0}.flatMap{$0} } var boundTypesAL: [String] { return boundTypes } var refTypes: [String] { return boundTypesAL } } extension PatternBindingSyntax { var type: String { if let t = typeAnnotation?.type.description.trimmed { return t } if let val = initializer?.value { if let expr = val.as(FunctionCallExprSyntax.self) { return expr.calledExpression.description.trimmed } else if let expr = val.as(ExprSyntax.self) { return expr.description.trimmed } } return .unknownVal } func boundTypes(isTransparent: Bool) -> [String] { var list = [String]() if let bound = typeAnnotation?.type.tokens.exprTokenList { list.append(contentsOf: bound) } if let val = initializer?.value { if let expr = val.as(FunctionCallExprSyntax.self) { let exprList = [ expr.calledExpression.tokens.exprTokenList, expr.argumentList.map{$0.expression.tokens.exprTokenList}.flatMap{$0} ].flatMap{$0} list.append(contentsOf: exprList) } else if let expr = val.as(ExprSyntax.self) { list.append(contentsOf: expr.tokens.exprTokenList) } } if isTransparent { if let bodyTokens = accessor?.tokens.exprTokenList { list.append(contentsOf: bodyTokens) } } return list } } extension VariableDeclSyntax: DeclProtocol { func declMetadatas(path: String, module: String, encloser: String, description: String, imports: [String]) -> [DeclMetadata] { var list = [DeclMetadata]() for binding in bindings { if let idpattern = binding.pattern.as(IdentifierPatternSyntax.self) { let id = idpattern.identifier.text if id == "_" { continue } let ty = binding.type let full = id + "_" + ty let bound = binding.boundTypes(isTransparent: isTransparent) let val = DeclMetadata(path: path, module: module, imports: imports, encloser: encloser, name: id, type: ty, fullName: full, description: description, declType: declType, inheritedTypes: inheritedTypes, boundTypes: bound, boundTypesAL: bound, isPublicOrOpen: accessLevel.isPublicOrOpen, isOverride: isOverride, used: false) list.append(val) } else if let tuple = binding.pattern.as(TuplePatternSyntax.self) { for el in tuple.elements { if let idpattern = el.pattern.as(IdentifierPatternSyntax.self) { let id = idpattern.identifier.text if id == "_" { continue } let ty = binding.type let full = id + "_" + ty let bound = binding.boundTypes(isTransparent: isTransparent) let val = DeclMetadata(path: path, module: module, imports: imports, encloser: encloser, name: id, type: ty, fullName: full, description: description, declType: declType, inheritedTypes: inheritedTypes, boundTypes: bound, boundTypesAL: bound, isPublicOrOpen: accessLevel.isPublicOrOpen, isOverride: isOverride, used: false) list.append(val) } } } } return list } var isTransparent: Bool { return attributesDescription.contains(String.transparent) } var name: String { let ret = bindings.compactMap { $0.pattern.description.trimmed }.joined().raw return ret } var inheritedTypes: [String] { return [] } var isExprOrStmt: Bool { return false } var fullName: String { return name + "_" + type } var declType: DeclType { return .varType } var accessLevel: String { return self.modifiers?.acl ?? "" } var isOverride: Bool { return modifiers?.isOverride ?? false } var attributesDescription: String { return attributes?.trimmedDescription ?? "" } var type: String { return bindings.first?.type ?? "" } var boundTypes: [String] { var list = [String]() for b in bindings { list.append(contentsOf: b.boundTypes(isTransparent: isTransparent)) } if let attrs = attributes?.tokens.exprTokenList { list.append(contentsOf: attrs) } return list.filter{$0 != name} } var boundTypesAL: [String] { return boundTypes } var refTypes: [String] { let ret = [boundTypesAL, bindings.compactMap{$0.accessor?.tokens.exprTokenList}.flatMap{$0} ].compactMap{$0}.flatMap{$0} return ret } } extension FunctionDeclSyntax: DeclProtocol { var name: String { return self.identifier.description.trimmed.raw } var type: String { return signature.output?.returnType.description.trimmed ?? "" } var fullName: String { return name + signature.description.trimmed } var declType: DeclType { if self.identifier.tokenKind == .spacedBinaryOperator(self.identifier.text) { return .operatorType } return .funcType } var accessLevel: String { return self.modifiers?.acl ?? "" } var isOverride: Bool { return modifiers?.isOverride ?? false } var attributesDescription: String { return attributes?.trimmedDescription ?? "" } var inheritedTypes: [String] { return [] } var isExprOrStmt: Bool { return false } var boundTypes: [String] { let genericParamTypes = genericParameterClause?.genericParameterList.tokens.exprTokenList let genericWhereTypes = genericWhereClause?.tokens.exprTokenList let paramTypes = signature.input.parameterList.compactMap{$0.type?.tokens.exprTokenList}.flatMap{$0} let paramVals = signature.input.parameterList.compactMap{$0.defaultArgument?.value.tokens.exprTokenList}.flatMap{$0} let returnTypes = signature.output?.returnType.tokens.exprTokenList let attrs = attributes?.tokens.exprTokenList // e.g. @FunctionBuilder var list = [genericParamTypes, genericWhereTypes, paramTypes, paramVals, returnTypes, attrs].compactMap{$0}.flatMap{$0} if attributesDescription.contains(String.transparent) { if let bodyTokens = body?.tokens.exprTokenList { list.append(contentsOf: bodyTokens) } } return list.filter{$0 != name} } var boundTypesAL: [String] { return boundTypes } var refTypes: [String] { return [boundTypesAL, body?.tokens.exprTokenList ].compactMap{$0}.flatMap{$0} } } extension InitializerDeclSyntax: DeclProtocol { var name: String { return "init" } var type: String { return name } var fullName: String { return name + "_" + parameters.description.trimmed } var declType: DeclType { return .initType } var isOverride: Bool { return modifiers?.isOverride ?? false } var attributesDescription: String { return attributes?.trimmedDescription ?? "" } var accessLevel: String { return modifiers?.acl ?? "" } var boundTypes: [String] { let genericParamTypes = genericParameterClause?.genericParameterList.tokens.exprTokenList let genericWhereTypes = genericWhereClause?.tokens.exprTokenList var paramList = [String]() for param in parameters.parameterList { if let pval = param.defaultArgument?.value { if let accessed = pval.as(MemberAccessExprSyntax.self), let base = accessed.base { paramList.append(accessed.description.trimmed) paramList.append(contentsOf: base.tokens.exprTokenList) } else { paramList.append(contentsOf: pval.tokens.exprTokenList) } } if let ptypes = param.type?.tokens.exprTokenList { paramList.append(contentsOf: ptypes) } } var list = [genericParamTypes, genericWhereTypes, paramList].compactMap{$0}.flatMap{$0} // @_transparent on public or @usableFromInline functions require all types in sig and body to be public if attributesDescription.contains(String.transparent) { if let bodyTokens = body?.tokens.exprTokenList { list.append(contentsOf: bodyTokens) } } return list.filter{$0 != name} } var boundTypesAL: [String] { return boundTypes } var refTypes: [String] { return [boundTypesAL, body?.tokens.exprTokenList ].compactMap{$0}.flatMap{$0} } var inheritedTypes: [String] { return [] } var isExprOrStmt: Bool { return false } func isRequired(with declType: DeclType) -> Bool { if declType == .protocolType { return true } else if declType == .classType { if let modifiers = self.modifiers { if modifiers.isConvenience { return false } return modifiers.isRequired } } return false } } extension SubscriptDeclSyntax: DeclProtocol { var fullName: String { return name + "_" + result.returnType.description.trimmed } var name: String { return self.subscriptKeyword.text } var declType: DeclType { return .subscriptType } var accessLevel: String { return modifiers?.acl ?? "" } var inheritedTypes: [String] { return [] } var isExprOrStmt: Bool { return false } var isOverride: Bool { return false } var type: String { return result.returnType.description.trimmed } var boundTypes: [String] { return [result.returnType.tokens.exprTokenList, genericParameterClause?.genericParameterList.tokens.exprTokenList, genericWhereClause?.tokens.exprTokenList, attributes?.tokens.exprTokenList, ].compactMap{$0}.flatMap{$0}.filter {$0 != name } } var boundTypesAL: [String] { return boundTypes } var refTypes: [String] { return [boundTypesAL, accessor?.tokens.exprTokenList ].compactMap{$0}.flatMap{$0} } } // MARK - extension AttributeListSyntax { var trimmedDescription: String? { return self.withoutTrivia().description.trimmingCharacters(in: .whitespacesAndNewlines) } } extension ModifierListSyntax { var acl: String { for modifier in self { for token in modifier.tokens { switch token.tokenKind { case .publicKeyword, .internalKeyword, .privateKeyword, .fileprivateKeyword: return token.text default: // For some reason openKeyword option is not available in TokenKind so need to address separately if token.text == String.open { return token.text } } } } return "" } var isStatic: Bool { return self.tokens.filter {$0.tokenKind == .staticKeyword }.count > 0 } var isRequired: Bool { return self.tokens.filter {$0.text == String.required }.count > 0 } var isConvenience: Bool { return self.tokens.filter {$0.text == String.convenience }.count > 0 } var isOverride: Bool { return self.tokens.filter {$0.text == String.override }.count > 0 } var isFinal: Bool { return self.tokens.filter {$0.text == String.final }.count > 0 } var isPrivate: Bool { return self.tokens.filter {$0.tokenKind == .privateKeyword || $0.tokenKind == .fileprivateKeyword }.count > 0 } var isPublic: Bool { return self.tokens.filter {$0.tokenKind == .publicKeyword }.count > 0 } var isOpen: Bool { return self.tokens.filter {$0.text == String.open }.count > 0 } } extension Trivia { // This parses arguments in annotation which can be used to override certain types. // // E.g. given /// @mockable(typealias: T = Any; U = AnyObject), it returns // a dictionary: [T: Any, U: AnyObject] which will be used to override inhertied types // of typealias decls for T and U. private func metadata(with annotation: String, in val: String) -> AnnotationMetadata? { if val.contains(annotation) { let comps = val.components(separatedBy: annotation) var ret = AnnotationMetadata() if var argsStr = comps.last, !argsStr.isEmpty { if argsStr.hasPrefix("(") { argsStr.removeFirst() } if argsStr.hasSuffix(")") { argsStr.removeLast() } if argsStr.contains(String.typealiasColon), let subStr = argsStr.components(separatedBy: String.typealiasColon).last, !subStr.isEmpty { ret.typeAliases = subStr.arguments(with: .annotationArgDelimiter) } if argsStr.contains(String.moduleColon), let subStr = argsStr.components(separatedBy: String.moduleColon).last, !subStr.isEmpty { let val = subStr.arguments(with: .annotationArgDelimiter) ret.module = val?[.prefix] } if argsStr.contains(String.rxColon), let subStr = argsStr.components(separatedBy: String.rxColon).last, !subStr.isEmpty { ret.varTypes = subStr.arguments(with: .annotationArgDelimiter) } if argsStr.contains(String.varColon), let subStr = argsStr.components(separatedBy: String.varColon).last, !subStr.isEmpty { if let val = subStr.arguments(with: .annotationArgDelimiter) { if ret.varTypes == nil { ret.varTypes = val } else { ret.varTypes?.merge(val, uniquingKeysWith: {$1}) } } } } return ret } return nil } // Looks up an annotation (e.g. /// @mockable) and its arguments if any. // See metadata(with:, in:) for more info on the annotation arguments. func annotationMetadata(with annotation: String) -> AnnotationMetadata? { guard !annotation.isEmpty else { return nil } var ret: AnnotationMetadata? for i in 0.. String? { if text.hasSuffix(suffix) { return String(text.dropLast(suffix.count)) } return nil } } extension TokenSequence { var tokenList: [String] { return self.compactMap { $0.stringToken } } var exprTokenList: [String] { return self.compactMap { $0.exprToken } } } ================================================ FILE: Sources/SwiftCodeSanKit/Utils/Extensions/SyntaxParserExtensions.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import SwiftSyntax import SwiftSyntaxParser extension SyntaxParser { public static func parse(_ fileData: Data, path: String, diagnosticHandler: ((Diagnostic) -> Void)? = nil) throws -> SourceFileSyntax { // Avoid using `String(contentsOf:)` because it creates a wrapped NSString. let source = fileData.withUnsafeBytes { buf in return String(decoding: buf.bindMemory(to: UInt8.self), as: UTF8.self) } return try parse(source: source, filenameForDiagnostics: path, diagnosticHandler: diagnosticHandler) } public static func parse(_ path: String) throws -> SourceFileSyntax { guard let fileData = FileManager.default.contents(atPath: path) else { fatalError("Retrieving contents of \(path) failed") } return try parse(fileData, path: path) } } ================================================ FILE: Sources/SwiftCodeSanKit/Utils/Logger.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation import os.signpost fileprivate let perfLog = OSLog(subsystem: "SwiftCodeSan", category: "PointsOfInterest") fileprivate var prevTime: CFAbsoluteTime? fileprivate var startTime: CFAbsoluteTime? public var minLogLevel = 0 /// Logs status and other messages depending on the level provided public enum LogLevel: Int { case verbose case info case warning case error } public func logTotalElapsed(_ arg: Any...) { let cur = CFAbsoluteTimeGetCurrent() if let startTime = startTime { print(arg, cur-startTime) } else { print("0.00") } } public func logTime(_ arg: Any...) { let cur = CFAbsoluteTimeGetCurrent() if let prevTime = prevTime { var str = arg let delta = (cur-prevTime) str.append("Took \(delta)") print(str) } prevTime = cur if startTime == nil { startTime = cur } } public func log(_ arg: Int, level: LogLevel = .info, interval: Int) { if arg > 0, arg % interval == 0 { log(arg, level: level) } } public func log(_ arg: Any..., level: LogLevel = .info, counter: inout Int, interval: Int, timed: Bool = false) { if counter > 0, counter % interval == 0 { log(arg, counter, level: level) if timed { logTime() } } counter += 1 } public func log(_ arg: Any..., level: LogLevel = .info) { guard level.rawValue >= minLogLevel else { return } switch level { case .info, .verbose: print(arg) case .warning: print("WARNING: \(arg)") case .error: print("ERROR: \(arg)") } } public func signpost_begin(name: StaticString) { if minLogLevel == LogLevel.verbose.rawValue { os_signpost(.begin, log: perfLog, name: name) } } public func signpost_end(name: StaticString) { if minLogLevel == LogLevel.verbose.rawValue { os_signpost(.end, log: perfLog, name: name) } } ================================================ FILE: Sources/SwiftCodeSanKit/Utils/Scanner.swift ================================================ // // Copyright (c) 2018. Uber Technologies // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Foundation public var scanConcurrencyLimit: Int? = nil func semaphore(_ numThreads: Int?) -> DispatchSemaphore? { let limit = concurrencyLimit(numThreads) var sema: DispatchSemaphore? if limit > 1 { sema = DispatchSemaphore(value: limit) } return sema } func queue(_ numThreads: Int?) -> DispatchQueue? { var q: DispatchQueue? if concurrencyLimit(numThreads) > 1 { q = DispatchQueue(label: "SwiftCodeSan-queue", qos: DispatchQoS.userInteractive, attributes: DispatchQueue.Attributes.concurrent) } return q } func concurrencyLimit(_ numThreads: Int?) -> Int { var limit = 1 if let n = numThreads { limit = n } else if let n = scanConcurrencyLimit { limit = n } return limit } public func scan(_ paths: [String], isDirectory: Bool, numThreads: Int? = nil, block: @escaping (_ path: String, _ lock: NSLock?) -> ()) { if isDirectory { scan(dirs: paths, block: block) } else { scan(paths, block: block) } } public func scan(dirs: [String], numThreads: Int? = nil, block: @escaping (_ path: String, _ lock: NSLock?) -> ()) { if let queue = queue(numThreads) { let sema = semaphore(numThreads) let lock = NSLock() scanDirs(dirs) { filePath in _ = sema?.wait(timeout: DispatchTime.distantFuture) queue.async { block(filePath, lock) sema?.signal() } } // Wait for queue to drain queue.sync(flags: .barrier) {} } else { scanDirs(dirs) { filePath in block(filePath, nil) } } } public func scan(_ elements: [T], numThreads: Int? = nil, block: @escaping (T, NSLock?) -> ()) { if let queue = queue(numThreads) { let sema = semaphore(numThreads) let lock = NSLock() for element in elements { _ = sema?.wait(timeout: DispatchTime.distantFuture) queue.async { block(element, lock) sema?.signal() } } queue.sync(flags: .barrier) { } } else { for element in elements { block(element, nil) } } } public func scan(_ elements: [T: U], numThreads: Int? = nil, block: @escaping (T, U, NSLock?) -> ()) { if let queue = queue(numThreads) { let sema = semaphore(numThreads) let lock = NSLock() for element in elements { _ = sema?.wait(timeout: DispatchTime.distantFuture) queue.async { block(element.key, element.value, lock) sema?.signal() } } queue.sync(flags: .barrier) { } } else { for element in elements { block(element.key, element.value, nil) } } } public func scanDirs(_ paths: [String], with callBack: (String) -> Void) { for path in paths { scanDir(path, with: callBack) } } func scanDir(_ path: String, with callBack: (String) -> Void) { let errorHandler = { (url: URL, error: Error) -> Bool in fatalError("Failed to traverse \(url) with error \(error).") } if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path, isDirectory: true), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles], errorHandler: errorHandler) { while let nextObjc = enumerator.nextObject() { if let fileUrl = nextObjc as? URL { callBack(fileUrl.path) } } } } ================================================ FILE: Tests/SwiftCodeSanTestCase.swift ================================================ import XCTest import SwiftCodeSanKit class SwiftCodeSanTestCase: XCTestCase { var srcFilePathsCount = 1 var mockFilePathsCount = 1 let bundle = Bundle(for: SwiftCodeSanTestCase.self) lazy var dstFilePath: String = { return bundle.bundlePath + "/Dst.swift" }() lazy var srcFilePaths: [String] = { var idx = 0 var paths = [String]() let prefix = bundle.bundlePath + "/Src" let suffix = ".swift" while idx < srcFilePathsCount { let path = prefix + "\(idx)" + suffix paths.append(path) idx += 1 } return paths }() lazy var mockFilePaths: [String] = { var idx = 0 var paths = [String]() let prefix = bundle.bundlePath + "/Mocks" let suffix = ".swift" while idx < mockFilePathsCount { let path = prefix + "\(idx)" + suffix paths.append(path) idx += 1 } return paths }() override func setUp() { // Put setup code here. This method is called before the invocation of each test method in the class. let created = FileManager.default.createFile(atPath: dstFilePath, contents: nil, attributes: nil) XCTAssert(created) } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. try? FileManager.default.removeItem(atPath: dstFilePath) for srcpath in srcFilePaths { try? FileManager.default.removeItem(atPath: srcpath) } } func verify(srcContent: String, dstContent: String, header: String = "", declType: DeclType = .protocolType, useTemplateFunc: Bool = false, testableImports: [String]? = [], concurrencyLimit: Int? = 1) { verify(srcContents: [srcContent], dstContent: dstContent, header: header, declType: declType, useTemplateFunc: useTemplateFunc, testableImports: testableImports, concurrencyLimit: concurrencyLimit) } func verify(srcContents: [String], dstContent: String, header: String, declType: DeclType, useTemplateFunc: Bool, testableImports: [String]?, concurrencyLimit: Int?) { var index = 0 srcFilePathsCount = srcContents.count for src in srcContents { if index < srcContents.count { let srcCreated = FileManager.default.createFile(atPath: srcFilePaths[index], contents: src.data(using: .utf8), attributes: nil) index += 1 XCTAssert(srcCreated) } } // TODO: Pass in srcfile path - module list removeDeadDecls(filesToModules: ["test1.swift": "test1"], whitelist: nil, topDeclsOnly: true, inplace: true, testFiles: [], inplaceTests: false, logFilePath: nil, concurrencyLimit: nil, onCompletion: { let output = (try? String(contentsOf: URL(fileURLWithPath: self.dstFilePath), encoding: .utf8)) ?? "" let outputContents = output.components(separatedBy: .whitespacesAndNewlines).filter{!$0.isEmpty} XCTAssert(outputContents.count > 0) }) } } ================================================ FILE: Tests/TestClasses/Fixtures/test0.swift ================================================ #if TESTFILE extension Integration { var isUnitTest: Bool { #if TEST if case .test = RunType.current { return !(Handler.isHandlerActive() || runner.sharedInstance.isActive) } #else let x = SomeType().someMethod() return x #endif } var workersProviderMock: IntegrationProviderMock { shared { IntegrationProviderMock() } } } let s = SwipeTransitionController() #endif ================================================ FILE: Tests/TestClasses/Fixtures/test1.swift ================================================ #if TESTFILE import Foundation public protocol FileHandler { func createFile(atPath: String, contents: Data?, attributes: [FileAttributeKey: Any]?) -> Bool func moveItem(at url: URL, to: URL) throws func contentsOfDirectory(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options mask: FileManager.DirectoryEnumerationOptions) throws -> [URL] func createDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey: Any]?) throws func fileExists(atPath: String) -> Bool func createFileHandle(forWritingToURL: URL) throws -> FileHandle func read(contentsOf url: URL) throws -> String @discardableResult func write(dictionary: [String: Any], to url: URL, atomically: Bool) -> Bool func write(data: Data, to url: URL, options: Data.WritingOptions) throws func read(dictionaryAt url: URL) -> [String: Any]? func data(forURL url: URL) -> Data? func isDeletableFile(atURL url: URL) -> Bool func removeItem(at URL: URL) throws func url(for directory: FileManager.SearchPathDirectory, in domain: FileManager.SearchPathDomainMask) throws -> URL func urls(for directory: FileManager.SearchPathDirectory, in domainMask: FileManager.SearchPathDomainMask) -> [URL] func copyItem(at srcURL: URL, to dstURL: URL) throws func setResourceValues(_ values: URLResourceValues, at url: inout URL) throws } extension FileManager: FileHandler { public func write(data: Data, to url: URL, options: Data.WritingOptions) throws { try data.write(to: url, options: options) } public func createFileHandle(forWritingToURL: URL) throws -> FileHandle { return try FileHandle(forWritingTo: forWritingToURL) } public func read(contentsOf url: URL) throws -> String { return try String(contentsOf: url, encoding: String.Encoding.utf8) } public func write(dictionary: [String: Any], to url: URL, atomically: Bool) -> Bool { return (dictionary as NSDictionary).write(to: url as URL, atomically: atomically) } public func read(dictionaryAt url: URL) -> [String: Any]? { return NSDictionary(contentsOf: url as URL) as? [String: Any] } public func data(forURL url: URL) -> Data? { return try? Data(contentsOf: url) } public func isDeletableFile(atURL url: URL) -> Bool { return isDeletableFile(atPath: url.path) } public func url(for directory: FileManager.SearchPathDirectory, in domain: FileManager.SearchPathDomainMask) throws -> URL { return try url(for: directory, in: domain, appropriateFor: nil, create: true) } public func setResourceValues(_ values: URLResourceValues, at url: inout URL) throws { try url.setResourceValues(values) } } #endif ================================================ FILE: Tests/TestClasses/Fixtures/test2.swift ================================================ // // Copyright © Uber Technologies, Inc. All rights reserved. // #if TESTFILE import Foundation @_transparent public func testAssert(_ resumable: Bool) { let x = AssertStaticString() print(x) if testAssertionDisabled { return } reassert(message(), file: unsafeBitCast(assertStaticString, to: StaticString.self), function: function, line: line) let handler = AssertionHandlers.assertionFailure print(handler) } class AssertionHandlers { } public struct AssertStaticString { public init() {} public func check() -> Int { return 5 } } public extension String { func someAssert(_ assertStaticString: AssertStaticString) { } } public typealias AssertionHandler = (String?, data: LogModelMetadata?, line: UInt) public let testAssertionDisabled: Bool = { return false }() public func reassert(_ message: String, file: StaticString, function: String, line: UInt) { } public class SynchronizedFoo { } public protocol KeyValueSubscripting { associatedtype Key associatedtype Value subscript(key: Key) -> Value? { get set } } public extension SynchronizedFoo: KeyValueSubscripting where T: KeyValueSubscripting { public subscript(key: T.Key) -> T.Value? { get { return read { (collection) -> T.Value? in return collection[key] } } set { write { (collection) -> () in collection[key] = newValue } } } public func bar() -> String { return "bar" } } #endif ================================================ FILE: Tests/TestClasses/Fixtures/test3.swift ================================================ #if TESTFILE import Foundation public protocol ProtocolY { func filterBy(uuid: String) -> Int? } public protocol ProtocolX: ProtocolY { } public final class FooKlass { public static var sharedInstance = FooKlass() func bar(arg: String) -> Int { let (lhs, rhs) = arg let ret = Double() ret.listener = lhs return ret } } final class ListInteractor { public var x: String = "", y: Int, z: Double public var (v, w): (String, Int) = ("", 0) public let application: UIApplicationProtocol, cachedExperiments: CachedExperimenting, deepLinkBuilder: (_ url: URL) -> DetailsDeeplink?, detailsBuilder: DetailsBuildable, listStream: Observable<[DetailsListStreaming]> } #endif ================================================ FILE: Tests/TestClasses/Fixtures/test4.swift ================================================ #if TESTFILE import Foundation public enum BarType { case current } class Klass: P { private static var unusedP: SomeType = SomeType() public init() { } func isActive(_ arg: AmbProtocol?) -> Bool { arg?.usedFunc() return true } public override func isApplicable(context: SomeContext, with flag: Flag) -> Bool { return flag.isActive } func unusedFunc() { print("...") } } public final class OtherKlass { public static var sharedInstance = OtherKlass() public var shouldRun: Bool { let (lhs, mhs, rhs) = asdf() print(lhs, rhs) return false } } public protocol Flag { } public extension Flag { var isActive: Bool { return true } var isActiveLong: Bool { return isEnabled(for: "Exp1") || isEnabled(for: "Exp2") } } public protocol YProtocol { var unusedY: String { get } } public extension YProtocol { func usedZ() { } } protocol UnusedInternal { } extension UnusedInternal { func unusedX() { } } #endif ================================================ FILE: Tests/TestClasses/Fixtures/test5.swift ================================================ #if TESTFILE enum State { case eat(SomeFood) case sleep(SomeTime) } public func toJSON(arg: String) -> JSON { var dict = [String: JSON]() switch arg { case .eat(let value): print(value) case .sleep(let value): dict["state"] = value } return .dictionary(dict) } public typealias T = (String, EventHandler) public typealias EventHandler = (Bool, StreamingResponse?, Error?) -> () #if SOME_CONDITION public extension String { func withAssertion(_ block: (_ str: StaticString) -> ()) { if let stringData = self.data(using: .utf8, allowLossyConversion: false) { var x = StaticString() x._utf8CodeUnitCount = stringData.count stringData.withUnsafeBytes { rawBufferPointer in if let rawPtr = rawBufferPointer.baseAddress { x._startPtrOrData = Int(bitPattern: rawPtr) } block(x) } } } } #endif #endif ================================================ FILE: Tests/TestClasses/Fixtures/test6.swift ================================================ #if TESTFILE import Foundation import Test1 private let error = NSError(domain: TestDomain, code: InvalidData, userInfo: nil) extension String { static func instance(from data: Data) throws -> String { let result = self.init(data: data, encoding: .utf8) if let result = result { return result } else { throw error } } } let SomeSingleton = SomeSingletonObject() // This is a comment // doc comment // asdf public protocol Bar: P1, P2 { } public typealias P1 = Cat.P1 public protocol XAB { subscript(key: Int) -> String? { get } } class KEY {} extension KEY: XAB { public subscript(key: Int) -> String? { return nil } func bar() { } var debugDescription: String { return "this is KEY" } } public protocol ObjectReference { } public protocol P { func iteratedObjects(_ object: Any) -> [ObjectReference] } public final class SynchronousKeyValueStoreWrapper: SynchronousKeyValueAssociating where Key: StoredKeying { var base: KeyValueStore? public subscript(key: T.Key) -> T.Value? { return nil } } #endif ================================================ FILE: Tests/TestClasses/Fixtures/test7.swift ================================================ #if TESTFILE extension Foo { #if DEBUG var baz: Bool { if case .test = RunType.current { return !(Handler.isHandlerActive() || runner.sharedInstance.isActive) } return false } var zmock: ZMock { shared { ZMock() } } fileprivate var zvar: Z { shared { Z(with: self) } } #else fileprivate var cat: Z { shared { Z(with: self) } } #endif } import Test6 let k = Klass() class Klass: P { public func iteratedObjects(_ object: Any) -> [Result] { return [] } private static var loadedFonts: Synchronized<[String: UIFont]> = Synchronized() public init() { testAssert(true) } public init(store: KeyValueStore, storeKey: Key, target: Int, initialHitTargetValue: Bool? = nil) { self.store = store self.storeKey = storeKey self.target = target + 1 self.hitTargetSubject = ReplaySubject.create(bufferSize: 1) queue.async { let count = store.synchronously(operate: { (container: SynchronousKeyValueStoreContainer?) -> Int? in return container?.item(for: storeKey) }) print(count) } } } public final class KeyValueStore where Key: StoredKeying { public func synchronously(operations: (SynchronousKeyValueStoreContainer?) -> T) -> T { let container = SynchronousKeyValueStoreContainer(self) let result = operations(container) container.invalidate() return result } } #endif ================================================ FILE: Tests/TestClasses/Fixtures/test8.swift ================================================ #if TESTFILE import Test9 import Test10 import Foundation class Foo: ProtocolX { func filterBy(uuid: String) -> Int? { return nil } var bar: Bool { let found = FooKlass.sharedInstance.bar(arg) let notFound = ListInteractor.application if found, !notFound { return true } } var baz: String { if case let .someCase = BarType.current { return !(Klass().isActive() || Klass().isApplicable() || OtherKlass.sharedInstance.shouldRun) } return "" } } let foo = Foo() #endif ================================================ FILE: Tests/TestClasses/SwiftCodeSanTests.swift ================================================ import Foundation class DCETests: SwiftCodeSanTestCase { func testExample() { // TODO: add a test } } ================================================ FILE: install-script.sh ================================================ #!/bin/bash # Causes the shell to exit if any subcommand returns a non-zero status set -e showhelp() { echo "Description: Builds and installs target specified Usage: -s/--source-dir [source dir], -t/--target [name of target to build/install], -d/--destination-dir [destination dir], -o/--output [output file name in tar.gz]" exit } if [[ $1 == "" ]] then showhelp fi realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD" } while [[ $# -gt 0 ]] do key="$1" case $key in -s|--source-dir) SRCDIR=$(realpath "$2") shift # past argument shift # past value ;; -d|--destination-dir) DESTDIR=$(realpath "$2") shift # past argument shift # past value ;; -t|--target) TARGET="$2" shift # past argument shift # past value ;; -o|--output) OUTFILE="$2" shift # past argument shift # past value ;; -h|--help) showhelp ;; *) showhelp ;; esac done CUR=$PWD echo "** Clean/Build..." echo "SOURCE DIR = ${SRCDIR}" echo "TARGET = ${TARGET}" echo "DESTINATION DIR = ${DESTDIR}" echo "OUTPUT FILE = ${OUTFILE}" cd "$SRCDIR" rm -rf .build swift build -c release cd .build/release echo "** Install..." cp "$(xcode-select -p)"/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/lib_InternalSwiftSyntaxParser.dylib . install_name_tool -change @rpath/lib_InternalSwiftSyntaxParser.dylib @executable_path/lib_InternalSwiftSyntaxParser.dylib "$TARGET" tar -cvzf "$OUTFILE" "$TARGET" lib_InternalSwiftSyntaxParser.dylib mv "$OUTFILE" "$DESTDIR" cd "$CUR" echo "** Output file is at $DESTDIR/$OUTFILE" echo "** Done."