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

Build documentation for extensions on external types. #230

Merged
merged 10 commits into from
Apr 23, 2021
20 changes: 20 additions & 0 deletions Sources/SwiftDoc/Interface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,24 @@ 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] {
var pathComponents: [String] = []
for component in name.split(separator: ".", omittingEmptySubsequences: true) {
pathComponents.append("\(component)")
guard resolvingTypealiases else { continue }

if let symbols = symbolsGroupedByQualifiedName[pathComponents.joined(separator: ".")],
let symbol = symbols.first(where: { $0.api is Typealias }),
let `typealias` = symbol.api as? Typealias,
let initializedType = `typealias`.initializedType
{
pathComponents = (symbol.context.compactMap { ($0 as? Symbol)?.name ?? ($0 as? Extension)?.extendedType }) + [initializedType]
}
}

return symbolsGroupedByQualifiedName[pathComponents.joined(separator: ".")] ?? []
}
}
6 changes: 6 additions & 0 deletions Sources/SwiftDoc/Symbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,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 @@ -75,6 +75,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 @@ -101,11 +112,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
55 changes: 55 additions & 0 deletions Tests/SwiftDocTests/InterfaceTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,59 @@ final class InterfaceTypeTests: XCTestCase {
XCTAssertEqual(defaultImplementations[0].name, "someMethod()")
XCTAssertEqual(defaultImplementations[1].name, "someOtherMethod()")
}

func testExternalSymbols() throws {
let source = #"""
import UIKit

public class SomeClass {
public struct InnerObject { }

typealias ActuallyExternal = UIView

typealias ActuallyInternal = InnerStruct

struct InnerStruct {}
}

public typealias MyClass = SomeClass

public typealias ExternalClass = UIGestureRecognizer
"""#


let url = try temporaryFile(contents: source)
let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent())
let module = Module(name: "Module", sourceFiles: [sourceFile])

XCTAssertEqual(module.interface.symbols(named: "SomeClass", resolvingTypealiases: true).first?.name, "SomeClass")
XCTAssertEqual(module.interface.symbols(named: "SomeClass", resolvingTypealiases: false).first?.name, "SomeClass")

XCTAssertEqual(module.interface.symbols(named: "SomeClass.InnerObject", resolvingTypealiases: true).first?.name, "InnerObject")
XCTAssertEqual(module.interface.symbols(named: "SomeClass.InnerObject", resolvingTypealiases: false).first?.name, "InnerObject")

XCTAssertEqual(module.interface.symbols(named: "SomeClass.ActuallyInternal", resolvingTypealiases: true).first?.name, "InnerStruct")
XCTAssertEqual(module.interface.symbols(named: "SomeClass.ActuallyInternal", resolvingTypealiases: false).first?.name, "ActuallyInternal")

XCTAssertEqual(module.interface.symbols(named: "MyClass", resolvingTypealiases: true).first?.name, "SomeClass")
XCTAssertEqual(module.interface.symbols(named: "MyClass", resolvingTypealiases: false).first?.name, "MyClass")

XCTAssertEqual(module.interface.symbols(named: "MyClass.InnerObject", resolvingTypealiases: true).first?.name, "InnerObject")
XCTAssertNil(module.interface.symbols(named: "MyClass.InnerObject", resolvingTypealiases: false).first)

XCTAssertNil(module.interface.symbols(named: "ExternalClass", resolvingTypealiases: true).first)
XCTAssertTrue(module.interface.symbols(named: "ExternalClass", resolvingTypealiases: false).first?.api is Typealias)

XCTAssertNil(module.interface.symbols(named: "ExternalClass.State", resolvingTypealiases: true).first)
XCTAssertNil(module.interface.symbols(named: "ExternalClass.State", resolvingTypealiases: false).first)

XCTAssertNil(module.interface.symbols(named: "SomeClass.ActuallyExternal", resolvingTypealiases: true).first)
XCTAssertTrue(module.interface.symbols(named: "SomeClass.ActuallyExternal", resolvingTypealiases: false).first?.api is Typealias)

XCTAssertNil(module.interface.symbols(named: "UIGestureRecognizer", resolvingTypealiases: true).first)
XCTAssertNil(module.interface.symbols(named: "UIGestureRecognizer", resolvingTypealiases: false).first)

XCTAssertNil(module.interface.symbols(named: "UIGestureRecognizer.State", resolvingTypealiases: true).first)
XCTAssertNil(module.interface.symbols(named: "UIGestureRecognizer.State", resolvingTypealiases: false).first)
}
}