Skip to content

Commit 1357a2d

Browse files
Add logic to look for overload groups when emitting warnings (#885)
* Add logic to look for overload groups when emitting warnings and suggestions about ambiguous symbols. If exactly one of the ambiguous symbols is an overload group, then suggest the author simply remove the incorrect disambiguation. * Refactor logic for ambiguous symbol warnings to first emit a message about selecting the preferred symbol without any disambiguation hash, followed by a list of all the remaining symbols to select from with their hashes. * PR Feedback: - PathHierarchy.makePartialResultError checks for a preferred symbol, normally an overload group, when throwing processing unknownDisambiguation errors. - Simplify makeTopicReferenceResolutionErrorInfo and limit it to formatting the warning message. * PR Feedback: Simplify PathHierarchy+Error.swift and PathHierarchy.makePartialResultError further. * PR Feedback: - Further simplify logic for displaying unknown disambiguation warnings
1 parent a87f610 commit 1357a2d

File tree

5 files changed

+68
-26
lines changed

5 files changed

+68
-26
lines changed

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Error.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ extension PathHierarchy.Error {
150150
let solutions: [Solution] = candidates
151151
.sorted(by: collisionIsBefore)
152152
.map { (node: PathHierarchy.Node, disambiguation: String) -> Solution in
153-
return Solution(summary: "\(Self.replacementOperationDescription(from: disambiguations.dropFirst(), to: disambiguation)) for\n\(fullNameOfNode(node).singleQuoted)", replacements: [
154-
Replacement(range: replacementRange, replacement: "-" + disambiguation)
153+
return Solution(summary: "\(Self.replacementOperationDescription(from: disambiguations, to: disambiguation)) for\n\(fullNameOfNode(node).singleQuoted)", replacements: [
154+
Replacement(range: replacementRange, replacement: disambiguation)
155155
])
156156
}
157157

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -355,29 +355,52 @@ extension PathHierarchy {
355355
remaining: ArraySlice<PathComponent>,
356356
rawPathForError: String
357357
) -> Error {
358-
if let disambiguationTree = node.children[String(remaining.first!.name)] {
359-
return Error.unknownDisambiguation(
358+
guard let disambiguationTree = node.children[String(remaining.first!.name)] else {
359+
return Error.unknownName(
360360
partialResult: (
361361
node,
362362
pathForError(of: rawPathForError, droppingLast: remaining.count)
363363
),
364364
remaining: Array(remaining),
365-
candidates: disambiguationTree.disambiguatedValues().map {
366-
(node: $0.value, disambiguation: String($0.disambiguation.makeSuffix().dropFirst()))
367-
}
365+
availableChildren: Set(node.children.keys)
368366
)
369367
}
370-
371-
return Error.unknownName(
368+
369+
// Use a empty disambiguation suffix for the preferred symbol, if there
370+
// is one, which will trigger the warning to suggest removing the
371+
// suffix entirely.
372+
let candidates = disambiguationTree.disambiguatedValues()
373+
let favoredSuffix = favoredSuffix(from: candidates)
374+
let suffixes = candidates.map { $0.disambiguation.makeSuffix() }
375+
let candidatesAndSuffixes = zip(candidates, suffixes).map { (candidate, suffix) in
376+
if suffix == favoredSuffix {
377+
return (node: candidate.value, disambiguation: "")
378+
} else {
379+
return (node: candidate.value, disambiguation: suffix)
380+
}
381+
}
382+
return Error.unknownDisambiguation(
372383
partialResult: (
373384
node,
374385
pathForError(of: rawPathForError, droppingLast: remaining.count)
375386
),
376387
remaining: Array(remaining),
377-
availableChildren: Set(node.children.keys)
388+
candidates: candidatesAndSuffixes
378389
)
379390
}
380-
391+
392+
/// Check if exactly one of the given candidate symbols is preferred, because it is not disfavored
393+
/// for link resolution and all the other symbols are.
394+
/// - Parameters:
395+
/// - from: An array of candidate node and disambiguation tuples.
396+
/// - Returns: An optional string set to the disambiguation suffix string, without the hyphen separator e.g. "abc123",
397+
/// or nil if there is no preferred symbol.
398+
private func favoredSuffix(from candidates: [(value: PathHierarchy.Node, disambiguation: PathHierarchy.DisambiguationContainer.Disambiguation)]) -> String? {
399+
return candidates.singleMatch({
400+
!$0.value.specialBehaviors.contains(PathHierarchy.Node.SpecialBehaviors.disfavorInLinkCollision)
401+
})?.disambiguation.makeSuffix()
402+
}
403+
381404
private func pathForError(
382405
of rawPath: String,
383406
droppingLast trailingComponentsToDrop: Int

Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ final class PathHierarchyBasedLinkResolver {
251251
func fullName(of node: PathHierarchy.Node, in context: DocumentationContext) -> String {
252252
guard let identifier = node.identifier else { return node.name }
253253
if let symbol = node.symbol {
254-
if let fragments = symbol.declarationFragments {
254+
// Use the simple title for overload group symbols to avoid showing detailed type info
255+
if !symbol.isOverloadGroup, let fragments = symbol.declarationFragments {
255256
return fragments.map(\.spelling).joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ")
256257
}
257258
return symbol.names.title

Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,8 @@ class ExternalPathHierarchyResolverTests: XCTestCase {
379379
authoredLink: "/MixedFramework/CollisionsWithDifferentKinds/something-class",
380380
errorMessage: "'class' isn't a disambiguation for 'something' at '/MixedFramework/CollisionsWithDifferentKinds'",
381381
solutions: [
382-
.init(summary: "Replace 'class' with 'enum.case' for\n'case something'", replacement: ("-enum.case", 54, 60)),
383-
.init(summary: "Replace 'class' with 'property' for\n'var something: String { get }'", replacement: ("-property", 54, 60)),
382+
.init(summary: "Replace '-class' with '-enum.case' for\n'case something'", replacement: ("-enum.case", 54, 60)),
383+
.init(summary: "Replace '-class' with '-property' for\n'var something: String { get }'", replacement: ("-property", 54, 60)),
384384
]
385385
)
386386

@@ -407,9 +407,9 @@ class ExternalPathHierarchyResolverTests: XCTestCase {
407407
authoredLink: "/MixedFramework/CollisionsWithEscapedKeywords/init()-abc123",
408408
errorMessage: "'abc123' isn't a disambiguation for 'init()' at '/MixedFramework/CollisionsWithEscapedKeywords'",
409409
solutions: [
410-
.init(summary: "Replace 'abc123' with 'method' for\n'func `init`()'", replacement: ("-method", 52, 59)),
411-
.init(summary: "Replace 'abc123' with 'init' for\n'init()'", replacement: ("-init", 52, 59)),
412-
.init(summary: "Replace 'abc123' with 'type.method' for\n'static func `init`()'", replacement: ("-type.method", 52, 59)),
410+
.init(summary: "Replace '-abc123' with '-method' for\n'func `init`()'", replacement: ("-method", 52, 59)),
411+
.init(summary: "Replace '-abc123' with '-init' for\n'init()'", replacement: ("-init", 52, 59)),
412+
.init(summary: "Replace '-abc123' with '-type.method' for\n'static func `init`()'", replacement: ("-type.method", 52, 59)),
413413
]
414414
)
415415
// Providing disambiguation will narrow down the suggestions. Note that `()` is missing in the last path component
@@ -469,8 +469,8 @@ class ExternalPathHierarchyResolverTests: XCTestCase {
469469
authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-abc123",
470470
errorMessage: "'abc123' isn't a disambiguation for 'something(argument:)' at '/MixedFramework/CollisionsWithDifferentFunctionArguments'",
471471
solutions: [
472-
.init(summary: "Replace 'abc123' with '1cyvp' for\n'func something(argument: Int) -> Int'", replacement: ("-1cyvp", 77, 84)),
473-
.init(summary: "Replace 'abc123' with '2vke2' for\n'func something(argument: String) -> Int'", replacement: ("-2vke2", 77, 84)),
472+
.init(summary: "Replace '-abc123' with '-1cyvp' for\n'func something(argument: Int) -> Int'", replacement: ("-1cyvp", 77, 84)),
473+
.init(summary: "Replace '-abc123' with '-2vke2' for\n'func something(argument: String) -> Int'", replacement: ("-2vke2", 77, 84)),
474474
]
475475
)
476476
// Providing disambiguation will narrow down the suggestions. Note that `argument` label is missing in the last path component

Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@ class PathHierarchyTests: XCTestCase {
327327
'class' isn't a disambiguation for 'something' at '/MixedFramework/CollisionsWithDifferentKinds'
328328
""") { error in
329329
XCTAssertEqual(error.solutions, [
330-
.init(summary: "Replace 'class' with 'enum.case' for\n'case something'", replacements: [("-enum.case", 54, 60)]),
331-
.init(summary: "Replace 'class' with 'property' for\n'var something: String { get }'", replacements: [("-property", 54, 60)]),
330+
.init(summary: "Replace '-class' with '-enum.case' for\n'case something'", replacements: [("-enum.case", 54, 60)]),
331+
.init(summary: "Replace '-class' with '-property' for\n'var something: String { get }'", replacements: [("-property", 54, 60)]),
332332
])
333333
}
334334

@@ -350,9 +350,9 @@ class PathHierarchyTests: XCTestCase {
350350
'abc123' isn't a disambiguation for 'init()' at '/MixedFramework/CollisionsWithEscapedKeywords'
351351
""") { error in
352352
XCTAssertEqual(error.solutions, [
353-
.init(summary: "Replace 'abc123' with 'method' for\n'func `init`()'", replacements: [("-method", 52, 59)]),
354-
.init(summary: "Replace 'abc123' with 'init' for\n'init()'", replacements: [("-init", 52, 59)]),
355-
.init(summary: "Replace 'abc123' with 'type.method' for\n'static func `init`()'", replacements: [("-type.method", 52, 59)]),
353+
.init(summary: "Replace '-abc123' with '-method' for\n'func `init`()'", replacements: [("-method", 52, 59)]),
354+
.init(summary: "Replace '-abc123' with '-init' for\n'init()'", replacements: [("-init", 52, 59)]),
355+
.init(summary: "Replace '-abc123' with '-type.method' for\n'static func `init`()'", replacements: [("-type.method", 52, 59)]),
356356
])
357357
}
358358
try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithEscapedKeywords/init()", in: tree, context: context, expectedErrorMessage: """
@@ -431,8 +431,8 @@ class PathHierarchyTests: XCTestCase {
431431
'abc123' isn't a disambiguation for 'something(argument:)' at '/MixedFramework/CollisionsWithDifferentFunctionArguments'
432432
""") { error in
433433
XCTAssertEqual(error.solutions, [
434-
.init(summary: "Replace 'abc123' with '1cyvp' for\n'func something(argument: Int) -> Int'", replacements: [("-1cyvp", 77, 84)]),
435-
.init(summary: "Replace 'abc123' with '2vke2' for\n'func something(argument: String) -> Int'", replacements: [("-2vke2", 77, 84)]),
434+
.init(summary: "Replace '-abc123' with '-1cyvp' for\n'func something(argument: Int) -> Int'", replacements: [("-1cyvp", 77, 84)]),
435+
.init(summary: "Replace '-abc123' with '-2vke2' for\n'func something(argument: String) -> Int'", replacements: [("-2vke2", 77, 84)]),
436436
])
437437
}
438438
// Providing disambiguation will narrow down the suggestions. Note that `argument` label is missing in the last path component
@@ -1318,6 +1318,24 @@ class PathHierarchyTests: XCTestCase {
13181318
// This overloaded protocol method should be able to resolve without a suffix at all, since it doesn't conflict with anything
13191319
let overloadedProtocolMethod = try tree.findNode(path: "/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)", onlyFindSymbols: true)
13201320
XCTAssert(overloadedProtocolMethod.symbol?.identifier.precise.hasSuffix(SymbolGraph.Symbol.overloadGroupIdentifierSuffix) == true)
1321+
1322+
}
1323+
1324+
func testAmbiguousPathsForOverloadedGroupSymbols() throws {
1325+
enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled)
1326+
let (_, context) = try testBundleAndContext(named: "OverloadedSymbols")
1327+
let tree = context.linkResolver.localResolver.pathHierarchy
1328+
try assertPathRaisesErrorMessage("/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-abc123", in: tree, context: context, expectedErrorMessage: """
1329+
'abc123' isn't a disambiguation for 'fourthTestMemberName(test:)' at '/ShapeKit/OverloadedProtocol'
1330+
""") { error in
1331+
XCTAssertEqual(error.solutions, [
1332+
.init(summary: "Remove '-abc123' for\n'fourthTestMemberName(test:)'", replacements: [("", 56, 63)]),
1333+
.init(summary: "Replace '-abc123' with '-8iuz7' for\n'func fourthTestMemberName(test: String) -> Double\'", replacements: [("-8iuz7", 56, 63)]),
1334+
.init(summary: "Replace '-abc123' with '-1h173' for\n'func fourthTestMemberName(test: String) -> Float\'", replacements: [("-1h173", 56, 63)]),
1335+
.init(summary: "Replace '-abc123' with '-91hxs' for\n'func fourthTestMemberName(test: String) -> Int\'", replacements: [("-91hxs", 56, 63)]),
1336+
.init(summary: "Replace '-abc123' with '-961zx' for\n'func fourthTestMemberName(test: String) -> String\'", replacements: [("-961zx", 56, 63)]),
1337+
])
1338+
}
13211339
}
13221340

13231341
func testDoesNotSuggestBundleNameForSymbolLink() throws {

0 commit comments

Comments
 (0)