Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support disambiguating links with type signature information #643

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6fb3ae4
Experimental support for disambiguating links with type signatures
d-ronnqvist May 25, 2023
8dc4fb0
Merge branch 'main' into readable-symbol-link-disambiguation
d-ronnqvist Jul 7, 2023
6e69607
Merge branch 'main' into readable-symbol-link-disambiguation
d-ronnqvist Jul 12, 2023
9e6d3a6
Split PathHierarchy implementation into different files.
d-ronnqvist Jul 12, 2023
db3e63f
Delete unused tree type
d-ronnqvist Jul 12, 2023
f39847f
Add a more robust parser for type signature disambiguation in the link
d-ronnqvist Jul 12, 2023
3fb6cec
Merge branch 'main' into readable-symbol-link-disambiguation
d-ronnqvist Dec 1, 2023
7341760
Support function signature disambiguation for external links
d-ronnqvist Dec 1, 2023
d4371a8
Merge branch 'main' into readable-symbol-link-disambiguation
d-ronnqvist Dec 14, 2023
3827762
Fix parsing of subtract operators with parameter type disambiguation
d-ronnqvist Dec 14, 2023
73e46d5
Merge branch 'main' into readable-symbol-link-disambiguation
d-ronnqvist Mar 4, 2024
bd4a4a3
Only use hash and kind disambiguation in topic references and URLs
d-ronnqvist Mar 4, 2024
37b6b43
Display function signature in PathHierarchy debug dump
d-ronnqvist Mar 4, 2024
ec9307b
Update tests for subscripts with type signature disambiguation
d-ronnqvist Mar 4, 2024
393716b
Improve presentation of solutions for ambiguous links on command line
d-ronnqvist Mar 4, 2024
39a2636
Update tests to expect the added space in the warning summary
d-ronnqvist Apr 4, 2024
945a74f
Merge branch 'main' into readable-symbol-link-disambiguation
d-ronnqvist Apr 4, 2024
0dc50d5
Extract the full type from the function signature for disambiguation
d-ronnqvist Apr 4, 2024
53429bd
Merge branch 'main' into readable-symbol-link-disambiguation
d-ronnqvist Apr 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,25 @@ private func symbolFileName(_ symbolName: String) -> String {
extension PathHierarchy {
/// Determines the least disambiguated paths for all symbols in the path hierarchy.
///
/// The path hierarchy is capable of producing shorter, less disambiguated, and more readable paths than what's used for topic references and URLs.
/// Each disambiguation improvement has a boolean parameter to disable it so that DocC can emit the same topic references and URLs as it used to.
///
/// - Parameters:
/// - includeDisambiguationForUnambiguousChildren: Whether or not descendants unique to a single collision should maintain the containers disambiguation.
/// - includeLanguage: Whether or not kind disambiguation information should include the source language.
/// - allowAdvancedDisambiguation: Whether or not to support more advanced and more human readable types of disambiguation.
/// - Returns: A map of unique identifier strings to disambiguated file paths.
func caseInsensitiveDisambiguatedPaths(
includeDisambiguationForUnambiguousChildren: Bool = false,
includeLanguage: Bool = false
includeLanguage: Bool = false,
allowAdvancedDisambiguation: Bool = true
) -> [String: String] {
return disambiguatedPaths(
caseSensitive: false,
transformToFileNames: true,
includeDisambiguationForUnambiguousChildren: includeDisambiguationForUnambiguousChildren,
includeLanguage: includeLanguage
includeLanguage: includeLanguage,
allowAdvancedDisambiguation: allowAdvancedDisambiguation
)
}

Expand All @@ -47,7 +53,7 @@ extension PathHierarchy {

func gatherLinksFrom(_ containers: some Sequence<DisambiguationContainer>) {
for container in containers {
let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: false)
let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: false, allowAdvancedDisambiguation: true)

for (node, disambiguation) in disambiguatedChildren {
guard let id = node.identifier, let symbolID = node.symbol?.identifier.precise else { continue }
Expand Down Expand Up @@ -83,15 +89,17 @@ extension PathHierarchy {
caseSensitive: true,
transformToFileNames: false,
includeDisambiguationForUnambiguousChildren: false,
includeLanguage: false
includeLanguage: false,
allowAdvancedDisambiguation: true
)
}

private func disambiguatedPaths(
caseSensitive: Bool,
transformToFileNames: Bool,
includeDisambiguationForUnambiguousChildren: Bool,
includeLanguage: Bool
includeLanguage: Bool,
allowAdvancedDisambiguation: Bool
) -> [String: String] {
let nameTransform: (String) -> String
if transformToFileNames {
Expand All @@ -111,7 +119,7 @@ extension PathHierarchy {
}, uniquingKeysWith: { $0.merge(with: $1) })

for (_, container) in children {
let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: includeLanguage)
let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: includeLanguage, allowAdvancedDisambiguation: allowAdvancedDisambiguation)
let uniqueNodesWithChildren = Set(disambiguatedChildren.filter { $0.disambiguation.value() != nil && !$0.value.children.isEmpty }.map { $0.value.symbol?.identifier.precise })

for (node, disambiguation) in disambiguatedChildren {
Expand Down Expand Up @@ -182,7 +190,8 @@ extension PathHierarchy.DisambiguationContainer {

static func disambiguatedValues(
for elements: some Sequence<Element>,
includeLanguage: Bool = false
includeLanguage: Bool = false,
allowAdvancedDisambiguation: Bool = true
) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] {
var collisions: [(value: PathHierarchy.Node, disambiguation: Disambiguation)] = []

Expand All @@ -203,6 +212,71 @@ extension PathHierarchy.DisambiguationContainer {
return collisions
}

if allowAdvancedDisambiguation {
// Next, if a symbol returns a tuple with a unique number of values, disambiguate by that (without specifying what those arguments are)
let groupedByReturnCount = [Int?: [Element]](grouping: elements, by: \.returnTypes?.count)
for (returnTypesCount, elements) in groupedByReturnCount {
guard let returnTypesCount = returnTypesCount else { continue }
guard elements.count > 1 else {
// Only one element has this number of return values. Disambiguate with only underscores.
let element = elements.first!
guard remainingIDs.contains(element.node.identifier) else { continue } // Don't disambiguate the same element more than once
collisions.append((value: elements.first!.node, disambiguation: .returnTypes(.init(repeating: "_", count: returnTypesCount))))
remainingIDs.remove(element.node.identifier)
continue
}
guard returnTypesCount > 0 else { continue } // Need at least one return value to disambiguate

for returnTypeIndex in 0..<returnTypesCount {
let grouped = [String: [Element]](grouping: elements, by: { $0.returnTypes![returnTypeIndex] })
for (returnType, elements) in grouped where elements.count == 1 {
// Only one element has this return type
let element = elements.first!
guard remainingIDs.contains(element.node.identifier) else { continue } // Don't disambiguate the same element more than once
var disambiguation = [String](repeating: "_", count: returnTypesCount)
disambiguation[returnTypeIndex] = returnType
collisions.append((value: elements.first!.node, disambiguation: .returnTypes(disambiguation)))
remainingIDs.remove(element.node.identifier)
continue
}
}
}
if remainingIDs.isEmpty {
return collisions
}

let groupedByParameterCount = [Int?: [Element]](grouping: elements, by: \.parameterTypes?.count)
for (parameterTypesCount, elements) in groupedByParameterCount {
guard let parameterTypesCount = parameterTypesCount else { continue }
guard elements.count > 1 else {
// Only one element has this number of parameters. Disambiguate with only underscores.
let element = elements.first!
guard remainingIDs.contains(element.node.identifier) else { continue } // Don't disambiguate the same element more than once
collisions.append((value: elements.first!.node, disambiguation: .parameterTypes(.init(repeating: "_", count: parameterTypesCount))))
remainingIDs.remove(element.node.identifier)
continue
}
guard parameterTypesCount > 0 else { continue } // Need at least one return value to disambiguate

for parameterTypeIndex in 0..<parameterTypesCount {
let grouped = [String: [Element]](grouping: elements, by: { $0.parameterTypes![parameterTypeIndex] })
for (returnType, elements) in grouped where elements.count == 1 {
// Only one element has this return type
let element = elements.first!
guard remainingIDs.contains(element.node.identifier) else { continue } // Don't disambiguate the same element more than once
var disambiguation = [String](repeating: "_", count: parameterTypesCount)
disambiguation[parameterTypeIndex] = returnType
collisions.append((value: elements.first!.node, disambiguation: .parameterTypes(disambiguation)))
remainingIDs.remove(element.node.identifier)
continue
}
}
}
if remainingIDs.isEmpty {
return collisions
}
}

for element in elements where remainingIDs.contains(element.node.identifier) {
collisions.append((value: element.node, disambiguation: element.hash.map { .hash($0) } ?? .none))
}
Expand All @@ -211,19 +285,29 @@ extension PathHierarchy.DisambiguationContainer {

/// Returns all values paired with their disambiguation suffixes.
///
/// - Parameter includeLanguage: Whether or not the kind disambiguation information should include the language, for example: "swift".
func disambiguatedValues(includeLanguage: Bool = false) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] {
/// - Parameters:
/// - includeLanguage: Whether or not the kind disambiguation information should include the language, for example: "swift".
/// - allowAdvancedDisambiguation: Whether or not to support more advanced and more human readable types of disambiguation.
func disambiguatedValues(
includeLanguage: Bool = false,
allowAdvancedDisambiguation: Bool = true
) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] {
if storage.count == 1 {
return [(storage.first!.node, .none)]
}

return Self.disambiguatedValues(for: storage, includeLanguage: includeLanguage)
return Self.disambiguatedValues(for: storage, includeLanguage: includeLanguage, allowAdvancedDisambiguation: allowAdvancedDisambiguation)
}

/// Returns all values paired with their disambiguation suffixes without needing to disambiguate between two different versions of the same symbol.
///
/// - Parameter includeLanguage: Whether or not the kind disambiguation information should include the language, for example: "swift".
func disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: Bool) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] {
/// - Parameters:
/// - includeLanguage: Whether or not the kind disambiguation information should include the language, for example: "swift".
/// - allowAdvancedDisambiguation: Whether or not to support more advanced and more human readable types of disambiguation.
func disambiguatedValuesWithCollapsedUniqueSymbols(
includeLanguage: Bool,
allowAdvancedDisambiguation: Bool
) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] {
typealias DisambiguationPair = (String, String)

var uniqueSymbolIDs = [String: [Element]]()
Expand All @@ -244,24 +328,25 @@ extension PathHierarchy.DisambiguationContainer {

var new = PathHierarchy.DisambiguationContainer()
for element in nonSymbols {
new.add(element.node, kind: element.kind, hash: element.hash)
new.add(element.node, kind: element.kind, hash: element.hash, parameterTypes: element.parameterTypes, returnTypes: element.returnTypes)
}
for (id, symbolDisambiguations) in uniqueSymbolIDs {
let element = symbolDisambiguations.first!
new.add(element.node, kind: element.kind, hash: element.hash)
new.add(element.node, kind: element.kind, hash: element.hash, parameterTypes: element.parameterTypes, returnTypes: element.returnTypes)

if symbolDisambiguations.count > 1 {
duplicateSymbols[id] = symbolDisambiguations.dropFirst()
}
}

var disambiguated = new.disambiguatedValues(includeLanguage: includeLanguage)
var disambiguated = new.disambiguatedValues(includeLanguage: includeLanguage, allowAdvancedDisambiguation: allowAdvancedDisambiguation)
guard !duplicateSymbols.isEmpty else {
return disambiguated
}

for (id, disambiguations) in duplicateSymbols {
let primaryDisambiguation = disambiguated.first(where: { $0.value.symbol?.identifier.precise == id })!.disambiguation

for element in disambiguations {
disambiguated.append((element.node, primaryDisambiguation.updated(kind: element.kind, hash: element.hash)))
}
Expand All @@ -278,14 +363,20 @@ extension PathHierarchy.DisambiguationContainer {
case kind(String)
/// This node is disambiguated by its hash.
case hash(String)

/// This node is disambiguated by its parameter types.
case parameterTypes([String])
/// This node is disambiguated by its return types.
case returnTypes([String])

/// Returns the kind or hash value that disambiguates this node.
func value() -> String! {
switch self {
case .none:
return nil
case .kind(let value), .hash(let value):
return value
default:
return String(makeSuffix().dropFirst())
}
}
/// Makes a new disambiguation suffix string.
Expand All @@ -295,6 +386,14 @@ extension PathHierarchy.DisambiguationContainer {
return ""
case .kind(let value), .hash(let value):
return "-"+value
case .returnTypes(let types):
switch types.count {
case 0: return "->()"
case 1: return "->\(types.first!)"
default: return "->(\(types.joined(separator: ",")))"
}
case .parameterTypes(let types):
return "-(\(types.joined(separator: ",")))"
}
}

Expand All @@ -307,6 +406,8 @@ extension PathHierarchy.DisambiguationContainer {
return kind.map { .kind($0) } ?? self
case .hash:
return hash.map { .hash($0) } ?? self
case .parameterTypes, .returnTypes:
return self
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
// This API isn't exposed anywhere and is only used from a debugger.
#if DEBUG

import SymbolKit

/// A node in a tree structure that can be printed into a visual representation for debugging.
private struct DumpableNode {
var name: String
Expand All @@ -21,20 +23,25 @@ private extension PathHierarchy.Node {
/// Maps the path hierarchy subtree into a representation that can be printed into a visual form for debugging.
func dumpableNode() -> DumpableNode {
// Each node is printed as 3-layer hierarchy with the child names, their kind disambiguation, and their hash disambiguation.

// One layer for the node itself that displays information about the symbol
return DumpableNode(
name: symbol.map { "{ \($0.identifier.precise) : \($0.kind.identifier.identifier) [\(languages.map(\.name).joined(separator: ", "))] }" } ?? "[ \(name) ]",
children: children.sorted(by: \.key).map { (key, disambiguationTree) -> DumpableNode in
name: symbol.map(describe(_:)) ?? "[ \(name) ]",
children: children.sorted(by: \.key).map { (childName, disambiguationTree) -> DumpableNode in

// A second layer that displays the kind disambiguation
let grouped = [String: [PathHierarchy.DisambiguationContainer.Element]](grouping: disambiguationTree.storage, by: { $0.kind ?? "_" })
return DumpableNode(
name: key,
name: childName,
children: grouped.sorted(by: \.key).map { (kind, kindTree) -> DumpableNode in

// A third layer that displays the hash disambiguation
DumpableNode(
name: kind,
children: kindTree.sorted(by: { lhs, rhs in (lhs.hash ?? "_") < (rhs.hash ?? "_") }).map { (element) -> DumpableNode in
DumpableNode(
name: element.hash ?? "_",
children: [element.node.dumpableNode()]
)

// Recursively dump the subtree
DumpableNode(name: element.hash ?? "_", children: [element.node.dumpableNode()])
}
)
}
Expand All @@ -44,6 +51,14 @@ private extension PathHierarchy.Node {
}
}

private func describe(_ symbol: SymbolGraph.Symbol) -> String {
guard let (parameterTypes, returnValueTypes) = PathHierarchy.functionSignatureTypeNames(for: symbol) else {
return "{ \(symbol.identifier.precise) }"
}

return "{ \(symbol.identifier.precise) : (\(parameterTypes.joined(separator: ", "))) -> (\(returnValueTypes.joined(separator: ", "))) }"
}

extension PathHierarchy {
/// Creates a visual representation or the path hierarchy for debugging.
func dump() -> String {
Expand Down