├── .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 | [](https://github.com/simonbs/dependency-graph/actions/workflows/build_and_test.yml) [](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 | |
|
|
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 |
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 |
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 |
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 |
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 |
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
--------------------------------------------------------------------------------