Skip to content

Commit e9fd5f4

Browse files
committed
remove changes relevant to absolute paths and shadowing
1 parent 3b1d814 commit e9fd5f4

File tree

5 files changed

+81
-192
lines changed

5 files changed

+81
-192
lines changed

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,8 @@ final class DocumentationCacheBasedLinkResolver {
7878
func referenceFor(absoluteSymbolPath path: String, parent: ResolvedTopicReference) -> ResolvedTopicReference? {
7979
// Check if `destination` is a known absolute reference URL.
8080
if let match = referencesIndex[path] { return match }
81-
82-
// Check if `destination` is a known absolute symbol path...
83-
if !path.hasPrefix("/") && parent.pathComponents.count > 2 {
84-
// ...in the parent's module
85-
let parentModule = parent.pathComponents[2]
86-
let referenceURLString = "doc://\(parent.bundleIdentifier)/documentation/\(parentModule)/\(path)"
87-
if let reference = referencesIndex[referenceURLString] {
88-
return reference
89-
}
90-
}
91-
// ...globally
81+
82+
// Check if `destination` is a known absolute symbol path.
9283
let referenceURLString = "doc://\(parent.bundleIdentifier)/documentation/\(path.hasPrefix("/") ? String(path.dropFirst()) : path)"
9384
return referencesIndex[referenceURLString]
9485
}
@@ -308,6 +299,8 @@ final class DocumentationCacheBasedLinkResolver {
308299
}
309300

310301

302+
// MARK: Symbol reference creation
303+
311304
/// Returns a map between symbol identifiers and topic references.
312305
///
313306
/// - Parameters:

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

Lines changed: 25 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -305,36 +305,17 @@ struct PathHierarchy {
305305
}
306306

307307
private func findNode(path rawPath: String, parent: ResolvedIdentifier?, onlyFindSymbols: Bool) throws -> Node {
308+
// The search for a documentation element can be though of as 3 steps:
308309
// First, parse the path into structured path components.
309310
let (path, isAbsolute) = Self.parse(path: rawPath)
310311
guard !path.isEmpty else {
311312
throw Error.notFound(availableChildren: [])
312313
}
313314

314-
// Second, we try to the node matching the path. This is done by first finding
315-
// the root of the (possibly relative) path and then searching for the child
316-
// from that root.
317-
// A relative path could have multiple root candidates (where the first
318-
// component is a match). We start searching at the `parent`, working
319-
// our way up the tree, trying to find a child for each root candidate.
320-
// This function reports all errors found on the way and - if successful - the
321-
// matching node.
322-
let (node, errors) = try searchForChildOnAllPossibleRoots(parentID: parent, path: path, isAbsolute: isAbsolute, onlyFindSymbols: onlyFindSymbols)
323-
324-
if let node = node {
325-
return node
326-
}
327-
328-
// Currently, we only report the first error, which corresponds to
329-
// the root candidate closest to the `parent`. Aggregating errors could
330-
// help giving more precise suggestions in the future.
331-
throw errors.first!
332-
}
333-
334-
/// Tries to find a node in the subtree of `node` where `remaining` is the relative path from `node` to the child.
335-
private func findChild(of node: Node, remaining: ArraySlice<PathComponent>) throws -> Node {
336-
var node = node
337-
var remaining = remaining
315+
// Second, find the node to start the search relative to.
316+
// This may consume or or more path components. See implementation for details.
317+
var remaining = path[...]
318+
var node = try findRoot(parentID: parent, remaining: &remaining, isAbsolute: isAbsolute, onlyFindSymbols: onlyFindSymbols)
338319

339320
// Third, search for the match relative to the start node.
340321
if remaining.isEmpty {
@@ -458,13 +439,11 @@ struct PathHierarchy {
458439
///
459440
/// - Parameters:
460441
/// - parentID: An optional ID of the node to start the search relative to.
461-
/// - path: The parsed path components.
442+
/// - remaining: The parsed path components.
462443
/// - isAbsolute: If the parsed path represent an absolute documentation link.
463444
/// - onlyFindSymbols: If symbol results are required.
464445
/// - Returns: The node to start the relative search relative to.
465-
private func searchForChildOnAllPossibleRoots(parentID: ResolvedIdentifier?, path: [PathComponent], isAbsolute: Bool, onlyFindSymbols: Bool) throws -> (Node?, [Error]) {
466-
var remaining = path[...]
467-
446+
private func findRoot(parentID: ResolvedIdentifier?, remaining: inout ArraySlice<PathComponent>, isAbsolute: Bool, onlyFindSymbols: Bool) throws -> Node {
468447
// If the first path component is "tutorials" or "documentation" then that
469448
let isKnownTutorialPath = remaining.first!.full == "tutorials"
470449
let isKnownDocumentationPath = remaining.first!.full == "documentation"
@@ -487,34 +466,29 @@ struct PathHierarchy {
487466
}
488467
}
489468
remaining = remaining.dropFirst()
490-
return (try findChild(of: articlesContainer, remaining: remaining) , [])
469+
return articlesContainer
491470
} else if articlesContainer.children.keys.contains(component.name) || articlesContainer.children.keys.contains(component.full) {
492-
return (try findChild(of: articlesContainer, remaining: remaining) , [])
471+
return articlesContainer
493472
}
494473
}
495474
if !isKnownDocumentationPath {
496475
if tutorialContainer.name == component.name || tutorialContainer.name == component.full {
497476
remaining = remaining.dropFirst()
498-
return (try findChild(of: tutorialContainer, remaining: remaining) , [])
477+
return tutorialContainer
499478
} else if tutorialContainer.children.keys.contains(component.name) || tutorialContainer.children.keys.contains(component.full) {
500-
return (try findChild(of: tutorialContainer, remaining: remaining) , [])
479+
return tutorialContainer
501480
}
502481
// The parent for tutorial overviews / technologies is "tutorials" which has already been removed above, so no need to check against that name.
503482
else if tutorialOverviewContainer.children.keys.contains(component.name) || tutorialOverviewContainer.children.keys.contains(component.full) {
504-
return (try findChild(of: tutorialOverviewContainer, remaining: remaining) , [])
483+
return tutorialOverviewContainer
505484
}
506485
}
507-
}
508-
509-
if !isKnownTutorialPath && isAbsolute {
510-
// If this is an absolute non-tutorial link, then the first component will be a module name.
511-
if let matched = modules[component.name] ?? modules[component.full] {
512-
remaining = remaining.dropFirst()
513-
return (try findChild(of: matched, remaining: remaining) , [])
514-
} else {
515-
// This is an absolute path that doesn't start with a valid module. Don't continue the search
516-
// in relative mode.
517-
throw Error.notFound(availableChildren: Array(modules.keys))
486+
if !isKnownTutorialPath && isAbsolute {
487+
// If this is an absolute non-tutorial link, then the first component will be a module name.
488+
if let matched = modules[component.name] ?? modules[component.full] {
489+
remaining = remaining.dropFirst()
490+
return matched
491+
}
518492
}
519493
}
520494

@@ -529,73 +503,37 @@ struct PathHierarchy {
529503
}
530504

531505
if let parentID = parentID {
532-
// We're dealing with a relative path, so search will be a bit more complicated.
533-
// Starting from the parent, we ascend in the tree trying to find a node that matches
534-
// our search path's first component. If we find one, we try to obtain the descendant
535-
// matching the remainder of the search path using `findChild(of:remaining:)`. If that
536-
// fails, we continue the search up the tree, after we've saved the error to be returned
537-
// later.
538-
539-
// Errors collected during the process
540-
var errors: [Error] = []
541-
542506
// If a parent ID was provided, start at that node and continue up the hierarchy until that node has a child that matches the first path components name.
543507
var parentNode = lookup[parentID]!
544508
let firstComponent = remaining.first!
545509
if matches(node: parentNode, component: firstComponent) {
546510
remaining = remaining.dropFirst()
547-
do {
548-
return (try findChild(of: parentNode, remaining: remaining), errors)
549-
} catch let error as Error {
550-
errors.append(error)
551-
}
511+
return parentNode
552512
}
553-
while true {
554-
if parentNode.children.keys.contains(firstComponent.name) || parentNode.children.keys.contains(firstComponent.full) {
555-
do {
556-
return (try findChild(of: parentNode, remaining: remaining), errors)
557-
} catch let error as Error {
558-
errors.append(error)
559-
}
560-
}
561-
513+
while !parentNode.children.keys.contains(firstComponent.name) && !parentNode.children.keys.contains(firstComponent.full) {
562514
guard let parent = parentNode.parent else {
563515
if matches(node: parentNode, component: firstComponent){
564516
remaining = remaining.dropFirst()
565-
do {
566-
return (try findChild(of: parentNode, remaining: remaining), errors)
567-
} catch let error as Error {
568-
errors.append(error)
569-
}
517+
return parentNode
570518
}
571519
if let matched = modules[component.name] ?? modules[component.full] {
572520
remaining = remaining.dropFirst()
573-
do {
574-
return (try findChild(of: matched, remaining: remaining), errors)
575-
} catch let error as Error {
576-
errors.append(error)
577-
}
521+
return matched
578522
}
579-
580523
// No node up the hierarchy from the provided parent has a child that matches the first path component.
581524
// Go back to the provided parent node for diagnostic information about its available children.
582525
parentNode = lookup[parentID]!
583-
584-
// We've reached the top of the tree...we return all the errors we obtained in the process along with
585-
// the final error providing the partial result.
586-
587-
errors.append(Error.partialResult(partialResult: parentNode, remainingSubpath: remaining.map({ $0.full }).joined(separator: "/"), availableChildren: parentNode.children.keys.sorted(by: availableChildNameIsBefore)))
588-
return (nil, errors)
526+
throw Error.partialResult(partialResult: parentNode, remainingSubpath: remaining.map({ $0.full }).joined(separator: "/"), availableChildren: parentNode.children.keys.sorted(by: availableChildNameIsBefore))
589527
}
590-
591528
parentNode = parent
592529
}
530+
return parentNode
593531
}
594532

595533
// If no parent ID was provided, check if the first path component is a module name.
596534
if let matched = modules[component.name] ?? modules[component.full] {
597535
remaining = remaining.dropFirst()
598-
return (try findChild(of: matched, remaining: remaining) , [])
536+
return matched
599537
}
600538

601539
// No place to start the search from could be found.

Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,11 @@ doc://com.example/path/to/documentation/page#optional-heading
2222
bundle ID path in docs hierarchy heading name
2323
```
2424

25-
Both types of links can be used in a relative or absolute way. Absolute symbol links have a leading slash (`/`) and must start with the module they are referring to, for example ` ``/MyModule/MyClass/myProperty`` `.
26-
2725
## Resolving a Documentation Link
2826

29-
To make authored documentation links easier to write and easier to read in plain text format all authored documentation links can be written as relative links. The symbol links in documentation extension headers are written relative to the scope of modules. All other authored documentation links are written relative to the page where the link is written.
30-
31-
These relative documentation links can specify path components from higher up in the documentation hierarchy to reference container symbols or container pages. If a higher-up container page is shadowed by one of its descendants because they share the same name, the higher-up container page must be linked to using an absolute link or a sufficiently unambigious relative link.
32-
33-
```swift
34-
struct Container {
35-
struct Container {
36-
/// ``Container`` links to `Container.Container`
37-
/// ``MyModule/Container`` links to the outer `Container`
38-
func foo() { }
39-
}
40-
}
41-
```
27+
To make authored documentation links easier to write and easier to read in plain text format all authored documentation links are relative links. The symbol links in documentation extension headers are written relative to the scope of modules. All other authored documentation links are written relative to the page where the link is written.
4228

43-
> Note: If `MyModule` were to be named `Container` too, only the absolute link `/Container/Container` could be used to refer to the outer `Container`.
29+
These relative documentation links can specify path components from higher up in the documentation hierarchy to reference container symbols or container pages.
4430

4531
### Handling Ambiguous Links
4632

Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -344,37 +344,67 @@ class ReferenceResolverTests: XCTestCase {
344344
XCTAssertEqual(referencingFileDiagnostics.filter({ $0.identifier == "org.swift.docc.unresolvedTopicReference" }).count, 1)
345345
}
346346

347-
func testAbsoluteAndRelativeReferencesToExternalAndExtensionSymbols() throws {
348-
let (bundleURL, bundle, context) = try testBundleAndContext(copying: "BundleWithRelativePathAmbiguity")
347+
func testRelativeReferencesToExtensionSymbols() throws {
348+
let (bundleURL, bundle, context) = try testBundleAndContext(copying: "BundleWithRelativePathAmbiguity") { root in
349+
// We don't want the external target to be part of the archive as that is not
350+
// officially supported yet.
351+
try FileManager.default.removeItem(at: root.appendingPathComponent("Dependency.symbols.json"))
352+
353+
try """
354+
# ``BundleWithRelativePathAmbiguity/Dependency``
355+
356+
## Overview
357+
358+
### Module Scope Links
359+
360+
- ``BundleWithRelativePathAmbiguity/Dependency``
361+
- ``BundleWithRelativePathAmbiguity/Dependency/AmbiguousType``
362+
- ``BundleWithRelativePathAmbiguity/Dependency/AmbiguousType/foo()``
363+
364+
### Extended Module Scope Links
365+
366+
- ``Dependency``
367+
- ``Dependency/AmbiguousType``
368+
- ``Dependency/AmbiguousType/foo()``
369+
370+
### Local Scope Links
371+
372+
- ``Dependency``
373+
- ``AmbiguousType``
374+
- ``AmbiguousType/foo()``
375+
""".write(to: root.appendingPathComponent("Article.md"), atomically: true, encoding: .utf8)
376+
}
349377

350378
defer { try? FileManager.default.removeItem(at: bundleURL) }
351379

352380
// Get a translated render node
353-
let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/BundleWithRelativePathAmbiguity", sourceLanguage: .swift))
381+
let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/BundleWithRelativePathAmbiguity/Dependency", sourceLanguage: .swift))
354382
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference, source: nil)
355383
let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode
356384

357385
let content = try XCTUnwrap(renderNode.primaryContentSections.first as? ContentRenderSection).content
358386

359-
func assertListedReferencesInSectionMatchHeading(_ absoluteShorthandReference: String) throws {
360-
let headingString = "`\(absoluteShorthandReference)`"
361-
let absoluteReferenceString = "doc://org.swift.docc.example/documentation\(absoluteShorthandReference)"
387+
let expectedReferences = [
388+
"doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency",
389+
"doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType",
390+
"doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType/foo()",
391+
]
392+
393+
let sectionContents = [
394+
content.contents(of: "Module Scope Links"),
395+
content.contents(of: "Extended Module Scope Links"),
396+
content.contents(of: "Local Scope Links"),
397+
]
398+
399+
let sectionReferences = try sectionContents.map { sectionContent in
400+
try sectionContent.listItems().map { item in try XCTUnwrap(item.firstReference(), "found no reference for \(item)") }
401+
}
362402

363-
for listItem in content.contents(of: headingString).listItems() {
364-
let reference = try XCTUnwrap(listItem.firstReference(), "found no reference for \(listItem)")
365-
XCTAssertEqual(reference.identifier, absoluteReferenceString, "found mismatch for \(listItem)")
403+
for resolvedReferencesOfSection in sectionReferences {
404+
zip(resolvedReferencesOfSection, expectedReferences).forEach { resolved, expected in
405+
XCTAssertEqual(resolved.identifier, expected)
366406
}
367407
}
368-
369-
try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity")
370-
try assertListedReferencesInSectionMatchHeading("/Dependency")
371-
try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity/Dependency")
372-
try assertListedReferencesInSectionMatchHeading("/Dependency/AmbiguousType")
373-
try assertListedReferencesInSectionMatchHeading("/Dependency/AmbiguousProtocol")
374-
try assertListedReferencesInSectionMatchHeading("/Dependency/UnambiguousType")
375-
try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType")
376-
try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity/Dependency/AmbiguousProtocol")
377-
try assertListedReferencesInSectionMatchHeading("/Dependency/AmbiguousType/unambiguousFunction()")
378408
}
379409

380410
struct TestExternalReferenceResolver: ExternalReferenceResolver {

0 commit comments

Comments
 (0)