Skip to content

Commit

Permalink
Support disambiguating links with type signature information (#643)
Browse files Browse the repository at this point in the history
* Experimental support for disambiguating links with type signatures

rdar://112224233

* Add a more robust parser for type signature disambiguation in the link

* Support function signature disambiguation for external links

* Fix parsing of subtract operators with parameter type disambiguation

* Only use hash and kind disambiguation in topic references and URLs

* Display function signature in PathHierarchy debug dump

* Update tests for subscripts with type signature disambiguation

* Improve presentation of solutions for ambiguous links on command line

* Update tests to expect the added space in the warning summary

* Extract the full type from the function signature for disambiguation

* Update new code to account for C++ operator parsing logic

* Handle disambiguation in error messages more consistently.

Also, format the error message better in single-line presentation

* Fix new bug where overload groups prevent type disambiguation for one of the overloaded symbols

Also, improve paths for overload groups and other preferred symbols

* Add convenience accessors for inspecting node's special behaviors

* Extract private `onlyIndex(where:)` utility

* Remove accidental print statement

* Reimplement private `typesMatch` helper using `allSatisfy`

* Fix typo and missing info in implementation code comment.

* Create an empty substring using the standard initializer

* Extract common code for disambiguating by type signature

* Change `typeSpellings(for:)` to specify _included_ token kinds

* Add examples of disambiguation string in code comment

* Avoid creating disambiguation string to find `Disambiguation.none`
  • Loading branch information
d-ronnqvist authored Sep 18, 2024
1 parent 02346ee commit f8d16c5
Show file tree
Hide file tree
Showing 23 changed files with 1,708 additions and 327 deletions.

Large diffs are not rendered by default.

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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation
import struct Markdown.SourceRange
import struct Markdown.SourceLocation
import SymbolKit
Expand Down Expand Up @@ -80,10 +81,38 @@ extension PathHierarchy.Error {
/// - fullNameOfNode: A closure that determines the full name of a node, to be displayed in collision diagnostics to precisely identify symbols and other pages.
/// - Note: `Replacement`s produced by this function use `SourceLocation`s relative to the link text excluding its surrounding syntax.
func makeTopicReferenceResolutionErrorInfo(fullNameOfNode: (PathHierarchy.Node) -> String) -> TopicReferenceResolutionErrorInfo {
// Both `.unknownDisambiguation(...)` and `.lookupCollisions(...)` create solutions on the same format from the same information.
// This is defined inline because it captures `fullNameOfNode`.
func collisionIsBefore(_ lhs: (node: PathHierarchy.Node, disambiguation: String), _ rhs: (node: PathHierarchy.Node, disambiguation: String)) -> Bool {
return fullNameOfNode(lhs.node) + lhs.disambiguation
< fullNameOfNode(rhs.node) + rhs.disambiguation
func makeCollisionSolutions(
from candidates: [(node: PathHierarchy.Node, disambiguation: String)],
nextPathComponent: PathHierarchy.PathComponent,
partialResultPrefix: Substring
) -> (
pathPrefix: Substring,
foundDisambiguation: Substring,
solutions: [Solution]
) {
let pathPrefix = partialResultPrefix + nextPathComponent.name
let foundDisambiguation = nextPathComponent.full.dropFirst(nextPathComponent.name.count)
let replacementRange = SourceRange.makeRelativeRange(startColumn: pathPrefix.count, length: foundDisambiguation.count)

let solutions: [Solution] = candidates
.map { (fullName: fullNameOfNode($0.node), disambiguation: $0.disambiguation) }
.sorted { lhs, rhs in
// Sort by name first and disambiguation second
if lhs.fullName == rhs.fullName {
return lhs.disambiguation < rhs.disambiguation
}
return lhs.fullName < rhs.fullName
}
.map { (fullName: String, suggestedDisambiguation: String) -> Solution in
// In contexts that display the solution message on a single line by removing newlines, this extra whitespace makes it look correct ─────────────╮
// ▼
return Solution(summary: "\(Self.replacementOperationDescription(from: foundDisambiguation, to: suggestedDisambiguation, forCollision: true)) for \n\(fullName.singleQuoted)", replacements: [
Replacement(range: replacementRange, replacement: suggestedDisambiguation)
])
}
return (pathPrefix, foundDisambiguation, solutions)
}

switch self {
Expand Down Expand Up @@ -127,7 +156,7 @@ extension PathHierarchy.Error {

case .unfindableMatch(let node):
return TopicReferenceResolutionErrorInfo("""
\(node.name.singleQuoted) can't be linked to in a partial documentation build
\(node.name.singleQuoted) can't be linked to in a partial documentation build
""")

case .nonSymbolMatchForSymbolLink(path: let path):
Expand All @@ -142,24 +171,13 @@ extension PathHierarchy.Error {

case .unknownDisambiguation(partialResult: let partialResult, remaining: let remaining, candidates: let candidates):
let nextPathComponent = remaining.first!
let validPrefix = partialResult.pathPrefix + nextPathComponent.name

let disambiguations = nextPathComponent.full.dropFirst(nextPathComponent.name.count)
let replacementRange = SourceRange.makeRelativeRange(startColumn: validPrefix.count, length: disambiguations.count)

let solutions: [Solution] = candidates
.sorted(by: collisionIsBefore)
.map { (node: PathHierarchy.Node, disambiguation: String) -> Solution in
return Solution(summary: "\(Self.replacementOperationDescription(from: disambiguations, to: disambiguation)) for\n\(fullNameOfNode(node).singleQuoted)", replacements: [
Replacement(range: replacementRange, replacement: disambiguation)
])
}
let (pathPrefix, foundDisambiguation, solutions) = makeCollisionSolutions(from: candidates, nextPathComponent: nextPathComponent, partialResultPrefix: partialResult.pathPrefix)

return TopicReferenceResolutionErrorInfo("""
\(disambiguations.dropFirst().singleQuoted) isn't a disambiguation for \(nextPathComponent.name.singleQuoted) at \(partialResult.node.pathWithoutDisambiguation().singleQuoted)
\(foundDisambiguation.dropFirst().singleQuoted) isn't a disambiguation for \(nextPathComponent.name.singleQuoted) at \(partialResult.node.pathWithoutDisambiguation().singleQuoted)
""",
solutions: solutions,
rangeAdjustment: .makeRelativeRange(startColumn: validPrefix.count, length: disambiguations.count)
rangeAdjustment: .makeRelativeRange(startColumn: pathPrefix.count, length: foundDisambiguation.count)
)

case .unknownName(partialResult: let partialResult, remaining: let remaining, availableChildren: let availableChildren):
Expand Down Expand Up @@ -201,17 +219,7 @@ extension PathHierarchy.Error {

case .lookupCollision(partialResult: let partialResult, remaining: let remaining, collisions: let collisions):
let nextPathComponent = remaining.first!

let pathPrefix = partialResult.pathPrefix + nextPathComponent.name

let disambiguations = nextPathComponent.full.dropFirst(nextPathComponent.name.count)
let replacementRange = SourceRange.makeRelativeRange(startColumn: pathPrefix.count, length: disambiguations.count)

let solutions: [Solution] = collisions.sorted(by: collisionIsBefore).map { (node: PathHierarchy.Node, disambiguation: String) -> Solution in
return Solution(summary: "\(Self.replacementOperationDescription(from: disambiguations.dropFirst(), to: disambiguation)) for\n\(fullNameOfNode(node).singleQuoted)", replacements: [
Replacement(range: replacementRange, replacement: "-" + disambiguation)
])
}
let (pathPrefix, _, solutions) = makeCollisionSolutions(from: collisions, nextPathComponent: nextPathComponent, partialResultPrefix: partialResult.pathPrefix)

return TopicReferenceResolutionErrorInfo("""
\(nextPathComponent.full.singleQuoted) is ambiguous at \(partialResult.node.pathWithoutDisambiguation().singleQuoted)
Expand All @@ -222,14 +230,25 @@ extension PathHierarchy.Error {
}
}

private static func replacementOperationDescription(from: some StringProtocol, to: some StringProtocol) -> String {
private static func replacementOperationDescription(from: some StringProtocol, to: some StringProtocol, forCollision: Bool = false) -> String {
if from.isEmpty {
return "Insert \(to.singleQuoted)"
}
if to.isEmpty {
return "Remove \(from.singleQuoted)"
}
return "Replace \(from.singleQuoted) with \(to.singleQuoted)"

guard forCollision else {
return "Replace \(from.singleQuoted) with \(to.singleQuoted)"
}

if to.hasPrefix("->") || from.hasPrefix("->") {
// If either the "to" or "from" descriptions are a return type disambiguation, include the full arrow for both.
// Only a ">" prefix doesn't read as an "arrow", and it looks incorrect when only of the descriptions have a "-" prefix.
return "Replace \(from.singleQuoted) with \(to.singleQuoted)"
}
// For other replacement descriptions, drop the leading "-" to focus on the text that's different.
return "Replace \(from.dropFirst().singleQuoted) with \(to.dropFirst().singleQuoted)"
}
}

Expand Down
Loading

0 comments on commit f8d16c5

Please sign in to comment.