Skip to content

Commit 1f13ab5

Browse files
authored
Add --include-extended-types flag (#34)
The --include-extended-types flag is transformed to the --emit-extension-block-symbols flag of the swift package dump-symbol-graph command. It is available starting from Swift 5.8.
1 parent 1e386e0 commit 1f13ab5

File tree

16 files changed

+410
-13
lines changed

16 files changed

+410
-13
lines changed

IntegrationTests/Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ let package = Package(
3030
.copy("Fixtures/MixedTargets"),
3131
.copy("Fixtures/TargetWithDocCCatalog"),
3232
.copy("Fixtures/PackageWithSnippets"),
33+
.copy("Fixtures/LibraryTargetWithExtensionSymbols"),
3334
]
3435
),
3536
]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
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 testGenerateDocumentationWithoutEnablementFlag() throws {
29+
let result = try swiftPackage(
30+
"generate-documentation",
31+
workingDirectory: try setupTemporaryDirectoryForFixture(named: "LibraryTargetWithExtensionSymbols")
32+
)
33+
34+
result.assertExitStatusEquals(0)
35+
XCTAssertEqual(result.referencedDocCArchives.count, 1)
36+
37+
let doccArchiveURL = try XCTUnwrap(result.referencedDocCArchives.first)
38+
39+
let dataDirectoryContents = try filesIn(.dataSubdirectory, of: doccArchiveURL)
40+
41+
XCTAssertEqual(
42+
Set(dataDirectoryContents.map(\.lastTwoPathComponents)),
43+
[
44+
"documentation/library.json",
45+
46+
"library/foo.json",
47+
"foo/foo().json",
48+
49+
"library/customfooconvertible.json",
50+
"customfooconvertible/asfoo.json",
51+
]
52+
)
53+
}
54+
55+
func testGenerateDocumentationWithEnablementFlag() throws {
56+
let result = try swiftPackage(
57+
"generate-documentation",
58+
"--include-extended-types",
59+
workingDirectory: try setupTemporaryDirectoryForFixture(named: "LibraryTargetWithExtensionSymbols")
60+
)
61+
62+
result.assertExitStatusEquals(0)
63+
XCTAssertEqual(result.referencedDocCArchives.count, 1)
64+
65+
let doccArchiveURL = try XCTUnwrap(result.referencedDocCArchives.first)
66+
67+
let dataDirectoryContents = try filesIn(.dataSubdirectory, of: doccArchiveURL)
68+
69+
XCTAssertEqual(
70+
Set(dataDirectoryContents.map(\.lastTwoPathComponents)),
71+
[
72+
"documentation/library.json",
73+
"library/swift.json",
74+
75+
"swift/int.json",
76+
"int/isarray.json",
77+
"int/asfoo.json",
78+
"int/customfooconvertible-implementations.json",
79+
80+
"swift/array.json",
81+
"array/isarray.json",
82+
83+
"library/foo.json",
84+
"foo/foo().json",
85+
86+
"library/customfooconvertible.json",
87+
"customfooconvertible/asfoo.json",
88+
]
89+
)
90+
}
91+
}

Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,27 @@ extension PackageManager {
3838
for target: SwiftSourceModuleTarget,
3939
context: PluginContext,
4040
verbose: Bool,
41-
snippetExtractor: SnippetExtractor?
41+
snippetExtractor: SnippetExtractor?,
42+
customSymbolGraphOptions: [PluginFlag]
4243
) throws -> DocCSymbolGraphResult {
4344
// First generate the primary symbol graphs containing information about the
4445
// symbols defined in the target itself.
4546

46-
let symbolGraphOptions = target.defaultSymbolGraphOptions(in: context.package)
47+
var symbolGraphOptions = target.defaultSymbolGraphOptions(in: context.package)
48+
49+
// Modify the symbol graph options with the custom ones
50+
for customSymbolGraphOption in customSymbolGraphOptions {
51+
switch customSymbolGraphOption {
52+
case .extendedTypes:
53+
#if swift(>=5.8)
54+
symbolGraphOptions.emitExtensionBlocks = true
55+
#else
56+
print("warning: detected '--include-extended-types' option, which is incompatible with your swift version (required: 5.8)")
57+
#endif
58+
default:
59+
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")
60+
}
61+
}
4762

4863
if verbose {
4964
print("symbol graph options: '\(symbolGraphOptions)'")

Plugins/SharedPackagePluginExtensions/Target+defaultSymbolGraphOptions.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,21 @@ extension SwiftSourceModuleTarget {
2727
return PackageManager.SymbolGraphOptions(
2828
minimumAccessLevel: targetMinimumAccessLevel,
2929
includeSynthesized: true,
30-
includeSPI: false
30+
includeSPI: false,
31+
emitExtensionBlocks: false
3132
)
3233
}
3334
}
35+
36+
37+
#if swift(<5.8)
38+
private extension PackageManager.SymbolGraphOptions {
39+
/// A compatibility layer for lower Swift versions which discards unknown parameters.
40+
init(minimumAccessLevel: PackagePlugin.PackageManager.SymbolGraphOptions.AccessLevel = .public,
41+
includeSynthesized: Bool = false,
42+
includeSPI: Bool = false,
43+
emitExtensionBlocks: Bool) {
44+
self.init(minimumAccessLevel: minimumAccessLevel, includeSynthesized: includeSynthesized, includeSPI: includeSPI)
45+
}
46+
}
47+
#endif

Plugins/Swift-DocC Convert/SwiftDocCConvert.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ import PackagePlugin
7070
for: target,
7171
context: context,
7272
verbose: verbose,
73-
snippetExtractor: snippetExtractor
73+
snippetExtractor: snippetExtractor,
74+
customSymbolGraphOptions: parsedArguments.symbolGraphArguments
7475
)
7576

