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

Fix nested type handling #62

Merged
merged 9 commits into from
Apr 10, 2020
8 changes: 8 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `--base-url` option.
#65 by @kean.

### Fixed

- Fixed relationship handling for members of nested types.
#62 by @victor-pavlychko.
- Fixed rendering of type relationships section when no graph data is available.
#62 by @victor-pavlychko.


## [1.0.0-beta.2] - 2020-04-08

### Changed
Expand Down
6 changes: 3 additions & 3 deletions Sources/SwiftDoc/Interface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ public final class Interface: Codable {
public private(set) lazy var relationships: [Relationship] = {
var relationships: Set<Relationship> = []
for symbol in symbols {
let `extension` = symbol.context.compactMap({ $0 as? Extension }).first
let lastDeclarationScope = symbol.context.last(where: { $0 is Extension || $0 is Symbol })

if let container = symbol.context.compactMap({ $0 as? Symbol }).last {
if let container = lastDeclarationScope as? Symbol {
let predicate: Relationship.Predicate

switch container.api {
Expand All @@ -74,7 +74,7 @@ public final class Interface: Codable {
relationships.insert(Relationship(subject: symbol, predicate: predicate, object: container))
}

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

let predicate: Relationship.Predicate
Expand Down
47 changes: 26 additions & 21 deletions Sources/swift-doc/Supporting Types/Components/Relationships.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ struct Relationships: Component {
self.inheritedTypes = module.interface.typesInherited(by: symbol) + module.interface.typesConformed(by: symbol)
}

var graphHTML: HypertextLiteral.HTML? {
var graph = symbol.graph(in: module)
guard !graph.edges.isEmpty else { return nil }

graph.aspectRatio = 0.125
graph.center = true
graph.overlap = "compress"

let algorithm: LayoutAlgorithm = graph.nodes.count > 3 ? .neato : .dot

do {
return try HypertextLiteral.HTML(String(data: graph.render(using: algorithm, to: .svg), encoding: .utf8) ?? "")
} catch {
logger.error("\(error)")
return nil
}
}

var sections: [(title: String, symbols: [Symbol])] {
return [
Expand Down Expand Up @@ -73,33 +90,21 @@ struct Relationships: Component {
}

var html: HypertextLiteral.HTML {
var graph = symbol.graph(in: module)
guard !graph.edges.isEmpty else { return "" }

graph.aspectRatio = 0.125
graph.center = true
graph.overlap = "compress"

let algorithm: LayoutAlgorithm = graph.nodes.count > 3 ? .neato : .dot
var svg: HypertextLiteral.HTML?

do {
svg = try HypertextLiteral.HTML(String(data: graph.render(using: algorithm, to: .svg), encoding: .utf8) ?? "")
} catch {
logger.error("\(error)")
}
guard !sections.isEmpty else { return "" }

return #"""
<section id="relationships">
<h2 hidden>Relationships</h2>
<figure>
\#(svg ?? "")
\#(graphHTML.flatMap { graphHTML in
return #"""
<figure>
\#(graphHTML)

<figcaption hidden>Inheritance graph for \#(symbol.id).</figcaption>
</figure>
<figcaption hidden>Inheritance graph for \#(symbol.id).</figcaption>
</figure>
"""#
} ?? "")
\#(sections.compactMap { (heading, symbols) -> HypertextLiteral.HTML? in
guard !symbols.isEmpty else { return nil }

let partitioned = symbols.filter { !($0.api is Unknown) } + symbols.filter { ($0.api is Unknown) }

return #"""
Expand Down
127 changes: 127 additions & 0 deletions Tests/SwiftDocTests/NestedTypesTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import XCTest

import SwiftDoc
import SwiftSemantics
import struct SwiftSemantics.Protocol
import SwiftSyntax

final class NestedTypesTests: XCTestCase {
func testNestedTypes() throws {
let source = #"""
public class C { }

extension C {
public enum E {
case c
}
}

extension C.E {
public static let tp = 0
}
"""#

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

XCTAssertEqual(sourceFile.symbols.count, 4)

// `class C`
let `class` = sourceFile.symbols[0]
XCTAssert(`class`.api is Class)

// `enum E`
let `enum` = sourceFile.symbols[1]
XCTAssert(`enum`.api is Enumeration)

// `case c`
let `case` = sourceFile.symbols[2]
XCTAssert(`case`.api is Enumeration.Case)

// `let tp`
let `let` = sourceFile.symbols[3]
XCTAssert(`let`.api is Variable)

// `class C` contains `enum E`
let classRelationships = try XCTUnwrap(module.interface.relationshipsByObject[`class`.id])
XCTAssertEqual(classRelationships.count, 1)
XCTAssertTrue(classRelationships.allSatisfy({ $0.predicate == .memberOf }))
XCTAssertEqual(Set(classRelationships.map({ $0.subject.id })), Set([`enum`.id]))

// `enum C` contains `case c` and `let tp`
let enumRelationships = try XCTUnwrap(module.interface.relationshipsByObject[`enum`.id])
XCTAssertEqual(enumRelationships.count, 2)
XCTAssertTrue(enumRelationships.allSatisfy({ $0.predicate == .memberOf }))
XCTAssertEqual(Set(enumRelationships.map({ $0.subject.id })), Set([`case`.id, `let`.id]))

// `case c` and `let tp` have no relationships
XCTAssertNil(module.interface.relationshipsByObject[`case`.id])
XCTAssertNil(module.interface.relationshipsByObject[`let`.id])

// no other relationships present in module
XCTAssertEqual(
module.interface.relationships.count,
[classRelationships, enumRelationships].joined().count
)
}

#if false // Disabling tests for `swift-doc` code, executable targers are not testable.

func testRelationshipsSectionWithNestedTypes() throws {
let source = #"""
public class C {
public enum E {
}
}
"""#

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

// `class C`
let `class` = sourceFile.symbols[0]
XCTAssert(`class`.api is Class)

// `enum E`
let `enum` = sourceFile.symbols[1]
XCTAssert(`enum`.api is Enumeration)

let classRelationships = Relationships(of: `class`, in: module)
XCTAssertNotEqual(classRelationships.html, "")

let enumRelationships = Relationships(of: `enum`, in: module)
XCTAssertNotEqual(enumRelationships.html, "")
}

func testNoRelationshipsSection() throws {
let source = #"""
public class C {
}

public enum E {
}
"""#

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

// `class C`
let `class` = sourceFile.symbols[0]
XCTAssert(`class`.api is Class)

// `enum E`
let `enum` = sourceFile.symbols[1]
XCTAssert(`enum`.api is Enumeration)

let classRelationships = Relationships(of: `class`, in: module)
XCTAssertEqual(classRelationships.html, "")

let enumRelationships = Relationships(of: `enum`, in: module)
XCTAssertEqual(enumRelationships.html, "")
}

#endif
}