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

Group operators #228

Merged
merged 7 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Assets/css/all.css
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
--icon-extension: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='m54.43 81.93h-33.92v-63.86h33.92v12.26h-21.82v13.8h20.45v11.32h-20.45v14.22h21.82z'/%3E%3Cpath d='m68.74 74.58h-.27l-2.78 7.35h-7.28l5.59-12.61-6-12.54h8l2.74 7.3h.27l2.76-7.3h7.64l-6.14 12.54 5.89 12.61h-7.64z'/%3E%3C/g%3E%3C/svg%3E%0A");
--icon-function: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m24.25 75.66a5.47 5.47 0 0 1 5.75-5.73c1.55 0 3.55.41 6.46.41 3.19 0 4.78-1.55 5.46-6.65l1.5-10.14h-9.34a6 6 0 1 1 0-12h11.1l1.09-7.27c1.55-10.89 8.01-16.58 17.73-16.58 6.69 0 11.74 1.77 11.74 6.64a5.47 5.47 0 0 1 -5.74 5.73c-1.55 0-3.55-.41-6.46-.41-3.14 0-4.73 1.51-5.46 6.65l-.78 5.27h11.44a6 6 0 1 1 .05 12h-13.19l-1.78 12.11c-1.59 10.92-8.1 16.61-17.82 16.61-6.7 0-11.75-1.77-11.75-6.64z' fill='%23fff'/%3E%3C/svg%3E%0A");
--icon-method: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%235a98f8' height='90' rx='8' stroke='%232974ed' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m70.61 81.71v-39.6h-.31l-15.69 39.6h-9.22l-15.65-39.6h-.35v39.6h-14.19v-63.42h18.63l16 41.44h.36l16-41.44h18.61v63.42z' fill='%23fff'/%3E%3C/svg%3E%0A");
--icon-operator: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cellipse fill='%23fff' cx='50' cy='50' rx='16' ry='16'/%3E%3C/svg%3E%0A");
--icon-property: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m52.31 18.29c13.62 0 22.85 8.84 22.85 22.46s-9.71 22.37-23.82 22.37h-10.34v18.59h-16.16v-63.42zm-11.31 32.71h7c6.85 0 10.89-3.56 10.89-10.2s-4.08-10.16-10.89-10.16h-7z' fill='%23fff'/%3E%3C/svg%3E%0A");
--icon-protocol: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23ff6682' height='90' rx='8' stroke='%23ff2d55' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='m46.28 18.29c11.84 0 20 8.66 20 21.71s-8.44 21.71-20.6 21.71h-10.81v20h-12.09v-63.42zm-11.41 33.05h8.13c6.93 0 11-4 11-11.29s-4-11.25-10.93-11.25h-8.2z'/%3E%3Cpath d='m62 57.45h8v4.77h.16c.84-3.45 2.54-5.12 5.17-5.12a5.06 5.06 0 0 1 1.92.35v7.55a5.69 5.69 0 0 0 -2.39-.51c-3.08 0-4.66 1.74-4.66 5.12v12.1h-8.2z'/%3E%3C/g%3E%3C/svg%3E%0A");
--icon-structure: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23b57edf' height='90' rx='8' stroke='%239454c2' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m38.38 63c.74 4.53 5.62 7.16 11.82 7.16s10.37-2.81 10.37-6.68c0-3.51-2.73-5.31-10.24-6.76l-6.5-1.23c-12.66-2.35-19.21-8.49-19.21-18.21 0-12.22 10.59-20.09 25.18-20.09 16 0 25.36 7.83 25.53 19.91h-15c-.26-4.57-4.57-7.29-10.42-7.29s-9.31 2.63-9.31 6.37c0 3.34 2.9 5.18 9.8 6.5l6.5 1.23c13.56 2.6 19.71 8.09 19.71 18.09 0 12.74-10 20.83-26.72 20.83-15.82 0-26.28-7.3-26.5-19.78z' fill='%23fff'/%3E%3C/svg%3E%0A");
Expand Down Expand Up @@ -950,6 +951,10 @@ h1 small {
color: var(--quaternary-label);
}

h3 small {
color: var(--tertiary-label);
}

