-
Notifications
You must be signed in to change notification settings - Fork 128
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
Use only canonical paths for symbol breadcrumbs #1081
Changes from 10 commits
d44e403
c1c4062
346bb8c
603ffe1
cc1e626
8647adf
e63244e
0468927
1e4a296
d78cdd2
4c2acd4
7b432d0
759de67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/* | ||
This source file is part of the Swift.org open source project | ||
|
||
Copyright (c) 2024 Apple Inc. and the Swift project authors | ||
Licensed under Apache License v2.0 with Runtime Library Exception | ||
|
||
See https://swift.org/LICENSE.txt for license information | ||
See https://swift.org/CONTRIBUTORS.txt for Swift project authors | ||
*/ | ||
|
||
import Foundation | ||
import SymbolKit | ||
|
||
extension PathHierarchyBasedLinkResolver { | ||
|
||
/// | ||
/// Finds the canonical path, also called "breadcrumbs", to the given symbol in the path hierarchy. | ||
/// The path is a list of references that describe a walk through the path hierarchy descending from the module down to, but not including, the given `reference`. | ||
/// | ||
/// - Parameters: | ||
/// - reference: The symbol reference to find the canonical path to. | ||
/// - sourceLanguage: The source language representation of the symbol to fin the canonical path for. | ||
/// - Returns: The canonical path to the given symbol reference, or `nil` if the reference isn't a symbol or if the symbol doesn't have a representation in the given source language. | ||
func breadcrumbs(of reference: ResolvedTopicReference, in sourceLanguage: SourceLanguage) -> [ResolvedTopicReference]? { | ||
guard let nodeID = resolvedReferenceMap[reference] else { return nil } | ||
var node = pathHierarchy.lookup[nodeID]! // Only the path hierarchy can create its IDs and a created ID always matches a node | ||
|
||
func matchesRequestedLanguage(_ node: PathHierarchy.Node) -> Bool { | ||
guard let symbol = node.symbol, | ||
let language = SourceLanguage(knownLanguageIdentifier: symbol.identifier.interfaceLanguage) | ||
else { | ||
return false | ||
} | ||
return language == sourceLanguage | ||
} | ||
|
||
if !matchesRequestedLanguage(node) { | ||
guard let counterpart = node.counterpart, matchesRequestedLanguage(counterpart) else { | ||
// Neither this symbol, nor its counterpart matched the requested language | ||
return nil | ||
} | ||
// Traverse from the counterpart instead because it matches the requested language | ||
node = counterpart | ||
} | ||
|
||
// Traverse up the hierarchy and gather each reference | ||
return sequence(first: node, next: \.parent) | ||
// The hierarchy traversal happened from the starting point up, but the callers of `breadcrumbs(of:in:)` | ||
// expect paths going from the root page, excluding the starting point itself (already dropped above). | ||
// To match the caller's expectations we remove the starting point and then flip the paths. | ||
.dropFirst().reversed() | ||
.compactMap { | ||
// Ignore any "unfindable" or "sparse" nodes resulting from a "partial" symbol graph. | ||
// | ||
// When the `ConvertService` builds documentation for a single symbol with multiple path components, | ||
// the path hierarchy fills in unfindable nodes for the other path components to construct a connected hierarchy. | ||
// | ||
// These unfindable nodes can be traversed up and down, but are themselves considered to be "not found". | ||
$0.identifier.flatMap { resolvedReferenceMap[$0] } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we assert on any There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, but I can add a comment that mentions why The way that the ConvertService builds documentation for a single symbol may result in nodes in the hierarchy which doesn't correspond to any real symbols. The PathHierarchy internally refers to these as "sparse nodes". They only exist to construct a tree structure but links to them can't resolve. The ConvertService doesn't create any breadcrumbs, so handling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a code comment in 8647adf |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/* | ||
This source file is part of the Swift.org open source project | ||
|
||
Copyright (c) 2024 Apple Inc. and the Swift project authors | ||
Licensed under Apache License v2.0 with Runtime Library Exception | ||
|
||
See https://swift.org/LICENSE.txt for license information | ||
See https://swift.org/CONTRIBUTORS.txt for Swift project authors | ||
*/ | ||
|
||
import XCTest | ||
@testable import SwiftDocC | ||
|
||
class SymbolBreadcrumbTests: XCTestCase { | ||
func testLanguageSpecificBreadcrumbs() throws { | ||
let (_, context) = try testBundleAndContext(named: "GeometricalShapes") | ||
let resolver = try XCTUnwrap(context.linkResolver.localResolver) | ||
let moduleReference = try XCTUnwrap(context.soleRootModuleReference) | ||
|
||
// typedef struct { | ||
// CGPoint center; | ||
// CGFloat radius; | ||
// } TLACircle NS_SWIFT_NAME(Circle); | ||
do { | ||
let reference = moduleReference.appendingPath("Circle/center") | ||
|
||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .swift)?.map(\.path), [ | ||
"/documentation/GeometricalShapes", | ||
"/documentation/GeometricalShapes/Circle", | ||
]) | ||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .objectiveC)?.map(\.path), [ | ||
"/documentation/GeometricalShapes", | ||
"/documentation/GeometricalShapes/Circle", // named TLACircle in Objective-C | ||
]) | ||
} | ||
|
||
// extern const TLACircle TLACircleZero NS_SWIFT_NAME(Circle.zero); | ||
do { | ||
let reference = moduleReference.appendingPath("Circle/zero") | ||
|
||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .swift)?.map(\.path), [ | ||
"/documentation/GeometricalShapes", | ||
"/documentation/GeometricalShapes/Circle", // The Swift representation is a member | ||
]) | ||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .objectiveC)?.map(\.path), [ | ||
"/documentation/GeometricalShapes", // The Objective-C representation is a top-level function | ||
]) | ||
} | ||
|
||
// BOOL TLACircleIntersects(TLACircle circle, TLACircle otherCircle) NS_SWIFT_NAME(Circle.intersects(self:_:)); | ||
do { | ||
let reference = moduleReference.appendingPath("Circle/intersects(_:)") | ||
|
||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .swift)?.map(\.path), [ | ||
"/documentation/GeometricalShapes", | ||
"/documentation/GeometricalShapes/Circle", // The Swift representation is a member | ||
]) | ||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .objectiveC)?.map(\.path), [ | ||
"/documentation/GeometricalShapes", // The Objective-C representation is a top-level function | ||
]) | ||
} | ||
|
||
// TLACircle TLACircleMake(CGPoint center, CGFloat radius) NS_SWIFT_UNAVAILABLE("Use 'Circle.init(center:radius:)' instead."); | ||
do { | ||
let reference = moduleReference.appendingPath("TLACircleMake") | ||
|
||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .swift)?.map(\.path), nil) // There is no Swift representation | ||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .objectiveC)?.map(\.path), [ | ||
"/documentation/GeometricalShapes", // The Objective-C representation is a top-level function | ||
]) | ||
} | ||
|
||
do { | ||
let reference = moduleReference.appendingPath("Circle/init(center:radius:)") | ||
|
||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .swift)?.map(\.path), [ | ||
"/documentation/GeometricalShapes", | ||
"/documentation/GeometricalShapes/Circle", // The Swift representation is a member | ||
]) | ||
XCTAssertEqual(resolver.breadcrumbs(of: reference, in: .objectiveC)?.map(\.path), nil) // There is no Objective-C representation | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use a singular name for this since it only returns a single breadcrumb, not a list of breadcrumbs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I consider a path to be a list of breadcrumbs and a single breadcrumb to only be one path component.
I see that I used singular "breadcrumb" in the documentation comment. I'll update that documentation to say "breadcrumbs"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed to plural "breadcrumbs" in the documentation comment in cc1e626