7677
if try FileManager.default.contentsOfDirectory(atPath: symbolGraphs.targetSymbolGraphsDirectory.path).isEmpty {

Plugins/Swift-DocC Preview/SwiftDocCPreview.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ import PackagePlugin
8383
for: target,
8484
context: context,
8585
verbose: verbose,
86-
snippetExtractor: snippetExtractor
86+
snippetExtractor: snippetExtractor,
87+
customSymbolGraphOptions: parsedArguments.symbolGraphArguments
8788
)
8889

8990
if try FileManager.default.contentsOfDirectory(atPath: symbolGraphs.targetSymbolGraphsDirectory.path).isEmpty {

Sources/SwiftDocCPluginUtilities/HelpInformation.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,17 @@ public enum HelpInformation {
4343
helpText = previewPluginHelpOverview
4444
}
4545

46-
let supportedPluginFlags = [
46+
var supportedPluginFlags = [
4747
PluginFlag.disableIndex,
4848
]
4949

50+
// stops 'not mutated' warning for Swift 5.7 and lower
51+
supportedPluginFlags += []
52+
53+
#if swift(>=5.8)
54+
supportedPluginFlags += [PluginFlag.extendedTypes]
55+
#endif
56+
5057
for flag in supportedPluginFlags {
5158
helpText += """
5259
\(flag.parsedValues.sorted().joined(separator: ", "))

Sources/SwiftDocCPluginUtilities/ParsedArguments.swift

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public struct ParsedArguments {
2121

2222
/// Returns the arguments that should be passed to `docc` to invoke the given plugin action.
2323
///
24-
/// Merges the arguments provided upon initialization of the parsed arguments
25-
/// with default fallback values for required options that were not provided.
24+
/// Merges the arguments provided upon initialization of the parsed arguments that are relevant
25+
/// to `docc` with default fallback values for required options that were not provided.
2626
///
2727
/// For example, if ParsedArguments is initialized like so:
2828
///
@@ -90,7 +90,7 @@ public struct ParsedArguments {
9090
symbolGraphDirectoryPath: String,
9191
outputPath: String
9292
) -> Arguments {
93-
var doccArguments = arguments
93+
var doccArguments = arguments.filter(for: .docc)
9494

9595
// Iterate through the flags required for the `docc` invocation
9696
// and append any that are not already present.
@@ -153,8 +153,19 @@ public struct ParsedArguments {
153153
/// Creates a new set of parsed arguments with the given arguments.
154154
public init(_ arguments: [String]) {
155155
self.arguments = arguments
156+
157+
let symbolGraphArguments = arguments.filter(for: .dumpSymbolGraph)
158+
159+
self.symbolGraphArguments = ParsedArguments.ArgumentConsumer.dumpSymbolGraph.flags.filter { option in
160+
option.parsedValues.contains(where: symbolGraphArguments.contains)
161+
}
156162
}
157163

164+
// Build array with plugin flags that modify the symbol graph generation,
165+
// filtering from the available custom symbol graph options those
166+
// that correspond to the received flags
167+
var symbolGraphArguments: [PluginFlag]
168+
158169
/// The command-line options required by the `docc` tool.
159170
private static let requiredOptions: [CommandLineOption] = [
160171
.fallbackDisplayName,
@@ -170,6 +181,59 @@ public struct ParsedArguments {
170181
]
171182

172183
private static let argumentsTransformers: [ArgumentsTransforming] = [
173-
PluginFlag.disableIndex
184+
PluginFlag.disableIndex,
185+
PluginFlag.extendedTypes
174186
]
175187
}
188+
189+
private extension ParsedArguments {
190+
enum ArgumentConsumer: CaseIterable {
191+
/// The `docc` command
192+
case docc
193+
/// The `swift package dump-symbol-graph` command
194+
case dumpSymbolGraph
195+
196+
/// Returns the flags applicable to an `ArgumentConsumer`.
197+
///
198+
/// If `flags.isEmpty` is `true`, this `ArgumentConsumer` is assumed to
199+
/// consume all flags not consumed by any of the other `ArgumentConsumer`s.
200+
var flags: [PluginFlag] {
201+
switch self {
202+
case .dumpSymbolGraph:
203+
return [
204+
PluginFlag.extendedTypes
205+
]
206+
case .docc:
207+
return []
208+
}
209+
}
210+
}
211+
}
212+
213+
private extension Arguments {
214+
/// Returns the subset of arguments which are applicable to the given `consumer`.
215+
func filter(for consumer: ParsedArguments.ArgumentConsumer) -> Arguments {
216+
if !consumer.flags.isEmpty {
217+
// If the consumer can provide a complete list of valid flags,
218+
// we only include elements that are included in one of these flags'
219+
// `parsedValues`, i.e. if one of these flags can be applied to the
220+
// element.
221+
let flagsToInclude = consumer.flags
222+
return self.filter { argument in
223+
flagsToInclude.contains(where: { flag in
224+
flag.parsedValues.contains(argument)
225+
})
226+
}
227+
} else {
228+
// If the consumer cannot provide a complete list of valid flags, (which
229+
// should only happen for the `.docc` case) we return all elements
230+
// that are not applicable to any of the other `ArgumentConsumer`s.
231+
let flagsToExclude = ParsedArguments.ArgumentConsumer.allCases.flatMap(\.flags)
232+
return self.filter { argument in
233+
!flagsToExclude.contains(where: { flag in
234+
flag.parsedValues.contains(argument)
235+
})
236+
}
237+
}
238+
}
239+
}

0 commit comments

Comments
 (0)