├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .spi.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── SwiftInspector │ └── main.swift ├── SwiftInspectorAnalyzers │ ├── Analyzer.swift │ ├── CachedSyntaxTree.swift │ ├── FileManager+SwiftFiles.swift │ ├── ImportsAnalyzer.swift │ ├── InitializerAnalyzer.swift │ ├── StandardAnalyzer.swift │ ├── StaticUsageAnalyzer.swift │ ├── Tests │ │ ├── FileManagerSpec.swift │ │ ├── InitializerAnalyzerSpec.swift │ │ ├── StandardAnalyzerSpec.swift │ │ ├── StaticUsageAnalyzerSpec.swift │ │ ├── TypeConformanceAnalyzerSpec.swift │ │ ├── TypeLocationAnalyzerSpec.swift │ │ ├── TypealiasAnalyzerSpec.swift │ │ └── TypesAnalyzerSpec.swift │ ├── TypeConformanceAnalyzer.swift │ ├── TypeLocationAnalyzer.swift │ ├── TypealiasAnalyzer.swift │ └── TypesAnalyzer.swift ├── SwiftInspectorCommands │ ├── ImportsCommand.swift │ ├── InitializerCommand.swift │ ├── InspectorCommand.swift │ ├── InspectorError.swift │ ├── StaticUsageCommand.swift │ ├── Tests │ │ ├── ImportCommandSpec.swift │ │ ├── InitializerCommandSpec.swift │ │ ├── StaticUsageCommandSpec.swift │ │ ├── TestTask.swift │ │ ├── TypeConformanceCommandSpec.swift │ │ ├── TypeLocationCommandSpec.swift │ │ ├── TypealiasCommandSpec.swift │ │ ├── TypesCommandSpec.swift │ │ └── XCTestManifests.swift │ ├── TypeConformanceCommand.swift │ ├── TypeLocationCommand.swift │ ├── TypealiasCommand.swift │ └── TypesCommand.swift ├── SwiftInspectorTestHelpers │ ├── SyntaxVisitor+WalkContent.swift │ ├── Temporary.swift │ └── Tests │ │ └── TemporarySpec.swift └── SwiftInspectorVisitors │ ├── AssertionFailure.swift │ ├── AssociatedtypeVisitor.swift │ ├── DeclarationModifierVisitor.swift │ ├── ExtensionVisitor.swift │ ├── FileVisitor.swift │ ├── FunctionDeclarationVisitor.swift │ ├── GenericParameterVisitor.swift │ ├── GenericRequirementVisitor.swift │ ├── ImportVisitor.swift │ ├── Modifiers.swift │ ├── NestableDeclSyntax.swift │ ├── NestableTypeInfo.swift │ ├── NestableTypeVisitor.swift │ ├── ParsingTracker.swift │ ├── PropertyInfo.swift │ ├── PropertyVisitor.swift │ ├── ProtocolVisitor.swift │ ├── Tests │ ├── AssociatedtypeVisitorSpec.swift │ ├── ExtensionVisitorSpec.swift │ ├── FileVisitorSpec.swift │ ├── FunctionDeclarationVisitorSpec.swift │ ├── GenericRequirementVisitorSpec.swift │ ├── ImportVisitorSpec.swift │ ├── NestableTypeVisitorSpec.swift │ ├── PropertyInfoSpec.swift │ ├── PropertyVisitorSpec.swift │ ├── ProtocolVisitorSpec.swift │ ├── TypeDescriptionSpec.swift │ ├── TypeInheritanceVisitorSpec.swift │ └── TypealiasVisitorSpec.swift │ ├── TypeDescription.swift │ ├── TypeInheritanceVisitor.swift │ └── TypealiasVisitor.swift ├── codecov.yml ├── docs └── Releasing.md └── img ├── macstadium.png └── swiftinspector.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-12 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Select Xcode 13.3.1 13 | run: xcversion select 13.3.1 14 | 15 | - name: Build project 16 | run: swift build 17 | 18 | - name: Run tests and gather code coverage 19 | run: swift test --enable-code-coverage 20 | 21 | - name: Prepare code coverage 22 | run: xcrun llvm-cov export -format="lcov" .build/debug/SwiftInspectorPackageTests.xctest/Contents/MacOS/SwiftInspectorPackageTests -instr-profile .build/debug/codecov/default.profdata > info.lcov 23 | 24 | - name: Upload code coverage 25 | run: bash <(curl https://codecov.io/bash) 26 | env: 27 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | .swiftpm 6 | xcuserdata/ 7 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: 5 | - SwiftInspectorAnalyzers 6 | - SwiftInspectorVisitors 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to SwiftInspector 2 | 3 | :+1: Thanks for taking the time to contribute to Swift Inspector! :+1: 4 | 5 | This document explains some of the guidelines around contributing to this project as well as how this project is structured, so you can jump in! 6 | 7 | ## Alignment first 8 | 9 | We ask you that if you have an idea for a new feature you [open an issue](../../issues/new). 10 | 11 | ## Submitting your changes 12 | 13 | Whenever your feature is ready for review, please [open a PR](../../pull/new/master) with a clear list of what you've done. 14 | 15 | For any change you make, we ask you to also **add corresponding unit tests**. 16 | 17 | ## How to contribute 18 | 19 | ### Structure of SwiftInspector 20 | 21 | SwiftInspector is divided into four main parts: 22 | 23 | #### SwiftInspector 24 | 25 | Contains the main executable for this command line tool. It only contains the entry point `main.swift` file. 26 | 27 | #### SwiftInspectorCommand 28 | 29 | Contains all the files for managing commands and options for these commands. We can think of this as the *frontend* of this project. 30 | 31 | #### SwiftInspectorAnalyzers 32 | 33 | Comprises this project's analyzers. Any file related to analyzing Swift code should be put here. This is the layer that the Command interacts with directly. 34 | 35 | #### SwiftInspectorVisitors 36 | 37 | Comprises this project's syntax visitors. Any file that visits Swift syntax nodes should be put here. Analyzers should use the visitors in this module. 38 | 39 | ## Suggested workflow 40 | 41 | ### Writing a new Command 42 | 43 | To add a new command create a `YourCommand.swift` file inside `SwiftInspectorCommand` and add it to the `InspectorCommand` subcommands. Your command should delegate to `SwiftInspectorAnalyzer` for all the logic related to analyzing Swift code. 44 | 45 | When you're ready to write a new command, I suggest you start by writing unit tests by relying on the [TestTask.swift](https://github.com/fdiaz/SwiftInspector/blob/407f34bb93df750d95cedaa10f656f0586d0769e/Sources/SwiftInspectorCommands/Tests/TestTask.swift) file to create fake commands with arguments: 46 | 47 | ```swift 48 | private struct YourNewCommand { 49 | fileprivate static func run(path: String, arguments: [String] = []) throws -> TaskStatus { 50 | let arguments = ["newcommand", "--path", path] + arguments 51 | return try TestTask.run(withArguments: arguments) 52 | } 53 | } 54 | ``` 55 | 56 | Refer to the [tests in the Commands target](https://github.com/fdiaz/SwiftInspector/tree/407f34bb93df750d95cedaa10f656f0586d0769e/Sources/SwiftInspectorCommands/Tests) for examples. 57 | 58 | ### Writing new Analyzer functionality 59 | 60 | Since we want to separate the commands from the analyzer functionality, you should abstract your analyzer functionality in a class that lives in `SwiftInspectorAnalyzer`. Analyzers are a thin bridge between commands and syntax visitors – they are responsible for kicking off syntax visitation and then packaging up and returning the information gathered from syntax visitors. 61 | 62 | ### Writing new Visitor functionality 63 | 64 | Code that visits Swift syntax nodes should be live in the `SwiftInspectorVisitors` module. 65 | 66 | I suggest relying on the [Swift AST Explorer](https://swift-ast-explorer.com/) to understand the AST better and play around with different use cases. 67 | 68 | When you're ready to write some code, I suggest you to start by writing unit tests by relying on the [Temporary.swift](https://github.com/fdiaz/SwiftInspector/blob/be2efb40fb1d085e69ae92a873c64fab9b66fa9a/Sources/SwiftInspectorTestHelpers/Temporary.swift) file to create fake files for testing. 69 | 70 | ```swift 71 | context("when something happens") { 72 | beforeEach { 73 | fileURL = try! Temporary.makeFile( 74 | content: """ 75 | typealias SomeTypealias = TypeA & TypeB 76 | """ 77 | ) 78 | } 79 | 80 | it("something happens") { 81 | let result = try? sut.analyze(fileURL: fileURL) 82 | expect(result) == Something 83 | } 84 | } 85 | ``` 86 | 87 | Refer to the [tests in the Analyzer target](https://github.com/fdiaz/SwiftInspector/tree/407f34bb93df750d95cedaa10f656f0586d0769e/Sources/SwiftInspectorAnalyzers/Tests) for examples. 88 | 89 | ### Things to consider: 90 | - We use Quick and Nimble in this repo, we rely on the following convention: 91 | - Use `describe` blocks for each internal and public method 92 | - Use `context` to setup different scenarios (e.g. "when A happens") 93 | - Only use one assert per test whenever possible 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Francisco Diaz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prefix ?= /usr/local 2 | bindir ?= $(prefix)/bin 3 | xcode_path ?= $(shell xcode-select -p) 4 | xcode_toolchain ?= $(xcode_path)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx 5 | 6 | .PHONY: build 7 | build: 8 | xcrun swift build -c release --arch arm64 --arch x86_64 9 | 10 | .PHONY: install 11 | install: build 12 | install ".build/release/swiftinspector" "$(bindir)" 13 | 14 | .PHONY: uninstall 15 | uninstall: 16 | rm -rf "$(bindir)/swiftinspector" 17 | 18 | .PHONY: clean 19 | clean: 20 | rm -rf .build 21 | 22 | .PHONY: release 23 | release: build 24 | cp .build/apple/Products/Release/swiftinspector . 25 | zip "swiftinspector-$(shell git rev-parse --short HEAD).zip" swiftinspector 26 | rm swiftinspector 27 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "cwlcatchexception", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state" : { 8 | "revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea", 9 | "version" : "2.1.1" 10 | } 11 | }, 12 | { 13 | "identity" : "cwlpreconditiontesting", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state" : { 17 | "revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", 18 | "version" : "2.1.0" 19 | } 20 | }, 21 | { 22 | "identity" : "nimble", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/Quick/Nimble.git", 25 | "state" : { 26 | "revision" : "c93f16c25af5770f0d3e6af27c9634640946b068", 27 | "version" : "9.2.1" 28 | } 29 | }, 30 | { 31 | "identity" : "quick", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/Quick/Quick.git", 34 | "state" : { 35 | "revision" : "bd86ca0141e3cfb333546de5a11ede63f0c4a0e6", 36 | "version" : "4.0.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-argument-parser", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-argument-parser", 43 | "state" : { 44 | "revision" : "f3c9084a71ef4376f2fabbdf1d3d90a49f1fabdb", 45 | "version" : "1.1.2" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-syntax", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-syntax.git", 52 | "state" : { 53 | "revision" : "0b6c22b97f8e9320bca62e82cdbee601cf37ad3f", 54 | "version" : "0.50600.1" 55 | } 56 | } 57 | ], 58 | "version" : 2 59 | } 60 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftInspector", 8 | platforms: [ 9 | .macOS(.v12) 10 | ], 11 | products: [ 12 | .executable(name: "swiftinspector", targets: ["SwiftInspector"]), 13 | .library(name: "SwiftInspectorAnalyzers", targets: ["SwiftInspectorAnalyzers"]), 14 | .library(name: "SwiftInspectorVisitors", targets: ["SwiftInspectorVisitors"]), 15 | .library(name: "SwiftInspectorTestHelpers", targets: ["SwiftInspectorTestHelpers"]), 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), 19 | .package(url: "https://github.com/apple/swift-syntax.git", exact: "0.50600.1"), 20 | .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "9.0.1")), 21 | .package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "4.0.0")), 22 | ], 23 | targets: [ 24 | .executableTarget( 25 | name: "SwiftInspector", 26 | dependencies: [ 27 | "SwiftInspectorCommands", 28 | "lib_InternalSwiftSyntaxParser", 29 | ], 30 | linkerSettings: [.unsafeFlags(["-Xlinker", "-dead_strip_dylibs"])]), 31 | 32 | .target( 33 | name: "SwiftInspectorCommands", 34 | dependencies: [ 35 | "SwiftInspectorAnalyzers", 36 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 37 | ], exclude: ["Tests"]), 38 | .testTarget( 39 | name: "SwiftInspectorCommandsTests", 40 | dependencies: [ 41 | "SwiftInspectorCommands", 42 | "SwiftInspectorTestHelpers", 43 | "Nimble", 44 | "Quick", 45 | ], 46 | path: "Sources/SwiftInspectorCommands/Tests", 47 | linkerSettings: [.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "$DT_TOOLCHAIN_DIR/usr/lib/swift/macosx"])]), 48 | 49 | .target( 50 | name: "SwiftInspectorAnalyzers", 51 | dependencies: [ 52 | .product(name: "SwiftSyntax", package: "swift-syntax"), 53 | .product(name: "SwiftSyntaxParser", package: "swift-syntax"), 54 | "SwiftInspectorVisitors", 55 | ], 56 | exclude: ["Tests"]), 57 | .testTarget( 58 | name: "SwiftInspectorAnalyzersTests", 59 | dependencies: [ 60 | "SwiftInspectorAnalyzers", 61 | "SwiftInspectorTestHelpers", 62 | "SwiftInspectorVisitors", 63 | "Nimble", 64 | "Quick", 65 | ], 66 | path: "Sources/SwiftInspectorAnalyzers/Tests", 67 | linkerSettings: [.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "$DT_TOOLCHAIN_DIR/usr/lib/swift/macosx"])]), 68 | 69 | .target( 70 | name: "SwiftInspectorTestHelpers", 71 | dependencies: [ 72 | .product(name: "SwiftSyntax", package: "swift-syntax"), 73 | .product(name: "SwiftSyntaxParser", package: "swift-syntax"), 74 | ], 75 | exclude: ["Tests"]), 76 | .testTarget( 77 | name: "SwiftInspectorTestHelpersTests", 78 | dependencies: [ 79 | "SwiftInspectorTestHelpers", 80 | "Nimble", 81 | "Quick", 82 | ], 83 | path: "Sources/SwiftInspectorTestHelpers/Tests", 84 | linkerSettings: [.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "$DT_TOOLCHAIN_DIR/usr/lib/swift/macosx"])]), 85 | 86 | .target( 87 | name: "SwiftInspectorVisitors", 88 | dependencies: [ 89 | .product(name: "SwiftSyntax", package: "swift-syntax"), 90 | ], 91 | exclude: ["Tests"]), 92 | .testTarget( 93 | name: "SwiftInspectorVisitorsTests", 94 | dependencies: [ 95 | "SwiftInspectorTestHelpers", 96 | "SwiftInspectorVisitors", 97 | "Nimble", 98 | "Quick", 99 | ], 100 | path: "Sources/SwiftInspectorVisitors/Tests", 101 | linkerSettings: [.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "$DT_TOOLCHAIN_DIR/usr/lib/swift/macosx"])]), 102 | 103 | .binaryTarget( 104 | name: "lib_InternalSwiftSyntaxParser", 105 | url: "https://github.com/keith/StaticInternalSwiftSyntaxParser/releases/download/5.6/lib_InternalSwiftSyntaxParser.xcframework.zip", 106 | checksum: "88d748f76ec45880a8250438bd68e5d6ba716c8042f520998a438db87083ae9d" 107 | ), 108 | ] 109 | ) 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![swiftinspector](img/swiftinspector.png) 2 | 3 | ![](https://github.com/fdiaz/SwiftInspector/workflows/macOS/badge.svg) 4 | [![codecov](https://codecov.io/gh/fdiaz/SwiftInspector/branch/main/graph/badge.svg)](https://codecov.io/gh/fdiaz/SwiftInspector) 5 | [![Project Status: Abandoned – Initial development has started, but there has not yet been a stable, usable release; the project has been abandoned and the author(s) do not intend on continuing development.](https://www.repostatus.org/badges/latest/abandoned.svg)](https://www.repostatus.org/#abandoned) 6 | 7 | `SwiftInspector` is a command line tool and set of SPM libraries built on top of [SwiftSyntax](https://github.com/apple/swift-syntax). `SwiftInspector` reliably finds usages of classes, protocols, properties, etc. in a codebase by analyzing the Swift [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree). 8 | 9 | ## Archived Project 10 | 11 | This project is considered abandoned and has been archived. 12 | 13 | --- 14 | 15 | ## Requirements 16 | 17 | - Swift 5.6 18 | - Xcode 13.3.1 19 | 20 | ## Install 21 | 22 | Run the following command: 23 | 24 | ``` 25 | $ git clone git@github.com:fdiaz/SwiftInspector.git 26 | $ cd SwiftInspector 27 | $ make install 28 | ``` 29 | 30 | ## Develop 31 | 32 | If you want to contribute to this project, please take a look at our [CONTRIBUTING](CONTRIBUTING.md) guidelines. To open the project in Xcode, open the `Package.swift` file at the root of the repo. 33 | 34 | ## Default branch 35 | The default branch of this repository is `main`. Between the initial commit and [75bd9f4 36 | ](https://github.com/fdiaz/SwiftInspector/commit/75bd9f440d72ade9abd1e1d8e9d118e8bb8701a0), the default branch of this repository was `master`. See [#38](https://github.com/fdiaz/SwiftInspector/issues/38) for more details on why this change was made. 37 | 38 | ## License 39 | 40 | [MIT](LICENSE) 41 | -------------------------------------------------------------------------------- /Sources/SwiftInspector/main.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 09/18/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import SwiftInspectorCommands 26 | 27 | InspectorCommand.main() 28 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/Analyzer.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/23/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | 27 | /// A protocol that defines how to analyze a Swift file from an URL and converts it into a generic output 28 | public protocol Analyzer { 29 | associatedtype Output 30 | 31 | /// Analyzes a Swift file and returns an StandardOutputConvertible output 32 | /// - Parameter fileURL: The fileURL where the Swift file is located 33 | func analyze(fileURL: URL) throws -> Output 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/CachedSyntaxTree.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 1/13/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | import SwiftSyntaxParser 28 | 29 | /// A type that knows how to return a source file syntax given a file URL 30 | public final class CachedSyntaxTree { 31 | public init() { 32 | cachedSyntax = [:] 33 | } 34 | 35 | /// Returns a memoized `SourceFileSyntax` tree if it exists, otherwise calculates, memoizes, and returns the syntax tree of the file URL 36 | /// 37 | /// - Parameter fileURL: The location of the Swift file to parse 38 | /// 39 | /// - Warning: This method is not thread-safe 40 | func syntaxTree(for fileURL: URL) throws -> SourceFileSyntax { 41 | guard let syntax = cachedSyntax[fileURL] else { 42 | return try memoizeSyntaxTree(at: fileURL) 43 | } 44 | return syntax 45 | } 46 | 47 | /// It reads the `SourceFileSyntax` at the file URL location and parses it into a `SourceFileSyntax, then it caches the result in memory 48 | /// 49 | /// - Parameter fileURL: The location of the Swift file to parse 50 | private func memoizeSyntaxTree(at fileURL: URL) throws -> SourceFileSyntax { 51 | let cached = try SyntaxParser.parse(fileURL) 52 | cachedSyntax[fileURL] = cached 53 | return cached 54 | } 55 | 56 | private var cachedSyntax: [URL: SourceFileSyntax] 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/FileManager+SwiftFiles.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/14/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | 27 | extension FileManager { 28 | 29 | /// Finds all the swift files at a given file URL 30 | /// If the baseURL is a folder, it will traverse all its subdirectories for any .swift file 31 | /// 32 | /// - Parameter baseURL: The URL to look for Swift files. It can either be a file or a directory 33 | /// - Returns: All the files with a .swift extension at the given URL or an empty array if none exist 34 | public func swiftFiles(at baseURL: URL) -> [URL] { 35 | guard baseURL.hasDirectoryPath else { 36 | return baseURL.pathExtension == "swift" ? [baseURL] : [] 37 | } 38 | 39 | var swiftFiles: [URL] = [] 40 | 41 | guard let enumerator = self.enumerator( 42 | at: baseURL, 43 | includingPropertiesForKeys: nil, 44 | options: [.skipsHiddenFiles]) 45 | else 46 | { 47 | return [] 48 | } 49 | 50 | for case let fileURL as URL in enumerator { 51 | if fileURL.pathExtension == "swift" { 52 | swiftFiles.append(fileURL.standardizedFileURL) 53 | } 54 | } 55 | 56 | return swiftFiles 57 | } 58 | 59 | /// Checks whether a URL contains a Swift file 60 | /// 61 | /// - Parameter baseURL: The URL to look for a Swift file 62 | /// - Returns: `true` if the URL contains a Swift file, `false` otherwise 63 | public func isSwiftFile(at baseURL: URL) -> Bool { 64 | FileManager.default.fileExists(atPath: baseURL.path) && !baseURL.hasDirectoryPath && baseURL.pathExtension == "swift" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/ImportsAnalyzer.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/28/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftInspectorVisitors 27 | 28 | extension StandardAnalyzer { 29 | 30 | public func analyzeImports(fileURL: URL) throws -> [ImportStatement] { 31 | let visitor = ImportVisitor() 32 | try analyze(fileURL: fileURL, withVisitor: visitor) 33 | return visitor.imports 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/StandardAnalyzer.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/27/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | 28 | /// A struct that enables analyzing a Swift file at a URL with a Swift syntax visitor. 29 | public struct StandardAnalyzer { 30 | 31 | /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from 32 | public init(cachedSyntaxTree: CachedSyntaxTree = .init()) { 33 | self.cachedSyntaxTree = cachedSyntaxTree 34 | } 35 | 36 | /// Analyzes a Swift file with the provided visitor 37 | /// - Parameters: 38 | /// - fileURL: The fileURL where the Swift file is located 39 | /// - visitor: A Swift syntax visitor to use to analyze the provided file 40 | func analyze(fileURL: URL, withVisitor visitor: SyntaxVisitor) throws 41 | { 42 | let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL) 43 | visitor.walk(syntax) 44 | } 45 | 46 | // MARK: Private 47 | 48 | private let cachedSyntaxTree: CachedSyntaxTree 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/StaticUsageAnalyzer.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/16/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | 28 | public final class StaticUsageAnalyzer: Analyzer { 29 | 30 | /// - Parameter staticMember: The type and member names of the static member we're looking 31 | /// - Parameter cachedSyntaxTree: The cached syntax tree from which to return the AST tree 32 | public init(staticMember: StaticMember, cachedSyntaxTree: CachedSyntaxTree = .init()) { 33 | self.staticMember = staticMember 34 | self.cachedSyntaxTree = cachedSyntaxTree 35 | } 36 | 37 | /// Analyzes if the Swift file contains the static member specified 38 | /// - Parameter fileURL: The fileURL where the Swift file is located 39 | public func analyze(fileURL: URL) throws -> StaticUsage { 40 | var isUsed = false 41 | let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL) 42 | let reader = StaticUsageReader() { [unowned self] node in 43 | isUsed = isUsed || self.isSyntaxNode(node, ofType: self.staticMember) 44 | } 45 | reader.walk(syntax) 46 | 47 | return StaticUsage(staticMember: self.staticMember, isUsed: isUsed) 48 | } 49 | 50 | // MARK: Private 51 | private func isSyntaxNode(_ node: MemberAccessExprSyntax, ofType staticMember: StaticMember) -> Bool { 52 | // A MemberAccessExprSyntax contains a base, a dot and a name. 53 | // The base in this case will be the type of the staticMember, while the name is the member 54 | 55 | let baseNode = node.base?.as(IdentifierExprSyntax.self) 56 | let nameText = node.name.text 57 | guard let baseText = baseNode?.identifier.text else { 58 | return false 59 | } 60 | 61 | return baseText == staticMember.typeName && nameText == staticMember.memberName 62 | } 63 | 64 | private let staticMember: StaticMember 65 | private let cachedSyntaxTree: CachedSyntaxTree 66 | } 67 | 68 | public struct StaticMember: Codable, Equatable { 69 | public init(typeName: String, memberName: String) { 70 | self.typeName = typeName 71 | self.memberName = memberName 72 | } 73 | 74 | public let typeName: String 75 | public let memberName: String 76 | } 77 | 78 | public struct StaticUsage: Equatable { 79 | public let staticMember: StaticMember 80 | public let isUsed: Bool 81 | } 82 | 83 | private final class StaticUsageReader: SyntaxVisitor { 84 | init(onNodeVisit: @escaping (MemberAccessExprSyntax) -> Void) { 85 | self.onNodeVisit = onNodeVisit 86 | } 87 | 88 | override func visit(_ node: MemberAccessExprSyntax) -> SyntaxVisitorContinueKind { 89 | onNodeVisit(node) 90 | return .visitChildren 91 | } 92 | 93 | private let onNodeVisit: (MemberAccessExprSyntax) -> Void 94 | } 95 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/Tests/FileManagerSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/14/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import Nimble 27 | import Quick 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorAnalyzers 31 | 32 | final class FileManagerSpec: QuickSpec { 33 | override func spec() { 34 | var fileManager: FileManager! 35 | 36 | beforeEach { 37 | fileManager = FileManager.default 38 | } 39 | 40 | describe("swiftFileURLs(at:)") { 41 | 42 | context("with a file") { 43 | var fileURL: URL! 44 | beforeEach { 45 | fileURL = try! Temporary.makeFile(content: "") 46 | } 47 | afterEach { 48 | try? Temporary.removeItem(at: fileURL) 49 | } 50 | 51 | it("returns the file path") { 52 | expect(fileManager.swiftFiles(at: fileURL)) == [fileURL] 53 | } 54 | } 55 | 56 | context("with a directory") { 57 | var parentURL: URL! 58 | beforeEach { 59 | parentURL = try! Temporary.makeFolder() 60 | } 61 | afterEach { 62 | try? Temporary.removeItem(at: parentURL) 63 | } 64 | 65 | context("when there are no swift files") { 66 | it("returns an empty array") { 67 | expect(fileManager.swiftFiles(at: parentURL)) == [] 68 | } 69 | 70 | it("does not return other file extensions") { 71 | let _ = try! Temporary.makeFile(content: "", fileExtension: "txt", atPath: parentURL.path) 72 | let _ = try! Temporary.makeFile(content: "", fileExtension: "png", atPath: parentURL.path) 73 | 74 | expect(fileManager.swiftFiles(at: parentURL)) == [] 75 | } 76 | } 77 | 78 | context("when there is one swift file") { 79 | it("returns an array with that file URL") { 80 | let fileURL = try! Temporary.makeFile(content: "", atPath: parentURL.path) 81 | expect(fileManager.swiftFiles(at: parentURL)) == [fileURL] 82 | } 83 | } 84 | 85 | context("when there are multiple swift files") { 86 | var fileURL1: URL! 87 | var fileURL2: URL! 88 | 89 | beforeEach { 90 | fileURL1 = try! Temporary.makeFile(content: "abc", atPath: parentURL.path) 91 | fileURL2 = try! Temporary.makeFile(content: "xyz", atPath: parentURL.path) 92 | } 93 | 94 | it("returns an array with both of the file URLs") { 95 | let sut = fileManager.swiftFiles(at: parentURL) 96 | 97 | expect(sut).to(contain([fileURL1, fileURL2])) 98 | } 99 | 100 | it("contains only 2 elements") { 101 | let sut = fileManager.swiftFiles(at: parentURL) 102 | 103 | expect(sut.count) == 2 104 | } 105 | } 106 | 107 | context("when there are swift files in subfolders") { 108 | it("returns the swift file in the subfolder") { 109 | let parentURL = try! Temporary.makeFolder() 110 | let subfolderURL = try! Temporary.makeFolder(parentPath: parentURL.path) 111 | let fileURL = try! Temporary.makeFile(content: "", atPath: subfolderURL.path) 112 | 113 | expect(fileManager.swiftFiles(at: parentURL)) == [fileURL] 114 | } 115 | } 116 | 117 | context("with a subfolder with spaces") { 118 | it("returns the swift files in the subfolder") { 119 | let parentURL = try! Temporary.makeFolder(parentPath: parentURL.path) 120 | let subfolderURL = try! Temporary.makeFolder(name: "Some Folder Name", parentPath: parentURL.path) 121 | let fileURL = try! Temporary.makeFile(content: "", atPath: subfolderURL.path) 122 | 123 | expect(fileManager.swiftFiles(at: parentURL)) == [fileURL] 124 | 125 | } 126 | } 127 | } 128 | 129 | } 130 | 131 | describe("isSwiftFile(at:)") { 132 | context("with a directory") { 133 | var parentURL: URL! 134 | afterEach { 135 | try? Temporary.removeItem(at: parentURL) 136 | } 137 | 138 | it("returns false") { 139 | parentURL = try! Temporary.makeFolder() 140 | expect(fileManager.isSwiftFile(at: parentURL)) == false 141 | } 142 | 143 | context("when the directory ends in .swift") { 144 | it("returns false") { 145 | parentURL = try! Temporary.makeFolder(name: "Some.swift") 146 | expect(fileManager.isSwiftFile(at: parentURL)) == false 147 | } 148 | } 149 | } 150 | 151 | context("with a file") { 152 | var fileURL: URL! 153 | 154 | afterEach { 155 | try? Temporary.removeItem(at: fileURL) 156 | } 157 | 158 | it("returns false if it's not a Swift file") { 159 | fileURL = try! Temporary.makeFile(content: "", fileExtension: ".txt") 160 | expect(fileManager.isSwiftFile(at: fileURL)) == false 161 | } 162 | 163 | it("returns true if it's a Swift file") { 164 | fileURL = try! Temporary.makeFile(content: "", fileExtension: ".swift") 165 | expect(fileManager.isSwiftFile(at: fileURL)) == true 166 | } 167 | } 168 | 169 | context("with an invalid URL") { 170 | it("returns false") { 171 | let fileURL = URL(fileURLWithPath: "") 172 | expect(fileManager.isSwiftFile(at: fileURL)) == false 173 | } 174 | } 175 | 176 | } 177 | 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/Tests/StandardAnalyzerSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/27/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import Nimble 27 | import Quick 28 | import SwiftSyntax 29 | import SwiftInspectorTestHelpers 30 | 31 | @testable import SwiftInspectorAnalyzers 32 | 33 | final class StandardAnalyzerSpec: QuickSpec { 34 | 35 | private final class MockVisitor: SyntaxVisitor { 36 | var ifStatementSyntaxVisitCount = 0 37 | override func visitPost(_ node: IfStmtSyntax) { 38 | ifStatementSyntaxVisitCount += 1 39 | } 40 | } 41 | 42 | private final class MockRewriter: SyntaxRewriter { 43 | var ifStatementSyntaxVisitCount = 0 44 | override func visit(_ node: IfStmtSyntax) -> StmtSyntax { 45 | ifStatementSyntaxVisitCount += 1 46 | return super.visit(node) 47 | } 48 | } 49 | 50 | override func spec() { 51 | var fileURL: URL! 52 | describe("analyze(fileURL:withVisitor:)") { 53 | beforeEach { 54 | fileURL = try? Temporary.makeFile(content: "if true {}") 55 | } 56 | afterEach { 57 | guard let fileURL = fileURL else { 58 | return 59 | } 60 | try? Temporary.removeItem(at: fileURL) 61 | } 62 | 63 | it("walks the visitor over the content") { 64 | let visitor = MockVisitor() 65 | try StandardAnalyzer().analyze(fileURL: fileURL, withVisitor: visitor) 66 | 67 | expect(visitor.ifStatementSyntaxVisitCount) == 1 68 | } 69 | 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/Tests/TypealiasAnalyzerSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/25/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Nimble 26 | import Quick 27 | import Foundation 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorAnalyzers 31 | 32 | final class TypealiasAnalyzerSpec: QuickSpec { 33 | override func spec() { 34 | var fileURL: URL! 35 | var sut = TypealiasAnalyzer() 36 | 37 | beforeEach { 38 | sut = TypealiasAnalyzer() 39 | } 40 | 41 | afterEach { 42 | guard let fileURL = fileURL else { 43 | return 44 | } 45 | try? Temporary.removeItem(at: fileURL) 46 | } 47 | 48 | describe("analyze(fileURL:)") { 49 | context("when there is no typealias statement") { 50 | it("returns an empty array") { 51 | fileURL = try! Temporary.makeFile( 52 | content: """ 53 | final class SomeClass {} 54 | """ 55 | ) 56 | 57 | let result = try? sut.analyze(fileURL: fileURL) 58 | expect(result).to(beEmpty()) 59 | } 60 | } 61 | 62 | context("with a single typealias statement") { 63 | beforeEach { 64 | fileURL = try! Temporary.makeFile( 65 | content: """ 66 | typealias SomeTypealias = TypeA & TypeB 67 | """ 68 | ) 69 | } 70 | 71 | it("returns the name of the typelias") { 72 | let result = try? sut.analyze(fileURL: fileURL) 73 | expect(result?.first?.name) == "SomeTypealias" 74 | } 75 | 76 | it("returns the identifiers") { 77 | let result = try? sut.analyze(fileURL: fileURL) 78 | expect(result?.first?.identifiers) == ["TypeA", "TypeB"] 79 | } 80 | } 81 | 82 | context("with multiple typealias statements") { 83 | beforeEach { 84 | fileURL = try! Temporary.makeFile( 85 | content: """ 86 | final class Some { 87 | typealias SomeTypealias = SomeType 88 | & SomeOtherType 89 | } 90 | 91 | typealias AnotherTypealias = AnotherType 92 | """ 93 | ) 94 | } 95 | 96 | it("returns all the names of the typeliases") { 97 | let result = try? sut.analyze(fileURL: fileURL) 98 | expect(result?.first?.name) == "SomeTypealias" 99 | expect(result?.last?.name) == "AnotherTypealias" 100 | } 101 | 102 | it("returns the identifiers") { 103 | let result = try? sut.analyze(fileURL: fileURL) 104 | expect(result?.first?.identifiers) == ["SomeType", "SomeOtherType"] 105 | expect(result?.last?.identifiers) == ["AnotherType"] 106 | } 107 | } 108 | 109 | context("with multiple typealias with the same name") { 110 | beforeEach { 111 | fileURL = try! Temporary.makeFile( 112 | content: """ 113 | typealias Foo = Bar 114 | 115 | struct MyNamespace { 116 | 117 | typealias Foo = Bar 118 | 119 | } 120 | """ 121 | ) 122 | } 123 | 124 | it("returns all the names of the typeliases") { 125 | let result = try? sut.analyze(fileURL: fileURL) 126 | expect(result?.count) == 2 127 | expect(result?.first?.name) == "Foo" 128 | expect(result?.last?.name) == "Foo" 129 | } 130 | 131 | it("returns all the typealias identifiers") { 132 | let result = try? sut.analyze(fileURL: fileURL) 133 | expect(result?.count) == 2 134 | expect(result?.first?.identifiers) == ["Bar"] 135 | expect(result?.last?.identifiers) == ["Bar"] 136 | } 137 | } 138 | 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/TypeConformanceAnalyzer.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/14/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | 28 | public final class TypeConformanceAnalyzer: Analyzer { 29 | 30 | /// - Parameter typeName: The name of the type we're looking a type to conform to 31 | /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from 32 | public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) { 33 | self.typeName = typeName 34 | self.cachedSyntaxTree = cachedSyntaxTree 35 | } 36 | 37 | /// Analyzes if the Swift file contains conformances to the typeName provided 38 | /// - Parameter fileURL: The fileURL where the Swift file is located 39 | public func analyze(fileURL: URL) throws -> TypeConformance { 40 | var doesConform = false 41 | var conformingTypes: [String] = [] 42 | 43 | let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL) 44 | let reader = TypeConformanceSyntaxVisitor() { [unowned self] node in 45 | let nodeConforms = self.isSyntaxNode(node, ofType: self.typeName) 46 | guard nodeConforms else { return } 47 | doesConform = doesConform || nodeConforms 48 | 49 | guard let node = self.findConformingType(of: node) else { return } 50 | conformingTypes.append(node) 51 | } 52 | 53 | reader.walk(syntax) 54 | 55 | return TypeConformance(typeName: typeName, doesConform: doesConform, conformingTypeNames: conformingTypes) 56 | } 57 | 58 | // MARK: Private 59 | 60 | private func isSyntaxNode(_ node: InheritedTypeSyntax, ofType typeName: String) -> Bool { 61 | // Remove leading and trailing whitespace trivia 62 | let syntaxTypeName = String(describing: node.typeName).trimmingCharacters(in: .whitespaces) 63 | return (syntaxTypeName == self.typeName) 64 | } 65 | 66 | private func findConformingType(of node: SyntaxProtocol?) -> String? { 67 | guard let originalNode = node else { return nil } 68 | 69 | guard let parent = originalNode.parent else { return nil } 70 | 71 | // A conforming type can only be a class, struct or enum 72 | // See: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html 73 | if let classSyntax = parent.as(ClassDeclSyntax.self) { 74 | return classSyntax.identifier.text 75 | } else if let structSyntax = parent.as(StructDeclSyntax.self) { 76 | return structSyntax.identifier.text 77 | } else if let enumSyntax = parent.as(EnumDeclSyntax.self) { 78 | return enumSyntax.identifier.text 79 | } else { 80 | return findConformingType(of: parent.parent) 81 | } 82 | } 83 | 84 | private let typeName: String 85 | private let cachedSyntaxTree: CachedSyntaxTree 86 | } 87 | 88 | public struct TypeConformance: Equatable { 89 | public let typeName: String 90 | public let doesConform: Bool 91 | public let conformingTypeNames: [String] 92 | } 93 | 94 | private final class TypeConformanceSyntaxVisitor: SyntaxVisitor { 95 | init(onNodeVisit: @escaping (InheritedTypeSyntax) -> Void) { 96 | self.onNodeVisit = onNodeVisit 97 | } 98 | 99 | override func visit(_ node: InheritedTypeSyntax) -> SyntaxVisitorContinueKind { 100 | onNodeVisit(node) 101 | return .visitChildren 102 | } 103 | 104 | private let onNodeVisit: (InheritedTypeSyntax) -> Void 105 | } 106 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/TypeLocationAnalyzer.swift: -------------------------------------------------------------------------------- 1 | // Created by Michael Bachand on 3/28/20. 2 | // 3 | // Distributed under the MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | import Foundation 24 | import SwiftSyntax 25 | 26 | // MARK: - TypeLocationAnalyzer 27 | 28 | public final class TypeLocationAnalyzer: Analyzer { 29 | 30 | /// - Parameter typeName: The name of the type to locate 31 | /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from 32 | public init(typeName: String, cachedSyntaxTree: CachedSyntaxTree = .init()) { 33 | self.typeName = typeName 34 | self.cachedSyntaxTree = cachedSyntaxTree 35 | } 36 | 37 | /// Finds the location(s) of the specified type name in the Swift file 38 | /// - Parameter fileURL: The fileURL where the Swift file is located 39 | public func analyze(fileURL: URL) throws -> [LocatedType] { 40 | let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL) 41 | var result = [LocatedType]() 42 | let visitor = TypeLocationSyntaxVisitor() { [unowned self] locatedType in 43 | guard self.typeName == locatedType.name else { return } 44 | result.append(locatedType) 45 | } 46 | visitor.walk(syntax) 47 | return result 48 | } 49 | 50 | // MARK: Private 51 | 52 | private let cachedSyntaxTree: CachedSyntaxTree 53 | private let typeName: String 54 | } 55 | 56 | // MARK: - TypeLocationSyntaxVisitor 57 | 58 | private final class TypeLocationSyntaxVisitor: SyntaxVisitor { 59 | 60 | init(onNodeVisit: @escaping (_ locatedType: LocatedType) -> Void) { 61 | self.onNodeVisit = onNodeVisit 62 | } 63 | 64 | override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { 65 | processLocatedType(name: node.identifier.text, keywordToken: node.classOrActorKeyword, for: node) 66 | return .visitChildren 67 | } 68 | 69 | override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { 70 | processLocatedType(name: node.identifier.text, keywordToken: node.enumKeyword, for: node) 71 | return .visitChildren 72 | } 73 | 74 | override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { 75 | processLocatedType(name: node.identifier.text, keywordToken: node.protocolKeyword, for: node) 76 | return .visitChildren 77 | } 78 | 79 | override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { 80 | processLocatedType(name: node.identifier.text, keywordToken: node.structKeyword, for: node) 81 | return .visitChildren 82 | } 83 | 84 | override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind { 85 | // Some nodes seem to include trivia from other nodes. Only counting newlines for trivia 86 | // associated with tokens ensures we get an accurate count. 87 | // Tokens are processed after `*DeclSyntax` nodes. 88 | if let leadingTrivia = token.leadingTrivia { 89 | currentLineNumber += leadingTrivia.countOfNewlines() 90 | } 91 | if let trailingTrivia = token.trailingTrivia { 92 | currentLineNumber += trailingTrivia.countOfNewlines() 93 | } 94 | return .visitChildren 95 | } 96 | 97 | private var currentLineNumber = 0 98 | private let onNodeVisit: (LocatedType) -> Void 99 | 100 | /// Compute the location of the type and invoke the callback. 101 | private func processLocatedType(name: String, keywordToken: TokenSyntax, for node: SyntaxProtocol) { 102 | var indexOfStartingLine = currentLineNumber 103 | // `currentLineNumber` doesn't yet include newlines from the leading trivia for the first token. 104 | indexOfStartingLine += node.firstToken?.leadingTrivia.countOfNewlines() ?? 0 105 | 106 | let indexOfEndingLine = indexOfStartingLine + countOfNewlines(within: node) 107 | 108 | let locatedType = LocatedType( 109 | name: name, 110 | indexOfStartingLine: indexOfStartingLine, 111 | indexOfEndingLine: indexOfEndingLine) 112 | onNodeVisit(locatedType) 113 | } 114 | 115 | /// Find the number of newlines within this node. 116 | private func countOfNewlines(within node: SyntaxProtocol) -> Int { 117 | var countOfNewlinesInsideType = 0 118 | 119 | for (offset, token) in node.tokens.enumerated() { 120 | // We've already counted the leading trivia for the first token. 121 | if let leadingTrivia = token.leadingTrivia, offset != 0 { 122 | countOfNewlinesInsideType += leadingTrivia.countOfNewlines() 123 | } 124 | if let trailingTrivia = token.trailingTrivia{ 125 | countOfNewlinesInsideType += trailingTrivia.countOfNewlines() 126 | } 127 | } 128 | 129 | return countOfNewlinesInsideType 130 | } 131 | } 132 | 133 | // MARK: - LocatedType 134 | 135 | /// Information about a located type. Indices start with 0. 136 | public struct LocatedType: Hashable { 137 | /// The name of the type. 138 | public let name: String 139 | /// The first line of the type. 140 | public let indexOfStartingLine: Int 141 | /// The last line of the type. 142 | public let indexOfEndingLine: Int 143 | } 144 | 145 | // MARK: Trivia 146 | 147 | extension Trivia { 148 | 149 | fileprivate func countOfNewlines() -> Int { 150 | var result = 0 151 | for triviaPiece in self { 152 | switch triviaPiece { 153 | case .newlines(let count): 154 | result += count 155 | default: 156 | break 157 | } 158 | } 159 | return result 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/TypealiasAnalyzer.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/25/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | 28 | public final class TypealiasAnalyzer: Analyzer { 29 | 30 | /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from 31 | public init(cachedSyntaxTree: CachedSyntaxTree = .init()) { 32 | self.cachedSyntaxTree = cachedSyntaxTree 33 | } 34 | 35 | /// Analyzes the matching initializers of the Swift file 36 | /// - Parameter fileURL: The fileURL where the Swift file is located 37 | public func analyze(fileURL: URL) throws -> [TypealiasStatement] { 38 | let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL) 39 | var allTypealias: [TypealiasStatement] = [] 40 | let reader = TypealiasSyntaxReader() { [unowned self] node in 41 | let statement = self.typealiasStatement(from: node) 42 | allTypealias.append(statement) 43 | } 44 | reader.walk(syntax) 45 | 46 | return allTypealias 47 | } 48 | 49 | // MARK: Private 50 | private let cachedSyntaxTree: CachedSyntaxTree 51 | 52 | private func typealiasStatement(from node: TypealiasDeclSyntax) -> TypealiasStatement { 53 | var identifiers: [String] = [] 54 | 55 | for child in node.children { 56 | guard let typeInitializerSyntax = child.as(TypeInitializerClauseSyntax.self) else { 57 | continue 58 | } 59 | 60 | identifiers = findIdentifiers(from: typeInitializerSyntax) 61 | } 62 | 63 | return TypealiasStatement(name: node.identifier.text, identifiers:identifiers) 64 | } 65 | 66 | private func findIdentifiers(from node: TypeInitializerClauseSyntax) -> [String] { 67 | return node.tokens.reduce(into: []) { result, token in 68 | switch token.tokenKind { 69 | case .identifier(let name): result.append(name) 70 | default: return 71 | } 72 | } 73 | } 74 | } 75 | 76 | private final class TypealiasSyntaxReader: SyntaxVisitor { 77 | init(onNodeVisit: @escaping (TypealiasDeclSyntax) -> Void) { 78 | self.onNodeVisit = onNodeVisit 79 | } 80 | 81 | override func visit(_ node: TypealiasDeclSyntax) -> SyntaxVisitorContinueKind { 82 | onNodeVisit(node) 83 | return .visitChildren 84 | } 85 | 86 | let onNodeVisit: (TypealiasDeclSyntax) -> Void 87 | } 88 | 89 | public struct TypealiasStatement: Hashable { 90 | public let name: String 91 | public var identifiers: [String] 92 | } 93 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorAnalyzers/TypesAnalyzer.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 8/12/20. 2 | // 3 | // Copyright (c) 2020 Tyler Hedrick 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | 26 | import Foundation 27 | import SwiftSyntax 28 | 29 | public final class TypesAnalyzer: Analyzer { 30 | 31 | /// - Parameter cachedSyntaxTree: The cached syntax tree to return the AST tree from 32 | public init(cachedSyntaxTree: CachedSyntaxTree = .init()) { 33 | self.cachedSyntaxTree = cachedSyntaxTree 34 | } 35 | 36 | /// Analyzes the types located in the provided file 37 | /// - Parameter fileURL: The fileURL where the Swift file is located 38 | public func analyze(fileURL: URL) throws -> [TypeInfo] { 39 | let syntax: SourceFileSyntax = try cachedSyntaxTree.syntaxTree(for: fileURL) 40 | var result = [TypeInfo]() 41 | let visitor = TypeInfoSyntaxVisitor() { typeInfo in 42 | result.append(typeInfo) 43 | } 44 | visitor.walk(syntax) 45 | return result 46 | } 47 | 48 | // MARK: Private 49 | 50 | private let cachedSyntaxTree: CachedSyntaxTree 51 | } 52 | 53 | private final class TypeInfoSyntaxVisitor: SyntaxVisitor { 54 | init(_ onNodeVisit: @escaping (TypeInfo) -> Void) { 55 | self.onNodeVisit = onNodeVisit 56 | } 57 | 58 | override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { 59 | onNodeVisit(.init( 60 | type: .class, 61 | name: node.identifier.text, 62 | comment: comment(from: node.leadingTrivia))) 63 | return .visitChildren 64 | } 65 | 66 | override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { 67 | onNodeVisit(.init( 68 | type: .enum, 69 | name: node.identifier.text, 70 | comment: comment(from: node.leadingTrivia))) 71 | return .visitChildren 72 | } 73 | 74 | override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { 75 | onNodeVisit(.init( 76 | type: .protocol, 77 | name: node.identifier.text, 78 | comment: comment(from: node.leadingTrivia))) 79 | return .visitChildren 80 | } 81 | 82 | override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { 83 | onNodeVisit(.init( 84 | type: .struct, 85 | name: node.identifier.text, 86 | comment: comment(from: node.leadingTrivia))) 87 | return .visitChildren 88 | } 89 | 90 | // MARK: Private 91 | 92 | private let onNodeVisit: (TypeInfo) -> Void 93 | 94 | private func comment(from trivia: Trivia?) -> String { 95 | guard let trivia = trivia else { return "" } 96 | return trivia.compactMap { piece -> String? in 97 | switch piece { 98 | case .lineComment(let str): return str 99 | case .blockComment(let str): return str 100 | case .docLineComment(let str): return str 101 | case .docBlockComment(let str): return str 102 | default: return nil 103 | } 104 | }.joined(separator: "\n") 105 | } 106 | } 107 | 108 | // MARK: TypeInfo 109 | 110 | public struct TypeInfo { 111 | public enum SwiftType: String { 112 | case `class` 113 | case `struct` 114 | case `protocol` 115 | case `enum` 116 | } 117 | 118 | /// Swift type name (class, struct, protocol, or enum) 119 | public let type: SwiftType 120 | /// The name of the type 121 | public let name: String 122 | /// Comments associated with this type (leadingTrivia from SwiftSyntax) 123 | public let comment: String 124 | } 125 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/ImportsCommand.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/11/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import ArgumentParser 26 | import Foundation 27 | import SwiftInspectorAnalyzers 28 | import SwiftInspectorVisitors 29 | 30 | final class ImportsCommand: ParsableCommand { 31 | static var configuration = CommandConfiguration( 32 | commandName: "imports", 33 | abstract: "Finds all the declared imports" 34 | ) 35 | 36 | @Option(help: "The absolute path to the file or directory to inspect") 37 | var path: String 38 | 39 | @Option(help: OutputMode.help) 40 | var mode: OutputMode = .main 41 | 42 | /// Runs the command 43 | func run() throws { 44 | let cachedSyntaxTree = CachedSyntaxTree() 45 | let analyzer = StandardAnalyzer(cachedSyntaxTree: cachedSyntaxTree) 46 | let fileURL = URL(fileURLWithPath: path) 47 | 48 | let outputArray = try FileManager.default.swiftFiles(at: fileURL) 49 | .reduce(Set()) { result, url in 50 | let output = try analyzer.analyzeImports(fileURL: url) 51 | .map { outputString(from: $0) } 52 | return result.union(output) 53 | } 54 | 55 | let output = outputArray.joined(separator: "\n") 56 | print(output) 57 | } 58 | 59 | /// Validates if the arguments of this command are valid 60 | func validate() throws { 61 | guard !path.isEmpty else { 62 | throw InspectorError.emptyArgument(argumentName: "--path") 63 | } 64 | guard FileManager.default.fileExists(atPath: path) else { 65 | throw InspectorError.invalidArgument(argumentName: "--path", value: path) 66 | } 67 | } 68 | 69 | private func outputString(from statement: ImportStatement) -> String { 70 | switch mode { 71 | case .main: 72 | return statement.mainModule 73 | case .full: 74 | let attribute = statement.attribute.isEmpty ? "" : "@\(statement.attribute)" 75 | var module = statement.mainModule 76 | if !statement.submodule.isEmpty { 77 | module += ".\(statement.submodule)" 78 | } 79 | return "\(attribute) \(statement.kind) \(module)" 80 | } 81 | } 82 | } 83 | 84 | enum OutputMode: String, ExpressibleByArgument, Decodable { 85 | /// Outputs the main module name only 86 | case main 87 | 88 | /// Outputs the full import statement 89 | case full 90 | } 91 | 92 | extension OutputMode { 93 | static var help: ArgumentHelp { 94 | ArgumentHelp("The granularity of what's outputted", 95 | discussion: """ 96 | If main is passed, it only outputs the main module on the import, 97 | ignoring the attribute, kind and submodule. 98 | If full is passed, it outputs every property on the import. 99 | """ 100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/InitializerCommand.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/27/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import ArgumentParser 26 | import Foundation 27 | import SwiftInspectorAnalyzers 28 | 29 | final class InitializerCommand: ParsableCommand { 30 | static var configuration = CommandConfiguration( 31 | commandName: "initializer", 32 | abstract: "Finds information about the initializers of the specified type" 33 | ) 34 | 35 | @Option(help: "The absolute path to the file to inspect") 36 | var path: String 37 | 38 | @Option(help: "The name of the type whose initializer information we'll be looking for") 39 | var name: String 40 | 41 | @Flag(name: .shortAndLong, inversion: .prefixedEnableDisable, help: typeOnlyHelp) 42 | var typeOnly: Bool = true 43 | 44 | @Option(parsing: .upToNextOption, help: argumentNameHelp) 45 | var parameterName: [String] = [] 46 | 47 | /// Runs the command 48 | func run() throws { 49 | let cachedSyntaxTree = CachedSyntaxTree() 50 | let analyzer = InitializerAnalyzer(name: name, cachedSyntaxTree: cachedSyntaxTree) 51 | let fileURL = URL(fileURLWithPath: path) 52 | 53 | let initializerStatements = try analyzer.analyze(fileURL: fileURL) 54 | let outputArray = initializerStatements.filter(shouldReturnParameters).map { outputString(from: $0) } 55 | 56 | let output = outputArray.joined(separator: "\n") 57 | print(output) 58 | } 59 | 60 | /// Validates if the arguments of this command are valid 61 | func validate() throws { 62 | guard !name.isEmpty else { 63 | throw InspectorError.emptyArgument(argumentName: "--name") 64 | } 65 | guard !path.isEmpty else { 66 | throw InspectorError.emptyArgument(argumentName: "--path") 67 | } 68 | 69 | let pathURL = URL(fileURLWithPath: path) 70 | guard FileManager.default.isSwiftFile(at: pathURL) else { 71 | throw InspectorError.invalidArgument(argumentName: "--path", value: path) 72 | } 73 | } 74 | 75 | private func outputString(from statement: InitializerStatement) -> String { 76 | if typeOnly { 77 | return statement.parameters.map { $0.typeNames.joined(separator: " ") }.joined(separator: " ") 78 | } else { 79 | return statement.parameters.map { "\($0.name),\($0.typeNames.joined(separator: ","))" }.joined(separator: " ") 80 | } 81 | } 82 | 83 | /// Filters the provided `InitializerStatement` given the user-provided `argumentName` 84 | /// 85 | /// - Returns: `true` if the provided initializer statement should be used as output, `false` otherwise 86 | private func shouldReturnParameters(from statement: InitializerStatement) -> Bool { 87 | guard !parameterName.isEmpty else { 88 | return true 89 | } 90 | 91 | let parameterNames = statement.parameters.map { $0.name } 92 | 93 | return parameterNames.sorted().elementsEqual(parameterName.sorted()) 94 | } 95 | } 96 | 97 | private var typeOnlyHelp = ArgumentHelp("The granularity of the output", 98 | discussion: """ 99 | Outputs a list of the type names by default. If disabled it outputs the name of the parameter and the name of the type (e.g. 'foo,Int bar,String') 100 | """) 101 | 102 | private var argumentNameHelp = ArgumentHelp("A list of parameter names to filter initializers for", 103 | discussion: """ 104 | When this value is provided, the command will only return the initializers that contain the list of parameters provided and will filter out everything else 105 | """) 106 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/InspectorCommand.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/11/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import ArgumentParser 26 | 27 | public struct InspectorCommand: ParsableCommand { 28 | public init() {} 29 | 30 | public static var configuration = CommandConfiguration( 31 | commandName: "swiftinspector", 32 | abstract: "A command line tool to help inspect usage of classes, protocols, properties, etc in a Swift codebase.", 33 | subcommands: [ 34 | ImportsCommand.self, 35 | InitializerCommand.self, 36 | StaticUsageCommand.self, 37 | TypealiasCommand.self, 38 | TypesCommand.self, 39 | TypeConformanceCommand.self, 40 | TypeLocationCommand.self, 41 | ]) 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/InspectorError.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/11/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import ArgumentParser 26 | 27 | enum InspectorError { 28 | static func emptyArgument(argumentName: String) -> ValidationError { 29 | ValidationError("Please provide a \(argumentName) value") 30 | } 31 | 32 | static func invalidArgument(argumentName: String, value: String) -> ValidationError { 33 | ValidationError("The provided \(argumentName) value \(value) is invalid") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/StaticUsageCommand.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/15/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import ArgumentParser 26 | import Foundation 27 | import SwiftInspectorAnalyzers 28 | 29 | final class StaticUsageCommand: ParsableCommand { 30 | 31 | static var configuration = CommandConfiguration( 32 | commandName: "static-usage", 33 | abstract: "Finds information related to the usage of a static member of a type" 34 | ) 35 | 36 | @Option(parsing: .upToNextOption, transform: StaticMember.make) 37 | var statics: [StaticMember] 38 | 39 | @Option(help: "The absolute path to the file to inspect") 40 | var path: String 41 | 42 | /// Runs the command 43 | func run() throws { 44 | let cachedSyntaxTree = CachedSyntaxTree() 45 | 46 | for staticMember in statics { 47 | let analyzer = StaticUsageAnalyzer(staticMember: staticMember, cachedSyntaxTree: cachedSyntaxTree) 48 | let fileURL = URL(fileURLWithPath: path) 49 | let result: StaticUsage = try analyzer.analyze(fileURL: fileURL) 50 | output(from: result) 51 | } 52 | } 53 | 54 | /// Validates if the arguments of this command are valid 55 | func validate() throws { 56 | guard !path.isEmpty else { 57 | throw InspectorError.emptyArgument(argumentName: "--path") 58 | } 59 | 60 | let pathURL = URL(fileURLWithPath: path) 61 | guard FileManager.default.isSwiftFile(at: pathURL) else { 62 | throw InspectorError.invalidArgument(argumentName: "--path", value: path) 63 | } 64 | } 65 | 66 | /// Outputs to standard output 67 | private func output(from staticUsage: StaticUsage) { 68 | print("\(path) \(staticUsage.staticMember.typeName).\(staticUsage.staticMember.memberName) \(staticUsage.isUsed)") 69 | } 70 | } 71 | 72 | private extension StaticMember { 73 | static func make(argument: String) throws -> StaticMember { 74 | let splitted = argument.split(separator: ".").map { String($0) } 75 | // We need a type and a member from the arguments. Let's fail if this doesn't happen 76 | guard 77 | let type = splitted.first, 78 | let member = splitted.last, 79 | splitted.count == 2 80 | else 81 | { 82 | throw InspectorError.invalidArgument(argumentName: "--statics", value: argument) 83 | } 84 | 85 | return StaticMember(typeName: type, memberName: member) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/Tests/ImportCommandSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/11/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Nimble 26 | import Quick 27 | import Foundation 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorAnalyzers 31 | 32 | final class ImportCommandSpec: QuickSpec { 33 | override func spec() { 34 | describe("run") { 35 | 36 | context("with no arguments") { 37 | it("fails") { 38 | let result = try? TestTask.run(withArguments: ["imports"]) 39 | expect(result?.didFail) == true 40 | } 41 | } 42 | 43 | context("with an empty --path argument") { 44 | it("fails") { 45 | let result = try? TestStaticUsageTask.run(path: "") 46 | expect(result?.didFail) == true 47 | } 48 | } 49 | 50 | context("when path doesn't exist") { 51 | it("fails") { 52 | let result = try? TestStaticUsageTask.run(path: "/abc") 53 | expect(result?.didFail) == true 54 | } 55 | } 56 | 57 | context("when path exists") { 58 | var fileURL: URL! 59 | 60 | beforeEach { 61 | fileURL = try? Temporary.makeFile(content: "@testable import struct Foundation.Some") 62 | } 63 | 64 | afterEach { 65 | try? Temporary.removeItem(at: fileURL) 66 | } 67 | 68 | it("succeeds") { 69 | let result = try? TestStaticUsageTask.run(path: fileURL.path) 70 | expect(result?.didSucceed) == true 71 | } 72 | 73 | it("outputs only the main module by default") { 74 | let result = try? TestStaticUsageTask.run(path: fileURL.path) 75 | expect(result?.outputMessage) == "Foundation\n" 76 | } 77 | 78 | it("outputs the full import if full is passed") { 79 | let result = try? TestStaticUsageTask.run(path: fileURL.path, arguments: ["--mode", "full"]) 80 | expect(result?.outputMessage).to(contain("@testable struct Foundation.Some")) 81 | } 82 | } 83 | 84 | context("when path is a folder") { 85 | var folderURL: URL! 86 | beforeEach { 87 | folderURL = try! Temporary.makeFolder() 88 | } 89 | afterEach { 90 | try! Temporary.removeItem(at: folderURL) 91 | } 92 | 93 | it("succeeds") { 94 | let result = try? TestStaticUsageTask.run(path: folderURL.path) 95 | 96 | expect(result?.didSucceed) == true 97 | } 98 | 99 | it("outputs the correct modules") { 100 | let _ = try? Temporary.makeFile( 101 | content: """ 102 | import Foundation 103 | import UIKit 104 | import MyService 105 | """, 106 | atPath: folderURL.path) 107 | 108 | let result = try? TestStaticUsageTask.run(path: folderURL.path) 109 | let outputMessageLines = result?.outputMessage?.split { $0.isNewline } 110 | expect(outputMessageLines).to(contain(["Foundation", "UIKit", "MyService"])) 111 | } 112 | } 113 | 114 | } 115 | 116 | } 117 | } 118 | 119 | private struct TestStaticUsageTask { 120 | fileprivate static func run(path: String, arguments: [String] = []) throws -> TaskStatus { 121 | let arguments = ["imports", "--path", path] + arguments 122 | return try TestTask.run(withArguments: arguments) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/Tests/InitializerCommandSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/27/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Nimble 26 | import Quick 27 | import Foundation 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorAnalyzers 31 | 32 | final class InitializerCommandSpec: QuickSpec { 33 | override func spec() { 34 | describe("run") { 35 | 36 | context("with no arguments") { 37 | it("fails") { 38 | let result = try? TestTask.run(withArguments: ["initializer"]) 39 | expect(result?.didFail) == true 40 | } 41 | } 42 | 43 | context("when path is invalid") { 44 | it("fails when empty") { 45 | let result = try? TestTask.run(withArguments: ["initializer", "--path", "", "--name", "FakeName"]) 46 | expect(result?.didFail) == true 47 | } 48 | 49 | it("fails when it doesn't exist") { 50 | let result = try? TestTask.run(withArguments: ["initializer", "--path", "/fake/path", "--name", "FakeName"]) 51 | expect(result?.didFail) == true 52 | } 53 | } 54 | 55 | context("when name is passed and path exists") { 56 | var fileURL: URL! 57 | 58 | beforeEach { 59 | fileURL = try? Temporary.makeFile(content: """ 60 | final class Some { 61 | init(some: String, someInt: Int) {} 62 | } 63 | """) 64 | } 65 | 66 | afterEach { 67 | try? Temporary.removeItem(at: fileURL) 68 | } 69 | 70 | it("fails when name is empty") { 71 | let result = try? TestTask.run(withArguments: ["initializer", "--path", fileURL.path, "--name", ""]) 72 | expect(result?.didFail) == true 73 | } 74 | 75 | it("succeeds") { 76 | let result = try? TestTask.run(withArguments: ["initializer", "--path", fileURL.path, "--name", "Some"]) 77 | expect(result?.didSucceed) == true 78 | } 79 | 80 | it("returns only the type names by default") { 81 | let result = try? TestTask.run(withArguments: ["initializer", "--path", fileURL.path, "--name", "Some"]) 82 | expect(result?.outputMessage).to(contain("String Int")) 83 | } 84 | 85 | it("returns the name and type name if we disable type only") { 86 | let result = try? TestTask.run(withArguments: ["initializer", "--path", fileURL.path, "--name", "Some", "--disable-type-only"]) 87 | expect(result?.outputMessage).to(contain("some,String someInt,Int")) 88 | } 89 | } 90 | 91 | context("when parameter-name is passed") { 92 | var fileURL: URL! 93 | 94 | beforeEach { 95 | fileURL = try? Temporary.makeFile(content: """ 96 | final class Some { 97 | init(some: String, someInt: Int) {} 98 | } 99 | """) 100 | } 101 | 102 | it("filters out the initializers that don't have the parameter names") { 103 | let result = try? TestTask.run(withArguments: ["initializer", "--path", fileURL.path, "--name", "Some", "--parameter-name", "AnotherType"]) 104 | expect(result?.outputMessage).toNot(contain("String")) 105 | } 106 | 107 | it("returns the initializers that have the same names") { 108 | let result = try? TestTask.run(withArguments: ["initializer", "--path", fileURL.path, "--name", "Some", "--parameter-name", "some", "someInt"]) 109 | expect(result?.outputMessage).to(contain("String Int")) 110 | } 111 | 112 | it("returns the initializers that have the same names in different order") { 113 | let result = try? TestTask.run(withArguments: ["initializer", "--path", fileURL.path, "--name", "Some", "--parameter-name", "someInt", "some"]) 114 | expect(result?.outputMessage).to(contain("String Int")) 115 | } 116 | } 117 | 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/Tests/StaticUsageCommandSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/11/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import Nimble 27 | import Quick 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorAnalyzers 31 | 32 | final class StaticUsageCommandSpec: QuickSpec { 33 | 34 | override func spec() { 35 | describe("run") { 36 | var fileURL: URL! 37 | 38 | beforeEach { 39 | fileURL = try? Temporary.makeFile(content: "") 40 | } 41 | 42 | afterEach { 43 | try? Temporary.removeItem(at: fileURL) 44 | } 45 | 46 | context("when missing arguments") { 47 | 48 | context("with no arguments") { 49 | it("fails") { 50 | let result = try? TestTask.run(withArguments: ["static-usage"]) 51 | expect(result?.didFail) == true 52 | } 53 | } 54 | 55 | context("with no --statics argument") { 56 | it("fails") { 57 | let result = try? TestTask.run(withArguments: ["static-usage", "--path", fileURL.path]) 58 | expect(result?.didFail) == true 59 | } 60 | } 61 | 62 | context("with no --path argument") { 63 | it("fails") { 64 | let result = try? TestTask.run(withArguments: ["static-usage", "--statics", "SomeType.shared"]) 65 | expect(result?.didFail) == true 66 | } 67 | } 68 | 69 | } 70 | 71 | context("with an empty --path argument") { 72 | it("fails") { 73 | let result = try? TestStaticUsageTask.run(statics: "SomeType.shared", path: "") 74 | expect(result?.didFail) == true 75 | } 76 | } 77 | 78 | context("with an empty --statics argument") { 79 | it("fails") { 80 | let result = try? TestStaticUsageTask.run(statics: "", path: fileURL.path) 81 | expect(result?.didFail) == true 82 | } 83 | } 84 | 85 | context("with a type only --statics argument") { 86 | it("fails") { 87 | let result = try? TestStaticUsageTask.run(statics: "SomeType", path: fileURL.path) 88 | expect(result?.didFail) == true 89 | } 90 | } 91 | 92 | context("with a member only --statics argument") { 93 | it("fails") { 94 | let result = try? TestStaticUsageTask.run(statics: ".shared", path: fileURL.path) 95 | expect(result?.didFail) == true 96 | } 97 | } 98 | 99 | context("with multiple separated --statics argument") { 100 | it("succeeds passing multiple --statics") { 101 | let result = try? TestTask.run(withArguments: ["static-usage", "--statics", "A.some", "--statics", "B.some", "--path", fileURL.path]) 102 | expect(result?.didSucceed) == true 103 | } 104 | 105 | it("succeeds passing one --statics") { 106 | let result = try? TestTask.run(withArguments: ["static-usage", "--statics", "A.some", "B.some", "--path", fileURL.path]) 107 | expect(result?.didSucceed) == true 108 | } 109 | } 110 | 111 | context("when path doesn't exist") { 112 | it("fails") { 113 | let result = try? TestStaticUsageTask.run(statics: "SomeType.shared", path: "/abc") 114 | expect(result?.didFail) == true 115 | } 116 | } 117 | 118 | context("when path exists") { 119 | it("succeeds") { 120 | let result = try? TestStaticUsageTask.run(statics: "SomeType.shared", path: fileURL.path) 121 | expect(result?.didSucceed) == true 122 | } 123 | 124 | it("outputs to standard output") { 125 | fileURL = try? Temporary.makeFile(content: "SomeType.shared") 126 | let result = try? TestStaticUsageTask.run(statics: "SomeType.shared", path: fileURL.path) 127 | expect(result?.outputMessage).to(contain("SomeType.shared true")) 128 | } 129 | 130 | it("outputs the path to standard output") { 131 | let result = try? TestStaticUsageTask.run(statics: "SomeType.shared", path: fileURL.path) 132 | expect(result?.outputMessage).to(contain(fileURL.lastPathComponent)) 133 | } 134 | } 135 | 136 | } 137 | } 138 | } 139 | 140 | private struct TestStaticUsageTask { 141 | fileprivate static func run(statics: String, path: String) throws -> TaskStatus { 142 | try TestTask.run(withArguments: ["static-usage", "--statics", statics, "--path", path]) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/Tests/TestTask.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/13/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | 27 | struct TestTask { 28 | 29 | /// Finds the Swift Inspector executable in Derived Data and passes an array of arguments to it. 30 | /// This method should only be used for testing. 31 | /// - Parameter arguments: A set of arguments to pass to the Swift Inspector executable 32 | static func run(withArguments arguments: [String]) throws -> TaskStatus { 33 | let process = Process() 34 | process.executableURL = productsDirectory.appendingPathComponent("swiftinspector") 35 | process.arguments = arguments 36 | 37 | let standardOutputPipe = Pipe() 38 | process.standardOutput = standardOutputPipe 39 | 40 | let standardErrorPipe = Pipe() 41 | process.standardError = standardErrorPipe 42 | 43 | try process.run() 44 | process.waitUntilExit() 45 | 46 | let outputData = standardOutputPipe.fileHandleForReading.readDataToEndOfFile() 47 | let outputString = String(decoding: outputData, as: UTF8.self) 48 | 49 | let errorData = standardErrorPipe.fileHandleForReading.readDataToEndOfFile() 50 | let errorString = String(decoding: errorData, as: UTF8.self) 51 | 52 | return TaskStatus(exitStatus: process.terminationStatus, stdOutputString: outputString, stdErrorString: errorString) 53 | } 54 | 55 | // Locates the Products directory in Derived Data where the executable should be 56 | private static var productsDirectory: URL { 57 | #if os(macOS) 58 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 59 | return bundle.bundleURL.deletingLastPathComponent() 60 | } 61 | fatalError("Couldn't find the products directory") 62 | #else 63 | return Bundle.main.bundleURL 64 | #endif 65 | } 66 | } 67 | 68 | enum TaskStatus: Equatable { 69 | init(exitStatus: Int32, stdOutputString: String?, stdErrorString: String?) { 70 | if exitStatus == 0 { 71 | self = .success(message: stdOutputString) 72 | } else { 73 | self = .failure(message: stdErrorString, exitStatus: exitStatus) 74 | } 75 | } 76 | 77 | case success(message: String?) 78 | case failure(message: String?, exitStatus: Int32) 79 | 80 | var didSucceed: Bool { 81 | switch self { 82 | case .success(_): return true 83 | case .failure(_, _): return false 84 | } 85 | } 86 | 87 | var didFail: Bool { return !didSucceed } 88 | 89 | var outputMessage: String? { 90 | switch self { 91 | case .success(let message): return message 92 | case .failure(_, _): return nil 93 | } 94 | } 95 | 96 | var errorMessage: String? { 97 | switch self { 98 | case .success(_): return nil 99 | case .failure(let message, _): return message 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/Tests/TypeConformanceCommandSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/11/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import Nimble 27 | import Quick 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorAnalyzers 31 | 32 | final class TypeConformanceCommandSpec: QuickSpec { 33 | 34 | override func spec() { 35 | describe("run") { 36 | context("with no arguments") { 37 | it("fails") { 38 | let result = try? TestTask.run(withArguments: ["type-conformance"]) 39 | expect(result?.didFail) == true 40 | } 41 | } 42 | 43 | context("with no --type-names argument") { 44 | it("fails") { 45 | let result = try? TestTask.run(withArguments: ["type-conformance", "--path", "."]) 46 | expect(result?.didFail) == true 47 | } 48 | } 49 | 50 | context("with an empty --type-names argument") { 51 | it("fails") { 52 | let result = try? TestTask.run(withArguments: ["type-conformance", "--type-names", "", "--path", "/abc"]) 53 | expect(result?.didFail) == true 54 | } 55 | } 56 | 57 | context("with no --path argument") { 58 | it("fails") { 59 | let result = try? TestTask.run(withArguments: ["type-conformance", "--type-names", "SomeType"]) 60 | expect(result?.didFail) == true 61 | } 62 | } 63 | 64 | context("with an empty --path argument") { 65 | it("fails") { 66 | let result = try? TestTask.run(withArguments: ["type-conformance", "--type-names", "SomeType", "--path", ""]) 67 | expect(result?.didFail) == true 68 | } 69 | } 70 | 71 | context("with all arguments") { 72 | context("when path doesn't exist") { 73 | it("fails") { 74 | let result = try? TestTask.run(withArguments: ["type-conformance", "--type-names", "SomeType", "--path", "/abc"]) 75 | expect(result?.didSucceed) == false 76 | } 77 | } 78 | 79 | context("when path exists") { 80 | var fileURL: URL! 81 | var path: String! 82 | 83 | beforeEach { 84 | fileURL = try? Temporary.makeFile(content: "final class Foo: SomeType { }") 85 | path = fileURL?.path ?? "" 86 | } 87 | 88 | afterEach { 89 | try? Temporary.removeItem(at: fileURL) 90 | } 91 | 92 | context("when type conformance contains multiple types") { 93 | it("succeeds") { 94 | let result = try? TestTask.run(withArguments: ["type-conformance", "--type-names", "SomeType", "AnotherType", "AThirdType", "--path", path]) 95 | expect(result?.didSucceed) == true 96 | } 97 | } 98 | 99 | context("when type conformance contains one type") { 100 | it("succeeds") { 101 | let result = try? TestTask.run(withArguments: ["type-conformance", "--type-names", "SomeType", "--path", path]) 102 | expect(result?.didSucceed) == true 103 | } 104 | 105 | it("outputs to standard output") { 106 | let result = try? TestTask.run(withArguments: ["type-conformance", "--type-names", "SomeType", "--path", path]) 107 | expect(result?.outputMessage).to(contain("SomeType true")) 108 | } 109 | 110 | it("outputs the path of the file") { 111 | let result = try? TestTask.run(withArguments: ["type-conformance", "--type-names", "SomeType", "--path", path]) 112 | expect(result?.outputMessage).to(contain(fileURL.lastPathComponent)) 113 | } 114 | 115 | it("outputs the conforming type name") { 116 | let result = try? TestTask.run(withArguments: ["type-conformance", "--type-names", "SomeType", "--path", path]) 117 | expect(result?.outputMessage).to(contain("Foo")) 118 | } 119 | } 120 | 121 | } 122 | 123 | } 124 | 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/Tests/TypeLocationCommandSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Michael Bachand on 3/28/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Nimble 26 | import Quick 27 | import Foundation 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorCommands 31 | @testable import SwiftInspectorAnalyzers 32 | 33 | final class TypeLocationCommandSpec: QuickSpec { 34 | 35 | override func spec() { 36 | var pathURL: URL! 37 | 38 | describe("TypeLocationCommand") { 39 | afterEach { 40 | guard let pathURL = pathURL else { 41 | return 42 | } 43 | try? Temporary.removeItem(at: pathURL) 44 | } 45 | 46 | describe("run") { 47 | 48 | context("with no arguments") { 49 | it("fails") { 50 | let result = try? TestTypeLocationTask.run(path: nil, name: nil) 51 | expect(result?.didFail) == true 52 | } 53 | } 54 | 55 | context("path is valid") { 56 | beforeEach { pathURL = try? Temporary.makeFile(content: "struct Foo { }") } 57 | 58 | context("with no --name argument") { 59 | it("fails") { 60 | let result = try? TestTypeLocationTask.run(path: pathURL.path, name: nil) 61 | expect(result?.didFail) == true 62 | } 63 | } 64 | 65 | context("with an empty --name argument") { 66 | it("fails") { 67 | let result = try? TestTypeLocationTask.run(path: pathURL.path, name: "") 68 | expect(result?.didFail) == true 69 | } 70 | } 71 | } 72 | 73 | context("name is valid") { 74 | context("with no --path argument") { 75 | it("fails") { 76 | let result = try? TestTypeLocationTask.run(path: nil, name: "Foo") 77 | expect(result?.didFail) == true 78 | } 79 | } 80 | 81 | context("with an empty --path argument") { 82 | it("fails") { 83 | let result = try? TestTypeLocationTask.run(path: "", name: "Foo") 84 | expect(result?.didFail) == true 85 | } 86 | } 87 | 88 | context("path is a directory") { 89 | beforeEach { pathURL = try? Temporary.makeFolder() } 90 | 91 | it("fails") { 92 | let result = try? TestTypeLocationTask.run(path: pathURL.path, name: "Foo") 93 | expect(result?.didFail) == true 94 | } 95 | } 96 | } 97 | 98 | context("all arguments are valid") { 99 | beforeEach { pathURL = try? Temporary.makeFile(content: "struct Foo { }") } 100 | 101 | it("succeeds") { 102 | let result = try? TestTypeLocationTask.run(path: pathURL.path, name: "Foo") 103 | expect(result?.didSucceed) == true 104 | } 105 | 106 | context("type is found") { 107 | it("outputs the correct line numbers") { 108 | let contents = 109 | """ 110 | import Foundation 111 | 112 | struct Foo { } 113 | """ 114 | 115 | pathURL = try? Temporary.makeFile(content: contents) 116 | 117 | let result = try? TestTypeLocationTask.run(path: pathURL.path, name: "Foo") 118 | expect(result?.outputMessage).to(contain("2 2")) 119 | } 120 | } 121 | 122 | context("type is not found") { 123 | it("outputs nothing") { 124 | let contents = 125 | """ 126 | import Foundation 127 | 128 | struct Foo { } 129 | """ 130 | 131 | pathURL = try? Temporary.makeFile(content: contents) 132 | 133 | let result = try? TestTypeLocationTask.run(path: pathURL.path, name: "Bar") 134 | expect(result?.outputMessage).to(beEmpty()) 135 | } 136 | } 137 | 138 | context("multiple types found") { 139 | it("outputs line numbers for each type") { 140 | let contents = 141 | """ 142 | import Foundation 143 | 144 | struct Foo { } 145 | 146 | public final class Bar { 147 | enum Foo { } 148 | } 149 | """ 150 | 151 | pathURL = try? Temporary.makeFile(content: contents) 152 | 153 | let result = try? TestTypeLocationTask.run(path: pathURL.path, name: "Foo") 154 | let lines = result?.outputMessage?.split { $0.isNewline } 155 | expect(lines?.count) == 2 156 | expect(lines?.first).to(equal("2 2")) 157 | expect(lines?.last).to(equal("5 5")) 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | private struct TestTypeLocationTask { 167 | /// - Parameters: 168 | /// - path: The path with which to execute the task. The `--path` argument is not passed to the task if `nil`. 169 | /// - name: The name with which to execute the task. The `--name` argument is not passed to task if `nil`. 170 | fileprivate static func run(path: String?, name: String?) throws -> TaskStatus { 171 | var arguments = ["type-location"] 172 | name.map { arguments += ["--name", $0] } 173 | path.map { arguments += ["--path", $0] } 174 | return try TestTask.run(withArguments: arguments) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/Tests/TypealiasCommandSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/25/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import Nimble 27 | import Quick 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorAnalyzers 31 | 32 | final class TypealiasCommandSpec: QuickSpec { 33 | override func spec() { 34 | var fileURL: URL! 35 | var path: String! 36 | 37 | beforeEach { 38 | fileURL = try? Temporary.makeFile(content: "typealias SomeAlias = SomeType") 39 | path = fileURL?.path ?? "" 40 | } 41 | 42 | afterEach { 43 | try? Temporary.removeItem(at: fileURL) 44 | } 45 | 46 | describe("run") { 47 | context("with no arguments") { 48 | it("fails") { 49 | let result = try? TestTask.run(withArguments: ["typealias"]) 50 | expect(result?.didFail) == true 51 | } 52 | } 53 | 54 | context("with no --name argument") { 55 | var result: TaskStatus? 56 | beforeEach { 57 | result = try? TestTask.run(withArguments: ["typealias", "--path", path]) 58 | } 59 | 60 | it("succeeds") { 61 | expect(result?.didSucceed) == true 62 | } 63 | 64 | it("outputs the typealias information") { 65 | expect(result?.outputMessage).to(contain("SomeAlias")) 66 | } 67 | 68 | it("does not output the typelias identifiers") { 69 | expect(result?.outputMessage).toNot(contain("SomeType")) 70 | } 71 | 72 | context("when a typealias is defined multiple times") { 73 | it("only returns one the typealias name once") { 74 | fileURL = try? Temporary.makeFile(content: """ 75 | typealias SomeAlias = SomeType 76 | typealias SomeAlias = SomethingElse 77 | """ 78 | ) 79 | result = try? TestTask.run(withArguments: ["typealias", "--path", fileURL!.path]) 80 | 81 | let lines = result?.outputMessage?.split { $0.isNewline }.count 82 | expect(lines) == 1 83 | } 84 | } 85 | } 86 | 87 | context("with an empty --name argument") { 88 | it("succeeds") { 89 | let result = try? TestTask.run(withArguments: ["typealias", "--name", "", "--path", path]) 90 | expect(result?.didSucceed) == true 91 | } 92 | } 93 | 94 | context("with an empty --path argument") { 95 | it("fails") { 96 | let result = try? TestTask.run(withArguments: ["typealias", "--path", ""]) 97 | expect(result?.didFail) == true 98 | } 99 | } 100 | 101 | context("with --name and --path") { 102 | 103 | context("when path doesn't exist") { 104 | it("fails") { 105 | let result = try? TestTask.run(withArguments: ["typealias", "--name", "SomeAlias", "--path", "/abc"]) 106 | expect(result?.didFail) == true 107 | } 108 | } 109 | 110 | context("when path exists") { 111 | it("succeeds") { 112 | let result = try? TestTask.run(withArguments: ["typealias", "--name", "SomeAlias", "--path", path]) 113 | expect(result?.didSucceed) == true 114 | } 115 | } 116 | 117 | it("outputs the typealias information") { 118 | let result = try? TestTask.run(withArguments: ["typealias", "--name", "SomeAlias", "--path", path]) 119 | expect(result?.outputMessage).to(contain("SomeAlias")) 120 | } 121 | 122 | it("does not output the typealias information if the name is different") { 123 | let result = try? TestTask.run(withArguments: ["typealias", "--name", "AnotherTypealias", "--path", path]) 124 | expect(result?.outputMessage).toNot(contain("SomeAlias")) 125 | } 126 | 127 | it("outputs the typealias identifiers") { 128 | let result = try? TestTask.run(withArguments: ["typealias", "--name", "SomeAlias", "--path", path]) 129 | expect(result?.outputMessage).to(contain("SomeAlias")) 130 | expect(result?.outputMessage).to(contain("SomeType")) 131 | } 132 | 133 | context("with a file that has two typealias with the same name and identifiers") { 134 | it("returns the typealias information once") { 135 | let fileURL = try! Temporary.makeFile( 136 | content: """ 137 | typealias Foo = Bar 138 | 139 | struct MyNamespace { 140 | 141 | typealias Foo = Bar 142 | 143 | } 144 | """ 145 | ) 146 | let result = try? TestTask.run(withArguments: ["typealias", "--name", "Foo", "--path", fileURL.path]) 147 | 148 | let lines = result?.outputMessage?.split { $0.isNewline } 149 | 150 | expect(lines?.count) == 1 151 | } 152 | } 153 | 154 | context("with a file that has two typealias with the same name and different identifiers") { 155 | it("returns the typealias information twice") { 156 | let fileURL = try! Temporary.makeFile( 157 | content: """ 158 | typealias Foo = Bar1 159 | 160 | struct MyNamespace { 161 | 162 | typealias Foo = Bar2 163 | 164 | } 165 | """ 166 | ) 167 | let result = try? TestTask.run(withArguments: ["typealias", "--name", "Foo", "--path", fileURL.path]) 168 | 169 | let lines = result?.outputMessage?.split { $0.isNewline } 170 | 171 | expect(lines?.count) == 2 172 | } 173 | } 174 | } 175 | 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/Tests/TypesCommandSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 8/13/20. 2 | // 3 | // Copyright (c) 2020 Tyler Hedrick 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Nimble 26 | import Quick 27 | import Foundation 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorAnalyzers 31 | 32 | final class TypesCommandSpec: QuickSpec { 33 | override func spec() { 34 | var fileURL: URL! 35 | 36 | afterEach { 37 | guard let fileURL = fileURL else { 38 | return 39 | } 40 | try? Temporary.removeItem(at: fileURL) 41 | } 42 | 43 | 44 | describe("run") { 45 | context("with no arguments") { 46 | it("fails") { 47 | let result = try? TestTask.run(withArguments: ["types"]) 48 | expect(result?.didFail) == true 49 | } 50 | } 51 | 52 | context("when path is invalid") { 53 | it("fails when empty") { 54 | let result = try? TestTypesCommandTask.run(path: "") 55 | expect(result?.didFail) == true 56 | } 57 | 58 | it("fails when it doesn't exist") { 59 | let result = try? TestTypesCommandTask.run(path: "/fake/path") 60 | expect(result?.didFail) == true 61 | } 62 | } 63 | 64 | context("when path is valid file") { 65 | it("succeeds") { 66 | fileURL = try? Temporary.makeFile(content: "struct Foo { }") 67 | let path = fileURL?.path ?? "" 68 | let result = try? TestTypesCommandTask.run(path: path) 69 | expect(result?.didSucceed) == true 70 | } 71 | 72 | it("runs and outputs the result without comment") { 73 | fileURL = try? Temporary.makeFile(content: """ 74 | // This is a comment 75 | struct Foo { } 76 | """) 77 | let path = fileURL?.path ?? "" 78 | let result = try? TestTypesCommandTask.run(path: path) 79 | expect(result?.outputMessage).to(contain("Foo,struct")) 80 | } 81 | } 82 | 83 | context("when path is a folder") { 84 | var folderURL: URL! 85 | beforeEach { 86 | folderURL = try! Temporary.makeFolder() 87 | } 88 | afterEach { 89 | try! Temporary.removeItem(at: folderURL) 90 | } 91 | 92 | it("succeeds") { 93 | let result = try? TestTypesCommandTask.run(path: folderURL.path) 94 | 95 | expect(result?.didSucceed) == true 96 | } 97 | 98 | it("outputs the correct types") { 99 | let _ = try? Temporary.makeFile( 100 | content: """ 101 | class Foo { } 102 | """, 103 | atPath: folderURL.path) 104 | 105 | let _ = try? Temporary.makeFile( 106 | content: """ 107 | class Bar { } 108 | """, 109 | atPath: folderURL.path) 110 | 111 | let result = try? TestTypesCommandTask.run(path: folderURL.path) 112 | let outputMessageLines = result?.outputMessage?.split { $0.isNewline } 113 | expect(outputMessageLines).to(contain(["Foo,class", "Bar,class"])) 114 | } 115 | } 116 | 117 | } 118 | } 119 | } 120 | 121 | private struct TestTypesCommandTask { 122 | fileprivate static func run(path: String, arguments: [String] = []) throws -> TaskStatus { 123 | let arguments = ["types", "--path", path] + arguments 124 | return try TestTask.run(withArguments: arguments) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/Tests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SwiftInspectorTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/TypeConformanceCommand.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/11/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import ArgumentParser 26 | import Foundation 27 | import SwiftInspectorAnalyzers 28 | 29 | final class TypeConformanceCommand: ParsableCommand { 30 | 31 | static var configuration = CommandConfiguration( 32 | commandName: "type-conformance", 33 | abstract: "Finds information related to the conformance to a type name" 34 | ) 35 | 36 | @Option(name: .customLong("type-names"), parsing: .upToNextOption) 37 | var typeNames: [String] 38 | 39 | @Option(help: "The absolute path to the file to inspect") 40 | var path: String 41 | 42 | /// Runs the command 43 | func run() throws { 44 | let cachedSyntaxTree = CachedSyntaxTree() 45 | 46 | for typeName in typeNames { 47 | let analyzer = TypeConformanceAnalyzer(typeName: typeName, cachedSyntaxTree: cachedSyntaxTree) 48 | let fileURL = URL(fileURLWithPath: path) 49 | let result: TypeConformance = try analyzer.analyze(fileURL: fileURL) 50 | output(from: result) 51 | } 52 | } 53 | 54 | /// Validates if the arguments of this command are valid 55 | func validate() throws { 56 | guard !path.isEmpty else { 57 | throw InspectorError.emptyArgument(argumentName: "--path") 58 | } 59 | 60 | let pathURL = URL(fileURLWithPath: path) 61 | guard FileManager.default.isSwiftFile(at: pathURL) else { 62 | throw InspectorError.invalidArgument(argumentName: "--path", value: path) 63 | } 64 | } 65 | 66 | // Output to standard output 67 | private func output(from conformance: TypeConformance) { 68 | guard !conformance.conformingTypeNames.isEmpty else { 69 | print("\(path) \(conformance.typeName) \(conformance.doesConform)") 70 | return 71 | } 72 | 73 | conformance.conformingTypeNames.forEach { 74 | print("\(path) \(conformance.typeName) \(conformance.doesConform) \($0)") 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/TypeLocationCommand.swift: -------------------------------------------------------------------------------- 1 | // Created by Michael Bachand on 3/28/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import ArgumentParser 26 | import Foundation 27 | import SwiftInspectorAnalyzers 28 | 29 | final class TypeLocationCommand: ParsableCommand { 30 | static var configuration = CommandConfiguration( 31 | commandName: "type-location", 32 | abstract: "Finds the line numbers on which a type is declared" 33 | ) 34 | 35 | @Option(help: nameArgumentHelp) 36 | var name: String 37 | 38 | @Option(help: "The absolute path of the file to inspect") 39 | var path: String 40 | 41 | /// Runs the command 42 | func run() throws { 43 | let cachedSyntaxTree = CachedSyntaxTree() 44 | 45 | let analyzer = TypeLocationAnalyzer(typeName: name, cachedSyntaxTree: cachedSyntaxTree) 46 | let fileURL = URL(fileURLWithPath: path) 47 | let typeLocations = try analyzer.analyze(fileURL: fileURL) 48 | 49 | if !typeLocations.isEmpty { 50 | print(typeLocations.map { outputString(from: $0) }.joined(separator: "\n")) 51 | } 52 | } 53 | 54 | /// Validates if the arguments of this command are valid 55 | func validate() throws { 56 | guard !name.isEmpty else { 57 | throw InspectorError.emptyArgument(argumentName: "--name") 58 | } 59 | guard !path.isEmpty else { 60 | throw InspectorError.emptyArgument(argumentName: "--path") 61 | } 62 | let pathURL = URL(fileURLWithPath: path) 63 | guard FileManager.default.isSwiftFile(at: pathURL) else { 64 | throw InspectorError.invalidArgument(argumentName: "--path", value: path) 65 | } 66 | } 67 | 68 | private func outputString(from statement: LocatedType) -> String { 69 | "\(statement.indexOfStartingLine) \(statement.indexOfEndingLine)" 70 | } 71 | } 72 | 73 | private let nameArgumentHelp = ArgumentHelp( 74 | "The name of the type to find the location of", 75 | discussion: "This may be a enum, class, struct, or protocol.") 76 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/TypealiasCommand.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 3/25/20. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import ArgumentParser 26 | import Foundation 27 | import SwiftInspectorAnalyzers 28 | 29 | final class TypealiasCommand: ParsableCommand { 30 | static var configuration = CommandConfiguration( 31 | commandName: "typealias", 32 | abstract: "Finds information related to the declaration of a typelias" 33 | ) 34 | 35 | @Option(help: Help.name) 36 | var name: String = "" 37 | 38 | @Option(help: "The absolute path to the file to inspect") 39 | var path: String 40 | 41 | /// Runs the command 42 | func run() throws { 43 | let cachedSyntaxTree = CachedSyntaxTree() 44 | let analyzer = TypealiasAnalyzer(cachedSyntaxTree: cachedSyntaxTree) 45 | let fileURL = URL(fileURLWithPath: path) 46 | let outputArray = try FileManager.default.swiftFiles(at: fileURL) 47 | .reduce(Set()) { result, url in 48 | let statements = try analyzer.analyze(fileURL: url) 49 | let output = filterOutput(statements).map { outputString(from: $0) } 50 | return result.union(output) 51 | } 52 | 53 | let output = outputArray.filter { !$0.isEmpty }.joined(separator: "\n") 54 | print(output) 55 | } 56 | 57 | /// Validates if the arguments of this command are valid 58 | func validate() throws { 59 | guard !path.isEmpty else { 60 | throw InspectorError.emptyArgument(argumentName: "--path") 61 | } 62 | 63 | let pathURL = URL(fileURLWithPath: path) 64 | guard FileManager.default.isSwiftFile(at: pathURL) else { 65 | throw InspectorError.invalidArgument(argumentName: "--path", value: path) 66 | } 67 | } 68 | 69 | /// Filters the output based on command line inputs 70 | private func filterOutput(_ output: [TypealiasStatement]) -> [TypealiasStatement] { 71 | guard !name.isEmpty else { 72 | return output 73 | } 74 | 75 | return output.filter { $0.name == name } 76 | } 77 | 78 | private func outputString(from statement: TypealiasStatement) -> String { 79 | guard !name.isEmpty else { 80 | return statement.name 81 | } 82 | 83 | return "\(statement.name) \(statement.identifiers.joined(separator: " "))" 84 | } 85 | } 86 | 87 | private struct Help { 88 | static var name: ArgumentHelp { 89 | ArgumentHelp("Used to filter by the name of the typelias", 90 | discussion: """ 91 | If a value is passed, it outputs the name of the typealias and the 92 | information of the identifiers being declared in that typealias. 93 | """ 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorCommands/TypesCommand.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 8/12/20. 2 | // 3 | // Copyright (c) 2020 Tyler Hedrick 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import ArgumentParser 26 | import Foundation 27 | import SwiftInspectorAnalyzers 28 | 29 | final class TypesCommand: ParsableCommand { 30 | static var configuration = CommandConfiguration( 31 | commandName: "types", 32 | abstract: "Finds information about types in a file" 33 | ) 34 | 35 | @Option(help: "The absolute path to the file or directory to inspect") 36 | var path: String 37 | 38 | /// Runs the command 39 | func run() throws { 40 | let cachedSyntaxTree = CachedSyntaxTree() 41 | let analyzer = TypesAnalyzer(cachedSyntaxTree: cachedSyntaxTree) 42 | let fileURL = URL(fileURLWithPath: path) 43 | let outputArray = try FileManager.default.swiftFiles(at: fileURL) 44 | .reduce(Set()) { result, url in 45 | let statements = try analyzer.analyze(fileURL: url) 46 | let output = statements.map { outputString(from: $0) } 47 | return result.union(output) 48 | } 49 | 50 | let output = outputArray.filter { !$0.isEmpty }.joined(separator: "\n") 51 | print(output) 52 | } 53 | 54 | /// Validates if the arguments of this command are valid 55 | func validate() throws { 56 | guard !path.isEmpty else { 57 | throw InspectorError.emptyArgument(argumentName: "--path") 58 | } 59 | 60 | let pathURL = URL(fileURLWithPath: path) 61 | guard FileManager.default.isSwiftFile(at: pathURL) || pathURL.hasDirectoryPath else { 62 | throw InspectorError.invalidArgument(argumentName: "--path", value: path) 63 | } 64 | } 65 | 66 | private func outputString(from info: TypeInfo) -> String { 67 | "\(info.name),\(info.type)" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorTestHelpers/SyntaxVisitor+WalkContent.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/26/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | import SwiftSyntaxParser 28 | 29 | extension SyntaxVisitor { 30 | /// Walks the visitor along the content's syntax. 31 | /// 32 | /// - Parameters: 33 | /// - content: The content to turn into source and walk. 34 | public func walkContent(_ content: String) throws { 35 | let syntax: SourceFileSyntax = try SyntaxParser.parse(source: content) 36 | walk(syntax) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorTestHelpers/Temporary.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/6/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | 27 | public struct Temporary { 28 | /// Creates a Swift file in a temporary directory 29 | /// 30 | /// - Parameter content: A String representing the contents of the Swift file 31 | /// - Parameter name: The name of the Swift file 32 | /// - Parameter fileExtension: The name of the file. Defaults to "swift" 33 | /// - Parameter parentPath: The path to the parent folder where this file will be created 34 | /// - Returns: The file URL where the created file is stored 35 | public static func makeFile( 36 | content: String, 37 | name: String = UUID().uuidString, 38 | fileExtension: String = "swift", 39 | atPath parentPath: String = NSTemporaryDirectory()) throws -> URL 40 | { 41 | let temporaryDirectoryURL = URL(fileURLWithPath: parentPath, 42 | isDirectory: true) 43 | let fileName = "\(name).\(fileExtension)" 44 | let temporaryFileURL = 45 | temporaryDirectoryURL.appendingPathComponent(fileName) 46 | 47 | let data = Data(content.utf8) 48 | try data.write(to: temporaryFileURL, 49 | options: .atomic) 50 | 51 | return temporaryFileURL 52 | } 53 | 54 | /// Creates a folder inside a temporary directory 55 | /// 56 | /// - Parameter name: The name of the directory 57 | /// - Parameter parentPath: The path to the parent folder where this folder will be created 58 | /// - Returns: The file URL where the directory was created 59 | public static func makeFolder(name: String = UUID().uuidString, parentPath: String = NSTemporaryDirectory()) throws -> URL { 60 | let temporaryDirectoryURL = URL(fileURLWithPath: parentPath, isDirectory: true) 61 | let folderURL = temporaryDirectoryURL.appendingPathComponent(name, isDirectory: true) 62 | try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true) 63 | 64 | return folderURL 65 | } 66 | 67 | /// Removes an item (file or directory) in the specified location 68 | /// 69 | /// - Parameter fileURL: The location of the file to remove 70 | public static func removeItem(at fileURL: URL) throws { 71 | try FileManager.default.removeItem(at: fileURL) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorTestHelpers/Tests/TemporarySpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/9/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import Nimble 27 | import Quick 28 | 29 | @testable import SwiftInspectorTestHelpers 30 | 31 | final class TemporarySpec: QuickSpec { 32 | override func spec() { 33 | describe("makeSwiftFile(content:name:)") { 34 | it("creates a file") { 35 | guard let savedURL = try? Temporary.makeFile(content: "abc") else { 36 | return fail("Something went wrong when creating a temporary file. This shouldn't fail.") 37 | } 38 | 39 | expect(FileManager.default.fileExists(atPath: savedURL.path)) == true 40 | } 41 | 42 | it("saves the correct content") { 43 | let content = "protocol Some { }" 44 | guard let savedURL = try? Temporary.makeFile(content: content) else { 45 | return fail("Something went wrong when creating a temporary file. This shouldn't fail.") 46 | } 47 | 48 | let savedContent = try? String(contentsOf: savedURL, encoding: .utf8) 49 | expect(savedContent) == "protocol Some { }" 50 | } 51 | 52 | it("uses the correct filename") { 53 | guard let savedURL = try? Temporary.makeFile(content: "", name: "SomeName") else { 54 | return fail("Something went wrong when creating a temporary file. This shouldn't fail.") 55 | } 56 | 57 | expect(savedURL.lastPathComponent) == "SomeName.swift" 58 | } 59 | } 60 | 61 | describe("makeFolder(name:)") { 62 | it("creates a directory") { 63 | guard let folderURL = try? Temporary.makeFolder(name: "abc") else { 64 | return fail("Something went wrong when creating a temporary folder. This shouldn't fail.") 65 | } 66 | 67 | expect(FileManager.default.fileExists(atPath: folderURL.path)) == true 68 | } 69 | } 70 | 71 | describe("removeItem(at:)") { 72 | it("deletes a existing file") { 73 | guard let savedURL = try? Temporary.makeFile(content: "abc") else { 74 | return fail("Something went wrong when creating a temporary file. This shouldn't fail.") 75 | } 76 | 77 | expect(FileManager.default.fileExists(atPath: savedURL.path)) == true 78 | 79 | try? Temporary.removeItem(at: savedURL) 80 | 81 | expect(FileManager.default.fileExists(atPath: savedURL.path)) == false 82 | } 83 | 84 | it("deletes an existing folder") { 85 | guard let folderURL = try? Temporary.makeFolder(name: "abc") else { 86 | return fail("Something went wrong when creating a temporary folder. This shouldn't fail.") 87 | } 88 | 89 | expect(FileManager.default.fileExists(atPath: folderURL.path)) == true 90 | 91 | try? Temporary.removeItem(at: folderURL) 92 | 93 | expect(FileManager.default.fileExists(atPath: folderURL.path)) == false 94 | 95 | } 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/AssertionFailure.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 9/11/21. 2 | // Copyright © 2021 Dan Federman 3 | // 4 | // Distributed under the MIT License 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// Indicates that an internal confidence check failed. 27 | /// Posts the `AssertionFailure.notification` if `AssertionFailure.postNotification` is `true`, otherwise calls `Swift.assertionFailure`. 28 | /// Using this method instead of `Swift.assertionFailure` allows for testing that assertions are triggered. 29 | @inlinable public func assertionFailureOrPostNotification(_ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line) { 30 | if AssertionFailure.postNotification { 31 | NotificationCenter.default.post(AssertionFailure.notification) 32 | } else { 33 | assertionFailure(message(), file: file, line: line) 34 | } 35 | } 36 | 37 | /// A collection of constants that enable testing assertion failures. 38 | public enum AssertionFailure { 39 | /// The notification that is posted when `assertionFailureOrPostNotification` is called and `postNotification` is `true`. 40 | public static let notification = Notification(name: Notification.Name(rawValue: "SwiftInspector.AssertionFailure")) 41 | /// Set to `true` to post notifications in `assertionFailureOrPostNotification` rather than calling through to `Swift.assertionFailure`. 42 | public static var postNotification = false 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/AssociatedtypeVisitor.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/17/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import SwiftSyntax 26 | 27 | public final class AssociatedtypeVisitor: SyntaxVisitor { 28 | 29 | public private(set) var associatedTypes = [AssociatedtypeInfo]() 30 | 31 | public override func visit(_ node: AssociatedtypeDeclSyntax) -> SyntaxVisitorContinueKind { 32 | let name = node.identifier.text 33 | 34 | let typeInheritanceVisitor = TypeInheritanceVisitor() 35 | if let inheritanceClause = node.inheritanceClause { 36 | typeInheritanceVisitor.walk(inheritanceClause) 37 | } 38 | 39 | let genericRequirementVisitor = GenericRequirementVisitor() 40 | if let genericWhereClause = node.genericWhereClause { 41 | genericRequirementVisitor.walk(genericWhereClause) 42 | } 43 | 44 | associatedTypes.append( 45 | .init( 46 | name: name, 47 | inheritsFromTypes: typeInheritanceVisitor.inheritsFromTypes, 48 | initializer: node.initializer?.value.typeDescription, 49 | genericRequirements: genericRequirementVisitor.genericRequirements)) 50 | 51 | return .skipChildren 52 | } 53 | } 54 | 55 | public struct AssociatedtypeInfo: Codable, Hashable { 56 | public let name: String 57 | public let inheritsFromTypes: [TypeDescription] 58 | public let initializer: TypeDescription? 59 | public let genericRequirements: [GenericRequirement] 60 | } 61 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/DeclarationModifierVisitor.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/9/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | 28 | final class DeclarationModifierVisitor: SyntaxVisitor { 29 | 30 | public var modifiers: Modifiers { 31 | rawModifiers.reduce(Modifiers()) { partialResult, nextStringModifier in 32 | partialResult.union(Modifiers(stringValue: nextStringModifier)) 33 | } 34 | } 35 | 36 | public override func visit(_ node: DeclModifierSyntax) -> SyntaxVisitorContinueKind { 37 | if 38 | let leftParen = node.detailLeftParen, 39 | let detail = node.detail, 40 | let rightParen = node.detailRightParen 41 | { 42 | rawModifiers.append(node.name.text + leftParen.text + detail.text + rightParen.text) 43 | } else { 44 | rawModifiers.append(node.name.text) 45 | } 46 | 47 | return .skipChildren 48 | } 49 | 50 | // MARK: Private 51 | 52 | private var rawModifiers = [String]() 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/FileVisitor.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/28/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | 28 | public final class FileVisitor: SyntaxVisitor { 29 | 30 | public init(fileURL: URL) { 31 | fileInfo = FileInfo(url: fileURL) 32 | } 33 | 34 | public private(set) var fileInfo: FileInfo 35 | 36 | public override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { 37 | let importVisitor = ImportVisitor() 38 | importVisitor.walk(node) 39 | 40 | fileInfo.appendImports(importVisitor.imports) 41 | 42 | // We don't need to visit children because our visitor just did that for us. 43 | return .skipChildren 44 | } 45 | public override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { 46 | let protocolVisitor = ProtocolVisitor() 47 | protocolVisitor.walk(node) 48 | 49 | if let protocolInfo = protocolVisitor.protocolInfo { 50 | fileInfo.appendProtocol(protocolInfo) 51 | fileInfo.appendTypealiases(protocolInfo.innerTypealiases) 52 | } 53 | 54 | // We don't need to visit children because our visitor just did that for us. 55 | return .skipChildren 56 | } 57 | 58 | public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { 59 | let visitor = FunctionDeclarationVisitor() 60 | visitor.walk(node) 61 | fileInfo.appendFreeFunctions(visitor.functionDeclarations) 62 | return .skipChildren 63 | } 64 | 65 | public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { 66 | visitNestableDeclaration(node: node) 67 | } 68 | 69 | public override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { 70 | visitNestableDeclaration(node: node) 71 | } 72 | 73 | public override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { 74 | visitNestableDeclaration(node: node) 75 | } 76 | 77 | public override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { 78 | let extensionVisitor = ExtensionVisitor() 79 | extensionVisitor.walk(node) 80 | 81 | if let extensionInfo = extensionVisitor.extensionInfo { 82 | fileInfo.appendExtension(extensionInfo) 83 | fileInfo.appendEnums(extensionVisitor.innerEnums) 84 | fileInfo.appendClasses(extensionVisitor.innerClasses) 85 | fileInfo.appendStructs(extensionVisitor.innerStructs) 86 | fileInfo.appendTypealiases(extensionVisitor.innerTypealiases) 87 | } 88 | 89 | // We don't need to visit children because our visitor just did that for us. 90 | return .skipChildren 91 | } 92 | 93 | public override func visit(_ node: TypealiasDeclSyntax) -> SyntaxVisitorContinueKind { 94 | let typealiasVisitor = TypealiasVisitor() 95 | typealiasVisitor.walk(node) 96 | 97 | fileInfo.appendTypealiases(typealiasVisitor.typealiases) 98 | 99 | // We don't need to visit children because our visitor just did that for us. 100 | return .skipChildren 101 | } 102 | 103 | // MARK: Private 104 | 105 | private func visitNestableDeclaration(node: DeclSyntax) -> SyntaxVisitorContinueKind { 106 | let declarationVisitor = NestableTypeVisitor() 107 | declarationVisitor.walk(node) 108 | 109 | fileInfo.appendStructs(declarationVisitor.structs) 110 | fileInfo.appendClasses(declarationVisitor.classes) 111 | fileInfo.appendEnums(declarationVisitor.enums) 112 | fileInfo.appendTypealiases(declarationVisitor.typealiases) 113 | 114 | // We don't need to visit children because our visitor just did that for us. 115 | return .skipChildren 116 | } 117 | 118 | } 119 | 120 | public struct FileInfo: Codable, Hashable { 121 | public let url: URL 122 | public private(set) var imports = [ImportStatement]() 123 | public private(set) var protocols = [ProtocolInfo]() 124 | public private(set) var structs = [StructInfo]() 125 | public private(set) var classes = [ClassInfo]() 126 | public private(set) var enums = [EnumInfo]() 127 | public private(set) var extensions = [ExtensionInfo]() 128 | public private(set) var typealiases = [TypealiasInfo]() 129 | public private(set) var freeFunctions = [FunctionDeclarationInfo]() 130 | 131 | mutating func appendImports(_ imports: [ImportStatement]) { 132 | self.imports += imports 133 | } 134 | mutating func appendProtocol(_ protocolInfo: ProtocolInfo) { 135 | protocols.append(protocolInfo) 136 | } 137 | mutating func appendStructs(_ structs: [StructInfo]) { 138 | self.structs += structs 139 | } 140 | mutating func appendClasses(_ classes: [ClassInfo]) { 141 | self.classes += classes 142 | } 143 | mutating func appendEnums(_ enums: [EnumInfo]) { 144 | self.enums += enums 145 | } 146 | mutating func appendExtension(_ extensionInfo: ExtensionInfo) { 147 | extensions.append(extensionInfo) 148 | } 149 | mutating func appendTypealiases(_ typealiases: [TypealiasInfo]) { 150 | self.typealiases += typealiases 151 | } 152 | mutating func appendFreeFunctions(_ functionDeclarations: [FunctionDeclarationInfo]) { 153 | freeFunctions.append(contentsOf: functionDeclarations) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/FunctionDeclarationVisitor.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | public final class FunctionDeclarationVisitor: SyntaxVisitor { 4 | public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { 5 | let name = node.identifier.withoutTrivia().description 6 | 7 | let functionSignatureVisitor = FunctionSignatureVisitor() 8 | functionSignatureVisitor.walk(node) 9 | 10 | let modifiersVisitor = DeclarationModifierVisitor() 11 | if let modifiers = node.modifiers { 12 | modifiersVisitor.walk(modifiers) 13 | } 14 | 15 | let info = FunctionDeclarationInfo( 16 | modifiers: modifiersVisitor.modifiers, 17 | name: name, 18 | parameters: functionSignatureVisitor.parameters, 19 | returnType: functionSignatureVisitor.returnType) 20 | functionDeclarations.append(info) 21 | return .skipChildren 22 | } 23 | 24 | public override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { 25 | assertionFailureOrPostNotification("Encountered a class declaration. This is a usage error: a single FunctionDeclarationVisitor instance should start walking only over a function declaration node") 26 | return .skipChildren 27 | } 28 | 29 | public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { 30 | assertionFailureOrPostNotification("Encountered a struct declaration. This is a usage error: a single FunctionDeclarationVisitor instance should start walking only over a function declaration node") 31 | return .skipChildren 32 | } 33 | 34 | public override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { 35 | assertionFailureOrPostNotification("Encountered an enum declaration. This is a usage error: a single FunctionDeclarationVisitor instance should start walking only over a function declaration node") 36 | return .skipChildren 37 | } 38 | 39 | public override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { 40 | assertionFailureOrPostNotification("Encountered a protocol declaration. This is a usage error: a single FunctionDeclarationVisitor instance should start walking only over a function declaration node") 41 | return .skipChildren 42 | } 43 | 44 | public override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { 45 | assertionFailureOrPostNotification("Encountered an extension declaration. This is a usage error: a single FunctionDeclarationVisitor instance should start walking only over a function declaration node") 46 | return .skipChildren 47 | } 48 | 49 | public var functionDeclarations: [FunctionDeclarationInfo] = [] 50 | } 51 | 52 | fileprivate final class FunctionSignatureVisitor: SyntaxVisitor { 53 | override func visit(_ node: FunctionParameterSyntax) -> SyntaxVisitorContinueKind { 54 | guard 55 | let firstMember = node.firstName?.text, 56 | let type = node.type?.typeDescription 57 | else { return .skipChildren } 58 | 59 | let secondMember = node.secondName?.text 60 | 61 | appendParameter(firstMember: firstMember, secondMember: secondMember, type: type) 62 | return .skipChildren 63 | } 64 | override func visit(_ node: ReturnClauseSyntax) -> SyntaxVisitorContinueKind { 65 | returnType = node.returnType.typeDescription 66 | return .skipChildren 67 | } 68 | 69 | fileprivate func appendParameter(firstMember: String, secondMember: String?, type: TypeDescription) { 70 | var parameters = self.parameters ?? [] 71 | // Each function parameter has both an argument label and a parameter name. 72 | // The argument label is used when calling the function; each argument is written in the function call with its argument label before it. 73 | // The parameter name is used in the implementation of the function. By default, parameters use their parameter name as their argument label. 74 | let argumentLabel = firstMember 75 | let parameterName = secondMember ?? firstMember 76 | parameters.append(.init(argumentLabelName: argumentLabel, parameterName: parameterName, type: type)) 77 | self.parameters = parameters 78 | } 79 | 80 | fileprivate var parameters: [FunctionDeclarationInfo.ParameterInfo]? 81 | fileprivate var returnType: TypeDescription? 82 | } 83 | 84 | public struct FunctionDeclarationInfo: Codable, Hashable { 85 | public let modifiers: Modifiers 86 | public let name: String 87 | public let parameters: [ParameterInfo]? 88 | public let returnType: TypeDescription? 89 | 90 | /// A convenience for creating a selector string that can be reference in Objective-C code. 91 | public var selectorName: String { 92 | "\(name)(\((parameters ?? []).map { "\($0.argumentLabelName):" }.joined()))" 93 | } 94 | 95 | public struct ParameterInfo: Codable, Hashable { 96 | public let argumentLabelName: String 97 | public let parameterName: String 98 | public let type: TypeDescription 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/GenericParameterVisitor.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/17/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import SwiftSyntax 26 | 27 | public final class GenericParameterVisitor: SyntaxVisitor { 28 | 29 | public private(set) var genericParameters = [GenericParameter]() 30 | 31 | public override func visit(_ node: GenericParameterSyntax) -> SyntaxVisitorContinueKind { 32 | genericParameters.append( 33 | .init( 34 | name: node.name.text, 35 | inheritsFrom: node.inheritedType?.typeDescription)) 36 | return .skipChildren 37 | } 38 | } 39 | 40 | public struct GenericParameter: Codable, Hashable { 41 | public let name: String 42 | public let inheritsFrom: TypeDescription? 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/GenericRequirementVisitor.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/26/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | 28 | public final class GenericRequirementVisitor: SyntaxVisitor { 29 | 30 | public private(set) var genericRequirements = [GenericRequirement]() 31 | 32 | public override func visit(_ node: SameTypeRequirementSyntax) -> SyntaxVisitorContinueKind { 33 | genericRequirements.append(GenericRequirement(node: node)) 34 | // Children don't have any more information about generic requirements, so don't visit them. 35 | return .skipChildren 36 | } 37 | 38 | public override func visit(_ node: ConformanceRequirementSyntax) -> SyntaxVisitorContinueKind { 39 | genericRequirements.append(GenericRequirement(node: node)) 40 | // Children don't have any more information about generic requirements, so don't visit them. 41 | return .skipChildren 42 | } 43 | 44 | public override func visit(_ node: MemberDeclBlockSyntax) -> SyntaxVisitorContinueKind { 45 | // A member declaration block means we've found the body of the type. 46 | // There's nothing in this body that would help us determine generic requirements. 47 | .skipChildren 48 | } 49 | } 50 | 51 | public struct GenericRequirement: Codable, Hashable { 52 | 53 | // MARK: Lifecycle 54 | 55 | init(node: SameTypeRequirementSyntax) { 56 | leftType = node.leftTypeIdentifier.typeDescription 57 | rightType = node.rightTypeIdentifier.typeDescription 58 | relationship = .equals 59 | } 60 | 61 | init(node: ConformanceRequirementSyntax) { 62 | leftType = node.leftTypeIdentifier.typeDescription 63 | rightType = node.rightTypeIdentifier.typeDescription 64 | relationship = .conformsTo 65 | } 66 | 67 | // MARK: Public 68 | 69 | public let leftType: TypeDescription 70 | public let rightType: TypeDescription 71 | public let relationship: Relationship 72 | 73 | // MARK: - Relationship 74 | 75 | public enum Relationship: Int, Codable { 76 | case equals = 1 77 | case conformsTo 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/ImportVisitor.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/14/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | 28 | public final class ImportVisitor: SyntaxVisitor { 29 | public private(set) var imports: [ImportStatement] = [] 30 | 31 | public override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { 32 | let statement = importStatement(from: node) 33 | imports.append(statement) 34 | return .visitChildren 35 | } 36 | 37 | public override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { 38 | // We don't need to visit children because this code can't have imports. 39 | .skipChildren 40 | } 41 | 42 | public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { 43 | // We don't need to visit children because this code can't have imports. 44 | .skipChildren 45 | } 46 | 47 | public override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { 48 | // We don't need to visit children because this code can't have imports. 49 | .skipChildren 50 | } 51 | 52 | public override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { 53 | // We don't need to visit children because this code can't have imports. 54 | .skipChildren 55 | } 56 | 57 | private func importStatement(from syntaxNode: ImportDeclSyntax) -> ImportStatement { 58 | let attribute = findAttribute(from: syntaxNode) 59 | let (main, submodule) = findModule(from: syntaxNode) 60 | let kind = findImportKind(from: syntaxNode) 61 | 62 | return ImportStatement(attribute: attribute, kind: kind, mainModule: main, submodule: submodule) 63 | } 64 | 65 | /// Finds the type of an import 66 | /// 67 | /// - Parameter syntaxNode: The Swift Syntax import declaration node 68 | /// 69 | /// - Returns: A string representing the kind of the import 70 | /// e.g `import class UIKit.UIViewController` returns class 71 | /// while `import UIKit` and `import UIKit.UIViewController` return an empty String 72 | private func findImportKind(from syntaxNode: ImportDeclSyntax) -> String { 73 | for token in syntaxNode.tokens { 74 | switch token.tokenKind { 75 | // List is from https://thoughtbot.com/blog/swift-imports 76 | case .typealiasKeyword, 77 | .structKeyword, 78 | .classKeyword, 79 | .enumKeyword, 80 | .protocolKeyword, 81 | .letKeyword, 82 | .varKeyword, 83 | .funcKeyword: 84 | return token.text 85 | default: 86 | break 87 | } 88 | } 89 | 90 | return "" 91 | } 92 | 93 | /// Finds the module and submodule of an import 94 | /// 95 | /// - Parameter syntaxNode: The Swift Syntax import declaration node 96 | /// 97 | /// - Returns: A String tuple representing the main module and submodule of the import. 98 | /// e.g `import class UIKit.UIViewController` returns ("UIKit", "UIViewController") 99 | private func findModule(from syntaxNode: ImportDeclSyntax) -> (main: String, submodule: String) { 100 | var moduleIdentifier: String = "" 101 | var submoduleIdentifier: String = "" 102 | 103 | for child in syntaxNode.children { 104 | guard let accessPath = child.as(AccessPathSyntax.self) else { 105 | continue 106 | } 107 | 108 | for accessPathComponent in accessPath { 109 | // The main module name always comes before the submodule name, so we fullfill that first 110 | if moduleIdentifier.isEmpty { 111 | moduleIdentifier = accessPathComponent.name.text 112 | continue 113 | } else { 114 | submoduleIdentifier = accessPathComponent.name.text 115 | break 116 | } 117 | } 118 | } 119 | 120 | return (moduleIdentifier, submoduleIdentifier) 121 | } 122 | 123 | private func findAttribute(from syntaxNode: ImportDeclSyntax) -> String { 124 | for child in syntaxNode.children { 125 | guard let attributeList = child.as(AttributeListSyntax.self) else { 126 | continue 127 | } 128 | 129 | // This AttributeList is of the form ["@", "attribute"] 130 | // So we grab the last token 131 | return attributeList.lastToken?.text ?? "" 132 | } 133 | return "" 134 | } 135 | } 136 | 137 | public struct ImportStatement: Codable, Hashable { 138 | public var attribute: String = "" 139 | public var kind: String = "" 140 | public let mainModule: String 141 | public var submodule: String = "" 142 | } 143 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/Modifiers.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 4/29/22. 2 | // 3 | // Distributed under the MIT License 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // MARK: - Modifiers 24 | 25 | public struct Modifiers: Codable, Hashable, OptionSet { 26 | public let rawValue: Int 27 | 28 | // general accessors 29 | public static let `open` = Self(rawValue: 1 << 0) 30 | public static let `internal` = Self(rawValue: 1 << 1) 31 | public static let `public` = Self(rawValue: 1 << 2) 32 | public static let `private` = Self(rawValue: 1 << 3) 33 | public static let `fileprivate` = Self(rawValue: 1 << 4) 34 | // set accessors 35 | public static let privateSet = Self(rawValue: 1 << 5) 36 | public static let internalSet = Self(rawValue: 1 << 6) 37 | public static let publicSet = Self(rawValue: 1 << 7) 38 | // access control 39 | public static let `instance` = Self(rawValue: 1 << 8) 40 | public static let `static` = Self(rawValue: 1 << 9) 41 | // function modifiers 42 | public static let designated = Self(rawValue: 1 << 10) 43 | public static let convenience = Self(rawValue: 1 << 11) 44 | public static let override = Self(rawValue: 1 << 12) 45 | public static let required = Self(rawValue: 1 << 13) 46 | 47 | public init(rawValue: Int) { 48 | self.rawValue = rawValue 49 | } 50 | 51 | public init(stringValue: String) { 52 | switch stringValue { 53 | case "open": self = .open 54 | case "public": self = .public 55 | case "private": self = .private 56 | case "fileprivate": self = .fileprivate 57 | case "private(set)": self = .privateSet 58 | case "internal(set)": self = .internalSet 59 | case "public(set)": self = .publicSet 60 | case "internal": self = .internal 61 | case "static": self = .static 62 | case "convenience": self = .convenience 63 | case "override": self = .override 64 | case "required": self = .required 65 | default: self = [] 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/NestableDeclSyntax.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/15/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import SwiftSyntax 26 | 27 | protocol NestableDeclSyntax: SyntaxProtocol { 28 | var modifiers: ModifierListSyntax? { get } 29 | var identifier: TokenSyntax { get } 30 | var inheritanceClause: TypeInheritanceClauseSyntax? { get } 31 | var genericParameterClause: GenericParameterClauseSyntax? { get } 32 | var genericWhereClause: GenericWhereClauseSyntax? { get } 33 | var members: MemberDeclBlockSyntax { get } 34 | } 35 | 36 | extension ClassDeclSyntax: NestableDeclSyntax {} 37 | extension StructDeclSyntax: NestableDeclSyntax {} 38 | extension EnumDeclSyntax: NestableDeclSyntax { 39 | var genericParameterClause: GenericParameterClauseSyntax? { 40 | // Not sure why enums's `GenericParameterClauseSyntax` has a different 41 | // accessor name, but it does. So let's remap. 42 | genericParameters 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/NestableTypeInfo.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/15/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | public struct NestableTypeInfo: Codable, Hashable { 26 | public let name: String 27 | public let inheritsFromTypes: [TypeDescription] 28 | public let parentType: TypeDescription? 29 | public let modifiers: Modifiers 30 | public let genericParameters: [GenericParameter] 31 | public let genericRequirements: [GenericRequirement] 32 | public internal(set) var properties: [PropertyInfo] 33 | public internal(set) var functionDeclarations: [FunctionDeclarationInfo] 34 | } 35 | 36 | public typealias ClassInfo = NestableTypeInfo 37 | public typealias EnumInfo = NestableTypeInfo 38 | public typealias StructInfo = NestableTypeInfo 39 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/ParsingTracker.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/2/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | 27 | struct ParsingTracker { 28 | var hasFinishedParsing: Bool { 29 | hasStartedParsing && !isParsing 30 | } 31 | 32 | var isParsing: Bool { 33 | parsingCount != 0 34 | } 35 | 36 | mutating func increment() { 37 | parsingCount += 1 38 | } 39 | 40 | mutating func decrement() { 41 | parsingCount -= 1 42 | } 43 | 44 | private var hasStartedParsing = false 45 | private var parsingCount = 0 { 46 | didSet { 47 | hasStartedParsing = true 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/ProtocolVisitor.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/28/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import SwiftSyntax 26 | 27 | public final class ProtocolVisitor: SyntaxVisitor { 28 | 29 | public var protocolInfo: ProtocolInfo? { 30 | guard let name = name else { return nil } 31 | return ProtocolInfo( 32 | name: name, 33 | associatedTypes: associatedtypes, 34 | inheritsFromTypes: inheritsFromTypes ?? [], 35 | genericRequirements: genericRequirements ?? [], 36 | modifiers: modifiers ?? [], 37 | innerTypealiases: typealiases, 38 | properties: properties, 39 | functionDeclarations: functionDeclarations) 40 | } 41 | 42 | public override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind { 43 | 44 | guard !hasFinishedParsingProtocol else { 45 | assertionFailureOrPostNotification("Encountered more than one top-level protocol. This is a usage error: a single ProtocolVisitor instance should start walking only over a node of type `ProtocolDeclSyntax`") 46 | return .skipChildren 47 | } 48 | let name = node.identifier.text 49 | self.name = name 50 | parentType = .simple(name: name) 51 | 52 | let typeInheritanceVisitor = TypeInheritanceVisitor() 53 | if let inheritanceClause = node.inheritanceClause { 54 | typeInheritanceVisitor.walk(inheritanceClause) 55 | inheritsFromTypes = typeInheritanceVisitor.inheritsFromTypes 56 | } 57 | let genericRequirementVisitor = GenericRequirementVisitor() 58 | if let genericWhereClause = node.genericWhereClause { 59 | genericRequirementVisitor.walk(genericWhereClause) 60 | genericRequirements = genericRequirementVisitor.genericRequirements 61 | } 62 | 63 | let declarationModifierVisitor = DeclarationModifierVisitor() 64 | if let modifiers = node.modifiers { 65 | declarationModifierVisitor.walk(modifiers) 66 | self.modifiers = declarationModifierVisitor.modifiers 67 | } 68 | 69 | return .visitChildren 70 | } 71 | 72 | public override func visitPost(_ node: ProtocolDeclSyntax) { 73 | hasFinishedParsingProtocol = true 74 | } 75 | 76 | public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { 77 | assertionFailureOrPostNotification("Encountered a top-level struct. This is a usage error: a single ProtocolVisitor instance should start walking only over a node of type `ProtocolDeclSyntax`") 78 | return .skipChildren 79 | } 80 | 81 | public override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind { 82 | assertionFailureOrPostNotification("Encountered a top-level class. This is a usage error: a single ProtocolVisitor instance should start walking only over a node of type `ProtocolDeclSyntax`") 83 | return .skipChildren 84 | } 85 | 86 | public override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { 87 | assertionFailureOrPostNotification("Encountered a top-level enum. This is a usage error: a single ProtocolVisitor instance should start walking only over a node of type `ProtocolDeclSyntax`") 88 | return .skipChildren 89 | } 90 | 91 | public override func visit(_ node: AssociatedtypeDeclSyntax) -> SyntaxVisitorContinueKind { 92 | let associatedtypeVisitor = AssociatedtypeVisitor() 93 | associatedtypeVisitor.walk(node) 94 | associatedtypes.append(contentsOf: associatedtypeVisitor.associatedTypes) 95 | 96 | // We don't need to visit children because our visitor just did that for us. 97 | return .skipChildren 98 | } 99 | 100 | public override func visit(_ node: TypealiasDeclSyntax) -> SyntaxVisitorContinueKind { 101 | let typealiasVisitor = TypealiasVisitor(parentType: parentType) 102 | typealiasVisitor.walk(node) 103 | typealiases.append(contentsOf: typealiasVisitor.typealiases) 104 | 105 | // We don't need to visit children because our visitor just did that for us. 106 | return .skipChildren 107 | } 108 | 109 | public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { 110 | let propertiesVisitor = PropertyVisitor() 111 | propertiesVisitor.walk(node) 112 | properties.append(contentsOf: propertiesVisitor.properties) 113 | 114 | // We don't need to visit children because our visitor just did that for us. 115 | return .skipChildren 116 | } 117 | 118 | public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { 119 | let visitor = FunctionDeclarationVisitor() 120 | visitor.walk(node) 121 | let functions = visitor.functionDeclarations 122 | functionDeclarations.append(contentsOf: functions) 123 | 124 | // We don't need to visit children because our visitor just did that for us. 125 | return .skipChildren 126 | } 127 | 128 | // MARK: Private 129 | 130 | private var hasFinishedParsingProtocol = false 131 | private var name: String? 132 | private var modifiers: Modifiers? 133 | private var inheritsFromTypes: [TypeDescription]? 134 | private var genericRequirements: [GenericRequirement]? 135 | private var associatedtypes: [AssociatedtypeInfo] = [] 136 | private var typealiases: [TypealiasInfo] = [] 137 | private var properties: [PropertyInfo] = [] 138 | private var functionDeclarations: [FunctionDeclarationInfo] = [] 139 | private var parentType: TypeDescription? 140 | } 141 | 142 | public struct ProtocolInfo: Codable, Hashable { 143 | public let name: String 144 | public let associatedTypes: [AssociatedtypeInfo] 145 | public let inheritsFromTypes: [TypeDescription] 146 | public let genericRequirements: [GenericRequirement] 147 | public let modifiers: Modifiers 148 | public let innerTypealiases: [TypealiasInfo] 149 | public let properties: [PropertyInfo] 150 | public let functionDeclarations: [FunctionDeclarationInfo] 151 | } 152 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/Tests/AssociatedtypeVisitorSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/17/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Nimble 26 | import Quick 27 | import SwiftInspectorTestHelpers 28 | 29 | @testable import SwiftInspectorVisitors 30 | 31 | final class AssociatedtypeVisitorSpec: QuickSpec { 32 | private var sut = AssociatedtypeVisitor() 33 | 34 | override func spec() { 35 | beforeEach { 36 | self.sut = AssociatedtypeVisitor() 37 | } 38 | 39 | describe("visit(_:)") { 40 | context("visiting a protocol with associated types") { 41 | var associatedTypeNameToInfoMap: [String: AssociatedtypeInfo]? 42 | beforeEach { 43 | let content = """ 44 | public protocol Collection { 45 | associatedtype Element 46 | 47 | associatedtype Index : Comparable where Self.Index == Self.Indices.Element 48 | 49 | associatedtype Iterator = IndexingIterator 50 | 51 | associatedtype SubSequence : Collection = Slice where Self.Element == Self.SubSequence.Element, Self.SubSequence == Self.SubSequence.SubSequence 52 | 53 | associatedtype Indices : Collection = DefaultIndices where Self.Indices == Self.Indices.SubSequence 54 | } 55 | """ 56 | 57 | try? self.sut.walkContent(content) 58 | 59 | associatedTypeNameToInfoMap = self.sut.associatedTypes 60 | .reduce(into: [String: AssociatedtypeInfo]()) { (result, associatedTypeInfo) in 61 | result[associatedTypeInfo.name] = associatedTypeInfo 62 | } 63 | } 64 | 65 | it("finds the associatedtype Element") { 66 | expect(associatedTypeNameToInfoMap?["Element"]).toNot(beNil()) 67 | } 68 | 69 | it("finds the associatedtype Index") { 70 | expect(associatedTypeNameToInfoMap?["Index"]).toNot(beNil()) 71 | } 72 | 73 | it("finds the associatedtype Index's inheritance") { 74 | expect(associatedTypeNameToInfoMap?["Index"]?.inheritsFromTypes.count) == 1 75 | expect(associatedTypeNameToInfoMap?["Index"]?.inheritsFromTypes.first?.asSource) == "Comparable" 76 | } 77 | 78 | it("finds the associatedtype Index's generic requirements") { 79 | expect(associatedTypeNameToInfoMap?["Index"]?.genericRequirements.count) == 1 80 | expect(associatedTypeNameToInfoMap?["Index"]?.genericRequirements.first?.leftType.asSource) == "Self.Index" 81 | expect(associatedTypeNameToInfoMap?["Index"]?.genericRequirements.first?.rightType.asSource) == "Self.Indices.Element" 82 | } 83 | 84 | it("finds the associatedtype Iterator") { 85 | expect(associatedTypeNameToInfoMap?["Iterator"]).toNot(beNil()) 86 | } 87 | 88 | it("finds the associatedtype Iterator's initializer") { 89 | expect(associatedTypeNameToInfoMap?["Iterator"]?.initializer?.asSource) == "IndexingIterator" 90 | } 91 | 92 | it("finds the associatedtype SubSequence") { 93 | expect(associatedTypeNameToInfoMap?["SubSequence"]).toNot(beNil()) 94 | } 95 | 96 | it("finds the associatedtype SubSequence's inheritance") { 97 | expect(associatedTypeNameToInfoMap?["SubSequence"]?.inheritsFromTypes.count) == 1 98 | expect(associatedTypeNameToInfoMap?["SubSequence"]?.inheritsFromTypes.first?.asSource) == "Collection" 99 | } 100 | 101 | it("finds the associatedtype SubSequence's generic requirements") { 102 | expect(associatedTypeNameToInfoMap?["SubSequence"]?.genericRequirements.count) == 2 103 | expect(associatedTypeNameToInfoMap?["SubSequence"]?.genericRequirements.first?.leftType.asSource) == "Self.Element" 104 | expect(associatedTypeNameToInfoMap?["SubSequence"]?.genericRequirements.first?.relationship) == .equals 105 | expect(associatedTypeNameToInfoMap?["SubSequence"]?.genericRequirements.first?.rightType.asSource) == "Self.SubSequence.Element" 106 | expect(associatedTypeNameToInfoMap?["SubSequence"]?.genericRequirements.last?.leftType.asSource) == "Self.SubSequence" 107 | expect(associatedTypeNameToInfoMap?["SubSequence"]?.genericRequirements.last?.relationship) == .equals 108 | expect(associatedTypeNameToInfoMap?["SubSequence"]?.genericRequirements.last?.rightType.asSource) == "Self.SubSequence.SubSequence" 109 | } 110 | 111 | it("finds the associatedtype Indices") { 112 | expect(associatedTypeNameToInfoMap?["Indices"]).toNot(beNil()) 113 | } 114 | 115 | it("finds the associatedtype Indices's inheritance") { 116 | expect(associatedTypeNameToInfoMap?["Indices"]?.inheritsFromTypes.count) == 1 117 | expect(associatedTypeNameToInfoMap?["Indices"]?.inheritsFromTypes.first?.asSource) == "Collection" 118 | } 119 | 120 | it("finds the associatedtype Indices's inheritance") { 121 | expect(associatedTypeNameToInfoMap?["Indices"]?.inheritsFromTypes.count) == 1 122 | expect(associatedTypeNameToInfoMap?["Indices"]?.initializer?.asSource) == "DefaultIndices" 123 | } 124 | 125 | it("finds the associatedtype Indices's generic requirement") { 126 | expect(associatedTypeNameToInfoMap?["Indices"]?.genericRequirements.count) == 1 127 | expect(associatedTypeNameToInfoMap?["Indices"]?.genericRequirements.first?.leftType.asSource) == "Self.Indices" 128 | expect(associatedTypeNameToInfoMap?["Indices"]?.genericRequirements.first?.relationship) == .equals 129 | expect(associatedTypeNameToInfoMap?["Indices"]?.genericRequirements.first?.rightType.asSource) == "Self.Indices.SubSequence" 130 | } 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/Tests/ImportVisitorSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Francisco Diaz on 10/16/19. 2 | // 3 | // Copyright (c) 2020 Francisco Diaz 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Nimble 26 | import Quick 27 | import Foundation 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorVisitors 31 | 32 | final class ImportVisitorSpec: QuickSpec { 33 | private var sut = ImportVisitor() 34 | 35 | override func spec() { 36 | beforeEach { 37 | self.sut = ImportVisitor() 38 | } 39 | 40 | describe("visit") { 41 | context("when there is no import statement") { 42 | let content = """ 43 | public final class Some { 44 | } 45 | """ 46 | 47 | it("returns an empty array") { 48 | try self.sut.walkContent(content) 49 | 50 | expect(self.sut.imports).to(beEmpty()) 51 | } 52 | } 53 | 54 | context("with a simple import statements") { 55 | beforeEach { 56 | let content = """ 57 | import SomeModule 58 | 59 | public final class Some { 60 | } 61 | """ 62 | try? self.sut.walkContent(content) 63 | } 64 | 65 | it("returns the appropriate main module name") { 66 | expect(self.sut.imports.first?.mainModule) == "SomeModule" 67 | } 68 | 69 | it("returns an empty String for the submodule name") { 70 | expect(self.sut.imports.first?.submodule) == "" 71 | } 72 | 73 | it("returns an empty String for the kind") { 74 | expect(self.sut.imports.first?.kind) == "" 75 | } 76 | 77 | } 78 | 79 | context("with an import statement with an attribute") { 80 | beforeEach { 81 | let content = """ 82 | @_exported import SomeModule 83 | 84 | public final protocol Some {} 85 | """ 86 | try? self.sut.walkContent(content) 87 | } 88 | 89 | it("returns the appropriate attribute name") { 90 | expect(self.sut.imports.first?.attribute) == "_exported" 91 | } 92 | } 93 | 94 | context("with an import statement with a submodule") { 95 | beforeEach { 96 | let content = """ 97 | import SomeModule.Submodule 98 | 99 | public final struct Some {} 100 | """ 101 | try? self.sut.walkContent(content) 102 | } 103 | 104 | it("returns the appropriate main module name") { 105 | expect(self.sut.imports.first?.mainModule) == "SomeModule" 106 | } 107 | 108 | it("returns the appropriate submodule name") { 109 | expect(self.sut.imports.first?.submodule) == "Submodule" 110 | } 111 | 112 | it("returns the appropriate kind") { 113 | expect(self.sut.imports.first?.kind) == "" 114 | } 115 | 116 | } 117 | 118 | context("with an import statement with a kind and a submodule") { 119 | beforeEach { 120 | let content = """ 121 | import struct SomeModule.Submodule 122 | 123 | public final enum Some {} 124 | """ 125 | try? self.sut.walkContent(content) 126 | } 127 | 128 | it("returns the appropriate main module name") { 129 | expect(self.sut.imports.first?.mainModule) == "SomeModule" 130 | } 131 | 132 | it("returns the appropriate submodule name") { 133 | expect(self.sut.imports.first?.submodule) == "Submodule" 134 | } 135 | 136 | it("returns the appropriate kind") { 137 | expect(self.sut.imports.first?.kind) == "struct" 138 | } 139 | 140 | } 141 | 142 | context("with multiple import statements with a kind and a submodule") { 143 | beforeEach { 144 | let content = """ 145 | import struct SomeModule.Submodule 146 | import class Another.AnotherSubmodule 147 | 148 | public final class Some {} 149 | """ 150 | try? self.sut.walkContent(content) 151 | } 152 | 153 | it("returns the appropriate main module name for all imports") { 154 | expect(self.sut.imports.first?.mainModule) == "SomeModule" 155 | expect(self.sut.imports.last?.mainModule) == "Another" 156 | } 157 | 158 | it("returns the appropriate submodule name for all imports") { 159 | expect(self.sut.imports.first?.submodule) == "Submodule" 160 | expect(self.sut.imports.last?.submodule) == "AnotherSubmodule" 161 | } 162 | 163 | it("returns the appropriate kind for all imports") { 164 | expect(self.sut.imports.first?.kind) == "struct" 165 | expect(self.sut.imports.last?.kind) == "class" 166 | } 167 | 168 | } 169 | 170 | } 171 | 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/Tests/TypeInheritanceVisitorSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/27/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Nimble 26 | import Quick 27 | import Foundation 28 | import SwiftInspectorTestHelpers 29 | 30 | @testable import SwiftInspectorVisitors 31 | 32 | final class TypeInheritanceVisitorSpec: QuickSpec { 33 | private var sut = TypeInheritanceVisitor() 34 | 35 | override func spec() { 36 | beforeEach { 37 | self.sut = TypeInheritanceVisitor() 38 | } 39 | 40 | describe("visit(_:)") { 41 | context("when a type conforms to a protocol") { 42 | context("with only one conformance") { 43 | beforeEach { 44 | let content = """ 45 | class SomeObject: Foo {} 46 | """ 47 | try? self.sut.walkContent(content) 48 | } 49 | 50 | it("returns the conforming type name") { 51 | expect(self.sut.inheritsFromTypes.map { $0.asSource }) == ["Foo"] 52 | } 53 | } 54 | 55 | context("with one fully-qualified conformance") { 56 | beforeEach { 57 | let content = """ 58 | class SomeObject: Swift.Equatable {} 59 | """ 60 | try? self.sut.walkContent(content) 61 | } 62 | 63 | it("returns the conforming type name") { 64 | expect(self.sut.inheritsFromTypes.map { $0.asSource }) == ["Swift.Equatable"] 65 | } 66 | } 67 | 68 | context("with multiple conformances on the same line") { 69 | beforeEach { 70 | let content = """ 71 | class SomeObject: Foo, Bar, FooBar {} 72 | """ 73 | try? self.sut.walkContent(content) 74 | } 75 | 76 | it("returns the conforming type names") { 77 | expect(self.sut.inheritsFromTypes.map { $0.asSource }) == ["Foo", "Bar", "FooBar"] 78 | } 79 | } 80 | 81 | context("with multiple conformances on multiple lines") { 82 | beforeEach { 83 | let content = """ 84 | class SomeObject: Foo, Bar, 85 | FooBar {} 86 | """ 87 | try? self.sut.walkContent(content) 88 | } 89 | 90 | it("returns the conforming type names") { 91 | expect(self.sut.inheritsFromTypes.map { $0.asSource }) == ["Foo", "Bar", "FooBar"] 92 | } 93 | } 94 | 95 | context("with composition conformances on multiple lines") { 96 | beforeEach { 97 | let content = """ 98 | protocol Protocol: FooProviding & BarProviding 99 | & FooBarProviding {} 100 | """ 101 | try? self.sut.walkContent(content) 102 | } 103 | 104 | it("returns the conforming type names") { 105 | expect(self.sut.inheritsFromTypes.map { $0.asSource }) == ["FooProviding & BarProviding & FooBarProviding"] 106 | } 107 | } 108 | } 109 | 110 | context("when an inner type conforms to a protocol but the outer type does not") { 111 | beforeEach { 112 | let content = """ 113 | class SomeObject { 114 | struct SomeStruct: Foo {} 115 | } 116 | """ 117 | try? self.sut.walkContent(content) 118 | } 119 | 120 | it("does not find the inner type's conformance the conforming type names") { 121 | expect(self.sut.inheritsFromTypes.map { $0.asSource }).to(beEmpty()) 122 | } 123 | } 124 | 125 | context("when both an outer and inner type conform to a protocol") { 126 | beforeEach { 127 | let content = """ 128 | class SomeObject: Bar { 129 | struct SomeStruct: Foo {} 130 | } 131 | """ 132 | try? self.sut.walkContent(content) 133 | } 134 | 135 | it("only finds the outer type's conformance") { 136 | expect(self.sut.inheritsFromTypes.map { $0.asSource }) == ["Bar"] 137 | } 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/Tests/TypealiasVisitorSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/17/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Nimble 26 | import Quick 27 | import SwiftInspectorTestHelpers 28 | 29 | @testable import SwiftInspectorVisitors 30 | 31 | final class TypealiasVisitorSpec: QuickSpec { 32 | private var sut = TypealiasVisitor() 33 | 34 | override func spec() { 35 | beforeEach { 36 | self.sut = TypealiasVisitor() 37 | } 38 | 39 | describe("visit(_:)") { 40 | context("visiting a typealias") { 41 | var associatedTypeNameToInfoMap: [String: TypealiasInfo]? 42 | beforeEach { 43 | let content = """ 44 | public typealias CountableClosedRange = ClosedRange where Bound : Strideable, Bound.Stride : SignedInteger 45 | """ 46 | 47 | try? self.sut.walkContent(content) 48 | 49 | associatedTypeNameToInfoMap = self.sut.typealiases 50 | .reduce(into: [String: TypealiasInfo]()) { (result, typealiasInfo) in 51 | result[typealiasInfo.name] = typealiasInfo 52 | } 53 | } 54 | 55 | it("finds the typealias CountableClosedRange") { 56 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]).toNot(beNil()) 57 | } 58 | 59 | it("finds the typealias CountableClosedRange's generic parameters") { 60 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.genericParameters.count) == 1 61 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.genericParameters.first?.name) == "Bound" 62 | } 63 | 64 | it("finds the typealias CountableClosedRange's generic requirements") { 65 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.genericRequirements.count) == 2 66 | 67 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.genericRequirements.first?.leftType.asSource) == "Bound" 68 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.genericRequirements.first?.relationship) == .conformsTo 69 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.genericRequirements.first?.rightType.asSource) == "Strideable" 70 | 71 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.genericRequirements.last?.leftType.asSource) == "Bound.Stride" 72 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.genericRequirements.last?.relationship) == .conformsTo 73 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.genericRequirements.last?.rightType.asSource) == "SignedInteger" 74 | } 75 | 76 | it("finds the typealias CountableClosedRange's modifiers") { 77 | expect(associatedTypeNameToInfoMap?["CountableClosedRange"]?.modifiers) == [.public] 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/TypeInheritanceVisitor.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 1/27/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import Foundation 26 | import SwiftSyntax 27 | 28 | public final class TypeInheritanceVisitor: SyntaxVisitor { 29 | 30 | public private(set) var inheritsFromTypes = [TypeDescription]() 31 | 32 | public override func visit(_ node: InheritedTypeSyntax) -> SyntaxVisitorContinueKind { 33 | inheritsFromTypes.append(node.typeName.typeDescription) 34 | // Children don't have any more information about inheritance, so don't visit them. 35 | return .skipChildren 36 | } 37 | 38 | public override func visit(_ node: MemberDeclBlockSyntax) -> SyntaxVisitorContinueKind { 39 | // A member declaration block means we've found the body of the type. 40 | // There's nothing in this body that would help us determine type inheritance. 41 | .skipChildren 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/SwiftInspectorVisitors/TypealiasVisitor.swift: -------------------------------------------------------------------------------- 1 | // Created by Dan Federman on 2/17/21. 2 | // 3 | // Copyright © 2021 Dan Federman 4 | // 5 | // Distributed under the MIT License 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import SwiftSyntax 26 | 27 | public final class TypealiasVisitor: SyntaxVisitor { 28 | 29 | public init(parentType: TypeDescription? = nil) { 30 | self.parentType = parentType 31 | } 32 | 33 | public private(set) var typealiases = [TypealiasInfo]() 34 | 35 | public override func visit(_ node: TypealiasDeclSyntax) -> SyntaxVisitorContinueKind { 36 | let name = node.identifier.text 37 | 38 | let genericTypeVisitor = GenericParameterVisitor() 39 | if let genericParameterClause = node.genericParameterClause { 40 | genericTypeVisitor.walk(genericParameterClause) 41 | } 42 | 43 | let genericRequirementVisitor = GenericRequirementVisitor() 44 | if let genericWhereClause = node.genericWhereClause { 45 | genericRequirementVisitor.walk(genericWhereClause) 46 | } 47 | 48 | let declarationModifierVisitor = DeclarationModifierVisitor() 49 | if let modifiers = node.modifiers { 50 | declarationModifierVisitor.walk(modifiers) 51 | } 52 | 53 | typealiases.append( 54 | .init( 55 | name: name, 56 | genericParameters: genericTypeVisitor.genericParameters, 57 | initializer: node.initializer?.value.typeDescription, 58 | genericRequirements: genericRequirementVisitor.genericRequirements, 59 | modifiers: declarationModifierVisitor.modifiers, 60 | parentType: parentType)) 61 | 62 | return .skipChildren 63 | } 64 | 65 | // MARK: Private 66 | 67 | private let parentType: TypeDescription? 68 | } 69 | 70 | public struct TypealiasInfo: Codable, Hashable { 71 | public let name: String 72 | public let genericParameters: [GenericParameter] 73 | public let initializer: TypeDescription? 74 | public let genericRequirements: [GenericRequirement] 75 | public let modifiers: Modifiers 76 | public let parentType: TypeDescription? 77 | } 78 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 0.25% 6 | patch: off 7 | ignore: 8 | - "Sources/SwiftInspectorAnalyzers/Tests" 9 | - "Sources/SwiftInspectorCommands/Tests" 10 | - "Sources/SwiftInspectorTestHelpers/Tests" 11 | - "Sources/SwiftInspectorVisitors/Tests" 12 | -------------------------------------------------------------------------------- /docs/Releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing a new version of SwiftInspector 2 | 3 | 1. Run `make release` from the root of the repo. This will generate a `swiftinspector.zip` file. 4 | 1. Create a new [draft release](https://github.com/fdiaz/SwiftInspector/releases/new) following [semantic versioning](https://semver.org/) for both the tag version and the release title. 5 | 1. In the description of the release point out all the PRs that have been merged since the previous version and describe the new features. 6 | 1. Attach `swiftinspector.zip` to the release 7 | 1. Select the `This is a pre-release` checkbox 8 | 1. Click on `Publish release` 9 | -------------------------------------------------------------------------------- /img/macstadium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdiaz/SwiftInspector/450c62a742b318f9b45643de37e8ef29a6c840f1/img/macstadium.png -------------------------------------------------------------------------------- /img/swiftinspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdiaz/SwiftInspector/450c62a742b318f9b45643de37e8ef29a6c840f1/img/swiftinspector.png --------------------------------------------------------------------------------