├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ └── config.yml └── pull_request_template.md ├── .gitignore ├── .spi.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── IntegrationTests ├── Package.swift ├── README.md └── Tests │ ├── CombinedDocumentationTests.swift │ ├── DocCArchiveIndexGenerationTests.swift │ ├── DocCConvertSynthesizedSymbolsTests.swift │ ├── Fixtures │ ├── LibraryTargetWithExtensionSymbols │ │ ├── Package.swift │ │ └── Sources │ │ │ └── Library │ │ │ └── Library.swift │ ├── MixedTargets │ │ ├── Package.swift │ │ ├── Sources │ │ │ ├── Executable │ │ │ │ └── Executable.swift │ │ │ └── Library │ │ │ │ └── Library.swift │ │ └── Tests │ │ │ └── Test │ │ │ └── FooTest.swift │ ├── PackageWithConformanceSymbols │ │ ├── Package.swift │ │ └── Sources │ │ │ └── PackageWithConformanceSymbols │ │ │ └── FooTest.swift │ ├── PackageWithSnippets │ │ ├── Package.swift │ │ ├── Snippets │ │ │ ├── Subdir │ │ │ │ └── SnippetInSubdir.swift │ │ │ └── TestTest.swift │ │ └── Sources │ │ │ └── Library │ │ │ └── Library.swift │ ├── SingleExecutableTarget │ │ ├── Package.swift │ │ └── Sources │ │ │ └── Executable │ │ │ └── Executable.swift │ ├── SingleLibraryTarget │ │ ├── Package.swift │ │ └── Sources │ │ │ └── Library │ │ │ └── Library.swift │ ├── SingleTestTarget │ │ ├── Package.swift │ │ └── Tests │ │ │ └── Test │ │ │ └── FooTest.swift │ ├── TargetWithDocCCatalog │ │ ├── Package.swift │ │ └── Sources │ │ │ └── LibraryWithDocCCatalog │ │ │ ├── Documentation.docc │ │ │ ├── Article One.md │ │ │ └── Article Two.md │ │ │ └── Library.swift │ └── TargetsWithDependencies │ │ ├── Package.swift │ │ └── Sources │ │ ├── InnerFirst │ │ └── SomeFile.swift │ │ ├── InnerSecond │ │ └── SomeFile.swift │ │ ├── NestedInner │ │ └── SomeFile.swift │ │ └── Outer │ │ └── SomeFile.swift │ ├── MixedTargetsTests.swift │ ├── SingleExecutableTargetTests.swift │ ├── SingleLibraryTargetTests.swift │ ├── SingleTestTargetTests.swift │ ├── SnippetDocumentationGenerationTests.swift │ ├── SwiftDocCPreviewTests.swift │ ├── SwiftPMSandboxTests.swift │ ├── TargetWithDocCCatalogTests.swift │ ├── TargetWithSwiftExtensionsTests.swift │ └── Utility │ ├── ConcurrencyRequiringTestCase.swift │ ├── FileManager+copyDirectoryWithoutHiddenFiles.swift │ ├── XCTest+filesInDataSubdirectory.swift │ ├── XCTestCase+availablePort.swift │ ├── XCTestCase+setupTemporaryDirectoryForFixture.swift │ ├── XCTestCase+swiftPackage.swift │ └── XCTestCase+waitForTimeInterval.swift ├── LICENSE.txt ├── Package.swift ├── Plugins ├── SharedPackagePluginExtensions │ ├── ArgumentExtractor+extractSpecifiedTargets.swift │ ├── PackageExtensions.swift │ ├── PackageManager+getSymbolGraphsForDocC.swift │ ├── PluginContext+doccExecutableURL.swift │ ├── SnippetExtractor+generateSnippetsForTarget.swift │ ├── SourceModuleTarget+doccCatalogPath.swift │ ├── Target+defaultSymbolGraphOptions.swift │ └── Target+doccArchiveOutputPath.swift ├── Swift-DocC Convert │ ├── SwiftDocCConvert.swift │ └── Symbolic Links │ │ ├── README.md │ │ ├── SharedPackagePluginExtensions │ │ └── SwiftDocCPluginUtilities └── Swift-DocC Preview │ ├── SwiftDocCPreview.swift │ └── Symbolic Links │ ├── README.md │ ├── SharedPackagePluginExtensions │ └── SwiftDocCPluginUtilities ├── README.md ├── Sources ├── Snippets │ ├── Model │ │ └── Snippet.swift │ └── Parsing │ │ └── SnippetParser.swift ├── SwiftDocCPluginDocumentation │ ├── EmptyFile.swift │ ├── README.md │ └── SwiftDocCPlugin.docc │ │ ├── Generating Documentation for Extended Types.md │ │ ├── Generating Documentation for Hosting Online.md │ │ ├── Generating Documentation for a Specific Target.md │ │ ├── Info.plist │ │ ├── Previewing Documentation.md │ │ ├── Publishing to GitHub Pages.md │ │ ├── Resources │ │ ├── extended-type-example.png │ │ └── extended-type-example~dark.png │ │ └── SwiftDocCPlugin.md ├── SwiftDocCPluginUtilities │ ├── BuildGraph │ │ ├── DocumentationBuildGraph.swift │ │ └── DocumentationBuildGraphRunner.swift │ ├── CommandLineArguments │ │ ├── CommandLineArgument.swift │ │ ├── CommandLineArguments.swift │ │ └── ParsedPluginArguments.swift │ ├── DispatchTimeInterval+descriptionInSeconds.swift │ ├── DocCFeatures.swift │ ├── DocumentationTargetKind.swift │ ├── DocumentedPluginFlags │ │ └── DocumentedArgument.swift │ ├── FoundationExtensions │ │ ├── SendableMetatypeShim.swift │ │ └── String+singleQuoted.swift │ ├── HelpInformation.swift │ ├── Lock.swift │ ├── ParsedArguments.swift │ ├── PluginAction.swift │ └── Snippets │ │ └── SnippetExtractor.swift └── snippet-extract │ ├── SnippetBuildCommand.swift │ └── Utility │ ├── SymbolGraph+Snippet.swift │ └── URL+Status.swift ├── Tests └── SwiftDocCPluginUtilitiesTests │ ├── CommandLineArgumentsTests.swift │ ├── DispatchTimeIntervalExtensionTests.swift │ ├── DocCFeaturesTests.swift │ ├── DocumentationBuildGraphRunnerTests.swift │ ├── HelpInformationTests.swift │ ├── ParsedArgumentsTests.swift │ ├── SnippetParseTests.swift │ ├── Snippets │ ├── SnippetExtractTests.swift │ └── SnippetSymbolTests.swift │ ├── Test Fixtures │ ├── DocCConvertHelpFixture.txt │ └── DocCPreviewHelpFixture.txt │ └── Utilities │ └── XCTest+testResourceAsString.swift └── bin ├── check-source ├── test └── update-gh-pages-documentation-site /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Something isn't working as expected 4 | labels: bug 5 | --- 6 | 7 | 17 | 18 | Replace this paragraph with a short description of the incorrect behavior. 19 | If you think this issue has been recently introduced and did not occur in an 20 | earlier version, please note that. If possible, include the last version that 21 | the behavior was correct in addition to your current version. 22 | 23 | ### Checklist 24 | - [ ] If possible, I've reproduced the issue using the `main` branch of this package. 25 | - [ ] This issue hasn't been addressed in an [existing GitHub issue](https://github.com/swiftlang/swift-docc-plugin/issues). 26 | 27 | ### Expected behavior 28 | Describe what you expected to happen. 29 | 30 | ### Actual behavior 31 | Describe or copy/paste the behavior you observe. 32 | 33 | ### Steps to Reproduce 34 | Replace this paragraph with an explanation of how to reproduce the incorrect behavior. 35 | This could include a reduced version of your documentation bundle, 36 | or a link to the documentation (or code) that is exhibiting the issue. 37 | 38 | ### Swift-DocC Plugin Version Information 39 | 40 | **Swift-DocC plugin version:** `1.0.0` for example, or a commit hash. 41 | **Swift Compiler version:** Output from `swiftc --version`. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: A suggestion for a new feature 4 | labels: enhancement 5 | --- 6 | 7 | 13 | 14 | ### Feature Request: __ 15 | 16 | #### Description: 17 | 18 | Replace this paragraph with a description of your proposed feature. 19 | Sample documentation catalogs that show what's missing, 20 | or what new capabilities will be possible, are very helpful! 21 | Provide links to existing issues or external references/discussions, if appropriate. 22 | 23 | #### Motivation: 24 | 25 | Replace this paragraph with a description of the use-case this proposal is trying to serve. 26 | 27 | #### Importance: 28 | 29 | Replace this paragraph with a description of the importance of this change. 30 | Does this feature unlock entirely new use-cases? 31 | Or is it possible to achieve the same functionality with existing functionality 32 | and this change would improve the user experience? 33 | 34 | #### Alternatives Considered 35 | 36 | If you considered alternative approaches for this feature, please include them here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # This source file is part of the Swift.org open source project 2 | # 3 | # Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | # Licensed under Apache License v2.0 with Runtime Library Exception 5 | # 6 | # See https://swift.org/LICENSE.txt for license information 7 | # See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | # This is set to `false` to encourage users to use the provided templates instead 10 | # of beginning with a blank one. 11 | # 12 | # For details, see: https://docs.github.com/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 13 | blank_issues_enabled: false 14 | 15 | contact_links: 16 | - name: Discussion Forum 17 | url: https://forums.swift.org/c/development/swift-docc 18 | about: Ask and answer questions about Swift-DocC 19 | - name: Documentation 20 | url: https://swiftlang.github.io/swift-docc-plugin/documentation/swiftdoccplugin/ 21 | about: Read about how to use the Swift-DocC Plugin in your projects -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Bug/issue #, if applicable: 2 | 3 | ## Summary 4 | 5 | _Provide a description of what your PR addresses, explaining the expected user experience. 6 | Also, provide an overview of your implementation._ 7 | 8 | ## Dependencies 9 | 10 | _Describe any dependencies this PR might have, such as an associated branch in another repository._ 11 | 12 | ## Testing 13 | 14 | _Describe how a reviewer can test the functionality of your PR. Provide test content to test with if 15 | applicable._ 16 | 17 | Steps: 18 | 1. _Provide setup instructions._ 19 | 2. _Explain in detail how the functionality can be tested._ 20 | 21 | ## Checklist 22 | 23 | Make sure you check off the following items. If they cannot be completed, provide a reason. 24 | 25 | - [ ] Added tests 26 | - [ ] Ran the `./bin/test` script and it succeeded 27 | - [ ] Updated documentation if necessary 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm 8 | /gh-pages 9 | Package.resolved 10 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [SwiftDocCPlugin] 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | 4 | 5 | 6 | 7 | ## 1.1.0 8 | 9 | - Adds support for [snippets](https://github.com/apple/swift-evolution/blob/main/proposals/0356-swift-snippets.md). 10 | 11 | ## 1.0.0 12 | 13 | Initial release. 14 | 15 | - Adds the `preview-documentation` and `generate-documentation` commands. 16 | 17 | 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This source file is part of the Swift.org open source project 2 | # 3 | # Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | # Licensed under Apache License v2.0 with Runtime Library Exception 5 | # 6 | # See https://swift.org/LICENSE.txt for license information 7 | # See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | FROM swiftlang/swift:nightly-focal 10 | -------------------------------------------------------------------------------- /IntegrationTests/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import PackageDescription 12 | 13 | let package = Package( 14 | name: "IntegrationTests", 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), 17 | ], 18 | targets: [ 19 | .testTarget( 20 | name: "IntegrationTests", 21 | dependencies: [ 22 | .product(name: "NIOCore", package: "swift-nio"), 23 | .product(name: "NIOPosix", package: "swift-nio"), 24 | ], 25 | path: "Tests", 26 | resources: [ 27 | .copy("Fixtures/SingleLibraryTarget"), 28 | .copy("Fixtures/SingleTestTarget"), 29 | .copy("Fixtures/SingleExecutableTarget"), 30 | .copy("Fixtures/MixedTargets"), 31 | .copy("Fixtures/TargetsWithDependencies"), 32 | .copy("Fixtures/TargetWithDocCCatalog"), 33 | .copy("Fixtures/PackageWithSnippets"), 34 | .copy("Fixtures/PackageWithConformanceSymbols"), 35 | .copy("Fixtures/LibraryTargetWithExtensionSymbols"), 36 | ] 37 | ), 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /IntegrationTests/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | Validation tests that ensure the Swift-DocC plugin compiles and behaves as expected with 4 | a variety of test fixtures. 5 | 6 | 7 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/CombinedDocumentationTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class CombinedDocumentationTests: ConcurrencyRequiringTestCase { 12 | func testCombinedDocumentation() throws { 13 | #if compiler(>=6.0) 14 | let result = try swiftPackage( 15 | "generate-documentation", 16 | "--enable-experimental-combined-documentation", 17 | "--verbose", // Necessary to see the 'docc convert' calls in the log and verify their parameters. 18 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "TargetsWithDependencies") 19 | ) 20 | 21 | result.assertExitStatusEquals(0) 22 | let outputArchives = result.referencedDocCArchives 23 | XCTAssertEqual(outputArchives.count, 1) 24 | XCTAssertEqual(outputArchives.map(\.lastPathComponent), [ 25 | "TargetsWithDependencies.doccarchive", 26 | ]) 27 | 28 | // Verify that the combined archive contains all target's documentation 29 | 30 | let combinedArchiveURL = try XCTUnwrap(outputArchives.first) 31 | let combinedDataDirectoryContents = try filesIn(.dataSubdirectory, of: combinedArchiveURL) 32 | .map(\.relativePath) 33 | .sorted() 34 | 35 | XCTAssertEqual(combinedDataDirectoryContents, [ 36 | "documentation.json", 37 | "documentation/innerfirst.json", 38 | "documentation/innerfirst/somethingpublic.json", 39 | "documentation/innersecond.json", 40 | "documentation/innersecond/somethingpublic.json", 41 | "documentation/nestedinner.json", 42 | "documentation/nestedinner/somethingpublic.json", 43 | "documentation/outer.json", 44 | "documentation/outer/somethingpublic.json", 45 | ]) 46 | 47 | // Verify that each 'docc convert' call was passed the expected dependencies 48 | 49 | let doccConvertCalls = result.standardOutput 50 | .components(separatedBy: .newlines) 51 | .filter { line in 52 | line.hasPrefix("docc invocation: '") && line.utf8.contains("docc convert ".utf8) 53 | }.map { line in 54 | line.trimmingCharacters(in: CharacterSet(charactersIn: "'")) 55 | .components(separatedBy: .whitespaces) 56 | .drop(while: { $0 != "convert" }) 57 | } 58 | 59 | XCTAssertEqual(doccConvertCalls.count, 4) 60 | 61 | func extractDependencyArchives(targetName: String, file: StaticString = #filePath, line: UInt = #line) throws -> [String] { 62 | let arguments = try XCTUnwrap( 63 | doccConvertCalls.first(where: { $0.contains(["--fallback-display-name", targetName]) }), 64 | file: file, line: line 65 | ) 66 | var dependencyPaths: [URL] = [] 67 | 68 | var remaining = arguments[...] 69 | while !remaining.isEmpty { 70 | remaining = remaining.drop(while: { $0 != "--dependency" }).dropFirst(/* the '--dependency' element */) 71 | if let path = remaining.popFirst() { 72 | dependencyPaths.append(URL(fileURLWithPath: path)) 73 | } 74 | } 75 | 76 | return dependencyPaths.map { $0.lastPathComponent }.sorted() 77 | } 78 | // Outer 79 | // ├─ InnerFirst 80 | // ╰─ InnerSecond 81 | // ╰─ NestedInner 82 | 83 | XCTAssertEqual(try extractDependencyArchives(targetName: "Outer"), [ 84 | "InnerFirst.doccarchive", 85 | "InnerSecond.doccarchive", 86 | ], "The outer target has depends on both inner targets") 87 | 88 | XCTAssertEqual(try extractDependencyArchives(targetName: "InnerFirst"), [], "The first inner target has no dependencies") 89 | 90 | XCTAssertEqual(try extractDependencyArchives(targetName: "InnerSecond"), [ 91 | "NestedInner.doccarchive", 92 | ], "The second inner target has depends on the nested inner target") 93 | 94 | XCTAssertEqual(try extractDependencyArchives(targetName: "NestedInner"), [], "The nested inner target has no dependencies") 95 | #else 96 | XCTSkip("This test requires a Swift-DocC version that support the link-dependencies feature") 97 | #endif 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/DocCArchiveIndexGenerationTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class DocCArchiveIndexGenerationTests: ConcurrencyRequiringTestCase { 12 | func testGenerateDocumentationWithIndexingEnabled() throws { 13 | let result = try swiftPackage( 14 | "generate-documentation", 15 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "SingleLibraryTarget") 16 | ) 17 | 18 | result.assertExitStatusEquals(0) 19 | let doccArchiveURL = try XCTUnwrap(result.referencedDocCArchives.first) 20 | 21 | let indexDirectoryContents = try filesIn(.indexSubdirectory, of: doccArchiveURL) 22 | XCTAssertFalse(indexDirectoryContents.isEmpty) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/DocCConvertSynthesizedSymbolsTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class DocCConvertSynthesizedSymbolsTests: ConcurrencyRequiringTestCase { 12 | func testGenerateDocumentationWithSkipSynthesizedSymbolsEnabled() throws { 13 | let result = try swiftPackage( 14 | "generate-documentation", "--experimental-skip-synthesized-symbols", 15 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "PackageWithConformanceSymbols") 16 | ) 17 | 18 | result.assertExitStatusEquals(0) 19 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 20 | 21 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), [ 22 | "documentation/packagewithconformancesymbols.json", 23 | "documentation/packagewithconformancesymbols/foo.json", 24 | ]) 25 | } 26 | 27 | func testGenerateDocumentationWithSynthesizedSymbols() throws { 28 | let result = try swiftPackage( 29 | "generate-documentation", 30 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "PackageWithConformanceSymbols") 31 | ) 32 | 33 | result.assertExitStatusEquals(0) 34 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 35 | 36 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), [ 37 | "documentation/packagewithconformancesymbols.json", 38 | "documentation/packagewithconformancesymbols/foo.json", 39 | "documentation/packagewithconformancesymbols/foo/!=(_:_:).json", 40 | "documentation/packagewithconformancesymbols/foo/equatable-implementations.json", 41 | ]) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/LibraryTargetWithExtensionSymbols/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2023 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "LibraryTargetWithExtensionSymbols", 16 | targets: [ 17 | .target(name: "Library"), 18 | ] 19 | ) 20 | 21 | // We only expect 'swift-docc-plugin' to be a sibling when this package 22 | // is running as part of a test. 23 | // 24 | // This allows the package to compile outside of tests for easier 25 | // test development. 26 | if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { 27 | package.dependencies += [ 28 | .package(path: "../swift-docc-plugin"), 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/LibraryTargetWithExtensionSymbols/Sources/Library/Library.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2023 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is foo's documentation. 10 | /// 11 | /// Foo is a public struct and should be included in documentation. 12 | public struct Foo { 13 | public func foo() {} 14 | } 15 | 16 | /// This is the documentation for ``Swift/Array``. 17 | /// 18 | /// This is the extension to ``Array`` with the longest documentation 19 | /// comment, thus it is used for doucmenting the extended type in this 20 | /// target. 21 | extension Array { 22 | /// This is the documentation for the ``isArray`` property 23 | /// we added to ``Swift/Array``. 24 | /// 25 | /// This is a public extension to an external type and should be included 26 | /// in the documentation. 27 | public var isArray: Bool { true } 28 | } 29 | 30 | /// This is the documentation for ``Swift/Int``. 31 | /// 32 | /// This is the extension to ``Int`` with the longest documentation 33 | /// comment, thus it is used for doucmenting the extended type in this 34 | /// target. 35 | extension Int { 36 | /// This is the documentation for the ``isArray`` property 37 | /// we added to ``Swift/Int``. 38 | /// 39 | /// This is a public extension to an external type and should be included 40 | /// in the documentation. 41 | public var isArray: Bool { false } 42 | } 43 | 44 | 45 | /// This is the documentation for ``CustomFooConvertible``. 46 | /// 47 | /// This is a public protocol and should be included in the documentation. 48 | public protocol CustomFooConvertible { 49 | /// This is the documentation for ``CustomFooConvertible/asFoo``. 50 | /// 51 | /// This is a public protocol requirement and should be included in the documentation. 52 | var asFoo: Foo { get } 53 | } 54 | 55 | /// This is not used as the documentation comment for ``Swift/Int`` 56 | /// as it is shorter than the comment on the other extension to `Int`. 57 | extension Int: CustomFooConvertible { 58 | /// This is the documentation for ``Swift/Int/asFoo``. 59 | /// 60 | /// This is a public protocol requirement implementation and should be included in the documentation. 61 | public var asFoo: Foo { Foo() } 62 | } 63 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/MixedTargets/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "MixedTargets", 16 | targets: [ 17 | .target(name: "Library"), 18 | .executableTarget(name: "Executable"), 19 | .testTarget(name: "Test"), 20 | ] 21 | ) 22 | 23 | // We only expect 'swift-docc-plugin' to be a sibling when this package 24 | // is running as part of a test. 25 | // 26 | // This allows the package to compile outside of tests for easier 27 | // test development. 28 | if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { 29 | package.dependencies += [ 30 | .package(path: "../swift-docc-plugin"), 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/MixedTargets/Sources/Executable/Executable.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is foo's documentation. 10 | /// 11 | /// Foo is an internal struct and should be included in documentation. 12 | @main struct Foo { 13 | func foo() {} 14 | 15 | static func main() {} 16 | 17 | init() {} 18 | } 19 | 20 | /// This is bar's documentation. 21 | /// 22 | /// Bar is a private struct and should not be included in documentation. 23 | private struct Bar { 24 | private func bar() {} 25 | 26 | private init() {} 27 | } 28 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/MixedTargets/Sources/Library/Library.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is foo's documentation. 10 | /// 11 | /// Foo is a public struct and should be included in documentation. 12 | public struct Foo { 13 | public func foo() {} 14 | } 15 | 16 | /// This is bar's documentation. 17 | /// 18 | /// Bar is an internal struct and should not be included in documentation. 19 | struct Bar { 20 | func bar() {} 21 | } 22 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/MixedTargets/Tests/Test/FooTest.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | class FooTest: XCTestCase { 12 | func testFoo() { 13 | XCTAssertTrue(true) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/PackageWithConformanceSymbols/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "PackageWithConformanceSymbols", 16 | targets: [ 17 | .target(name: "PackageWithConformanceSymbols") 18 | ] 19 | ) 20 | 21 | // We only expect 'swift-docc-plugin' to be a sibling when this package 22 | // is running as part of a test. 23 | // 24 | // This allows the package to compile outside of tests for easier 25 | // test development. 26 | if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { 27 | package.dependencies += [ 28 | .package(path: "../swift-docc-plugin"), 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/PackageWithConformanceSymbols/Sources/PackageWithConformanceSymbols/FooTest.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | public struct Foo: Hashable { 10 | } 11 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/PackageWithSnippets/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "PackageWithSnippets", 16 | products: [ 17 | .library(name: "Library", targets: ["Library"]) 18 | ], 19 | targets: [ 20 | .target(name: "Library"), 21 | ] 22 | ) 23 | 24 | // We only expect 'swift-docc-plugin' to be a sibling when this package 25 | // is running as part of a test. 26 | // 27 | // This allows the package to compile outside of tests for easier 28 | // test development. 29 | if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { 30 | package.dependencies += [ 31 | .package(path: "../swift-docc-plugin"), 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/PackageWithSnippets/Snippets/Subdir/SnippetInSubdir.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | // This snippet is in a subdirectory. 10 | 11 | func foo() {} 12 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/PackageWithSnippets/Snippets/TestTest.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | // Create a foo. 10 | import Library 11 | 12 | // snippet.best 13 | let best = BestStruct() 14 | best.best() 15 | // snippet.end 16 | 17 | // snippet.hide 18 | 19 | print(best) 20 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/PackageWithSnippets/Sources/Library/Library.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is BestStruct's documentation. 10 | /// 11 | /// Is best. 12 | public struct BestStruct { 13 | public init() {} 14 | public func best() {} 15 | } 16 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/SingleExecutableTarget/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "SingleExecutableTarget", 16 | targets: [ 17 | .executableTarget(name: "Executable"), 18 | ] 19 | ) 20 | 21 | // We only expect 'swift-docc-plugin' to be a sibling when this package 22 | // is running as part of a test. 23 | // 24 | // This allows the package to compile outside of tests for easier 25 | // test development. 26 | if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { 27 | package.dependencies += [ 28 | .package(path: "../swift-docc-plugin"), 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/SingleExecutableTarget/Sources/Executable/Executable.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is foo's documentation. 10 | /// 11 | /// Foo is an internal struct and should be included in documentation. 12 | @main struct Foo { 13 | func foo() {} 14 | 15 | static func main() {} 16 | 17 | init() {} 18 | } 19 | 20 | /// This is bar's documentation. 21 | /// 22 | /// Bar is a private struct and should not be included in documentation. 23 | private struct Bar { 24 | private func bar() {} 25 | 26 | private init() {} 27 | } 28 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/SingleLibraryTarget/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "SingleLibraryTarget", 16 | targets: [ 17 | .target(name: "Library"), 18 | ] 19 | ) 20 | 21 | // We only expect 'swift-docc-plugin' to be a sibling when this package 22 | // is running as part of a test. 23 | // 24 | // This allows the package to compile outside of tests for easier 25 | // test development. 26 | if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { 27 | package.dependencies += [ 28 | .package(path: "../swift-docc-plugin"), 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/SingleLibraryTarget/Sources/Library/Library.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is foo's documentation. 10 | /// 11 | /// Foo is a public struct and should be included in documentation. 12 | public struct Foo { 13 | public func foo() {} 14 | } 15 | 16 | /// This is bar's documentation. 17 | /// 18 | /// Bar is an internal struct and should not be included in documentation. 19 | struct Bar { 20 | func bar() {} 21 | } 22 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/SingleTestTarget/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "SingleTestTarget", 16 | targets: [ 17 | .testTarget(name: "Test"), 18 | ] 19 | ) 20 | 21 | // We only expect 'swift-docc-plugin' to be a sibling when this package 22 | // is running as part of a test. 23 | // 24 | // This allows the package to compile outside of tests for easier 25 | // test development. 26 | if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { 27 | package.dependencies += [ 28 | .package(path: "../swift-docc-plugin"), 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/SingleTestTarget/Tests/Test/FooTest.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | public class FooTest: XCTestCase { 12 | public func testFoo() { 13 | XCTAssertTrue(true) 14 | } 15 | } 16 | 17 | public struct Bar {} 18 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/TargetWithDocCCatalog/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "TargetWithDocCCatalog", 16 | targets: [ 17 | .target(name: "LibraryWithDocCCatalog"), 18 | ] 19 | ) 20 | 21 | // We only expect 'swift-docc-plugin' to be a sibling when this package 22 | // is running as part of a test. 23 | // 24 | // This allows the package to compile outside of tests for easier 25 | // test development. 26 | if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { 27 | package.dependencies += [ 28 | .package(path: "../swift-docc-plugin"), 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/TargetWithDocCCatalog/Sources/LibraryWithDocCCatalog/Documentation.docc/Article One.md: -------------------------------------------------------------------------------- 1 | # Article One 2 | 3 | This is the first article. 4 | 5 | 6 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/TargetWithDocCCatalog/Sources/LibraryWithDocCCatalog/Documentation.docc/Article Two.md: -------------------------------------------------------------------------------- 1 | # Article Two 2 | 3 | This is the second article. 4 | 5 | 6 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/TargetWithDocCCatalog/Sources/LibraryWithDocCCatalog/Library.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is foo's documentation. 10 | /// 11 | /// Foo is a public struct and should be included in documentation. 12 | public struct Foo { 13 | public func foo() {} 14 | } 15 | 16 | /// This is bar's documentation. 17 | /// 18 | /// Bar is an internal struct and should not be included in documentation. 19 | struct Bar { 20 | func bar() {} 21 | } 22 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/TargetsWithDependencies/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "TargetsWithDependencies", 16 | targets: [ 17 | // Outer 18 | // ├─ InnerFirst 19 | // ╰─ InnerSecond 20 | // ╰─ NestedInner 21 | .target(name: "Outer", dependencies: [ 22 | "InnerFirst", 23 | "InnerSecond", 24 | ]), 25 | .target(name: "InnerFirst"), 26 | .target(name: "InnerSecond", dependencies: [ 27 | "NestedInner" 28 | ]), 29 | .target(name: "NestedInner"), 30 | ] 31 | ) 32 | 33 | // We only expect 'swift-docc-plugin' to be a sibling when this package 34 | // is running as part of a test. 35 | // 36 | // This allows the package to compile outside of tests for easier 37 | // test development. 38 | if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { 39 | package.dependencies += [ 40 | .package(path: "../swift-docc-plugin"), 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/TargetsWithDependencies/Sources/InnerFirst/SomeFile.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is a public struct and should be included in the documentation for this library. 10 | public struct SomethingPublic {} 11 | 12 | /// This is an internal struct and should not be included in the documentation for this library. 13 | struct SomethingInternal {} 14 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/TargetsWithDependencies/Sources/InnerSecond/SomeFile.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is a public struct and should be included in the documentation for this library. 10 | public struct SomethingPublic {} 11 | 12 | /// This is an internal struct and should not be included in the documentation for this library. 13 | struct SomethingInternal {} 14 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/TargetsWithDependencies/Sources/NestedInner/SomeFile.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is a public struct and should be included in the documentation for this library. 10 | public struct SomethingPublic {} 11 | 12 | /// This is an internal struct and should not be included in the documentation for this library. 13 | struct SomethingInternal {} 14 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Fixtures/TargetsWithDependencies/Sources/Outer/SomeFile.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// This is a public struct and should be included in the documentation for this library. 10 | public struct SomethingPublic {} 11 | 12 | /// This is an internal struct and should not be included in the documentation for this library. 13 | struct SomethingInternal {} 14 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/MixedTargetsTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class MixedTargetsTests: ConcurrencyRequiringTestCase { 12 | func testGenerateDocumentationForSpecificTarget() throws { 13 | let result = try swiftPackage( 14 | "generate-documentation", "--target", "Executable", 15 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "MixedTargets") 16 | ) 17 | 18 | result.assertExitStatusEquals(0) 19 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 20 | 21 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), expectedExecutableDataFiles) 22 | } 23 | 24 | func testGenerateDocumentationForMultipleSpecificTargets() throws { 25 | let result = try swiftPackage( 26 | "generate-documentation", "--target", "Executable", "--target", "Library", 27 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "MixedTargets") 28 | ) 29 | 30 | result.assertExitStatusEquals(0) 31 | let outputArchives = result.referencedDocCArchives 32 | XCTAssertEqual(result.referencedDocCArchives.count, 2) 33 | 34 | let executableArchiveURL = try XCTUnwrap( 35 | outputArchives.first(where: { $0.lastPathComponent == "Executable.doccarchive" }) 36 | ) 37 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: executableArchiveURL), expectedExecutableDataFiles) 38 | 39 | let libraryArchiveURL = try XCTUnwrap( 40 | outputArchives.first(where: { $0.lastPathComponent == "Library.doccarchive" }) 41 | ) 42 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: libraryArchiveURL), expectedLibraryDataFiles) 43 | } 44 | 45 | func testMultipleTargetsOutputPath() throws { 46 | let outputDirectory = try temporaryDirectory().appendingPathComponent("output") 47 | 48 | let result = try swiftPackage( 49 | "--disable-sandbox", 50 | "generate-documentation", "--target", "Executable", "--target", "Library", 51 | "--output-path", outputDirectory.path, 52 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "MixedTargets") 53 | ) 54 | 55 | result.assertExitStatusEquals(0) 56 | let outputArchives = result.referencedDocCArchives 57 | XCTAssertEqual(outputArchives.count, 2) 58 | XCTAssertEqual(outputArchives.map(\.path), [ 59 | outputDirectory.appendingPathComponent("Executable.doccarchive").path, 60 | outputDirectory.appendingPathComponent("Library.doccarchive").path, 61 | ]) 62 | 63 | let executableArchiveURL = try XCTUnwrap( 64 | outputArchives.first(where: { $0.lastPathComponent == "Executable.doccarchive" }) 65 | ) 66 | let executableDataDirectoryContents = try filesIn(.dataSubdirectory, of: executableArchiveURL) 67 | .map(\.relativePath) 68 | .sorted() 69 | 70 | XCTAssertEqual(executableDataDirectoryContents, expectedExecutableDataFiles) 71 | 72 | let libraryArchiveURL = try XCTUnwrap( 73 | outputArchives.first(where: { $0.lastPathComponent == "Library.doccarchive" }) 74 | ) 75 | let libraryDataDirectoryContents = try filesIn(.dataSubdirectory, of: libraryArchiveURL) 76 | .map(\.relativePath) 77 | .sorted() 78 | 79 | XCTAssertEqual(libraryDataDirectoryContents, expectedLibraryDataFiles) 80 | } 81 | 82 | func testCombinedDocumentation() throws { 83 | #if compiler(>=6.0) 84 | let result = try swiftPackage( 85 | "generate-documentation", "--target", "Executable", "--target", "Library", 86 | "--enable-experimental-combined-documentation", 87 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "MixedTargets") 88 | ) 89 | 90 | result.assertExitStatusEquals(0) 91 | let outputArchives = result.referencedDocCArchives 92 | XCTAssertEqual(outputArchives.count, 1) 93 | XCTAssertEqual(outputArchives.map(\.lastPathComponent), [ 94 | "MixedTargets.doccarchive", 95 | ]) 96 | 97 | let combinedArchiveURL = try XCTUnwrap(outputArchives.first) 98 | let combinedDataDirectoryContents = try filesIn(.dataSubdirectory, of: combinedArchiveURL) 99 | .map(\.relativePath) 100 | .sorted() 101 | 102 | XCTAssertEqual(combinedDataDirectoryContents, expectedCombinedDataFiles) 103 | #else 104 | XCTSkip("This test requires a Swift-DocC version that support the link-dependencies feature") 105 | #endif 106 | } 107 | 108 | func testCombinedDocumentationWithOutputPath() throws { 109 | #if compiler(>=6.0) 110 | let outputDirectory = try temporaryDirectory().appendingPathComponent("output") 111 | 112 | let result = try swiftPackage( 113 | "--disable-sandbox", 114 | "generate-documentation", "--target", "Executable", "--target", "Library", 115 | "--enable-experimental-combined-documentation", 116 | "--output-path", outputDirectory.path, 117 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "MixedTargets") 118 | ) 119 | 120 | result.assertExitStatusEquals(0) 121 | let outputArchives = result.referencedDocCArchives 122 | XCTAssertEqual(outputArchives.count, 1) 123 | XCTAssertEqual(outputArchives.map(\.path), [ 124 | outputDirectory.path 125 | ]) 126 | 127 | let combinedArchiveURL = try XCTUnwrap(outputArchives.first) 128 | let combinedDataDirectoryContents = try filesIn(.dataSubdirectory, of: combinedArchiveURL) 129 | .map(\.relativePath) 130 | .sorted() 131 | 132 | XCTAssertEqual(combinedDataDirectoryContents, expectedCombinedDataFiles) 133 | #else 134 | XCTSkip("This test requires a Swift-DocC version that support the link-dependencies feature") 135 | #endif 136 | } 137 | } 138 | 139 | private let expectedCombinedDataFiles = [ 140 | "documentation.json" 141 | ] + expectedExecutableDataFiles + expectedLibraryDataFiles 142 | 143 | private let expectedExecutableDataFiles = [ 144 | "documentation/executable.json", 145 | "documentation/executable/foo.json", 146 | "documentation/executable/foo/foo().json", 147 | "documentation/executable/foo/init().json", 148 | "documentation/executable/foo/main().json", 149 | ] 150 | 151 | private let expectedLibraryDataFiles = [ 152 | "documentation/library.json", 153 | "documentation/library/foo.json", 154 | "documentation/library/foo/foo().json", 155 | ] 156 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/SingleExecutableTargetTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class SingleExecutableTargetTests: ConcurrencyRequiringTestCase { 12 | func testGenerateDocumentation() throws { 13 | let result = try swiftPackage( 14 | "generate-documentation", 15 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "SingleExecutableTarget") 16 | ) 17 | 18 | result.assertExitStatusEquals(0) 19 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 20 | 21 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), [ 22 | "documentation/executable.json", 23 | "documentation/executable/foo.json", 24 | "documentation/executable/foo/foo().json", 25 | "documentation/executable/foo/init().json", 26 | "documentation/executable/foo/main().json", 27 | ]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/SingleLibraryTargetTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class SingleLibraryTargetTests: ConcurrencyRequiringTestCase { 12 | func testGenerateDocumentation() throws { 13 | let result = try swiftPackage( 14 | "generate-documentation", 15 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "SingleLibraryTarget") 16 | ) 17 | 18 | result.assertExitStatusEquals(0) 19 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 20 | 21 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), [ 22 | "documentation/library.json", 23 | "documentation/library/foo.json", 24 | "documentation/library/foo/foo().json", 25 | ]) 26 | } 27 | 28 | func testGenerateDocumentationWithCustomOutput() throws { 29 | let customOutputDirectory = try temporaryDirectory().appendingPathComponent( 30 | "CustomOutput.doccarchive" 31 | ) 32 | 33 | let result = try swiftPackage( 34 | "--allow-writing-to-directory", customOutputDirectory.path, 35 | "generate-documentation", "--output-path", customOutputDirectory.path, 36 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "SingleLibraryTarget") 37 | ) 38 | 39 | result.assertExitStatusEquals(0) 40 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 41 | 42 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), [ 43 | "documentation/library.json", 44 | "documentation/library/foo.json", 45 | "documentation/library/foo/foo().json", 46 | ]) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/SingleTestTargetTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class SingleTestTargetTests: ConcurrencyRequiringTestCase { 12 | func testDoesNotGenerateDocumentation() throws { 13 | let result = try swiftPackage( 14 | "generate-documentation", 15 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "SingleTestTarget") 16 | ) 17 | 18 | result.assertExitStatusEquals(1) 19 | XCTAssertTrue(result.referencedDocCArchives.isEmpty) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/SnippetDocumentationGenerationTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class SnippetDocumentationGenerationTests: ConcurrencyRequiringTestCase { 12 | func testGenerateDocumentationForPackageWithSnippets() throws { 13 | let packageName = "PackageWithSnippets" 14 | let result = try swiftPackage( 15 | "generate-documentation", 16 | "--target", "Library", 17 | workingDirectory: try setupTemporaryDirectoryForFixture(named: packageName) 18 | ) 19 | 20 | result.assertExitStatusEquals(0) 21 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 22 | 23 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), [ 24 | "documentation/library.json", 25 | "documentation/library/beststruct.json", 26 | "documentation/library/beststruct/best().json", 27 | "documentation/library/beststruct/init().json", 28 | ]) 29 | 30 | let symbolGraphSubDirectories = try FileManager.default.contentsOfDirectory( 31 | at: result.symbolGraphsDirectory, 32 | includingPropertiesForKeys: nil 33 | ) 34 | XCTAssertEqual(symbolGraphSubDirectories.map(\.lastPathComponent).sorted(), [ 35 | "snippet-symbol-graphs", 36 | "unified-symbol-graphs", 37 | ]) 38 | 39 | let unifiedSymbolGraphDirectory = try XCTUnwrap(symbolGraphSubDirectories.last) 40 | let symbolGraphFileNames = try filesIn(unifiedSymbolGraphDirectory).map(\.lastPathComponent) 41 | XCTAssert(symbolGraphFileNames.contains([ 42 | "\(packageName)-snippets.symbols.json" 43 | ])) 44 | } 45 | 46 | func testPreviewDocumentationWithSnippets() throws { 47 | let outputDirectory = try temporaryDirectory().appendingPathComponent("output") 48 | 49 | let port = try getAvailablePort() 50 | 51 | let process = try swiftPackageProcess( 52 | [ 53 | "--disable-sandbox", 54 | "--allow-writing-to-directory", outputDirectory.path, 55 | "preview-documentation", 56 | "--target", "Library", 57 | "--port", port, 58 | "--output-path", outputDirectory.path, 59 | ], 60 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "PackageWithSnippets") 61 | ) 62 | 63 | let outputPipe = Pipe() 64 | process.standardOutput = outputPipe 65 | process.standardError = outputPipe 66 | 67 | try process.run() 68 | 69 | var previewServerHasStarted: Bool { 70 | // We expect docc to emit a `data` directory at the root of the 71 | // given output path when it's finished compilation. 72 | // 73 | // At this point we can expect that the preview server will start imminently. 74 | return FileManager.default.fileExists( 75 | atPath: outputDirectory.appendingPathComponent("data", isDirectory: true).path 76 | ) 77 | } 78 | 79 | let previewServerHasStartedExpectation = expectation(description: "Preview server started.") 80 | 81 | let checkPreviewServerTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in 82 | if previewServerHasStarted { 83 | previewServerHasStartedExpectation.fulfill() 84 | timer.invalidate() 85 | } 86 | } 87 | 88 | wait(for: [previewServerHasStartedExpectation], timeout: 15) 89 | checkPreviewServerTimer.invalidate() 90 | 91 | guard process.isRunning else { 92 | XCTFail( 93 | """ 94 | Preview server failed to start. 95 | 96 | Process output: 97 | \(try outputPipe.asString() ?? "nil") 98 | """ 99 | ) 100 | return 101 | } 102 | 103 | // Send an interrupt to the SwiftPM parent process 104 | process.interrupt() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/SwiftDocCPreviewTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | final class SwiftDocCPreview: ConcurrencyRequiringTestCase { 13 | func testRunPreviewServerOnSamePortRepeatedly() throws { 14 | // Because only a single server can bind to a given port at a time, 15 | // this test ensures that the preview server running in the `docc` 16 | // process exits when the an interrupt is sent to the `SwiftPM` process. 17 | // 18 | // If it doesn't, subsequent runs of the preview server on the same port will 19 | // fail because `docc` is still bound to it. 20 | 21 | // First ask the system for an available port. If we use an already bound port, 22 | // this test will fail for unrelated reasons. 23 | let port = try getAvailablePort() 24 | 25 | for index in 1...3 { 26 | let outputDirectory = try temporaryDirectory().appendingPathComponent("output") 27 | 28 | let process = try swiftPackageProcess( 29 | [ 30 | "--disable-sandbox", 31 | "--allow-writing-to-directory", outputDirectory.path, 32 | "preview-documentation", 33 | "--port", port, 34 | "--output-path", outputDirectory.path 35 | ], 36 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "SingleExecutableTarget") 37 | ) 38 | 39 | let outputPipe = Pipe() 40 | process.standardOutput = outputPipe 41 | process.standardError = outputPipe 42 | 43 | try process.run() 44 | 45 | var previewServerHasStarted: Bool { 46 | // We expect docc to emit a `data` directory at the root of the 47 | // given output path when it's finished compilation. 48 | // 49 | // At this point we can expect that the preview server will start imminently. 50 | return FileManager.default.fileExists( 51 | atPath: outputDirectory.appendingPathComponent("data", isDirectory: true).path 52 | ) 53 | } 54 | 55 | let previewServerHasStartedExpectation = expectation(description: "Preview server started.") 56 | 57 | let checkPreviewServerTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in 58 | if previewServerHasStarted { 59 | previewServerHasStartedExpectation.fulfill() 60 | timer.invalidate() 61 | } 62 | } 63 | 64 | wait(for: [previewServerHasStartedExpectation], timeout: 15) 65 | checkPreviewServerTimer.invalidate() 66 | 67 | guard previewServerHasStarted else { 68 | XCTFail( 69 | """ 70 | Preview server never started on iteration '\(index)'. 71 | 72 | Process output: 73 | \(try outputPipe.asString() ?? "nil") 74 | """ 75 | ) 76 | return 77 | } 78 | 79 | // Wait an additional half second 80 | wait(for: 0.5) 81 | 82 | guard process.isRunning else { 83 | XCTFail( 84 | """ 85 | Preview server failed to start on iteration '\(index)'. 86 | 87 | Process output: 88 | \(try outputPipe.asString() ?? "nil") 89 | """ 90 | ) 91 | return 92 | } 93 | 94 | // Wait 1.5 seconds 95 | wait(for: 1.5) 96 | 97 | // Assert that long-running preview server process is still running after 2 seconds 98 | guard process.isRunning else { 99 | XCTFail( 100 | """ 101 | Preview server failed early on iteration '\(index)'. 102 | 103 | Process output: 104 | \(try outputPipe.asString() ?? "nil") 105 | """ 106 | ) 107 | return 108 | } 109 | 110 | // Send an interrupt to the SwiftPM parent process 111 | process.interrupt() 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/SwiftPMSandboxTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class SwiftPMSandboxTests: ConcurrencyRequiringTestCase { 12 | func testEnableAdditionalSandboxedDirectories() throws { 13 | let outputDirectory = try temporaryDirectory() 14 | 15 | let result = try swiftPackage( 16 | "--allow-writing-to-directory", outputDirectory.path, 17 | "generate-documentation", 18 | "--output-path", outputDirectory.path, 19 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "SingleLibraryTarget") 20 | ) 21 | 22 | result.assertExitStatusEquals(0) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/TargetWithDocCCatalogTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import XCTest 10 | 11 | final class TargetWithDocCCatalogTests: ConcurrencyRequiringTestCase { 12 | func testGenerateDocumentation() throws { 13 | let result = try swiftPackage( 14 | "generate-documentation", 15 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "TargetWithDocCCatalog") 16 | ) 17 | 18 | result.assertExitStatusEquals(0) 19 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 20 | 21 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), [ 22 | "documentation/librarywithdocccatalog.json", 23 | "documentation/librarywithdocccatalog/article-one.json", 24 | "documentation/librarywithdocccatalog/article-two.json", 25 | "documentation/librarywithdocccatalog/foo.json", 26 | "documentation/librarywithdocccatalog/foo/foo().json", 27 | ]) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/TargetWithSwiftExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2023-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | final class TargetWithSwiftExtensionsTests: ConcurrencyRequiringTestCase { 13 | #if swift(>=5.8) 14 | let supportsIncludingSwiftExtendedTypes = true 15 | #else 16 | let supportsIncludingSwiftExtendedTypes = false 17 | #endif 18 | 19 | override func setUpWithError() throws { 20 | try XCTSkipUnless( 21 | supportsIncludingSwiftExtendedTypes, 22 | "The current toolchain does not support symbol graph generation for extended types." 23 | ) 24 | 25 | try super.setUpWithError() 26 | } 27 | 28 | func testGenerateDocumentationWithoutExtendedTypesFlag() throws { 29 | let result = try swiftPackage( 30 | "generate-documentation", 31 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "LibraryTargetWithExtensionSymbols") 32 | ) 33 | 34 | result.assertExitStatusEquals(0) 35 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 36 | 37 | let dataDirectoryContents = try relativeFilePathsIn(.dataSubdirectory, of: archiveURL) 38 | 39 | #if swift(>=5.9) 40 | XCTAssertEqual(dataDirectoryContents, expectedDataContentWithExtendedTypes) 41 | #else 42 | XCTAssertEqual(dataDirectoryContents, expectedDataContentWithoutExtendedTypes) 43 | #endif 44 | } 45 | 46 | func testGenerateDocumentationWithDisablementFlag() throws { 47 | let result = try swiftPackage( 48 | "generate-documentation", "--exclude-extended-types", 49 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "LibraryTargetWithExtensionSymbols") 50 | ) 51 | 52 | result.assertExitStatusEquals(0) 53 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 54 | 55 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), expectedDataContentWithoutExtendedTypes) 56 | } 57 | 58 | func testGenerateDocumentationWithEnablementFlag() throws { 59 | let result = try swiftPackage( 60 | "generate-documentation", "--include-extended-types", 61 | workingDirectory: try setupTemporaryDirectoryForFixture(named: "LibraryTargetWithExtensionSymbols") 62 | ) 63 | 64 | result.assertExitStatusEquals(0) 65 | let archiveURL = try XCTUnwrap(result.onlyOutputArchive) 66 | 67 | XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: archiveURL), expectedDataContentWithExtendedTypes) 68 | } 69 | } 70 | 71 | private let expectedDataContentWithoutExtendedTypes = [ 72 | "documentation/library.json", 73 | "documentation/library/customfooconvertible.json", 74 | "documentation/library/customfooconvertible/asfoo.json", 75 | "documentation/library/foo.json", 76 | "documentation/library/foo/foo().json", 77 | ] 78 | 79 | private let expectedDataContentWithExtendedTypes = 80 | expectedDataContentWithoutExtendedTypes + expectedDataExtendedTypesContent 81 | 82 | private let expectedDataExtendedTypesContent = [ 83 | "documentation/library/swift.json", 84 | "documentation/library/swift/array.json", 85 | "documentation/library/swift/array/isarray.json", 86 | "documentation/library/swift/int.json", 87 | "documentation/library/swift/int/asfoo.json", 88 | "documentation/library/swift/int/customfooconvertible-implementations.json", 89 | "documentation/library/swift/int/isarray.json", 90 | ] 91 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Utility/ConcurrencyRequiringTestCase.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | /// A test case that requires the host toolchain to support Swift concurrency in order 13 | /// to pass. 14 | /// 15 | /// All SwiftPM command plugins depend on Swift concurrency so we don't expect to be able 16 | /// to run any integration test that actually invokes the Swift-DocC Plugin without 17 | /// the Swift concurrency libraries. 18 | class ConcurrencyRequiringTestCase: XCTestCase { 19 | override func setUpWithError() throws { 20 | try XCTSkipUnless( 21 | supportsSwiftConcurrency(), 22 | "The current SDK and/or OS do not support Swift concurrency." 23 | ) 24 | } 25 | 26 | private static var _supportsSwiftConcurrency: Bool? 27 | 28 | // Adapted from https://github.com/apple/swift-package-manager/blob/dd7e9cc6/Sources/SPMTestSupport/Toolchain.swift#L55 29 | private func supportsSwiftConcurrency() throws -> Bool { 30 | #if os(macOS) 31 | if #available(macOS 12.0, *) { 32 | // On macOS 12 and later, concurrency is assumed to work. 33 | return true 34 | } else { 35 | if let _supportsSwiftConcurrency = Self._supportsSwiftConcurrency { 36 | return _supportsSwiftConcurrency 37 | } 38 | 39 | let temporaryDirectory = try temporaryDirectory() 40 | 41 | // On macOS 11 and earlier, we don't know if concurrency actually works because not all 42 | // SDKs and toolchains have the right bits. We could examine the SDK and the various 43 | // libraries, but the most accurate test is to just try to compile and run a snippet of 44 | // code that requires async/await support. It doesn't have to actually do anything, 45 | // it's enough that all the libraries can be found (but because the library reference 46 | // is weak we do need the linkage reference to `_swift_task_create` and the like). 47 | do { 48 | let inputPath = temporaryDirectory.appendingPathComponent("foo.swift") 49 | 50 | try """ 51 | public func foo() async {} 52 | 53 | Task { await foo() } 54 | """.write(to: inputPath, atomically: true, encoding: .utf8) 55 | 56 | let outputPath = temporaryDirectory.appendingPathComponent("foo") 57 | let process = Process() 58 | process.executableURL = try swiftExecutableURL 59 | .deletingLastPathComponent() 60 | .appendingPathComponent("swiftc") 61 | 62 | process.arguments = [inputPath.path, "-o", outputPath.path] 63 | 64 | try process.run() 65 | process.waitUntilExit() 66 | guard process.terminationStatus == EXIT_SUCCESS else { 67 | Self._supportsSwiftConcurrency = false 68 | return false 69 | } 70 | } catch { 71 | // On any failure we assume false. 72 | Self._supportsSwiftConcurrency = false 73 | return false 74 | } 75 | // If we get this far we could compile and run a trivial executable that uses 76 | // libConcurrency, so we can say that this toolchain supports concurrency on this host. 77 | Self._supportsSwiftConcurrency = true 78 | return true 79 | } 80 | #else 81 | // On other platforms, concurrency is assumed to work since with new enough versions 82 | // of the toolchain. 83 | return true 84 | #endif 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Utility/FileManager+copyDirectoryWithoutHiddenFiles.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | extension FileManager { 12 | func copyDirectoryWithoutHiddenFiles( 13 | at sourceURL: URL, 14 | to destURL: URL, 15 | additionalFilter: (URL) -> Bool = { _ in true } 16 | ) throws { 17 | try? FileManager.default.removeItem(at: destURL) 18 | 19 | try FileManager.default.createDirectory( 20 | at: destURL, 21 | withIntermediateDirectories: false 22 | ) 23 | 24 | let pluginPackageContents = try FileManager.default.contentsOfDirectory( 25 | at: sourceURL, 26 | includingPropertiesForKeys: nil, 27 | options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants] 28 | ) 29 | .filter(additionalFilter) 30 | 31 | for url in pluginPackageContents { 32 | try FileManager.default.copyItem( 33 | at: url, 34 | to: destURL.appendingPathComponent(url.lastPathComponent) 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Utility/XCTest+filesInDataSubdirectory.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | extension XCTestCase { 13 | enum DocCArchiveSubdirectory: String { 14 | case dataSubdirectory = "data" 15 | case indexSubdirectory = "index" 16 | } 17 | 18 | func filesIn(_ url: URL) throws -> [URL] { 19 | let enumerator = try XCTUnwrap( 20 | FileManager.default.enumerator( 21 | at: url, 22 | includingPropertiesForKeys: [.isDirectoryKey], 23 | options: .producesRelativePathURLs 24 | ) 25 | ) 26 | 27 | #if os(Linux) 28 | // The implementation in Linux CI doesn't produce relative URLs despite the `producesRelativePathURLs` option. 29 | // Pre-compute how many components the base URL has 30 | let urlPrefixComponentCount = url.standardizedFileURL.pathComponents.count 31 | #endif 32 | 33 | return try enumerator.compactMap { 34 | guard let url = $0 as? URL else { 35 | return nil 36 | } 37 | 38 | let isDirectory = try XCTUnwrap( 39 | url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory 40 | ) 41 | guard !isDirectory else { 42 | return nil 43 | } 44 | 45 | #if os(Linux) 46 | // The implementation in Linux CI doesn't produce relative URLs despite the `producesRelativePathURLs` option. 47 | // Use the pre-computed number of base URL components to manually create a relative URL. 48 | // Since this is only a test helper, a simplified implantation like this is sufficient. 49 | return URL( 50 | fileURLWithPath: url.standardizedFileURL.pathComponents.dropFirst(urlPrefixComponentCount).joined(separator: "/"), 51 | relativeTo: url 52 | ) 53 | #else 54 | return url 55 | #endif 56 | } 57 | } 58 | 59 | func filesIn(_ subdirectory: DocCArchiveSubdirectory, of doccArchive: URL) throws -> [URL] { 60 | try filesIn(doccArchive.appendingPathComponent(subdirectory.rawValue)) 61 | } 62 | 63 | func relativeFilePathsIn(_ subdirectory: DocCArchiveSubdirectory, of doccArchive: URL) throws -> [String] { 64 | try filesIn(subdirectory, of: doccArchive) 65 | .map(\.relativePath) 66 | .sorted() 67 | } 68 | 69 | func relativeFilePathsIn(_ url: URL) throws -> [String] { 70 | try filesIn(url) 71 | .map(\.relativePath) 72 | .sorted() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Utility/XCTestCase+availablePort.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import NIOCore 10 | import NIOPosix 11 | import XCTest 12 | 13 | extension XCTestCase { 14 | /// Returns a port number that is available on the system at the moment 15 | /// this function returns. 16 | /// 17 | /// This allows tests to more reliably run in parallel and in CI where 18 | /// we can't be sure which ports will be available. 19 | func getAvailablePort() throws -> Int { 20 | // Start up the server. 21 | let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 22 | let server = ServerBootstrap(group: eventLoopGroup) 23 | 24 | // Bind to port '0' on localhost. '0' is a reserved port on the system 25 | // so binding to it will cause the OS to allocate an available port to the server. 26 | // Then we can read that allocated port number and return it. 27 | let channel = try server.bind(host: "localhost", port: 0).wait() 28 | let port = try XCTUnwrap(channel.localAddress?.port) 29 | 30 | // Shut down the server 31 | try channel.close().wait() 32 | try eventLoopGroup.syncShutdownGracefully() 33 | 34 | return port 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Utility/XCTestCase+setupTemporaryDirectoryForFixture.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | extension XCTestCase { 13 | /// Identifies the location of the parent Swift-DocC plugin package. 14 | /// 15 | /// This integration test suite is nested inside of the primary Swift-DocC plugin package. 16 | static let swiftDocCPluginPackage = URL( 17 | fileURLWithPath: #file, 18 | isDirectory: false 19 | ) 20 | // Utility: 21 | .deletingLastPathComponent() 22 | // Tests: 23 | .deletingLastPathComponent() 24 | // IntegrationTests: 25 | .deletingLastPathComponent() 26 | // swift-docc-plugin 27 | .deletingLastPathComponent() 28 | 29 | /// Copies the given test fixture to a temporary directory and returns the URL for 30 | /// its temporary testing location. 31 | func setupTemporaryDirectoryForFixture(named testFixtureName: String) throws -> URL { 32 | let temporaryDirectoryURL = try temporaryDirectory() 33 | 34 | // Get the url for the requested test package 35 | let testFixtureURL = try XCTUnwrap( 36 | Bundle.module.url( 37 | forResource: testFixtureName, 38 | withExtension: nil 39 | ) 40 | ) 41 | 42 | // We'll copy the test package to a new location in the new temporary directory 43 | let testFixtureTemporaryURL = temporaryDirectoryURL.appendingPathComponent( 44 | testFixtureURL.lastPathComponent 45 | ) 46 | 47 | try FileManager.default.copyDirectoryWithoutHiddenFiles( 48 | at: testFixtureURL, 49 | to: testFixtureTemporaryURL 50 | ) 51 | 52 | // Copy the Swift-DocC plugin to the same temporary testing directory so that its 53 | // a sibling of the test package. 54 | try FileManager.default.copyDirectoryWithoutHiddenFiles( 55 | at: Self.swiftDocCPluginPackage, 56 | to: temporaryDirectoryURL.appendingPathComponent("swift-docc-plugin"), 57 | additionalFilter: { url in 58 | // Skip this integration test sub-package 59 | url.lastPathComponent != "IntegrationTests" 60 | } 61 | ) 62 | 63 | return testFixtureTemporaryURL 64 | } 65 | 66 | func temporaryDirectory() throws -> URL { 67 | let resourceURL = try XCTUnwrap(Bundle.module.resourceURL) 68 | 69 | // Create a temporary testing directory in the bundle's resource directory 70 | let temporaryDirectoryURL = resourceURL.appendingPathComponent( 71 | "TemporaryTestingDirectory-\(ProcessInfo.processInfo.globallyUniqueString)" 72 | ) 73 | try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: false) 74 | 75 | // Add a teardown block to remove the entire testing directory after the test runs 76 | addTeardownBlock { 77 | do { 78 | try FileManager.default.removeItem(at: temporaryDirectoryURL) 79 | } catch { 80 | XCTFail(""" 81 | Failed to remove temporary testing directory at '\(temporaryDirectoryURL.path)': \ 82 | '\(error.localizedDescription)'. 83 | """ 84 | ) 85 | } 86 | } 87 | 88 | return temporaryDirectoryURL 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/Utility/XCTestCase+waitForTimeInterval.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | extension XCTestCase { 13 | /// Waits for the specified time. 14 | func wait(for timeInterval: TimeInterval) { 15 | let waitForTimeIntervalExpectation = expectation( 16 | description: "Wait for '\(timeInterval)' second(s)." 17 | ) 18 | 19 | let waitTimer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { timer in 20 | waitForTimeIntervalExpectation.fulfill() 21 | timer.invalidate() 22 | } 23 | 24 | wait(for: [waitForTimeIntervalExpectation], timeout: timeInterval + 5.0) 25 | waitTimer.invalidate() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | 11 | import Foundation 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "SwiftDocCPlugin", 16 | platforms: [ 17 | .macOS("10.15.4"), 18 | ], 19 | products: [ 20 | .plugin(name: "Swift-DocC", targets: ["Swift-DocC"]), 21 | .plugin(name: "Swift-DocC Preview", targets: ["Swift-DocC Preview"]), 22 | ], 23 | dependencies: [ 24 | .package(url: "https://github.com/swiftlang/swift-docc-symbolkit", from: "1.0.0"), 25 | ], 26 | targets: [ 27 | .plugin( 28 | name: "Swift-DocC", 29 | capability: .command( 30 | intent: .documentationGeneration() 31 | ), 32 | dependencies: [ 33 | "snippet-extract", 34 | ], 35 | path: "Plugins/Swift-DocC Convert", 36 | exclude: ["Symbolic Links/README.md"] 37 | ), 38 | 39 | .plugin( 40 | name: "Swift-DocC Preview", 41 | capability: .command( 42 | intent: .custom( 43 | verb: "preview-documentation", 44 | description: "Preview the Swift-DocC documentation for a specified target." 45 | ) 46 | ), 47 | dependencies: [ 48 | "snippet-extract", 49 | ], 50 | exclude: ["Symbolic Links/README.md"] 51 | ), 52 | 53 | .target(name: "SwiftDocCPluginUtilities"), 54 | .testTarget( 55 | name: "SwiftDocCPluginUtilitiesTests", 56 | dependencies: [ 57 | "Snippets", 58 | "SwiftDocCPluginUtilities", 59 | "snippet-extract", 60 | ], 61 | resources: [ 62 | .copy("Test Fixtures"), 63 | ] 64 | ), 65 | 66 | // Empty target that builds the DocC catalog at /SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc. 67 | // The SwiftDocCPlugin catalog includes high-level, user-facing documentation about using 68 | // the Swift-DocC plugin from the command-line. 69 | .target( 70 | name: "SwiftDocCPlugin", 71 | path: "Sources/SwiftDocCPluginDocumentation", 72 | exclude: ["README.md"] 73 | ), 74 | .target(name: "Snippets"), 75 | .executableTarget( 76 | name: "snippet-extract", 77 | dependencies: [ 78 | "Snippets", 79 | .product(name: "SymbolKit", package: "swift-docc-symbolkit"), 80 | ]), 81 | ] 82 | ) 83 | -------------------------------------------------------------------------------- /Plugins/SharedPackagePluginExtensions/ArgumentExtractor+extractSpecifiedTargets.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import PackagePlugin 11 | 12 | enum ArgumentParsingError: LocalizedError, CustomStringConvertible { 13 | case unknownProduct(_ productName: String, compatibleProducts: String) 14 | case unknownTarget(_ targetName: String, compatibleTargets: String) 15 | case productDoesNotContainSourceModuleTargets(String) 16 | case packageDoesNotContainSourceModuleTargets 17 | case targetIsNotSourceModule(String) 18 | case testTarget(String) 19 | 20 | var description: String { 21 | switch self { 22 | case .unknownProduct(let productName, let compatibleProducts): 23 | return """ 24 | no product named '\(productName)' 25 | 26 | compatible products: \(compatibleProducts) 27 | """ 28 | case .unknownTarget(let targetName, let compatibleTargets): 29 | return """ 30 | no target named '\(targetName)' 31 | 32 | compatible targets: \(compatibleTargets) 33 | """ 34 | case .productDoesNotContainSourceModuleTargets(let string): 35 | return "product '\(string)' does not contain any Swift source modules" 36 | case .targetIsNotSourceModule(let string): 37 | return "target '\(string)' is not a Swift source module" 38 | case .testTarget(let string): 39 | return "target '\(string)' is a test target; only library and executable targets are supported by Swift-DocC" 40 | case .packageDoesNotContainSourceModuleTargets: 41 | return "the current package does not contain any compatible Swift source modules" 42 | } 43 | } 44 | 45 | var errorDescription: String? { 46 | return description 47 | } 48 | } 49 | 50 | extension ArgumentExtractor { 51 | mutating func extractSpecifiedTargets(in package: Package) throws -> [SourceModuleTarget] { 52 | let specifiedProducts = extractOption(named: "product") 53 | let specifiedTargets = extractOption(named: "target") 54 | 55 | let productTargets = try specifiedProducts.flatMap { specifiedProduct -> [SourceModuleTarget] in 56 | let product = package.allProducts.first { product in 57 | product.name == specifiedProduct 58 | } 59 | 60 | guard let product = product else { 61 | throw ArgumentParsingError.unknownProduct( 62 | specifiedProduct, 63 | compatibleProducts: package.compatibleProducts 64 | ) 65 | } 66 | 67 | let supportedSourceModuleTargets = product.targets.compactMap { target in 68 | target as? SourceModuleTarget 69 | } 70 | .filter { sourceModuleTarget in 71 | return sourceModuleTarget.kind != .test 72 | } 73 | 74 | guard !supportedSourceModuleTargets.isEmpty else { 75 | throw ArgumentParsingError.productDoesNotContainSourceModuleTargets(specifiedProduct) 76 | } 77 | 78 | return supportedSourceModuleTargets 79 | } 80 | 81 | let targets = try specifiedTargets.map { specifiedTarget -> SourceModuleTarget in 82 | let target = package.allTargets.first { target in 83 | target.name == specifiedTarget 84 | } 85 | 86 | guard let target = target else { 87 | throw ArgumentParsingError.unknownTarget( 88 | specifiedTarget, 89 | compatibleTargets: package.compatibleTargets 90 | ) 91 | } 92 | 93 | guard let sourceModuleTarget = target as? SourceModuleTarget else { 94 | throw ArgumentParsingError.targetIsNotSourceModule(specifiedTarget) 95 | } 96 | 97 | guard sourceModuleTarget.kind != .test else { 98 | throw ArgumentParsingError.testTarget(specifiedTarget) 99 | } 100 | 101 | return sourceModuleTarget 102 | } 103 | 104 | return productTargets + targets 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Plugins/SharedPackagePluginExtensions/PackageExtensions.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import PackagePlugin 11 | 12 | extension Package { 13 | /// Any targets defined in this package and its dependencies. 14 | var allTargets: [Target] { 15 | var insertedTargetIds = Set() 16 | var relevantTargets = [Target]() 17 | 18 | func addTargets(_ targets: [Target]) { 19 | for target in targets { 20 | guard !insertedTargetIds.contains(target.id) else { 21 | continue 22 | } 23 | 24 | relevantTargets.append(target) 25 | insertedTargetIds.insert(target.id) 26 | } 27 | } 28 | 29 | func addTargetDependencies(_ target: Target) { 30 | for dependency in target.dependencies { 31 | switch dependency { 32 | case .product(let product): 33 | addTargets(product.targets) 34 | case .target(let target): 35 | addTargets([target]) 36 | @unknown default: 37 | return 38 | } 39 | } 40 | } 41 | 42 | // Begin by adding the targets defined in the products 43 | // vended by this package as these are the most likely to be documented. 44 | for product in products { 45 | addTargets(product.targets) 46 | } 47 | 48 | // Then add the remaining targets defined in this package 49 | addTargets(targets) 50 | 51 | // Make a copy of al the targets directly defined in this package 52 | let topLevelTargets = relevantTargets 53 | 54 | // Iterate through them and add their dependencies. This ensures 55 | // that we always list targets defined in the package before 56 | // any we depend on from other packages. 57 | for topLevelTarget in topLevelTargets { 58 | addTargetDependencies(topLevelTarget) 59 | } 60 | 61 | return relevantTargets 62 | } 63 | 64 | /// Any regular products defined in this package and its dependencies. 65 | var allProducts: [Product] { 66 | return products + dependencies.flatMap(\.package.products) 67 | } 68 | 69 | /// All targets defined in this package and its dependencies that 70 | /// can produce documentation. 71 | var allDocumentableTargets: [SourceModuleTarget] { 72 | return allTargets.documentableTargets 73 | } 74 | 75 | /// All products defined in this package and its dependencies that 76 | /// can produce documentation. 77 | var allDocumentableProducts: [Product] { 78 | return allProducts.filter { product in 79 | !product.targets.documentableTargets.isEmpty 80 | } 81 | } 82 | 83 | /// All targets defined directly in this package that produce documentation. 84 | /// 85 | /// Excludes targets defined in dependencies. 86 | var topLevelDocumentableTargets: [SourceModuleTarget] { 87 | var insertedTargetIds = Set() 88 | var topLevelTargets = [Target]() 89 | 90 | func addTargets(_ targets: [Target]) { 91 | for target in targets { 92 | guard !insertedTargetIds.contains(target.id) else { 93 | continue 94 | } 95 | 96 | topLevelTargets.append(target) 97 | insertedTargetIds.insert(target.id) 98 | } 99 | } 100 | 101 | for product in products { 102 | addTargets(product.targets) 103 | } 104 | 105 | addTargets(targets) 106 | 107 | return topLevelTargets.documentableTargets 108 | } 109 | 110 | /// A list of targets that are compatible with this plugin, suitable for presentation. 111 | var compatibleTargets: String { 112 | guard !allDocumentableTargets.isEmpty else { 113 | return "none" 114 | } 115 | 116 | return allDocumentableTargets.map(\.name.singleQuoted).joined(separator: ", ") 117 | } 118 | 119 | /// A list of products that are compatible with this plugin, suitable for presentation. 120 | var compatibleProducts: String { 121 | guard !allDocumentableProducts.isEmpty else { 122 | return "none" 123 | } 124 | 125 | return allDocumentableProducts.map(\.name.singleQuoted).joined(separator: ", ") 126 | } 127 | 128 | func package(for target: SourceModuleTarget) -> Package? { 129 | let possiblePackages = dependencies.map(\.package) + [self] 130 | 131 | return possiblePackages.first { package in 132 | package.containsTarget(target) 133 | } 134 | } 135 | 136 | func containsTarget(_ target: SourceModuleTarget) -> Bool { 137 | return targets.contains { packageTarget in 138 | packageTarget.id == target.id 139 | } 140 | } 141 | } 142 | 143 | private extension Collection where Element == Target { 144 | var documentableTargets: [SourceModuleTarget] { 145 | return compactMap { target in 146 | guard let swiftSourceModuleTarget = target as? SourceModuleTarget else { 147 | return nil 148 | } 149 | 150 | guard swiftSourceModuleTarget.kind != .test else { 151 | return nil 152 | } 153 | 154 | return swiftSourceModuleTarget 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import PackagePlugin 11 | 12 | /// Generating symbol graphs is the only task that can't run in parallel. 13 | /// 14 | /// We serialize its execution to support build concurrency for the other tasks. 15 | private let symbolGraphGenerationLock = Lock() 16 | 17 | extension PackageManager { 18 | struct DocCSymbolGraphResult { 19 | let unifiedSymbolGraphsDirectory: URL 20 | let targetSymbolGraphsDirectory: URL 21 | let snippetSymbolGraphFile: URL? 22 | 23 | init( 24 | unifiedSymbolGraphsDirectory: URL, 25 | targetSymbolGraphsDirectory: URL, 26 | snippetSymbolGraphFile: URL? 27 | ) { 28 | self.unifiedSymbolGraphsDirectory = unifiedSymbolGraphsDirectory 29 | self.targetSymbolGraphsDirectory = targetSymbolGraphsDirectory 30 | self.snippetSymbolGraphFile = snippetSymbolGraphFile 31 | } 32 | 33 | init(targetSymbolGraphsDirectory: URL) { 34 | self.unifiedSymbolGraphsDirectory = targetSymbolGraphsDirectory 35 | self.targetSymbolGraphsDirectory = targetSymbolGraphsDirectory 36 | self.snippetSymbolGraphFile = nil 37 | } 38 | } 39 | 40 | /// Returns the relevant symbols graphs for Swift-DocC documentation generation for the given target. 41 | func doccSymbolGraphs( 42 | for target: SourceModuleTarget, 43 | context: PluginContext, 44 | verbose: Bool, 45 | snippetExtractor: SnippetExtractor?, 46 | customSymbolGraphOptions: ParsedSymbolGraphArguments 47 | ) throws -> DocCSymbolGraphResult { 48 | // First generate the primary symbol graphs containing information about the 49 | // symbols defined in the target itself. 50 | var symbolGraphOptions = target.defaultSymbolGraphOptions(in: context.package) 51 | 52 | if let rawMinimumAccessLevel = customSymbolGraphOptions.minimumAccessLevel, 53 | let minimumAccessLevel = SymbolGraphOptions.AccessLevel(rawValue: rawMinimumAccessLevel) 54 | { 55 | symbolGraphOptions.minimumAccessLevel = minimumAccessLevel 56 | } 57 | 58 | if customSymbolGraphOptions.skipSynthesizedSymbols == true { 59 | symbolGraphOptions.includeSynthesized = false 60 | } 61 | 62 | if let includeExtendedTypes = customSymbolGraphOptions.includeExtendedTypes { 63 | #if swift(<5.8) 64 | print("warning: detected '--\(includeExtendedTypes ? "include" : "exclude")-extended-types' option, which is incompatible with your swift version (required: 5.8)") 65 | #else 66 | symbolGraphOptions.emitExtensionBlocks = includeExtendedTypes 67 | #endif 68 | } 69 | 70 | if verbose { 71 | print("symbol graph options: '\(symbolGraphOptions)'") 72 | } 73 | 74 | let targetSymbolGraphs = try symbolGraphGenerationLock.withLock { 75 | try getSymbolGraph(for: target, options: symbolGraphOptions) 76 | } 77 | let targetSymbolGraphsDirectory = URL( 78 | fileURLWithPath: targetSymbolGraphs.directoryPath.string, 79 | isDirectory: true 80 | ) 81 | 82 | if verbose { 83 | print("target symbol graph directory path: '\(targetSymbolGraphsDirectory.path)'") 84 | } 85 | 86 | // Then, check to see if we were provided a snippet extractor. If so, 87 | // we should attempt to generate symbol graphs for any snippets included in the 88 | // target's containing package. 89 | guard let snippetExtractor = snippetExtractor else { 90 | return DocCSymbolGraphResult(targetSymbolGraphsDirectory: targetSymbolGraphsDirectory) 91 | } 92 | 93 | if verbose { 94 | print("snippet extractor provided, attempting to generate snippet symbol graph") 95 | } 96 | 97 | guard let snippetSymbolGraphFile = try snippetExtractor.generateSnippets( 98 | for: target, 99 | context: context 100 | ) else { 101 | if verbose { 102 | print("no snippet symbol graphs generated") 103 | } 104 | 105 | return DocCSymbolGraphResult(targetSymbolGraphsDirectory: targetSymbolGraphsDirectory) 106 | } 107 | 108 | if verbose { 109 | print("snippet symbol graph file: '\(snippetSymbolGraphFile.path)'") 110 | } 111 | 112 | // Since we successfully produced symbol graphs for snippets contained in the 113 | // target's containing package, we need to move all generated symbol graphs into 114 | // a single, unified, symbol graph directory. 115 | // 116 | // This is necessary because the `docc` CLI only supports accepting a single directory 117 | // of symbol graphs. 118 | 119 | let unifiedSymbolGraphsDirectory = URL( 120 | fileURLWithPath: context.pluginWorkDirectory.string, 121 | isDirectory: true 122 | ) 123 | .appendingPathComponent(".build", isDirectory: true) 124 | .appendingPathComponent("symbol-graphs", isDirectory: true) 125 | .appendingPathComponent("unified-symbol-graphs", isDirectory: true) 126 | .appendingPathComponent("\(target.name)-\(target.id)", isDirectory: true) 127 | 128 | if verbose { 129 | print("unified symbol graphs directory path: '\(unifiedSymbolGraphsDirectory.path)'") 130 | } 131 | 132 | // If there's an existing directory containing unified symbol graphs for this target, 133 | // just remove it. Ignore the error that could occur if the directory doesn't exist. 134 | try? FileManager.default.removeItem(atPath: unifiedSymbolGraphsDirectory.path) 135 | 136 | try FileManager.default.createDirectory( 137 | atPath: unifiedSymbolGraphsDirectory.path, 138 | withIntermediateDirectories: true, 139 | attributes: nil 140 | ) 141 | 142 | let targetSymbolGraphsUnifiedDirectory = unifiedSymbolGraphsDirectory.appendingPathComponent( 143 | "target-symbol-graphs", isDirectory: true 144 | ) 145 | 146 | // Copy the symbol graphs for the target into the unified directory 147 | try FileManager.default.copyItem( 148 | atPath: targetSymbolGraphsDirectory.path, 149 | toPath: targetSymbolGraphsUnifiedDirectory.path 150 | ) 151 | 152 | let snippetSymbolGraphFileInUnifiedDirectory = unifiedSymbolGraphsDirectory.appendingPathComponent( 153 | snippetSymbolGraphFile.lastPathComponent, isDirectory: false 154 | ) 155 | 156 | // Copy the snippet symbol graphs into the unified directory 157 | try FileManager.default.copyItem( 158 | atPath: snippetSymbolGraphFile.path, 159 | toPath: snippetSymbolGraphFileInUnifiedDirectory.path 160 | ) 161 | 162 | return DocCSymbolGraphResult( 163 | unifiedSymbolGraphsDirectory: unifiedSymbolGraphsDirectory, 164 | targetSymbolGraphsDirectory: targetSymbolGraphsUnifiedDirectory, 165 | snippetSymbolGraphFile: snippetSymbolGraphFileInUnifiedDirectory 166 | ) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Plugins/SharedPackagePluginExtensions/PluginContext+doccExecutableURL.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import PackagePlugin 11 | 12 | extension PluginContext { 13 | /// Returns the executable url that should be used for any invocations of the 14 | /// `docc` command-line tool in this plugin context. 15 | /// 16 | /// If the user has set the `DOCC_EXEC` environment variable this will return the URL 17 | /// at the given path. Otherwise, this attempts to find the `docc` executable 18 | /// in the current toolchain. 19 | var doccExecutable: URL { 20 | get throws { 21 | if let doccExecPath = ProcessInfo.processInfo.environment["DOCC_EXEC"] { 22 | return URL(fileURLWithPath: doccExecPath) 23 | } else { 24 | let doccTool = try tool(named: "docc") 25 | return URL(fileURLWithPath: doccTool.path.string) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Plugins/SharedPackagePluginExtensions/SnippetExtractor+generateSnippetsForTarget.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import PackagePlugin 11 | 12 | extension SnippetExtractor { 13 | func generateSnippets( 14 | for target: SourceModuleTarget, 15 | context: PluginContext 16 | ) throws -> URL? { 17 | guard let package = context.package.package(for: target) else { 18 | return nil 19 | } 20 | 21 | return try generateSnippets( 22 | for: package.id, 23 | packageDisplayName: package.displayName, 24 | packageDirectory: URL(fileURLWithPath: package.directory.string, isDirectory: true) 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Plugins/SharedPackagePluginExtensions/SourceModuleTarget+doccCatalogPath.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import PackagePlugin 11 | 12 | extension SourceModuleTarget { 13 | /// The path to this target's DocC catalog, if any. 14 | var doccCatalogPath: String? { 15 | return sourceFiles.first { sourceFile in 16 | sourceFile.path.extension?.lowercased() == "docc" 17 | }?.path.string 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import PackagePlugin 10 | 11 | extension SourceModuleTarget { 12 | /// Returns the default options that should be used for generating a symbol graph for the 13 | /// current target in the given package. 14 | func defaultSymbolGraphOptions(in package: Package) -> PackageManager.SymbolGraphOptions { 15 | let targetMinimumAccessLevel: PackageManager.SymbolGraphOptions.AccessLevel 16 | 17 | if kind == .executable { 18 | // The target represents an executable so we'll use an 'internal' minimum 19 | // access level. 20 | targetMinimumAccessLevel = .internal 21 | } else { 22 | // Since the target isn't an executable, we assume it's a library and use 23 | // the 'public' minimum access level. 24 | targetMinimumAccessLevel = .public 25 | } 26 | 27 | #if swift(>=5.9) 28 | let emitExtensionBlockSymbolDefault = true 29 | #else 30 | let emitExtensionBlockSymbolDefault = false 31 | #endif 32 | 33 | return PackageManager.SymbolGraphOptions( 34 | minimumAccessLevel: targetMinimumAccessLevel, 35 | includeSynthesized: true, 36 | includeSPI: false, 37 | emitExtensionBlocks: emitExtensionBlockSymbolDefault 38 | ) 39 | } 40 | } 41 | 42 | 43 | #if swift(<5.8) 44 | private extension PackageManager.SymbolGraphOptions { 45 | /// A compatibility layer for lower Swift versions which discards unknown parameters. 46 | init(minimumAccessLevel: PackagePlugin.PackageManager.SymbolGraphOptions.AccessLevel = .public, 47 | includeSynthesized: Bool = false, 48 | includeSPI: Bool = false, 49 | emitExtensionBlocks: Bool) { 50 | self.init(minimumAccessLevel: minimumAccessLevel, includeSynthesized: includeSynthesized, includeSPI: includeSPI) 51 | } 52 | } 53 | #endif 54 | -------------------------------------------------------------------------------- /Plugins/SharedPackagePluginExtensions/Target+doccArchiveOutputPath.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import PackagePlugin 10 | import Foundation 11 | 12 | extension Target { 13 | func doccArchiveOutputPath(in context: PluginContext) -> String { 14 | context.pluginWorkDirectory.appending(archiveName).string 15 | } 16 | 17 | func dependencyDocCArchiveOutputPath(in context: PluginContext) -> String { 18 | context.pluginWorkDirectory.appending("dependencies").appending(archiveName).string 19 | } 20 | 21 | private var archiveName: String { 22 | "\(name).doccarchive" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Plugins/Swift-DocC Convert/Symbolic Links/README.md: -------------------------------------------------------------------------------- 1 | # Symbolic Links 2 | 3 | This directory contains symbolic links to shared source code 4 | used by the Swift-DocC Convert plugin. This is a workaround until SwiftPM has 5 | native support for sharing code between plugins. 6 | 7 | 8 | -------------------------------------------------------------------------------- /Plugins/Swift-DocC Convert/Symbolic Links/SharedPackagePluginExtensions: -------------------------------------------------------------------------------- 1 | ../../SharedPackagePluginExtensions -------------------------------------------------------------------------------- /Plugins/Swift-DocC Convert/Symbolic Links/SwiftDocCPluginUtilities: -------------------------------------------------------------------------------- 1 | ../../../Sources/SwiftDocCPluginUtilities -------------------------------------------------------------------------------- /Plugins/Swift-DocC Preview/Symbolic Links/README.md: -------------------------------------------------------------------------------- 1 | # Symbolic Links 2 | 3 | This directory contains symbolic links to shared source code 4 | used by the Swift-DocC Preview plugin. This is a workaround until SwiftPM has 5 | native support for sharing code between plugins. 6 | 7 | 8 | -------------------------------------------------------------------------------- /Plugins/Swift-DocC Preview/Symbolic Links/SharedPackagePluginExtensions: -------------------------------------------------------------------------------- 1 | ../../SharedPackagePluginExtensions -------------------------------------------------------------------------------- /Plugins/Swift-DocC Preview/Symbolic Links/SwiftDocCPluginUtilities: -------------------------------------------------------------------------------- 1 | ../../../Sources/SwiftDocCPluginUtilities -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift-DocC Plugin 2 | 3 | The Swift-DocC plugin is a Swift Package Manager command plugin that supports building 4 | documentation for SwiftPM libraries and executables. 5 | 6 | ## Usage 7 | 8 | Please see 9 | [the plugin's documentation](https://swiftlang.github.io/swift-docc-plugin/documentation/swiftdoccplugin/) 10 | for more detailed usage instructions. 11 | 12 | ### Adding the Swift-DocC Plugin as a Dependency 13 | 14 | To use the Swift-DocC plugin with your package, first add it as a dependency: 15 | 16 | ```swift 17 | let package = Package( 18 | // name, platforms, products, etc. 19 | dependencies: [ 20 | // other dependencies 21 | .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), 22 | ], 23 | targets: [ 24 | // targets 25 | ] 26 | ) 27 | ``` 28 | 29 | Swift 5.6 is required in order to run the plugin. 30 | 31 | ### Converting Documentation 32 | 33 | You can then invoke the plugin from the root of your repository like so: 34 | 35 | ```shell 36 | swift package generate-documentation 37 | ``` 38 | 39 | This will generate documentation for all compatible targets defined in your package and its dependencies 40 | and print the location of the resulting DocC archives. 41 | 42 | If you'd like to generate documentation for a specific target and output that 43 | to a specific directory, you can do something like the following: 44 | 45 | ```shell 46 | swift package --allow-writing-to-directory ./docs \ 47 | generate-documentation --target MyFramework --output-path ./docs 48 | ``` 49 | 50 | Notice that the output path must also be passed to SwiftPM via the 51 | `--allow-writing-to-directory` option. Otherwise SwiftPM will throw an error 52 | as it's a sandbox violation for a plugin to write to a package directory without explicit 53 | permission. 54 | 55 | Any flag passed after the `generate-documentation` plugin invocation is passed 56 | along to the `docc` command-line tool. For example, to take advantage of Swift-DocC's new support 57 | for hosting in static environments like GitHub Pages, you could run the following: 58 | 59 | ```shell 60 | swift package --allow-writing-to-directory ./docs \ 61 | generate-documentation --target MyFramework --output-path ./docs \ 62 | --transform-for-static-hosting --hosting-base-path MyFramework 63 | ``` 64 | 65 | ### Previewing Documentation 66 | 67 | The Swift-DocC plugin also supports previewing documentation with a local web server. However, 68 | unlike converting documentation, previewing is limited to a single target a time. 69 | 70 | To preview documentation for the MyFramework target, you could run the following: 71 | 72 | ```shell 73 | swift package --disable-sandbox preview-documentation --target MyFramework 74 | ``` 75 | 76 | To preview documentation for a product defined by one of your package's dependencies, 77 | you could run the following: 78 | 79 | ```shell 80 | swift package --disable-sandbox preview-documentation --product OtherFramework 81 | ``` 82 | 83 | ### Hosting Documentation 84 | 85 | For details on how to best build documentation for hosting online and a specific 86 | tutorial for publishing to GitHub Pages, please see 87 | [the plugin's documentation](https://swiftlang.github.io/swift-docc-plugin/documentation/swiftdoccplugin/). 88 | 89 | ## Bug Reports and Feature Requests 90 | 91 | ### Submitting a Bug Report 92 | 93 | The Swift-DocC plugin tracks all bug reports with 94 | [GitHub Issues](https://github.com/swiftlang/swift-docc-plugin/issues). 95 | When you submit a bug report we ask that you follow the 96 | [provided template](https://github.com/swiftlang/swift-docc-plugin/issues/new?template=BUG_REPORT.md) 97 | and provide as many details as possible. 98 | 99 | If you can confirm that the bug occurs when using the latest commit of the Swift-DocC plugin 100 | from the `main` branch, that will help us track down the bug faster. 101 | 102 | ### Submitting a Feature Request 103 | 104 | For feature requests, please feel free to file a 105 | [GitHub issue](https://github.com/swiftlang/swift-docc-plugin/issues/new?template=FEATURE_REQUEST.md) 106 | or start a discussion on the [Swift Forums](https://forums.swift.org/c/development/swift-docc). 107 | 108 | Don't hesitate to submit a feature request if you see a way 109 | the Swift-DocC plugin can be improved to better meet your needs. 110 | 111 | All user-facing features must be discussed 112 | in the [Swift Forums](https://forums.swift.org/c/development/swift-docc) 113 | before being enabled by default. 114 | 115 | ## Contributing to the Swift-DocC Plugin 116 | 117 | Please see the [contributing guide](/CONTRIBUTING.md) for more information. 118 | 119 | 120 | -------------------------------------------------------------------------------- /Sources/Snippets/Model/Snippet.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | /// A Swift code snippet. 12 | /// 13 | /// A *snippet* is a short, focused code example that can be shown with little to no context or prose. 14 | public struct Snippet { 15 | /// The URL of the source file for this snippet. 16 | public var sourceFile: URL 17 | 18 | /// A short abstract explaining what the snippet does. 19 | public var explanation: String 20 | 21 | /// The ``presentationLines`` joined with a newline `"\n"` separator. 22 | public var presentationCode: String { 23 | return presentationLines.joined(separator: "\n") 24 | } 25 | 26 | /// The code to display as the snippet. 27 | public var presentationLines: [String] 28 | 29 | /// Named line ranges in the snippet. 30 | public var slices: [String: Range] 31 | 32 | init(parsing source: String, sourceFile: URL) { 33 | let extractor = SnippetParser(source: source) 34 | self.explanation = extractor.explanationLines.joined(separator: "\n") 35 | self.presentationLines = extractor.presentationLines.map(String.init) 36 | self.slices = extractor.slices 37 | self.sourceFile = sourceFile 38 | } 39 | 40 | /// Create a Swift snippet by parsing a file. 41 | /// 42 | /// - Parameter sourceFile: The URL of the file to parse. 43 | public init(parsing sourceFile: URL) throws { 44 | let source = try String(contentsOf: sourceFile) 45 | self.init(parsing: source, sourceFile: sourceFile) 46 | } 47 | 48 | subscript(sliceIdentifier: String) -> String? { 49 | guard let slice = slices[sliceIdentifier] else { 50 | return nil 51 | } 52 | return presentationLines[slice].joined(separator: "\n") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/EmptyFile.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | // This is an empty source file used to make SwiftDocCPluginDocumentation a valid 10 | // documentation build target. 11 | // 12 | // SwiftDocCPluginDocumentation is an otherwise empty target that includes high-level, 13 | // user-facing documentation about using the Swift-DocC Plugin from the command-line. 14 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/README.md: -------------------------------------------------------------------------------- 1 | # Swift-DocC Plugin Documentation 2 | 3 | `SwiftDocCPluginDocumentation` is an otherwise empty target that includes high-level, 4 | user-facing documentation about using the Swift-DocC plugin from the command-line. 5 | 6 | The documentation content in the `SwiftDocCPlugin.docc` catalog is published on GitHub pages 7 | at https://swiftlang.github.io/swift-docc-plugin/documentation/swiftdoccplugin via the 8 | [`bin/update-gh-pages-documentation-site`](../../bin/update-gh-pages-documentation-site) 9 | script in this repository. 10 | 11 | 12 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Generating Documentation for Extended Types.md: -------------------------------------------------------------------------------- 1 | # Generating Documentation for Extended Types 2 | 3 | Generate documentation for the extensions you make to types from other modules. 4 | 5 | ## Overview 6 | 7 | The Swift-DocC plugin allows you to document extensions you make to types that are not from the module you're generating documentation for. 8 | 9 | As of Swift 5.9, extension support is enabled by default. If you're using an older version of Swift or would like to configure this behavior pass the `--include-extended-types` or `--exclude-extended-types` flag to enable or disable the feature, respectively: 10 | 11 | $ swift package generate-documentation --include-extended-types 12 | 13 | $ swift package generate-documentation --exclude-extended-types 14 | 15 | > Note: Extension support is available when using Swift 5.8 or later and the Swift-DocC plugin 1.2 or later. Extension support is enabled by default starting with Swift 5.9 and the Swift-DocC plugin 1.3. 16 | 17 | ## Understanding What is an Extended Type 18 | 19 | Not every type you add an extension to is an extended type. If the extension is declared in the same target as the type it is extending, the extension's contents will always be included in the documentation. Only extensions you make to types from other targets are represented as an external type in your documentation archive. 20 | 21 | ```swift 22 | public struct Sloth { } 23 | 24 | extension Sloth { 25 | // This method is always included 26 | // in the documentation. 27 | public func wake() { /* ... */ } 28 | } 29 | 30 | // `Collection` is from the standard library, 31 | // not the `SlothCreator` library, so this is 32 | // what we call an "extended type". 33 | extension Collection where Element == Sloth { 34 | // This property is only included in 35 | // the documentation if extension 36 | // support is enabled. 37 | public func wake() { 38 | for sloth in self { 39 | sloth.wake() 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | ## Finding Extended Types in your Documentation 46 | 47 | Extended Types are part of the documentation archive of the target that declares the extensions. 48 | 49 | ![The rendered documentation for SlothCreator/Swift/Collection](extended-type-example) 50 | 51 | Within that documentation archive, Extended Types are grouped by the Extended Module they belong to. You can find the latter on your documentation's landing page in a section called "Extended Modules". In our example from above, we have one Extended Module called "Swift" - the name of the standard library. This page can be referenced like this: ` ``SlothCreator/Swift`` `. 52 | 53 | The Extended Type ` ``SlothCreator/Swift/Collection`` ` is a child of the Extended Module and is listed under "Extended Protocols" on the ` ``SlothCreator/Swift`` ` page. 54 | 55 | > Note: The references above use the full path, including the name of the catalog's target, `SlothCreator`. This should help to understand the symbol's exact location, but usually isn't necessary. 56 | 57 | 58 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Generating Documentation for Hosting Online.md: -------------------------------------------------------------------------------- 1 | # Generating Documentation for Hosting Online 2 | 3 | Generate documentation for hosting online at static hosts like 4 | GitHub Pages or on your own server. 5 | 6 | ## Overview 7 | 8 | By default, the Swift-DocC plugin will generate an index of content that is useful for IDE's, 9 | like Xcode, to render a navigator of all included documentation in the archive. Since this index 10 | isn't relevant when hosting online, you can pass the `--disable-indexing` flag 11 | when generating documentation thats intended for an online host. 12 | 13 | $ swift package generate-documentation --target [target-name] --disable-indexing 14 | 15 | You'll likely want to pass an output path to Swift-DocC to send the relevant files 16 | to a specific directory. 17 | 18 | > Important: Remember to pass the `--allow-writing-to-directory` flag 19 | > to include that directory in the SwiftPM sandboxing environment for the Swift-DocC plugin. 20 | 21 | $ swift package --allow-writing-to-directory [output-directory-path] \ 22 | generate-documentation --target [target-name] --disable-indexing \ 23 | --output-path [output-directory-path] 24 | 25 | The files at the passed `[output-directory-path]` are now ready to be published online. Please 26 | see the documentation 27 | [here](https://www.swift.org/documentation/docc/distributing-documentation-to-other-developers#Host-a-Documentation-Archive-on-Your-Website) 28 | that explains how to configure the necessary routing rules for hosting a Swift-DocC archive 29 | on your local server. 30 | 31 | ## Transforming for Static Hosting 32 | 33 | Alternatively, if you'd like to avoid setting custom routing rules on your server or are 34 | hosting in an environment where this isn't possible, you can generate documentation that 35 | has been transformed for static hosting. 36 | 37 | $ swift package --allow-writing-to-directory [output-directory-path] \ 38 | generate-documentation --target [target-name] --disable-indexing \ 39 | --output-path [output-directory-path] \ 40 | --transform-for-static-hosting 41 | 42 | This addition of the `--transform-for-static-hosting` flag removes the need of setting 43 | any custom routing rules on your website, as long as you're hosting the documentation 44 | at the root of your website. If you'd like to host your documentation at a sub-path, you 45 | can use the `--hosting-base-path` argument. 46 | 47 | $ swift package --allow-writing-to-directory [output-directory-path] \ 48 | generate-documentation --target [target-name] --disable-indexing \ 49 | --output-path [output-directory-path] \ 50 | --transform-for-static-hosting \ 51 | --hosting-base-path [hosting-base-path] 52 | 53 | 54 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Generating Documentation for a Specific Target.md: -------------------------------------------------------------------------------- 1 | # Generating Documentation for a Specific Target 2 | 3 | Generate documentation for a specific target in your package. 4 | 5 | ## Overview 6 | 7 | By default, the Swift-DocC plugin will generate documentation for every 8 | compatible target in the current package and its dependencies. 9 | 10 | $ swift package generate-documentation 11 | 12 | To limit to a specific target defined in your package, pass the name of the target to the 13 | `generate-documentation` command. 14 | 15 | $ swift package generate-documentation --target SwiftMarkdown 16 | 17 | To limit to a specific product defined in your package _or one of its dependencies_, pass the 18 | name of the product to the `generate-documentation` command. 19 | 20 | $ swift package generate-documentation --product ArgumentParser 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | CDDefaultCodeListingLanguage 15 | shell 16 | CDDefaultModuleKind 17 | Command Plugin 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Previewing Documentation.md: -------------------------------------------------------------------------------- 1 | # Previewing Documentation 2 | 3 | Use your web browser to preview the documentation for a target in your package. 4 | 5 | ## Overview 6 | 7 | The Swift-DocC plugin supports running a local web server to allow for exploring the documentation 8 | in a project or previewing the documentation you're writing. 9 | 10 | Because the `preview-documentation` command uses a local web server, you'll need to disable 11 | the SwiftPM plugin's sandboxing functionality which blocks all network access by default. 12 | 13 | To preview documentation for a specific target in your project, run the following from 14 | the root of the Swift Package containing the target you'd like to preview: 15 | 16 | $ swift package --disable-sandbox preview-documentation --target [target-name] 17 | 18 | Swift-DocC will print something like the following: 19 | 20 | ======================================== 21 | Starting Local Preview Server 22 | Address: http://localhost:8000/documentation/swiftdoccplugin 23 | ======================================== 24 | Monitoring /Developer/swift-docc-plugin/Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc for changes... 25 | 26 | Navigate to the printed address in your browser to preview documentation for the target. 27 | 28 | > Tip: You can also preview documentation for a product defined by any of your package's 29 | > dependencies. This may be useful for learning about any packages you're importing. 30 | > 31 | > ```shell 32 | > $ swift package --disable-sandbox preview-documentation --product [product-name] 33 | > ``` 34 | 35 | 36 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Publishing to GitHub Pages.md: -------------------------------------------------------------------------------- 1 | # Publishing to GitHub Pages 2 | 3 | Build and publish documentation from your Swift Package to GitHub Pages or other static 4 | web hosts. 5 | 6 | ## Overview 7 | 8 | This documentation is specific to hosting on GitHub Pages but the steps 9 | should apply to most static hosting solutions you're familiar with. 10 | 11 | ## Configure GitHub Pages 12 | 13 | Begin by following 14 | [GitHub's documentation](https://docs.github.com/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site#choosing-a-publishing-source) 15 | to enable GitHub pages on your repository and select a publishing source. 16 | 17 | You can choose to either publish from your `main` branch alongside your project's source code 18 | or a specific branch that you'll use exclusively for your GitHub pages site. 19 | 20 | Either way, you should choose the option to publish from the `/docs` 21 | subdirectory instead of the default option to publish from your repository's root. 22 | 23 | Next, clone the repository you just configured for GitHub pages and checkout 24 | the branch you chose as a publishing source. 25 | 26 | $ git clone [repository-url] 27 | $ cd [repository-name] 28 | $ git checkout [gh-pages-branch] 29 | 30 | ## Understanding your Project's Configuration 31 | 32 | Now that you've set up a local clone of the repository you'll be publishing documentation from, 33 | you can build your Swift-DocC documentation site. 34 | 35 | > Tip: Before invoking the Swift-DocC plugin, you'll need to add it as a dependency of your package, 36 | > see for details. 37 | 38 | Before running the `swift package generate-documentation` command, you'll need to know two things. 39 | 40 | 1. What is the **base path** your documentation will be published at? 41 | 42 | This differs based on the 43 | [type of GitHub Pages site you have](https://docs.github.com/pages/getting-started-with-github-pages/about-github-pages#types-of-github-pages-sites) 44 | but is _almost_ always the name of your GitHub repository. 45 | 46 | Your documentation site will be published at something like 47 | 48 | ```txt 49 | https://.github.io//... 50 | ``` 51 | 52 | and Swift-DocC needs to know about any base path after the `github.io` portion in order 53 | to correctly configure relative links. In the above case, that would be ``. 54 | 55 | However, there are some configurations of GitHub Pages where this base URL path will not be required. For example, GitHub Enterprise Cloud has a feature that allows you to publish your GitHub Pages site privately. In situations like this, the URL structure will be different and will not require a custom base path. 56 | 57 | Your documentation site will be published at something like 58 | 59 | ```txt 60 | https://.pages.github.io/... 61 | ``` 62 | 63 | Because these websites have their own unique subdomain, your website is published directly at the root and the `[hosting-base-path]` argument is not needed. 64 | 65 | > Tip: If you're unsure what kind of GitHub Pages site you have, pay close attention to the URL listed on the GitHub Pages tab of your repository's settings after you've enabled the feature. 66 | 67 | 68 | 2. Which **target** in your Swift Package would you like to publish documentation for? 69 | 70 | Swift-DocC can build documentation for a single target at a time. When publishing documentation, 71 | you should select one target per documentation site. 72 | 73 | Once you've determined your hosting **base path** and Swift Package **target**, you're ready to 74 | generate documentation. 75 | 76 | ## Generating the Documentation Site 77 | 78 | To build documentation for your site and send the output to the `/docs` directory at the root 79 | of the repository you cloned to host your documentation, run the following **from the root 80 | of the Swift package you want to generate documentation from**: 81 | 82 | $ swift package --allow-writing-to-directory [path-to-docs-directory] \ 83 | generate-documentation --target [target-name] \ 84 | --disable-indexing \ 85 | --transform-for-static-hosting \ 86 | --hosting-base-path [hosting-base-path] \ 87 | --output-path [path-to-docs-directory] 88 | 89 | Here's a mapping of the tokens in the above command to what they should be replaced with: 90 | 91 | | Token | Description | 92 | |----------------------------|----------------------------------------------------------------------------------------------------------------| 93 | | `[path-to-docs-directory]` | The path to the `/docs` directory at the root of the repository you configured for publishing to GitHub pages. | 94 | | `[target-name]` | The name of the Swift Package target you'd like to build documentation for. | 95 | | `[hosting-base-path]` | The base path, if any, your website will be hosted at. Most likely this will be the name of your GitHub repository. | 96 | 97 | ## Publishing the Documentation Site 98 | 99 | To publish your documentation site, commit and push the changes in the repository and 100 | branch you configured for publishing to GitHub Pages. 101 | 102 | $ cd [path-to-github-pages-repository] 103 | $ git add docs 104 | $ git commit -m "Update GitHub pages documentation site." 105 | $ git push 106 | 107 | Once the push completes, the documentation site will be available at: 108 | 109 | https://.github.io//documentation/ 110 | 111 | If your GitHub Pages site is published privately, then the documentation will be available at: 112 | 113 | https://.pages.github.io/documentation/ 114 | 115 | 116 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Resources/extended-type-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftlang/swift-docc-plugin/d1691545d53581400b1de9b0472d45eb25c19fed/Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Resources/extended-type-example.png -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Resources/extended-type-example~dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftlang/swift-docc-plugin/d1691545d53581400b1de9b0472d45eb25c19fed/Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Resources/extended-type-example~dark.png -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/SwiftDocCPlugin.md: -------------------------------------------------------------------------------- 1 | # ``SwiftDocCPlugin`` 2 | 3 | Produce Swift-DocC documentation for Swift Package libraries and executables. 4 | 5 | @Metadata { 6 | @DisplayName("Swift-DocC Plugin") 7 | } 8 | 9 | ## Overview 10 | 11 | The Swift-DocC plugin is a Swift Package Manager command plugin that supports building 12 | documentation for SwiftPM libraries and executables. 13 | 14 | To use the Swift-DocC plugin with your package, first add it as a dependency: 15 | 16 | ```swift 17 | let package = Package( 18 | // name, platforms, products, etc. 19 | dependencies: [ 20 | // other dependencies 21 | .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // targets 25 | ] 26 | ) 27 | ``` 28 | 29 | > Note: Swift 5.6 or higher is required in order to run the plugin. 30 | 31 | Then, build documentation for the libraries and executables in that package and its dependencies by running the 32 | following from the command-line: 33 | 34 | $ swift package generate-documentation 35 | 36 | Use the `--help` flag to get a list of all supported arguments: 37 | 38 | $ swift package plugin generate-documentation --help 39 | 40 | The documentation on this site is focused on the Swift-DocC plugin specifically. For more 41 | general documentation on how to use Swift-DocC, see the documentation 42 | [here](https://www.swift.org/documentation/docc/). 43 | 44 | ## Topics 45 | 46 | ### Getting Started 47 | 48 | - 49 | - 50 | - 51 | 52 | ### Publishing Documentation 53 | 54 | - 55 | - 56 | 57 | 58 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/BuildGraph/DocumentationBuildGraph.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024-2025 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | /// A target that can have a documentation task in the build graph 12 | protocol DocumentationBuildGraphTarget: SendableMetatype { 13 | typealias ID = String 14 | /// The unique identifier of this target 15 | var id: ID { get } 16 | /// The unique identifiers of this target's direct dependencies (non-transitive). 17 | var dependencyIDs: [ID] { get } 18 | } 19 | 20 | /// A build graph of documentation tasks. 21 | struct DocumentationBuildGraph { 22 | fileprivate typealias ID = Target.ID 23 | /// All the documentation tasks 24 | let tasks: [Task] 25 | 26 | /// Creates a new documentation build graph for a series of targets with dependencies. 27 | init(targets: some Sequence) { 28 | // Create tasks 29 | let taskLookup: [ID: Task] = targets.reduce(into: [:]) { acc, target in 30 | acc[target.id] = Task(target: target) 31 | } 32 | // Add dependency information to each task 33 | for task in taskLookup.values { 34 | task.dependencies = task.target.dependencyIDs.compactMap { taskLookup[$0] } 35 | } 36 | 37 | tasks = Array(taskLookup.values) 38 | } 39 | 40 | /// Creates a list of dependent operations to perform the given work for each task in the build graph. 41 | /// 42 | /// You can add these operations to an `OperationQueue` to perform them in dependency order 43 | /// (dependencies before dependents). The queue can run these operations concurrently. 44 | /// 45 | /// - Parameter work: The work to perform for each task in the build graph. 46 | /// - Returns: A list of dependent operations that performs `work` for each documentation task task. 47 | func makeOperations(performing work: @escaping (Task) -> Void) -> [Operation] { 48 | var builder = OperationBuilder(work: work) 49 | for task in tasks { 50 | builder.buildOperationHierarchy(for: task) 51 | } 52 | 53 | return Array(builder.operationsByID.values) 54 | } 55 | } 56 | 57 | extension DocumentationBuildGraph { 58 | /// A documentation task in the build graph 59 | final class Task { 60 | /// The target to build documentation for 61 | let target: Target 62 | /// The unique identifier of the task 63 | fileprivate var id: ID { target.id } 64 | /// The other documentation tasks that this task depends on. 65 | fileprivate(set) var dependencies: [Task] 66 | 67 | init(target: Target) { 68 | self.target = target 69 | self.dependencies = [] 70 | } 71 | } 72 | } 73 | 74 | extension DocumentationBuildGraph { 75 | /// A type that builds a hierarchy of dependent operations 76 | private struct OperationBuilder { 77 | /// The work that each operation should perform 78 | let work: (Task) -> Void 79 | /// A lookup of operations by their ID 80 | private(set) var operationsByID: [ID: Operation] = [:] 81 | 82 | /// Adds new dependent operations to the builder. 83 | /// 84 | /// You can access the created dependent operations using `operationsByID.values`. 85 | mutating func buildOperationHierarchy(for task: Task) { 86 | let operation = makeOperation(for: task) 87 | for dependency in task.dependencies { 88 | let hasAlreadyVisitedTask = operationsByID[dependency.id] != nil 89 | 90 | let dependentOperation = makeOperation(for: dependency) 91 | operation.addDependency(dependentOperation) 92 | 93 | if !hasAlreadyVisitedTask { 94 | buildOperationHierarchy(for: dependency) 95 | } 96 | } 97 | } 98 | 99 | /// Returns the existing operation for the given task or creates a new operation if the builder didn't already have an operation for this task. 100 | private mutating func makeOperation(for task: Task) -> Operation { 101 | if let existing = operationsByID[task.id] { 102 | return existing 103 | } 104 | // Copy the closure and the target into a block operation object 105 | let new = BlockOperation { [work, task] in 106 | work(task) 107 | } 108 | operationsByID[task.id] = new 109 | return new 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/BuildGraph/DocumentationBuildGraphRunner.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | 10 | import Foundation 11 | 12 | /// A type that runs tasks for each target in a build graph in dependency order. 13 | struct DocumentationBuildGraphRunner { 14 | 15 | let buildGraph: DocumentationBuildGraph 16 | 17 | typealias Work = (DocumentationBuildGraph.Task) throws -> Result 18 | 19 | func perform(_ work: @escaping Work) throws -> [Result] { 20 | // Create a serial queue to perform each documentation build task 21 | let queue = OperationQueue() 22 | 23 | // Operations can't raise errors. Instead we catch the error from 'performBuildTask(_:)' 24 | // and cancel the remaining tasks. 25 | let resultLock = Lock() 26 | var caughtError: Error? 27 | var results: [Result] = [] 28 | 29 | let operations = buildGraph.makeOperations { [work] task in 30 | do { 31 | let result = try work(task) 32 | resultLock.withLock { 33 | results.append(result) 34 | } 35 | } catch { 36 | resultLock.withLock { 37 | caughtError = error 38 | queue.cancelAllOperations() 39 | } 40 | } 41 | } 42 | 43 | // Run all the documentation build tasks in dependency order (dependencies before dependents). 44 | queue.addOperations(operations, waitUntilFinished: true) 45 | 46 | // If any of the build tasks raised an error. Re-throw that error. 47 | if let caughtError { 48 | throw caughtError 49 | } 50 | 51 | return results 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/CommandLineArguments/CommandLineArgument.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// A named command line argument; either a flag or an option with a value. 10 | public struct CommandLineArgument { 11 | /// The names of this command line argument. 12 | public var names: Names 13 | /// The kind of command line argument. 14 | public var kind: Kind 15 | 16 | /// A collection of names for a command line argument. 17 | public struct Names: Hashable { 18 | /// The preferred name for this command line argument. 19 | public var preferred: String 20 | /// All possible names for this command line argument. 21 | public var all: Set 22 | 23 | /// Creates a new command line argument collection of names. 24 | /// 25 | /// - Parameters: 26 | /// - preferred: The preferred name for this command line argument. 27 | /// - alternatives: A collection of alternative names for this command line argument. 28 | public init(preferred: String, alternatives: Set = []) { 29 | self.all = alternatives.union([preferred]) 30 | self.preferred = preferred 31 | } 32 | } 33 | 34 | /// A kind of command line argument. 35 | public enum Kind { 36 | /// A flag argument without an associated value. 37 | /// 38 | /// For example: `"--some-flag"`. 39 | case flag 40 | /// An option argument with an associated value. 41 | /// 42 | /// For example: `"--some-option", "value"` or `"--some-option=value"`. 43 | case option(value: String, kind: Option.Kind) 44 | } 45 | 46 | // Only create arguments from flags or options (with a value) 47 | 48 | init(_ flag: Flag) { 49 | names = flag.names 50 | kind = .flag 51 | } 52 | 53 | init(_ option: Option, value: String) { 54 | names = option.names 55 | kind = .option(value: value, kind: option.kind) 56 | } 57 | } 58 | 59 | extension CommandLineArgument { 60 | /// A flag argument without an associated value. 61 | /// 62 | /// For example: `"--some-flag"`. 63 | public struct Flag { 64 | /// The names of this command line flag. 65 | public var names: Names 66 | /// The negative names for this flag, if any. 67 | public var inverseNames: CommandLineArgument.Names? 68 | 69 | /// Creates a new command line flag. 70 | /// 71 | /// - Parameters: 72 | /// - preferred: The preferred name for this flag. 73 | /// - alternatives: A collection of alternative names for this flag. 74 | /// - inverseNames: The negative names for this flag, if any. 75 | public init(preferred: String, alternatives: Set = [], inverseNames: CommandLineArgument.Names? = nil) { 76 | // This is duplicating the `Names` parameters to offer a nicer initializer for the common case. 77 | names = .init(preferred: preferred, alternatives: alternatives) 78 | self.inverseNames = inverseNames 79 | } 80 | } 81 | 82 | /// An option argument that will eventually associated with a value. 83 | /// 84 | /// For example: `"--some-option", "value"` or `"--some-option=value"`. 85 | public struct Option { 86 | /// The names of this command line option. 87 | public var names: Names 88 | /// The kind of value for this command line option. 89 | public var kind: Kind 90 | 91 | /// A kind of value(s) that a command line option supports. 92 | public enum Kind { 93 | /// An option that supports a single value. 94 | case singleValue 95 | /// An option that supports an array of different values. 96 | case arrayOfValues 97 | } 98 | 99 | /// Creates a new command line option. 100 | /// 101 | /// - Parameters: 102 | /// - preferred: The preferred name for this option. 103 | /// - alternatives: A collection of alternative names for this option. 104 | /// - kind: The kind of value(s) that this option supports. 105 | public init(preferred: String, alternatives: Set = [], kind: Kind = .singleValue) { 106 | // This is duplicating the `Names` parameters to offer a nicer initializer for the common case. 107 | self.init( 108 | Names(preferred: preferred, alternatives: alternatives), 109 | kind: kind 110 | ) 111 | } 112 | 113 | init(_ names: Names, kind: Kind = .singleValue) { 114 | self.names = names 115 | self.kind = kind 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/CommandLineArguments/ParsedPluginArguments.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | /// A container of parsed values for the command line arguments that apply to the plugin itself. 12 | struct ParsedPluginArguments { 13 | var enableCombinedDocumentation: Bool 14 | var disableLMDBIndex: Bool 15 | var verbose: Bool 16 | var help: Bool 17 | 18 | /// Creates a new plugin arguments container by extracting the known plugin values from a command line argument list. 19 | init(extractingFrom arguments: inout CommandLineArguments) { 20 | enableCombinedDocumentation = arguments.extractFlag(.enableCombinedDocumentation) ?? false 21 | disableLMDBIndex = arguments.extractFlag(.disableLMDBIndex) ?? false 22 | verbose = arguments.extractFlag(.verbose) ?? false 23 | help = arguments.extract(Self.help).last ?? false 24 | } 25 | 26 | /// A common command line tool flag to print the help text instead of running the command. 27 | static let help = CommandLineArgument.Flag( 28 | preferred: "--help", alternatives: ["-h"] 29 | ) 30 | } 31 | 32 | /// A container of parsed values for the command line arguments that apply to symbol graph extraction. 33 | struct ParsedSymbolGraphArguments { 34 | // The default values for these are only known within the plugin itself. 35 | var minimumAccessLevel: String? 36 | var skipSynthesizedSymbols: Bool? 37 | var includeExtendedTypes: Bool? 38 | 39 | /// Creates a new symbol graph arguments container by extracting the known plugin values from a command line argument list. 40 | init(extractingFrom arguments: inout CommandLineArguments) { 41 | minimumAccessLevel = arguments.extractOption(.minimumAccessLevel) 42 | skipSynthesizedSymbols = arguments.extractFlag(.skipSynthesizedSymbols) 43 | includeExtendedTypes = arguments.extractFlag(.extendedTypes) 44 | } 45 | } 46 | 47 | private extension CommandLineArguments { 48 | mutating func extractFlag(_ flag: DocumentedArgument) -> Bool? { 49 | guard case .flag(let flag) = flag.argument else { 50 | assertionFailure("Unexpectedly used flag-only API with an option \(flag.argument)") 51 | return nil 52 | } 53 | return extract(flag).last 54 | } 55 | 56 | mutating func extractOption(_ flag: DocumentedArgument) -> String? { 57 | guard case .option(let option) = flag.argument else { 58 | assertionFailure("Unexpectedly used option-only API with a flag \(flag.argument)") 59 | return nil 60 | } 61 | 62 | assert(option.kind == .singleValue, "Unexpectedly used single-value option API with an array-of-values option \(option)") 63 | return extract(option).last 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/DispatchTimeInterval+descriptionInSeconds.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | // These extensions are slightly modified version of the ones originally declared in the 12 | // Swift Package Manager here: 13 | // https://github.com/apple/swift-package-manager/blob/beac985/Sources/Basics/DispatchTimeInterval+Extensions.swift 14 | extension DispatchTimeInterval { 15 | /// A description of the current time interval suitable for presentation, in seconds. 16 | /// 17 | /// Emits a value to a precision of 2 decimal points. For example, this might be `"42.08s"`, or 18 | /// `"0.00s"`, or `"3.04s"`. 19 | public var descriptionInSeconds: String { 20 | switch self { 21 | case .seconds(let value): 22 | return "\(value).00s" 23 | case .milliseconds(let value): 24 | return String(format: "%.2f", Double(value)/Double(1000)) + "s" 25 | case .microseconds(let value): 26 | return String(format: "%.2f", Double(value)/Double(1_000_000)) + "s" 27 | case .nanoseconds(let value): 28 | return String(format: "%.2f", Double(value)/Double(1_000_000_000)) + "s" 29 | case .never: 30 | return "n/a" 31 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 32 | @unknown default: 33 | return "n/a" 34 | #endif 35 | } 36 | } 37 | } 38 | 39 | // `distance(to:)` is currently only available on macOS. This should be removed 40 | // when it's available on all platforms. 41 | #if os(Linux) || os(Windows) || os(Android) || os(OpenBSD) 42 | extension DispatchTime { 43 | public func distance(to: DispatchTime) -> DispatchTimeInterval { 44 | let duration = to.uptimeNanoseconds - self.uptimeNanoseconds 45 | return .nanoseconds(duration >= Int.max ? Int.max : Int(duration)) 46 | } 47 | } 48 | #endif 49 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/DocCFeatures.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | /// The features that a `docc` executable lists in its corresponding "features.json" file. 12 | /// 13 | /// In a Swift toolchain, the `docc` executable is located at `usr/bin/docc` and the 14 | /// corresponding features file is located at `usr/share/docc/features.json`. 15 | /// 16 | /// The "features.json" file is a list of named features. For example: 17 | /// 18 | /// ```json 19 | /// { 20 | /// "features": [ 21 | /// { 22 | /// "name": "diagnostics-file" 23 | /// }, 24 | /// { 25 | /// "name": "dependency" 26 | /// }, 27 | /// { 28 | /// "name": "overloads" 29 | /// } 30 | /// ] 31 | /// } 32 | /// ``` 33 | struct DocCFeatures: Decodable { 34 | /// A single named feature that's supported for a given `docc` executable. 35 | struct Feature: Decodable, Hashable { 36 | var name: String 37 | } 38 | private var features: Set 39 | 40 | /// Decodes the DocC features that correspond to a given `docc` executable in a Swift toolchain. 41 | init(doccExecutable: URL) throws { 42 | let data = try Data(contentsOf: Self._featuresURL(forDoccExecutable: doccExecutable)) 43 | self = try JSONDecoder().decode(DocCFeatures.self, from: data) 44 | } 45 | 46 | /// Creates an empty list of supported DocC features. 47 | init() { 48 | features = [] 49 | } 50 | 51 | /// Returns the "features.json" file for a given `docc` executable in a Swift toolchain. 52 | static func _featuresURL(forDoccExecutable doccExecutable: URL) -> URL { 53 | doccExecutable 54 | .deletingLastPathComponent() // docc 55 | .deletingLastPathComponent() // bin 56 | .appendingPathComponent("share/docc/features.json") 57 | } 58 | } 59 | 60 | extension DocCFeatures: Collection { 61 | typealias Index = Set.Index 62 | typealias Element = Set.Element 63 | 64 | var startIndex: Index { features.startIndex } 65 | var endIndex: Index { features.endIndex } 66 | 67 | subscript(index: Index) -> Iterator.Element { 68 | get { features[index] } 69 | } 70 | 71 | func index(after i: Index) -> Index { 72 | return features.index(after: i) 73 | } 74 | } 75 | 76 | // MARK: Known features 77 | 78 | extension DocCFeatures.Feature { 79 | /// DocC supports writing diagnostic information to a JSON file, specified by `--diagnostics-output-path`. 80 | static let diagnosticsFileOutput = DocCFeatures.Feature(name: "diagnostics-file") 81 | 82 | /// DocC supports linking between documentation builds and merging archives into a combined archive. 83 | static let linkDependencies = DocCFeatures.Feature(name: "dependency") 84 | 85 | /// DocC supports grouping overloaded symbols. 86 | static let overloads = DocCFeatures.Feature(name: "overloads") 87 | 88 | /// DocC supports specifying the display name and kind of the synthesized landing page for combined archive. 89 | static let synthesizedLandingPageName = DocCFeatures.Feature(name: "synthesized-landing-page-name") 90 | } 91 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/DocumentationTargetKind.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// A Swift-DocC documentation target kind. 10 | enum DocumentationTargetKind: String { 11 | /// A Swift Package Manager library target. 12 | case library 13 | 14 | /// A Swift Package Manager executable target. 15 | case executable 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/DocumentedPluginFlags/DocumentedArgument.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// A documented command-line argument for the plugin. 10 | /// 11 | /// This may include some arguments (flags or options) that the plugin forwards to the symbol graph extract tool or to DocC. 12 | struct DocumentedArgument { 13 | /// A command line argument (flag or option) that is wrapped with documentation. 14 | enum Argument { 15 | case flag(CommandLineArgument.Flag) 16 | case option(CommandLineArgument.Option) 17 | } 18 | 19 | /// The command line argument (flag or option) that this documentation applies to. 20 | var argument: Argument 21 | 22 | /// The positive names for this flag 23 | var names: CommandLineArgument.Names { 24 | switch argument { 25 | case .flag(let flag): 26 | return flag.names 27 | case .option(let option): 28 | return option.names 29 | } 30 | } 31 | 32 | /// A short user-facing description of this argument (flag or option). 33 | var abstract: String 34 | 35 | /// An expanded user-facing description of this argument (flag or option). 36 | var discussion: String? 37 | 38 | init(flag: CommandLineArgument.Flag, abstract: String, discussion: String? = nil) { 39 | self.argument = .flag(flag) 40 | self.abstract = abstract 41 | self.discussion = discussion 42 | } 43 | 44 | init(option: CommandLineArgument.Option, abstract: String, discussion: String? = nil) { 45 | self.argument = .option(option) 46 | self.abstract = abstract 47 | self.discussion = discussion 48 | } 49 | } 50 | 51 | // MARK: Plugin flags 52 | 53 | extension DocumentedArgument { 54 | /// A plugin feature flag to enable building combined documentation for multiple targets. 55 | /// 56 | /// - Note: This flag requires that the `docc` executable supports ``Feature/linkDependencies``. 57 | static let enableCombinedDocumentation = Self( 58 | flag: .init(preferred: "--enable-experimental-combined-documentation"), 59 | abstract: "Create a combined DocC archive with all generated documentation.", 60 | discussion: """ 61 | Experimental feature that allows targets to link to pages in their dependencies and that \ 62 | creates an additional "combined" DocC archive containing all the generated documentation. 63 | """ 64 | ) 65 | 66 | /// A plugin feature flag to skip adding the `--emit-lmdb-index` flag, that the plugin adds by default. 67 | static let disableLMDBIndex = Self( 68 | flag: .init(preferred: "--disable-indexing", alternatives: ["--no-indexing"]), 69 | abstract: "Disable indexing for the produced DocC archive.", 70 | discussion: """ 71 | Produces a DocC archive that is best-suited for hosting online but incompatible with Xcode. 72 | """ 73 | ) 74 | 75 | /// A plugin feature flag to enable verbose logging. 76 | static let verbose = Self( 77 | flag: .init(preferred: "--verbose"), 78 | abstract: "Increase verbosity to include informational output.", 79 | discussion: nil 80 | ) 81 | 82 | // We don't need to document the `--help` flag 83 | } 84 | 85 | // MARK: Symbol graph flags 86 | 87 | extension DocumentedArgument { 88 | /// A plugin feature flag to either include or exclude extended types in documentation archives. 89 | /// 90 | /// Enables/disables the extension block symbol format when calling the dump symbol graph API. 91 | /// 92 | /// - Note: This flag is only available starting from Swift 5.8. It should be hidden from the `--help` command for lower toolchain versions. 93 | /// However, we do not hide the flag entirely, because this enables us to give a more precise warning when accidentally used with Swift 5.7 or lower. 94 | static let extendedTypes = Self( 95 | flag: .init( 96 | preferred: "--include-extended-types", 97 | inverseNames: .init(preferred: "--exclude-extended-types") 98 | ), 99 | abstract: "Control whether extended types from other modules are shown in the produced DocC archive. (default: --\(Self.defaultExtendedTypesValue ? "include" : "exclude")-extended-types)", 100 | discussion: "Allows documenting symbols that a target adds to its dependencies." 101 | ) 102 | 103 | /// A plugin feature flag to exclude synthesized symbols from the generated documentation. 104 | /// 105 | /// `--experimental-skip-synthesized-symbols` produces a DocC archive without compiler-synthesized symbols. 106 | static let skipSynthesizedSymbols = Self( 107 | flag: .init(preferred: "--experimental-skip-synthesized-symbols"), 108 | abstract: "Exclude synthesized symbols from the generated documentation.", 109 | discussion: """ 110 | Experimental feature that produces a DocC archive without compiler synthesized symbols. 111 | """ 112 | ) 113 | 114 | /// The minimum access level that the symbol graph extractor will emit symbols for 115 | static let minimumAccessLevel = Self( 116 | option: .init(preferred: "--symbol-graph-minimum-access-level"), 117 | abstract: "Include symbols with this access level or more.", 118 | discussion: """ 119 | Supported access level values are: `open`, `public`, `internal`, `private`, `fileprivate` 120 | """ 121 | ) 122 | 123 | #if swift(>=5.9) 124 | private static let defaultExtendedTypesValue = true 125 | #else 126 | private static let defaultExtendedTypesValue = false 127 | #endif 128 | } 129 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/FoundationExtensions/SendableMetatypeShim.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2025 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | // In Swift 6.2, metatypes are no longer sendable by default (SE-0470). 10 | // Instead a type needs to conform to `SendableMetatype` to indicate that its metatype is sendable. 11 | // 12 | // However, `SendableMetatype` doesn't exist before Swift 6.1 so we define an internal alias to `Any` here. 13 | // This means that conformances to `SendableMetatype` has no effect before 6.2 indicates metatype sendability in 6.2 onwards. 14 | // 15 | // Note: Adding a protocol requirement to a _public_ API is a breaking change. 16 | 17 | #if compiler(<6.2) 18 | typealias SendableMetatype = Any 19 | #endif 20 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/FoundationExtensions/String+singleQuoted.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | extension String { 12 | var singleQuoted: String { 13 | return "'\(self)'" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/Lock.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | /// A wrapper around NSLock. 12 | /// 13 | /// This type exist to offer an alternative to `NSLock.withLock` on Linux before Swift 6.0. 14 | struct Lock { 15 | private let innerLock = NSLock() 16 | 17 | func withLock(_ body: () throws -> Result) rethrows -> Result { 18 | // Use `lock()` and `unlock()` because Linux doesn't support `NSLock.withLock` before Swift 6.0 19 | innerLock.lock() 20 | defer { innerLock.unlock() } 21 | 22 | return try body() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/PluginAction.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | /// A Swift-DocC plugin action. 10 | public enum PluginAction: String { 11 | /// Creates a Swift-DocC documentation archive from a Swift Package. 12 | case convert 13 | 14 | /// Creates and previews a Swift-DocC documentation archive from a Swift Package. 15 | case preview 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftDocCPluginUtilities/Snippets/SnippetExtractor.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | /// Manages snippet symbol graph extraction. 12 | public class SnippetExtractor { 13 | /// Uniquely identifies a Swift Package Manager package. 14 | public typealias PackageIdentifier = String 15 | 16 | enum SymbolGraphExtractionResult { 17 | case packageDoesNotProduceSnippets 18 | case packageContainsSnippets(symbolGraphFile: URL) 19 | } 20 | 21 | private let snippetTool: URL 22 | private let workingDirectory: URL 23 | 24 | private var snippetSymbolGraphExtractionResults = [PackageIdentifier : SymbolGraphExtractionResult]() 25 | 26 | /// Create a new snippet extractor with the given tool 27 | /// and working directory. 28 | public init(snippetTool: URL, workingDirectory: URL) { 29 | self.snippetTool = snippetTool 30 | self.workingDirectory = workingDirectory 31 | } 32 | 33 | private func snippetsDirectory(in packageDirectory: URL) -> URL? { 34 | let snippetsDirectory = packageDirectory.appendingPathComponent("Snippets") 35 | guard _fileExists(snippetsDirectory.path) else { 36 | return nil 37 | } 38 | 39 | return snippetsDirectory 40 | } 41 | 42 | private func snippetsOutputDirectory( 43 | in pluginWorkingDirectory: URL, 44 | packageIdentifier: PackageIdentifier, 45 | packageDisplayName: String 46 | ) -> URL { 47 | return pluginWorkingDirectory 48 | .appendingPathComponent(".build", isDirectory: true) 49 | .appendingPathComponent("symbol-graphs", isDirectory: true) 50 | .appendingPathComponent("snippet-symbol-graphs", isDirectory: true) 51 | .appendingPathComponent("\(packageDisplayName)-\(packageIdentifier)", isDirectory: true) 52 | } 53 | 54 | /// Runs the given process and waits for it to exit. 55 | /// 56 | /// Provided for testing. 57 | var _runProcess: (Process) throws -> () = { process in 58 | try process.run() 59 | process.waitUntilExit() 60 | } 61 | 62 | /// Returns true if the given file exists on disk. 63 | /// 64 | /// Provided for testing. 65 | var _fileExists: (_ path: String) -> Bool = { path in 66 | return FileManager.default.fileExists(atPath: path) 67 | } 68 | 69 | /// Returns all of the `.swift` files under a directory recursively. 70 | /// 71 | /// Provided for testing. 72 | var _findSnippetFilesInDirectory: (_ directory: URL) -> [String] = { directory -> [String] in 73 | guard let snippetEnumerator = FileManager.default.enumerator(at: directory, 74 | includingPropertiesForKeys: nil, 75 | options: [.skipsHiddenFiles]) else { 76 | return [] 77 | 78 | } 79 | var snippetInputFiles = [String]() 80 | for case let potentialSnippetURL as URL in snippetEnumerator { 81 | guard potentialSnippetURL.pathExtension.lowercased() == "swift" else { 82 | continue 83 | } 84 | snippetInputFiles.append(potentialSnippetURL.path) 85 | } 86 | 87 | return snippetInputFiles 88 | } 89 | 90 | /// Generate snippets for the given package. 91 | /// 92 | /// The snippet extractor has an internal cache so it's safe to call this 93 | /// function multiple times with the same package identifier. 94 | /// 95 | /// - Parameters: 96 | /// - packageIdentifier: A unique identifier for the package. 97 | /// - packageDisplayName: A display name for the package. 98 | /// - packageDirectory: The root directory for this package. 99 | /// 100 | /// The snippet extractor will look for a `Snippets` subdirectory 101 | /// within this directory. 102 | /// 103 | /// - Returns: A URL for the output file of the generated snippets symbol graph JSON file. 104 | public func generateSnippets( 105 | for packageIdentifier: PackageIdentifier, 106 | packageDisplayName: String, 107 | packageDirectory: URL 108 | ) throws -> URL? { 109 | switch snippetSymbolGraphExtractionResults[packageIdentifier] { 110 | case .packageContainsSnippets(symbolGraphFile: let symbolGraphFile): 111 | return symbolGraphFile 112 | case .packageDoesNotProduceSnippets: 113 | return nil 114 | case .none: 115 | // No existing build result for this package identifier 116 | break 117 | } 118 | 119 | guard let snippetsDirectory = snippetsDirectory(in: packageDirectory) else { 120 | snippetSymbolGraphExtractionResults[packageIdentifier] = .packageDoesNotProduceSnippets 121 | return nil 122 | } 123 | 124 | let snippetInputFiles = _findSnippetFilesInDirectory(snippetsDirectory) 125 | 126 | guard !snippetInputFiles.isEmpty else { 127 | snippetSymbolGraphExtractionResults[packageIdentifier] = .packageDoesNotProduceSnippets 128 | return nil 129 | } 130 | 131 | let outputDirectory = snippetsOutputDirectory( 132 | in: workingDirectory, 133 | packageIdentifier: packageIdentifier, 134 | packageDisplayName: packageDisplayName 135 | ) 136 | 137 | let outputFile = outputDirectory.appendingPathComponent("\(packageDisplayName)-snippets.symbols.json") 138 | 139 | let process = Process() 140 | process.executableURL = snippetTool 141 | process.arguments = [ 142 | "--output", outputFile.path, 143 | "--module-name", packageDisplayName, 144 | ] + snippetInputFiles 145 | 146 | try _runProcess(process) 147 | 148 | if _fileExists(outputFile.path) { 149 | snippetSymbolGraphExtractionResults[packageIdentifier] = .packageContainsSnippets(symbolGraphFile: outputFile) 150 | return outputFile 151 | } else { 152 | snippetSymbolGraphExtractionResults[packageIdentifier] = .packageDoesNotProduceSnippets 153 | return nil 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Sources/snippet-extract/SnippetBuildCommand.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import Snippets 11 | import SymbolKit 12 | 13 | @main 14 | struct SnippetExtractCommand { 15 | enum OptionName: String { 16 | case moduleName = "--module-name" 17 | case outputFile = "--output" 18 | } 19 | 20 | enum Argument { 21 | case moduleName(String) 22 | case outputFile(String) 23 | case inputFile(String) 24 | } 25 | 26 | enum ArgumentError: Error, CustomStringConvertible { 27 | case missingOption(OptionName) 28 | case missingOptionValue(OptionName) 29 | case snippetNotContainedInSnippetsDirectory(URL) 30 | 31 | var description: String { 32 | switch self { 33 | case .missingOption(let optionName): 34 | return "Missing required option \(optionName.rawValue)" 35 | case .missingOptionValue(let optionName): 36 | return "Missing required option value for \(optionName.rawValue)" 37 | case .snippetNotContainedInSnippetsDirectory(let snippetFileURL): 38 | return "Snippet file '\(snippetFileURL.path)' is not contained in a directory called 'Snippets' at any level, so this tool is not able to compute the path components that would be used for linking to the snippet. It may exist in a subdirectory, but one of its parent directories must be named 'Snippets'." 39 | } 40 | } 41 | } 42 | 43 | var snippetFiles = [String]() 44 | var outputFile: String 45 | var moduleName: String 46 | 47 | static func printUsage() { 48 | let usage = """ 49 | USAGE: snippet-extract --output --module-name 50 | 51 | ARGUMENTS: 52 | (Required) 53 | The path of the output Symbol Graph JSON file representing the snippets for the module or package 54 | (Required) 55 | The module name to use for the Symbol Graph (typically should be the package name) 56 | 57 | One or more absolute paths to snippet files to interpret as snippets 58 | """ 59 | print(usage) 60 | } 61 | 62 | init(arguments: [String]) throws { 63 | var arguments = arguments 64 | 65 | var parsedOutputFile: String? = nil 66 | var parsedModuleName: String? = nil 67 | 68 | while let argument = try arguments.parseSnippetArgument() { 69 | switch argument { 70 | case .inputFile(let inputFile): 71 | snippetFiles.append(inputFile) 72 | case .moduleName(let moduleName): 73 | parsedModuleName = moduleName 74 | case .outputFile(let outputFile): 75 | parsedOutputFile = outputFile 76 | } 77 | } 78 | 79 | guard let parsedOutputFile = parsedOutputFile else { 80 | throw ArgumentError.missingOption(.outputFile) 81 | } 82 | self.outputFile = parsedOutputFile 83 | 84 | guard let parsedModuleName = parsedModuleName else { 85 | throw ArgumentError.missingOption(.moduleName) 86 | } 87 | self.moduleName = parsedModuleName 88 | } 89 | 90 | func run() throws { 91 | let snippets = try snippetFiles.map { 92 | try Snippet(parsing: URL(fileURLWithPath: $0)) 93 | } 94 | guard snippets.count > 0 else { return } 95 | let symbolGraphFilename = URL(fileURLWithPath: outputFile) 96 | try emitSymbolGraph(for: snippets, to: symbolGraphFilename, moduleName: moduleName) 97 | } 98 | 99 | func emitSymbolGraph(for snippets: [Snippet], to emitFilename: URL, moduleName: String) throws { 100 | let snippetSymbols = try snippets.map { try SymbolGraph.Symbol($0, moduleName: moduleName) } 101 | let metadata = SymbolGraph.Metadata(formatVersion: .init(major: 0, minor: 1, patch: 0), generator: "snippet-extract") 102 | let module = SymbolGraph.Module(name: moduleName, platform: .init(architecture: nil, vendor: nil, operatingSystem: nil, environment: nil), isVirtual: true) 103 | let symbolGraph = SymbolGraph(metadata: metadata, module: module, symbols: snippetSymbols, relationships: []) 104 | try FileManager.default.createDirectory(atPath: emitFilename.deletingLastPathComponent().path, withIntermediateDirectories: true, attributes: nil) 105 | let encoder = JSONEncoder() 106 | let data = try encoder.encode(symbolGraph) 107 | try data.write(to: emitFilename) 108 | } 109 | 110 | func files(in directory: URL, withExtension fileExtension: String? = nil) throws -> [URL] { 111 | guard directory.isDirectory else { 112 | return [] 113 | } 114 | 115 | let files = try FileManager.default.contentsOfDirectory(atPath: directory.path) 116 | .map { directory.appendingPathComponent($0) } 117 | .filter { $0.isFile } 118 | 119 | guard let fileExtension = fileExtension else { 120 | return files 121 | } 122 | 123 | return files.filter { $0.pathExtension == fileExtension } 124 | } 125 | 126 | func subdirectories(in directory: URL) throws -> [URL] { 127 | guard directory.isDirectory else { 128 | return [] 129 | } 130 | return try FileManager.default.contentsOfDirectory(atPath: directory.path) 131 | .map { directory.appendingPathComponent($0) } 132 | .filter { $0.isDirectory } 133 | } 134 | 135 | static func main() throws { 136 | if CommandLine.arguments.count == 1 || CommandLine.arguments.contains("-h") || CommandLine.arguments.contains("--help") { 137 | printUsage() 138 | exit(0) 139 | } 140 | do { 141 | let snippetExtract = try SnippetExtractCommand(arguments: Array(CommandLine.arguments.dropFirst(1))) 142 | try snippetExtract.run() 143 | } catch let error as ArgumentError { 144 | printUsage() 145 | throw error 146 | } 147 | } 148 | } 149 | 150 | extension Array where Element == String { 151 | mutating func parseSnippetArgument() throws -> SnippetExtractCommand.Argument? { 152 | guard let thisArgument = first else { 153 | return nil 154 | } 155 | removeFirst() 156 | switch thisArgument { 157 | case "--module-name": 158 | guard let nextArgument = first else { 159 | throw SnippetExtractCommand.ArgumentError.missingOptionValue(.moduleName) 160 | } 161 | removeFirst() 162 | return .moduleName(nextArgument) 163 | case "--output": 164 | guard let nextArgument = first else { 165 | throw SnippetExtractCommand.ArgumentError.missingOptionValue(.outputFile) 166 | } 167 | removeFirst() 168 | return .outputFile(nextArgument) 169 | default: 170 | return .inputFile(thisArgument) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/snippet-extract/Utility/SymbolGraph+Snippet.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import Snippets 11 | import struct SymbolKit.SymbolGraph 12 | 13 | extension SymbolGraph.Symbol { 14 | /// Create a symbol for a given snippet. 15 | /// 16 | /// - Parameters: 17 | /// - snippet: The snippet to create a symbol for. 18 | /// - moduleName: The name to use for the package name in the snippet symbol's precise identifier. 19 | public init(_ snippet: Snippets.Snippet, moduleName: String) throws { 20 | let basename = snippet.sourceFile.deletingPathExtension().lastPathComponent 21 | let identifier = SymbolGraph.Symbol.Identifier(precise: "$snippet__\(moduleName).\(basename)", interfaceLanguage: "swift") 22 | let names = SymbolGraph.Symbol.Names.init(title: basename, navigator: nil, subHeading: nil, prose: nil) 23 | 24 | var pathComponents = Array(snippet.sourceFile.absoluteURL.deletingPathExtension().pathComponents[...]) 25 | 26 | guard let snippetsPathComponentIndex = pathComponents.firstIndex(where: { 27 | $0 == "Snippets" 28 | }) else { 29 | throw SnippetExtractCommand.ArgumentError.snippetNotContainedInSnippetsDirectory(snippet.sourceFile) 30 | } 31 | 32 | // In theory, there may be differently named snippet root directories in the future. 33 | // Replace that path component with the standardized `Snippets`. 34 | pathComponents.replaceSubrange(pathComponents.startIndex...snippetsPathComponentIndex, 35 | with: CollectionOfOne("Snippets")) 36 | 37 | let docComment = SymbolGraph.LineList(snippet.explanation 38 | .split(separator: "\n", maxSplits: Int.max, omittingEmptySubsequences: false) 39 | .map { line in 40 | SymbolGraph.LineList.Line(text: String(line), range: nil) 41 | }) 42 | let accessLevel = SymbolGraph.Symbol.AccessControl(rawValue: "public") 43 | 44 | let kind = SymbolGraph.Symbol.Kind(parsedIdentifier: .snippet, displayName: "Snippet") 45 | 46 | self.init(identifier: identifier, 47 | names: names, 48 | pathComponents: pathComponents, 49 | docComment: docComment, 50 | accessLevel: accessLevel, 51 | kind: kind, 52 | mixins: [ 53 | SymbolGraph.Symbol.Snippet.mixinKey: SymbolGraph.Symbol.Snippet(language: "swift", lines: snippet.presentationLines, slices: snippet.slices) 54 | ], 55 | isVirtual: true) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/snippet-extract/Utility/URL+Status.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022-2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | 11 | extension URL { 12 | var isFile: Bool { 13 | let attrs = try? FileManager.default.attributesOfItem(atPath: self.path) 14 | return attrs?[.type] as? FileAttributeType == .typeRegular 15 | } 16 | 17 | var isDirectory: Bool { 18 | var isADirectory: ObjCBool = false 19 | return FileManager.default.fileExists(atPath: self.path, isDirectory: &isADirectory) 20 | && isADirectory.boolValue 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/SwiftDocCPluginUtilitiesTests/DispatchTimeIntervalExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import SwiftDocCPluginUtilities 11 | import XCTest 12 | 13 | final class DispatchTimeIntervalExtensionTests: XCTestCase { 14 | func testDescriptionInSeconds() { 15 | XCTAssertEqual(DispatchTimeInterval.nanoseconds(1000).descriptionInSeconds, "0.00s") 16 | XCTAssertEqual(DispatchTimeInterval.nanoseconds(10000000).descriptionInSeconds, "0.01s") 17 | XCTAssertEqual(DispatchTimeInterval.nanoseconds(6000000000).descriptionInSeconds, "6.00s") 18 | 19 | XCTAssertEqual(DispatchTimeInterval.microseconds(1000).descriptionInSeconds, "0.00s") 20 | XCTAssertEqual(DispatchTimeInterval.microseconds(10000).descriptionInSeconds, "0.01s") 21 | XCTAssertEqual(DispatchTimeInterval.microseconds(8000000).descriptionInSeconds, "8.00s") 22 | XCTAssertEqual(DispatchTimeInterval.microseconds(185009000).descriptionInSeconds, "185.01s") 23 | 24 | XCTAssertEqual(DispatchTimeInterval.milliseconds(200).descriptionInSeconds, "0.20s") 25 | XCTAssertEqual(DispatchTimeInterval.milliseconds(1000).descriptionInSeconds, "1.00s") 26 | XCTAssertEqual(DispatchTimeInterval.milliseconds(80040).descriptionInSeconds, "80.04s") 27 | 28 | XCTAssertEqual(DispatchTimeInterval.seconds(5).descriptionInSeconds, "5.00s") 29 | XCTAssertEqual(DispatchTimeInterval.seconds(305).descriptionInSeconds, "305.00s") 30 | 31 | XCTAssertEqual(DispatchTimeInterval.never.descriptionInSeconds, "n/a") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/SwiftDocCPluginUtilitiesTests/DocCFeaturesTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2024 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | @testable import SwiftDocCPluginUtilities 11 | import XCTest 12 | 13 | final class DocCFeaturesTests: XCTestCase { 14 | func testKnownFeatures() throws { 15 | let json = Data(""" 16 | { 17 | "features": [ 18 | { 19 | "name": "diagnostics-file" 20 | }, 21 | { 22 | "name": "dependency" 23 | }, 24 | { 25 | "name": "overloads" 26 | } 27 | ] 28 | } 29 | """.utf8) 30 | let features = try JSONDecoder().decode(DocCFeatures.self, from: json) 31 | 32 | XCTAssertEqual(features.count, 3) 33 | 34 | XCTAssert(features.contains(.diagnosticsFileOutput)) 35 | XCTAssert(features.contains(.overloads)) 36 | XCTAssert(features.contains(.linkDependencies)) 37 | 38 | XCTAssertFalse(features.contains(.init(name: "some-unknown-feature"))) 39 | } 40 | 41 | func testUnknownFeatures() throws { 42 | let json = Data(""" 43 | { 44 | "features": [ 45 | { 46 | "name": "some-unknown-feature" 47 | } 48 | ] 49 | } 50 | """.utf8) 51 | let features = try JSONDecoder().decode(DocCFeatures.self, from: json) 52 | 53 | XCTAssertEqual(features.count, 1) 54 | 55 | XCTAssert(features.contains(.init(name: "some-unknown-feature"))) 56 | 57 | XCTAssertFalse(features.contains(.diagnosticsFileOutput)) 58 | XCTAssertFalse(features.contains(.overloads)) 59 | XCTAssertFalse(features.contains(.linkDependencies)) 60 | } 61 | 62 | func testFeaturesURL() { 63 | XCTAssertEqual( 64 | DocCFeatures._featuresURL(forDoccExecutable: URL(fileURLWithPath: "/path/to/toolchain/usr/bin/docc")), 65 | URL(fileURLWithPath: "/path/to/toolchain/usr/share/docc/features.json") 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/SwiftDocCPluginUtilitiesTests/Snippets/SnippetSymbolTests.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | 10 | @testable import Snippets 11 | import struct SymbolKit.SymbolGraph 12 | @testable import snippet_extract 13 | import XCTest 14 | 15 | class SnippetSymbolTests: XCTestCase { 16 | func testThrowsErrorWhenCreatingFloatingSwiftSnippet() throws { 17 | let source = """ 18 | // A snippet. 19 | foo() {} 20 | """ 21 | let snippet = Snippets.Snippet(parsing: source, 22 | sourceFile: URL(fileURLWithPath: "/tmp/to/floating/File.swift")) 23 | XCTAssertThrowsError(try SymbolGraph.Symbol(snippet, moduleName: "MyModule"), 24 | "Expected snippetNotContainedInSnippetsDirectory error", 25 | { (error: Error) in 26 | guard let argumentError = error as? SnippetExtractCommand.ArgumentError, 27 | case .snippetNotContainedInSnippetsDirectory = argumentError else { 28 | XCTFail("Expected snippetNotContainedInSnippetsDirectory error") 29 | return 30 | } 31 | }) 32 | } 33 | 34 | func testPathComponentsForSnippetSymbol() throws { 35 | let source = """ 36 | // A snippet. 37 | foo() {} 38 | """ 39 | let snippet = Snippets.Snippet(parsing: source, 40 | sourceFile: URL(fileURLWithPath: "/path/to/my-package/Snippets/ASnippet.swift")) 41 | let symbol = try SymbolGraph.Symbol(snippet, moduleName: "my-package") 42 | XCTAssertEqual(["Snippets", "ASnippet"], symbol.pathComponents) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/SwiftDocCPluginUtilitiesTests/Test Fixtures/DocCConvertHelpFixture.txt: -------------------------------------------------------------------------------- 1 | OVERVIEW: Convert documentation markup, assets, and symbol information into a documentation archive. 2 | 3 | When building documentation for source code, the 'convert' command is commonly invoked by other tools as part of a build workflow. Such build workflows can perform tasks to extract symbol graph information and may 4 | infer values for 'docc' flags and options from other build configuration. 5 | 6 | When building documentation for a catalog that only contain articles or tutorial content, interacting with the 'docc convert' command directly can be a good alternative to using DocC via a build workflow. 7 | 8 | USAGE: docc convert [] [--additional-symbol-graph-dir ] 9 | docc convert [] [--additional-symbol-graph-dir ] [--output-dir ] 10 | docc convert [] [--additional-symbol-graph-dir ] [--output-dir ] [] [] [] [] [] [] [] 11 | 12 | INPUTS & OUTPUTS: 13 | Path to a '.docc' documentation catalog directory. 14 | --additional-symbol-graph-dir 15 | A path to a directory of additional symbol graph files. 16 | -o, --output-path, --output-dir 17 | The location where the documentation compiler writes the built documentation. 18 | 19 | AVAILABILITY OPTIONS: 20 | --platform Specify information about the current release of a platform. 21 | Each platform's information is specified via separate "--platform" values using the following format: "name={platform name},version={semantic version}". 22 | Optionally, the platform information can include a 'beta={true|false}' component. If no beta information is provided, the platform is considered not in beta. 23 | 24 | SOURCE REPOSITORY OPTIONS: 25 | --checkout-path 26 | The root path on disk of the repository's checkout. 27 | --source-service 28 | The source code service used to host the project's sources. 29 | Required when using '--source-service-base-url'. Supported values are 'github', 'gitlab', and 'bitbucket'. 30 | --source-service-base-url 31 | The base URL where the source service hosts the project's sources. 32 | Required when using '--source-service'. For example, 'https://github.com/my-org/my-repo/blob/main'. 33 | 34 | HOSTING OPTIONS: 35 | --hosting-base-path 36 | The base path your documentation website will be hosted at. 37 | For example, if you deploy your site to 'example.com/my_name/my_project/documentation' instead of 'example.com/documentation', pass '/my_name/my_project' as the base path. 38 | --transform-for-static-hosting/--no-transform-for-static-hosting 39 | Produce a DocC archive that supports static hosting environments. (default: --transform-for-static-hosting) 40 | 41 | DIAGNOSTIC OPTIONS: 42 | --analyze Include 'note'/'information' level diagnostics in addition to warnings and errors. 43 | --diagnostics-file, --diagnostics-output-path 44 | The location where the documentation compiler writes the diagnostics file. 45 | Specifying a diagnostic file path implies '--ide-console-output'. 46 | --diagnostic-filter, --diagnostic-level 47 | Filter diagnostics with a lower severity than this level. 48 | This option is ignored if `--analyze` is passed. 49 | 50 | This filter level is inclusive. If a level of 'note' is specified, diagnostics with a severity up to and including 'note' will be printed. 51 | The supported diagnostic filter levels are: 52 | - error 53 | - warning 54 | - note, info, information 55 | - hint, notice 56 | --ide-console-output, --emit-fixits 57 | Format output to the console intended for an IDE or other tool to parse. 58 | --warnings-as-errors Treat warnings as errors 59 | 60 | INFO.PLIST FALLBACKS: 61 | --default-code-listing-language 62 | A fallback default language for code listings if no value is provided in the documentation catalogs's Info.plist file. 63 | --fallback-display-name, --display-name 64 | A fallback display name if no value is provided in the documentation catalogs's Info.plist file. 65 | If no display name is provided in the catalogs's Info.plist file or via the '--fallback-display-name' option, DocC will infer a display name from the documentation catalog base name or from the module name 66 | from the symbol graph files provided via the '--additional-symbol-graph-dir' option. 67 | --fallback-bundle-identifier, --bundle-identifier 68 | A fallback bundle identifier if no value is provided in the documentation catalogs's Info.plist file. 69 | If no bundle identifier is provided in the catalogs's Info.plist file or via the '--fallback-bundle-identifier' option, DocC will infer a bundle identifier from the display name. 70 | --fallback-default-module-kind 71 | A fallback default module kind if no value is provided in the documentation catalogs's Info.plist file. 72 | If no module kind is provided in the catalogs's Info.plist file or via the '--fallback-default-module-kind' option, DocC will display the module kind as a "Framework". 73 | 74 | DOCUMENTATION COVERAGE (EXPERIMENTAL): 75 | --experimental-documentation-coverage 76 | Generate documentation coverage output. 77 | Detailed documentation coverage information will be written to 'documentation-coverage.json' in the output directory. 78 | --coverage-summary-level 79 | The level of documentation coverage information to write on standard out. (default: brief) 80 | The '--coverage-summary-level' level has no impact on the information in the 'documentation-coverage.json' file. 81 | The supported coverage summary levels are 'brief' and 'detailed'. 82 | --coverage-symbol-kind-filter 83 | Filter documentation coverage to only analyze symbols of the specified symbol kinds. 84 | Specify a list of symbol kind values to filter the documentation coverage to only those types symbols. 85 | The supported symbol kind values are: associated-type, class, dictionary, enumeration, enumeration-case, function, global-variable, http-request, initializer, instance-method, instance-property, 86 | instance-subcript, instance-variable, module, operator, protocol, structure, type-alias, type-method, type-property, type-subscript, typedef 87 | 88 | LINK RESOLUTION OPTIONS (EXPERIMENTAL): 89 | --dependency 90 | A path to a documentation archive to resolve external links against. 91 | Only documentation archives built with '--enable-experimental-external-link-support' are supported as dependencies. 92 | 93 | FEATURE FLAGS: 94 | --experimental-enable-custom-templates 95 | Allows for custom templates, like `header.html`. 96 | --enable-inherited-docs Inherit documentation for inherited symbols 97 | --allow-arbitrary-catalog-directories 98 | Experimental: allow catalog directories without the `.docc` extension. 99 | --enable-experimental-external-link-support 100 | Support external links to this documentation output. 101 | Write additional link metadata files to the output directory to support resolving documentation links to the documentation in that output directory. 102 | --emit-digest Write additional metadata files to the output directory. 103 | --emit-lmdb-index Writes an LMDB representation of the navigator index to the output directory. 104 | A JSON representation of the navigator index is emitted by default. 105 | 106 | OPTIONS: 107 | -h, --help Show help information. 108 | -------------------------------------------------------------------------------- /Tests/SwiftDocCPluginUtilitiesTests/Test Fixtures/DocCPreviewHelpFixture.txt: -------------------------------------------------------------------------------- 1 | OVERVIEW: Convert documentation inputs and preview the documentation output. 2 | 3 | The 'preview' command extends the 'convert' command by running a preview server and monitoring the documentation input for modifications to rebuild the documentation. 4 | 5 | USAGE: docc preview [] [--port ] [--additional-symbol-graph-dir ] 6 | docc preview [] [--port ] [--additional-symbol-graph-dir ] [--output-dir ] 7 | docc preview [] [--port ] [--additional-symbol-graph-dir ] [--output-dir ] [] [] [] [] [] [] [] 8 | 9 | PREVIEW OPTIONS: 10 | -p, --port 11 | Port number to use for the preview web server. (default: 8080) 12 | Path to a '.docc' documentation catalog directory. 13 | --additional-symbol-graph-dir 14 | A path to a directory of additional symbol graph files. 15 | -o, --output-path, --output-dir 16 | The location where the documentation compiler writes the built documentation. 17 | --platform Specify information about the current release of a platform. 18 | Each platform's information is specified via separate "--platform" values using the following format: "name={platform name},version={semantic version}". 19 | Optionally, the platform information can include a 'beta={true|false}' component. If no beta information is provided, the platform is considered not in beta. 20 | --checkout-path 21 | The root path on disk of the repository's checkout. 22 | --source-service 23 | The source code service used to host the project's sources. 24 | Required when using '--source-service-base-url'. Supported values are 'github', 'gitlab', and 'bitbucket'. 25 | --source-service-base-url 26 | The base URL where the source service hosts the project's sources. 27 | Required when using '--source-service'. For example, 'https://github.com/my-org/my-repo/blob/main'. 28 | --hosting-base-path 29 | The base path your documentation website will be hosted at. 30 | For example, if you deploy your site to 'example.com/my_name/my_project/documentation' instead of 'example.com/documentation', pass '/my_name/my_project' as the base path. 31 | --transform-for-static-hosting/--no-transform-for-static-hosting 32 | Produce a DocC archive that supports static hosting environments. (default: --transform-for-static-hosting) 33 | --analyze Include 'note'/'information' level diagnostics in addition to warnings and errors. 34 | --diagnostics-file, --diagnostics-output-path 35 | The location where the documentation compiler writes the diagnostics file. 36 | Specifying a diagnostic file path implies '--ide-console-output'. 37 | --diagnostic-filter, --diagnostic-level 38 | Filter diagnostics with a lower severity than this level. 39 | This option is ignored if `--analyze` is passed. 40 | 41 | This filter level is inclusive. If a level of 'note' is specified, diagnostics with a severity up to and including 'note' will be printed. 42 | The supported diagnostic filter levels are: 43 | - error 44 | - warning 45 | - note, info, information 46 | - hint, notice 47 | --ide-console-output, --emit-fixits 48 | Format output to the console intended for an IDE or other tool to parse. 49 | --warnings-as-errors Treat warnings as errors 50 | --default-code-listing-language 51 | A fallback default language for code listings if no value is provided in the documentation catalogs's Info.plist file. 52 | --fallback-display-name, --display-name 53 | A fallback display name if no value is provided in the documentation catalogs's Info.plist file. 54 | If no display name is provided in the catalogs's Info.plist file or via the '--fallback-display-name' option, DocC will infer a display name from the documentation catalog base name or from the module name 55 | from the symbol graph files provided via the '--additional-symbol-graph-dir' option. 56 | --fallback-bundle-identifier, --bundle-identifier 57 | A fallback bundle identifier if no value is provided in the documentation catalogs's Info.plist file. 58 | If no bundle identifier is provided in the catalogs's Info.plist file or via the '--fallback-bundle-identifier' option, DocC will infer a bundle identifier from the display name. 59 | --fallback-default-module-kind 60 | A fallback default module kind if no value is provided in the documentation catalogs's Info.plist file. 61 | If no module kind is provided in the catalogs's Info.plist file or via the '--fallback-default-module-kind' option, DocC will display the module kind as a "Framework". 62 | --experimental-documentation-coverage 63 | Generate documentation coverage output. 64 | Detailed documentation coverage information will be written to 'documentation-coverage.json' in the output directory. 65 | --coverage-summary-level 66 | The level of documentation coverage information to write on standard out. (default: brief) 67 | The '--coverage-summary-level' level has no impact on the information in the 'documentation-coverage.json' file. 68 | The supported coverage summary levels are 'brief' and 'detailed'. 69 | --coverage-symbol-kind-filter 70 | Filter documentation coverage to only analyze symbols of the specified symbol kinds. 71 | Specify a list of symbol kind values to filter the documentation coverage to only those types symbols. 72 | The supported symbol kind values are: associated-type, class, dictionary, enumeration, enumeration-case, function, global-variable, http-request, initializer, instance-method, instance-property, 73 | instance-subcript, instance-variable, module, operator, protocol, structure, type-alias, type-method, type-property, type-subscript, typedef 74 | --dependency 75 | A path to a documentation archive to resolve external links against. 76 | Only documentation archives built with '--enable-experimental-external-link-support' are supported as dependencies. 77 | --experimental-enable-custom-templates 78 | Allows for custom templates, like `header.html`. 79 | --enable-inherited-docs Inherit documentation for inherited symbols 80 | --allow-arbitrary-catalog-directories 81 | Experimental: allow catalog directories without the `.docc` extension. 82 | --enable-experimental-external-link-support 83 | Support external links to this documentation output. 84 | Write additional link metadata files to the output directory to support resolving documentation links to the documentation in that output directory. 85 | --emit-digest Write additional metadata files to the output directory. 86 | --emit-lmdb-index Writes an LMDB representation of the navigator index to the output directory. 87 | A JSON representation of the navigator index is emitted by default. 88 | 89 | OPTIONS: 90 | -h, --help Show help information. 91 | -------------------------------------------------------------------------------- /Tests/SwiftDocCPluginUtilitiesTests/Utilities/XCTest+testResourceAsString.swift: -------------------------------------------------------------------------------- 1 | // This source file is part of the Swift.org open source project 2 | // 3 | // Copyright (c) 2022 Apple Inc. and the Swift project authors 4 | // Licensed under Apache License v2.0 with Runtime Library Exception 5 | // 6 | // See https://swift.org/LICENSE.txt for license information 7 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 8 | 9 | import Foundation 10 | import XCTest 11 | 12 | extension XCTestCase { 13 | func testResourceAsString(named resourceName: String) throws -> String { 14 | let resourceURL = try XCTUnwrap( 15 | Bundle.module.url( 16 | forResource: resourceName, 17 | withExtension: "txt", 18 | subdirectory: "Test Fixtures" 19 | ) 20 | ) 21 | 22 | return try String(contentsOf: resourceURL, encoding: .utf8) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bin/check-source: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This source file is part of the Swift.org open source project 4 | # 5 | # Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | # Licensed under Apache License v2.0 with Runtime Library Exception 7 | # 8 | # See https://swift.org/LICENSE.txt for license information 9 | # See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | # 11 | 12 | # This script performs code checks such as verifying that source files 13 | # use the correct license header. Its contents are largely copied from 14 | # https://github.com/apple/swift-nio/blob/main/scripts/soundness.sh 15 | 16 | set -eu 17 | here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 18 | 19 | function replace_acceptable_years() { 20 | # this needs to replace all acceptable forms with 'YEARS' 21 | sed -e 's/20[12][789012345]-20[12][89012345]/YEARS/' -e 's/20[12][89012345]/YEARS/' 22 | } 23 | 24 | printf "=> Checking for unacceptable language… " 25 | # This greps for unacceptable terminology. The square bracket[s] are so that 26 | # "git grep" doesn't find the lines that greps :). 27 | unacceptable_terms=( 28 | -e blacklis[t] 29 | -e whitelis[t] 30 | -e slav[e] 31 | ) 32 | # Ignore the NonInclusiveLangeChecker source and tests 33 | if git grep --color=never -i "${unacceptable_terms[@]}" -- :/**/^NonInclusiveLanguageChecker*.swift > /dev/null; then 34 | printf "\033[0;31mUnacceptable language found.\033[0m\n" 35 | git grep -i "${unacceptable_terms[@]}" 36 | exit 1 37 | fi 38 | printf "\033[0;32mokay.\033[0m\n" 39 | 40 | printf "=> Checking license headers… " 41 | tmp=$(mktemp /tmp/.docc-check-source_XXXXXX) 42 | 43 | for language in swift-or-c bash md-or-tutorial; do 44 | declare -a matching_files 45 | declare -a exceptions 46 | declare -a reader 47 | matching_files=( -name '*' ) 48 | reader=head 49 | case "$language" in 50 | swift-or-c) 51 | exceptions=( -name Package.swift ) 52 | matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' ) 53 | cat > "$tmp" <<"EOF" 54 | // This source file is part of the Swift.org open source project 55 | // 56 | // Copyright (c) YEARS Apple Inc. and the Swift project authors 57 | // Licensed under Apache License v2.0 with Runtime Library Exception 58 | // 59 | // See https://swift.org/LICENSE.txt for license information 60 | // See https://swift.org/CONTRIBUTORS.txt for Swift project authors 61 | EOF 62 | ;; 63 | bash) 64 | matching_files=( -name '*.sh' ) 65 | cat > "$tmp" <<"EOF" 66 | #!/bin/bash 67 | # 68 | # This source file is part of the Swift.org open source project 69 | # 70 | # Copyright (c) YEARS Apple Inc. and the Swift project authors 71 | # Licensed under Apache License v2.0 with Runtime Library Exception 72 | # 73 | # See https://swift.org/LICENSE.txt for license information 74 | # See https://swift.org/CONTRIBUTORS.txt for Swift project authors 75 | # 76 | EOF 77 | ;; 78 | md-or-tutorial) 79 | exceptions=( -path "./.github/*.md") 80 | matching_files=( -name '*.md' -o -name '*.tutorial' ) 81 | reader=tail 82 | cat > "$tmp" <<"EOF" 83 | 84 | EOF 85 | ;; 86 | *) 87 | echo >&2 "ERROR: unknown language '$language'" 88 | ;; 89 | esac 90 | 91 | # Determine which SHA command to use; not all platforms support shasum, but they 92 | # likely either support shasum or sha256sum. 93 | SHA_CMD="" 94 | if [ -x "$(command -v shasum)" ]; then 95 | SHA_CMD="shasum" 96 | elif [ -x "$(command -v sha256sum)" ]; then 97 | SHA_CMD="sha256sum" 98 | else 99 | echo >&2 "No sha command found; install shasum or sha256sum or submit a PR that supports another platform" 100 | fi 101 | 102 | expected_lines=$(cat "$tmp" | wc -l) 103 | expected_sha=$(cat "$tmp" | "$SHA_CMD") 104 | 105 | ( 106 | cd "$here/.." 107 | find . \ 108 | \( \! -path '*/.build/*' -a \ 109 | \! -name '.' -a \ 110 | \( "${matching_files[@]}" \) -a \ 111 | \( \! -path './swift-docc/*' \) -a \ 112 | \( \! -path './swift-docc-render-artifact/*' \) -a \ 113 | \( \! \( "${exceptions[@]}" \) \) \) | while read line; do 114 | if [[ "$(cat "$line" | replace_acceptable_years | $reader -n $expected_lines | $SHA_CMD)" != "$expected_sha" ]]; then 115 | printf "\033[0;31mmissing headers in file '$line'!\033[0m\n" 116 | diff -u <(cat "$line" | replace_acceptable_years | $reader -n $expected_lines) "$tmp" 117 | exit 1 118 | fi 119 | done 120 | ) 121 | done 122 | 123 | printf "\033[0;32mokay.\033[0m\n" 124 | rm "$tmp" 125 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This source file is part of the Swift.org open source project 4 | # 5 | # Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | # Licensed under Apache License v2.0 with Runtime Library Exception 7 | # 8 | # See https://swift.org/LICENSE.txt for license information 9 | # See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | # 11 | 12 | set -eu 13 | 14 | # A `realpath` alternative using the default C implementation. 15 | filepath() { 16 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" 17 | } 18 | 19 | # First get the absolute path to this file so we can get the 20 | # absolute file path to the Swift-DocC-Plugin root source dir. 21 | SWIFT_DOCC_PLUGIN_ROOT="$(dirname $(dirname $(filepath $0)))" 22 | 23 | SWIFT_SKIP_BUILDING_UPSTREAM_DOCC=${SWIFT_SKIP_BUILDING_UPSTREAM_DOCC:=false} 24 | 25 | if [[ $SWIFT_SKIP_BUILDING_UPSTREAM_DOCC != true ]] 26 | then 27 | SWIFT_DOCC_ROOT="$SWIFT_DOCC_PLUGIN_ROOT/swift-docc" 28 | SWIFT_DOCC_RENDER_ARTIFACT_ROOT="$SWIFT_DOCC_PLUGIN_ROOT/swift-docc-render-artifact" 29 | export DOCC_HTML_DIR="$SWIFT_DOCC_RENDER_ARTIFACT_ROOT/dist" 30 | 31 | SWIFT_DOCC_REPO=${SWIFT_DOCC_REPO:="https://github.com/swiftlang/swift-docc.git"} 32 | SWIFT_DOCC_RENDER_ARTIFACT_REPO=${SWIFT_DOCC_RENDER_ARTIFACT_REPO:="https://github.com/swiftlang/swift-docc-render-artifact.git"} 33 | 34 | SWIFT_DOCC_BRANCH=${SWIFT_DOCC_BRANCH:="main"} 35 | SWIFT_DOCC_RENDER_ARTIFACT_BRANCH=${SWIFT_DOCC_RENDER_ARTIFACT_BRANCH:="main"} 36 | 37 | # The script will clone swift-docc and swift-docc-render-artifact at the 38 | # branches pulled from the environment above. The tests will then run using 39 | # that built DocC. This can be useful for testing interdependent changes that 40 | # need to land together and make it possible to test multiple pull requests 41 | # together. 42 | 43 | echo "Cloning docc..." 44 | rm -rf "$SWIFT_DOCC_ROOT" 45 | git clone -b "$SWIFT_DOCC_BRANCH" "${SWIFT_DOCC_REPO}" "$SWIFT_DOCC_ROOT" || exit 1 46 | 47 | echo "Cloning docc-render-artifact..." 48 | rm -rf "$SWIFT_DOCC_RENDER_ARTIFACT_ROOT" 49 | git clone -b "${SWIFT_DOCC_RENDER_ARTIFACT_BRANCH}" "${SWIFT_DOCC_RENDER_ARTIFACT_REPO}" "$SWIFT_DOCC_RENDER_ARTIFACT_ROOT" || exit 1 50 | 51 | echo "Building docc..." 52 | swift build --package-path "$SWIFT_DOCC_ROOT" --product docc --configuration release || exit 1 53 | 54 | export DOCC_EXEC="$(swift build --package-path "$SWIFT_DOCC_ROOT" --show-bin-path --configuration release)/docc" 55 | if [[ ! -f "$DOCC_EXEC" ]]; then 56 | echo "docc executable not found, expected at $DOCC_EXEC" 57 | exit 1 58 | else 59 | echo "Using docc executable: $DOCC_EXEC" 60 | fi 61 | fi 62 | # Build and test Swift-DocC Plugin 63 | swift test --parallel --package-path "$SWIFT_DOCC_PLUGIN_ROOT" 64 | 65 | # Build and test Swift-DocC Plugin integration tests 66 | # We use a shared cache when building the plugin so these tests shouldn't be run in parallel. 67 | swift test --package-path "$SWIFT_DOCC_PLUGIN_ROOT/IntegrationTests" 68 | 69 | # Run source code checks for the codebase. 70 | LC_ALL=C "$SWIFT_DOCC_PLUGIN_ROOT"/bin/check-source 71 | 72 | printf "\033[0;32mokay.\033[0m\n" 73 | -------------------------------------------------------------------------------- /bin/update-gh-pages-documentation-site: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This source file is part of the Swift.org open source project 4 | # 5 | # Copyright (c) 2022 Apple Inc. and the Swift project authors 6 | # Licensed under Apache License v2.0 with Runtime Library Exception 7 | # 8 | # See https://swift.org/LICENSE.txt for license information 9 | # See https://swift.org/CONTRIBUTORS.txt for Swift project authors 10 | # 11 | # Updates the GitHub Pages documentation site thats published from the 'docs' 12 | # subdirectory in the 'gh-pages' branch of this repository. 13 | # 14 | # This script should be run by someone with commit access to the 'gh-pages' branch 15 | # at a regular frequency so that the documentation content on the GitHub Pages site 16 | # is up-to-date with the content in this repo. 17 | # 18 | 19 | set -eu 20 | 21 | # A `realpath` alternative using the default C implementation. 22 | filepath() { 23 | [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" 24 | } 25 | 26 | SWIFT_DOCC_PLUGIN_ROOT="$(dirname $(dirname $(filepath $0)))" 27 | 28 | # Set current directory to the repository root 29 | cd "$SWIFT_DOCC_PLUGIN_ROOT" 30 | 31 | # Use git worktree to checkout the gh-pages branch of this repository in a gh-pages sub-directory 32 | git fetch 33 | git worktree add --checkout gh-pages origin/gh-pages 34 | 35 | # Pretty print DocC JSON output so that it can be consistently diffed between commits 36 | export DOCC_JSON_PRETTYPRINT="YES" 37 | 38 | # Generate documentation for the 'SwiftDocCPlugin' target and output it 39 | # to the /docs subdirectory in the gh-pages worktree directory. 40 | export SWIFTPM_ENABLE_COMMAND_PLUGINS=1 41 | swift package \ 42 | --allow-writing-to-directory "$SWIFT_DOCC_PLUGIN_ROOT/gh-pages/docs" \ 43 | generate-documentation \ 44 | --target SwiftDocCPlugin \ 45 | --disable-indexing \ 46 | --transform-for-static-hosting \ 47 | --hosting-base-path swift-docc-plugin \ 48 | --output-path "$SWIFT_DOCC_PLUGIN_ROOT/gh-pages/docs" 49 | 50 | # Save the current commit we've just built documentation from in a variable 51 | CURRENT_COMMIT_HASH=`git rev-parse --short HEAD` 52 | 53 | # Commit and push our changes to the gh-pages branch 54 | cd gh-pages 55 | git add docs 56 | 57 | if [ -n "$(git status --porcelain)" ]; then 58 | echo "Documentation changes found. Commiting the changes to the 'gh-pages' branch and pushing to origin." 59 | git commit -m "Update GitHub Pages documentation site to '$CURRENT_COMMIT_HASH'." 60 | git push origin HEAD:gh-pages 61 | else 62 | # No changes found, nothing to commit. 63 | echo "No documentation changes found." 64 | fi 65 | 66 | # Delete the git worktree we created 67 | cd .. 68 | git worktree remove gh-pages 69 | --------------------------------------------------------------------------------