diff --git a/IntegrationTests/Tests/TargetWithSwiftExtensionsTests.swift b/IntegrationTests/Tests/TargetWithSwiftExtensionsTests.swift index 9993b75..7819183 100644 --- a/IntegrationTests/Tests/TargetWithSwiftExtensionsTests.swift +++ b/IntegrationTests/Tests/TargetWithSwiftExtensionsTests.swift @@ -25,31 +25,31 @@ final class TargetWithSwiftExtensionsTests: ConcurrencyRequiringTestCase { try super.setUpWithError() } - func testGenerateDocumentationWithoutEnablementFlag() throws { + func testGenerateDocumentationWithoutExtendedTypesFlag() throws { let result = try swiftPackage( "generate-documentation", workingDirectory: try setupTemporaryDirectoryForFixture(named: "LibraryTargetWithExtensionSymbols") ) - result.assertExitStatusEquals(0) - XCTAssertEqual(result.referencedDocCArchives.count, 1) + let dataDirectoryContents = try unwrapDataDirectoryContents(of: result) - let doccArchiveURL = try XCTUnwrap(result.referencedDocCArchives.first) + #if swift(>=5.9) + try assertDirectoryContentsWithExtendedTypes(dataDirectoryContents) + #else + try assertDirectoryContentsWithoutExtendedTypes(dataDirectoryContents) + #endif + } + + func testGenerateDocumentationWithDisablementFlag() throws { + let result = try swiftPackage( + "generate-documentation", + "--exclude-extended-types", + workingDirectory: try setupTemporaryDirectoryForFixture(named: "LibraryTargetWithExtensionSymbols") + ) - let dataDirectoryContents = try filesIn(.dataSubdirectory, of: doccArchiveURL) + let dataDirectoryContents = try unwrapDataDirectoryContents(of: result) - XCTAssertEqual( - Set(dataDirectoryContents.map(\.lastTwoPathComponents)), - [ - "documentation/library.json", - - "library/foo.json", - "foo/foo().json", - - "library/customfooconvertible.json", - "customfooconvertible/asfoo.json", - ] - ) + try assertDirectoryContentsWithoutExtendedTypes(dataDirectoryContents) } func testGenerateDocumentationWithEnablementFlag() throws { @@ -59,15 +59,17 @@ final class TargetWithSwiftExtensionsTests: ConcurrencyRequiringTestCase { 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) + let dataDirectoryContents = try unwrapDataDirectoryContents(of: result) + try assertDirectoryContentsWithExtendedTypes(dataDirectoryContents) + } + + func assertDirectoryContentsWithExtendedTypes(_ contents: [URL], + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) throws { XCTAssertEqual( - Set(dataDirectoryContents.map(\.lastTwoPathComponents)), + Set(contents.map(\.lastTwoPathComponents)), [ "documentation/library.json", "library/swift.json", @@ -85,7 +87,43 @@ final class TargetWithSwiftExtensionsTests: ConcurrencyRequiringTestCase { "library/customfooconvertible.json", "customfooconvertible/asfoo.json", - ] + ], + message(), + file: file, + line: line ) } + + func assertDirectoryContentsWithoutExtendedTypes(_ contents: [URL], + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) throws { + XCTAssertEqual( + Set(contents.map(\.lastTwoPathComponents)), + [ + "documentation/library.json", + + "library/foo.json", + "foo/foo().json", + + "library/customfooconvertible.json", + "customfooconvertible/asfoo.json", + ], + message(), + file: file, + line: line + ) + } + + func unwrapDataDirectoryContents(of result: SwiftInvocationResult, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) throws -> [URL] { + result.assertExitStatusEquals(0) + XCTAssertEqual(result.referencedDocCArchives.count, 1, message(), file: file, line: line) + + let doccArchiveURL = try XCTUnwrap(result.referencedDocCArchives.first, message(), file: file, line: line) + + return try filesIn(.dataSubdirectory, of: doccArchiveURL) + } } diff --git a/Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift b/Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift index c5ff17a..4578457 100644 --- a/Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift +++ b/Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift @@ -49,11 +49,17 @@ extension PackageManager { // Modify the symbol graph options with the custom ones for customSymbolGraphOption in customSymbolGraphOptions { switch customSymbolGraphOption { - case .extendedTypes: + case .extendedTypes.positive: #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 + case .extendedTypes.negative: +#if swift(>=5.8) + symbolGraphOptions.emitExtensionBlocks = false +#else + print("warning: detected '--exclude-extended-types' option, which is incompatible with your swift version (required: 5.8)") #endif case .skipSynthesizedSymbols: symbolGraphOptions.includeSynthesized = false diff --git a/Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift b/Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift index 9b00cbe..c72e74b 100644 --- a/Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift +++ b/Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift @@ -24,11 +24,17 @@ extension SwiftSourceModuleTarget { targetMinimumAccessLevel = .public } +#if swift(>=5.9) + let emitExtensionBlockSymbolDefault = true +#else + let emitExtensionBlockSymbolDefault = false +#endif + return PackageManager.SymbolGraphOptions( minimumAccessLevel: targetMinimumAccessLevel, includeSynthesized: true, includeSPI: false, - emitExtensionBlocks: false + emitExtensionBlocks: emitExtensionBlockSymbolDefault ) } } diff --git a/Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Generating Documentation for Extended Types.md b/Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Generating Documentation for Extended Types.md index 173e811..f190e49 100644 --- a/Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Generating Documentation for Extended Types.md +++ b/Sources/SwiftDocCPluginDocumentation/SwiftDocCPlugin.docc/Generating Documentation for Extended Types.md @@ -4,24 +4,26 @@ Generate documentation for the extensions you make to types from other modules. ## Overview -By default, the Swift-DocC plugin ignores extensions you make to types that are not from the module you're generating documentation for. +The Swift-DocC plugin allows you to document extensions you make to types that are not from the module you're generating documentation for. -To include documentation for extended types, add the `--include-extended-types` flag to your invocations: +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: $ swift package generate-documentation --include-extended-types -> Note: Extension support is available when using Swift 5.8 or later and the Swift-DocC plugin 1.2 or later. + $ swift package generate-documentation --exclude-extended-types -## Understanding What is Included by Default +> 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. -Not everything that is declared in an extension is hidden behind the `--include-extended-types` flag. If the extension is declared in the same target as the type it is extending, the extension's contents will be included in the documentation by default. +## Understanding What is an Extended Type + +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. ```swift public struct Sloth { } extension Sloth { - // This function is included in the - // documentation by default. + // This method is always included + // in the documentation. public func wake() { /* ... */ } } @@ -29,8 +31,9 @@ extension Sloth { // not the `SlothCreator` library, so this is // what we call an "extended type". extension Collection where Element == Sloth { - // This property is not included in - // the documentation by default. + // This property is only included in + // the documentation if extension + // support is enabled. public func wake() { for sloth in self { sloth.wake() diff --git a/Sources/SwiftDocCPluginUtilities/HelpInformation.swift b/Sources/SwiftDocCPluginUtilities/HelpInformation.swift index 59d36ed..4cfdc48 100644 --- a/Sources/SwiftDocCPluginUtilities/HelpInformation.swift +++ b/Sources/SwiftDocCPluginUtilities/HelpInformation.swift @@ -56,8 +56,13 @@ public enum HelpInformation { #endif for flag in supportedPluginFlags { + var flagListText = flag.positive.parsedValues.sorted().joined(separator: ", ") + if !flag.negative.parsedValues.isEmpty { + flagListText += " / \(flag.negative.parsedValues.sorted().joined(separator: ", "))" + } + helpText += """ - \(flag.parsedValues.sorted().joined(separator: ", ")) + \(flagListText) \(flag.abstract) \(flag.description) diff --git a/Sources/SwiftDocCPluginUtilities/ParsedArguments.swift b/Sources/SwiftDocCPluginUtilities/ParsedArguments.swift index 38931ad..e28d78d 100644 --- a/Sources/SwiftDocCPluginUtilities/ParsedArguments.swift +++ b/Sources/SwiftDocCPluginUtilities/ParsedArguments.swift @@ -156,8 +156,8 @@ public struct ParsedArguments { let symbolGraphArguments = arguments.filter(for: .dumpSymbolGraph) - self.symbolGraphArguments = ParsedArguments.ArgumentConsumer.dumpSymbolGraph.flags.filter { option in - option.parsedValues.contains(where: symbolGraphArguments.contains) + self.symbolGraphArguments = ParsedArguments.ArgumentConsumer.dumpSymbolGraph.flags.compactMap { + $0.value(for: symbolGraphArguments) } } diff --git a/Sources/SwiftDocCPluginUtilities/PluginFlags/ExtendedTypesFlag.swift b/Sources/SwiftDocCPluginUtilities/PluginFlags/ExtendedTypesFlag.swift index d22c623..9cb475e 100644 --- a/Sources/SwiftDocCPluginUtilities/PluginFlags/ExtendedTypesFlag.swift +++ b/Sources/SwiftDocCPluginUtilities/PluginFlags/ExtendedTypesFlag.swift @@ -7,9 +7,9 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors extension PluginFlag { - /// Include extended types in documentation archives. + /// Include or exclude extended types in documentation archives. /// - /// Enables the extension block symbol format when calling the + /// Enables/disables 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 @@ -17,13 +17,20 @@ extension PluginFlag { /// 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: [ + positiveValues: [ "--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. - """, + negativeValues: [ + "--exclude-extended-types", + ], + abstract: "Control whether extended types from other modules are shown in the produced DocC archive. (default: \(Self.default))", + description: "Allows documenting symbols that a target adds to its dependencies.", argumentTransformation: { $0 } ) + +#if swift(>=5.9) + private static let `default` = "--include-extended-types" +#else + private static let `default` = "--exclude-extended-types" +#endif } diff --git a/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag+Equatable.swift b/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag+Equatable.swift index 214c336..d880af5 100644 --- a/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag+Equatable.swift +++ b/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag+Equatable.swift @@ -8,6 +8,7 @@ extension PluginFlag: Equatable { static func ==(lhs: PluginFlag, rhs: PluginFlag) -> Bool { - return lhs.parsedValues == rhs.parsedValues + return lhs.positive.parsedValues == rhs.positive.parsedValues + && lhs.negative.parsedValues == rhs.negative.parsedValues } } diff --git a/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag.swift b/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag.swift index 6b45ee1..494f01a 100644 --- a/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag.swift +++ b/Sources/SwiftDocCPluginUtilities/PluginFlags/PluginFlag.swift @@ -13,7 +13,13 @@ struct PluginFlag: ArgumentsTransforming { /// The string values that will be parsed when detecting this flag. /// /// For example, this might be `["--disable-index"]`. - let parsedValues: Set + var parsedValues: Set { + positiveValues.union(negativeValues) + } + + private let positiveValues: Set + + private let negativeValues: Set /// A short, user-facing description of this flag. let abstract: String @@ -23,6 +29,41 @@ struct PluginFlag: ArgumentsTransforming { let argumentTransformation: (Arguments) -> Arguments + /// A version of this flag that only parses its positive values. + var positive: Self { + .init(positiveValues: positiveValues, + negativeValues: [], + abstract: abstract, + description: description, + argumentTransformation: argumentTransformation) + } + + /// A version of this flag that only parses its negative values. + var negative: Self { + .init(positiveValues: [], + negativeValues: negativeValues, + abstract: abstract, + description: description, + argumentTransformation: argumentTransformation) + } + + /// Returns either the ``positive`` or ``negative`` version + /// of this flag, depending on the last element in `arguments` that + /// triggers this flag. + func value(for arguments: Arguments) -> Self? { + guard let last = arguments + .filter({ parsedValues.contains($0) }) + .last else { + return nil + } + + if positiveValues.contains(last) { + return positive + } else { + return negative + } + } + /// Transforms the given set of arguments if they include any of this flag's /// parsed values. /// @@ -49,20 +90,44 @@ struct PluginFlag: ArgumentsTransforming { /// Create a new command-line flag. /// /// - Parameters: - /// - parsedValues: The string values that should be parsed to detect this flag. + /// - positiveValues: The string values that should be parsed to enable this flag. + /// - negativeValues: The string values that should be parsed to disable this flag. /// - abstract: The user-facing description of this flag. /// - description: An expanded, user-facing description of this flag. /// - argumentTransformation: A closure that can be applied to a given /// set of parsed arguments if the arguments include any of the this flag's parsed values. init( - parsedValues: Set, + positiveValues: Set, + negativeValues: Set, abstract: String, description: String, argumentTransformation: @escaping (Arguments) -> Arguments ) { - self.parsedValues = parsedValues + self.positiveValues = positiveValues + self.negativeValues = negativeValues self.abstract = abstract self.description = description self.argumentTransformation = argumentTransformation } + + /// Create a new command-line flag. + /// + /// - Parameters: + /// - parsedValues: The string values that should be parsed to detect this flag. + /// - abstract: The user-facing description of this flag. + /// - description: An expanded, user-facing description of this flag. + /// - argumentTransformation: A closure that can be applied to a given + /// set of parsed arguments if the arguments include any of the this flag's parsed values. + init( + parsedValues: Set, + abstract: String, + description: String, + argumentTransformation: @escaping (Arguments) -> Arguments + ) { + self.init(positiveValues: parsedValues, + negativeValues: [], + abstract: abstract, + description: description, + argumentTransformation: argumentTransformation) + } } diff --git a/Tests/SwiftDocCPluginUtilitiesTests/HelpInformationTests.swift b/Tests/SwiftDocCPluginUtilitiesTests/HelpInformationTests.swift index 58df174..5b88071 100644 --- a/Tests/SwiftDocCPluginUtilitiesTests/HelpInformationTests.swift +++ b/Tests/SwiftDocCPluginUtilitiesTests/HelpInformationTests.swift @@ -42,7 +42,7 @@ final class HelpInformationTests: XCTestCase { Produces a DocC archive that is best-suited for hosting online but incompatible with Xcode. --experimental-skip-synthesized-symbols Exclude synthesized symbols from the generated documentation - Experimental feature that produces a DocC archive without compiler synthesized symbols.\(includeExtendedTypesSection) + Experimental feature that produces a DocC archive without compiler synthesized symbols.\(extendedTypesSection) DOCC OPTIONS: --platform Set the current release version of a platform. @@ -134,7 +134,7 @@ final class HelpInformationTests: XCTestCase { Produces a DocC archive that is best-suited for hosting online but incompatible with Xcode. --experimental-skip-synthesized-symbols Exclude synthesized symbols from the generated documentation - Experimental feature that produces a DocC archive without compiler synthesized symbols.\(includeExtendedTypesSection) + Experimental feature that produces a DocC archive without compiler synthesized symbols.\(extendedTypesSection) DOCC OPTIONS: --platform Set the current release version of a platform. @@ -191,12 +191,18 @@ final class HelpInformationTests: XCTestCase { } #if swift(>=5.8) -private let includeExtendedTypesSection = """ +private let extendedTypesSection = """ - --include-extended-types - Include extended types from other modules in the produced DocC archive. + --include-extended-types / --exclude-extended-types + Control whether extended types from other modules are shown in the produced DocC archive. (default: \(extendedTypesDefault)) Allows documenting symbols that a target adds to its dependencies. """ #else -private let includeExtendedTypesSection = "" +private let extendedTypesSection = "" +#endif + +#if swift(>=5.9) +private let extendedTypesDefault = "--include-extended-types" +#else +private let extendedTypesDefault = "--exclude-extended-types" #endif diff --git a/Tests/SwiftDocCPluginUtilitiesTests/ParsedArgumentsTests.swift b/Tests/SwiftDocCPluginUtilitiesTests/ParsedArgumentsTests.swift index dcd7a96..cace8c3 100644 --- a/Tests/SwiftDocCPluginUtilitiesTests/ParsedArgumentsTests.swift +++ b/Tests/SwiftDocCPluginUtilitiesTests/ParsedArgumentsTests.swift @@ -460,9 +460,19 @@ final class ParsedArgumentsTests: XCTestCase { } func testDumpSymbolGraphArguments() { - let dumpSymbolGraphArguments = ParsedArguments(["--include-extended-types", "--experimental-skip-synthesized-symbols"]) + var dumpSymbolGraphArguments: ParsedArguments + + dumpSymbolGraphArguments = ParsedArguments(["--include-extended-types", "--experimental-skip-synthesized-symbols"]) + XCTAssertEqual(dumpSymbolGraphArguments.symbolGraphArguments, [.extendedTypes.positive, .skipSynthesizedSymbols]) + + dumpSymbolGraphArguments = ParsedArguments(["--exclude-extended-types", "--experimental-skip-synthesized-symbols"]) + XCTAssertEqual(dumpSymbolGraphArguments.symbolGraphArguments, [.extendedTypes.negative, .skipSynthesizedSymbols]) + + dumpSymbolGraphArguments = ParsedArguments(["--include-extended-types", "--experimental-skip-synthesized-symbols", "--exclude-extended-types"]) + XCTAssertEqual(dumpSymbolGraphArguments.symbolGraphArguments, [.extendedTypes.negative, .skipSynthesizedSymbols]) - XCTAssertEqual(dumpSymbolGraphArguments.symbolGraphArguments, [.extendedTypes, .skipSynthesizedSymbols]) + dumpSymbolGraphArguments = ParsedArguments(["--exclude-extended-types", "--include-extended-types"]) + XCTAssertEqual(dumpSymbolGraphArguments.symbolGraphArguments, [.extendedTypes.positive]) } func testDumpSymbolGraphArgumentsWithDocCArguments() { diff --git a/Tests/SwiftDocCPluginUtilitiesTests/PluginFlags/ExtendedTypesFlagTests.swift b/Tests/SwiftDocCPluginUtilitiesTests/PluginFlags/ExtendedTypesFlagTests.swift index c45be20..87a5092 100644 --- a/Tests/SwiftDocCPluginUtilitiesTests/PluginFlags/ExtendedTypesFlagTests.swift +++ b/Tests/SwiftDocCPluginUtilitiesTests/PluginFlags/ExtendedTypesFlagTests.swift @@ -20,6 +20,51 @@ final class ExtendedTypesFlagTests: XCTestCase { ) } + func testReplacesExcludeExtendedTypesFlagWhenPresent() { + XCTAssertEqual( + PluginFlag.extendedTypes.transform( + ["--exclude-extended-types", "--other-flag"] + ), + ["--other-flag"] + ) + } + + func testPositiveReplacesIncludeExtendedTypesFlag() { + XCTAssertEqual( + PluginFlag.extendedTypes.positive.transform( + ["--include-extended-types", "--other-flag"] + ), + ["--other-flag"] + ) + } + + func testNegativeReplacesExcludeExtendedTypesFlag() { + XCTAssertEqual( + PluginFlag.extendedTypes.negative.transform( + ["--exclude-extended-types", "--other-flag"] + ), + ["--other-flag"] + ) + } + + func testPositiveDoesNotReplaceExcludeExtendedTypesFlag() { + XCTAssertEqual( + PluginFlag.extendedTypes.positive.transform( + ["--exclude-extended-types", "--other-flag"] + ), + ["--exclude-extended-types", "--other-flag"] + ) + } + + func testNegativeDoesNotReplaceIncludeExtendedTypesFlag() { + XCTAssertEqual( + PluginFlag.extendedTypes.negative.transform( + ["--include-extended-types", "--other-flag"] + ), + ["--include-extended-types", "--other-flag"] + ) + } + func testDoesNotAddEmitExtensionBlockSymbolsFlagWhenAbsent() { XCTAssertEqual( PluginFlag.extendedTypes.transform(