├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ ├── build_and_test.yml │ └── swiftlint.yml ├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ └── DependencyGraph.xcscheme ├── CODEOWNERS ├── Formula └── dependency-graph.rb ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Library │ ├── Commands │ │ └── GraphCommand │ │ │ ├── DirectedGraphWriterFactory.swift │ │ │ ├── GraphCommand.swift │ │ │ └── Syntax.swift │ ├── Graphing │ │ ├── D2GraphMapper │ │ │ ├── D2GraphMapper.swift │ │ │ └── D2GraphSettings.swift │ │ ├── DOTGraphMapper │ │ │ ├── DOTGraphMapper.swift │ │ │ └── DOTGraphSettings.swift │ │ ├── DirectedGraph │ │ │ ├── DirectedGraph+Cluster.swift │ │ │ ├── DirectedGraph+Edge.swift │ │ │ ├── DirectedGraph+Node.swift │ │ │ └── DirectedGraph.swift │ │ ├── DirectedGraphMapper │ │ │ └── DirectedGraphMapper.swift │ │ ├── DirectedGraphXcodeHelpers │ │ │ ├── DirectedGraph+XcodeHelpers.swift │ │ │ └── String+SafeName.swift │ │ ├── MermaidGraphMapper │ │ │ ├── MermaidGraphMapper.swift │ │ │ └── MermaidGraphSettings.swift │ │ ├── PackageGraphBuilder │ │ │ └── PackageGraphBuilder.swift │ │ ├── PackageGraphBuilderLive │ │ │ ├── Internal │ │ │ │ ├── AllDependenciesGraphBuilder.swift │ │ │ │ └── PackagesOnlyGraphBuilder.swift │ │ │ └── PackageGraphBuilderLive.swift │ │ ├── XcodeProjectGraphBuilder │ │ │ └── XcodeProjectGraphBuilder.swift │ │ └── XcodeProjectGraphBuilderLive │ │ │ ├── Internal │ │ │ ├── AllDependenciesGraphBuilder.swift │ │ │ └── PackagesOnlyGraphBuilder.swift │ │ │ └── XcodeProjectGraphBuilderLive.swift │ ├── Outputting │ │ ├── DirectedGraphWriter │ │ │ └── DirectedGraphWriter.swift │ │ ├── MappingDirectedGraphWriter │ │ │ └── MappingDirectedGraphWriter.swift │ │ ├── StdoutWriter │ │ │ └── StdoutWriter.swift │ │ └── Writer │ │ │ └── Writer.swift │ ├── Parsing │ │ ├── DumpPackageService │ │ │ └── DumpPackageService.swift │ │ ├── DumpPackageServiceLive │ │ │ └── DumpPackageServiceLive.swift │ │ ├── PackageSwiftFile │ │ │ ├── PackageSwiftFile+Dependency.swift │ │ │ ├── PackageSwiftFile+Product.swift │ │ │ ├── PackageSwiftFile+Target+Dependency.swift │ │ │ ├── PackageSwiftFile+Target.swift │ │ │ └── PackageSwiftFile.swift │ │ ├── PackageSwiftFileParser │ │ │ └── PackageSwiftFileParser.swift │ │ ├── PackageSwiftFileParserCache │ │ │ └── PackageSwiftFileParserCache.swift │ │ ├── PackageSwiftFileParserCacheLive │ │ │ └── PackageSwiftFileParserCacheLive.swift │ │ ├── PackageSwiftFileParserLive │ │ │ ├── Internal │ │ │ │ ├── IntermediatePackageSwiftFile+Dependency.swift │ │ │ │ ├── IntermediatePackageSwiftFile+Product.swift │ │ │ │ ├── IntermediatePackageSwiftFile+Target+Dependency.swift │ │ │ │ ├── IntermediatePackageSwiftFile+Target.swift │ │ │ │ ├── IntermediatePackageSwiftFile.swift │ │ │ │ └── PackageSwiftFileMapper.swift │ │ │ └── PackageSwiftParserLive.swift │ │ ├── ProjectRootClassifier │ │ │ ├── ProjectRoot.swift │ │ │ └── ProjectRootClassifier.swift │ │ ├── ProjectRootClassifierLive │ │ │ └── ProjectRootClassifierLive.swift │ │ ├── XcodeProject │ │ │ ├── XcodeProject+SwiftPackage.swift │ │ │ ├── XcodeProject+Target.swift │ │ │ └── XcodeProject.swift │ │ ├── XcodeProjectParser │ │ │ └── XcodeProjectParser.swift │ │ └── XcodeProjectParserLive │ │ │ └── XcodeProjectParserLive.swift │ └── Utilities │ │ ├── FileSystem │ │ └── FileSystem.swift │ │ ├── FileSystemLive │ │ └── FileSystemLive.swift │ │ ├── ShellCommandRunner │ │ ├── ShellCommandOutput.swift │ │ └── ShellCommandRunner.swift │ │ ├── ShellCommandRunnerLive │ │ └── ShellCommandRunnerLive.swift │ │ └── StringIndentHelpers │ │ └── Indent.swift └── Main │ ├── CompositionRoot.swift │ └── DependencyGraph.swift ├── Tests ├── D2GraphMapperTests │ ├── D2GraphMapperTests.swift │ └── Mock │ │ └── DirectedGraph.swift ├── DOTGraphMapperTests │ ├── DOTGraphMapperTests.swift │ └── Mock │ │ └── DirectedGraph.swift ├── DirectedGraphTests │ └── DirectedGraphTests.swift ├── DirectedGraphXcodeHelpersTests │ └── String+SafeNameTests.swift ├── DumpPackageServiceLiveTests │ ├── DumpPackageServiceLiveTests.swift │ └── Mock │ │ └── ShellCommandRunnerMock.swift ├── ExampleProject │ ├── Example.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ ├── Example │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── SceneDelegate.swift │ │ └── ViewController.swift │ ├── ExamplePackageA │ │ ├── .gitignore │ │ ├── Package.swift │ │ └── Sources │ │ │ └── ExampleLibraryA │ │ │ └── Dummy.swift │ ├── ExamplePackageB │ │ ├── .gitignore │ │ ├── Package.swift │ │ └── Sources │ │ │ └── ExampleLibraryB │ │ │ └── Dummy.swift │ ├── ExamplePackageC │ │ ├── .gitignore │ │ ├── Package.swift │ │ └── Sources │ │ │ └── ExampleLibraryC │ │ │ └── Dummy.swift │ ├── ExampleTests │ │ └── ExampleTests.swift │ └── ExampleUITests │ │ ├── ExampleUITests.swift │ │ └── ExampleUITestsLaunchTests.swift ├── GraphCommandTests │ ├── DirectedGraphWriterFactoryTests.swift │ ├── GraphCommandTests.swift │ └── Mock │ │ ├── DirectedGraphWriterMock.swift │ │ ├── PackageDependencyGraphBuilderMock.swift │ │ ├── PackageSwiftFileParserMocker.swift │ │ ├── ProjectRootClassifierMock.swift │ │ ├── XcodeProjectDependencyGraphBuilderMock.swift │ │ └── XcodeProjectParserMock.swift ├── MappingDirectedGraphWriterTests │ ├── MappingDirectedGraphWriterTests.swift │ └── Mock │ │ ├── DirectedGraphMapperMock.swift │ │ └── WriterMock.swift ├── MermaidGraphMapperTests │ ├── MermaidGraphMapperTests.swift │ └── Mock │ │ └── DirectedGraph.swift ├── PackageGraphBuilderLiveTests │ ├── Mock │ │ └── PackageSwiftFile+Mock.swift │ └── PackageGraphBuilderLiveTests.swift ├── PackageSwiftFileParserLiveTests │ ├── Mock │ │ ├── DumpPackageServiceMock.swift │ │ ├── PackageSwiftFileParserCacheMock.swift │ │ └── URL+Mock.swift │ ├── MockData │ │ ├── dependency-syntax-byname-with-platform-names.json │ │ ├── dependency-syntax-target.json │ │ ├── example-package-a.json │ │ ├── example-package-b.json │ │ └── example-package-c.json │ └── PackageSwiftFileParserLiveTests.swift ├── ProjectRootClassifierLiveTests │ ├── Mock │ │ └── FileSystemMock.swift │ └── ProjectRootClassifierLiveTests.swift ├── StringIndentHelpersTests │ └── StringIndentHelpersTests.swift ├── XcodeProjectGraphBuilderLiveTests │ ├── Mock │ │ ├── PackageDependencyGraphBuilderMock.swift │ │ ├── PackageSwiftFileParserMock.swift │ │ ├── URL+Mock.swift │ │ └── XcodeProject+Mock.swift │ └── XcodeProjectGraphBuilderLiveTests.swift └── XcodeProjectParserLiveTests │ ├── Example │ ├── Example.xcodeproj │ │ └── project.pbxproj │ ├── ExamplePackageA │ │ ├── .gitignore │ │ ├── .swiftpm │ │ │ └── xcode │ │ │ │ └── package.xcworkspace │ │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── Package.swift │ ├── ExamplePackageB │ │ ├── .gitignore │ │ ├── .swiftpm │ │ │ └── xcode │ │ │ │ └── package.xcworkspace │ │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── Package.swift │ └── ExamplePackageC │ │ ├── .gitignore │ │ └── Package.swift │ ├── Mock │ └── FileExistenceCheckerMock.swift │ └── XcodeProjectParserLiveTests.swift ├── example-d2-elk.png ├── example-d2.png ├── example-dot.png ├── example-mermaid.png ├── example-packages-only.png ├── example-swift-package.png └── example-xcodeproj.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: simonbs 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | labels: ["bug"] 4 | assignees: 5 | - simonbs 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: textarea 12 | id: bug-description 13 | attributes: 14 | label: What happened? 15 | description: Please describe the bug. 16 | placeholder: Description of the bug. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: steps-to-reproduce 21 | attributes: 22 | label: What are the steps to reproduce? 23 | description: Please describe the steps to reproduce the bug. 24 | placeholder: | 25 | Step 1: ... 26 | Step 2: ... 27 | Step 3: ... 28 | validations: 29 | required: true 30 | - type: textarea 31 | id: expected-behavior 32 | attributes: 33 | label: What is the expected behavior? 34 | description: Please describe the behavior you expect of Runestone. 35 | placeholder: I expect that Runestone would... 36 | validations: 37 | required: true 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: ["feature"] 4 | assignees: 5 | - simonbs 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this feature request! 11 | - type: textarea 12 | id: problem 13 | attributes: 14 | label: Is your feature request related to a problem? 15 | description: A clear and concise description of what the problem is. 16 | placeholder: Yes, the problem is that... 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: solution 21 | attributes: 22 | label: What solution would you like? 23 | description: A clear and concise description of what you want to happen. 24 | placeholder: I would like that... 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: alternatives 29 | attributes: 30 | label: What alternatives have you considered? 31 | description: A clear and concise description of any alternative solutions or features you've considered. 32 | placeholder: I have considered to... 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: context 37 | attributes: 38 | label: Any additional context? 39 | description: Add any other context or screenshots about the feature request here. 40 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: 3 | workflow_dispatch: {} 4 | pull_request: 5 | branches: [ main ] 6 | paths: 7 | - 'Sources/**' 8 | - 'Tests/**' 9 | jobs: 10 | build: 11 | name: Build and test 12 | runs-on: macOS-12 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | with: 17 | submodules: recursive 18 | - name: Build 19 | run: xcodebuild build-for-testing -scheme DependencyGraph -destination "platform=macOS" 20 | - name: Test 21 | run: xcodebuild test-without-building -scheme DependencyGraph -destination "platform=macOS" 22 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | on: 3 | workflow_dispatch: {} 4 | pull_request: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | jobs: 10 | SwiftLint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | submodules: recursive 17 | - name: GitHub Action for SwiftLint 18 | uses: norio-nomura/action-swiftlint@3.2.1 19 | with: 20 | args: --strict 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .swiftpm/xcode/xcuserdata 3 | .swiftpm/xcode/package.xcworkspace 4 | Tests/**/.swiftpm 5 | Tests/**/xcuserdata 6 | .build 7 | DerivedData/ 8 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - .build 3 | disabled_rules: 4 | - nesting 5 | opt_in_rules: 6 | - anonymous_argument_in_multiline_closure 7 | - array_init 8 | - capture_variable 9 | - collection_alignment 10 | - conditional_returns_on_newline 11 | - contains_over_filter_count 12 | - contains_over_filter_is_empty 13 | - contains_over_first_not_nil 14 | - contains_over_range_nil_comparison 15 | - convenience_type 16 | - discarded_notification_center_observer 17 | - discouraged_assert 18 | - discouraged_none_name 19 | - discouraged_object_literal 20 | - discouraged_optional_boolean 21 | - empty_collection_literal 22 | - empty_count 23 | - empty_string 24 | - empty_xctest_method 25 | - explicit_init 26 | - fallthrough 27 | - fatal_error_message 28 | - file_name_no_space 29 | - first_where 30 | - flatmap_over_map_reduce 31 | - identical_operands 32 | - implicitly_unwrapped_optional 33 | - joined_default_parameter 34 | - last_where 35 | - literal_expression_end_indentation 36 | - lower_acl_than_parent 37 | - modifier_order 38 | - number_separator 39 | - operator_usage_whitespace 40 | - overridden_super_call 41 | - pattern_matching_keywords 42 | - prefer_self_in_static_references 43 | - prefer_self_type_over_type_of_self 44 | - prefer_zero_over_explicit_init 45 | - prohibited_interface_builder 46 | - prohibited_super_call 47 | - reduce_into 48 | - redundant_nil_coalescing 49 | - redundant_type_annotation 50 | - single_test_class 51 | - sorted_first_last 52 | - sorted_imports 53 | - static_operator 54 | - switch_case_on_newline 55 | - test_case_accessibility 56 | - toggle_bool 57 | - trailing_closure 58 | - unneeded_parentheses_in_closure_argument 59 | - untyped_error_in_catch 60 | - vertical_parameter_alignment_on_call 61 | - yoda_condition 62 | line_length: 63 | warning: 150 64 | error: 175 65 | ignores_comments: true 66 | type_name: 67 | max_length: 68 | warning: 60 69 | error: 60 70 | identifier_name: 71 | allowed_symbols: "_" 72 | max_length: 73 | warning: 60 74 | error: 60 75 | min_length: 76 | warning: 1 77 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @simonbs 2 | -------------------------------------------------------------------------------- /Formula/dependency-graph.rb: -------------------------------------------------------------------------------- 1 | class DependencyGraph < Formula 2 | desc "Generates graphs of the dependencies in an Xcode project or Swift package." 3 | homepage "https://github.com/simonbs/DependencyGraph" 4 | url "https://github.com/simonbs/dependency-graph.git", tag: "1.2.0", using: :git 5 | head "https://github.com/simonbs/dependency-graph", branch: "main" 6 | 7 | depends_on xcode: ["12.0", :build] 8 | 9 | def install 10 | system "swift", "build", "--disable-sandbox", "-c", "release" 11 | bin.install ".build/release/dependency-graph" 12 | end 13 | 14 | test do 15 | system "#{bin}/dependency-graph", "--version" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Simon Støvring 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 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "aexml", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/tadija/AEXML.git", 7 | "state" : { 8 | "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", 9 | "version" : "4.6.1" 10 | } 11 | }, 12 | { 13 | "identity" : "pathkit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/kylef/PathKit.git", 16 | "state" : { 17 | "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", 18 | "version" : "1.0.1" 19 | } 20 | }, 21 | { 22 | "identity" : "spectre", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/kylef/Spectre.git", 25 | "state" : { 26 | "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", 27 | "version" : "0.10.1" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-argument-parser", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-argument-parser", 34 | "state" : { 35 | "revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d", 36 | "version" : "1.2.0" 37 | } 38 | }, 39 | { 40 | "identity" : "xcodeproj", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/tuist/XcodeProj.git", 43 | "state" : { 44 | "revision" : "b6de1bfe021b861c94e7c83821b595083f74b997", 45 | "version" : "8.8.0" 46 | } 47 | } 48 | ], 49 | "version" : 2 50 | } 51 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "DependencyGraph", 8 | platforms: [.macOS(.v12)], 9 | products: [ 10 | .executable(name: "dependency-graph", targets: ["Main"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"), 14 | .package(url: "https://github.com/tuist/XcodeProj.git", .upToNextMajor(from: "8.8.0")) 15 | ], 16 | targets: [ 17 | .executableTarget(name: "Main", dependencies: [ 18 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 19 | "D2GraphMapper", 20 | "DirectedGraphMapper", 21 | "DirectedGraphWriter", 22 | "DOTGraphMapper", 23 | "DumpPackageService", 24 | "DumpPackageServiceLive", 25 | "FileSystem", 26 | "FileSystemLive", 27 | "GraphCommand", 28 | "MappingDirectedGraphWriter", 29 | "MermaidGraphMapper", 30 | "PackageGraphBuilder", 31 | "PackageGraphBuilderLive", 32 | "PackageSwiftFileParser", 33 | "PackageSwiftFileParserCache", 34 | "PackageSwiftFileParserCacheLive", 35 | "PackageSwiftFileParserLive", 36 | "ProjectRootClassifier", 37 | "ProjectRootClassifierLive", 38 | "ShellCommandRunner", 39 | "ShellCommandRunnerLive", 40 | "StdoutWriter", 41 | "XcodeProjectGraphBuilder", 42 | "XcodeProjectGraphBuilderLive", 43 | "XcodeProjectParser", 44 | "XcodeProjectParserLive" 45 | ]), 46 | 47 | // Sources/Library/Commands 48 | .target(name: "GraphCommand", dependencies: [ 49 | "DirectedGraphMapper", 50 | "DirectedGraphWriter", 51 | "PackageGraphBuilder", 52 | "PackageSwiftFileParser", 53 | "ProjectRootClassifier", 54 | "XcodeProjectParser", 55 | "XcodeProjectGraphBuilder" 56 | ], path: "Sources/Library/Commands/GraphCommand"), 57 | 58 | // Sources/Library/Graphing 59 | .target(name: "D2GraphMapper", dependencies: [ 60 | "DirectedGraph", 61 | "DirectedGraphMapper", 62 | "StringIndentHelpers" 63 | ], path: "Sources/Library/Graphing/D2GraphMapper"), 64 | .target(name: "DirectedGraph", path: "Sources/Library/Graphing/DirectedGraph"), 65 | .target(name: "DirectedGraphXcodeHelpers", dependencies: [ 66 | "DirectedGraph" 67 | ], path: "Sources/Library/Graphing/DirectedGraphXcodeHelpers"), 68 | .target(name: "DirectedGraphMapper", dependencies: [ 69 | "DirectedGraph" 70 | ], path: "Sources/Library/Graphing/DirectedGraphMapper"), 71 | .target(name: "DOTGraphMapper", dependencies: [ 72 | "DirectedGraph", 73 | "DirectedGraphMapper", 74 | "StringIndentHelpers" 75 | ], path: "Sources/Library/Graphing/DOTGraphMapper"), 76 | .target(name: "MermaidGraphMapper", dependencies: [ 77 | "DirectedGraph", 78 | "DirectedGraphMapper", 79 | "StringIndentHelpers" 80 | ], path: "Sources/Library/Graphing/MermaidGraphMapper"), 81 | .target(name: "PackageGraphBuilder", dependencies: [ 82 | "DirectedGraph", 83 | "PackageSwiftFile" 84 | ], path: "Sources/Library/Graphing/PackageGraphBuilder"), 85 | .target(name: "PackageGraphBuilderLive", dependencies: [ 86 | "DirectedGraph", 87 | "DirectedGraphXcodeHelpers", 88 | "PackageGraphBuilder", 89 | "PackageSwiftFile" 90 | ], path: "Sources/Library/Graphing/PackageGraphBuilderLive"), 91 | .target(name: "XcodeProjectGraphBuilder", dependencies: [ 92 | "DirectedGraph", 93 | "PackageGraphBuilder", 94 | "XcodeProject" 95 | ], path: "Sources/Library/Graphing/XcodeProjectGraphBuilder"), 96 | .target(name: "XcodeProjectGraphBuilderLive", dependencies: [ 97 | "DirectedGraph", 98 | "DirectedGraphXcodeHelpers", 99 | "PackageGraphBuilder", 100 | "PackageSwiftFile", 101 | "PackageSwiftFileParser", 102 | "XcodeProjectGraphBuilder", 103 | "XcodeProject" 104 | ], path: "Sources/Library/Graphing/XcodeProjectGraphBuilderLive"), 105 | 106 | // Sources/Library/Parsing 107 | .target(name: "DumpPackageService", path: "Sources/Library/Parsing/DumpPackageService"), 108 | .target(name: "DumpPackageServiceLive", dependencies: [ 109 | "DumpPackageService", 110 | "ShellCommandRunner" 111 | ], path: "Sources/Library/Parsing/DumpPackageServiceLive"), 112 | .target(name: "PackageSwiftFile", path: "Sources/Library/Parsing/PackageSwiftFile"), 113 | .target(name: "PackageSwiftFileParser", dependencies: [ 114 | "PackageSwiftFile" 115 | ], path: "Sources/Library/Parsing/PackageSwiftFileParser"), 116 | .target(name: "PackageSwiftFileParserCache", dependencies: [ 117 | "PackageSwiftFile" 118 | ], path: "Sources/Library/Parsing/PackageSwiftFileParserCache"), 119 | .target(name: "PackageSwiftFileParserCacheLive", dependencies: [ 120 | "PackageSwiftFile", 121 | "PackageSwiftFileParserCache" 122 | ], path: "Sources/Library/Parsing/PackageSwiftFileParserCacheLive"), 123 | .target(name: "PackageSwiftFileParserLive", dependencies: [ 124 | "DumpPackageService", 125 | "PackageSwiftFile", 126 | "PackageSwiftFileParser", 127 | "PackageSwiftFileParserCache" 128 | ], path: "Sources/Library/Parsing/PackageSwiftFileParserLive"), 129 | .target(name: "ProjectRootClassifier", path: "Sources/Library/Parsing/ProjectRootClassifier"), 130 | .target(name: "ProjectRootClassifierLive", dependencies: [ 131 | "FileSystem", 132 | "ProjectRootClassifier" 133 | ], path: "Sources/Library/Parsing/ProjectRootClassifierLive"), 134 | .target(name: "XcodeProject", path: "Sources/Library/Parsing/XcodeProject"), 135 | .target(name: "XcodeProjectParser", dependencies: [ 136 | "XcodeProject" 137 | ], path: "Sources/Library/Parsing/XcodeProjectParser"), 138 | .target(name: "XcodeProjectParserLive", dependencies: [ 139 | "FileSystem", 140 | .product(name: "XcodeProj", package: "XcodeProj"), 141 | "XcodeProject", 142 | "XcodeProjectParser" 143 | ], path: "Sources/Library/Parsing/XcodeProjectParserLive"), 144 | 145 | // Sources/Library/Outputting 146 | .target(name: "DirectedGraphWriter", dependencies: [ 147 | "DirectedGraph", 148 | "Writer" 149 | ], path: "Sources/Library/Outputting/DirectedGraphWriter"), 150 | .target(name: "MappingDirectedGraphWriter", dependencies: [ 151 | "DirectedGraph", 152 | "DirectedGraphMapper", 153 | "DirectedGraphWriter" 154 | ], path: "Sources/Library/Outputting/MappingDirectedGraphWriter"), 155 | .target(name: "StdoutWriter", dependencies: [ 156 | "Writer" 157 | ], path: "Sources/Library/Outputting/StdoutWriter"), 158 | .target(name: "Writer", path: "Sources/Library/Outputting/Writer"), 159 | 160 | // Sources/Library/Utilities 161 | .target(name: "FileSystem", path: "Sources/Library/Utilities/FileSystem"), 162 | .target(name: "FileSystemLive", dependencies: [ 163 | "FileSystem" 164 | ], path: "Sources/Library/Utilities/FileSystemLive"), 165 | .target(name: "ShellCommandRunner", path: "Sources/Library/Utilities/ShellCommandRunner"), 166 | .target(name: "ShellCommandRunnerLive", dependencies: [ 167 | "ShellCommandRunner" 168 | ], path: "Sources/Library/Utilities/ShellCommandRunnerLive"), 169 | .target(name: "StringIndentHelpers", path: "Sources/Library/Utilities/StringIndentHelpers"), 170 | 171 | // Tests 172 | .testTarget(name: "D2GraphMapperTests", dependencies: [ 173 | "DirectedGraph", 174 | "D2GraphMapper" 175 | ]), 176 | .testTarget(name: "DirectedGraphTests", dependencies: [ 177 | "DirectedGraph" 178 | ]), 179 | .testTarget(name: "DirectedGraphXcodeHelpersTests", dependencies: [ 180 | "DirectedGraphXcodeHelpers" 181 | ]), 182 | .testTarget(name: "DOTGraphMapperTests", dependencies: [ 183 | "DirectedGraph", 184 | "DOTGraphMapper" 185 | ]), 186 | .testTarget(name: "DumpPackageServiceLiveTests", dependencies: [ 187 | "DumpPackageServiceLive", 188 | "ShellCommandRunner" 189 | ]), 190 | .testTarget(name: "GraphCommandTests", dependencies: [ 191 | "GraphCommand" 192 | ]), 193 | .testTarget(name: "MappingDirectedGraphWriterTests", dependencies: [ 194 | "MappingDirectedGraphWriter" 195 | ]), 196 | .testTarget(name: "MermaidGraphMapperTests", dependencies: [ 197 | "DirectedGraph", 198 | "MermaidGraphMapper" 199 | ]), 200 | .testTarget(name: "PackageGraphBuilderLiveTests", dependencies: [ 201 | "DirectedGraph", 202 | "DirectedGraphXcodeHelpers", 203 | "PackageGraphBuilderLive", 204 | "PackageSwiftFile" 205 | ]), 206 | .testTarget(name: "PackageSwiftFileParserLiveTests", dependencies: [ 207 | "DumpPackageService", 208 | "PackageSwiftFileParser", 209 | "PackageSwiftFileParserCache", 210 | "PackageSwiftFileParserLive" 211 | ], resources: [.copy("MockData")]), 212 | .testTarget(name: "ProjectRootClassifierLiveTests", dependencies: [ 213 | "FileSystem", 214 | "ProjectRootClassifierLive" 215 | ]), 216 | .testTarget(name: "StringIndentHelpersTests", dependencies: [ 217 | "StringIndentHelpers" 218 | ]), 219 | .testTarget(name: "XcodeProjectGraphBuilderLiveTests", dependencies: [ 220 | "DirectedGraph", 221 | "DirectedGraphXcodeHelpers", 222 | "PackageGraphBuilder", 223 | "PackageSwiftFileParser", 224 | "XcodeProjectGraphBuilderLive", 225 | "XcodeProject" 226 | ]), 227 | .testTarget(name: "XcodeProjectParserLiveTests", dependencies: [ 228 | "FileSystem", 229 | "XcodeProject", 230 | "XcodeProjectParserLive" 231 | ], resources: [.copy("Example")]) 232 | ] 233 | ) 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🕸️ dependency-graph 2 | 3 | [![Build and Test](https://github.com/simonbs/dependency-graph/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/simonbs/dependency-graph/actions/workflows/build_and_test.yml) [![SwiftLint](https://github.com/simonbs/dependency-graph/actions/workflows/swiftlint.yml/badge.svg)](https://github.com/simonbs/dependency-graph/actions/workflows/swiftlint.yml) 4 | 5 | dependency-graph is a command-line tool that can visualize the dependencies of packages. The tool takes the path to an Xcode project or a Package.swift file as input and outputs a graph that shows the dependencies of the packages in the project or package. 6 | 7 | ## 👀 Examples 8 | 9 | The following graphs are examples of the graphs that dependency-graph can output. The first graph built by providing dependency-graph the path to a Package.swift file and the second graph was made by providing dependency-graph the path to an .xcodeproj file as input. 10 | 11 | |Swift Package|Xcode Project| 12 | |-|-| 13 | |Example graph showing the dependencies of this package.|Example graph showing the dependencies of an Xcode project.| 14 | 15 | Nodes shaped as ellipsis represent products, e.g. the libraries in a Swift package, and the square nodes represent targets. 16 | 17 | ## 🚀 Getting Started 18 | 19 | Start off by installing the tool with [Homebrew](https://brew.sh). 20 | 21 | ```bash 22 | brew tap simonbs/dependency-graph https://github.com/simonbs/dependency-graph.git 23 | brew install dependency-graph 24 | ``` 25 | 26 | > **Note** 27 | > If you get the following error when attempting to install dependency-graph: 28 | > 29 | > ``` 30 | > Error: Cannot install under Rosetta 2 in ARM default prefix (/opt/homebrew)! 31 | > To rerun under ARM use: 32 | > arch -arm64 brew install ... 33 | > To install under x86_64, install Homebrew into /usr/local. 34 | > ``` 35 | > 36 | > You can use the the following to install dependency-graph: 37 | > 38 | > ```bash 39 | > arch -arm64 brew install dependency-graph 40 | > ``` 41 | 42 | You may now run the following command to verify that the tool was installed correctly. The following command should print information on how the tool can be used. 43 | 44 | ``` 45 | dependency-graph --help 46 | ``` 47 | 48 | Run the `dependency-graph` command with the path to a folder containing an .xcodeproj or Package.swift file. 49 | 50 | ```bash 51 | dependency-graph ~/Developer/Example 52 | ``` 53 | 54 | You may also pass the full path to the .xcodeproj or Package.swift file as shown below. 55 | 56 | ```bash 57 | dependency-graph ~/Developer/Example/Example.xcodeproj 58 | ``` 59 | 60 | ### Rendering a Graph 61 | 62 | The `dependency-graph` command will output a textual representation of a graph. By default the tool will output a graph using the [DOT syntax](https://graphviz.org/doc/info/lang.html). For example, if the Xcode project or Package.swift file contains the following dependencies: 63 | 64 | ``` 65 | Library A in Package A depends on Target A 66 | Library B in Package B depends on Target B 67 | Library A in Package A depends on Library B in Package B 68 | ``` 69 | 70 | The output of the tool would be a graph that looks like this: 71 | 72 | ```dot 73 | digraph g { 74 | subgraph cluster_packageA { 75 | label="Package A" 76 | libraryA [label="LibraryB", shape=ellipse] 77 | targetA [label="TargetA", shape=box] 78 | } 79 | 80 | subgraph cluster_packageB { 81 | label="Package B" 82 | libraryB [label="LibraryB", shape=ellipse] 83 | targetB [label="TargetB", shape=box] 84 | } 85 | 86 | libraryA -> targetA 87 | libraryB -> targetB 88 | libraryA -> libraryB 89 | } 90 | ``` 91 | 92 | The output can be rendered to an image by piping it to a renderer. See the following sections for details on the supported renderers. 93 | 94 | #### DOT 95 | 96 | Example graph rendered with dot. 97 | 98 | By default dependency-graph will use the DOT syntax which can be rendered by the [dot CLI](https://graphviz.org/doc/info/command.html), which is part of [Graphviz](https://graphviz.org). 99 | 100 | Install Graphviz and run `dependency-graph` and pass the output to the newly installed `dot` CLI. 101 | 102 | ```bash 103 | brew install graphviz 104 | dependency-graph ~/Developer/Example | dot -Tsvg -o graph.svg 105 | ``` 106 | 107 | When rendering the graph to a PNG, you will likely want to specify the size of the output to ensure it is readable. To generate an image with dot that is exactly 6000 pixels wide or 8000 pixels tall but not necessarily both, do the following: 108 | 109 | ```bash 110 | dependency-graph ~/Developer/Example | dot -Tpng -Gsize=60,80\! -Gdpi=100 -o graph.png 111 | ``` 112 | 113 | You may want to play around with the values for `--node-spacing` and `--rank-spacing` to increase the readability of the graph. 114 | 115 | ```bash 116 | dependency-graph --node-spacing 50 --rank-spacing 150 ~/Developer/Example | dot -Tsvg -o graph.svg 117 | ``` 118 | 119 | For large projects the graph may become unreadable. Passing the output through Grahpviz' [unflatten](https://graphviz.org/docs/cli/unflatten/) command may improve the results. 120 | 121 | ```bash 122 | dependency-graph ~/Developer/Example | unflatten -l 100 -c 100 -f | dot -Tpng -o graph.png 123 | ``` 124 | 125 | #### Mermaid 126 | 127 | Example graph rendered with mermaid. 128 | 129 | Specify the `--syntax mermaid` option to have dependency-graph output a graph using [the Mermaid diagram syntax](https://mermaid-js.github.io/mermaid/#/flowchart). 130 | 131 | The output be rendered to an image using the [the mermaid cli](https://github.com/mermaid-js/mermaid-cli). 132 | 133 | ```bash 134 | npm install -g @mermaid-js/mermaid-cli 135 | dependency-graph --syntax mermaid ~/Developer/Example | mmdc -o graph.svg 136 | ``` 137 | 138 | To generate an image on a page that is 6000 pixels wide with mermaid, do the following: 139 | 140 | ```bash 141 | dependency-graph --syntax mermaid ~/Developer/Example | mmdc -o graph.png -w 6000 142 | ``` 143 | 144 | You may also want to play around with the values for `--node-spacing` and `--rank-spacing` to increase the readability of the graph. 145 | 146 | ```bash 147 | dependency-graph --syntax mermaid --node-spacing 50 --rank-spacing 150 ~/Developer/Example | mmdc -o graph.png 148 | ``` 149 | 150 | #### D2 151 | 152 | Example graph rendered with D2. 153 | 154 | Specify the `--syntax d2` option to have dependency-graph output a graph using [the d2 scripting language](https://d2lang.com/tour/intro). 155 | 156 | The output be rendered to an image using the [the d2 cli](https://github.com/terrastruct/d2#install). 157 | 158 | ```bash 159 | curl -fsSL https://d2lang.com/install.sh | sh -s -- 160 | dependency-graph --syntax d2 ~/Developer/Example | d2 - graph.png 161 | ``` 162 | 163 | The ELK layout engine renders some quite tidy graphs, as shown in the example below. 164 | 165 | Example graph rendered with D2 and the ELK layout engine. 166 | 167 | ## Graphing Packages Only 168 | 169 | Pass the `--packages-only` flag to include only the Xcode project and Swift packages in the graph. This omits the libraries and targets within the Xcode project and Swift packages. 170 | 171 | Example graph showing only an Xcode project and Swift packages. 172 | 173 | ## 🤷‍♂️ OK, why? 174 | 175 | As I'm splitting my iOS and macOS applications into small Swift packages with several small targets, I started wishing for a way to visualise the relationship between the products and targets in my Swift packages. That's why I built this tool. 176 | 177 | Several other tools can visualise a Swift package, however, I wanted a tool that can take both a Swift package and an Xcode project as input. 178 | 179 | The example in the top of this README shows a visualization of a Swift package and the graph below shows a visualisation of an Xcode project. 180 | Notice that the left-most subgraph represents an Xcode project named ScriptUIEditor.xcodeproj and it has three targets: ScriptUIEditor, ScriptBrowserFeature, and ScriptBrowserFeatureUITests. Two of these depends on the Swift packages represented by the remaining subgraphs. 181 | 182 | These graphs provide a good way to get an overview of a package or the relationship between several packages. Sometimes it can be helpful to generate multiple graphs to get a good overview, for example, a graph of the entire project and graphs of selected packages. Fortunately, the `dependency-graph` CLI makes this easy as it can take either an Xcode project and a Package.swift file as input. 183 | 184 | ## 🧐 ...but how? 185 | 186 | dependency-graph parses Xcode project using [XcodeProj](https://github.com/tuist/XcodeProj) and interprets Package.swift files using the output from the `swift package dump-package` command. 187 | 188 | This means that dependency-graph does not perform any package resolution or build the project, making it very fast to run the `dependency-graph` command but also produces a less detailed output that tools that rely on package resolution. 189 | 190 | The tool has a focus on visualising local dependencies, that is, Swift packages stored locally in a project. dependency-graph will include remote dependencies in the visualisation but it will not clone those dependencies to determine their dependency graph. It is technically possible to include this but it has not been necessary for my use cases. 191 | -------------------------------------------------------------------------------- /Sources/Library/Commands/GraphCommand/DirectedGraphWriterFactory.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraphWriter 2 | 3 | public struct DirectedGraphWriterFactory { 4 | private let d2GraphWriter: any DirectedGraphWriter 5 | private let dotGraphWriter: any DirectedGraphWriter 6 | private let mermaidGraphWriter: any DirectedGraphWriter 7 | 8 | public init(d2GraphWriter: any DirectedGraphWriter, dotGraphWriter: any DirectedGraphWriter, mermaidGraphWriter: any DirectedGraphWriter) { 9 | self.d2GraphWriter = d2GraphWriter 10 | self.dotGraphWriter = dotGraphWriter 11 | self.mermaidGraphWriter = mermaidGraphWriter 12 | } 13 | 14 | func writer(for syntax: Syntax) -> any DirectedGraphWriter { 15 | switch syntax { 16 | case .d2: 17 | return d2GraphWriter 18 | case .dot: 19 | return dotGraphWriter 20 | case .mermaid: 21 | return mermaidGraphWriter 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Library/Commands/GraphCommand/GraphCommand.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraphMapper 2 | import Foundation 3 | import PackageGraphBuilder 4 | import PackageSwiftFileParser 5 | import ProjectRootClassifier 6 | import StdoutWriter 7 | import XcodeProjectGraphBuilder 8 | import XcodeProjectParser 9 | 10 | private enum GraphCommandError: LocalizedError { 11 | case unknownProject(URL) 12 | 13 | var errorDescription: String? { 14 | switch self { 15 | case .unknownProject(let fileURL): 16 | return "Unknown project at \(fileURL.path)" 17 | } 18 | } 19 | } 20 | 21 | public struct GraphCommand { 22 | private let projectRootClassifier: ProjectRootClassifier 23 | private let packageSwiftFileParser: PackageSwiftFileParser 24 | private let xcodeProjectParser: XcodeProjectParser 25 | private let packageGraphBuilder: PackageGraphBuilder 26 | private let xcodeProjectGraphBuilder: XcodeProjectGraphBuilder 27 | private let directedGraphWriterFactory: DirectedGraphWriterFactory 28 | 29 | public init(projectRootClassifier: ProjectRootClassifier, 30 | packageSwiftFileParser: PackageSwiftFileParser, 31 | xcodeProjectParser: XcodeProjectParser, 32 | packageGraphBuilder: PackageGraphBuilder, 33 | xcodeProjectGraphBuilder: XcodeProjectGraphBuilder, 34 | directedGraphWriterFactory: DirectedGraphWriterFactory) { 35 | self.projectRootClassifier = projectRootClassifier 36 | self.xcodeProjectParser = xcodeProjectParser 37 | self.packageSwiftFileParser = packageSwiftFileParser 38 | self.packageGraphBuilder = packageGraphBuilder 39 | self.xcodeProjectGraphBuilder = xcodeProjectGraphBuilder 40 | self.directedGraphWriterFactory = directedGraphWriterFactory 41 | } 42 | 43 | public func run(withInput input: String, syntax: Syntax) throws { 44 | let fileURL = NSURL.fileURL(withPath: input) 45 | let projectRoot = projectRootClassifier.classifyProject(at: fileURL) 46 | let directedGraphWriter = directedGraphWriterFactory.writer(for: syntax) 47 | switch projectRoot { 48 | case .xcodeproj(let xcodeprojFileURL): 49 | let xcodeProject = try xcodeProjectParser.parseProject(at: xcodeprojFileURL) 50 | let graph = try xcodeProjectGraphBuilder.buildGraph(from: xcodeProject) 51 | try directedGraphWriter.write(graph) 52 | case .packageSwiftFile(let packageSwiftFileURL): 53 | let packageSwiftFile = try packageSwiftFileParser.parseFile(at: packageSwiftFileURL) 54 | let graph = try packageGraphBuilder.buildGraph(from: packageSwiftFile) 55 | try directedGraphWriter.write(graph) 56 | case .unknown: 57 | throw GraphCommandError.unknownProject(fileURL) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Library/Commands/GraphCommand/Syntax.swift: -------------------------------------------------------------------------------- 1 | public enum Syntax: String { 2 | case d2 3 | case dot 4 | case mermaid 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/D2GraphMapper/D2GraphMapper.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphMapper 3 | import Foundation 4 | import StringIndentHelpers 5 | 6 | enum D2GraphMapperError: LocalizedError { 7 | case failedFindingClusterContainingNode(DirectedGraph.Node) 8 | 9 | var errorDescription: String? { 10 | switch self { 11 | case .failedFindingClusterContainingNode(let node): 12 | return "Could not find cluster containing node named '\(node.name)'" 13 | } 14 | } 15 | } 16 | 17 | public struct D2GraphMapper: DirectedGraphMapper { 18 | private let settings = D2GraphSettings() 19 | 20 | public init() {} 21 | 22 | public func map(_ graph: DirectedGraph) throws -> String { 23 | return try graph.stringRepresentation(withSettings: settings) 24 | } 25 | } 26 | 27 | extension DirectedGraph { 28 | func stringRepresentation(withSettings settings: D2GraphSettings) throws -> String { 29 | var lines: [String] = [] 30 | lines.append(settings.stringRepresentation) 31 | if !clusters.isEmpty { 32 | lines.append(clusters.stringRepresentation) 33 | } 34 | if !nodes.isEmpty { 35 | lines.append(nodes.stringRepresentation) 36 | } 37 | if !edges.isEmpty { 38 | lines.append(try edges.stringRepresentation(in: self)) 39 | } 40 | return lines.joined(separator: "\n\n") 41 | } 42 | } 43 | 44 | extension DirectedGraph.Cluster { 45 | var stringRepresentation: String { 46 | var lines = ["\(name): \(label) {"] 47 | lines += nodes.map(\.stringRepresentation).indented 48 | lines += ["}"] 49 | return lines.joined(separator: "\n") 50 | } 51 | } 52 | 53 | extension DirectedGraph.Node { 54 | var stringRepresentation: String { 55 | var lines = [name + ": " + label] 56 | switch shape { 57 | case .box: 58 | lines += [name + ".shape: rectangle"] 59 | case .ellipse: 60 | lines += [name + ".shape: oval"] 61 | } 62 | return lines.joined(separator: "\n") 63 | } 64 | } 65 | 66 | extension DirectedGraph.Edge { 67 | func stringRepresentation(in graph: DirectedGraph) throws -> String { 68 | func path(for node: DirectedGraph.Node) throws -> String { 69 | guard !graph.isRootNode(node) else { 70 | return node.name 71 | } 72 | guard let cluster = graph.cluster(containing: node) else { 73 | throw D2GraphMapperError.failedFindingClusterContainingNode(node) 74 | } 75 | // print(cluster.name + "." + node.name) 76 | return cluster.name + "." + node.name 77 | } 78 | return "\(try path(for: sourceNode)) -> \(try path(for: destinationNode))" 79 | } 80 | } 81 | 82 | extension Array where Element == DirectedGraph.Cluster { 83 | var stringRepresentation: String { 84 | return map { $0.stringRepresentation }.joined(separator: "\n\n") 85 | } 86 | } 87 | 88 | extension Array where Element == DirectedGraph.Node { 89 | var stringRepresentation: String { 90 | return map(\.stringRepresentation).joined(separator: "\n") 91 | } 92 | } 93 | 94 | extension Array where Element == DirectedGraph.Edge { 95 | func stringRepresentation(in graph: DirectedGraph) throws -> String { 96 | return try map { try $0.stringRepresentation(in: graph) }.joined(separator: "\n") 97 | } 98 | } 99 | 100 | private extension DirectedGraph { 101 | func isRootNode(_ node: DirectedGraph.Node) -> Bool { 102 | return nodes.contains(node) 103 | } 104 | 105 | func cluster(containing node: DirectedGraph.Node) -> DirectedGraph.Cluster? { 106 | return clusters.first { cluster in 107 | return cluster.nodes.contains { $0.name == node.name } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/D2GraphMapper/D2GraphSettings.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct D2GraphSettings { 4 | public let direction: String 5 | 6 | public init() { 7 | direction = "right" 8 | } 9 | } 10 | 11 | extension D2GraphSettings { 12 | var stringRepresentation: String { 13 | let lines = ["direction: \(direction)"] 14 | return lines.joined(separator: "\n") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/DOTGraphMapper/DOTGraphMapper.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphMapper 3 | import StringIndentHelpers 4 | 5 | public struct DOTGraphMapper: DirectedGraphMapper { 6 | private let settings: DOTGraphSettings 7 | 8 | public init(settings: DOTGraphSettings = DOTGraphSettings()) { 9 | self.settings = settings 10 | } 11 | 12 | public func map(_ graph: DirectedGraph) throws -> String { 13 | return graph.stringRepresentation(withSetings: settings) 14 | } 15 | } 16 | 17 | extension DirectedGraph { 18 | func stringRepresentation(withSetings settings: DOTGraphSettings) -> String { 19 | var graphBodyLines: [String] = [] 20 | graphBodyLines.append(settings.stringRepresentation) 21 | if !clusters.isEmpty { 22 | graphBodyLines.append(clusters.stringRepresentation) 23 | } 24 | if !nodes.isEmpty { 25 | graphBodyLines.append(nodes.stringRepresentation) 26 | } 27 | if !edges.isEmpty { 28 | graphBodyLines.append(edges.stringRepresentation) 29 | } 30 | let graphBody = graphBodyLines.indented.joined(separator: "\n\n") 31 | return ["digraph g {", graphBody, "}"].joined(separator: "\n") 32 | } 33 | } 34 | 35 | extension DirectedGraph.Cluster { 36 | var stringRepresentation: String { 37 | var lines = ["subgraph cluster_\(name) {"] 38 | lines += ["label=\"\(label)\""].indented 39 | lines += nodes.map(\.stringRepresentation).indented 40 | lines += ["}"] 41 | return lines.joined(separator: "\n") 42 | } 43 | } 44 | 45 | extension DirectedGraph.Node { 46 | var stringRepresentation: String { 47 | var settings: [String] = ["label=\"\(label)\""] 48 | switch shape { 49 | case .ellipse: 50 | settings += ["shape=ellipse"] 51 | case .box: 52 | settings += ["shape=box"] 53 | } 54 | return name + " [" + settings.joined(separator: ", ") + "]" 55 | } 56 | } 57 | 58 | extension DirectedGraph.Edge { 59 | var stringRepresentation: String { 60 | return "\(sourceNode.name) -> \(destinationNode.name)" 61 | } 62 | } 63 | 64 | extension Array where Element == DirectedGraph.Cluster { 65 | var stringRepresentation: String { 66 | return map(\.stringRepresentation).joined(separator: "\n\n") 67 | } 68 | } 69 | 70 | extension Array where Element == DirectedGraph.Node { 71 | var stringRepresentation: String { 72 | return map(\.stringRepresentation).joined(separator: "\n") 73 | } 74 | } 75 | 76 | extension Array where Element == DirectedGraph.Edge { 77 | var stringRepresentation: String { 78 | return map(\.stringRepresentation).joined(separator: "\n") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/DOTGraphMapper/DOTGraphSettings.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct DOTGraphSettings { 4 | public let layout: String 5 | public let rankdir: String 6 | public let nodesep: Float? 7 | public let ranksep: Float? 8 | 9 | public init(nodesep: Float? = nil, ranksep: Float? = nil) { 10 | self.layout = "dot" 11 | self.rankdir = "LR" 12 | self.nodesep = nodesep 13 | self.ranksep = ranksep 14 | } 15 | } 16 | 17 | extension DOTGraphSettings { 18 | var stringRepresentation: String { 19 | var lines = [ 20 | "layout=\(layout)", 21 | "rankdir=\(rankdir)" 22 | ] 23 | if let nodesep = nodesep { 24 | lines += ["nodesep=\(nodesep)"] 25 | } 26 | if let ranksep = ranksep { 27 | lines += ["ranksep=\(ranksep)"] 28 | } 29 | return lines.joined(separator: "\n") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/DirectedGraph/DirectedGraph+Cluster.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension DirectedGraph { 4 | final class Cluster: Equatable { 5 | public let name: String 6 | public let label: String 7 | public private(set) var nodes: [Node] 8 | 9 | public init(name: String, label: String, nodes: [Node] = []) { 10 | self.name = name 11 | self.label = label 12 | self.nodes = nodes 13 | } 14 | 15 | public func node(named name: String) -> Node? { 16 | return nodes.first { $0.name == name } 17 | } 18 | 19 | @discardableResult 20 | public func addUniqueNode(_ node: Node) -> Node { 21 | if let node = self.node(named: node.name) { 22 | return node 23 | } else { 24 | nodes.append(node) 25 | return node 26 | } 27 | } 28 | 29 | public static func == (lhs: Cluster, rhs: Cluster) -> Bool { 30 | return lhs.label == rhs.label && lhs.nodes == rhs.nodes 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/DirectedGraph/DirectedGraph+Edge.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension DirectedGraph { 4 | final class Edge: Equatable { 5 | public let sourceNode: Node 6 | public let destinationNode: Node 7 | 8 | public init(from sourceNode: Node, to destinationNode: Node) { 9 | self.sourceNode = sourceNode 10 | self.destinationNode = destinationNode 11 | } 12 | 13 | public static func from(_ sourceNode: Node, to destinationNode: Node) -> Edge { 14 | return Self(from: sourceNode, to: destinationNode) 15 | } 16 | 17 | public static func == (lhs: Edge, rhs: Edge) -> Bool { 18 | return lhs.sourceNode == rhs.sourceNode && lhs.destinationNode == rhs.destinationNode 19 | } 20 | } 21 | } 22 | 23 | extension DirectedGraph.Edge: CustomDebugStringConvertible { 24 | public var debugDescription: String { 25 | return "\(sourceNode.name) -> \(destinationNode.name)" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/DirectedGraph/DirectedGraph+Node.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension DirectedGraph { 4 | final class Node: Equatable { 5 | public enum Shape { 6 | case box 7 | case ellipse 8 | } 9 | 10 | public let name: String 11 | public let label: String 12 | public let shape: Shape 13 | 14 | public init(name: String, label: String, shape: Shape = .box) { 15 | self.name = name 16 | self.label = label 17 | self.shape = shape 18 | } 19 | 20 | public static func == (lhs: Node, rhs: Node) -> Bool { 21 | return lhs.name == rhs.name && lhs.label == rhs.label && lhs.shape == rhs.shape 22 | } 23 | } 24 | } 25 | 26 | extension DirectedGraph.Node: CustomDebugStringConvertible { 27 | public var debugDescription: String { 28 | return "\(name) [label=\(label), shape=\(shape.title)]" 29 | } 30 | } 31 | 32 | private extension DirectedGraph.Node.Shape { 33 | var title: String { 34 | switch self { 35 | case .box: 36 | return "box" 37 | case .ellipse: 38 | return "ellipse" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/DirectedGraph/DirectedGraph.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class DirectedGraph: Equatable { 4 | public private(set) var clusters: [Cluster] 5 | public private(set) var nodes: [Node] 6 | public private(set) var edges: [Edge] 7 | 8 | public init(clusters: [Cluster] = [], nodes: [Node] = [], edges: [Edge] = []) { 9 | self.clusters = clusters 10 | self.nodes = nodes 11 | self.edges = edges 12 | } 13 | 14 | @discardableResult 15 | public func addUniqueNode(_ node: Node) -> Node { 16 | if let node = self.node(named: node.name) { 17 | return node 18 | } else { 19 | nodes.append(node) 20 | return node 21 | } 22 | } 23 | 24 | @discardableResult 25 | public func addUniqueCluster(_ cluster: Cluster) -> Cluster { 26 | if let existingCluster = clusters.first(where: { $0.name == cluster.name }) { 27 | for node in cluster.nodes { 28 | existingCluster.addUniqueNode(node) 29 | } 30 | return existingCluster 31 | } else { 32 | clusters.append(cluster) 33 | return cluster 34 | } 35 | } 36 | 37 | @discardableResult 38 | public func addUniqueEdge(_ edge: Edge) -> Edge { 39 | if let edge = edges.first(where: { $0.sourceNode == edge.sourceNode && $0.destinationNode == edge.destinationNode }) { 40 | return edge 41 | } else { 42 | edges.append(edge) 43 | return edge 44 | } 45 | } 46 | 47 | public func node(named name: String) -> Node? { 48 | if let node = nodes.first(where: { $0.name == name }) { 49 | return node 50 | } 51 | for cluster in clusters { 52 | if let node = cluster.node(named: name) { 53 | return node 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | public func union(_ graph: DirectedGraph) { 60 | for node in graph.nodes { 61 | addUniqueNode(node) 62 | } 63 | for cluster in graph.clusters { 64 | addUniqueCluster(cluster) 65 | } 66 | for edge in graph.edges { 67 | addUniqueEdge(edge) 68 | } 69 | } 70 | 71 | public static func == (lhs: DirectedGraph, rhs: DirectedGraph) -> Bool { 72 | return lhs.clusters == rhs.clusters && lhs.nodes == rhs.nodes && lhs.edges == rhs.edges 73 | } 74 | } 75 | 76 | extension DirectedGraph: CustomDebugStringConvertible { 77 | public var debugDescription: String { 78 | var lines = ["DirectedGraph = ("] 79 | lines += [" Clusters = ("] + clusters.flatMap { cluster in 80 | let clusterString = " □ \(cluster.label) (\(cluster.name))" 81 | let nodesStrings = cluster.nodes.map { " ○ \($0)" } 82 | return [clusterString, " Nodes = ("] + nodesStrings + [" )"] 83 | } 84 | lines += [" )"] 85 | lines += [" Nodes = ("] + nodes.map { " ○ \($0)" } + [" )"] 86 | lines += [" Edges = ("] + edges.map { " - \($0)" } + [" )"] 87 | lines += [")"] 88 | return lines.joined(separator: "\n") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/DirectedGraphMapper/DirectedGraphMapper.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | 3 | public protocol DirectedGraphMapper { 4 | associatedtype OutputType 5 | func map(_ graph: DirectedGraph) throws -> OutputType 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/DirectedGraphXcodeHelpers/DirectedGraph+XcodeHelpers.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | 3 | public extension DirectedGraph { 4 | func packageNode(labeled label: String) -> DirectedGraph.Node? { 5 | return node(named: NodeName.package(label)) 6 | } 7 | 8 | func packageProductNode(labeled label: String) -> DirectedGraph.Node? { 9 | return node(named: NodeName.packageProduct(label)) 10 | } 11 | 12 | func targetNode(labeled label: String) -> DirectedGraph.Node? { 13 | return node(named: NodeName.target(label)) 14 | } 15 | 16 | @discardableResult 17 | func addProjectCluster(labeled label: String) -> DirectedGraph.Cluster { 18 | return addUniqueCluster(.project(labeled: label)) 19 | } 20 | 21 | @discardableResult 22 | func addPackageCluster(labeled label: String) -> DirectedGraph.Cluster { 23 | return addUniqueCluster(.package(labeled: label)) 24 | } 25 | } 26 | 27 | public extension DirectedGraph.Cluster { 28 | static func project(labeled label: String, nodes: [DirectedGraph.Node] = []) -> DirectedGraph.Cluster { 29 | return Self(name: ClusterName.project(label), label: label, nodes: nodes) 30 | } 31 | 32 | static func package(labeled label: String, nodes: [DirectedGraph.Node] = []) -> DirectedGraph.Cluster { 33 | return Self(name: ClusterName.package(label), label: label, nodes: nodes) 34 | } 35 | } 36 | 37 | public extension DirectedGraph.Node { 38 | static func project(labeled label: String) -> DirectedGraph.Node { 39 | return Self(name: NodeName.project(label), label: label, shape: .ellipse) 40 | } 41 | 42 | static func package(labeled label: String) -> DirectedGraph.Node { 43 | return Self(name: NodeName.package(label), label: label, shape: .ellipse) 44 | } 45 | 46 | static func packageProduct(labeled label: String) -> DirectedGraph.Node { 47 | return Self(name: NodeName.packageProduct(label), label: label, shape: .ellipse) 48 | } 49 | 50 | static func target(labeled label: String) -> DirectedGraph.Node { 51 | return Self(name: NodeName.target(label), label: label, shape: .box) 52 | } 53 | } 54 | 55 | private enum ClusterName { 56 | static func project(_ string: String) -> String { 57 | return "project_" + string.safeName 58 | } 59 | 60 | static func package(_ string: String) -> String { 61 | return "package_" + string.safeName 62 | } 63 | } 64 | 65 | private enum NodeName { 66 | static func project(_ string: String) -> String { 67 | return "project_" + string.safeName 68 | } 69 | 70 | static func package(_ string: String) -> String { 71 | return "package_" + string.safeName 72 | } 73 | 74 | static func packageProduct(_ string: String) -> String { 75 | return "packageProduct_" + string.safeName 76 | } 77 | 78 | static func target(_ string: String) -> String { 79 | return "target_" + string.safeName 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/DirectedGraphXcodeHelpers/String+SafeName.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | var safeName: String { 5 | return components(separatedBy: .alphanumerics.inverted).joined() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/MermaidGraphMapper/MermaidGraphMapper.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphMapper 3 | import StringIndentHelpers 4 | 5 | public struct MermaidGraphMapper: DirectedGraphMapper { 6 | private let settings: MermaidGraphSettings 7 | 8 | public init(settings: MermaidGraphSettings = MermaidGraphSettings()) { 9 | self.settings = settings 10 | } 11 | 12 | public func map(_ graph: DirectedGraph) throws -> String { 13 | return graph.stringRepresentation(withSettings: settings) 14 | } 15 | } 16 | 17 | extension DirectedGraph { 18 | func stringRepresentation(withSettings settings: MermaidGraphSettings) -> String { 19 | var graphBodyLines: [String] = [] 20 | graphBodyLines.append(settings.stringRepresentation) 21 | if !clusters.isEmpty { 22 | graphBodyLines.append(clusters.stringRepresentation(withSettings: settings)) 23 | } 24 | if !nodes.isEmpty { 25 | graphBodyLines.append(nodes.stringRepresentation(withSettings: settings)) 26 | } 27 | if !edges.isEmpty { 28 | graphBodyLines.append(edges.stringRepresentation) 29 | } 30 | let graphBody = graphBodyLines.indented.joined(separator: "\n\n") 31 | return ["graph LR", graphBody].joined(separator: "\n") 32 | } 33 | } 34 | 35 | extension DirectedGraph.Cluster { 36 | func stringRepresentation(withSettings settings: MermaidGraphSettings) -> String { 37 | var lines = ["subgraph \(name)[\(label)]"] 38 | lines += [settings.stringRepresentation.indented] 39 | lines += nodes.map(\.stringRepresentation).indented 40 | lines += ["end"] 41 | return lines.joined(separator: "\n") 42 | } 43 | } 44 | 45 | extension DirectedGraph.Node { 46 | var stringRepresentation: String { 47 | switch shape { 48 | case .box: 49 | return name + "[" + label + "]" 50 | case .ellipse: 51 | return name + "([" + label + "])" 52 | } 53 | } 54 | } 55 | 56 | extension DirectedGraph.Edge { 57 | var stringRepresentation: String { 58 | return "\(sourceNode.name) --> \(destinationNode.name)" 59 | } 60 | } 61 | 62 | extension Array where Element == DirectedGraph.Cluster { 63 | func stringRepresentation(withSettings settings: MermaidGraphSettings) -> String { 64 | return map { $0.stringRepresentation(withSettings: settings) }.joined(separator: "\n\n") 65 | } 66 | } 67 | 68 | extension Array where Element == DirectedGraph.Node { 69 | func stringRepresentation(withSettings settings: MermaidGraphSettings) -> String { 70 | return map(\.stringRepresentation).joined(separator: "\n") 71 | } 72 | } 73 | 74 | extension Array where Element == DirectedGraph.Edge { 75 | var stringRepresentation: String { 76 | return map(\.stringRepresentation).joined(separator: "\n") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/MermaidGraphMapper/MermaidGraphSettings.swift: -------------------------------------------------------------------------------- 1 | public struct MermaidGraphSettings { 2 | public let nodeSpacing: Int? 3 | public let rankSpacing: Int? 4 | 5 | public init(nodeSpacing: Int? = nil, rankSpacing: Int? = nil) { 6 | self.nodeSpacing = nodeSpacing 7 | self.rankSpacing = rankSpacing 8 | } 9 | } 10 | 11 | extension MermaidGraphSettings { 12 | var stringRepresentation: String { 13 | var flowchartSettings: [String] = [] 14 | if let nodeSpacing = nodeSpacing { 15 | flowchartSettings += ["'nodeSpacing': \(nodeSpacing)"] 16 | } 17 | if let rankSpacing = rankSpacing { 18 | flowchartSettings += ["'rankSpacing': \(rankSpacing)"] 19 | } 20 | return "%%{init:{'flowchart':{\(flowchartSettings.joined(separator: ", "))}}}%%" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/PackageGraphBuilder/PackageGraphBuilder.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import PackageSwiftFile 3 | 4 | public protocol PackageGraphBuilder { 5 | func buildGraph(from packageSwiftFile: PackageSwiftFile) throws -> DirectedGraph 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/PackageGraphBuilderLive/Internal/AllDependenciesGraphBuilder.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphXcodeHelpers 3 | import PackageGraphBuilder 4 | import PackageSwiftFile 5 | 6 | enum AllDependenciesGraphBuilder { 7 | static func buildGraph(from packageSwiftFile: PackageSwiftFile) throws -> DirectedGraph { 8 | let dependencyGraphs = try packageSwiftFile.dependencies.compactMap(graph(from:)) 9 | let graph = DirectedGraph() 10 | for dependencyGraph in dependencyGraphs { 11 | graph.union(dependencyGraph) 12 | } 13 | let cluster = graph.addPackageCluster(labeled: packageSwiftFile.name) 14 | for product in packageSwiftFile.products { 15 | let productNode = cluster.addUniqueNode(.packageProduct(labeled: product.name)) 16 | for target in product.targets { 17 | let targetNode = cluster.addUniqueNode(.target(labeled: target)) 18 | graph.addUniqueEdge(.from(productNode, to: targetNode)) 19 | } 20 | } 21 | for target in packageSwiftFile.targets { 22 | let targetNode = cluster.addUniqueNode(.target(labeled: target.name)) 23 | for dependency in target.dependencies { 24 | switch dependency { 25 | case .name(let parameters): 26 | if let dependencyTargetNode = dependencyGraphs.findNode({ $0.targetNode(labeled: parameters.name) }) { 27 | graph.addUniqueEdge(.from(targetNode, to: dependencyTargetNode)) 28 | } else { 29 | let dependencyTargetNode = cluster.addUniqueNode(.target(labeled: parameters.name)) 30 | graph.addUniqueEdge(.from(targetNode, to: dependencyTargetNode)) 31 | } 32 | case .productInPackage(let parameters): 33 | let packageClusterNode = graph.addPackageCluster(labeled: parameters.packageName) 34 | let dependencyProductNode = packageClusterNode.addUniqueNode(.packageProduct(labeled: parameters.name)) 35 | graph.addUniqueEdge(.from(targetNode, to: dependencyProductNode)) 36 | } 37 | } 38 | } 39 | return graph 40 | } 41 | } 42 | 43 | private extension AllDependenciesGraphBuilder { 44 | private static func graph(from dependency: PackageSwiftFile.Dependency) throws -> DirectedGraph? { 45 | switch dependency { 46 | case .fileSystem(let parameters): 47 | return try buildGraph(from: parameters.packageSwiftFile) 48 | case .sourceControl: 49 | return nil 50 | } 51 | } 52 | } 53 | 54 | private extension Array where Element == DirectedGraph { 55 | func findNode(_ f: (DirectedGraph) -> DirectedGraph.Node?) -> DirectedGraph.Node? { 56 | for graph in self { 57 | if let node = f(graph) { 58 | return node 59 | } 60 | } 61 | return nil 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/PackageGraphBuilderLive/Internal/PackagesOnlyGraphBuilder.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphXcodeHelpers 3 | import PackageGraphBuilder 4 | import PackageSwiftFile 5 | 6 | enum PackagesOnlyGraphBuilder { 7 | static func buildGraph(from packageSwiftFile: PackageSwiftFile) throws -> DirectedGraph { 8 | let graph = DirectedGraph() 9 | let node = graph.addUniqueNode(.package(labeled: packageSwiftFile.name)) 10 | for dependency in packageSwiftFile.dependencies { 11 | switch dependency { 12 | case .fileSystem(let parameters): 13 | let subgraph = try buildGraph(from: parameters.packageSwiftFile) 14 | graph.union(subgraph) 15 | if let dependencyNode = graph.packageNode(labeled: parameters.packageSwiftFile.name) { 16 | graph.addUniqueEdge(.from(node, to: dependencyNode)) 17 | } else { 18 | throw PackageGraphBuilderLiveError.dependencyNotFound( 19 | dependency: parameters.packageSwiftFile.name, 20 | dependant: packageSwiftFile.name 21 | ) 22 | } 23 | case .sourceControl: 24 | break 25 | } 26 | } 27 | return graph 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/PackageGraphBuilderLive/PackageGraphBuilderLive.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphXcodeHelpers 3 | import Foundation 4 | import PackageGraphBuilder 5 | import PackageSwiftFile 6 | 7 | public enum PackageGraphBuilderLiveError: LocalizedError { 8 | case dependencyNotFound(dependency: String, dependant: String) 9 | 10 | public var errorDescription: String? { 11 | switch self { 12 | case let .dependencyNotFound(dependency, dependant): 13 | return "\(dependant) depends on \(dependency) but the dependency was not found in the graph." 14 | } 15 | } 16 | } 17 | 18 | public struct PackageGraphBuilderLive: PackageGraphBuilder { 19 | private let packagesOnly: Bool 20 | 21 | public init(packagesOnly: Bool) { 22 | self.packagesOnly = packagesOnly 23 | } 24 | 25 | public func buildGraph(from packageSwiftFile: PackageSwiftFile) throws -> DirectedGraph { 26 | if packagesOnly { 27 | return try PackagesOnlyGraphBuilder.buildGraph(from: packageSwiftFile) 28 | } else { 29 | return try AllDependenciesGraphBuilder.buildGraph(from: packageSwiftFile) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/XcodeProjectGraphBuilder/XcodeProjectGraphBuilder.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import XcodeProject 3 | 4 | public protocol XcodeProjectGraphBuilder { 5 | func buildGraph(from xcodeProject: XcodeProject) throws -> DirectedGraph 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/XcodeProjectGraphBuilderLive/Internal/AllDependenciesGraphBuilder.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphXcodeHelpers 3 | import Foundation 4 | import PackageGraphBuilder 5 | import PackageSwiftFile 6 | import PackageSwiftFileParser 7 | import XcodeProject 8 | import XcodeProjectGraphBuilder 9 | 10 | public struct AllDependenciesGraphBuilder { 11 | private let packageSwiftFileParser: PackageSwiftFileParser 12 | private let packageGraphBuilder: PackageGraphBuilder 13 | 14 | public init(packageSwiftFileParser: PackageSwiftFileParser, packageGraphBuilder: PackageGraphBuilder) { 15 | self.packageSwiftFileParser = packageSwiftFileParser 16 | self.packageGraphBuilder = packageGraphBuilder 17 | } 18 | 19 | public func buildGraph(from xcodeProject: XcodeProject) throws -> DirectedGraph { 20 | let graph = DirectedGraph() 21 | for swiftPackage in xcodeProject.swiftPackages { 22 | try process(swiftPackage, into: graph) 23 | } 24 | let projectCluster = graph.addProjectCluster(labeled: xcodeProject.name) 25 | for target in xcodeProject.targets { 26 | let targetNode = projectCluster.addUniqueNode(.target(labeled: target.name)) 27 | for dependency in target.packageProductDependencies { 28 | if let destinationNode = graph.packageProductNode(labeled: dependency) { 29 | graph.addUniqueEdge(.from(targetNode, to: destinationNode)) 30 | } else { 31 | throw XcodeProjectGraphBuilderLiveError.dependencyNotFound(dependency: dependency, dependant: target.name) 32 | } 33 | } 34 | } 35 | return graph 36 | } 37 | } 38 | 39 | private extension AllDependenciesGraphBuilder { 40 | private func process(_ swiftPackage: XcodeProject.SwiftPackage, into graph: DirectedGraph) throws { 41 | switch swiftPackage { 42 | case .local(let parameters): 43 | let packageSwiftFile = try packageSwiftFileParser.parseFile(at: parameters.fileURL) 44 | let childGraph = try packageGraphBuilder.buildGraph(from: packageSwiftFile) 45 | graph.union(childGraph) 46 | case .remote(let parameters): 47 | let cluster = graph.addPackageCluster(labeled: parameters.name) 48 | for product in parameters.products { 49 | cluster.addUniqueNode(.packageProduct(labeled: product)) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/XcodeProjectGraphBuilderLive/Internal/PackagesOnlyGraphBuilder.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphXcodeHelpers 3 | import Foundation 4 | import PackageGraphBuilder 5 | import PackageSwiftFile 6 | import PackageSwiftFileParser 7 | import XcodeProject 8 | import XcodeProjectGraphBuilder 9 | 10 | public struct PackagesOnlyGraphBuilder { 11 | private let packageSwiftFileParser: PackageSwiftFileParser 12 | private let packageGraphBuilder: PackageGraphBuilder 13 | 14 | public init(packageSwiftFileParser: PackageSwiftFileParser, packageGraphBuilder: PackageGraphBuilder) { 15 | self.packageSwiftFileParser = packageSwiftFileParser 16 | self.packageGraphBuilder = packageGraphBuilder 17 | } 18 | 19 | public func buildGraph(from xcodeProject: XcodeProject) throws -> DirectedGraph { 20 | let graph = DirectedGraph() 21 | let node = graph.addUniqueNode(.project(labeled: xcodeProject.name)) 22 | for swiftPackage in xcodeProject.swiftPackages { 23 | switch swiftPackage { 24 | case .local(let parameters): 25 | let packageSwiftFile = try packageSwiftFileParser.parseFile(at: parameters.fileURL) 26 | let childGraph = try packageGraphBuilder.buildGraph(from: packageSwiftFile) 27 | graph.union(childGraph) 28 | if let dependencyNode = graph.packageNode(labeled: parameters.name) { 29 | graph.addUniqueEdge(.from(node, to: dependencyNode)) 30 | } else { 31 | throw XcodeProjectGraphBuilderLiveError.dependencyNotFound(dependency: parameters.name, dependant: xcodeProject.name) 32 | } 33 | case .remote(let parameters): 34 | let dependencyNode = graph.addUniqueNode(.package(labeled: parameters.name)) 35 | graph.addUniqueEdge(.from(node, to: dependencyNode)) 36 | } 37 | } 38 | return graph 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Library/Graphing/XcodeProjectGraphBuilderLive/XcodeProjectGraphBuilderLive.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphXcodeHelpers 3 | import Foundation 4 | import PackageGraphBuilder 5 | import PackageSwiftFile 6 | import PackageSwiftFileParser 7 | import XcodeProject 8 | import XcodeProjectGraphBuilder 9 | 10 | public enum XcodeProjectGraphBuilderLiveError: LocalizedError { 11 | case dependencyNotFound(dependency: String, dependant: String) 12 | 13 | public var errorDescription: String? { 14 | switch self { 15 | case let .dependencyNotFound(dependency, dependant): 16 | return "\(dependant) depends on \(dependency) but the dependency was not found in the graph." 17 | } 18 | } 19 | } 20 | 21 | public final class XcodeProjectGraphBuilderLive: XcodeProjectGraphBuilder { 22 | private let packageSwiftFileParser: PackageSwiftFileParser 23 | private let packageGraphBuilder: PackageGraphBuilder 24 | private let packagesOnly: Bool 25 | 26 | public init(packageSwiftFileParser: PackageSwiftFileParser, packageGraphBuilder: PackageGraphBuilder, packagesOnly: Bool) { 27 | self.packageSwiftFileParser = packageSwiftFileParser 28 | self.packageGraphBuilder = packageGraphBuilder 29 | self.packagesOnly = packagesOnly 30 | } 31 | 32 | public func buildGraph(from xcodeProject: XcodeProject) throws -> DirectedGraph { 33 | if packagesOnly { 34 | let graphBuilder = PackagesOnlyGraphBuilder(packageSwiftFileParser: packageSwiftFileParser, packageGraphBuilder: packageGraphBuilder) 35 | return try graphBuilder.buildGraph(from: xcodeProject) 36 | } else { 37 | let graphBuilder = AllDependenciesGraphBuilder(packageSwiftFileParser: packageSwiftFileParser, packageGraphBuilder: packageGraphBuilder) 38 | return try graphBuilder.buildGraph(from: xcodeProject) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Library/Outputting/DirectedGraphWriter/DirectedGraphWriter.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import Writer 3 | 4 | public protocol DirectedGraphWriter: Writer { 5 | func write(_ directedGraph: DirectedGraph) throws 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Library/Outputting/MappingDirectedGraphWriter/MappingDirectedGraphWriter.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphMapper 3 | import DirectedGraphWriter 4 | import Writer 5 | 6 | public struct MappingDirectedGraphWriter: DirectedGraphWriter { 7 | private let _write: (DirectedGraph) throws -> Void 8 | 9 | public init(mapper: M, writer: W) where M.OutputType == W.Input { 10 | _write = { try writer.write(try mapper.map($0)) } 11 | } 12 | 13 | public func write(_ directedGraph: DirectedGraph) throws { 14 | try _write(directedGraph) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Library/Outputting/StdoutWriter/StdoutWriter.swift: -------------------------------------------------------------------------------- 1 | import Writer 2 | 3 | public struct StdoutWriter: Writer { 4 | public init() {} 5 | 6 | public func write(_ string: String) { 7 | print(string) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Library/Outputting/Writer/Writer.swift: -------------------------------------------------------------------------------- 1 | public protocol Writer { 2 | associatedtype Input 3 | func write(_ input: Input) throws 4 | } 5 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/DumpPackageService/DumpPackageService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol DumpPackageService { 4 | func dumpPackageForSwiftPackageFile(at fileURL: URL) throws -> Data 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/DumpPackageServiceLive/DumpPackageServiceLive.swift: -------------------------------------------------------------------------------- 1 | import DumpPackageService 2 | import Foundation 3 | import ShellCommandRunner 4 | 5 | public enum DumpPackageServiceLiveError: LocalizedError { 6 | case invalidPackageSwiftFileURL 7 | case unexpectedTerminationStatus(Int32) 8 | case failedConvertingStringToData 9 | 10 | public var errorDescription: String? { 11 | switch self { 12 | case .invalidPackageSwiftFileURL: 13 | return "Expected URL to a Package.swift file." 14 | case .unexpectedTerminationStatus(let status): 15 | return "Unexpected termination status \(status)." 16 | case .failedConvertingStringToData: 17 | return "Failed converting string to data." 18 | } 19 | } 20 | } 21 | 22 | public struct DumpPackageServiceLive: DumpPackageService { 23 | private let shellCommandRunner: ShellCommandRunner 24 | 25 | public init(shellCommandRunner: ShellCommandRunner) { 26 | self.shellCommandRunner = shellCommandRunner 27 | } 28 | 29 | public func dumpPackageForSwiftPackageFile(at fileURL: URL) throws -> Data { 30 | guard fileURL.lastPathComponent == "Package.swift" else { 31 | throw DumpPackageServiceLiveError.invalidPackageSwiftFileURL 32 | } 33 | let directoryURL = fileURL.deletingLastPathComponent() 34 | return try dumpPackage(at: directoryURL) 35 | } 36 | } 37 | 38 | private extension DumpPackageServiceLive { 39 | private func dumpPackage(at directoryURL: URL) throws -> Data { 40 | let output = shellCommandRunner.run(withArguments: ["swift", "package", "dump-package"], fromDirectoryURL: directoryURL) 41 | guard output.status == 0 else { 42 | throw DumpPackageServiceLiveError.unexpectedTerminationStatus(output.status) 43 | } 44 | guard let data = output.message.data(using: .utf8) else { 45 | throw DumpPackageServiceLiveError.failedConvertingStringToData 46 | } 47 | return data 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFile/PackageSwiftFile+Dependency.swift: -------------------------------------------------------------------------------- 1 | extension PackageSwiftFile { 2 | public enum Dependency: Equatable { 3 | public struct SourceControlParameters: Equatable { 4 | public let identity: String 5 | 6 | public init(identity: String) { 7 | self.identity = identity 8 | } 9 | } 10 | 11 | public struct FileSystemParameters: Equatable { 12 | public let identity: String 13 | public let path: String 14 | public let packageSwiftFile: PackageSwiftFile 15 | 16 | public init(identity: String, path: String, packageSwiftFile: PackageSwiftFile) { 17 | self.identity = identity 18 | self.path = path 19 | self.packageSwiftFile = packageSwiftFile 20 | } 21 | } 22 | 23 | case sourceControl(SourceControlParameters) 24 | case fileSystem(FileSystemParameters) 25 | 26 | public static func sourceControl(identity: String) -> Self { 27 | let parameters = SourceControlParameters(identity: identity) 28 | return .sourceControl(parameters) 29 | } 30 | 31 | public static func fileSystem(identity: String, path: String, packageSwiftFile: PackageSwiftFile) -> Self { 32 | let parameters = FileSystemParameters(identity: identity, path: path, packageSwiftFile: packageSwiftFile) 33 | return .fileSystem(parameters) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFile/PackageSwiftFile+Product.swift: -------------------------------------------------------------------------------- 1 | public extension PackageSwiftFile { 2 | struct Product: Equatable { 3 | public let name: String 4 | public let targets: [String] 5 | 6 | public init(name: String, targets: [String] = []) { 7 | self.name = name 8 | self.targets = targets 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFile/PackageSwiftFile+Target+Dependency.swift: -------------------------------------------------------------------------------- 1 | public extension PackageSwiftFile.Target { 2 | enum Dependency: Equatable { 3 | public struct NameParameters: Equatable { 4 | public let name: String 5 | 6 | public init(name: String) { 7 | self.name = name 8 | } 9 | } 10 | 11 | public struct ProductInPackageParameters: Equatable { 12 | public let name: String 13 | public let packageName: String 14 | 15 | public init(name: String, packageName: String) { 16 | self.name = name 17 | self.packageName = packageName 18 | } 19 | } 20 | 21 | case name(NameParameters) 22 | case productInPackage(ProductInPackageParameters) 23 | 24 | public static func name(_ name: String) -> Self { 25 | return .name(.init(name: name)) 26 | } 27 | 28 | public static func product(_ name: String, inPackage package: String) -> Self { 29 | return .productInPackage(.init(name: name, packageName: package)) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFile/PackageSwiftFile+Target.swift: -------------------------------------------------------------------------------- 1 | public extension PackageSwiftFile { 2 | struct Target: Equatable { 3 | public let name: String 4 | public let dependencies: [Dependency] 5 | 6 | public init(name: String, dependencies: [Dependency] = []) { 7 | self.name = name 8 | self.dependencies = dependencies 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFile/PackageSwiftFile.swift: -------------------------------------------------------------------------------- 1 | public struct PackageSwiftFile: Equatable { 2 | public let name: String 3 | public let products: [Product] 4 | public let targets: [Target] 5 | public let dependencies: [Dependency] 6 | 7 | public init(name: String, products: [Product] = [], targets: [Target] = [], dependencies: [Dependency] = []) { 8 | self.name = name 9 | self.products = products 10 | self.targets = targets 11 | self.dependencies = dependencies 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParser/PackageSwiftFileParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackageSwiftFile 3 | 4 | public protocol PackageSwiftFileParser { 5 | func parseFile(at fileURL: URL) throws -> PackageSwiftFile 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParserCache/PackageSwiftFileParserCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackageSwiftFile 3 | 4 | public protocol PackageSwiftFileParserCache { 5 | func cache(_ packageSwiftFile: PackageSwiftFile, for url: URL) 6 | func cachedPackageSwiftFile(for url: URL) -> PackageSwiftFile? 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParserCacheLive/PackageSwiftFileParserCacheLive.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackageSwiftFile 3 | import PackageSwiftFileParserCache 4 | 5 | public final class PackageSwiftFileParserCacheLive: PackageSwiftFileParserCache { 6 | private var values: [URL: PackageSwiftFile] = [:] 7 | 8 | public init() {} 9 | 10 | public func cache(_ packageSwiftFile: PackageSwiftFile, for url: URL) { 11 | values[url] = packageSwiftFile 12 | } 13 | 14 | public func cachedPackageSwiftFile(for url: URL) -> PackageSwiftFile? { 15 | return values[url] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParserLive/Internal/IntermediatePackageSwiftFile+Dependency.swift: -------------------------------------------------------------------------------- 1 | extension IntermediatePackageSwiftFile { 2 | enum Dependency: Decodable { 3 | private enum CodingKeys: String, CodingKey { 4 | case sourceControl 5 | case fileSystem 6 | } 7 | 8 | struct SourceControlParameters: Decodable { 9 | let identity: String 10 | } 11 | 12 | struct FileSystemParameters: Decodable { 13 | let identity: String 14 | let path: String 15 | } 16 | 17 | case sourceControl(SourceControlParameters) 18 | case fileSystem(FileSystemParameters) 19 | 20 | init(from decoder: Decoder) throws { 21 | let container = try decoder.container(keyedBy: CodingKeys.self) 22 | if container.allKeys.contains(CodingKeys.sourceControl) { 23 | self = try .decodeSourceControl(using: container) 24 | } else if container.allKeys.contains(CodingKeys.fileSystem) { 25 | self = try .decodeFileSystem(using: container) 26 | } else { 27 | let keysString = container.allKeys.map(\.stringValue).joined(separator: ", ") 28 | let debugDescription = "Unsupported keys: " + keysString 29 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 30 | } 31 | } 32 | } 33 | } 34 | 35 | private extension IntermediatePackageSwiftFile.Dependency { 36 | private static func decodeSourceControl(using container: KeyedDecodingContainer) throws -> Self { 37 | let parametersContainer = try container.decode([SourceControlParameters].self, forKey: .sourceControl) 38 | guard parametersContainer.count == 1 else { 39 | let debugDescription = "Expected to decode exactly 1 parameter object but found \(parametersContainer.count)" 40 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 41 | } 42 | return .sourceControl(parametersContainer[0]) 43 | } 44 | 45 | private static func decodeFileSystem(using container: KeyedDecodingContainer) throws -> Self { 46 | let parametersContainer = try container.decode([FileSystemParameters].self, forKey: .fileSystem) 47 | guard parametersContainer.count == 1 else { 48 | let debugDescription = "Expected to decode exactly 1 parameter object but found \(parametersContainer.count)" 49 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 50 | } 51 | return .fileSystem(parametersContainer[0]) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParserLive/Internal/IntermediatePackageSwiftFile+Product.swift: -------------------------------------------------------------------------------- 1 | extension IntermediatePackageSwiftFile { 2 | struct Product: Decodable { 3 | let name: String 4 | let targets: [String] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParserLive/Internal/IntermediatePackageSwiftFile+Target+Dependency.swift: -------------------------------------------------------------------------------- 1 | extension IntermediatePackageSwiftFile.Target { 2 | enum Dependency: Decodable { 3 | private enum CodingKeys: CodingKey { 4 | case byName 5 | case target 6 | case product 7 | } 8 | 9 | struct ByNameParameters { 10 | let name: String 11 | } 12 | 13 | struct ProductParameters { 14 | let name: String 15 | let package: String 16 | } 17 | 18 | case byName(ByNameParameters) 19 | case product(ProductParameters) 20 | 21 | init(from decoder: Decoder) throws { 22 | let container = try decoder.container(keyedBy: CodingKeys.self) 23 | if container.allKeys.contains(CodingKeys.byName) { 24 | self = try .decodeName(using: container, forKey: .byName) 25 | } else if container.allKeys.contains(CodingKeys.target) { 26 | self = try .decodeName(using: container, forKey: .target) 27 | } else if container.allKeys.contains(CodingKeys.product) { 28 | self = try .decodeProduct(using: container) 29 | } else { 30 | let keysString = container.allKeys.map(\.stringValue).joined(separator: ", ") 31 | let debugDescription = "Unsupported keys: " + keysString 32 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 33 | } 34 | } 35 | } 36 | } 37 | 38 | private extension IntermediatePackageSwiftFile.Target.Dependency { 39 | private static func decodeName(using container: KeyedDecodingContainer, forKey key: CodingKeys) throws -> Self { 40 | let values = try container.decode([NameComponent].self, forKey: key) 41 | guard values.count >= 1 else { 42 | let debugDescription = "Expected to decode at least 1 string but found \(values.count)" 43 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 44 | } 45 | guard case let .string(name) = values[0] else { 46 | let debugDescription = "Expected library name to be non-null" 47 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 48 | } 49 | let parameters = ByNameParameters(name: name) 50 | return .byName(parameters) 51 | } 52 | 53 | private static func decodeProduct(using container: KeyedDecodingContainer) throws -> Self { 54 | let values = try container.decode([ProductComponent].self, forKey: .product) 55 | guard values.count >= 2 else { 56 | let debugDescription = "Expected to decode at least 2 strings but found \(values.count)" 57 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 58 | } 59 | guard case let .string(name) = values[0] else { 60 | let debugDescription = "Expected library name to be non-null" 61 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 62 | } 63 | guard case let .string(package) = values[1] else { 64 | let debugDescription = "Expected package name to be non-null" 65 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 66 | } 67 | let parameters = ProductParameters(name: name, package: package) 68 | return .product(parameters) 69 | } 70 | } 71 | 72 | extension IntermediatePackageSwiftFile.Target.Dependency { 73 | private enum NameComponent: Decodable { 74 | struct PlatformNamesContainer: Decodable { 75 | let platformNames: [String] 76 | } 77 | 78 | case string(String) 79 | case platformNamesContainer(PlatformNamesContainer) 80 | case null 81 | 82 | init(from decoder: Decoder) throws { 83 | let container = try decoder.singleValueContainer() 84 | if let str = try? container.decode(String.self) { 85 | self = .string(str) 86 | } else if let container = try? container.decode(PlatformNamesContainer.self) { 87 | self = .platformNamesContainer(container) 88 | } else if container.decodeNil() { 89 | self = .null 90 | } else { 91 | let debugDescription = "Unexpected byName component" 92 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 93 | } 94 | } 95 | } 96 | 97 | private enum ProductComponent: Decodable { 98 | case string(String) 99 | case condition(Condition) 100 | case null 101 | 102 | init(from decoder: Decoder) throws { 103 | let container = try decoder.singleValueContainer() 104 | if let str = try? container.decode(String.self) { 105 | self = .string(str) 106 | } else if let condition = try? container.decode(Condition.self) { 107 | self = .condition(condition) 108 | } else if container.decodeNil() { 109 | self = .null 110 | } else { 111 | let debugDescription = "Unexpected product component" 112 | throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: debugDescription)) 113 | } 114 | } 115 | } 116 | } 117 | 118 | extension IntermediatePackageSwiftFile.Target.Dependency { 119 | struct Condition: Decodable { 120 | let platformNames: [String] 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParserLive/Internal/IntermediatePackageSwiftFile+Target.swift: -------------------------------------------------------------------------------- 1 | extension IntermediatePackageSwiftFile { 2 | struct Target: Decodable { 3 | let name: String 4 | let dependencies: [Dependency] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParserLive/Internal/IntermediatePackageSwiftFile.swift: -------------------------------------------------------------------------------- 1 | struct IntermediatePackageSwiftFile: Decodable { 2 | let name: String 3 | let products: [Product] 4 | let targets: [Target] 5 | let dependencies: [Dependency] 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParserLive/Internal/PackageSwiftFileMapper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackageSwiftFile 3 | import PackageSwiftFileParser 4 | 5 | struct PackageSwiftFileMapper { 6 | private let packageSwiftFileParser: PackageSwiftFileParser 7 | 8 | init(packageSwiftFileParser: PackageSwiftFileParser) { 9 | self.packageSwiftFileParser = packageSwiftFileParser 10 | } 11 | 12 | func map(_ intermediate: IntermediatePackageSwiftFile) throws -> PackageSwiftFile { 13 | let products = intermediate.products.map(map) 14 | let targets = intermediate.targets.map(map) 15 | let dependencies = try intermediate.dependencies.map(map) 16 | return PackageSwiftFile(name: intermediate.name, products: products, targets: targets, dependencies: dependencies) 17 | } 18 | } 19 | 20 | private extension PackageSwiftFileMapper { 21 | private func map(_ intermediate: IntermediatePackageSwiftFile.Product) -> PackageSwiftFile.Product { 22 | return PackageSwiftFile.Product(name: intermediate.name, targets: intermediate.targets) 23 | } 24 | 25 | private func map(_ intermediate: IntermediatePackageSwiftFile.Target) -> PackageSwiftFile.Target { 26 | let dependencies = intermediate.dependencies.map(map) 27 | return PackageSwiftFile.Target(name: intermediate.name, dependencies: dependencies) 28 | } 29 | 30 | private func map(_ intermediate: IntermediatePackageSwiftFile.Target.Dependency) -> PackageSwiftFile.Target.Dependency { 31 | switch intermediate { 32 | case .byName(let parameters): 33 | return .name(parameters.name) 34 | case .product(let parameters): 35 | return .product(parameters.name, inPackage: parameters.package) 36 | } 37 | } 38 | 39 | private func map(_ intermediate: IntermediatePackageSwiftFile.Dependency) throws -> PackageSwiftFile.Dependency { 40 | switch intermediate { 41 | case .sourceControl(let parameters): 42 | return .sourceControl(identity: parameters.identity) 43 | case .fileSystem(let parameters): 44 | let fileURL = (NSURL.fileURL(withPath: parameters.path) as NSURL).appendingPathComponent("Package.swift")! 45 | let packageSwiftFile = try packageSwiftFileParser.parseFile(at: fileURL) 46 | return .fileSystem(identity: parameters.identity, path: parameters.path, packageSwiftFile: packageSwiftFile) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/PackageSwiftFileParserLive/PackageSwiftParserLive.swift: -------------------------------------------------------------------------------- 1 | import DumpPackageService 2 | import Foundation 3 | import PackageSwiftFile 4 | import PackageSwiftFileParser 5 | import PackageSwiftFileParserCache 6 | 7 | private enum PackageSwiftFileParserLiveError: LocalizedError { 8 | case failedParsing(URL, Error) 9 | 10 | var errorDescription: String? { 11 | switch self { 12 | case let .failedParsing(fileURL, decodingError): 13 | return "Failed parsing dumped Package.swift file at \(fileURL.path): \(decodingError)" 14 | } 15 | } 16 | } 17 | 18 | public final class PackageSwiftFileParserLive: PackageSwiftFileParser { 19 | private let cache: PackageSwiftFileParserCache 20 | private let dumpPackageService: DumpPackageService 21 | 22 | public init(cache: PackageSwiftFileParserCache, dumpPackageService: DumpPackageService) { 23 | self.cache = cache 24 | self.dumpPackageService = dumpPackageService 25 | } 26 | 27 | public func parseFile(at fileURL: URL) throws -> PackageSwiftFile { 28 | if let packageSwiftFile = cache.cachedPackageSwiftFile(for: fileURL) { 29 | return packageSwiftFile 30 | } else { 31 | let packageSwiftFile = try justParseFile(at: fileURL) 32 | cache.cache(packageSwiftFile, for: fileURL) 33 | return packageSwiftFile 34 | } 35 | } 36 | } 37 | 38 | private extension PackageSwiftFileParserLive { 39 | private func justParseFile(at fileURL: URL) throws -> PackageSwiftFile { 40 | do { 41 | let contents = try dumpPackageService.dumpPackageForSwiftPackageFile(at: fileURL) 42 | let decoder = JSONDecoder() 43 | let intermediate = try decoder.decode(IntermediatePackageSwiftFile.self, from: contents) 44 | let mapper = PackageSwiftFileMapper(packageSwiftFileParser: self) 45 | return try mapper.map(intermediate) 46 | } catch { 47 | throw PackageSwiftFileParserLiveError.failedParsing(fileURL, error) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/ProjectRootClassifier/ProjectRoot.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ProjectRoot: Equatable { 4 | case xcodeproj(URL) 5 | case packageSwiftFile(URL) 6 | case unknown 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/ProjectRootClassifier/ProjectRootClassifier.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ProjectRootClassifier { 4 | func classifyProject(at fileURL: URL) -> ProjectRoot 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/ProjectRootClassifierLive/ProjectRootClassifierLive.swift: -------------------------------------------------------------------------------- 1 | import FileSystem 2 | import Foundation 3 | import ProjectRootClassifier 4 | 5 | public struct ProjectRootClassifierLive: ProjectRootClassifier { 6 | private let fileSystem: FileSystem 7 | private let xcodeprojPathExtension = "xcodeproj" 8 | private let packageSwiftFilename = "Package.swift" 9 | 10 | public init(fileSystem: FileSystem) { 11 | self.fileSystem = fileSystem 12 | } 13 | 14 | public func classifyProject(at itemURL: URL) -> ProjectRoot { 15 | if itemURL.pathExtension == xcodeprojPathExtension { 16 | return .xcodeproj(itemURL) 17 | } else if itemURL.lastPathComponent == packageSwiftFilename { 18 | return .packageSwiftFile(itemURL) 19 | } else if fileSystem.isDirectory(at: itemURL) { 20 | return classifyDirectory(at: itemURL) 21 | } else { 22 | return .unknown 23 | } 24 | } 25 | } 26 | 27 | private extension ProjectRootClassifierLive { 28 | private func classifyDirectory(at directoryURL: URL) -> ProjectRoot { 29 | let filenames = fileSystem.contentsOfDirectory(at: directoryURL) 30 | if let filename = filenames.first(where: { $0.hasSuffix("." + xcodeprojPathExtension) }) { 31 | let xcodeprojFileURL = (directoryURL as NSURL).appendingPathComponent(filename)! 32 | return .xcodeproj(xcodeprojFileURL) 33 | } else if filenames.contains(where: { $0 == packageSwiftFilename }) { 34 | let packageSwiftFileURL = (directoryURL as NSURL).appendingPathComponent(packageSwiftFilename)! 35 | return .packageSwiftFile(packageSwiftFileURL) 36 | } else { 37 | return .unknown 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/XcodeProject/XcodeProject+SwiftPackage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension XcodeProject { 4 | enum SwiftPackage: Equatable { 5 | public struct LocalParameters: Equatable { 6 | public let name: String 7 | public let fileURL: URL 8 | 9 | public init(name: String, fileURL: URL) { 10 | self.name = name 11 | self.fileURL = fileURL 12 | } 13 | } 14 | 15 | public struct RemoteParameters: Equatable { 16 | public let name: String 17 | public let repositoryURL: URL 18 | public let products: [String] 19 | 20 | public init(name: String, repositoryURL: URL, products: [String] = []) { 21 | self.name = name 22 | self.repositoryURL = repositoryURL 23 | self.products = products 24 | } 25 | } 26 | 27 | case local(LocalParameters) 28 | case remote(RemoteParameters) 29 | 30 | public var name: String { 31 | switch self { 32 | case .local(let parameters): 33 | return parameters.name 34 | case .remote(let parameters): 35 | return parameters.name 36 | } 37 | } 38 | 39 | public static func local(name: String, fileURL: URL) -> Self { 40 | let parameters = LocalParameters(name: name, fileURL: fileURL) 41 | return .local(parameters) 42 | } 43 | 44 | public static func remote(name: String, repositoryURL: URL, products: [String] = []) -> Self { 45 | let parameters = RemoteParameters(name: name, repositoryURL: repositoryURL, products: products) 46 | return .remote(parameters) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/XcodeProject/XcodeProject+Target.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension XcodeProject { 4 | public struct Target: Equatable { 5 | public let name: String 6 | public let packageProductDependencies: [String] 7 | 8 | public init(name: String, packageProductDependencies: [String] = []) { 9 | self.name = name 10 | self.packageProductDependencies = packageProductDependencies 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/XcodeProject/XcodeProject.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct XcodeProject: Equatable { 4 | public let name: String 5 | public let targets: [Target] 6 | public let swiftPackages: [SwiftPackage] 7 | 8 | public init(name: String, targets: [Target] = [], swiftPackages: [SwiftPackage] = []) { 9 | self.name = name 10 | self.targets = targets 11 | self.swiftPackages = swiftPackages 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/XcodeProjectParser/XcodeProjectParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XcodeProject 3 | 4 | public protocol XcodeProjectParser { 5 | func parseProject(at fileURL: URL) throws -> XcodeProject 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Library/Parsing/XcodeProjectParserLive/XcodeProjectParserLive.swift: -------------------------------------------------------------------------------- 1 | import FileSystem 2 | import Foundation 3 | import PathKit 4 | import XcodeProj 5 | import XcodeProject 6 | import XcodeProjectParser 7 | 8 | public struct XcodeProjectParserLive: XcodeProjectParser { 9 | private let fileSystem: FileSystem 10 | 11 | public init(fileSystem: FileSystem) { 12 | self.fileSystem = fileSystem 13 | } 14 | 15 | public func parseProject(at fileURL: URL) throws -> XcodeProject { 16 | let path = Path(fileURL.relativePath) 17 | let project = try XcodeProj(path: path) 18 | let sourceRoot = fileURL.deletingLastPathComponent() 19 | let remoteSwiftPackages = remoteSwiftPackages(in: project) 20 | let localSwiftPackages = try localSwiftPackages(in: project, atSourceRoot: sourceRoot) 21 | return XcodeProject( 22 | name: fileURL.lastPathComponent, 23 | targets: targets(in: project), 24 | swiftPackages: (remoteSwiftPackages + localSwiftPackages) 25 | ) 26 | } 27 | } 28 | 29 | private extension XcodeProjectParserLive { 30 | func targets(in project: XcodeProj) -> [XcodeProject.Target] { 31 | return project.pbxproj.nativeTargets.map { target in 32 | let packageProductDependencies = target.packageProductDependencies.map(\.productName) 33 | return .init(name: target.name, packageProductDependencies: packageProductDependencies) 34 | } 35 | } 36 | 37 | func remoteSwiftPackages(in project: XcodeProj) -> [XcodeProject.SwiftPackage] { 38 | struct IntermediateRemoteSwiftPackage { 39 | let name: String 40 | let repositoryURL: URL 41 | let products: [String] 42 | } 43 | var swiftPackages: [IntermediateRemoteSwiftPackage] = [] 44 | for target in project.pbxproj.nativeTargets { 45 | for dependency in target.packageProductDependencies { 46 | guard let package = dependency.package, let packageName = package.name else { 47 | continue 48 | } 49 | guard let rawRepositoryURL = package.repositoryURL, let repositoryURL = URL(string: rawRepositoryURL) else { 50 | continue 51 | } 52 | if let existingSwiftPackageIndex = swiftPackages.firstIndex(where: { $0.name == packageName }) { 53 | let existingSwiftPackage = swiftPackages[existingSwiftPackageIndex] 54 | let newProducts = existingSwiftPackage.products + [dependency.productName] 55 | let newSwiftPackage = IntermediateRemoteSwiftPackage(name: packageName, repositoryURL: repositoryURL, products: newProducts) 56 | swiftPackages[existingSwiftPackageIndex] = newSwiftPackage 57 | } else { 58 | let products = [dependency.productName] 59 | let swiftPackage = IntermediateRemoteSwiftPackage(name: packageName, repositoryURL: repositoryURL, products: products) 60 | swiftPackages.append(swiftPackage) 61 | } 62 | } 63 | } 64 | return swiftPackages.map { .remote(name: $0.name, repositoryURL: $0.repositoryURL, products: $0.products) } 65 | } 66 | 67 | func localSwiftPackages(in project: XcodeProj, atSourceRoot sourceRoot: URL) throws -> [XcodeProject.SwiftPackage] { 68 | return project.pbxproj.fileReferences.compactMap { fileReference in 69 | guard fileReference.isPotentialSwiftPackage else { 70 | return nil 71 | } 72 | guard let packageName = fileReference.potentialPackageName else { 73 | return nil 74 | } 75 | guard let packageSwiftFileURL = fileReference.potentialPackageSwiftFileURL(forSourceRoot: sourceRoot) else { 76 | return nil 77 | } 78 | guard fileSystem.fileExists(at: packageSwiftFileURL) else { 79 | return nil 80 | } 81 | return .local(.init(name: packageName, fileURL: packageSwiftFileURL)) 82 | } 83 | } 84 | } 85 | 86 | private extension PBXFileReference { 87 | var isPotentialSwiftPackage: Bool { 88 | return lastKnownFileType == "folder" || lastKnownFileType == "wrapper" 89 | } 90 | 91 | var potentialPackageName: String? { 92 | return name ?? path 93 | } 94 | 95 | func potentialPackageSwiftFileURL(forSourceRoot sourceRoot: URL) -> URL? { 96 | guard let path = path else { 97 | return nil 98 | } 99 | return ((sourceRoot as NSURL).appendingPathComponent(path) as? NSURL)?.appendingPathComponent("Package.swift") 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/Library/Utilities/FileSystem/FileSystem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol FileSystem { 4 | func fileExists(at itemURL: URL) -> Bool 5 | func isDirectory(at itemURL: URL) -> Bool 6 | func contentsOfDirectory(at directoryURL: URL) -> [String] 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Library/Utilities/FileSystemLive/FileSystemLive.swift: -------------------------------------------------------------------------------- 1 | import FileSystem 2 | import Foundation 3 | 4 | public struct FileSystemLive: FileSystem { 5 | public init() {} 6 | 7 | public func fileExists(at itemURL: URL) -> Bool { 8 | return FileManager.default.fileExists(atPath: itemURL.path) 9 | } 10 | 11 | public func isDirectory(at itemURL: URL) -> Bool { 12 | var isDirectory: ObjCBool = false 13 | FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory) 14 | return isDirectory.boolValue 15 | } 16 | 17 | public func contentsOfDirectory(at directoryURL: URL) -> [String] { 18 | do { 19 | return try FileManager.default.contentsOfDirectory(atPath: directoryURL.path) 20 | } catch { 21 | return [] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Library/Utilities/ShellCommandRunner/ShellCommandOutput.swift: -------------------------------------------------------------------------------- 1 | public struct ShellCommandOutput { 2 | public let status: Int32 3 | public let message: String 4 | 5 | public init(status: Int32, message: String) { 6 | self.status = status 7 | self.message = message 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Library/Utilities/ShellCommandRunner/ShellCommandRunner.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ShellCommandRunner { 4 | func run(withArguments arguments: [String], fromDirectoryURL directoryURL: URL) -> ShellCommandOutput 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Library/Utilities/ShellCommandRunnerLive/ShellCommandRunnerLive.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ShellCommandRunner 3 | 4 | public struct ShellCommandRunnerLive: ShellCommandRunner { 5 | public init() {} 6 | 7 | public func run(withArguments arguments: [String], fromDirectoryURL directoryURL: URL) -> ShellCommandOutput { 8 | let task = Process() 9 | let pipe = Pipe() 10 | task.currentDirectoryURL = directoryURL 11 | task.standardOutput = pipe 12 | task.arguments = ["-c", arguments.joined(separator: " ")] 13 | task.launchPath = "/bin/zsh" 14 | task.standardInput = nil 15 | task.launch() 16 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 17 | task.waitUntilExit() 18 | let message = String(data: data, encoding: .utf8)! 19 | let status = task.terminationStatus 20 | return ShellCommandOutput(status: status, message: message) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Library/Utilities/StringIndentHelpers/Indent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Array where Element == String { 4 | var indented: [String] { 5 | return indented(by: 1) 6 | } 7 | 8 | func indented(by indentation: Int) -> [String] { 9 | return map { $0.indented(by: indentation) } 10 | } 11 | } 12 | 13 | public extension String { 14 | var indented: String { 15 | return indented(by: 1) 16 | } 17 | 18 | func indented(by indentation: Int) -> String { 19 | let indentString = String(repeating: " ", count: indentation) 20 | return split(separator: "\n", omittingEmptySubsequences: false).map { lineString in 21 | if lineString.trimmingCharacters(in: .whitespaces).isEmpty { 22 | return "" 23 | } else { 24 | return indentString + lineString 25 | } 26 | }.joined(separator: "\n") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Main/CompositionRoot.swift: -------------------------------------------------------------------------------- 1 | import D2GraphMapper 2 | import DirectedGraphMapper 3 | import DirectedGraphWriter 4 | import DOTGraphMapper 5 | import DumpPackageService 6 | import DumpPackageServiceLive 7 | import FileSystem 8 | import FileSystemLive 9 | import GraphCommand 10 | import MappingDirectedGraphWriter 11 | import MermaidGraphMapper 12 | import PackageGraphBuilder 13 | import PackageGraphBuilderLive 14 | import PackageSwiftFileParser 15 | import PackageSwiftFileParserCache 16 | import PackageSwiftFileParserCacheLive 17 | import PackageSwiftFileParserLive 18 | import ProjectRootClassifier 19 | import ProjectRootClassifierLive 20 | import ShellCommandRunner 21 | import ShellCommandRunnerLive 22 | import StdoutWriter 23 | import XcodeProjectGraphBuilder 24 | import XcodeProjectGraphBuilderLive 25 | import XcodeProjectParser 26 | import XcodeProjectParserLive 27 | 28 | public enum CompositionRoot { 29 | static func graphCommand(packagesOnly: Bool, nodeSpacing: Float?, rankSpacing: Float?) -> GraphCommand { 30 | let directedGraphWriterFactory = directedGraphWriterFactory(nodeSpacing: nodeSpacing, rankSpacing: rankSpacing) 31 | return GraphCommand(projectRootClassifier: projectRootClassifier, 32 | packageSwiftFileParser: packageSwiftFileParser, 33 | xcodeProjectParser: xcodeProjectParser, 34 | packageGraphBuilder: packageGraphBuilder(packagesOnly: packagesOnly), 35 | xcodeProjectGraphBuilder: xcodeProjectGraphBuilder(packagesOnly: packagesOnly), 36 | directedGraphWriterFactory: directedGraphWriterFactory) 37 | } 38 | } 39 | 40 | private extension CompositionRoot { 41 | private static func directedGraphWriterFactory(nodeSpacing: Float?, rankSpacing: Float?) -> DirectedGraphWriterFactory { 42 | let dotGraphWriter = dotGraphWriter(nodesep: nodeSpacing, ranksep: rankSpacing) 43 | let mermaidGraphWriter = mermaidGraphWriter(nodeSpacing: nodeSpacing.map(Int.init), rankSpacing: rankSpacing.map(Int.init)) 44 | return DirectedGraphWriterFactory(d2GraphWriter: d2GraphWriter, dotGraphWriter: dotGraphWriter, mermaidGraphWriter: mermaidGraphWriter) 45 | } 46 | 47 | private static var d2GraphWriter: some DirectedGraphWriter { 48 | let mapper = D2GraphMapper() 49 | return MappingDirectedGraphWriter(mapper: mapper, writer: stdoutWriter) 50 | } 51 | 52 | private static func dotGraphWriter(nodesep: Float?, ranksep: Float?) -> some DirectedGraphWriter { 53 | let settings = DOTGraphSettings(nodesep: nodesep, ranksep: ranksep) 54 | let mapper = DOTGraphMapper(settings: settings) 55 | return MappingDirectedGraphWriter(mapper: mapper, writer: stdoutWriter) 56 | } 57 | 58 | private static func mermaidGraphWriter(nodeSpacing: Int?, rankSpacing: Int?) -> some DirectedGraphWriter { 59 | let settings = MermaidGraphSettings(nodeSpacing: nodeSpacing, rankSpacing: rankSpacing) 60 | let mapper = MermaidGraphMapper(settings: settings) 61 | return MappingDirectedGraphWriter(mapper: mapper, writer: stdoutWriter) 62 | } 63 | 64 | private static var projectRootClassifier: ProjectRootClassifier { 65 | return ProjectRootClassifierLive(fileSystem: fileSystem) 66 | } 67 | 68 | private static var dumpPackageService: DumpPackageService { 69 | return DumpPackageServiceLive(shellCommandRunner: shellCommandRunner) 70 | } 71 | 72 | private static var packageSwiftFileParser: PackageSwiftFileParser { 73 | return PackageSwiftFileParserLive(cache: packageSwiftFileParserCache, dumpPackageService: dumpPackageService) 74 | } 75 | 76 | private static func packageGraphBuilder(packagesOnly: Bool) -> PackageGraphBuilder { 77 | return PackageGraphBuilderLive(packagesOnly: packagesOnly) 78 | } 79 | 80 | private static let packageSwiftFileParserCache: PackageSwiftFileParserCache = PackageSwiftFileParserCacheLive() 81 | 82 | private static var xcodeProjectParser: XcodeProjectParser { 83 | return XcodeProjectParserLive(fileSystem: fileSystem) 84 | } 85 | 86 | private static func xcodeProjectGraphBuilder(packagesOnly: Bool) -> XcodeProjectGraphBuilder { 87 | return XcodeProjectGraphBuilderLive(packageSwiftFileParser: packageSwiftFileParser, 88 | packageGraphBuilder: packageGraphBuilder(packagesOnly: packagesOnly), 89 | packagesOnly: packagesOnly) 90 | } 91 | 92 | private static var fileSystem: FileSystem { 93 | return FileSystemLive() 94 | } 95 | 96 | private static var shellCommandRunner: ShellCommandRunner { 97 | return ShellCommandRunnerLive() 98 | } 99 | 100 | private static var stdoutWriter: StdoutWriter { 101 | return StdoutWriter() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Main/DependencyGraph.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | import GraphCommand 4 | 5 | @main 6 | struct DependencyGraph: ParsableCommand { 7 | @Argument(help: "The input to show dependencies for. Can be en .xcodeproj file or a Package.swift file.") 8 | var input: String 9 | 10 | @Option(name: [.short, .long], help: """ 11 | The syntax of the output. 12 | 13 | Valid values are: 14 | 15 | - d2: D2 document to be passed to d2. More info: https://github.com/terrastruct/d2 16 | 17 | - dot: DOT document to be passed to GraphViz. More info: https://graphviz.org/doc/info/command.html 18 | 19 | - mermaid: Mermaid.js' diagram syntax. To be be passed to the Mermaid CLI. More info: https://github.com/mermaid-js/mermaid-cli. 20 | 21 | 22 | """) 23 | var syntax: Syntax = .dot 24 | 25 | @Flag(name: .long, help: "Enable to only show packages in the graph, thus omitting products and targets.") 26 | var packagesOnly = false 27 | 28 | // swiftlint:disable:next line_length 29 | @Option(name: .long, help: "Specifies the spacing between adjacement nodes in the same rank. Supported by the DOT and Mermaid.js syntaxes. Translates to the nodesep option in DOT and the nodeSpacing option in Mermaid.js.") 30 | var nodeSpacing: Float? 31 | 32 | // swiftlint:disable:next line_length 33 | @Option(name: .long, help: "Specifies the spacing between nodes in different ranks. Supported by the DOT and Mermaid.js syntaxes. Translates to the ranksep option in DOT and the rankSpacing option in Mermaid.js.") 34 | var rankSpacing: Float? 35 | 36 | static let configuration = CommandConfiguration( 37 | abstract: """ 38 | Generates graphs of the dependencies in an Xcode project or Swift package. 39 | 40 | Nodes shaped as an ellipse represent products, e.g. the libraries in a Swift package, and the square nodes represent targets. 41 | """, 42 | version: "1.2.0" 43 | ) 44 | 45 | func run() throws { 46 | let command = CompositionRoot.graphCommand(packagesOnly: packagesOnly, nodeSpacing: nodeSpacing, rankSpacing: rankSpacing) 47 | try command.run(withInput: input, syntax: syntax) 48 | } 49 | } 50 | 51 | extension Syntax: ExpressibleByArgument {} 52 | -------------------------------------------------------------------------------- /Tests/D2GraphMapperTests/D2GraphMapperTests.swift: -------------------------------------------------------------------------------- 1 | @testable import D2GraphMapper 2 | import XCTest 3 | 4 | final class D2GraphMapperTests: XCTestCase { 5 | func testMapsGraphToD2Syntax() throws { 6 | let mapper = D2GraphMapper() 7 | let string = try mapper.map(.mock) 8 | let expectedString = """ 9 | direction: right 10 | 11 | Foo: Foo { 12 | Foo: Foo 13 | Foo.shape: rectangle 14 | } 15 | 16 | Bar: Bar { 17 | Bar: Bar 18 | Bar.shape: rectangle 19 | } 20 | 21 | Baz: Baz { 22 | Baz: Baz 23 | Baz.shape: oval 24 | } 25 | 26 | Foo.Foo -> Bar.Bar 27 | Foo.Foo -> Baz.Baz 28 | """ 29 | XCTAssertEqual(string, expectedString) 30 | } 31 | 32 | func testMapsGraphWithRootNodesToD2Syntax() throws { 33 | let mapper = D2GraphMapper() 34 | let string = try mapper.map(.mockWithRootNodes) 35 | let expectedString = """ 36 | direction: right 37 | 38 | Foo: Foo 39 | Foo.shape: rectangle 40 | Bar: Bar 41 | Bar.shape: rectangle 42 | Baz: Baz 43 | Baz.shape: oval 44 | 45 | Foo -> Bar 46 | Foo -> Baz 47 | """ 48 | XCTAssertEqual(string, expectedString) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/D2GraphMapperTests/Mock/DirectedGraph.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | 3 | extension DirectedGraph { 4 | static var mock: DirectedGraph { 5 | let fooNode = DirectedGraph.Node(name: "Foo", label: "Foo") 6 | let fooCluster = DirectedGraph.Cluster(name: "Foo", label: "Foo", nodes: [fooNode]) 7 | let barNode = DirectedGraph.Node(name: "Bar", label: "Bar") 8 | let barCluster = DirectedGraph.Cluster(name: "Bar", label: "Bar", nodes: [barNode]) 9 | let bazNode = DirectedGraph.Node(name: "Baz", label: "Baz", shape: .ellipse) 10 | let bazCluster = DirectedGraph.Cluster(name: "Baz", label: "Baz", nodes: [bazNode]) 11 | return DirectedGraph(clusters: [ 12 | fooCluster, 13 | barCluster, 14 | bazCluster 15 | ], edges: [ 16 | DirectedGraph.Edge(from: fooNode, to: barNode), 17 | DirectedGraph.Edge(from: fooNode, to: bazNode) 18 | ]) 19 | } 20 | 21 | static var mockWithRootNodes: DirectedGraph { 22 | let fooNode = DirectedGraph.Node(name: "Foo", label: "Foo") 23 | let barNode = DirectedGraph.Node(name: "Bar", label: "Bar") 24 | let bazNode = DirectedGraph.Node(name: "Baz", label: "Baz", shape: .ellipse) 25 | return DirectedGraph(nodes: [ 26 | fooNode, 27 | barNode, 28 | bazNode 29 | ], edges: [ 30 | DirectedGraph.Edge(from: fooNode, to: barNode), 31 | DirectedGraph.Edge(from: fooNode, to: bazNode) 32 | ]) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/DOTGraphMapperTests/DOTGraphMapperTests.swift: -------------------------------------------------------------------------------- 1 | @testable import DOTGraphMapper 2 | import XCTest 3 | 4 | final class DOTGraphMapperTests: XCTestCase { 5 | func testMapsGraphToDotSyntax() throws { 6 | let settings = DOTGraphSettings(nodesep: 1.5, ranksep: 1.2) 7 | let mapper = DOTGraphMapper(settings: settings) 8 | let string = try mapper.map(.mock) 9 | let expectedString = """ 10 | digraph g { 11 | layout=dot 12 | rankdir=LR 13 | nodesep=1.5 14 | ranksep=1.2 15 | 16 | subgraph cluster_Foo { 17 | label="Foo" 18 | Foo [label="Foo", shape=box] 19 | } 20 | 21 | subgraph cluster_Bar { 22 | label="Bar" 23 | Bar [label="Bar", shape=box] 24 | } 25 | 26 | subgraph cluster_Baz { 27 | label="Baz" 28 | Baz [label="Baz", shape=ellipse] 29 | } 30 | 31 | Foo -> Bar 32 | Foo -> Baz 33 | } 34 | """ 35 | XCTAssertEqual(string, expectedString) 36 | } 37 | 38 | func testMapsGraphWithRootNodesToDotSyntax() throws { 39 | let settings = DOTGraphSettings(nodesep: 1.5, ranksep: 1.2) 40 | let mapper = DOTGraphMapper(settings: settings) 41 | let string = try mapper.map(.mockWithRootNodes) 42 | let expectedString = """ 43 | digraph g { 44 | layout=dot 45 | rankdir=LR 46 | nodesep=1.5 47 | ranksep=1.2 48 | 49 | Foo [label="Foo", shape=box] 50 | Bar [label="Bar", shape=box] 51 | Baz [label="Baz", shape=ellipse] 52 | 53 | Foo -> Bar 54 | Foo -> Baz 55 | } 56 | """ 57 | XCTAssertEqual(string, expectedString) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/DOTGraphMapperTests/Mock/DirectedGraph.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | 3 | extension DirectedGraph { 4 | static var mock: DirectedGraph { 5 | let fooNode = DirectedGraph.Node(name: "Foo", label: "Foo") 6 | let fooCluster = DirectedGraph.Cluster(name: "Foo", label: "Foo", nodes: [fooNode]) 7 | let barNode = DirectedGraph.Node(name: "Bar", label: "Bar") 8 | let barCluster = DirectedGraph.Cluster(name: "Bar", label: "Bar", nodes: [barNode]) 9 | let bazNode = DirectedGraph.Node(name: "Baz", label: "Baz", shape: .ellipse) 10 | let bazCluster = DirectedGraph.Cluster(name: "Baz", label: "Baz", nodes: [bazNode]) 11 | return DirectedGraph(clusters: [ 12 | fooCluster, 13 | barCluster, 14 | bazCluster 15 | ], edges: [ 16 | DirectedGraph.Edge(from: fooNode, to: barNode), 17 | DirectedGraph.Edge(from: fooNode, to: bazNode) 18 | ]) 19 | } 20 | 21 | static var mockWithRootNodes: DirectedGraph { 22 | let fooNode = DirectedGraph.Node(name: "Foo", label: "Foo") 23 | let barNode = DirectedGraph.Node(name: "Bar", label: "Bar") 24 | let bazNode = DirectedGraph.Node(name: "Baz", label: "Baz", shape: .ellipse) 25 | return DirectedGraph(nodes: [ 26 | fooNode, 27 | barNode, 28 | bazNode 29 | ], edges: [ 30 | DirectedGraph.Edge(from: fooNode, to: barNode), 31 | DirectedGraph.Edge(from: fooNode, to: bazNode) 32 | ]) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/DirectedGraphTests/DirectedGraphTests.swift: -------------------------------------------------------------------------------- 1 | @testable import DirectedGraph 2 | import XCTest 3 | 4 | final class DirectedGraphTests: XCTestCase { 5 | func testUnioningGraphsWithClusters() { 6 | let graphA_nodeA = DirectedGraph.Node(name: "NodeA", label: "NodeA", shape: .box) 7 | let graphA_nodeB = DirectedGraph.Node(name: "NodeB", label: "NodeB", shape: .box) 8 | let graphA_nodeC = DirectedGraph.Node(name: "NodeC", label: "NodeC", shape: .box) 9 | let graphA_clusterA = DirectedGraph.Cluster(name: "ClusterA", label: "ClusterA", nodes: [graphA_nodeA, graphA_nodeB]) 10 | let graphA_clusterB = DirectedGraph.Cluster(name: "ClusterB", label: "ClusterB", nodes: [graphA_nodeC]) 11 | let graphA_edge1: DirectedGraph.Edge = .from(graphA_nodeA, to: graphA_nodeB) 12 | let graphA_edge2: DirectedGraph.Edge = .from(graphA_nodeB, to: graphA_nodeC) 13 | let graphA = DirectedGraph(clusters: [graphA_clusterA, graphA_clusterB], edges: [graphA_edge1, graphA_edge2]) 14 | 15 | let graphB_nodeA = DirectedGraph.Node(name: "NodeA", label: "NodeA", shape: .box) 16 | let graphB_nodeD = DirectedGraph.Node(name: "NodeD", label: "NodeD", shape: .box) 17 | let graphB_nodeE = DirectedGraph.Node(name: "NodeE", label: "NodeE", shape: .box) 18 | let graphB_clusterA = DirectedGraph.Cluster(name: "ClusterA", label: "ClusterA", nodes: [graphB_nodeA]) 19 | let graphB_clusterB = DirectedGraph.Cluster(name: "ClusterB", label: "ClusterB", nodes: [graphB_nodeD]) 20 | let graphB_clusterC = DirectedGraph.Cluster(name: "ClusterC", label: "ClusterC", nodes: [graphB_nodeE]) 21 | let graphB_edge1: DirectedGraph.Edge = .from(graphB_nodeA, to: graphB_nodeD) 22 | let graphB_edge2: DirectedGraph.Edge = .from(graphB_nodeD, to: graphB_nodeE) 23 | let graphB = DirectedGraph(clusters: [graphB_clusterA, graphB_clusterB, graphB_clusterC], edges: [graphB_edge1, graphB_edge2]) 24 | 25 | graphA.union(graphB) 26 | 27 | let expectedGraph_nodeA = DirectedGraph.Node(name: "NodeA", label: "NodeA", shape: .box) 28 | let expectedGraph_nodeB = DirectedGraph.Node(name: "NodeB", label: "NodeB", shape: .box) 29 | let expectedGraph_nodeC = DirectedGraph.Node(name: "NodeC", label: "NodeC", shape: .box) 30 | let expectedGraph_nodeD = DirectedGraph.Node(name: "NodeD", label: "NodeD", shape: .box) 31 | let expectedGraph_nodeE = DirectedGraph.Node(name: "NodeE", label: "NodeE", shape: .box) 32 | let expectedGraph_clusterA = DirectedGraph.Cluster(name: "ClusterA", label: "ClusterA", nodes: [graphA_nodeA, graphA_nodeB]) 33 | let expectedGraph_clusterB = DirectedGraph.Cluster(name: "ClusterB", label: "ClusterB", nodes: [graphA_nodeC, expectedGraph_nodeD]) 34 | let expectedGraph_clusterC = DirectedGraph.Cluster(name: "ClusterC", label: "ClusterC", nodes: [expectedGraph_nodeE]) 35 | let expectedGraph_edge1: DirectedGraph.Edge = .from(expectedGraph_nodeA, to: expectedGraph_nodeB) 36 | let expectedGraph_edge2: DirectedGraph.Edge = .from(expectedGraph_nodeB, to: expectedGraph_nodeC) 37 | let expectedGraph_edge3: DirectedGraph.Edge = .from(expectedGraph_nodeA, to: expectedGraph_nodeD) 38 | let expectedGraph_edge4: DirectedGraph.Edge = .from(expectedGraph_nodeD, to: expectedGraph_nodeE) 39 | 40 | let expectedGraph = DirectedGraph(clusters: [ 41 | expectedGraph_clusterA, 42 | expectedGraph_clusterB, 43 | expectedGraph_clusterC 44 | ], edges: [ 45 | expectedGraph_edge1, 46 | expectedGraph_edge2, 47 | expectedGraph_edge3, 48 | expectedGraph_edge4 49 | ]) 50 | XCTAssertEqual(graphA, expectedGraph) 51 | } 52 | 53 | func testUnioningGraphsWithRootNodes() { 54 | let graphA_nodeA = DirectedGraph.Node(name: "NodeA", label: "NodeA", shape: .box) 55 | let graphA_nodeB = DirectedGraph.Node(name: "NodeB", label: "NodeB", shape: .box) 56 | let graphA_nodeC = DirectedGraph.Node(name: "NodeC", label: "NodeC", shape: .box) 57 | let graphA_edge1: DirectedGraph.Edge = .from(graphA_nodeA, to: graphA_nodeB) 58 | let graphA_edge2: DirectedGraph.Edge = .from(graphA_nodeB, to: graphA_nodeC) 59 | let graphA = DirectedGraph(nodes: [graphA_nodeA, graphA_nodeB, graphA_nodeC], edges: [graphA_edge1, graphA_edge2]) 60 | 61 | let graphB_nodeA = DirectedGraph.Node(name: "NodeA", label: "NodeA", shape: .box) 62 | let graphB_nodeD = DirectedGraph.Node(name: "NodeD", label: "NodeD", shape: .box) 63 | let graphB_nodeE = DirectedGraph.Node(name: "NodeE", label: "NodeE", shape: .box) 64 | let graphB_edge1: DirectedGraph.Edge = .from(graphB_nodeA, to: graphB_nodeD) 65 | let graphB_edge2: DirectedGraph.Edge = .from(graphB_nodeD, to: graphB_nodeE) 66 | let graphB = DirectedGraph(nodes: [graphB_nodeA, graphB_nodeD, graphB_nodeE], edges: [graphB_edge1, graphB_edge2]) 67 | 68 | graphA.union(graphB) 69 | 70 | let expectedGraph_nodeA = DirectedGraph.Node(name: "NodeA", label: "NodeA", shape: .box) 71 | let expectedGraph_nodeB = DirectedGraph.Node(name: "NodeB", label: "NodeB", shape: .box) 72 | let expectedGraph_nodeC = DirectedGraph.Node(name: "NodeC", label: "NodeC", shape: .box) 73 | let expectedGraph_nodeD = DirectedGraph.Node(name: "NodeD", label: "NodeD", shape: .box) 74 | let expectedGraph_nodeE = DirectedGraph.Node(name: "NodeE", label: "NodeE", shape: .box) 75 | let expectedGraph_edge1: DirectedGraph.Edge = .from(expectedGraph_nodeA, to: expectedGraph_nodeB) 76 | let expectedGraph_edge2: DirectedGraph.Edge = .from(expectedGraph_nodeB, to: expectedGraph_nodeC) 77 | let expectedGraph_edge3: DirectedGraph.Edge = .from(expectedGraph_nodeA, to: expectedGraph_nodeD) 78 | let expectedGraph_edge4: DirectedGraph.Edge = .from(expectedGraph_nodeD, to: expectedGraph_nodeE) 79 | 80 | let expectedGraph = DirectedGraph(nodes: [ 81 | expectedGraph_nodeA, 82 | expectedGraph_nodeB, 83 | expectedGraph_nodeC, 84 | expectedGraph_nodeD, 85 | expectedGraph_nodeE 86 | ], edges: [ 87 | expectedGraph_edge1, 88 | expectedGraph_edge2, 89 | expectedGraph_edge3, 90 | expectedGraph_edge4 91 | ]) 92 | XCTAssertEqual(graphA, expectedGraph) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/DirectedGraphXcodeHelpersTests/String+SafeNameTests.swift: -------------------------------------------------------------------------------- 1 | @testable import DirectedGraphXcodeHelpers 2 | import XCTest 3 | 4 | // swiftlint:disable:next type_name 5 | final class String_SafeNameTests: XCTestCase { 6 | func testRemovesDot() { 7 | let string = "Example.xcodeproj".safeName 8 | XCTAssertEqual(string, "Examplexcodeproj") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tests/DumpPackageServiceLiveTests/DumpPackageServiceLiveTests.swift: -------------------------------------------------------------------------------- 1 | @testable import DumpPackageServiceLive 2 | import XCTest 3 | 4 | final class DumpPackageServiceLiveTests: XCTestCase { 5 | func testThrowsWhenReceivingInvalidPackageSwiftFileURL() throws { 6 | let shellCommandRunner = ShellCommandRunnerMock() 7 | let service = DumpPackageServiceLive(shellCommandRunner: shellCommandRunner) 8 | let fileURL = URL(fileURLWithPath: "/Users/john/Mock/NotAPackageSwiftFile") 9 | XCTAssertThrowsError(try service.dumpPackageForSwiftPackageFile(at: fileURL)) 10 | } 11 | 12 | func testInvokesDumpPackageCommandOnSwiftCLI() throws { 13 | let shellCommandRunner = ShellCommandRunnerMock() 14 | let service = DumpPackageServiceLive(shellCommandRunner: shellCommandRunner) 15 | let fileURL = URL(fileURLWithPath: "/Users/john/Mock/Package.swift") 16 | _ = try service.dumpPackageForSwiftPackageFile(at: fileURL) 17 | XCTAssertEqual(shellCommandRunner.latestArguments, ["swift", "package", "dump-package"]) 18 | } 19 | 20 | func testSetsCurrentDirectoryToSwiftPackage() throws { 21 | let shellCommandRunner = ShellCommandRunnerMock() 22 | let service = DumpPackageServiceLive(shellCommandRunner: shellCommandRunner) 23 | let fileURL = URL(fileURLWithPath: "/Users/john/Mock/Package.swift") 24 | _ = try service.dumpPackageForSwiftPackageFile(at: fileURL) 25 | XCTAssertEqual(shellCommandRunner.latestDirectoryURL, NSURL.fileURL(withPath: "/Users/john/Mock/")) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/DumpPackageServiceLiveTests/Mock/ShellCommandRunnerMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ShellCommandRunner 3 | 4 | final class ShellCommandRunnerMock: ShellCommandRunner { 5 | private(set) var latestArguments: [String]? 6 | private(set) var latestDirectoryURL: URL? 7 | 8 | func run(withArguments arguments: [String], fromDirectoryURL directoryURL: URL) -> ShellCommandOutput { 9 | latestArguments = arguments 10 | latestDirectoryURL = directoryURL 11 | return ShellCommandOutput(status: 0, message: "Success") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "keyboardtoolbar", 5 | "kind" : "remoteSourceControl", 6 | "location" : "git@github.com:simonbs/KeyboardToolbar.git", 7 | "state" : { 8 | "revision" : "5f95f8e9af98f6163a58caaceaadc3bf720fd7bd", 9 | "version" : "0.1.1" 10 | } 11 | }, 12 | { 13 | "identity" : "runestone", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/simonbs/Runestone", 16 | "state" : { 17 | "branch" : "main", 18 | "revision" : "e9d220ae1a00b5ef00a7b6146d77f660f9e5dbe2" 19 | } 20 | }, 21 | { 22 | "identity" : "treesitterlanguages", 23 | "kind" : "remoteSourceControl", 24 | "location" : "git@github.com:simonbs/TreeSitterLanguages.git", 25 | "state" : { 26 | "branch" : "main", 27 | "revision" : "f23b8872561daf747e233ba7d6130d9e680f52e1" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application(_ application: UIApplication, 6 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | var window: UIWindow? 5 | 6 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {} 7 | } 8 | -------------------------------------------------------------------------------- /Tests/ExampleProject/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ViewController: UIViewController {} 4 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExamplePackageA/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExamplePackageA/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "ExamplePackageA", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "ExampleLibraryA", targets: ["ExampleLibraryA"]) 11 | ], 12 | targets: [ 13 | .target(name: "ExampleLibraryA") 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExamplePackageA/Sources/ExampleLibraryA/Dummy.swift: -------------------------------------------------------------------------------- 1 | public struct Dummy { 2 | public private(set) var text = "Hello, World!" 3 | 4 | public init() { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExamplePackageB/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExamplePackageB/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "ExamplePackageB", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "ExampleLibraryB", targets: ["ExampleLibraryB"]) 11 | ], 12 | dependencies: [ 13 | .package(path: "../ExamplePackageC"), 14 | .package(url: "git@github.com:simonbs/KeyboardToolbar.git", from: "0.1.1") 15 | ], 16 | targets: [ 17 | .target(name: "ExampleLibraryB", dependencies: [ 18 | .product(name: "ExampleLibraryC", package: "ExamplePackageC"), 19 | .product(name: "KeyboardToolbar", package: "KeyboardToolbar") 20 | ]) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExamplePackageB/Sources/ExampleLibraryB/Dummy.swift: -------------------------------------------------------------------------------- 1 | public struct Dummy { 2 | public private(set) var text = "Hello, World!" 3 | 4 | public init() { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExamplePackageC/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExamplePackageC/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "ExamplePackageC", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "ExampleLibraryC", targets: ["ExampleLibraryC"]) 11 | ], 12 | targets: [ 13 | .target(name: "ExampleLibraryC") 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExamplePackageC/Sources/ExampleLibraryC/Dummy.swift: -------------------------------------------------------------------------------- 1 | public struct Dummy { 2 | public private(set) var text = "Hello, World!" 3 | 4 | public init() { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExampleTests/ExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleTests.swift 3 | // ExampleTests 4 | // 5 | // Created by Simon Støvring on 03/12/2022. 6 | // 7 | 8 | @testable import Example 9 | import XCTest 10 | 11 | final class ExampleTests: XCTestCase {} 12 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExampleUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class ExampleUITests: XCTestCase {} 4 | -------------------------------------------------------------------------------- /Tests/ExampleProject/ExampleUITests/ExampleUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class ExampleUITestsLaunchTests: XCTestCase {} 4 | -------------------------------------------------------------------------------- /Tests/GraphCommandTests/DirectedGraphWriterFactoryTests.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphWriter 3 | @testable import GraphCommand 4 | import XCTest 5 | 6 | final class DirectedGraphWriterFactoryTests: XCTestCase { 7 | func testReturnsD2GraphMapper() throws { 8 | let d2GrapWriter = D2GraphWriterMock() 9 | let dotGrapWriter = DOTGraphWriterMock() 10 | let mermaidGraphWriter = MermaidGraphWriterMock() 11 | let factory = DirectedGraphWriterFactory(d2GraphWriter: d2GrapWriter, dotGraphWriter: dotGrapWriter, mermaidGraphWriter: mermaidGraphWriter) 12 | let writer = factory.writer(for: .d2) 13 | XCTAssertTrue(writer is D2GraphWriterMock) 14 | } 15 | 16 | func testReturnsDotGraphMapper() throws { 17 | let d2GrapWriter = DOTGraphWriterMock() 18 | let dotGrapWriter = DOTGraphWriterMock() 19 | let mermaidGraphWriter = MermaidGraphWriterMock() 20 | let factory = DirectedGraphWriterFactory(d2GraphWriter: d2GrapWriter, dotGraphWriter: dotGrapWriter, mermaidGraphWriter: mermaidGraphWriter) 21 | let writer = factory.writer(for: .dot) 22 | XCTAssertTrue(writer is DOTGraphWriterMock) 23 | } 24 | 25 | func testReturnsMermaidGraphMapper() throws { 26 | let d2GrapWriter = DOTGraphWriterMock() 27 | let dotGrapWriter = DOTGraphWriterMock() 28 | let mermaidGraphWriter = MermaidGraphWriterMock() 29 | let factory = DirectedGraphWriterFactory(d2GraphWriter: d2GrapWriter, dotGraphWriter: dotGrapWriter, mermaidGraphWriter: mermaidGraphWriter) 30 | let writer = factory.writer(for: .mermaid) 31 | XCTAssertTrue(writer is MermaidGraphWriterMock) 32 | } 33 | } 34 | 35 | private struct D2GraphWriterMock: DirectedGraphWriter { 36 | func write(_ directedGraph: DirectedGraph) throws {} 37 | } 38 | 39 | private struct DOTGraphWriterMock: DirectedGraphWriter { 40 | func write(_ directedGraph: DirectedGraph) throws {} 41 | } 42 | 43 | private struct MermaidGraphWriterMock: DirectedGraphWriter { 44 | func write(_ directedGraph: DirectedGraph) throws {} 45 | } 46 | -------------------------------------------------------------------------------- /Tests/GraphCommandTests/GraphCommandTests.swift: -------------------------------------------------------------------------------- 1 | @testable import GraphCommand 2 | import XCTest 3 | 4 | final class GraphCommandTests: XCTestCase { 5 | func testInvokesOnlyD2GraphWriter() throws { 6 | let d2GraphWriter = DirectedGraphWriterMock() 7 | let dotGraphWriter = DirectedGraphWriterMock() 8 | let mermaidGraphWriter = DirectedGraphWriterMock() 9 | let directedGraphWriterFactory = DirectedGraphWriterFactory( 10 | d2GraphWriter: d2GraphWriter, 11 | dotGraphWriter: dotGraphWriter, 12 | mermaidGraphWriter: mermaidGraphWriter 13 | ) 14 | let command = GraphCommand(projectRootClassifier: ProjectRootClassifierMock(), 15 | packageSwiftFileParser: PackageSwiftFileParserMock(), 16 | xcodeProjectParser: XcodeProjectParserMock(), 17 | packageGraphBuilder: PackageGraphBuilderMock(), 18 | xcodeProjectGraphBuilder: XcodeProjectGraphBuilderMock(), 19 | directedGraphWriterFactory: directedGraphWriterFactory) 20 | let fileURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example") 21 | try command.run(withInput: fileURL.path, syntax: .d2) 22 | XCTAssertTrue(d2GraphWriter.didWrite) 23 | XCTAssertFalse(dotGraphWriter.didWrite) 24 | XCTAssertFalse(mermaidGraphWriter.didWrite) 25 | } 26 | 27 | func testInvokesOnlyDotGraphWriter() throws { 28 | let d2GraphWriter = DirectedGraphWriterMock() 29 | let dotGraphWriter = DirectedGraphWriterMock() 30 | let mermaidGraphWriter = DirectedGraphWriterMock() 31 | let directedGraphWriterFactory = DirectedGraphWriterFactory( 32 | d2GraphWriter: d2GraphWriter, 33 | dotGraphWriter: dotGraphWriter, 34 | mermaidGraphWriter: mermaidGraphWriter 35 | ) 36 | let command = GraphCommand(projectRootClassifier: ProjectRootClassifierMock(), 37 | packageSwiftFileParser: PackageSwiftFileParserMock(), 38 | xcodeProjectParser: XcodeProjectParserMock(), 39 | packageGraphBuilder: PackageGraphBuilderMock(), 40 | xcodeProjectGraphBuilder: XcodeProjectGraphBuilderMock(), 41 | directedGraphWriterFactory: directedGraphWriterFactory) 42 | let fileURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example") 43 | try command.run(withInput: fileURL.path, syntax: .dot) 44 | XCTAssertFalse(d2GraphWriter.didWrite) 45 | XCTAssertTrue(dotGraphWriter.didWrite) 46 | XCTAssertFalse(mermaidGraphWriter.didWrite) 47 | } 48 | 49 | func testInvokesOnlyMermaidGraphWriter() throws { 50 | let d2GraphWriter = DirectedGraphWriterMock() 51 | let dotGraphWriter = DirectedGraphWriterMock() 52 | let mermaidGraphWriter = DirectedGraphWriterMock() 53 | let directedGraphWriterFactory = DirectedGraphWriterFactory( 54 | d2GraphWriter: d2GraphWriter, 55 | dotGraphWriter: dotGraphWriter, 56 | mermaidGraphWriter: mermaidGraphWriter 57 | ) 58 | let command = GraphCommand(projectRootClassifier: ProjectRootClassifierMock(), 59 | packageSwiftFileParser: PackageSwiftFileParserMock(), 60 | xcodeProjectParser: XcodeProjectParserMock(), 61 | packageGraphBuilder: PackageGraphBuilderMock(), 62 | xcodeProjectGraphBuilder: XcodeProjectGraphBuilderMock(), 63 | directedGraphWriterFactory: directedGraphWriterFactory) 64 | let fileURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example") 65 | try command.run(withInput: fileURL.path, syntax: .mermaid) 66 | XCTAssertFalse(d2GraphWriter.didWrite) 67 | XCTAssertFalse(dotGraphWriter.didWrite) 68 | XCTAssertTrue(mermaidGraphWriter.didWrite) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/GraphCommandTests/Mock/DirectedGraphWriterMock.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphWriter 3 | 4 | final class DirectedGraphWriterMock: DirectedGraphWriter { 5 | private(set) var didWrite = false 6 | 7 | func write(_ directedGraph: DirectedGraph) throws { 8 | didWrite = true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tests/GraphCommandTests/Mock/PackageDependencyGraphBuilderMock.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import PackageGraphBuilder 3 | import PackageSwiftFile 4 | 5 | struct PackageGraphBuilderMock: PackageGraphBuilder { 6 | func buildGraph(from packageSwiftFile: PackageSwiftFile) throws -> DirectedGraph { 7 | return DirectedGraph() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/GraphCommandTests/Mock/PackageSwiftFileParserMocker.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackageSwiftFile 3 | import PackageSwiftFileParser 4 | 5 | struct PackageSwiftFileParserMock: PackageSwiftFileParser { 6 | func parseFile(at fileURL: URL) throws -> PackageSwiftFile { 7 | return PackageSwiftFile(name: "Example") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/GraphCommandTests/Mock/ProjectRootClassifierMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ProjectRootClassifier 3 | 4 | struct ProjectRootClassifierMock: ProjectRootClassifier { 5 | func classifyProject(at fileURL: URL) -> ProjectRoot { 6 | return .xcodeproj(fileURL) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/GraphCommandTests/Mock/XcodeProjectDependencyGraphBuilderMock.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import XcodeProject 3 | import XcodeProjectGraphBuilder 4 | 5 | struct XcodeProjectGraphBuilderMock: XcodeProjectGraphBuilder { 6 | func buildGraph(from xcodeProject: XcodeProject) throws -> DirectedGraph { 7 | return DirectedGraph() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/GraphCommandTests/Mock/XcodeProjectParserMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XcodeProject 3 | import XcodeProjectParser 4 | 5 | struct XcodeProjectParserMock: XcodeProjectParser { 6 | func parseProject(at fileURL: URL) throws -> XcodeProject { 7 | return XcodeProject(name: "Example") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/MappingDirectedGraphWriterTests/MappingDirectedGraphWriterTests.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphMapper 3 | @testable import MappingDirectedGraphWriter 4 | import Writer 5 | import XCTest 6 | 7 | final class MappingDirectedGraphWriterTests: XCTestCase { 8 | func testInvokesMapper() throws { 9 | let mapper = DirectedGraphMapperMock() 10 | let writer = WriterMock() 11 | let obj = MappingDirectedGraphWriter(mapper: mapper, writer: writer) 12 | let graph = DirectedGraph() 13 | try obj.write(graph) 14 | XCTAssertTrue(mapper.didMap) 15 | } 16 | 17 | func testWritesMappedValue() throws { 18 | let mappedValue = "Hello world!" 19 | let mapper = DirectedGraphMapperMock(result: mappedValue) 20 | let writer = WriterMock() 21 | let obj = MappingDirectedGraphWriter(mapper: mapper, writer: writer) 22 | let graph = DirectedGraph() 23 | try obj.write(graph) 24 | XCTAssertEqual(writer.writtenValue, mappedValue) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/MappingDirectedGraphWriterTests/Mock/DirectedGraphMapperMock.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphMapper 3 | 4 | final class DirectedGraphMapperMock: DirectedGraphMapper { 5 | private(set) var didMap = false 6 | 7 | private let result: String 8 | 9 | init(result: String = "Hello world!") { 10 | self.result = result 11 | } 12 | 13 | func map(_ graph: DirectedGraph) throws -> String { 14 | didMap = true 15 | return result 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/MappingDirectedGraphWriterTests/Mock/WriterMock.swift: -------------------------------------------------------------------------------- 1 | import Writer 2 | 3 | final class WriterMock: Writer { 4 | private(set) var writtenValue: String? 5 | 6 | func write(_ input: String) throws { 7 | writtenValue = input 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/MermaidGraphMapperTests/MermaidGraphMapperTests.swift: -------------------------------------------------------------------------------- 1 | @testable import MermaidGraphMapper 2 | import XCTest 3 | 4 | final class MermaidGraphMapperTests: XCTestCase { 5 | func testMapsGraphToMermaidSyntax() throws { 6 | let settings = MermaidGraphSettings(nodeSpacing: 100, rankSpacing: 250) 7 | let mapper = MermaidGraphMapper(settings: settings) 8 | let string = try mapper.map(.mock) 9 | let expectedString = """ 10 | graph LR 11 | %%{init:{'flowchart':{'nodeSpacing': 100, 'rankSpacing': 250}}}%% 12 | 13 | subgraph Foo[Foo] 14 | %%{init:{'flowchart':{'nodeSpacing': 100, 'rankSpacing': 250}}}%% 15 | Foo[Foo] 16 | end 17 | 18 | subgraph Bar[Bar] 19 | %%{init:{'flowchart':{'nodeSpacing': 100, 'rankSpacing': 250}}}%% 20 | Bar[Bar] 21 | end 22 | 23 | subgraph Baz[Baz] 24 | %%{init:{'flowchart':{'nodeSpacing': 100, 'rankSpacing': 250}}}%% 25 | Baz([Baz]) 26 | end 27 | 28 | Foo --> Bar 29 | Foo --> Baz 30 | """ 31 | XCTAssertEqual(string, expectedString) 32 | } 33 | 34 | func testMapsGraphWithRootNodesToMermaidSyntax() throws { 35 | let settings = MermaidGraphSettings(nodeSpacing: 100, rankSpacing: 250) 36 | let mapper = MermaidGraphMapper(settings: settings) 37 | let string = try mapper.map(.mockWithRootNodes) 38 | let expectedString = """ 39 | graph LR 40 | %%{init:{'flowchart':{'nodeSpacing': 100, 'rankSpacing': 250}}}%% 41 | 42 | Foo[Foo] 43 | Bar[Bar] 44 | Baz([Baz]) 45 | 46 | Foo --> Bar 47 | Foo --> Baz 48 | """ 49 | XCTAssertEqual(string, expectedString) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/MermaidGraphMapperTests/Mock/DirectedGraph.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | 3 | extension DirectedGraph { 4 | static var mock: DirectedGraph { 5 | let fooNode = DirectedGraph.Node(name: "Foo", label: "Foo") 6 | let fooCluster = DirectedGraph.Cluster(name: "Foo", label: "Foo", nodes: [fooNode]) 7 | let barNode = DirectedGraph.Node(name: "Bar", label: "Bar") 8 | let barCluster = DirectedGraph.Cluster(name: "Bar", label: "Bar", nodes: [barNode]) 9 | let bazNode = DirectedGraph.Node(name: "Baz", label: "Baz", shape: .ellipse) 10 | let bazCluster = DirectedGraph.Cluster(name: "Baz", label: "Baz", nodes: [bazNode]) 11 | return DirectedGraph(clusters: [ 12 | fooCluster, 13 | barCluster, 14 | bazCluster 15 | ], edges: [ 16 | DirectedGraph.Edge(from: fooNode, to: barNode), 17 | DirectedGraph.Edge(from: fooNode, to: bazNode) 18 | ]) 19 | } 20 | 21 | static var mockWithRootNodes: DirectedGraph { 22 | let fooNode = DirectedGraph.Node(name: "Foo", label: "Foo") 23 | let barNode = DirectedGraph.Node(name: "Bar", label: "Bar") 24 | let bazNode = DirectedGraph.Node(name: "Baz", label: "Baz", shape: .ellipse) 25 | return DirectedGraph(nodes: [ 26 | fooNode, 27 | barNode, 28 | bazNode 29 | ], edges: [ 30 | DirectedGraph.Edge(from: fooNode, to: barNode), 31 | DirectedGraph.Edge(from: fooNode, to: bazNode) 32 | ]) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/PackageGraphBuilderLiveTests/Mock/PackageSwiftFile+Mock.swift: -------------------------------------------------------------------------------- 1 | import PackageSwiftFile 2 | 3 | extension PackageSwiftFile { 4 | static var noDependenciesMock: PackageSwiftFile { 5 | return PackageSwiftFile( 6 | name: "ExamplePackageA", 7 | products: [ 8 | PackageSwiftFile.Product(name: "ExampleLibraryA", targets: ["ExampleLibraryA"]) 9 | ], 10 | targets: [ 11 | PackageSwiftFile.Target(name: "ExampleLibraryA") 12 | ] 13 | ) 14 | } 15 | 16 | static var withDependenciesMock: PackageSwiftFile { 17 | return PackageSwiftFile( 18 | name: "ExamplePackageA", 19 | products: [ 20 | PackageSwiftFile.Product(name: "ExampleLibraryA", targets: ["ExampleLibraryA"]) 21 | ], 22 | targets: [ 23 | PackageSwiftFile.Target(name: "ExampleLibraryA", dependencies: [ 24 | .product("ExampleLibraryB", inPackage: "ExamplePackageB") 25 | ]) 26 | ], 27 | dependencies: [ 28 | .fileSystem( 29 | identity: "examplepackageb", 30 | path: "/Users/simon/Developer/Example/ExamplePackageB", 31 | packageSwiftFile: .examplePackageB 32 | ) 33 | ] 34 | ) 35 | } 36 | } 37 | 38 | private extension PackageSwiftFile { 39 | static var examplePackageB: PackageSwiftFile { 40 | return PackageSwiftFile( 41 | name: "ExamplePackageB", 42 | products: [ 43 | PackageSwiftFile.Product(name: "ExampleLibraryB", targets: ["ExampleLibraryB"]) 44 | ], 45 | targets: [ 46 | PackageSwiftFile.Target(name: "ExampleLibraryB", dependencies: [ 47 | .name("ExampleLibraryBFoo"), 48 | .product("ExampleLibraryC", inPackage: "ExamplePackageC") 49 | ]), 50 | PackageSwiftFile.Target(name: "ExampleLibraryBFoo") 51 | ], 52 | dependencies: [ 53 | .fileSystem( 54 | identity: "examplepackagec", 55 | path: "/Users/simon/Developer/Example/ExamplePackageC", 56 | packageSwiftFile: .examplePackageC 57 | ) 58 | ] 59 | ) 60 | } 61 | 62 | static var examplePackageC: PackageSwiftFile { 63 | return PackageSwiftFile( 64 | name: "ExamplePackageC", 65 | products: [ 66 | PackageSwiftFile.Product(name: "ExampleLibraryC", targets: ["ExampleLibraryC"]) 67 | ], 68 | targets: [ 69 | PackageSwiftFile.Target(name: "ExampleLibraryC") 70 | ] 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/PackageGraphBuilderLiveTests/PackageGraphBuilderLiveTests.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import DirectedGraphXcodeHelpers 3 | @testable import PackageGraphBuilderLive 4 | import PackageSwiftFile 5 | import XCTest 6 | 7 | final class PackageGraphBuilderLiveTests: XCTestCase { 8 | func testParsesPackageWithNoDependencies() throws { 9 | let graphBuilder = PackageGraphBuilderLive(packagesOnly: false) 10 | let graph = try graphBuilder.buildGraph(from: .noDependenciesMock) 11 | let packageProductNode: DirectedGraph.Node = .packageProduct(labeled: "ExampleLibraryA") 12 | let targetNode: DirectedGraph.Node = .target(labeled: "ExampleLibraryA") 13 | let expectedGraph = DirectedGraph(clusters: [ 14 | .package(labeled: "ExamplePackageA", nodes: [ 15 | packageProductNode, 16 | targetNode 17 | ]) 18 | ], edges: [ 19 | .from(packageProductNode, to: targetNode) 20 | ]) 21 | XCTAssertEqual(graph, expectedGraph) 22 | } 23 | 24 | func testParsesPackageWithDependencies() throws { 25 | let graphBuilder = PackageGraphBuilderLive(packagesOnly: false) 26 | let graph = try graphBuilder.buildGraph(from: .withDependenciesMock) 27 | 28 | let packageProductNodeA: DirectedGraph.Node = .packageProduct(labeled: "ExampleLibraryA") 29 | let targetNodeA: DirectedGraph.Node = .target(labeled: "ExampleLibraryA") 30 | 31 | let packageProductNodeB: DirectedGraph.Node = .packageProduct(labeled: "ExampleLibraryB") 32 | let targetNodeB: DirectedGraph.Node = .target(labeled: "ExampleLibraryB") 33 | let targetNodeBFoo: DirectedGraph.Node = .target(labeled: "ExampleLibraryBFoo") 34 | 35 | let packageProductNodeC: DirectedGraph.Node = .packageProduct(labeled: "ExampleLibraryC") 36 | let targetNodeC: DirectedGraph.Node = .target(labeled: "ExampleLibraryC") 37 | 38 | let expectedGraph = DirectedGraph(clusters: [ 39 | .package(labeled: "ExamplePackageC", nodes: [ 40 | packageProductNodeC, 41 | targetNodeC 42 | ]), 43 | .package(labeled: "ExamplePackageB", nodes: [ 44 | packageProductNodeB, 45 | targetNodeB, 46 | targetNodeBFoo 47 | ]), 48 | .package(labeled: "ExamplePackageA", nodes: [ 49 | packageProductNodeA, 50 | targetNodeA 51 | ]) 52 | ], edges: [ 53 | .from(packageProductNodeC, to: targetNodeC), 54 | .from(packageProductNodeB, to: targetNodeB), 55 | .from(targetNodeB, to: targetNodeBFoo), 56 | .from(targetNodeB, to: packageProductNodeC), 57 | .from(packageProductNodeA, to: targetNodeA), 58 | .from(targetNodeA, to: packageProductNodeB) 59 | ]) 60 | XCTAssertEqual(graph, expectedGraph) 61 | } 62 | 63 | func testBuildsGraphWithPackagesOnly() throws { 64 | let graphBuilder = PackageGraphBuilderLive(packagesOnly: true) 65 | let graph = try graphBuilder.buildGraph(from: .withDependenciesMock) 66 | let nodeA: DirectedGraph.Node = .package(labeled: "ExamplePackageA") 67 | let nodeB: DirectedGraph.Node = .package(labeled: "ExamplePackageB") 68 | let nodeC: DirectedGraph.Node = .package(labeled: "ExamplePackageC") 69 | let expectedGraph = DirectedGraph(nodes: [nodeA, nodeB, nodeC], edges: [ 70 | .from(nodeB, to: nodeC), 71 | .from(nodeA, to: nodeB) 72 | ]) 73 | XCTAssertEqual(graph, expectedGraph) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/PackageSwiftFileParserLiveTests/Mock/DumpPackageServiceMock.swift: -------------------------------------------------------------------------------- 1 | import DumpPackageService 2 | import Foundation 3 | 4 | struct DumpPackageServiceMock: DumpPackageService { 5 | private let fileURLMap: [URL: URL] = [ 6 | URL.Mock.Example.packageA: Bundle.module.url(forMockDataNamed: "example-package-a"), 7 | URL.Mock.Example.packageB: Bundle.module.url(forMockDataNamed: "example-package-b"), 8 | URL.Mock.Example.packageC: Bundle.module.url(forMockDataNamed: "example-package-c"), 9 | URL.Mock.DependencySyntax.byNameWithPlatformNames: Bundle.module.url(forMockDataNamed: "dependency-syntax-byname-with-platform-names"), 10 | URL.Mock.DependencySyntax.target: Bundle.module.url(forMockDataNamed: "dependency-syntax-target") 11 | ] 12 | 13 | func dumpPackageForSwiftPackageFile(at fileURL: URL) throws -> Data { 14 | let mappedFileURL = fileURLMap[fileURL] ?? fileURL 15 | return try Data(contentsOf: mappedFileURL) 16 | } 17 | } 18 | 19 | private extension Bundle { 20 | func url(forMockDataNamed filename: String) -> URL { 21 | return url(forResource: "MockData/" + filename, withExtension: "json")! 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/PackageSwiftFileParserLiveTests/Mock/PackageSwiftFileParserCacheMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackageSwiftFile 3 | import PackageSwiftFileParserCache 4 | 5 | final class PackageSwiftFileParserCacheMock: PackageSwiftFileParserCache { 6 | private var values: [URL: PackageSwiftFile] = [:] 7 | 8 | func cache(_ packageSwiftFile: PackageSwiftFile, for url: URL) { 9 | values[url] = packageSwiftFile 10 | } 11 | 12 | func cachedPackageSwiftFile(for url: URL) -> PackageSwiftFile? { 13 | return values[url] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/PackageSwiftFileParserLiveTests/Mock/URL+Mock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | enum Mock { 5 | enum Example { 6 | static var packageA: URL { 7 | return NSURL.fileURL(withPath: "/Users/simon/Developer/Example/PackageA/Package.swift") 8 | } 9 | 10 | static var packageB: URL { 11 | return NSURL.fileURL(withPath: "/Users/simon/Developer/Example/PackageB/Package.swift") 12 | } 13 | 14 | static var packageC: URL { 15 | return NSURL.fileURL(withPath: "/Users/simon/Developer/Example/PackageC/Package.swift") 16 | } 17 | } 18 | 19 | enum DependencySyntax { 20 | static var byNameWithPlatformNames: URL { 21 | return NSURL.fileURL(withPath: "/Users/simon/Developer/DependencySyntax/ByNamePlatformNames/Package.swift") 22 | } 23 | 24 | static var target: URL { 25 | return NSURL.fileURL(withPath: "/Users/simon/Developer/DependencySyntax/Target/Package.swift") 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/PackageSwiftFileParserLiveTests/MockData/dependency-syntax-byname-with-platform-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "cLanguageStandard" : null, 3 | "cxxLanguageStandard" : null, 4 | "dependencies" : [ 5 | { 6 | "fileSystem" : [ 7 | { 8 | "identity" : "examplepackagec", 9 | "path" : "/Users/simon/Developer/Example/PackageC", 10 | "productFilter" : null 11 | } 12 | ] 13 | } 14 | ], 15 | "name" : "DependencySyntaxByNameWithPlatformNames", 16 | "packageKind" : { 17 | "root" : [ 18 | "/Users/simon/Developer/DependencySyntax/ByNameWithPlatformNames" 19 | ] 20 | }, 21 | "pkgConfig" : null, 22 | "platforms" : [ 23 | { 24 | "options" : [ 25 | 26 | ], 27 | "platformName" : "ios", 28 | "version" : "14.0" 29 | } 30 | ], 31 | "products" : [ 32 | { 33 | "name" : "ExampleLibraryD", 34 | "settings" : [ 35 | 36 | ], 37 | "targets" : [ 38 | "ExampleLibraryD" 39 | ], 40 | "type" : { 41 | "library" : [ 42 | "automatic" 43 | ] 44 | } 45 | } 46 | ], 47 | "providers" : null, 48 | "swiftLanguageVersions" : null, 49 | "targets" : [ 50 | { 51 | "dependencies" : [ 52 | { 53 | "byName" : [ 54 | "ExampleLibraryC", 55 | { 56 | "platformNames": [ 57 | "ios" 58 | ] 59 | } 60 | ] 61 | } 62 | ], 63 | "exclude" : [ 64 | 65 | ], 66 | "name" : "ExampleLibraryD", 67 | "resources" : [ 68 | 69 | ], 70 | "settings" : [ 71 | 72 | ], 73 | "type" : "regular" 74 | } 75 | ], 76 | "toolsVersion" : { 77 | "_version" : "5.7.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/PackageSwiftFileParserLiveTests/MockData/dependency-syntax-target.json: -------------------------------------------------------------------------------- 1 | { 2 | "cLanguageStandard" : null, 3 | "cxxLanguageStandard" : null, 4 | "dependencies" : [ 5 | { 6 | "fileSystem" : [ 7 | { 8 | "identity" : "examplepackagec", 9 | "path" : "/Users/simon/Developer/Example/PackageC", 10 | "productFilter" : null 11 | } 12 | ] 13 | } 14 | ], 15 | "name" : "DependencySyntaxTarget", 16 | "packageKind" : { 17 | "root" : [ 18 | "/Users/simon/Developer/DependencySyntax/Target" 19 | ] 20 | }, 21 | "pkgConfig" : null, 22 | "platforms" : [ 23 | { 24 | "options" : [ 25 | 26 | ], 27 | "platformName" : "ios", 28 | "version" : "14.0" 29 | } 30 | ], 31 | "products" : [ 32 | { 33 | "name" : "ExampleLibraryD", 34 | "settings" : [ 35 | 36 | ], 37 | "targets" : [ 38 | "ExampleLibraryD" 39 | ], 40 | "type" : { 41 | "library" : [ 42 | "automatic" 43 | ] 44 | } 45 | } 46 | ], 47 | "providers" : null, 48 | "swiftLanguageVersions" : null, 49 | "targets" : [ 50 | { 51 | "dependencies" : [ 52 | { 53 | "target" : [ 54 | "ExampleLibraryC", 55 | null 56 | ] 57 | } 58 | ], 59 | "exclude" : [ 60 | 61 | ], 62 | "name" : "ExampleLibraryD", 63 | "resources" : [ 64 | 65 | ], 66 | "settings" : [ 67 | 68 | ], 69 | "type" : "regular" 70 | } 71 | ], 72 | "toolsVersion" : { 73 | "_version" : "5.7.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/PackageSwiftFileParserLiveTests/MockData/example-package-a.json: -------------------------------------------------------------------------------- 1 | { 2 | "cLanguageStandard" : null, 3 | "cxxLanguageStandard" : null, 4 | "dependencies" : [ 5 | 6 | ], 7 | "name" : "ExamplePackageA", 8 | "packageKind" : { 9 | "root" : [ 10 | "/Users/simon/Developer/Example/PackageA" 11 | ] 12 | }, 13 | "pkgConfig" : null, 14 | "platforms" : [ 15 | { 16 | "options" : [ 17 | 18 | ], 19 | "platformName" : "ios", 20 | "version" : "14.0" 21 | } 22 | ], 23 | "products" : [ 24 | { 25 | "name" : "ExampleLibraryA", 26 | "settings" : [ 27 | 28 | ], 29 | "targets" : [ 30 | "ExampleLibraryA" 31 | ], 32 | "type" : { 33 | "library" : [ 34 | "automatic" 35 | ] 36 | } 37 | } 38 | ], 39 | "providers" : null, 40 | "swiftLanguageVersions" : null, 41 | "targets" : [ 42 | { 43 | "dependencies" : [ 44 | 45 | ], 46 | "exclude" : [ 47 | 48 | ], 49 | "name" : "ExampleLibraryA", 50 | "resources" : [ 51 | 52 | ], 53 | "settings" : [ 54 | 55 | ], 56 | "type" : "regular" 57 | } 58 | ], 59 | "toolsVersion" : { 60 | "_version" : "5.7.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/PackageSwiftFileParserLiveTests/MockData/example-package-b.json: -------------------------------------------------------------------------------- 1 | { 2 | "cLanguageStandard" : null, 3 | "cxxLanguageStandard" : null, 4 | "dependencies" : [ 5 | { 6 | "fileSystem" : [ 7 | { 8 | "identity" : "examplepackagec", 9 | "path" : "/Users/simon/Developer/Example/PackageC", 10 | "productFilter" : null 11 | } 12 | ] 13 | }, 14 | { 15 | "sourceControl" : [ 16 | { 17 | "identity" : "keyboardtoolbar", 18 | "location" : { 19 | "remote" : [ 20 | "git@github.com:simonbs/KeyboardToolbar.git" 21 | ] 22 | }, 23 | "productFilter" : null, 24 | "requirement" : { 25 | "range" : [ 26 | { 27 | "lowerBound" : "0.1.1", 28 | "upperBound" : "1.0.0" 29 | } 30 | ] 31 | } 32 | } 33 | ] 34 | } 35 | ], 36 | "name" : "ExamplePackageB", 37 | "packageKind" : { 38 | "root" : [ 39 | "/Users/simon/Developer/Example/PackageB" 40 | ] 41 | }, 42 | "pkgConfig" : null, 43 | "platforms" : [ 44 | { 45 | "options" : [ 46 | 47 | ], 48 | "platformName" : "ios", 49 | "version" : "14.0" 50 | } 51 | ], 52 | "products" : [ 53 | { 54 | "name" : "ExampleLibraryB", 55 | "settings" : [ 56 | 57 | ], 58 | "targets" : [ 59 | "ExampleLibraryB" 60 | ], 61 | "type" : { 62 | "library" : [ 63 | "automatic" 64 | ] 65 | } 66 | } 67 | ], 68 | "providers" : null, 69 | "swiftLanguageVersions" : null, 70 | "targets" : [ 71 | { 72 | "dependencies" : [ 73 | { 74 | "product" : [ 75 | "ExampleLibraryC", 76 | "ExamplePackageC", 77 | null, 78 | null 79 | ] 80 | }, 81 | { 82 | "product" : [ 83 | "KeyboardToolbar", 84 | "KeyboardToolbar", 85 | null, 86 | { 87 | "platformNames" : [ 88 | "ios" 89 | ] 90 | } 91 | ] 92 | } 93 | ], 94 | "exclude" : [ 95 | 96 | ], 97 | "name" : "ExampleLibraryB", 98 | "resources" : [ 99 | 100 | ], 101 | "settings" : [ 102 | 103 | ], 104 | "type" : "regular" 105 | } 106 | ], 107 | "toolsVersion" : { 108 | "_version" : "5.7.0" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Tests/PackageSwiftFileParserLiveTests/MockData/example-package-c.json: -------------------------------------------------------------------------------- 1 | { 2 | "cLanguageStandard" : null, 3 | "cxxLanguageStandard" : null, 4 | "dependencies" : [ 5 | 6 | ], 7 | "name" : "ExamplePackageC", 8 | "packageKind" : { 9 | "root" : [ 10 | "/Users/simon/Developer/Example/PackageC" 11 | ] 12 | }, 13 | "pkgConfig" : null, 14 | "platforms" : [ 15 | { 16 | "options" : [ 17 | 18 | ], 19 | "platformName" : "ios", 20 | "version" : "14.0" 21 | } 22 | ], 23 | "products" : [ 24 | { 25 | "name" : "ExampleLibraryC", 26 | "settings" : [ 27 | 28 | ], 29 | "targets" : [ 30 | "ExampleLibraryC" 31 | ], 32 | "type" : { 33 | "library" : [ 34 | "automatic" 35 | ] 36 | } 37 | } 38 | ], 39 | "providers" : null, 40 | "swiftLanguageVersions" : null, 41 | "targets" : [ 42 | { 43 | "dependencies" : [ 44 | 45 | ], 46 | "exclude" : [ 47 | 48 | ], 49 | "name" : "ExampleLibraryC", 50 | "resources" : [ 51 | 52 | ], 53 | "settings" : [ 54 | 55 | ], 56 | "type" : "regular" 57 | } 58 | ], 59 | "toolsVersion" : { 60 | "_version" : "5.7.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/PackageSwiftFileParserLiveTests/PackageSwiftFileParserLiveTests.swift: -------------------------------------------------------------------------------- 1 | import PackageSwiftFile 2 | import PackageSwiftFileParser 3 | @testable import PackageSwiftFileParserLive 4 | import XCTest 5 | 6 | final class PackageSwiftFileParserLiveTests: XCTestCase { 7 | func testParsesName() throws { 8 | let cache = PackageSwiftFileParserCacheMock() 9 | let dumpPackageService = DumpPackageServiceMock() 10 | let parser = PackageSwiftFileParserLive(cache: cache, dumpPackageService: dumpPackageService) 11 | let packageSwiftFile = try parser.parseFile(at: URL.Mock.Example.packageA) 12 | XCTAssertEqual(packageSwiftFile.name, "ExamplePackageA") 13 | } 14 | 15 | func testParsesProducts() throws { 16 | let cache = PackageSwiftFileParserCacheMock() 17 | let dumpPackageService = DumpPackageServiceMock() 18 | let parser = PackageSwiftFileParserLive(cache: cache, dumpPackageService: dumpPackageService) 19 | let packageSwiftFile = try parser.parseFile(at: URL.Mock.Example.packageA) 20 | XCTAssertEqual(packageSwiftFile.products, [ 21 | PackageSwiftFile.Product(name: "ExampleLibraryA", targets: ["ExampleLibraryA"]) 22 | ]) 23 | } 24 | 25 | func testParsesTargets() throws { 26 | let cache = PackageSwiftFileParserCacheMock() 27 | let dumpPackageService = DumpPackageServiceMock() 28 | let parser = PackageSwiftFileParserLive(cache: cache, dumpPackageService: dumpPackageService) 29 | let packageSwiftFile = try parser.parseFile(at: URL.Mock.Example.packageA) 30 | XCTAssertEqual(packageSwiftFile.targets, [ 31 | PackageSwiftFile.Target(name: "ExampleLibraryA") 32 | ]) 33 | } 34 | 35 | func testParsesDependencies() throws { 36 | let cache = PackageSwiftFileParserCacheMock() 37 | let dumpPackageService = DumpPackageServiceMock() 38 | let parser = PackageSwiftFileParserLive(cache: cache, dumpPackageService: dumpPackageService) 39 | let swiftPackageFile = try parser.parseFile(at: URL.Mock.Example.packageB) 40 | XCTAssertEqual(swiftPackageFile.dependencies, [ 41 | .fileSystem( 42 | identity: "examplepackagec", 43 | path: "/Users/simon/Developer/Example/PackageC", 44 | packageSwiftFile: PackageSwiftFile( 45 | name: "ExamplePackageC", 46 | products: [ 47 | .init(name: "ExampleLibraryC", targets: ["ExampleLibraryC"]) 48 | ], 49 | targets: [ 50 | .init(name: "ExampleLibraryC") 51 | ] 52 | ) 53 | ), 54 | .sourceControl(identity: "keyboardtoolbar") 55 | ]) 56 | } 57 | 58 | func testReadsPackageSwiftFileFromCache() throws { 59 | let cachedPackageSwiftFile = PackageSwiftFile(name: "foo") 60 | let fileURL = NSURL.fileURL(withPath: "/Users/simonbs/Developer/foo") 61 | let cache = PackageSwiftFileParserCacheMock() 62 | cache.cache(cachedPackageSwiftFile, for: fileURL) 63 | let dumpPackageService = DumpPackageServiceMock() 64 | let parser = PackageSwiftFileParserLive(cache: cache, dumpPackageService: dumpPackageService) 65 | let parsedPackageSwiftFile = try parser.parseFile(at: fileURL) 66 | XCTAssertEqual(parsedPackageSwiftFile, cachedPackageSwiftFile) 67 | } 68 | 69 | func testParsesDependencySyntaxByNameDependencyWithPlatformNames() throws { 70 | let cache = PackageSwiftFileParserCacheMock() 71 | let dumpPackageService = DumpPackageServiceMock() 72 | let parser = PackageSwiftFileParserLive(cache: cache, dumpPackageService: dumpPackageService) 73 | let packageSwiftFile = try parser.parseFile(at: URL.Mock.DependencySyntax.byNameWithPlatformNames) 74 | let expectedPackageSwiftFile = PackageSwiftFile( 75 | name: "DependencySyntaxByNameWithPlatformNames", 76 | products: [ 77 | PackageSwiftFile.Product(name: "ExampleLibraryD", targets: ["ExampleLibraryD"]) 78 | ], 79 | targets: [ 80 | PackageSwiftFile.Target(name: "ExampleLibraryD", dependencies: [ 81 | .name("ExampleLibraryC") 82 | ]) 83 | ], 84 | dependencies: [ 85 | .fileSystem( 86 | identity: "examplepackagec", 87 | path: "/Users/simon/Developer/Example/PackageC", 88 | packageSwiftFile: PackageSwiftFile( 89 | name: "ExamplePackageC", 90 | products: [ 91 | .init(name: "ExampleLibraryC", targets: ["ExampleLibraryC"]) 92 | ], 93 | targets: [ 94 | .init(name: "ExampleLibraryC") 95 | ] 96 | ) 97 | ) 98 | ]) 99 | XCTAssertEqual(packageSwiftFile, expectedPackageSwiftFile) 100 | } 101 | 102 | func testParsesDependencySyntaxTarget() throws { 103 | let cache = PackageSwiftFileParserCacheMock() 104 | let dumpPackageService = DumpPackageServiceMock() 105 | let parser = PackageSwiftFileParserLive(cache: cache, dumpPackageService: dumpPackageService) 106 | let packageSwiftFile = try parser.parseFile(at: URL.Mock.DependencySyntax.target) 107 | let expectedPackageSwiftFile = PackageSwiftFile( 108 | name: "DependencySyntaxTarget", 109 | products: [ 110 | PackageSwiftFile.Product(name: "ExampleLibraryD", targets: ["ExampleLibraryD"]) 111 | ], 112 | targets: [ 113 | PackageSwiftFile.Target(name: "ExampleLibraryD", dependencies: [ 114 | .name("ExampleLibraryC") 115 | ]) 116 | ], 117 | dependencies: [ 118 | .fileSystem( 119 | identity: "examplepackagec", 120 | path: "/Users/simon/Developer/Example/PackageC", 121 | packageSwiftFile: PackageSwiftFile( 122 | name: "ExamplePackageC", 123 | products: [ 124 | .init(name: "ExampleLibraryC", targets: ["ExampleLibraryC"]) 125 | ], 126 | targets: [ 127 | .init(name: "ExampleLibraryC") 128 | ] 129 | ) 130 | ) 131 | ]) 132 | XCTAssertEqual(packageSwiftFile, expectedPackageSwiftFile) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Tests/ProjectRootClassifierLiveTests/Mock/FileSystemMock.swift: -------------------------------------------------------------------------------- 1 | import FileSystem 2 | import Foundation 3 | 4 | final class FileSystemMock: FileSystem { 5 | var fileExists = true 6 | var isDirectory = false 7 | var directoryContents: [String] = [] 8 | 9 | func fileExists(at itemURL: URL) -> Bool { 10 | return fileExists 11 | } 12 | 13 | func isDirectory(at itemURL: URL) -> Bool { 14 | return isDirectory 15 | } 16 | 17 | func contentsOfDirectory(at directoryURL: URL) -> [String] { 18 | return directoryContents 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/ProjectRootClassifierLiveTests/ProjectRootClassifierLiveTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import ProjectRootClassifierLive 3 | import XCTest 4 | 5 | final class ProjectRootClassifierLiveTests: XCTestCase { 6 | func testClassifiesPathToProject() { 7 | let fileSystem = FileSystemMock() 8 | let classifier = ProjectRootClassifierLive(fileSystem: fileSystem) 9 | let fileURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example/Example.xcodeproj") 10 | let projectRoot = classifier.classifyProject(at: fileURL) 11 | XCTAssertEqual(projectRoot, .xcodeproj(fileURL)) 12 | } 13 | 14 | func testClassifiesPathToPackageSwiftFile() { 15 | let fileSystem = FileSystemMock() 16 | let classifier = ProjectRootClassifierLive(fileSystem: fileSystem) 17 | let fileURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example/Package.swift") 18 | let projectRoot = classifier.classifyProject(at: fileURL) 19 | XCTAssertEqual(projectRoot, .packageSwiftFile(fileURL)) 20 | } 21 | 22 | func testClassifiesDirectoryContainingXcodeproj() { 23 | let fileSystem = FileSystemMock() 24 | fileSystem.isDirectory = true 25 | fileSystem.directoryContents = [".gitignore", "README.md", "Example.xcodeproj"] 26 | let classifier = ProjectRootClassifierLive(fileSystem: fileSystem) 27 | let directoryURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example") 28 | let expectedFileURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example/Example.xcodeproj") 29 | let projectRoot = classifier.classifyProject(at: directoryURL) 30 | XCTAssertEqual(projectRoot, .xcodeproj(expectedFileURL)) 31 | } 32 | 33 | func testClassifiesDirectoryContainingPackageSwiftFile() { 34 | let fileSystem = FileSystemMock() 35 | fileSystem.isDirectory = true 36 | fileSystem.directoryContents = [".gitignore", "README.md", "Package.swift"] 37 | let classifier = ProjectRootClassifierLive(fileSystem: fileSystem) 38 | let directoryURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example") 39 | let expectedFileURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example/Package.swift") 40 | let projectRoot = classifier.classifyProject(at: directoryURL) 41 | XCTAssertEqual(projectRoot, .packageSwiftFile(expectedFileURL)) 42 | } 43 | 44 | func testFailsClassifyingUnknownFile() { 45 | let fileSystem = FileSystemMock() 46 | let classifier = ProjectRootClassifierLive(fileSystem: fileSystem) 47 | let directoryURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example/README.md") 48 | let projectRoot = classifier.classifyProject(at: directoryURL) 49 | XCTAssertEqual(projectRoot, .unknown) 50 | } 51 | 52 | func testFailsClassifyingDirectoryContainingUnknownFiles() { 53 | let fileSystem = FileSystemMock() 54 | fileSystem.isDirectory = true 55 | fileSystem.directoryContents = [".gitignore", "README.md"] 56 | let classifier = ProjectRootClassifierLive(fileSystem: fileSystem) 57 | let directoryURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example") 58 | let projectRoot = classifier.classifyProject(at: directoryURL) 59 | XCTAssertEqual(projectRoot, .unknown) 60 | } 61 | 62 | func testFailsClassifyingNonExistingFile() { 63 | let fileSystem = FileSystemMock() 64 | fileSystem.fileExists = false 65 | fileSystem.directoryContents = [".gitignore", "README.md"] 66 | let classifier = ProjectRootClassifierLive(fileSystem: fileSystem) 67 | let directoryURL = NSURL.fileURL(withPath: "/Users/simon/Developer/Example/README.md") 68 | let projectRoot = classifier.classifyProject(at: directoryURL) 69 | XCTAssertEqual(projectRoot, .unknown) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/StringIndentHelpersTests/StringIndentHelpersTests.swift: -------------------------------------------------------------------------------- 1 | @testable import StringIndentHelpers 2 | import XCTest 3 | 4 | final class StringIndentHelpersTests: XCTestCase { 5 | func testStringIsIndented() { 6 | let string = "foo".indented(by: 1) 7 | XCTAssertEqual(string, " foo") 8 | } 9 | 10 | func testStringIsIndentedByThree() { 11 | let string = "foo".indented(by: 3) 12 | XCTAssertEqual(string, " foo") 13 | } 14 | 15 | func testStringArrayIsIndented() { 16 | let stringArray = ["foo", "bar", "baz"].indented(by: 1) 17 | XCTAssertEqual(stringArray, [" foo", " bar", " baz"]) 18 | } 19 | 20 | func testStringWithLineBreaksIsIndented() { 21 | let stringArray = "foo\nbar\nbaz".indented(by: 1) 22 | XCTAssertEqual(stringArray, " foo\n bar\n baz") 23 | } 24 | 25 | func testArrayOfStringsWithLineBreaksIsIndented() { 26 | let stringArray = ["foo\nbar\nbaz", "foo\nbar\nbaz"].indented(by: 1) 27 | XCTAssertEqual(stringArray, [" foo\n bar\n baz", " foo\n bar\n baz"]) 28 | } 29 | 30 | func testIndentStringWithTwoLineBreaks() { 31 | let string = "foo\n\nbar".indented(by: 1) 32 | XCTAssertEqual(string, " foo\n\n bar") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/XcodeProjectGraphBuilderLiveTests/Mock/PackageDependencyGraphBuilderMock.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | import Foundation 3 | import PackageGraphBuilder 4 | import PackageSwiftFile 5 | 6 | private enum PackageGraphBuilderMockError: LocalizedError { 7 | case mockNotFound(PackageSwiftFile) 8 | 9 | var errorDescription: String? { 10 | switch self { 11 | case .mockNotFound(let packageSwiftFile): 12 | return "Mock not found for package \(packageSwiftFile.name)" 13 | } 14 | } 15 | } 16 | 17 | struct PackageGraphBuilderMock: PackageGraphBuilder { 18 | private let packagesOnly: Bool 19 | 20 | init(packagesOnly: Bool) { 21 | self.packagesOnly = packagesOnly 22 | } 23 | 24 | func buildGraph(from packageSwiftFile: PackageSwiftFile) throws -> DirectedGraph { 25 | switch packageSwiftFile.name { 26 | case "ExamplePackageA": 27 | return .examplePackageA(packagesOnly: packagesOnly) 28 | case "ExamplePackageB": 29 | return .examplePackageB(packagesOnly: packagesOnly) 30 | default: 31 | throw PackageGraphBuilderMockError.mockNotFound(packageSwiftFile) 32 | } 33 | } 34 | } 35 | 36 | private extension DirectedGraph { 37 | static func examplePackageA(packagesOnly: Bool) -> DirectedGraph { 38 | if packagesOnly { 39 | return DirectedGraph(nodes: [.package(labeled: "ExamplePackageA")]) 40 | } else { 41 | let packageProductNode: DirectedGraph.Node = .packageProduct(labeled: "ExampleLibraryA") 42 | let targetNode: DirectedGraph.Node = .target(labeled: "ExampleLibraryA") 43 | let cluster: DirectedGraph.Cluster = .package(labeled: "ExamplePackageA", nodes: [packageProductNode, targetNode]) 44 | return DirectedGraph(clusters: [cluster], edges: [.from(packageProductNode, to: targetNode)]) 45 | } 46 | } 47 | 48 | static func examplePackageB(packagesOnly: Bool) -> DirectedGraph { 49 | if packagesOnly { 50 | return DirectedGraph(nodes: [.package(labeled: "ExamplePackageB")]) 51 | } else { 52 | let packageProductNode: DirectedGraph.Node = .packageProduct(labeled: "ExampleLibraryB") 53 | let targetNode: DirectedGraph.Node = .target(labeled: "ExampleLibraryB") 54 | let cluster: DirectedGraph.Cluster = .package(labeled: "ExamplePackageB", nodes: [packageProductNode, targetNode]) 55 | return DirectedGraph(clusters: [cluster], edges: [.from(packageProductNode, to: targetNode)]) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/XcodeProjectGraphBuilderLiveTests/Mock/PackageSwiftFileParserMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PackageSwiftFile 3 | import PackageSwiftFileParser 4 | 5 | private enum PackageSwiftFileParserMockError: LocalizedError { 6 | case fileURLNotMapped(URL) 7 | 8 | var errorDescription: String? { 9 | switch self { 10 | case .fileURLNotMapped(let fileURL): 11 | return "Package.swift file not mapped for \(fileURL.path)." 12 | } 13 | } 14 | } 15 | 16 | struct PackageSwiftFileParserMock: PackageSwiftFileParser { 17 | func parseFile(at fileURL: URL) throws -> PackageSwiftFile { 18 | switch fileURL.absoluteString { 19 | case "file:///Users/simon/Developer/Example/ExamplePackageA/Package.swift": 20 | return .mockA 21 | case "file:///Users/simon/Developer/Example/ExamplePackageB/Package.swift": 22 | return .mockB 23 | default: 24 | throw PackageSwiftFileParserMockError.fileURLNotMapped(fileURL) 25 | } 26 | } 27 | } 28 | 29 | private extension PackageSwiftFile { 30 | static var mockA: PackageSwiftFile { 31 | return PackageSwiftFile(name: "ExamplePackageA", products: [ 32 | PackageSwiftFile.Product(name: "ExampleLibraryA", targets: ["ExampleLibraryA"]) 33 | ], targets: [ 34 | PackageSwiftFile.Target(name: "ExampleLibraryA") 35 | ]) 36 | } 37 | 38 | static var mockB: PackageSwiftFile { 39 | return PackageSwiftFile(name: "ExamplePackageB", products: [ 40 | PackageSwiftFile.Product(name: "ExampleLibraryB", targets: ["ExampleLibraryB"]) 41 | ], targets: [ 42 | PackageSwiftFile.Target(name: "ExampleLibraryB") 43 | ]) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/XcodeProjectGraphBuilderLiveTests/Mock/URL+Mock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | enum Mock { 5 | static var examplePackageA: URL { 6 | return NSURL.fileURL(withPath: "/Users/simon/Developer/Example/ExamplePackageA/Package.swift") 7 | } 8 | 9 | static var examplePackageB: URL { 10 | return NSURL.fileURL(withPath: "/Users/simon/Developer/Example/ExamplePackageB/Package.swift") 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/XcodeProjectGraphBuilderLiveTests/Mock/XcodeProject+Mock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XcodeProject 3 | 4 | extension XcodeProject { 5 | static var mock: XcodeProject { 6 | return XcodeProject(name: "Example.xcodeproj", targets: [ 7 | XcodeProject.Target(name: "Example", packageProductDependencies: [ 8 | "ExampleLibraryA", 9 | "ExampleLibraryB", 10 | "RemoteA", 11 | "RemoteBFoo", 12 | "RemoteBBar" 13 | ]), 14 | XcodeProject.Target(name: "ExampleTests"), 15 | XcodeProject.Target(name: "ExampleUITests") 16 | ], swiftPackages: [ 17 | .local(name: "ExamplePackageA", fileURL: URL.Mock.examplePackageA), 18 | .local(name: "ExamplePackageB", fileURL: URL.Mock.examplePackageB), 19 | .remote(name: "RemoteA", repositoryURL: URL(string: "https://github.com/simonbs/RemoteA")!, products: [ 20 | "RemoteA" 21 | ]), 22 | .remote(name: "RemoteB", repositoryURL: URL(string: "git@github.com:simonbs/RemoteB.git")!, products: [ 23 | "RemoteBFoo", 24 | "RemoteBBar" 25 | ]) 26 | ]) 27 | } 28 | 29 | static var mockWithMissingDependency: XcodeProject { 30 | return XcodeProject(name: "Example.xcodeproj", targets: [ 31 | XcodeProject.Target(name: "Example", packageProductDependencies: [ 32 | "ExampleLibraryA" 33 | ]) 34 | ]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/XcodeProjectGraphBuilderLiveTests/XcodeProjectGraphBuilderLiveTests.swift: -------------------------------------------------------------------------------- 1 | import DirectedGraph 2 | @testable import XcodeProjectGraphBuilderLive 3 | import XCTest 4 | 5 | final class XcodeProjectGraphBuilderLiveTests: XCTestCase { 6 | // swiftlint:disable:next function_body_length 7 | func testBuildsGraphs() throws { 8 | let packageSwiftFileParser = PackageSwiftFileParserMock() 9 | let packageGraphBuilder = PackageGraphBuilderMock(packagesOnly: false) 10 | let graphBuilder = XcodeProjectGraphBuilderLive(packageSwiftFileParser: packageSwiftFileParser, 11 | packageGraphBuilder: packageGraphBuilder, 12 | packagesOnly: false) 13 | let graph = try graphBuilder.buildGraph(from: .mock) 14 | 15 | let exampleLibraryAPackageProductNode: DirectedGraph.Node = .packageProduct(labeled: "ExampleLibraryA") 16 | let exampleLibraryATargetNode: DirectedGraph.Node = .target(labeled: "ExampleLibraryA") 17 | let examplePackageACluster: DirectedGraph.Cluster = .package(labeled: "ExamplePackageA", nodes: [ 18 | exampleLibraryAPackageProductNode, 19 | exampleLibraryATargetNode 20 | ]) 21 | 22 | let exampleLibraryBPackageProductNode: DirectedGraph.Node = .packageProduct(labeled: "ExampleLibraryB") 23 | let exampleLibraryBTargetNode: DirectedGraph.Node = .target(labeled: "ExampleLibraryB") 24 | let examplePackageBCluster: DirectedGraph.Cluster = .package(labeled: "ExamplePackageB", nodes: [ 25 | exampleLibraryBPackageProductNode, 26 | exampleLibraryBTargetNode 27 | ]) 28 | 29 | let remoteAPackageProductNode: DirectedGraph.Node = .packageProduct(labeled: "RemoteA") 30 | let remoteAPackageCluster: DirectedGraph.Cluster = .package(labeled: "RemoteA", nodes: [ 31 | remoteAPackageProductNode 32 | ]) 33 | 34 | let remoteBFooPackageProductNode: DirectedGraph.Node = .packageProduct(labeled: "RemoteBFoo") 35 | let remoteBBarPackageProductNode: DirectedGraph.Node = .packageProduct(labeled: "RemoteBBar") 36 | let remoteBPackageCluster: DirectedGraph.Cluster = .package(labeled: "RemoteB", nodes: [ 37 | remoteBFooPackageProductNode, 38 | remoteBBarPackageProductNode 39 | ]) 40 | 41 | let exampleTargetNode: DirectedGraph.Node = .target(labeled: "Example") 42 | let exampleTestsTargetNode: DirectedGraph.Node = .target(labeled: "ExampleTests") 43 | let exampleUITestsTargetNode: DirectedGraph.Node = .target(labeled: "ExampleUITests") 44 | let projectCluster: DirectedGraph.Cluster = .project(labeled: "Example.xcodeproj", nodes: [ 45 | exampleTargetNode, 46 | exampleTestsTargetNode, 47 | exampleUITestsTargetNode 48 | ]) 49 | 50 | let expectedGraph = DirectedGraph(clusters: [ 51 | examplePackageACluster, 52 | examplePackageBCluster, 53 | remoteAPackageCluster, 54 | remoteBPackageCluster, 55 | projectCluster 56 | ], edges: [ 57 | .from(exampleLibraryAPackageProductNode, to: exampleLibraryATargetNode), 58 | .from(exampleLibraryBPackageProductNode, to: exampleLibraryBTargetNode), 59 | .from(exampleTargetNode, to: exampleLibraryAPackageProductNode), 60 | .from(exampleTargetNode, to: exampleLibraryBPackageProductNode), 61 | .from(exampleTargetNode, to: remoteAPackageProductNode), 62 | .from(exampleTargetNode, to: remoteBFooPackageProductNode), 63 | .from(exampleTargetNode, to: remoteBBarPackageProductNode) 64 | ]) 65 | XCTAssertEqual(graph, expectedGraph) 66 | } 67 | 68 | func testThrowsWhenBuildingGraphWithMissingDependency() throws { 69 | let packageSwiftFileParser = PackageSwiftFileParserMock() 70 | let packageGraphBuilder = PackageGraphBuilderMock(packagesOnly: false) 71 | let graphBuilder = XcodeProjectGraphBuilderLive(packageSwiftFileParser: packageSwiftFileParser, 72 | packageGraphBuilder: packageGraphBuilder, 73 | packagesOnly: false) 74 | XCTAssertThrowsError(try graphBuilder.buildGraph(from: .mockWithMissingDependency)) 75 | } 76 | 77 | func testBuildsGraphWithPackagesOnly() throws { 78 | let packageSwiftFileParser = PackageSwiftFileParserMock() 79 | let packageGraphBuilder = PackageGraphBuilderMock(packagesOnly: true) 80 | let graphBuilder = XcodeProjectGraphBuilderLive(packageSwiftFileParser: packageSwiftFileParser, 81 | packageGraphBuilder: packageGraphBuilder, 82 | packagesOnly: true) 83 | let graph = try graphBuilder.buildGraph(from: .mock) 84 | 85 | let nodeProject: DirectedGraph.Node = .project(labeled: "Example.xcodeproj") 86 | let nodePackageA: DirectedGraph.Node = .package(labeled: "ExamplePackageA") 87 | let nodePackageB: DirectedGraph.Node = .package(labeled: "ExamplePackageB") 88 | let nodeRemoteA: DirectedGraph.Node = .package(labeled: "RemoteA") 89 | let nodeRemoteB: DirectedGraph.Node = .package(labeled: "RemoteB") 90 | 91 | let expectedGraph = DirectedGraph(nodes: [ 92 | nodeProject, 93 | nodePackageA, 94 | nodePackageB, 95 | nodeRemoteA, 96 | nodeRemoteB 97 | ], edges: [ 98 | .from(nodeProject, to: nodePackageA), 99 | .from(nodeProject, to: nodePackageB), 100 | .from(nodeProject, to: nodeRemoteA), 101 | .from(nodeProject, to: nodeRemoteB) 102 | ]) 103 | XCTAssertEqual(graph, expectedGraph) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/Example/ExamplePackageA/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/Example/ExamplePackageA/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/Example/ExamplePackageA/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "ExamplePackageA", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "ExampleLibraryA", targets: ["ExampleLibraryA"]) 11 | ], 12 | targets: [ 13 | .target(name: "ExampleLibraryA") 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/Example/ExamplePackageB/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/Example/ExamplePackageB/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/Example/ExamplePackageB/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "ExamplePackageB", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "ExampleLibraryB", targets: ["ExampleLibraryB"]) 11 | ], 12 | dependencies: [ 13 | .package(path: "../ExamplePackageC"), 14 | .package(url: "git@github.com:simonbs/KeyboardToolbar.git", from: "0.1.1") 15 | ], 16 | targets: [ 17 | .target(name: "ExampleLibraryB", dependencies: [ 18 | .product(name: "ExampleLibraryC", package: "ExamplePackageC"), 19 | .product(name: "KeyboardToolbar", package: "KeyboardToolbar") 20 | ]) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/Example/ExamplePackageC/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/Example/ExamplePackageC/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "ExamplePackageC", 8 | platforms: [.iOS(.v14)], 9 | products: [ 10 | .library(name: "ExampleLibraryC", targets: ["ExampleLibraryC"]) 11 | ], 12 | targets: [ 13 | .target(name: "ExampleLibraryC") 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/Mock/FileExistenceCheckerMock.swift: -------------------------------------------------------------------------------- 1 | import FileSystem 2 | import Foundation 3 | 4 | struct FileSystemMock: FileSystem { 5 | func fileExists(at itemURL: URL) -> Bool { 6 | return true 7 | } 8 | 9 | func isDirectory(at itemURL: URL) -> Bool { 10 | return false 11 | } 12 | 13 | func contentsOfDirectory(at directoryURL: URL) -> [String] { 14 | return [] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/XcodeProjectParserLiveTests/XcodeProjectParserLiveTests.swift: -------------------------------------------------------------------------------- 1 | @testable import XcodeProject 2 | @testable import XcodeProjectParserLive 3 | import XCTest 4 | 5 | final class XcodeProjectParserLiveTests: XCTestCase { 6 | func testParsesProjectName() throws { 7 | let parser = XcodeProjectParserLive(fileSystem: FileSystemMock()) 8 | let xcodeProject = try parser.parseProject(at: URL.Mock.exampleXcodeProject) 9 | XCTAssertEqual(xcodeProject.name, "Example.xcodeproj") 10 | } 11 | 12 | func testParsesTargets() throws { 13 | let parser = XcodeProjectParserLive(fileSystem: FileSystemMock()) 14 | let xcodeProject = try parser.parseProject(at: URL.Mock.exampleXcodeProject) 15 | let exampleTarget = xcodeProject.targets.first { $0.name == "Example" } 16 | let exampleTestsTarget = xcodeProject.targets.first { $0.name == "ExampleTests" } 17 | let exampleUITestsTarget = xcodeProject.targets.first { $0.name == "ExampleUITests" } 18 | XCTAssertNotNil(exampleTarget) 19 | XCTAssertNotNil(exampleTestsTarget) 20 | XCTAssertNotNil(exampleUITestsTarget) 21 | } 22 | 23 | func testParsesTargetPackageProductDependencies() throws { 24 | let parser = XcodeProjectParserLive(fileSystem: FileSystemMock()) 25 | let xcodeProject = try parser.parseProject(at: URL.Mock.exampleXcodeProject) 26 | let exampleTarget = xcodeProject.targets.first { $0.name == "Example" } 27 | let packageProductDependencies = exampleTarget?.packageProductDependencies ?? [] 28 | XCTAssertTrue(packageProductDependencies.contains("Runestone")) 29 | XCTAssertTrue(packageProductDependencies.contains("TreeSitterJSONRunestone")) 30 | XCTAssertTrue(packageProductDependencies.contains("TreeSitterJavaScriptRunestone")) 31 | XCTAssertTrue(packageProductDependencies.contains("ExampleLibraryA")) 32 | XCTAssertTrue(packageProductDependencies.contains("ExampleLibraryB")) 33 | } 34 | 35 | func testSwiftPackageCount() throws { 36 | let parser = XcodeProjectParserLive(fileSystem: FileSystemMock()) 37 | let xcodeProject = try parser.parseProject(at: URL.Mock.exampleXcodeProject) 38 | XCTAssertEqual(xcodeProject.swiftPackages.count, 4) 39 | } 40 | 41 | func testParsesLocalSwiftPackage() throws { 42 | let parser = XcodeProjectParserLive(fileSystem: FileSystemMock()) 43 | let xcodeProject = try parser.parseProject(at: URL.Mock.exampleXcodeProject) 44 | let swiftPackage = xcodeProject.swiftPackages.first { $0.name == "ExamplePackageA" } 45 | XCTAssertNotNil(swiftPackage) 46 | if case let .local(parameters) = swiftPackage { 47 | XCTAssertEqual(parameters.name, "ExamplePackageA") 48 | let fileURLHasPackageSwiftSuffix = parameters.fileURL.absoluteString.hasSuffix("ExamplePackageA/Package.swift") 49 | XCTAssertTrue(fileURLHasPackageSwiftSuffix, "Expected file URL to end with the package name and Package.swift") 50 | } else { 51 | XCTFail("Expected ExamplePackageA to be a local package") 52 | } 53 | } 54 | 55 | func testParsesRemoteSwiftPackageWithSingleProduct() throws { 56 | let parser = XcodeProjectParserLive(fileSystem: FileSystemMock()) 57 | let xcodeProject = try parser.parseProject(at: URL.Mock.exampleXcodeProject) 58 | let swiftPackage = xcodeProject.swiftPackages.first { $0.name == "Runestone" } 59 | XCTAssertNotNil(swiftPackage) 60 | if case let .remote(parameters) = swiftPackage { 61 | XCTAssertEqual(parameters.name, "Runestone") 62 | XCTAssertEqual(parameters.repositoryURL, URL(string: "https://github.com/simonbs/Runestone")) 63 | XCTAssertEqual(parameters.products, ["Runestone"]) 64 | } else { 65 | XCTFail("Expected Runestone to be a remote package") 66 | } 67 | } 68 | 69 | func testParsesRemoteSwiftPackageWithMultipleProducts() throws { 70 | let parser = XcodeProjectParserLive(fileSystem: FileSystemMock()) 71 | let xcodeProject = try parser.parseProject(at: URL.Mock.exampleXcodeProject) 72 | let swiftPackage = xcodeProject.swiftPackages.first { $0.name == "TreeSitterLanguages" } 73 | XCTAssertNotNil(swiftPackage) 74 | if case let .remote(parameters) = swiftPackage { 75 | XCTAssertEqual(parameters.name, "TreeSitterLanguages") 76 | XCTAssertEqual(parameters.repositoryURL, URL(string: "git@github.com:simonbs/TreeSitterLanguages.git")) 77 | XCTAssertTrue(parameters.products.contains("TreeSitterJSONRunestone")) 78 | XCTAssertTrue(parameters.products.contains("TreeSitterJavaScriptRunestone")) 79 | } else { 80 | XCTFail("Expected TreeSitterLanguages to be a remote package") 81 | } 82 | } 83 | } 84 | 85 | private extension URL { 86 | enum Mock { 87 | static let exampleXcodeProject = Bundle.module.url(forResource: "Example/Example", withExtension: "xcodeproj")! 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /example-d2-elk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbs/dependency-graph/6c89d4be7c90bdaea7feecf979f9ccab947c3188/example-d2-elk.png -------------------------------------------------------------------------------- /example-d2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbs/dependency-graph/6c89d4be7c90bdaea7feecf979f9ccab947c3188/example-d2.png -------------------------------------------------------------------------------- /example-dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbs/dependency-graph/6c89d4be7c90bdaea7feecf979f9ccab947c3188/example-dot.png -------------------------------------------------------------------------------- /example-mermaid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbs/dependency-graph/6c89d4be7c90bdaea7feecf979f9ccab947c3188/example-mermaid.png -------------------------------------------------------------------------------- /example-packages-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbs/dependency-graph/6c89d4be7c90bdaea7feecf979f9ccab947c3188/example-packages-only.png -------------------------------------------------------------------------------- /example-swift-package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbs/dependency-graph/6c89d4be7c90bdaea7feecf979f9ccab947c3188/example-swift-package.png -------------------------------------------------------------------------------- /example-xcodeproj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbs/dependency-graph/6c89d4be7c90bdaea7feecf979f9ccab947c3188/example-xcodeproj.png --------------------------------------------------------------------------------