Skip to content
This repository was archived by the owner on Jun 1, 2023. It is now read-only.

Commit b8a0cd9

Browse files
Lukas-Stuehrkmattt
andauthored
Build documentation for extensions on external types. (#230)
* Build documentation for external types. Implements #122. * Display extensions in definition list Remove unnecessary style rules for extensions * Fix false positives for external types. * Better check for external symbols. * Refactor isExternalSymbol to perform more general symbol resolution * Remove unnecessary parameter * Add resolution for nested types through typealiases Refactor implementation of ID * Use typealias resolution when creating relationships * Add tests for extensions on typealiases. * Add changelog entry for #230 Co-authored-by: Mattt <mattt@me.com>
1 parent 1b2baaf commit b8a0cd9

File tree

9 files changed

+318
-24
lines changed

9 files changed

+318
-24
lines changed

Changelog.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Added support for generating documentation for
13+
extensions to external types.
14+
#230 by @Lukas-Stuehrk and @mattt.
1215
- Added end-to-end tests for command-line interface.
1316
#199 by @MaxDesiatov and @mattt.
1417
- Added `--minimum-access-level` option to `generate` and `coverage` commands.
@@ -61,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6164
#159 by @mattt.
6265
- Fixed relationship diagram to prevent linking to unknown symbols.
6366
#178 by @MattKiazyk.
64-
- Fixed problems in CommonMark output related to escaping emoji shortcode.
67+
- Fixed problems in CommonMark output related to escaping emoji shortcode.
6568
#167 by @mattt.
6669

6770
### Changed

Sources/SwiftDoc/Identifier.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,41 @@
11
public struct Identifier: Hashable {
2-
public let pathComponents: [String]
2+
public let context: [String]
33
public let name: String
4+
public let pathComponents: [String]
5+
6+
public init(context: [String], name: String) {
7+
self.context = context
8+
self.name = name
9+
self.pathComponents = context + CollectionOfOne(name)
10+
}
411

512
public func matches(_ string: String) -> Bool {
6-
(pathComponents + CollectionOfOne(name)).reversed().starts(with: string.split(separator: ".").map { String($0) }.reversed())
13+
return matches(string.split(separator: "."))
14+
}
15+
16+
public func matches(_ pathComponents: [Substring]) -> Bool {
17+
return matches(pathComponents.map(String.init))
18+
}
19+
20+
public func matches(_ pathComponents: [String]) -> Bool {
21+
return self.pathComponents.ends(with: pathComponents)
722
}
823
}
924

1025
// MARK: - CustomStringConvertible
1126

1227
extension Identifier: CustomStringConvertible {
1328
public var description: String {
14-
(pathComponents + CollectionOfOne(name)).joined(separator: ".")
29+
pathComponents.joined(separator: ".")
30+
}
31+
}
32+
33+
fileprivate extension Array {
34+
func ends<PossibleSuffix>(with possibleSuffix: PossibleSuffix) -> Bool
35+
where PossibleSuffix : Sequence,
36+
Self.Element == PossibleSuffix.Element,
37+
Self.Element: Equatable
38+
{
39+
reversed().starts(with: possibleSuffix)
1540
}
1641
}

Sources/SwiftDoc/Interface.swift

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ public final class Interface {
1010
self.imports = imports
1111
self.symbols = symbols
1212

13-
self.symbolsGroupedByIdentifier = Dictionary(grouping: symbols, by: { $0.id })
14-
self.symbolsGroupedByQualifiedName = Dictionary(grouping: symbols, by: { $0.id.description })
15-
self.topLevelSymbols = symbols.filter { $0.api is Type || $0.id.pathComponents.isEmpty }
13+
let symbolsGroupedByIdentifier = Dictionary(grouping: symbols, by: { $0.id })
14+
let symbolsGroupedByQualifiedName = Dictionary(grouping: symbols, by: { $0.id.description })
15+
16+
self.symbolsGroupedByIdentifier = symbolsGroupedByIdentifier
17+
self.symbolsGroupedByQualifiedName = symbolsGroupedByQualifiedName
18+
self.topLevelSymbols = symbols.filter { $0.api is Type || $0.id.context.isEmpty }
1619

1720
self.relationships = {
1821
let extensionsByExtendedType: [String: [Extension]] = Dictionary(grouping: symbols.flatMap { $0.context.compactMap { $0 as? Extension } }, by: { $0.extendedType })
1922

2023
var relationships: Set<Relationship> = []
2124
for symbol in symbols {
22-
2325
let lastDeclarationScope = symbol.context.last(where: { $0 is Extension || $0 is Symbol })
2426

2527
if let container = lastDeclarationScope as? Symbol {
@@ -40,8 +42,7 @@ public final class Interface {
4042
}
4143

4244
if let `extension` = lastDeclarationScope as? Extension {
43-
if let extended = symbols.first(where: { $0.api is Type && $0.id.matches(`extension`.extendedType) }) {
44-
45+
for extended in symbolsGroupedByIdentifier.named(`extension`.extendedType, resolvingTypealiases: true) {
4546
let predicate: Relationship.Predicate
4647
switch extended.api {
4748
case is Protocol:
@@ -66,7 +67,7 @@ public final class Interface {
6667
inheritedTypeNames = Set(inheritedTypeNames.flatMap { $0.split(separator: "&").map { $0.trimmingCharacters(in: .whitespaces) } })
6768

6869
for name in inheritedTypeNames {
69-
let inheritedTypes = symbols.filter({ ($0.api is Class || $0.api is Protocol) && $0.id.description == name })
70+
let inheritedTypes = symbolsGroupedByIdentifier.named(name, resolvingTypealiases: true).filter({ ($0.api is Class || $0.api is Protocol) && $0.id.description == name })
7071
if inheritedTypes.isEmpty {
7172
let inherited = Symbol(api: Unknown(name: name), context: [], declaration: [], documentation: nil, sourceRange: nil)
7273
relationships.insert(Relationship(subject: symbol, predicate: .conformsTo, object: inherited))
@@ -115,7 +116,6 @@ public final class Interface {
115116
}
116117

117118
return classClusters
118-
119119
}
120120

121121
public let relationships: [Relationship]
@@ -159,4 +159,37 @@ public final class Interface {
159159
public func defaultImplementations(of symbol: Symbol) -> [Symbol] {
160160
return relationshipsByObject[symbol.id]?.filter { $0.predicate == .defaultImplementationOf }.map { $0.subject }.sorted() ?? []
161161
}
162+
163+
// MARK: -
164+
165+
public func symbols(named name: String, resolvingTypealiases: Bool) -> [Symbol] {
166+
symbolsGroupedByIdentifier.named(name, resolvingTypealiases: resolvingTypealiases)
167+
}
168+
}
169+
170+
fileprivate extension Dictionary where Key == Identifier, Value == [Symbol] {
171+
func named(_ name: String, resolvingTypealiases: Bool) -> [Symbol] {
172+
var pathComponents: [String] = []
173+
for component in name.split(separator: ".") {
174+
pathComponents.append("\(component)")
175+
guard resolvingTypealiases else { continue }
176+
177+
if let symbols = first(where: { $0.key.pathComponents == pathComponents })?.value,
178+
let symbol = symbols.first(where: { $0.api is Typealias }),
179+
let `typealias` = symbol.api as? Typealias,
180+
let initializedType = `typealias`.initializedType
181+
{
182+
let initializedTypePathComponents = initializedType.split(separator: ".")
183+
let candidates = keys.filter { $0.matches(initializedTypePathComponents) }
184+
185+
if let id = candidates.max(by: { $0.pathComponents.count > $1.pathComponents.count }) {
186+
pathComponents = id.pathComponents
187+
} else {
188+
return []
189+
}
190+
}
191+
}
192+
193+
return first(where: { $0.key.pathComponents == pathComponents })?.value ?? []
194+
}
162195
}

Sources/SwiftDoc/Symbol.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import struct Highlighter.Token
66
public final class Symbol {
77
public typealias ID = Identifier
88

9+
public let id: ID
910
public let api: API
1011
public let context: [Contextual]
1112
public let declaration: [Token]
@@ -19,6 +20,9 @@ public final class Symbol {
1920
public private(set) lazy var conditions: [CompilationCondition] = context.compactMap { $0 as? CompilationCondition }
2021

2122
init(api: API, context: [Contextual], declaration: [Token], documentation: Documentation?, sourceRange: SourceRange?) {
23+
self.id = Identifier(context: context.compactMap {
24+
($0 as? Symbol)?.name ?? ($0 as? Extension)?.extendedType
25+
}, name: api.name)
2226
self.api = api
2327
self.context = context
2428
self.declaration = declaration
@@ -30,12 +34,6 @@ public final class Symbol {
3034
return api.name
3135
}
3236

33-
public private(set) lazy var id: ID = {
34-
Identifier(pathComponents: context.compactMap {
35-
($0 as? Symbol)?.name ?? ($0 as? Extension)?.extendedType
36-
}, name: name)
37-
}()
38-
3937
public var isPublic: Bool {
4038
if api is Unknown {
4139
return true
@@ -329,3 +327,9 @@ extension Symbol: Codable {
329327
try container.encode(sourceRange, forKey: .sourceRange)
330328
}
331329
}
330+
331+
extension Symbol: CustomDebugStringConvertible {
332+
public var debugDescription: String {
333+
return "\(self.declaration.map { $0.text }.joined())"
334+
}
335+
}

Sources/swift-doc/Subcommands/Generate.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ extension SwiftDoc {
8484
}
8585
}
8686

87+
// Extensions on external types.
88+
var symbolsByExternalType: [String: [Symbol]] = [:]
89+
for symbol in module.interface.symbols.filter(symbolFilter) {
90+
guard let extensionDeclaration = symbol.context.first as? Extension, symbol.context.count == 1 else { continue }
91+
guard module.interface.symbols(named: extensionDeclaration.extendedType, resolvingTypealiases: true).isEmpty else { continue }
92+
symbolsByExternalType[extensionDeclaration.extendedType, default: []] += [symbol]
93+
}
94+
for (typeName, symbols) in symbolsByExternalType {
95+
pages[route(for: typeName)] = ExternalTypePage(module: module, externalType: typeName, symbols: symbols, baseURL: baseURL)
96+
}
97+
8798
for (name, symbols) in globals {
8899
pages[route(for: name)] = GlobalPage(module: module, name: name, symbols: symbols, baseURL: baseURL)
89100
}
@@ -110,11 +121,11 @@ extension SwiftDoc {
110121
} else {
111122
switch format {
112123
case .commonmark:
113-
pages["Home"] = HomePage(module: module, baseURL: baseURL, symbolFilter: symbolFilter)
114-
pages["_Sidebar"] = SidebarPage(module: module, baseURL: baseURL, symbolFilter: symbolFilter)
124+
pages["Home"] = HomePage(module: module, externalTypes: Array(symbolsByExternalType.keys), baseURL: baseURL, symbolFilter: symbolFilter)
125+
pages["_Sidebar"] = SidebarPage(module: module, externalTypes: Set(symbolsByExternalType.keys), baseURL: baseURL, symbolFilter: symbolFilter)
115126
pages["_Footer"] = FooterPage(baseURL: baseURL)
116127
case .html:
117-
pages["Home"] = HomePage(module: module, baseURL: baseURL, symbolFilter: symbolFilter)
128+
pages["Home"] = HomePage(module: module, externalTypes: Array(symbolsByExternalType.keys), baseURL: baseURL, symbolFilter: symbolFilter)
118129
}
119130

120131
try pages.map { $0 }.parallelForEach {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import CommonMarkBuilder
2+
import SwiftDoc
3+
import HypertextLiteral
4+
import SwiftMarkup
5+
import SwiftSemantics
6+
7+
struct ExternalTypePage: Page {
8+
9+
let module: Module
10+
let externalType: String
11+
let baseURL: String
12+
13+
let typealiases: [Symbol]
14+
let initializers: [Symbol]
15+
let properties: [Symbol]
16+
let methods: [Symbol]
17+
18+
init(module: Module, externalType: String, symbols: [Symbol], baseURL: String) {
19+
self.module = module
20+
self.externalType = externalType
21+
self.baseURL = baseURL
22+
23+
self.typealiases = symbols.filter { $0.api is Typealias }
24+
self.initializers = symbols.filter { $0.api is Initializer }
25+
self.properties = symbols.filter { $0.api is Variable }
26+
self.methods = symbols.filter { $0.api is Function }
27+
}
28+
29+
var title: String { externalType }
30+
31+
var sections: [(title: String, members: [Symbol])] {
32+
return [
33+
("Nested Type Aliases", typealiases),
34+
("Initializers", initializers),
35+
("Properties", properties),
36+
("Methods", methods),
37+
].filter { !$0.members.isEmpty }
38+
}
39+
40+
var document: CommonMark.Document {
41+
Document {
42+
Heading { "Extensions on \(externalType)" }
43+
ForEach(in: sections) { section -> BlockConvertible in
44+
Section {
45+
Heading { section.title }
46+
47+
Section {
48+
ForEach(in: section.members) { member in
49+
Heading {
50+
Code { member.name }
51+
}
52+
Documentation(for: member, in: module, baseURL: baseURL)
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
59+
var html: HypertextLiteral.HTML {
60+
#"""
61+
<h1>
62+
<small>Extensions on</small>
63+
<code class="name">\#(externalType)</code>
64+
</h1>
65+
\#(sections.map { section -> HypertextLiteral.HTML in
66+
#"""
67+
<section id=\#(section.title.lowercased())>
68+
<h2>\#(section.title)</h2>
69+
70+
\#(section.members.map { member -> HypertextLiteral.HTML in
71+
let descriptor = String(describing: type(of: member.api)).lowercased()
72+
73+
return #"""
74+
<div role="article" class="\#(descriptor)" id=\#(member.id.description.lowercased().replacingOccurrences(of: " ", with: "-"))>
75+
<h3>
76+
<code>\#(softbreak(member.name))</code>
77+
</h3>
78+
\#(Documentation(for: member, in: module, baseURL: baseURL).html)
79+
</div>
80+
"""#
81+
})
82+
</section>
83+
"""#
84+
})
85+
"""#
86+
}
87+
}

Sources/swift-doc/Supporting Types/Pages/HomePage.swift

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ struct HomePage: Page {
1616
var globalFunctions: [Symbol] = []
1717
var globalVariables: [Symbol] = []
1818

19-
init(module: Module, baseURL: String, symbolFilter: (Symbol) -> Bool) {
19+
let externalTypes: [String]
20+
21+
init(module: Module, externalTypes: [String], baseURL: String, symbolFilter: (Symbol) -> Bool) {
2022
self.module = module
2123
self.baseURL = baseURL
2224

25+
self.externalTypes = externalTypes
26+
2327
for symbol in module.interface.topLevelSymbols.filter(symbolFilter) {
2428
switch symbol.api {
2529
case is Class:
@@ -70,6 +74,18 @@ struct HomePage: Page {
7074
}
7175
}
7276
}
77+
78+
if !externalTypes.isEmpty {
79+
Heading { "Extensions"}
80+
81+
List(of: externalTypes.sorted()) { typeName in
82+
List.Item {
83+
Paragraph {
84+
Link(urlString: path(for: route(for: typeName), with: baseURL), text: typeName)
85+
}
86+
}
87+
}
88+
}
7389
}
7490
}
7591

@@ -95,6 +111,23 @@ struct HomePage: Page {
95111
</section>
96112
"""#
97113
})
114+
\#((externalTypes.isEmpty ? "" :
115+
#"""
116+
<section id="extensions">
117+
<h2>Extensions</h2>
118+
<dl>
119+
\#(externalTypes.sorted().map {
120+
#"""
121+
<dt class="extension">
122+
<a href="\#(path(for: route(for: $0), with: baseURL))">\#($0)</a>
123+
</dt>
124+
<dd></dd>
125+
"""# as HypertextLiteral.HTML
126+
})
127+
</dl>
128+
<section>
129+
"""#
130+
) as HypertextLiteral.HTML)
98131
"""#
99132
}
100133
}

Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ struct SidebarPage: Page {
1414
var globalFunctionNames: Set<String> = []
1515
var globalVariableNames: Set<String> = []
1616

17-
init(module: Module, baseURL: String, symbolFilter: (Symbol) -> Bool) {
17+
let externalTypes: Set<String>
18+
19+
init(module: Module, externalTypes: Set<String>, baseURL: String, symbolFilter: (Symbol) -> Bool) {
1820
self.module = module
1921
self.baseURL = baseURL
2022

23+
self.externalTypes = externalTypes
24+
2125
for symbol in module.interface.topLevelSymbols.filter(symbolFilter) {
2226
switch symbol.api {
2327
case is Class:
@@ -55,7 +59,8 @@ struct SidebarPage: Page {
5559
("Global Typealiases", globalTypealiasNames),
5660
("Global Variables",globalVariableNames),
5761
("Global Functions", globalFunctionNames),
58-
("Operators", operatorNames)
62+
("Operators", operatorNames),
63+
("Extensions", externalTypes),
5964
] as [(title: String, names: Set<String>)]
6065
).filter { !$0.names.isEmpty }) { section in
6166
// FIXME: This should be an HTML block

0 commit comments

Comments
 (0)