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

Commit

Permalink
Build documentation for extensions on external types. (#230)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Lukas-Stuehrk and mattt authored Apr 23, 2021
1 parent 1b2baaf commit b8a0cd9
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 24 deletions.
5 changes: 4 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

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

### Changed
Expand Down
31 changes: 28 additions & 3 deletions Sources/SwiftDoc/Identifier.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
public struct Identifier: Hashable {
public let pathComponents: [String]
public let context: [String]
public let name: String
public let pathComponents: [String]

public init(context: [String], name: String) {
self.context = context
self.name = name
self.pathComponents = context + CollectionOfOne(name)
}

public func matches(_ string: String) -> Bool {
(pathComponents + CollectionOfOne(name)).reversed().starts(with: string.split(separator: ".").map { String($0) }.reversed())
return matches(string.split(separator: "."))
}

public func matches(_ pathComponents: [Substring]) -> Bool {
return matches(pathComponents.map(String.init))
}

public func matches(_ pathComponents: [String]) -> Bool {
return self.pathComponents.ends(with: pathComponents)
}
}

// MARK: - CustomStringConvertible

extension Identifier: CustomStringConvertible {
public var description: String {
(pathComponents + CollectionOfOne(name)).joined(separator: ".")
pathComponents.joined(separator: ".")
}
}

fileprivate extension Array {
func ends<PossibleSuffix>(with possibleSuffix: PossibleSuffix) -> Bool
where PossibleSuffix : Sequence,
Self.Element == PossibleSuffix.Element,
Self.Element: Equatable
{
reversed().starts(with: possibleSuffix)
}
}
49 changes: 41 additions & 8 deletions Sources/SwiftDoc/Interface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ public final class Interface {
self.imports = imports
self.symbols = symbols

self.symbolsGroupedByIdentifier = Dictionary(grouping: symbols, by: { $0.id })
self.symbolsGroupedByQualifiedName = Dictionary(grouping: symbols, by: { $0.id.description })
self.topLevelSymbols = symbols.filter { $0.api is Type || $0.id.pathComponents.isEmpty }
let symbolsGroupedByIdentifier = Dictionary(grouping: symbols, by: { $0.id })
let symbolsGroupedByQualifiedName = Dictionary(grouping: symbols, by: { $0.id.description })

self.symbolsGroupedByIdentifier = symbolsGroupedByIdentifier
self.symbolsGroupedByQualifiedName = symbolsGroupedByQualifiedName
self.topLevelSymbols = symbols.filter { $0.api is Type || $0.id.context.isEmpty }

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

var relationships: Set<Relationship> = []
for symbol in symbols {

let lastDeclarationScope = symbol.context.last(where: { $0 is Extension || $0 is Symbol })

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

if let `extension` = lastDeclarationScope as? Extension {
if let extended = symbols.first(where: { $0.api is Type && $0.id.matches(`extension`.extendedType) }) {

for extended in symbolsGroupedByIdentifier.named(`extension`.extendedType, resolvingTypealiases: true) {
let predicate: Relationship.Predicate
switch extended.api {
case is Protocol:
Expand All @@ -66,7 +67,7 @@ public final class Interface {
inheritedTypeNames = Set(inheritedTypeNames.flatMap { $0.split(separator: "&").map { $0.trimmingCharacters(in: .whitespaces) } })

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

return classClusters

}

public let relationships: [Relationship]
Expand Down Expand Up @@ -159,4 +159,37 @@ public final class Interface {
public func defaultImplementations(of symbol: Symbol) -> [Symbol] {
return relationshipsByObject[symbol.id]?.filter { $0.predicate == .defaultImplementationOf }.map { $0.subject }.sorted() ?? []
}

// MARK: -

public func symbols(named name: String, resolvingTypealiases: Bool) -> [Symbol] {
symbolsGroupedByIdentifier.named(name, resolvingTypealiases: resolvingTypealiases)
}
}

fileprivate extension Dictionary where Key == Identifier, Value == [Symbol] {
func named(_ name: String, resolvingTypealiases: Bool) -> [Symbol] {
var pathComponents: [String] = []
for component in name.split(separator: ".") {
pathComponents.append("\(component)")
guard resolvingTypealiases else { continue }

if let symbols = first(where: { $0.key.pathComponents == pathComponents })?.value,
let symbol = symbols.first(where: { $0.api is Typealias }),
let `typealias` = symbol.api as? Typealias,
let initializedType = `typealias`.initializedType
{
let initializedTypePathComponents = initializedType.split(separator: ".")
let candidates = keys.filter { $0.matches(initializedTypePathComponents) }

if let id = candidates.max(by: { $0.pathComponents.count > $1.pathComponents.count }) {
pathComponents = id.pathComponents
} else {
return []
}
}
}

return first(where: { $0.key.pathComponents == pathComponents })?.value ?? []
}
}
16 changes: 10 additions & 6 deletions Sources/SwiftDoc/Symbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import struct Highlighter.Token
public final class Symbol {
public typealias ID = Identifier

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

init(api: API, context: [Contextual], declaration: [Token], documentation: Documentation?, sourceRange: SourceRange?) {
self.id = Identifier(context: context.compactMap {
($0 as? Symbol)?.name ?? ($0 as? Extension)?.extendedType
}, name: api.name)
self.api = api
self.context = context
self.declaration = declaration
Expand All @@ -30,12 +34,6 @@ public final class Symbol {
return api.name
}

public private(set) lazy var id: ID = {
Identifier(pathComponents: context.compactMap {
($0 as? Symbol)?.name ?? ($0 as? Extension)?.extendedType
}, name: name)
}()

public var isPublic: Bool {
if api is Unknown {
return true
Expand Down Expand Up @@ -329,3 +327,9 @@ extension Symbol: Codable {
try container.encode(sourceRange, forKey: .sourceRange)
}
}

extension Symbol: CustomDebugStringConvertible {
public var debugDescription: String {
return "\(self.declaration.map { $0.text }.joined())"
}
}
17 changes: 14 additions & 3 deletions Sources/swift-doc/Subcommands/Generate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ extension SwiftDoc {
}
}

// Extensions on external types.
var symbolsByExternalType: [String: [Symbol]] = [:]
for symbol in module.interface.symbols.filter(symbolFilter) {
guard let extensionDeclaration = symbol.context.first as? Extension, symbol.context.count == 1 else { continue }
guard module.interface.symbols(named: extensionDeclaration.extendedType, resolvingTypealiases: true).isEmpty else { continue }
symbolsByExternalType[extensionDeclaration.extendedType, default: []] += [symbol]
}
for (typeName, symbols) in symbolsByExternalType {
pages[route(for: typeName)] = ExternalTypePage(module: module, externalType: typeName, symbols: symbols, baseURL: baseURL)
}

for (name, symbols) in globals {
pages[route(for: name)] = GlobalPage(module: module, name: name, symbols: symbols, baseURL: baseURL)
}
Expand All @@ -110,11 +121,11 @@ extension SwiftDoc {
} else {
switch format {
case .commonmark:
pages["Home"] = HomePage(module: module, baseURL: baseURL, symbolFilter: symbolFilter)
pages["_Sidebar"] = SidebarPage(module: module, baseURL: baseURL, symbolFilter: symbolFilter)
pages["Home"] = HomePage(module: module, externalTypes: Array(symbolsByExternalType.keys), baseURL: baseURL, symbolFilter: symbolFilter)
pages["_Sidebar"] = SidebarPage(module: module, externalTypes: Set(symbolsByExternalType.keys), baseURL: baseURL, symbolFilter: symbolFilter)
pages["_Footer"] = FooterPage(baseURL: baseURL)
case .html:
pages["Home"] = HomePage(module: module, baseURL: baseURL, symbolFilter: symbolFilter)
pages["Home"] = HomePage(module: module, externalTypes: Array(symbolsByExternalType.keys), baseURL: baseURL, symbolFilter: symbolFilter)
}

try pages.map { $0 }.parallelForEach {
Expand Down
87 changes: 87 additions & 0 deletions Sources/swift-doc/Supporting Types/Pages/ExternalTypePage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import CommonMarkBuilder
import SwiftDoc
import HypertextLiteral
import SwiftMarkup
import SwiftSemantics

struct ExternalTypePage: Page {

let module: Module
let externalType: String
let baseURL: String

let typealiases: [Symbol]
let initializers: [Symbol]
let properties: [Symbol]
let methods: [Symbol]

init(module: Module, externalType: String, symbols: [Symbol], baseURL: String) {
self.module = module
self.externalType = externalType
self.baseURL = baseURL

self.typealiases = symbols.filter { $0.api is Typealias }
self.initializers = symbols.filter { $0.api is Initializer }
self.properties = symbols.filter { $0.api is Variable }
self.methods = symbols.filter { $0.api is Function }
}

var title: String { externalType }

var sections: [(title: String, members: [Symbol])] {
return [
("Nested Type Aliases", typealiases),
("Initializers", initializers),
("Properties", properties),
("Methods", methods),
].filter { !$0.members.isEmpty }
}

var document: CommonMark.Document {
Document {
Heading { "Extensions on \(externalType)" }
ForEach(in: sections) { section -> BlockConvertible in
Section {
Heading { section.title }

Section {
ForEach(in: section.members) { member in
Heading {
Code { member.name }
}
Documentation(for: member, in: module, baseURL: baseURL)
}
}
}
}
}
}
var html: HypertextLiteral.HTML {
#"""
<h1>
<small>Extensions on</small>
<code class="name">\#(externalType)</code>
</h1>
\#(sections.map { section -> HypertextLiteral.HTML in
#"""
<section id=\#(section.title.lowercased())>
<h2>\#(section.title)</h2>
\#(section.members.map { member -> HypertextLiteral.HTML in
let descriptor = String(describing: type(of: member.api)).lowercased()

return #"""
<div role="article" class="\#(descriptor)" id=\#(member.id.description.lowercased().replacingOccurrences(of: " ", with: "-"))>
<h3>
<code>\#(softbreak(member.name))</code>
</h3>
\#(Documentation(for: member, in: module, baseURL: baseURL).html)
</div>
"""#
})
</section>
"""#
})
"""#
}
}
35 changes: 34 additions & 1 deletion Sources/swift-doc/Supporting Types/Pages/HomePage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ struct HomePage: Page {
var globalFunctions: [Symbol] = []
var globalVariables: [Symbol] = []

init(module: Module, baseURL: String, symbolFilter: (Symbol) -> Bool) {
let externalTypes: [String]

init(module: Module, externalTypes: [String], baseURL: String, symbolFilter: (Symbol) -> Bool) {
self.module = module
self.baseURL = baseURL

self.externalTypes = externalTypes

for symbol in module.interface.topLevelSymbols.filter(symbolFilter) {
switch symbol.api {
case is Class:
Expand Down Expand Up @@ -70,6 +74,18 @@ struct HomePage: Page {
}
}
}

if !externalTypes.isEmpty {
Heading { "Extensions"}

List(of: externalTypes.sorted()) { typeName in
List.Item {
Paragraph {
Link(urlString: path(for: route(for: typeName), with: baseURL), text: typeName)
}
}
}
}
}
}

Expand All @@ -95,6 +111,23 @@ struct HomePage: Page {
</section>
"""#
})
\#((externalTypes.isEmpty ? "" :
#"""
<section id="extensions">
<h2>Extensions</h2>
<dl>
\#(externalTypes.sorted().map {
#"""
<dt class="extension">
<a href="\#(path(for: route(for: $0), with: baseURL))">\#($0)</a>
</dt>
<dd></dd>
"""# as HypertextLiteral.HTML
})
</dl>
<section>
"""#
) as HypertextLiteral.HTML)
"""#
}
}
9 changes: 7 additions & 2 deletions Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ struct SidebarPage: Page {
var globalFunctionNames: Set<String> = []
var globalVariableNames: Set<String> = []

init(module: Module, baseURL: String, symbolFilter: (Symbol) -> Bool) {
let externalTypes: Set<String>

init(module: Module, externalTypes: Set<String>, baseURL: String, symbolFilter: (Symbol) -> Bool) {
self.module = module
self.baseURL = baseURL

self.externalTypes = externalTypes

for symbol in module.interface.topLevelSymbols.filter(symbolFilter) {
switch symbol.api {
case is Class:
Expand Down Expand Up @@ -55,7 +59,8 @@ struct SidebarPage: Page {
("Global Typealiases", globalTypealiasNames),
("Global Variables",globalVariableNames),
("Global Functions", globalFunctionNames),
("Operators", operatorNames)
("Operators", operatorNames),
("Extensions", externalTypes),
] as [(title: String, names: Set<String>)]
).filter { !$0.names.isEmpty }) { section in
// FIXME: This should be an HTML block
Expand Down
Loading

0 comments on commit b8a0cd9

Please sign in to comment.