diff --git a/IntegrationTests/Package.swift b/IntegrationTests/Package.swift index 05b73b3..e72f1af 100644 --- a/IntegrationTests/Package.swift +++ b/IntegrationTests/Package.swift @@ -30,6 +30,7 @@ let package = Package( .copy("Fixtures/MixedTargets"), .copy("Fixtures/TargetWithDocCCatalog"), .copy("Fixtures/PackageWithSnippets"), + .copy("Fixtures/LibraryTargetWithExtensionSymbols"), ] ), ] diff --git a/IntegrationTests/Tests/Fixtures/LibraryTargetWithExtensionSymbols/Package.swift b/IntegrationTests/Tests/Fixtures/LibraryTargetWithExtensionSymbols/Package.swift new file mode 100644 index 0000000..a3efff1 --- /dev/null +++ b/IntegrationTests/Tests/Fixtures/LibraryTargetWithExtensionSymbols/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version: 5.6 +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors + +import Foundation +import PackageDescription + +let package = Package( + name: "LibraryTargetWithExtensionSymbols", + targets: [ + .target(name: "Library"), + ] +) + +// We only expect 'swift-docc-plugin' to be a sibling when this package +// is running as part of a test. +// +// This allows the package to compile outside of tests for easier +// test development. +if FileManager.default.fileExists(atPath: "../swift-docc-plugin") { + package.dependencies += [ + .package(path: "../swift-docc-plugin"), + ] +} diff --git a/IntegrationTests/Tests/Fixtures/LibraryTargetWithExtensionSymbols/Sources/Library/Library.swift b/IntegrationTests/Tests/Fixtures/LibraryTargetWithExtensionSymbols/Sources/Library/Library.swift new file mode 100644 index 0000000..62ca5ca --- /dev/null +++ b/IntegrationTests/Tests/Fixtures/LibraryTargetWithExtensionSymbols/Sources/Library/Library.swift @@ -0,0 +1,62 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors + +/// This is foo's documentation. +/// +/// Foo is a public struct and should be included in documentation. +public struct Foo { + public func foo() {} +} + +/// This is the documentation for ``Swift/Array``. +/// +/// This is the extension to ``Array`` with the longest documentation +/// comment, thus it is used for doucmenting the extended type in this +/// target. +extension Array { + /// This is the documentation for the ``isArray`` property + /// we added to ``Swift/Array``. + /// + /// This is a public extension to an external type and should be included + /// in the documentation. + public var isArray: Bool { true } +} + +/// This is the documentation for ``Swift/Int``. +/// +/// This is the extension to ``Int`` with the longest documentation +/// comment, thus it is used for doucmenting the extended type in this +/// target. +extension Int { + /// This is the documentation for the ``isArray`` property + /// we added to ``Swift/Int``. + /// + /// This is a public extension to an external type and should be included + /// in the documentation. + public var isArray: Bool { false } +} + + +/// This is the documentation for ``CustomFooConvertible``. +/// +/// This is a public protocol and should be included in the documentation. +public protocol CustomFooConvertible { + /// This is the documentation for ``CustomFooConvertible/asFoo``. + /// + /// This is a public protocol requirement and should be included in the documentation. + var asFoo: Foo { get } +} + +/// This is not used as the documentation comment for ``Swift/Int`` +/// as it is shorter than the comment on the other extension to `Int`. +extension Int: CustomFooConvertible { + /// This is the documentation for ``Swift/Int/asFoo``. + /// + /// This is a public protocol requirement implementation and should be included in the documentation. + public var asFoo: Foo { Foo() } +} diff --git a/IntegrationTests/Tests/TargetWithSwiftExtensionsTests.swift b/IntegrationTests/Tests/TargetWithSwiftExtensionsTests.swift new file mode 100644 index 0000000..9993b75 --- /dev/null +++ b/IntegrationTests/Tests/TargetWithSwiftExtensionsTests.swift @@ -0,0 +1,91 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors + +import Foundation +import XCTest + +final class TargetWithSwiftExtensionsTests: ConcurrencyRequiringTestCase { +#if swift(>=5.8) + let supportsIncludingSwiftExtendedTypes = true +#else + let supportsIncludingSwiftExtendedTypes = false +#endif + + override func setUpWithError() throws { + try XCTSkipUnless( + supportsIncludingSwiftExtendedTypes, + "The current toolchain does not support symbol graph generation for extended types." + ) + + try super.setUpWithError() + } + + func testGenerateDocumentationWithoutEnablementFlag() throws { + let result = try swiftPackage( + "generate-documentation", + workingDirectory: try setupTemporaryDirectoryForFixture(named: "LibraryTargetWithExtensionSymbols") + ) + + result.assertExitStatusEquals(0) + XCTAssertEqual(result.referencedDocCArchives.count, 1) + + let doccArchiveURL = try XCTUnwrap(result.referencedDocCArchives.first) + + let dataDirectoryContents = try filesIn(.dataSubdirectory, of: doccArchiveURL) + + XCTAssertEqual( + Set(dataDirectoryContents.map(\.lastTwoPathComponents)), + [ + "documentation/library.json", + + "library/foo.json", + "foo/foo().json", + + "library/customfooconvertible.json", + "customfooconvertible/asfoo.json", + ] + ) + } + + func testGenerateDocumentationWithEnablementFlag() throws { + let result = try swiftPackage( + "generate-documentation", + "--include-extended-types", + workingDirectory: try setupTemporaryDirectoryForFixture(named: "LibraryTargetWithExtensionSymbols") + ) + + result.assertExitStatusEquals(0) + XCTAssertEqual(result.referencedDocCArchives.count, 1) + + let doccArchiveURL = try XCTUnwrap(result.referencedDocCArchives.first) + + let dataDirectoryContents = try filesIn(.dataSubdirectory, of: doccArchiveURL) + + XCTAssertEqual( + Set(dataDirectoryContents.map(\.lastTwoPathComponents)), + [ + "documentation/library.json", + "library/swift.json", + + "swift/int.json", + "int/isarray.json", + "int/asfoo.json", + "int/customfooconvertible-implementations.json", + + "swift/array.json", + "array/isarray.json", + + "library/foo.json", + "foo/foo().json", + + "library/customfooconvertible.json", + "customfooconvertible/asfoo.json", + ] + ) + } +} diff --git a/Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift b/Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift index 24c59d1..154b8c2 100644 --- a/Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift +++ b/Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift @@ -38,12 +38,27 @@ extension PackageManager { for target: SwiftSourceModuleTarget, context: PluginContext, verbose: Bool, - snippetExtractor: SnippetExtractor? + snippetExtractor: SnippetExtractor?, + customSymbolGraphOptions: [PluginFlag] ) throws -> DocCSymbolGraphResult { // First generate the primary symbol graphs containing information about the // symbols defined in the target itself. - let symbolGraphOptions = target.defaultSymbolGraphOptions(in: context.package) + var symbolGraphOptions = target.defaultSymbolGraphOptions(in: context.package) + + // Modify the symbol graph options with the custom ones + for customSymbolGraphOption in customSymbolGraphOptions { + switch customSymbolGraphOption { + case .extendedTypes: +#if swift(>=5.8) + symbolGraphOptions.emitExtensionBlocks = true +#else + print("warning: detected '--include-extended-types' option, which is incompatible with your swift version (required: 5.8)") +#endif + default: + fatalError("error: unknown PluginFlag (\(customSymbolGraphOption.parsedValues.joined(separator: ", "))) detected in symbol graph generation - please create an issue at https://github.com/apple/swift-docc-plugin") + } + } if verbose { print("symbol graph options: '\(symbolGraphOptions)'") diff --git a/Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift b/Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift index 724c917..9b00cbe 100644 --- a/Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift +++ b/Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift @@ -27,7 +27,21 @@ extension SwiftSourceModuleTarget { return PackageManager.SymbolGraphOptions( minimumAccessLevel: targetMinimumAccessLevel, includeSynthesized: true, - includeSPI: false + includeSPI: false, + emitExtensionBlocks: false ) } } + + +#if swift(<5.8) +private extension PackageManager.SymbolGraphOptions { + /// A compatibility layer for lower Swift versions which discards unknown parameters. + init(minimumAccessLevel: PackagePlugin.PackageManager.SymbolGraphOptions.AccessLevel = .public, + includeSynthesized: Bool = false, + includeSPI: Bool = false, + emitExtensionBlocks: Bool) { + self.init(minimumAccessLevel: minimumAccessLevel, includeSynthesized: includeSynthesized, includeSPI: includeSPI) + } +} +#endif diff --git a/Plugins/Swift-DocC Convert/SwiftDocCConvert.swift b/Plugins/Swift-DocC Convert/SwiftDocCConvert.swift index 2d5178f..bb91b71 100644 --- a/Plugins/Swift-DocC Convert/SwiftDocCConvert.swift +++ b/Plugins/Swift-DocC Convert/SwiftDocCConvert.swift @@ -70,7 +70,8 @@ import PackagePlugin for: target, context: context, verbose: verbose, - snippetExtractor: snippetExtractor + snippetExtractor: snippetExtractor, + customSymbolGraphOptions: parsedArguments.symbolGraphArguments ) if try FileManager.default.contentsOfDirectory(atPath: symbolGraphs.targetSymbolGraphsDirectory.path).isEmpty { diff --git a/Plugins/Swift-DocC Preview/SwiftDocCPreview.swift b/Plugins/Swift-DocC Preview/SwiftDocCPreview.swift index 454f0ea..9bb82e6 100644 --- a/Plugins/Swift-DocC Preview/SwiftDocCPreview.swift +++ b/Plugins/Swift-DocC Preview/SwiftDocCPreview.swift @@ -83,7 +83,8 @@ import PackagePlugin for: target, context: context, verbose: verbose, - snippetExtractor: snippetExtractor + snippetExtractor: snippetExtractor, + customSymbolGraphOptions: parsedArguments.symbolGraphArguments ) if try FileManager.default.contentsOfDirectory(atPath: symbolGraphs.targetSymbolGraphsDirectory.path).isEmpty { diff --git a/Sources/SwiftDocCPluginUtilities/HelpInformation.swift b/Sources/SwiftDocCPluginUtilities/HelpInformation.swift index 7c1ec62..77fb234 100644 --- a/Sources/SwiftDocCPluginUtilities/HelpInformation.swift +++ b/Sources/SwiftDocCPluginUtilities/HelpInformation.swift @@ -43,10 +43,17 @@ public enum HelpInformation { helpText = previewPluginHelpOverview } - let supportedPluginFlags = [ + var supportedPluginFlags = [ PluginFlag.disableIndex, ] + // stops 'not mutated' warning for Swift 5.7 and lower + supportedPluginFlags += [] + +#if swift(>=5.8) + supportedPluginFlags += [PluginFlag.extendedTypes] +#endif + for flag in supportedPluginFlags { helpText += """ \(flag.parsedValues.sorted().joined(separator: ", ")) diff --git a/Sources/SwiftDocCPluginUtilities/ParsedArguments.swift b/Sources/SwiftDocCPluginUtilities/ParsedArguments.swift index fcbbc1b..d3e7063 100644 --- a/Sources/SwiftDocCPluginUtilities/ParsedArguments.swift +++ b/Sources/SwiftDocCPluginUtilities/ParsedArguments.swift @@ -21,8 +21,8 @@ public struct ParsedArguments { /// Returns the arguments that should be passed to `docc` to invoke the given plugin action. /// - /// Merges the arguments provided upon initialization of the parsed arguments - /// with default fallback values for required options that were not provided. + /// Merges the arguments provided upon initialization of the parsed arguments that are relevant + /// to `docc` with default fallback values for required options that were not provided. /// /// For example, if ParsedArguments is initialized like so: /// @@ -90,7 +90,7 @@ public struct ParsedArguments { symbolGraphDirectoryPath: String, outputPath: String ) -> Arguments { - var doccArguments = arguments + var doccArguments = arguments.filter(for: .docc) // Iterate through the flags required for the `docc` invocation // and append any that are not already present. @@ -153,8 +153,19 @@ public struct ParsedArguments { /// Creates a new set of parsed arguments with the given arguments. public init(_ arguments: [String]) { self.arguments = arguments + + let symbolGraphArguments = arguments.filter(for: .dumpSymbolGraph) + + self.symbolGraphArguments = ParsedArguments.ArgumentConsumer.dumpSymbolGraph.flags.filter { option in + option.parsedValues.contains(where: symbolGraphArguments.contains) + } } + // Build array with plugin flags that modify the symbol graph generation, + // filtering from the available custom symbol graph options those + // that correspond to the received flags + var symbolGraphArguments: [PluginFlag] + /// The command-line options required by the `docc` tool. private static let requiredOptions: [CommandLineOption] = [ .fallbackDisplayName, @@ -170,6 +181,59 @@ public struct ParsedArguments { ] private static let argumentsTransformers: [ArgumentsTransforming] = [ - PluginFlag.disableIndex + PluginFlag.disableIndex, + PluginFlag.extendedTypes ] } + +private extension ParsedArguments { + enum ArgumentConsumer: CaseIterable { + /// The `docc` command + case docc + /// The `swift package dump-symbol-graph` command + case dumpSymbolGraph + + /// Returns the flags applicable to an `ArgumentConsumer`. + /// + /// If `flags.isEmpty` is `true`, this `ArgumentConsumer` is assumed to + /// consume all flags not consumed by any of the other `ArgumentConsumer`s. + var flags: [PluginFlag] { + switch self { + case .dumpSymbolGraph: + return [ + PluginFlag.extendedTypes + ] + case .docc: + return [] + } + } + } +} + +private extension Arguments { + /// Returns the subset of arguments which are applicable to the given `consumer`. + func filter(for consumer: ParsedArguments.ArgumentConsumer) -> Arguments { + if !consumer.flags.isEmpty { + // If the consumer can provide a complete list of valid flags, + // we only include elements that are included in one of these flags' + // `parsedValues`, i.e. if one of these flags can be applied to the + // element. + let flagsToInclude = consumer.flags + return self.filter { argument in + flagsToInclude.contains(where: { flag in + flag.parsedValues.contains(argument) + }) + } + } else { + // If the consumer cannot provide a complete list of valid flags, (which + // should only happen for the `.docc` case) we return all elements + // that are not applicable to any of the other `ArgumentConsumer`s. + let flagsToExclude = ParsedArguments.ArgumentConsumer.allCases.flatMap(\.flags) + return self.filter { argument in + !flagsToExclude.contains(where: { flag in + flag.parsedValues.contains(argument) + }) + } + } + } +} diff --git a/Sources/SwiftDocCPluginUtilities/PluginFlags/ExtendedTypesFlag.swift b/Sources/SwiftDocCPluginUtilities/PluginFlags/ExtendedTypesFlag.swift new file mode 100644 index 0000000..d22c623 --- /dev/null +++ b/Sources/SwiftDocCPluginUtilities/PluginFlags/ExtendedTypesFlag.swift @@ -0,0 +1,29 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors + +extension PluginFlag { + /// Include extended types in documentation archives. + /// + /// Enables the extension block symbol format when calling the + /// dump symbol graph API. + /// + /// - Note: This flag is only available starting from Swift 5.8. It should + /// be hidden from the `--help` command for lower toolchain versions. + /// 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. + static let extendedTypes = PluginFlag( + parsedValues: [ + "--include-extended-types", + ], + abstract: "Include extended types from other modules in the produced DocC archive.", + description: """ + Allows documenting symbols that a target adds to its dependencies. + """, + argumentTransformation: { $0 } + ) +} diff --git a/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag+Equatable.swift b/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag+Equatable.swift new file mode 100644 index 0000000..214c336 --- /dev/null +++ b/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag+Equatable.swift @@ -0,0 +1,13 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors + +extension PluginFlag: Equatable { + static func ==(lhs: PluginFlag, rhs: PluginFlag) -> Bool { + return lhs.parsedValues == rhs.parsedValues + } +} diff --git a/Tests/SwiftDocCPluginUtilitiesTests/HelpInformationTests.swift b/Tests/SwiftDocCPluginUtilitiesTests/HelpInformationTests.swift index 17dbfb2..2175a25 100644 --- a/Tests/SwiftDocCPluginUtilitiesTests/HelpInformationTests.swift +++ b/Tests/SwiftDocCPluginUtilitiesTests/HelpInformationTests.swift @@ -39,7 +39,7 @@ final class HelpInformationTests: XCTestCase { --product Generate documentation for the specified product. --disable-indexing, --no-indexing Disable indexing for the produced DocC archive. - Produces a DocC archive that is best-suited for hosting online but incompatible with Xcode. + Produces a DocC archive that is best-suited for hosting online but incompatible with Xcode.\(includeExtendedTypesSection) DOCC OPTIONS: --platform Set the current release version of a platform. @@ -128,7 +128,7 @@ final class HelpInformationTests: XCTestCase { --product Preview documentation for the specified product. --disable-indexing, --no-indexing Disable indexing for the produced DocC archive. - Produces a DocC archive that is best-suited for hosting online but incompatible with Xcode. + Produces a DocC archive that is best-suited for hosting online but incompatible with Xcode.\(includeExtendedTypesSection) DOCC OPTIONS: --platform Set the current release version of a platform. @@ -184,3 +184,13 @@ final class HelpInformationTests: XCTestCase { } } +#if swift(>=5.8) +private let includeExtendedTypesSection = """ + + --include-extended-types + Include extended types from other modules in the produced DocC archive. + Allows documenting symbols that a target adds to its dependencies. +""" +#else +private let includeExtendedTypesSection = "" +#endif diff --git a/Tests/SwiftDocCPluginUtilitiesTests/ParsedArgumentsTests.swift b/Tests/SwiftDocCPluginUtilitiesTests/ParsedArgumentsTests.swift index ccc9146..e476a19 100644 --- a/Tests/SwiftDocCPluginUtilitiesTests/ParsedArgumentsTests.swift +++ b/Tests/SwiftDocCPluginUtilitiesTests/ParsedArgumentsTests.swift @@ -442,4 +442,32 @@ final class ParsedArgumentsTests: XCTestCase { ] ) } + + func testDocCArgumentsWithDumpSymbolGraphArguments() { + let dumpSymbolGraphArguments = ParsedArguments(["--include-extended-types"]) + + let doccArguments = dumpSymbolGraphArguments.doccArguments( + action: .convert, + targetKind: .executable, + doccCatalogPath: "/my/catalog.docc", + targetName: "MyTarget", + symbolGraphDirectoryPath: "/my/symbol-graph", + outputPath: "/my/output-path" + ) + + XCTAssertFalse(doccArguments.contains("--include-extended-types")) + } + + func testDumpSymbolGraphArguments() { + let dumpSymbolGraphArguments = ParsedArguments(["--include-extended-types"]) + + XCTAssertEqual(dumpSymbolGraphArguments.symbolGraphArguments, [.extendedTypes]) + } + + func testDumpSymbolGraphArgumentsWithDocCArguments() { + let dumpSymbolGraphArguments = ParsedArguments(["--fallback-default-module-kind", "Executable"]) + + + XCTAssertEqual(dumpSymbolGraphArguments.symbolGraphArguments, []) + } } diff --git a/Tests/SwiftDocCPluginUtilitiesTests/PluginFlags/ExtendedTypesFlagTests.swift b/Tests/SwiftDocCPluginUtilitiesTests/PluginFlags/ExtendedTypesFlagTests.swift new file mode 100644 index 0000000..c45be20 --- /dev/null +++ b/Tests/SwiftDocCPluginUtilitiesTests/PluginFlags/ExtendedTypesFlagTests.swift @@ -0,0 +1,31 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors + +import Foundation +@testable import SwiftDocCPluginUtilities +import XCTest + +final class ExtendedTypesFlagTests: XCTestCase { + func testReplacesIncludeExtendedTypesFlagWhenPresent() { + XCTAssertEqual( + PluginFlag.extendedTypes.transform( + ["--include-extended-types", "--other-flag"] + ), + ["--other-flag"] + ) + } + + func testDoesNotAddEmitExtensionBlockSymbolsFlagWhenAbsent() { + XCTAssertEqual( + PluginFlag.extendedTypes.transform( + ["--other-flag"] + ), + ["--other-flag"] + ) + } +} diff --git a/bin/check-source b/bin/check-source index 5d1dc64..5205f28 100755 --- a/bin/check-source +++ b/bin/check-source @@ -18,7 +18,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function replace_acceptable_years() { # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/20[12][789012]-20[12][89012]/YEARS/' -e 's/20[12][89012]/YEARS/' + sed -e 's/20[12][7890123]-20[12][890123]/YEARS/' -e 's/20[12][890123]/YEARS/' } printf "=> Checking for unacceptable languageā€¦ "