@@ -361,20 +361,20 @@ struct PathHierarchy {
361361 break lookForArticleRoot
362362 }
363363 }
364- return try searchForNode ( descendingFrom: articlesContainer, pathComponents: remaining. dropFirst ( ) , parsedPathForError: parsedPathForError)
364+ return try searchForNode ( descendingFrom: articlesContainer, pathComponents: remaining. dropFirst ( ) , parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
365365 } else if articlesContainer. anyChildMatches ( firstComponent) {
366- return try searchForNode ( descendingFrom: articlesContainer, pathComponents: remaining, parsedPathForError: parsedPathForError)
366+ return try searchForNode ( descendingFrom: articlesContainer, pathComponents: remaining, parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
367367 }
368368 }
369369 if !isKnownDocumentationPath {
370370 if tutorialContainer. matches ( firstComponent) {
371- return try searchForNode ( descendingFrom: tutorialContainer, pathComponents: remaining. dropFirst ( ) , parsedPathForError: parsedPathForError)
371+ return try searchForNode ( descendingFrom: tutorialContainer, pathComponents: remaining. dropFirst ( ) , parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
372372 } else if tutorialContainer. anyChildMatches ( firstComponent) {
373- return try searchForNode ( descendingFrom: tutorialContainer, pathComponents: remaining, parsedPathForError: parsedPathForError)
373+ return try searchForNode ( descendingFrom: tutorialContainer, pathComponents: remaining, parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
374374 }
375375 // The parent for tutorial overviews / technologies is "tutorials" which has already been removed above, so no need to check against that name.
376376 else if tutorialOverviewContainer. anyChildMatches ( firstComponent) {
377- return try searchForNode ( descendingFrom: tutorialOverviewContainer, pathComponents: remaining, parsedPathForError: parsedPathForError)
377+ return try searchForNode ( descendingFrom: tutorialOverviewContainer, pathComponents: remaining, parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
378378 }
379379 }
380380 }
@@ -383,11 +383,11 @@ struct PathHierarchy {
383383 func searchForNodeInModules( ) throws -> Node {
384384 // Note: This captures `parentID`, `remaining`, and `parsedPathForError`.
385385 if let moduleMatch = modules [ firstComponent. full] ?? modules [ firstComponent. name] {
386- return try searchForNode ( descendingFrom: moduleMatch, pathComponents: remaining. dropFirst ( ) , parsedPathForError: parsedPathForError)
386+ return try searchForNode ( descendingFrom: moduleMatch, pathComponents: remaining. dropFirst ( ) , parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
387387 }
388388 if modules. count == 1 {
389389 do {
390- return try searchForNode ( descendingFrom: modules. first!. value, pathComponents: remaining, parsedPathForError: parsedPathForError)
390+ return try searchForNode ( descendingFrom: modules. first!. value, pathComponents: remaining, parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
391391 } catch {
392392 // Ignore this error and raise an error about not finding the module instead.
393393 }
@@ -405,7 +405,7 @@ struct PathHierarchy {
405405 } catch {
406406 // If the node couldn't be found in the modules, search the non-matching parent to achieve a more specific error message
407407 if let parentID = parentID {
408- return try searchForNode ( descendingFrom: lookup [ parentID] !, pathComponents: path, parsedPathForError: parsedPathForError)
408+ return try searchForNode ( descendingFrom: lookup [ parentID] !, pathComponents: path, parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
409409 }
410410 throw error
411411 }
@@ -420,15 +420,15 @@ struct PathHierarchy {
420420 // If the starting point's children match this component, descend the path hierarchy from there.
421421 if possibleStartingPoint. anyChildMatches ( firstComponent) {
422422 do {
423- return try searchForNode ( descendingFrom: possibleStartingPoint, pathComponents: path, parsedPathForError: parsedPathForError)
423+ return try searchForNode ( descendingFrom: possibleStartingPoint, pathComponents: path, parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
424424 } catch {
425425 innerMostError = error
426426 }
427427 }
428428 // It's possible that the component is ambiguous at the parent. Checking if this node matches the first component avoids that ambiguity.
429429 if possibleStartingPoint. matches ( firstComponent) {
430430 do {
431- return try searchForNode ( descendingFrom: possibleStartingPoint, pathComponents: path. dropFirst ( ) , parsedPathForError: parsedPathForError)
431+ return try searchForNode ( descendingFrom: possibleStartingPoint, pathComponents: path. dropFirst ( ) , parsedPathForError: parsedPathForError, onlyFindSymbols : onlyFindSymbols )
432432 } catch {
433433 if innerMostError == nil {
434434 innerMostError = error
@@ -453,7 +453,8 @@ struct PathHierarchy {
453453 private func searchForNode(
454454 descendingFrom startingPoint: Node ,
455455 pathComponents: ArraySlice < PathComponent > ,
456- parsedPathForError: ( ) -> [ PathComponent ]
456+ parsedPathForError: ( ) -> [ PathComponent ] ,
457+ onlyFindSymbols: Bool
457458 ) throws -> Node {
458459 var node = startingPoint
459460 var remaining = pathComponents [ ... ]
@@ -481,7 +482,7 @@ struct PathHierarchy {
481482 }
482483 } catch DisambiguationTree . Error . lookupCollision( let collisions) {
483484 func handleWrappedCollision( ) throws -> Node {
484- try handleCollision ( node: node, parsedPath: parsedPathForError ( ) , remaining: remaining, collisions: collisions)
485+ try handleCollision ( node: node, parsedPath: parsedPathForError, remaining: remaining, collisions: collisions, onlyFindSymbols : onlyFindSymbols )
485486 }
486487
487488 // See if the collision can be resolved by looking ahead on level deeper.
@@ -523,26 +524,45 @@ struct PathHierarchy {
523524 return possibleMatches. first ( where: { $0. symbol? . identifier. interfaceLanguage == " swift " } ) ?? possibleMatches. first!
524525 }
525526 // Couldn't resolve the collision by look ahead.
526- return try handleCollision ( node: node, parsedPath: parsedPathForError ( ) , remaining: remaining, collisions: collisions)
527+ return try handleCollision ( node: node, parsedPath: parsedPathForError, remaining: remaining, collisions: collisions, onlyFindSymbols : onlyFindSymbols )
527528 }
528529 }
529530 }
530531
531532 private func handleCollision(
532533 node: Node ,
533- parsedPath: [ PathComponent ] ,
534+ parsedPath: ( ) -> [ PathComponent ] ,
534535 remaining: ArraySlice < PathComponent > ,
535- collisions: [ ( node: PathHierarchy . Node , disambiguation: String ) ]
536+ collisions: [ ( node: PathHierarchy . Node , disambiguation: String ) ] ,
537+ onlyFindSymbols: Bool
536538 ) throws -> Node {
537- let favoredNodes = collisions. filter { $0. node. isDisfavoredInCollision == false }
538- if favoredNodes. count == 1 {
539- return favoredNodes. first!. node
539+ if let favoredMatch = collisions. singleMatch ( { $0. node. isDisfavoredInCollision == false } ) {
540+ return favoredMatch. node
541+ }
542+ // If a module has the same name as the article root (which is named after the bundle display name) then its possible
543+ // for an article a symbol to collide. Articles aren't supported in symbol links but symbols are supported in general
544+ // documentation links (although the non-symbol result is prioritized).
545+ //
546+ // There is a later check that the returned node is a symbol for symbol links, but that won't happen if the link is a
547+ // collision. To fully handle the collision in both directions, the check below uses `onlyFindSymbols` in the closure
548+ // so that only symbol matches are returned for symbol links (when `onlyFindSymbols` is `true`) and non-symbol matches
549+ // for general documentation links (when `onlyFindSymbols` is `false`).
550+ //
551+ // It's a more compact way to write
552+ //
553+ // if onlyFindSymbols {
554+ // return $0.node.symbol != nil
555+ // } else {
556+ // return $0.node.symbol == nil
557+ // }
558+ if let symbolOrNonSymbolMatch = collisions. singleMatch ( { ( $0. node. symbol != nil ) == onlyFindSymbols } ) {
559+ return symbolOrNonSymbolMatch. node
540560 }
541561
542562 throw Error . lookupCollision (
543563 partialResult: (
544564 node,
545- Array ( parsedPath. dropLast ( remaining. count) )
565+ Array ( parsedPath ( ) . dropLast ( remaining. count) )
546566 ) ,
547567 remaining: Array ( remaining) ,
548568 collisions: collisions. map { ( $0. node, $0. disambiguation) }
@@ -598,6 +618,25 @@ struct PathHierarchy {
598618 }
599619}
600620
621+ private extension Sequence {
622+ /// Returns the only element of the sequence that satisfies the given predicate.
623+ /// - Parameters:
624+ /// - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match.
625+ /// - Returns: The only element of the sequence that satisfies `predicate`, or `nil` if multiple elements satisfy the predicate or if no element satisfy the predicate.
626+ /// - Complexity: O(_n_), where _n_ is the length of the sequence.
627+ func singleMatch( _ predicate: ( Element ) -> Bool ) -> Element ? {
628+ var match : Element ?
629+ for element in self where predicate ( element) {
630+ guard match == nil else {
631+ // Found a second match. No need to check the rest of the sequence.
632+ return nil
633+ }
634+ match = element
635+ }
636+ return match
637+ }
638+ }
639+
601640extension PathHierarchy {
602641 /// A node in the path hierarchy.
603642 final class Node {
0 commit comments