p code,
dd code,
li code {
Expand Down Expand Up @@ -1036,6 +1041,11 @@ nav li[class] {
--link: var(--system-blue);
}

.operator {
--background-image: var(--icon-operator);
--link: var(--system-green);
}

.property {
--background-image: var(--icon-property);
--link: var(--system-teal);
Expand Down
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for generating documentation for
extensions to external types.
#230 by @Lukas-Stuehrk and @mattt.
- Added support for generating documentation for operators.
#228 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
2 changes: 1 addition & 1 deletion Resources/all.min.css

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion Sources/SwiftDoc/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ extension Enumeration.Case: API {}

extension Function: API {
public var name: String {
return "\(identifier)(\(signature.input.map { ($0.firstName ?? "_") + ":" }.joined()))"
if self.isOperator {
return identifier
} else {
return "\(identifier)(\(signature.input.map { ($0.firstName ?? "_") + ":" }.joined()))"
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions Sources/SwiftDoc/Extensions/SwiftSemantics+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,18 @@ extension Structure: Type {}
extension Unknown: Type {
public var inheritance: [String] { return [] }
}

// MARK: -

extension Operator.Kind: Comparable {
public static func < (lhs: Operator.Kind, rhs: Operator.Kind) -> Bool {
switch (lhs, rhs) {
case (_, .infix):
return true
case (.postfix, _):
return true
default:
return false
}
}
}
1 change: 1 addition & 0 deletions Sources/SwiftDoc/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public func route(for symbol: Symbol) -> String {

public func route(for name: CustomStringConvertible) -> String {
return name.description.replacingOccurrences(of: ".", with: "_")
.replacingOccurrences(of: " ", with: "-")
}

public func path(for symbol: Symbol, with baseURL: String) -> String {
Expand Down
27 changes: 26 additions & 1 deletion Sources/SwiftDoc/Interface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ public final class Interface {

self.symbolsGroupedByIdentifier = symbolsGroupedByIdentifier
self.symbolsGroupedByQualifiedName = symbolsGroupedByQualifiedName
self.topLevelSymbols = symbols.filter { $0.api is Type || $0.id.context.isEmpty }
self.topLevelSymbols = symbols.filter { symbol in
if symbol.api is Type || symbol.api is Operator {
return true
}

if let function = symbol.api as? Function, function.isOperator {
return false
}

return symbol.id.pathComponents.isEmpty
}

self.relationships = {
let extensionsByExtendedType: [String: [Extension]] = Dictionary(grouping: symbols.flatMap { $0.context.compactMap { $0 as? Extension } }, by: { $0.extendedType })
Expand Down Expand Up @@ -90,6 +100,20 @@ public final class Interface {
return Array(relationships)
}()

self.functionsByOperator = {
var functionsByOperator: [Symbol: Set<Symbol>] = [:]

let functionsGroupedByName = Dictionary(grouping: symbols.filter { $0.api is Function},
by: { $0.api.name })

for `operator` in symbols.filter({ $0.api is Operator }) {
let functions = functionsGroupedByName[`operator`.name] ?? []
functionsByOperator[`operator`] = Set(functions)
}

return functionsByOperator
}()

self.relationshipsBySubject = Dictionary(grouping: relationships, by: { $0.subject.id })
self.relationshipsByObject = Dictionary(grouping: relationships, by: { $0.object.id })
}
Expand All @@ -99,6 +123,7 @@ public final class Interface {
public let symbolsGroupedByIdentifier: [Symbol.ID: [Symbol]]
public let symbolsGroupedByQualifiedName: [String: [Symbol]]
public let topLevelSymbols: [Symbol]
public var functionsByOperator: [Symbol: Set<Symbol>]
public var baseClasses: [Symbol] {
symbols.filter { $0.api is Class && typesInherited(by: $0).isEmpty }
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/SwiftDoc/SourceFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ public struct SourceFile: Hashable, Codable {
return .skipChildren
}

override func visit(_ node: OperatorDeclSyntax) -> SyntaxVisitorContinueKind {
push(symbol(Operator.self, node))
return .skipChildren
}

override func visit(_ node: PrecedenceGroupDeclSyntax) -> SyntaxVisitorContinueKind {
push(symbol(PrecedenceGroup.self, node))
return .skipChildren
Expand Down
13 changes: 13 additions & 0 deletions Sources/SwiftDoc/Symbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,24 @@ public final class Symbol {
return api.name
}

public var kind: String {
switch api {
case let function as Function where function.isOperator:
return "Operator"
default:
return String(describing: type(of: api))
}
}

public var isPublic: Bool {
if api is Unknown {
return true
}

if api is Operator {
return true
}

if api.modifiers.contains(where: { $0.name == "public" || $0.name == "open" }) {
return true
}
Expand Down
28 changes: 28 additions & 0 deletions Sources/swift-doc/Extensions/StringProtocol+Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

extension StringProtocol {
func indented(by spaces: Int = 2) -> String {
return String(repeating: " ", count: spaces) + self
Expand All @@ -20,4 +22,30 @@ extension StringProtocol {
// See: https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax#using-emoji
return self.replacingOccurrences(of: ":", with: ":\u{200B}")
}

var escaped: String {
#if os(macOS)
return (CFXMLCreateStringByEscapingEntities(nil, String(self) as NSString, nil)! as NSString) as String
#else
return [
("&", "&amp;"),
("<", "&lt;"),
(">", "&gt;"),
("'", "&apos;"),
("\"", "&quot;"),
].reduce(String(self)) { (string, element) in
string.replacingOccurrences(of: element.0, with: element.1)
}
#endif
}

func escapingOccurrences<Target>(of target: Target, options: String.CompareOptions = [], range searchRange: Range<String.Index>? = nil) -> String where Target : StringProtocol {
return replacingOccurrences(of: target, with: target.escaped, options: options, range: searchRange)
}

func escapingOccurrences<Target>(of targets: [Target], options: String.CompareOptions = []) -> String where Target : StringProtocol {
return targets.reduce(into: String(self)) { (result, target) in
result = result.escapingOccurrences(of: target, options: options)
}
}
}
2 changes: 2 additions & 0 deletions Sources/swift-doc/Subcommands/Generate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ extension SwiftDoc {
pages[route(for: symbol)] = TypePage(module: module, symbol: symbol, baseURL: baseURL, includingChildren: symbolFilter)
case let `typealias` as Typealias:
pages[route(for: `typealias`.name)] = TypealiasPage(module: module, symbol: symbol, baseURL: baseURL)
case is Operator:
pages[route(for: symbol)] = OperatorPage(module: module, symbol: symbol, baseURL: baseURL)
case let function as Function where !function.isOperator:
globals[function.name, default: []] += [symbol]
case let variable as Variable:
Expand Down
5 changes: 4 additions & 1 deletion Sources/swift-doc/Supporting Types/Components/Members.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct Members: Component {
var cases: [Symbol]
var properties: [Symbol]
var methods: [Symbol]
let operatorImplementations: [Symbol]
var genericallyConstrainedMembers: [[GenericRequirement] : [Symbol]]
let defaultImplementations: [Symbol]

Expand All @@ -32,7 +33,8 @@ struct Members: Component {
self.initializers = members.filter { $0.api is Initializer }
self.cases = members.filter { $0.api is Enumeration.Case }
self.properties = members.filter { $0.api is Variable }
self.methods = members.filter { $0.api is Function }
self.methods = members.filter { ($0.api as? Function)?.isOperator == false }
self.operatorImplementations = members.filter { ($0.api as? Function)?.isOperator == true }
self.genericallyConstrainedMembers = Dictionary(grouping: members) { $0.`extension`?.genericRequirements ?? [] }.filter { !$0.key.isEmpty }
self.defaultImplementations = module.interface.defaultImplementations(of: symbol).filter(symbolFilter)
}
Expand All @@ -44,6 +46,7 @@ struct Members: Component {
("Enumeration Cases", cases),
("Properties", properties),
("Methods", methods),
("Operators", operatorImplementations),
("Default Implementations", defaultImplementations),
].filter { !$0.members.isEmpty }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import CommonMarkBuilder
import SwiftDoc
import SwiftMarkup
import SwiftSemantics
import HypertextLiteral

struct OperatorImplementations: Component {
var symbol: Symbol
var module: Module
let baseURL: String

var implementations: [Symbol]

init(of symbol: Symbol, in module: Module, baseURL: String, implementations: [Symbol]) {
self.symbol = symbol
self.module = module
self.baseURL = baseURL
self.implementations = implementations
}


// MARK: - Component

var fragment: Fragment {
guard !implementations.isEmpty else { return Fragment { "" } }

return Fragment {
ForEach(in: implementations) { implementation -> BlockConvertible in
Section {
Heading { implementation.name }

Documentation(for: implementation, in: module, baseURL: baseURL)
}
}
}
}

var html: HypertextLiteral.HTML {
let sections = implementations.compactMap { implementation -> HypertextLiteral.HTML? in
guard let `operator` = symbol.api as? Operator,
let function = implementation.api as? Function
else { return nil }

let heading: String
switch `operator`.kind {
case .infix:
guard function.signature.input.count == 2,
let lhs = function.signature.input.first,
let rhs = function.signature.input.last
else {
return nil
}

heading = [lhs.type, function.name, rhs.type].compactMap { $0 }.joined(separator: " ")
case .prefix:
guard function.signature.input.count == 2,
let operand = function.signature.input.first
else {
return nil
}
heading = [function.name, operand.type].compactMap { $0 }.joined(separator: " ")

case .postfix:
guard function.signature.input.count == 2,
let operand = function.signature.input.first
else {
return nil
}
heading = [operand.type, function.name].compactMap { $0 }.joined(separator: " ")
}

return #"""
<div role="article" class="function" id=\#(implementation.id.description.lowercased().replacingOccurrences(of: " ", with: "-"))>
<h3>
\#(heading)
\#(unsafeUnescaped: function.genericWhereClause.map({ #"<small>\#($0.escaped)</small>"# }) ?? "")
</h3>
\#(Documentation(for: implementation, in: module, baseURL: baseURL).html)
</div>
"""#
}

guard !sections.isEmpty else { return "" }

return #"""
<section id="implementations">
<h2>Implementations</h2>
\#(sections)
</section>
"""#
}
}

fileprivate extension Function {
var genericWhereClause: String? {
guard !genericRequirements.isEmpty else { return nil }
return "where \(genericRequirements.map { $0.description }.joined(separator: ", "))"
}
}
Loading