From a1d716b3f7ae18952ed1540842754e1da346d0f0 Mon Sep 17 00:00:00 2001 From: Max Obermeier Date: Thu, 7 Jul 2022 21:04:15 +0200 Subject: [PATCH 1/7] implement handling of extensions to external types as enabled by apple/swift#59047 - introduce transformation generating internal Extended Types Symbol Graph Format from the Extension Block Symbol Graph Format emmitted by the compiler - register symbols and relationships used by Extended Types Symbol Graph Format - adapt reference collision detection logic to deal with emitted path components that occur when extending external nested types - adapt reference resolution logic to first search for paths in the local module by default and introduce shorthand absolute syntax with "/" prefix to mark paths as absolute (to be searched for only in the global scope) - adapt SymbolGraphLoader to automatically detect if input Symbol Graph Files use the Extension Block Symbol Graph Format and apply the ExtendedTypesFormatTransformation in case - improve Swift title token parsing to correctly identify Symbol titles of nested types, which contain "." infixes --- Package.resolved | 6 +- Package.swift | 2 +- .../DocumentationCoverageOptions.swift | 10 +- .../Navigator/NavigatorIndex+Ext.swift | 2 +- .../Infrastructure/CoverageDataEntry.swift | 26 +- .../Infrastructure/DocumentationContext.swift | 245 +++++++- .../ExternalSymbolResolver+SymbolKind.swift | 10 + .../AccessControl+Comparable.swift | 40 ++ .../ExtendedTypesFormatExtension.swift | 93 ++++ .../ExtendedTypesFormatTransformation.swift | 524 ++++++++++++++++++ .../Symbol Graph/SymbolGraphLoader.swift | 81 +-- .../Topic Graph/AutomaticCuration.swift | 13 +- .../SwiftDocC/Model/DocumentationNode.swift | 6 +- Sources/SwiftDocC/Model/Kind.swift | 12 + .../DocumentationContentRenderer.swift | 129 +++-- .../RenderNodeTranslator+Swift.swift | 89 --- .../AutomaticCurationTests.swift | 8 +- .../DocumentationContextTests.swift | 5 + .../SymbolGraph/SymbolGraphLoaderTests.swift | 71 +-- ...mentationContentRenderer+SwiftTests.swift} | 14 +- .../XCTestCase+LoadingTestData.swift | 19 +- 21 files changed, 1162 insertions(+), 243 deletions(-) create mode 100644 Sources/SwiftDocC/Infrastructure/Symbol Graph/AccessControl+Comparable.swift create mode 100644 Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatExtension.swift create mode 100644 Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift delete mode 100644 Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator+Swift.swift rename Tests/SwiftDocCTests/Rendering/{RenderNodeTranslator+SwiftTests.swift => DocumentationContentRenderer+SwiftTests.swift} (82%) diff --git a/Package.resolved b/Package.resolved index a96e4455b1..324fe64579 100644 --- a/Package.resolved +++ b/Package.resolved @@ -39,10 +39,10 @@ }, { "package": "SymbolKit", - "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", + "repositoryURL": "https://github.com/themomax/swift-docc-symbolkit", "state": { - "branch": "main", - "revision": "da6cedd103e0e08a2bc7b14869ec37fba4db72d9", + "branch": "docc-extensions-to-external-types-base", + "revision": "dba9c2dc32a3c02d5be84b67e9b2c4af108d3ba0", "version": null } }, diff --git a/Package.swift b/Package.swift index 0900ffdeb1..266ba432c5 100644 --- a/Package.swift +++ b/Package.swift @@ -124,7 +124,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(name: "swift-markdown", url: "https://github.com/apple/swift-markdown.git", .branch("main")), .package(name: "CLMDB", url: "https://github.com/apple/swift-lmdb.git", .branch("main")), .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "1.0.1")), - .package(name: "SymbolKit", url: "https://github.com/apple/swift-docc-symbolkit", .branch("main")), + .package(name: "SymbolKit", url: "https://github.com/themomax/swift-docc-symbolkit", .branch("docc-extensions-to-external-types-base")), .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: "1.1.2")), ] diff --git a/Sources/SwiftDocC/Coverage/DocumentationCoverageOptions.swift b/Sources/SwiftDocC/Coverage/DocumentationCoverageOptions.swift index 2be6e972dd..8fcf2a6d4f 100644 --- a/Sources/SwiftDocC/Coverage/DocumentationCoverageOptions.swift +++ b/Sources/SwiftDocC/Coverage/DocumentationCoverageOptions.swift @@ -185,15 +185,15 @@ extension DocumentationCoverageOptions.KindFilterOptions { /// Converts given ``DocumentationNode.Kind`` to corresponding `BitFlagRepresentation` if possible. Returns `nil` if the given Kind is not representable. fileprivate init?(kind: DocumentationNode.Kind) { switch kind { - case .module: // 1 + case .module, .extendedModule: // 1 self = .module - case .class: // 2 + case .class, .extendedClass: // 2 self = .class - case .structure: // 3 + case .structure, .extendedStructure: // 3 self = .structure - case .enumeration: // 4 + case .enumeration, .extendedEnumeration: // 4 self = .enumeration - case .protocol: // 5 + case .protocol, .extendedProtocol: // 5 self = .protocol case .typeAlias: // 6 self = .typeAlias diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift index cd3e468421..f4ed35223e 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift @@ -76,7 +76,7 @@ public class FileSystemRenderNodeProvider: RenderNodeProvider { extension RenderNode { private static let typesThatShouldNotUseNavigatorTitle: Set = [ - .framework, .class, .structure, .enumeration, .protocol, .typeAlias, .associatedType + .framework, .class, .structure, .enumeration, .protocol, .typeAlias, .associatedType, .extension ] /// Returns a navigator title preferring the fragments inside the metadata, if applicable. diff --git a/Sources/SwiftDocC/Infrastructure/CoverageDataEntry.swift b/Sources/SwiftDocC/Infrastructure/CoverageDataEntry.swift index a4f4703eaf..aa18417839 100644 --- a/Sources/SwiftDocC/Infrastructure/CoverageDataEntry.swift +++ b/Sources/SwiftDocC/Infrastructure/CoverageDataEntry.swift @@ -243,7 +243,11 @@ extension CoverageDataEntry { .protocol, .typeAlias, .associatedType, - .typeDef: + .typeDef, + .extendedClass, + .extendedStructure, + .extendedEnumeration, + .extendedProtocol: self = .types case .localVariable, .instanceProperty, @@ -256,7 +260,7 @@ extension CoverageDataEntry { .typeSubscript, .instanceSubscript: self = .members - case .function, .module, .globalVariable, .operator: + case .function, .module, .globalVariable, .operator, .extendedModule: self = .globals case let kind where SummaryCategory.allKnownNonSymbolKindNames.contains(kind.name): self = .nonSymbol @@ -297,46 +301,46 @@ extension CoverageDataEntry { context: DocumentationContext ) throws { switch documentationNode.kind { - case DocumentationNode.Kind.class: + case .class, .extendedClass: self = try .class( memberStats: KindSpecificData.extractChildStats( documentationNode: documentationNode, context: context)) - case DocumentationNode.Kind.enumeration: + case .enumeration, .extendedEnumeration: self = try .enumeration( memberStats: KindSpecificData.extractChildStats( documentationNode: documentationNode, context: context)) - case DocumentationNode.Kind.structure: + case .structure, .extendedStructure: self = try .structure( memberStats: KindSpecificData.extractChildStats( documentationNode: documentationNode, context: context)) - case DocumentationNode.Kind.protocol: - self = try .enumeration( + case .protocol, .extendedProtocol: + self = try .protocol( memberStats: KindSpecificData.extractChildStats( documentationNode: documentationNode, context: context)) - case DocumentationNode.Kind.instanceMethod: + case .instanceMethod: self = try .instanceMethod( parameterStats: CoverageDataEntry.KindSpecificData.extractFunctionSignatureStats( documentationNode: documentationNode, context: context , fieldName: "method parameters")) - case DocumentationNode.Kind.operator: + case .operator: self = try .`operator`( parameterStats: CoverageDataEntry.KindSpecificData.extractFunctionSignatureStats( documentationNode: documentationNode, context: context, fieldName: "operator parameters")) - case DocumentationNode.Kind.function: + case .function: self = try .`operator`( parameterStats: CoverageDataEntry.KindSpecificData.extractFunctionSignatureStats( documentationNode: documentationNode, context: context, fieldName: "function parameters")) - case DocumentationNode.Kind.initializer: + case .initializer: self = try .`operator`( parameterStats: CoverageDataEntry.KindSpecificData.extractFunctionSignatureStats( documentationNode: documentationNode, diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index 9e9b07c280..e90896f496 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -274,6 +274,9 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { public var externalMetadata = ExternalMetadata() + /// The decoder used in the `SymbolGraphLoader` + var decoder: JSONDecoder = JSONDecoder() + /// Initializes a documentation context with a given `dataProvider` and registers all the documentation bundles that it provides. /// /// - Parameter dataProvider: The data provider to register bundles from. @@ -1018,11 +1021,249 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { return ((reference, symbol.uniqueIdentifier, graphNode, documentation), []) } + + /// Returns a map between symbol identifiers and topic references. + /// + /// - Parameters: + /// - symbolGraph: The complete symbol graph to walk through. + /// - bundle: The bundle to use when creating symbol references. + func referencesForSymbols(in unifiedGraphs: [String: UnifiedSymbolGraph], bundle: DocumentationBundle) -> [SymbolGraph.Symbol.Identifier: [ResolvedTopicReference]] { + // The implementation of this function is fairly tricky because in most cases it has to preserve past behavior. + // + // This is because symbol references bake the disambiguators into the path, making it the only version of that + // path that resolves to that symbol. In other words, a reference with "too few" or "too many" disambiguators + // will fail to resolve. Changing what's considered the "correct" disambiguators for a symbol means that links + // that used to resolve will break with the new behavior. + // + // The tests in `SymbolDisambiguationTests` cover the known behaviors that should be preserved. + // + // The real solution to this problem is to allow symbol links to over-specify disambiguators and improve the + // diagnostics when symbol links are ambiguous. (rdar://78518537) + // That will allow for fixes to the least amount of disambiguation without breaking existing links. + + + // The current implementation works in 3 phases: + // - First, it computes the paths without disambiguators to identify colliding paths. + // - Second, it computes the "correct" disambiguators for each collision. + // - Lastly, it joins together the results in a stable order to avoid indeterministic behavior. + + + let totalSymbolCount = unifiedGraphs.values.map { $0.symbols.count }.reduce(0, +) + + /// Temporary data structure to hold input to compute paths with or without disambiguation. + struct PathCollisionInfo { + let symbol: UnifiedSymbolGraph.Symbol + let moduleName: String + var languages: Set + } + var pathCollisionInfo = [[String]: [PathCollisionInfo]]() + pathCollisionInfo.reserveCapacity(totalSymbolCount) + + // Group symbols by path from all of the available symbol graphs + for (moduleName, symbolGraph) in unifiedGraphs { + let symbols = Array(symbolGraph.symbols.values) + + let referenceMap = symbols.concurrentMap { symbol in + (symbol, referencesWithoutDisambiguationFor(symbol, moduleName: moduleName, bundle: bundle)) + }.reduce(into: [String: [SourceLanguage: ResolvedTopicReference]](), { result, next in + let (symbol, references) = next + for reference in references { + result[symbol.uniqueIdentifier, default: [:]][reference.sourceLanguage] = reference + } + }) + + let parentMap = symbolGraph.relationshipsByLanguage.reduce(into: [String: [SourceLanguage: String]](), { parentMap, next in + let (selector, relationships) = next + guard let language = SourceLanguage(knownLanguageIdentifier: selector.interfaceLanguage) else { + return + } + + for relationship in relationships { + switch relationship.kind { + case .memberOf, .requirementOf, .declaredIn: + parentMap[relationship.source, default: [:]][language] = relationship.target + default: + break + } + } + }) + + let pathsAndLanguages: [[([String], SourceLanguage)]] = symbols.concurrentMap { symbol in + guard let references = referenceMap[symbol.uniqueIdentifier] else { + return [] + } + + return references.map { language, reference in + var prefixLength: Int + if let parentId = parentMap[symbol.uniqueIdentifier]?[language], + let parentReference = referenceMap[parentId]?[language] ?? referenceMap[parentId]?.values.first { + // This is a child of some other symbol + prefixLength = parentReference.pathComponents.count + } else { + // This is a top-level symbol or another symbol without parent (e.g. default implementation) + prefixLength = reference.pathComponents.count-1 + } + + // PathComponents can have prefixes which are not known locally. In that case, + // the "empty" segments will be cut out later on. We follow the same logic here, as otherwise + // some collisions would not be detected. + // E.g. consider an extension to an external nested type `SomeModule.SomeStruct.SomeStruct`. The + // parent of this extended type symbol is `SomeModule`, however, the path for the extended type symbol + // is `SomeModule/SomeStruct/SomeStruct`, later on, this will change to `SomeModule/SomeStruct`. Now, if + // we also extend `SomeModule.SomeStruct`, the paths for both extensions could collide. To recognize (and resolve) + // the collision here, we work with the same, shortened paths. + return ((reference.pathComponents[0.. 1, disambiguationSuffix == (false, false) { + let symbolReference = SymbolReference( + collisionInfo.symbol.uniqueIdentifier, + interfaceLanguages: collisionInfo.symbol.sourceLanguages, + defaultSymbol: collisionInfo.symbol.defaultSymbol, + shouldAddHash: false, + shouldAddKind: false + ) + + resultGroups[collisionInfo.symbol.defaultIdentifier, default: []].append( + IntermediateResultGroup( + conflictingSymbolLanguage: language, + disambiguatedReferences:[ResolvedTopicReference(symbolReference: symbolReference, moduleName: collisionInfo.moduleName, bundle: bundle)] + ) + ) + continue + } + + // Emit the disambiguated references for all languages for this symbol's collision. + var symbolSelectors = [collisionInfo.symbol.defaultSelector!] + for selector in collisionInfo.symbol.mainGraphSelectors where !symbolSelectors.contains(selector) { + symbolSelectors.append(selector) + } + symbolSelectors = symbolSelectors.filter { collisionInfo.languages.contains(SourceLanguage(id: $0.interfaceLanguage)) } + + resultGroups[collisionInfo.symbol.defaultIdentifier, default: []].append( + IntermediateResultGroup( + conflictingSymbolLanguage: language, + disambiguatedReferences: symbolSelectors.map { selector in + let symbolReference = SymbolReference( + collisionInfo.symbol.uniqueIdentifier, + interfaceLanguages: collisionInfo.symbol.sourceLanguages, + defaultSymbol: collisionInfo.symbol.symbol(forSelector: selector), + shouldAddHash: disambiguationSuffix.shouldAddIdHash, + shouldAddKind: disambiguationSuffix.shouldAddKind + ) + return ResolvedTopicReference(symbolReference: symbolReference, moduleName: collisionInfo.moduleName, bundle: bundle) + } + ) + ) + } + } + + return resultGroups.mapValues({ + return $0.sorted(by: { lhs, rhs in + switch (lhs.conflictingSymbolLanguage, rhs.conflictingSymbolLanguage) { + // If only one result group is Swift, that comes before the other result. + case (.swift, let other) where other != .swift: + return true + case (let other, .swift) where other != .swift: + return false + + // Otherwise, compare the first path to ensure a deterministic order. + default: + return lhs.disambiguatedReferences[0].path < rhs.disambiguatedReferences[0].path + } + }).flatMap({ $0.disambiguatedReferences }) + }) + } + + private func referencesWithoutDisambiguationFor(_ symbol: UnifiedSymbolGraph.Symbol, moduleName: String, bundle: DocumentationBundle) -> [ResolvedTopicReference] { + if let pathComponents = knownDisambiguatedSymbolPathComponents?[symbol.uniqueIdentifier], + let componentsCount = symbol.defaultSymbol?.pathComponents.count, + pathComponents.count == componentsCount + { + let symbolReference = SymbolReference(pathComponents: pathComponents, interfaceLanguages: symbol.sourceLanguages) + return [ResolvedTopicReference(symbolReference: symbolReference, moduleName: moduleName, bundle: bundle)] + } + + // A unified symbol that exist in multiple languages may have multiple references. + + // Find all of the relevant selectors, starting with the `defaultSelector`. + // Any reference after the first is considered an alias/alternative to the first reference + // and will resolve to the first reference. + var symbolSelectors = [symbol.defaultSelector] + for selector in symbol.mainGraphSelectors where !symbolSelectors.contains(selector) { + symbolSelectors.append(selector) + } + + return symbolSelectors.map { selector in + let defaultSymbol = symbol.symbol(forSelector: selector)! + let symbolReference = SymbolReference( + symbol.uniqueIdentifier, + interfaceLanguages: symbol.sourceLanguages.filter { $0 == SourceLanguage(id: defaultSymbol.identifier.interfaceLanguage) }, + defaultSymbol: defaultSymbol, + shouldAddHash: false, + shouldAddKind: false + ) + return ResolvedTopicReference(symbolReference: symbolReference, moduleName: moduleName, bundle: bundle) + } + } private func parentChildRelationship(from edge: SymbolGraph.Relationship) -> (ResolvedTopicReference, ResolvedTopicReference)? { // Filter only parent <-> child edges switch edge.kind { - case .memberOf, .requirementOf: + case .memberOf, .requirementOf, .declaredIn: guard let parentRef = symbolIndex[edge.target]?.reference, let childRef = symbolIndex[edge.source]?.reference else { return nil } @@ -1920,7 +2161,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { discoveryGroup.async(queue: discoveryQueue) { [unowned self] in symbolGraphLoader = SymbolGraphLoader(bundle: bundle, dataProvider: self.dataProvider) do { - try symbolGraphLoader.loadAll() + try symbolGraphLoader.loadAll(using: decoder) if LinkResolutionMigrationConfiguration.shouldSetUpHierarchyBasedLinkResolver { let pathHierarchy = PathHierarchy(symbolGraphLoader: symbolGraphLoader, bundleName: urlReadablePath(bundle.displayName), knownDisambiguatedPathComponents: knownDisambiguatedSymbolPathComponents) hierarchyBasedResolver = PathHierarchyBasedLinkResolver(pathHierarchy: pathHierarchy) diff --git a/Sources/SwiftDocC/Infrastructure/External Data/ExternalSymbolResolver+SymbolKind.swift b/Sources/SwiftDocC/Infrastructure/External Data/ExternalSymbolResolver+SymbolKind.swift index b202162df3..547381095a 100644 --- a/Sources/SwiftDocC/Infrastructure/External Data/ExternalSymbolResolver+SymbolKind.swift +++ b/Sources/SwiftDocC/Infrastructure/External Data/ExternalSymbolResolver+SymbolKind.swift @@ -66,6 +66,16 @@ extension ExternalSymbolResolver { symbolKind = .var case .module: symbolKind = .module + case .extendedModule: + symbolKind = .extendedModule + case .extendedStructure: + symbolKind = .extendedStructure + case .extendedClass: + symbolKind = .extendedClass + case .extendedEnumeration: + symbolKind = .extendedEnumeration + case .extendedProtocol: + symbolKind = .extendedProtocol // There shouldn't be any reason for a symbol graph file to reference one of these kinds outside of the symbol graph itself. // Return `.class` as the symbol kind (acting as "any symbol") so that the render reference gets a "symbol" role. diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/AccessControl+Comparable.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/AccessControl+Comparable.swift new file mode 100644 index 0000000000..215d56cf71 --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/AccessControl+Comparable.swift @@ -0,0 +1,40 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 SymbolKit + +extension SymbolGraph.Symbol.AccessControl: Comparable { + private var level: Int? { + switch self { + case .private: + return 0 + case .filePrivate: + return 1 + case .internal: + return 2 + case .public: + return 3 + case .open: + return 4 + default: + assertionFailure("Unknown AccessControl case was used in comparison.") + return nil + } + } + + public static func < (lhs: SymbolGraph.Symbol.AccessControl, rhs: SymbolGraph.Symbol.AccessControl) -> Bool { + guard let lhs = lhs.level, + let rhs = rhs.level else { + return false + } + + return lhs < rhs + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatExtension.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatExtension.swift new file mode 100644 index 0000000000..5973137414 --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatExtension.swift @@ -0,0 +1,93 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 SymbolKit + +// MARK: Custom Relationship Kind Identifiers + +extension SymbolGraph.Relationship.Kind { + static let declaredIn = Self(rawValue: "declaredIn") +} + +// MARK: Custom Symbol Kind Identifiers + +extension SymbolGraph.Symbol.KindIdentifier { + static let extendedProtocol = Self(rawValue: "protocol.extension") + + static let extendedStructure = Self(rawValue: "struct.extension") + + static let extendedClass = Self(rawValue: "class.extension") + + static let extendedEnumeration = Self(rawValue: "enum.extension") + + static let extendedModule = Self(rawValue: "module.extension") + + init?(extending other: Self) { + switch other { + case .struct: + self = .extendedStructure + case .protocol: + self = .extendedProtocol + case .class: + self = .extendedClass + case .enum: + self = .extendedEnumeration + case .module: + self = .extendedModule + default: + return nil + } + } + + static func extendedType(for extensionBlock: SymbolGraph.Symbol) -> Self? { + guard let extensionMixin = extensionBlock.mixins[SymbolGraph.Symbol.Swift.Extension.mixinKey] as? SymbolGraph.Symbol.Swift.Extension else { + return nil + } + + guard let typeKind = extensionMixin.typeKind else { + return nil + } + + return Self(extending: typeKind) + } +} + +extension SymbolGraph.Symbol.Kind { + static func extendedType(for extensionBlock: SymbolGraph.Symbol) -> Self { + let id = SymbolGraph.Symbol.KindIdentifier.extendedType(for: extensionBlock) + switch id { + case .some(.extendedProtocol): + return Self(parsedIdentifier: .extendedProtocol, displayName: "Extended Protocol") + case .some(.extendedStructure): + return Self(parsedIdentifier: .extendedStructure, displayName: "Extended Structure") + case .some(.extendedClass): + return Self(parsedIdentifier: .extendedClass, displayName: "Extended Class") + case .some(.extendedEnumeration): + return Self(parsedIdentifier: .extendedEnumeration, displayName: "Extended Enumeration") + default: + return Self(rawIdentifier: "unknown.extension", displayName: "Extended Type") + } + } +} + + +// MARK: Swift AccessControl Levels + +extension SymbolGraph.Symbol.AccessControl { + static let `private` = Self(rawValue: "private") + + static let filePrivate = Self(rawValue: "fileprivate") + + static let `internal` = Self(rawValue: "internal") + + static let `public` = Self(rawValue: "public") + + static let open = Self(rawValue: "open") +} diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift new file mode 100644 index 0000000000..c15954cbf2 --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift @@ -0,0 +1,524 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 + +/// A namespace comprising functionality for converting between the standard Symbol Graph File +/// format with extension block symbols and the extended types format extension used by SwiftDocC. +enum ExtendedTypesFormatTransformation { } + +extension ExtendedTypesFormatTransformation { + /// Merge symbols of kind ``SymbolKit/Symbolgraph/Symbol/KindIdentifier/extendedModule`` that represent the + /// same module. + /// + /// When using the Extended Type Symbol Format on normal (i.e. non-unified) symbol graphs, each of the extension symbol graphs + /// might contain an extended module symbol representing the same module. When merging all symbol graphs from one primary + /// module into one `UnifiedSymbolGraph`, this may result in this unified graph having more than one extended module symbol + /// with the same name. This function merges these duplicate extended module symbols and redirects the `declaredIn` relationships + /// accordingly. As a result, the final graph will only contain one extended module symbol for each extended module. + /// + /// This transformation is relevant in the following case. Consider a project of three modules, `A`, `B`, and `C`, where `B` imports + /// `A`, and `C` imports `A` and `B`. + /// + /// ```swift + /// // Module A + /// public struct AStruct { } + /// + /// // Module B + /// import A + /// + /// public extension AStruct { + /// struct BStruct { } + /// } + /// + /// public protocol BProtocol {} + /// + /// // Module C + /// import A + /// import B + /// + /// public extension AStruct.BStruct { + /// struct CStruct { } + /// } + /// + /// public extension BProtocol { + /// func foo() { } + /// } + /// ``` + /// + /// The Symbol Graph Files generated for module `C` are `C.symbols.json`, `C@A.symbols.json`, and + /// `C@B.symbols.json`. + /// + /// `CStruct`, as well as the respective `swift.extension` symbol are part of + /// `C@A.symbols.json`, as they are part of a top-level symbol declared in module `A`. However, since `CStruct`'s + /// direct partent type is `BStruct`, which is declared in module `B`. Therefore, `CStruct` is considered an extension + /// to module `B`, which is correctly stated in the `swiftExtension.extendedModule` property. Thus, the transformed + /// symbol graph for `C@A.symbols.json` contains an extended module symbol for module `B`. + /// + /// `BProtocol.foo()`, as well as the respective `swift.extension` symbol are obviously part of `C@B.symbols.json`. + /// Thus, this transformed symbol graph also contains an extended module symbol for module `B`. + /// + /// If one decides to merge the transformed symbol graphs for files `C.symbols.json`, `C@A.symbols.json`, and + /// `C@B.symbols.json`, the resulting unified graph will have two extended module symbols for module `B`, which is + /// undesirable. This method should therefore be applied to the unified symbol graph after all symbol graphs resulting from + /// module `C` have been merged. + static func mergeExtendedModuleSymbolsFromDifferentFiles(_ symbolGraph: UnifiedSymbolGraph) { + var canonicalSymbolByModuleName: [String: UnifiedSymbolGraph.Symbol] = [:] + var keyMap: [String: String] = [:] + + // choose canonical extended module symbol for each moduleName + for symbol in symbolGraph.symbols.values where symbol.kindIdentifier == "swift." + SymbolGraph.Symbol.KindIdentifier.extendedModule.identifier { + if let canonical = canonicalSymbolByModuleName[symbol.title] { + // merge accesslevel + for (selector, level) in symbol.accessLevel { + if let oldLevel = canonical.accessLevel[selector] { + canonical.accessLevel[selector] = max(oldLevel, level) + } else { + canonical.accessLevel[selector] = level + } + } + + canonicalSymbolByModuleName[symbol.title] = canonical + keyMap[symbol.uniqueIdentifier] = canonical.uniqueIdentifier + } else { + canonicalSymbolByModuleName[symbol.title] = symbol + } + } + + // delete extended module symbols that were not chosen + for alternativeId in keyMap.keys { + symbolGraph.symbols.removeValue(forKey: alternativeId) + } + + // remap `declaredIn` relationships to the respective chosen extended module symbol + + // this should only apply to `declaredIn` relationships + for (selector, var relationships) in symbolGraph.relationshipsByLanguage { + redirect(\.target, of: &relationships, using: keyMap) + + symbolGraph.relationshipsByLanguage[selector] = relationships + } + + redirect(\.target, of: &symbolGraph.orphanRelationships, using: keyMap) + } +} + +extension ExtendedTypesFormatTransformation { + /// Convert from the extension block symbol format to the extended type symbol format. + /// + /// First, the function checks if there are any symbols of kind `.extension` in the graph. + /// If not, function returns `false` without altering the graph in any way. + /// + /// If it finds such symbols, it applies the actual transformation. Refer to the sections below to find + /// out how the two formats differ. + /// + /// In addition, the transformation prepends the given `moduleName` to the `pathComponents` of all + /// symbols in the graph. + /// + /// ### The Extension Block Symbol Format + /// + /// The extension block symbol format captures extensions to external types in the following way: + /// - a member symbol of the according kind for all added members + /// - a symbol of kind `.extension` _for each extension block_ (i.e. `extension X { ... }`) + /// - a `.memberOf` relationship between each member symbol and the `.extension` symbol representing + /// the extension block the member was declared in + /// - a `.conformsTo` relationship between each relevant protocol and the `.extension` symbol representing + /// the extension block where the external type was conformed to the respective protocol + /// - an `.extensionTo` relationship between each `.extension` symbol and the symbol of the original declaration + /// of the external type it extends + /// + /// ``` + /// ┌──────────────┐ + /// ┌───────────conformsTo────►│swift.protocol│ + /// │ m └──────────────┘ + /// │ + /// ┌─────────────┐ │n ┌────────────────┐ + /// │Original Type│ ┌───────┴───────┐ │Extension Member│ + /// │ Symbol │◄────extensionTo──────┤swift.extension│◄────memberOf─────┤ Symbol │ + /// └─────────────┘ 1 n └───────────────┘ 1 n └────────────────┘ + /// ``` + /// + /// ### The Extended Type Symbol Format + /// + /// The extended type symbol format provides a more concise and hierarchical structure: + /// - a member symbol of the according kind for all added members + /// - an **extended type symbol** _for each external type that was extended_: + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedStruct`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extemdedClass`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedEnum`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedProtocol`` + /// - a `.memberOf` relationship between each member symbol and the **extended type symbol** representing + /// the type that was extended + /// - a `.conformsTo` relationship between each relevant protocol and the **extended type symbol** representing + /// the the type that was extended + /// - a ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedModule`` symbol for each module that + /// was extended with at leas one `.extension` symbol + /// - a ``SymbolKit/SymbolGraph/Relationship/declaredIn`` relationship between each **extended type symbol** + /// and the **extended module symbol** representing the module the extended type was originally declared in + /// + /// ``` + /// ┌──────────────┐ + /// ┌───────────conformsTo────►│swift.protocol│ + /// │ m └──────────────┘ + /// │n + /// ┌────────────┐ ┌───────┴─────┐ ┌────────────────┐ + /// │swift.module│ │Extended Type│ │Extension Member│ + /// │ .extension │◄────declaredIn───────┤ Symbol │◄──────memberOf─────┤ Symbol │ + /// └────────────┘ 1 n└─────────────┘ 1 n └────────────────┘ + /// ``` + /// + /// - Parameter symbolGraph: An (extension) symbol graph that should use the extensoin block symbol format. + /// - Returns: Returns whether the transformation was applied (the `symbolGraph` was an extension graph + /// in the extended type symbol format) or not + static func transformExtensionBlockFormatToExtendedTypeFormat(_ symbolGraph: inout SymbolGraph) throws -> Bool { + var extensionBlockSymbols = extractExtensionBlockSymbols(from: &symbolGraph) + + guard !extensionBlockSymbols.isEmpty else { + return false + } + + prependModuleNameToPathComponents(&symbolGraph.symbols.values) + prependModuleNameToPathComponents(&extensionBlockSymbols.values) + + var (extensionToRelationships, + memberOfRelationships, + conformsToRelationships) = extractRelationshipsTouchingExtensionBlockSymbols(from: &symbolGraph, using: extensionBlockSymbols) + + var (extendedTypeSymbols, + extensionBlockToExtendedTypeMapping, + extendedTypeToExtensionBlockMapping) = synthesizeExtendedTypeSymbols(using: extensionBlockSymbols, extensionToRelationships) + + redirect(\.target, of: &memberOfRelationships, using: extensionBlockToExtendedTypeMapping) + + redirect(\.source, of: &conformsToRelationships, using: extensionBlockToExtendedTypeMapping) + + attachDocComments(to: &extendedTypeSymbols.values, using: { (target) -> [SymbolGraph.Symbol] in + guard let relevantExtensionBlockSymbols = extendedTypeToExtensionBlockMapping[target.identifier.precise]?.compactMap({ id in extensionBlockSymbols[id] }).filter({ symbol in symbol.docComment != nil }) else { + return [] + } + + // we sort the symbols here because their order is not guaranteed to stay the same + // accross compilation processes and we always want to choose the same doc comment + // in case there are multiple candidates with maximum number of lines + if let winner = relevantExtensionBlockSymbols.sorted(by: \.identifier.precise).max(by: { a, b in (a.docComment?.lines.count ?? 0) < (b.docComment?.lines.count ?? 0) }) { + return [winner] + } else { + return [] + } + }) + + symbolGraph.relationships.append(contentsOf: memberOfRelationships) + symbolGraph.relationships.append(contentsOf: conformsToRelationships) + extendedTypeSymbols.values.forEach { symbol in symbolGraph.symbols[symbol.identifier.precise] = symbol } + + try synthesizeExtendedModuleSymbolsAndDeclaredInRelationships(on: &symbolGraph, using: extendedTypeSymbols.values.map(\.identifier.precise)) + + return true + } + + /// Tries to obtain `docComment`s for all `targets` and copies the documentaiton from sources to the target. + /// + /// Iterates over all `targets` calling the `source` method to obtain a list of symbols that should serve as sources for the target's `docComment`. + /// If there is more than one symbol containing a `docComment` in the compound list of target and the list returned by `source`, `onConflict` is + /// called iteratively on the (modified) target and the next source element. + private static func attachDocComments(to targets: inout T, + using source: (T.Element) -> [SymbolGraph.Symbol], + onConflict resolveConflict: (_ old: T.Element, _ new: SymbolGraph.Symbol) + -> SymbolGraph.LineList? = { _, _ in nil }) + where T.Element == SymbolGraph.Symbol { + for index in targets.indices { + var target = targets[index] + + guard target.docComment == nil else { + continue + } + + for source in source(target) { + if case (.some(_), .some(_)) = (target.docComment, source.docComment) { + target.docComment = resolveConflict(target, source) + } else { + target.docComment = target.docComment ?? source.docComment + } + } + + targets[index] = target + } + } + + /// Adds the `extendedModule` name from the `swiftExtension` mixin to the beginning of the `pathComponents` array of all `symbols`. + private static func prependModuleNameToPathComponents(_ symbols: inout S) where S.Element == SymbolGraph.Symbol { + for i in symbols.indices { + let symbol = symbols[i] + + guard let extendedModuleName = symbol[mixin: SymbolGraph.Symbol.Swift.Extension.self]?.extendedModule else { + continue + } + + symbols[i] = symbol.replacing(\.pathComponents, with: [extendedModuleName] + symbol.pathComponents) + } + } + + /// Collects all symbols with kind identifier `.extension`, removes them from the `symbolGraph`, and returns them separately. + /// + /// - Returns: The extracted symbols of kind `.extension` keyed by their precise identifier. + private static func extractExtensionBlockSymbols(from symbolGraph: inout SymbolGraph) -> [String: SymbolGraph.Symbol] { + var extensionBlockSymbols: [String: SymbolGraph.Symbol] = [:] + + symbolGraph.apply(compactMap: { symbol in + guard symbol.kind.identifier == SymbolGraph.Symbol.KindIdentifier.extension else { + return symbol + } + + extensionBlockSymbols[symbol.identifier.precise] = symbol + return nil + }) + + return extensionBlockSymbols + } + + /// Collects all relationships that touch any of the given extension symbols, removes them from the `symbolGraph`, and returns them separately. + /// + /// The relevant relationships in this context are of the follwing kinds: + /// + /// - `.extenisonTo`: the `source` must be of kind `.extension` + /// - `.conformsTo`: the `source` may be of kind `.extension` + /// - `.memberOf`: the `target` may be of kind `.extension` + /// + /// - Parameter extensionBlockSymbols: A mapping between Symbols of kind `.extension` and their precise identifiers. + /// + /// - Returns: The extracted relationships listed separately by kind. + private static func extractRelationshipsTouchingExtensionBlockSymbols(from symbolGraph: inout SymbolGraph, + using extensionBlockSymbols: [String: SymbolGraph.Symbol]) + -> (extensionToRelationships: [SymbolGraph.Relationship], + memberOfRelationships: [SymbolGraph.Relationship], + conformsToRelationships: [SymbolGraph.Relationship]) { + + var extensionToRelationships: [SymbolGraph.Relationship] = [] + var memberOfRelationships: [SymbolGraph.Relationship] = [] + var conformsToRelationships: [SymbolGraph.Relationship] = [] + + symbolGraph.relationships = symbolGraph.relationships.compactMap { relationship in + switch relationship.kind { + case .extensionTo: + if extensionBlockSymbols[relationship.source] != nil { + extensionToRelationships.append(relationship) + return nil + } + case .memberOf: + if extensionBlockSymbols[relationship.target] != nil { + memberOfRelationships.append(relationship) + return nil + } + case .conformsTo: + if extensionBlockSymbols[relationship.source] != nil { + conformsToRelationships.append(relationship) + return nil + } + default: + break + } + return relationship + } + + return (extensionToRelationships, memberOfRelationships, conformsToRelationships) + } + + /// Synthesizes extended type symbols from the given `extensionBlockSymbols` and `extensoinToRelationships`. + /// + /// Creates symbols of the following kinds: + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedStruct`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extemdedClass`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedEnum`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedProtocol`` + /// + /// Each created symbol comprises one or more symbols of kind `.extension` that have an `.extensionTo` relationship with the + /// same type. + /// + /// - Returns: - the created extended type symbols keyed by their precise identifier, along with a bidirectional + /// mapping between the extended type symbols and the `.extension` symbols + private static func synthesizeExtendedTypeSymbols(using extensionBlockSymbols: [String: SymbolGraph.Symbol], + _ extensionToRelationships: RS) + -> (extendedTypeSymbols: [String: SymbolGraph.Symbol], + extensionBlockToExtendedTypeMapping: [String: String], + extendedTypeToExtensionBlockMapping: [String: [String]]) + where RS.Element == SymbolGraph.Relationship { + + var extendedTypeSymbols: [String: SymbolGraph.Symbol] = [:] + var extensionBlockToExtendedTypeMapping: [String: String] = [:] + var extendedTypeToExtensionBlockMapping: [String: [String]] = [:] + + extensionBlockToExtendedTypeMapping.reserveCapacity(extensionBlockSymbols.count) + + let createExtendedTypeSymbol = { (extensionBlockSymbol: SymbolGraph.Symbol, id: String) -> SymbolGraph.Symbol in + var newMixins = [String: Mixin]() + + if var swiftExtension = extensionBlockSymbol[mixin: SymbolGraph.Symbol.Swift.Extension.self] { + swiftExtension.constraints = [] + newMixins[SymbolGraph.Symbol.Swift.Extension.mixinKey] = swiftExtension + } + + if let declarationFragments = extensionBlockSymbol[mixin: SymbolGraph.Symbol.DeclarationFragments.self]?.declarationFragments { + var prefixWithoutWhereClause: [SymbolGraph.Symbol.DeclarationFragments.Fragment] = Array(declarationFragments[..<3]) + + outer: for fragement in declarationFragments[3...] { + switch (fragement.kind, fragement.spelling) { + case (.typeIdentifier, _), + (.identifier, _), + (.text, "."): + prefixWithoutWhereClause.append(fragement) + default: + break outer + } + } + + newMixins[SymbolGraph.Symbol.DeclarationFragments.mixinKey] = SymbolGraph.Symbol.DeclarationFragments(declarationFragments: Array(prefixWithoutWhereClause)) + } + + return SymbolGraph.Symbol(identifier: .init(precise: id, + interfaceLanguage: extensionBlockSymbol.identifier.interfaceLanguage), + names: extensionBlockSymbol.names, + pathComponents: extensionBlockSymbol.pathComponents, + docComment: nil, + accessLevel: extensionBlockSymbol.accessLevel, + kind: .extendedType(for: extensionBlockSymbol), + mixins: newMixins) + } + + // mapping from the extensionTo.target to the TYPE_KIND.extension symbol's identifier.precise + var extendedTypeSymbolIdentifiers: [String: String] = [:] + + // we sort the relationships here because their order is not guaranteed to stay the same + // accross compilation processes and choosing the same base symbol (and its USR) is important + // to keeping (colliding) links stable + for extensionTo in extensionToRelationships.sorted(by: \.source) { + guard let extensionBlockSymbol = extensionBlockSymbols[extensionTo.source] else { + continue + } + + let extendedSymbolId = extendedTypeSymbolIdentifiers[extensionTo.target] ?? extensionBlockSymbol.identifier.precise + extendedTypeSymbolIdentifiers[extensionTo.target] = extendedSymbolId + + let symbol: SymbolGraph.Symbol = extendedTypeSymbols[extendedSymbolId]?.replacing(\.accessLevel) { oldSymbol in + max(oldSymbol.accessLevel, extensionBlockSymbol.accessLevel) + } ?? createExtendedTypeSymbol(extensionBlockSymbol, extendedSymbolId) + + extendedTypeSymbols[symbol.identifier.precise] = symbol + + extensionBlockToExtendedTypeMapping[extensionTo.source] = symbol.identifier.precise + extendedTypeToExtensionBlockMapping[symbol.identifier.precise] + = (extendedTypeToExtensionBlockMapping[symbol.identifier.precise] ?? []) + [extensionBlockSymbol.identifier.precise] + } + + return (extendedTypeSymbols, extensionBlockToExtendedTypeMapping, extendedTypeToExtensionBlockMapping) + } + + /// Updates the `anchor` of each relationship according to the given `keyMap`. + /// + /// If the `anchor` of a relationship cannot be found in the `keyMap`, the relationship is not modified. + /// + /// - Parameter anchor: usually either `\.source` or `\.target` + /// - Parameter relationships: the relationships to redirect + /// - Parameter keyMap: the mapping of old to new ids + private static func redirect(_ anchor: WritableKeyPath, + of relationships: inout RC, + using keyMap: [String: String]) where RC.Element == SymbolGraph.Relationship { + for index in relationships.indices { + let relationship = relationships[index] + + guard let newId = keyMap[relationship[keyPath: anchor]] else { + continue + } + + relationships[index] = relationship.replacing(anchor, with: newId) + } + } + + /// Synthesizes extended module symbols and declaredIn relationships on the given `symbolGraph` based on the given `extendedTypeSymbolIds`. + /// + /// Creates one symbol of kind ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedModule`` for all extended type symbols that + /// extend a type declared in the same module. The extended type symbols are connected with the extended module symbols using relationships of kind + /// ``SymbolKit/SymbolGraph/Relationship/declaredIn``. + private static func synthesizeExtendedModuleSymbolsAndDeclaredInRelationships(on symbolGraph: inout SymbolGraph, using extendedTypeSymbolIds: S) throws + where S.Element == String { + // extensionMixin.extendedModule to module.extension symbol's identifier.precise mapping + var moduleSymbolIdenitfiers: [String: String] = [:] + + // we sort the symbols here because their order is not guaranteed to stay the same + // accross compilation processes and choosing the same base symbol (and its USR) is important + // to keeping (colliding) links stable + for extendedTypeSymbolId in extendedTypeSymbolIds.sorted() { + guard let extendedTypeSymbol = symbolGraph.symbols[extendedTypeSymbolId] else { + continue + } + + guard let extensionMixin = extendedTypeSymbol[mixin: SymbolGraph.Symbol.Swift.Extension.self] else { + continue + } + + let id = moduleSymbolIdenitfiers[extensionMixin.extendedModule] ?? "s:m:" + extendedTypeSymbol.identifier.precise + moduleSymbolIdenitfiers[extensionMixin.extendedModule] = id + + + let symbol = symbolGraph.symbols[id]?.replacing(\.accessLevel) { oldSymbol in + max(oldSymbol.accessLevel, extendedTypeSymbol.accessLevel) + } ?? SymbolGraph.Symbol(identifier: .init(precise: id, interfaceLanguage: extendedTypeSymbol.identifier.interfaceLanguage), + names: .init(title: extensionMixin.extendedModule, navigator: nil, subHeading: nil, prose: nil), + pathComponents: [extensionMixin.extendedModule], + docComment: nil, + accessLevel: extendedTypeSymbol.accessLevel, + kind: .init(parsedIdentifier: .extendedModule, displayName: "Extended Module"), + mixins: [:]) + + symbolGraph.symbols[id] = symbol + + let relationship = SymbolGraph.Relationship(source: extendedTypeSymbol.identifier.precise, target: symbol.identifier.precise, kind: .declaredIn, targetFallback: symbol.names.title) + + symbolGraph.relationships.append(relationship) + } + } +} + +// MARK: Apply Mappings to SymbolGraph + +private extension SymbolGraph { + mutating func apply(compactMap include: (SymbolGraph.Symbol) throws -> SymbolGraph.Symbol?) rethrows { + for (key, symbol) in self.symbols { + self.symbols.removeValue(forKey: key) + if let newSymbol = try include(symbol) { + self.symbols[newSymbol.identifier.precise] = newSymbol + } + } + } +} + +// MARK: Replacing Convenience Functions + +private extension SymbolGraph.Symbol { + func replacing(_ keyPath: WritableKeyPath, with value: V) -> Self { + var new = self + new[keyPath: keyPath] = value + return new + } + + func replacing(_ keyPath: WritableKeyPath, with closue: (Self) -> V) -> Self { + var new = self + new[keyPath: keyPath] = closue(self) + return new + } +} + +private extension SymbolGraph.Relationship { + func replacing(_ keyPath: WritableKeyPath, with value: V) -> Self { + var new = self + new[keyPath: keyPath] = value + return new + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index e72edc972a..0ae32f8daf 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -44,13 +44,13 @@ struct SymbolGraphLoader { /// Loads all symbol graphs in the given bundle. /// + /// - Parameter decoder: A potentially customized `JSONDecoder` to be used for decoding. This decoder is only + /// used if the `decodingStrategy` is set to `concurrentlyAllFiles`! /// - Throws: If loading and decoding any of the symbol graph files throws, this method re-throws one of the encountered errors. - mutating func loadAll() throws { + mutating func loadAll(using decoder: JSONDecoder = JSONDecoder()) throws { let loadingLock = Lock() - let decoder = JSONDecoder() - var loadedGraphs = [URL: SymbolKit.SymbolGraph]() - let graphLoader = GraphCollector() + var loadedGraphs = [URL: (usesExtensionSymbolFormat: Bool?, graph: SymbolKit.SymbolGraph)]() var loadError: Error? let bundle = self.bundle let dataProvider = self.dataProvider @@ -73,14 +73,26 @@ struct SymbolGraphLoader { } // `moduleNameFor(_:at:)` is static because it's pure function. - let (moduleName, _) = Self.moduleNameFor(symbolGraph, at: symbolGraphURL) + let (moduleName, isMainSymbolGraph) = Self.moduleNameFor(symbolGraph, at: symbolGraphURL) // If the bundle provides availability defaults add symbol availability data. self.addDefaultAvailability(to: &symbolGraph, moduleName: moduleName) + // main symbol graphs are ambiguous + var usesExtensionSymbolFormat: Bool? = nil + + // transform extension block based structure emitted by the compiler to a + // custom structure where all extensions to the same type are collected in + // one extended type symbol + if !isMainSymbolGraph { + let containsExtensionSymbols = try ExtendedTypesFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&symbolGraph) + + // empty symbol graphs are ambiguous (but shouldn't exist) + usesExtensionSymbolFormat = symbolGraph.symbols.isEmpty ? nil : containsExtensionSymbols + } + // Store the decoded graph in `loadedGraphs` loadingLock.sync { - loadedGraphs[symbolGraphURL] = symbolGraph - graphLoader.mergeSymbolGraph(symbolGraph, at: symbolGraphURL) + loadedGraphs[symbolGraphURL] = (usesExtensionSymbolFormat, symbolGraph) } } catch { // If the symbol graph was invalid, store the error @@ -109,14 +121,41 @@ struct SymbolGraphLoader { bundle.symbolGraphURLs.forEach(loadGraphAtURL) } + // define an appropriate merging strategy based on the graph formats + let foundGraphUsingExtensionSymbolFormat = loadedGraphs.values.map(\.usesExtensionSymbolFormat).contains(true) + let foundGraphNotUsingExtensionSymbolFormat = loadedGraphs.values.map(\.usesExtensionSymbolFormat).contains(false) + + guard !foundGraphUsingExtensionSymbolFormat || !foundGraphNotUsingExtensionSymbolFormat else { + throw LoadingError.mixedGraphFormats + } + + let usingExtensionSymbolFormat = foundGraphUsingExtensionSymbolFormat + + let graphLoader = GraphCollector(extensionGraphAssociationStrategy: usingExtensionSymbolFormat ? .extendingGraph : .extendedGraph) + + // feed the loaded graphs into the `graphLoader` + for (url, (_, graph)) in loadedGraphs { + graphLoader.mergeSymbolGraph(graph, at: url) + } + // In case any of the symbol graphs errors, re-throw the error. // We will not process unexpected file formats. if let loadError = loadError { throw loadError } - self.symbolGraphs = loadedGraphs + self.symbolGraphs = loadedGraphs.mapValues(\.graph) (self.unifiedGraphs, self.graphLocations) = graphLoader.finishLoading() + + if usingExtensionSymbolFormat { + for (_, graph) in self.unifiedGraphs { + ExtendedTypesFormatTransformation.mergeExtendedModuleSymbolsFromDifferentFiles(graph) + } + } + } + + private enum LoadingError: Error { + case mixedGraphFormats } // Alias to declutter code @@ -147,7 +186,7 @@ struct SymbolGraphLoader { return (symbolGraph, isMainSymbolGraph) } - + /// If the bundle defines default availability for the symbols in the given symbol graph /// this method adds them to each of the symbols in the graph. private func addDefaultAvailability(to symbolGraph: inout SymbolGraph, moduleName: String) { @@ -266,30 +305,6 @@ struct SymbolGraphLoader { } return (moduleName, isMainSymbolGraph) } - - /// Returns the next-available symbol graph in the bundle. - /// - Parameter isMainSymbolGraph: An inout Boolean, if `false` the returned symbol graph is an extension to another symbol graph. - /// - Returns: The next symbol graph in the bundle and its URL, or `nil` if there are no more symbol graphs. - mutating func next(isMainSymbolGraph: inout Bool) throws -> (url: URL, symbolGraph: SymbolGraph)? { - isMainSymbolGraph = false - guard !symbolGraphs.isEmpty else { return nil } - - // The first remaining symbol graph, - // preferring main symbol graphs over extensions. - let url = symbolGraphs.keys - .sorted(by: { lhs, _ in - return !lhs.lastPathComponent.contains("@") - }) - .first! - - // Load the symbol graph - let symbolGraph: SymbolGraph - (symbolGraph, isMainSymbolGraph) = try loadSymbolGraph(at: url) - - // Remove the graph from the remaining queue and return. - symbolGraphs.removeValue(forKey: url) - return (url, symbolGraph) - } } extension SymbolGraph.SemanticVersion { diff --git a/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift b/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift index f6632f5dcc..24e4e51c3b 100644 --- a/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift +++ b/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift @@ -204,6 +204,11 @@ extension AutomaticCuration { case .`typealias`: return "Type Aliases" case .`var`: return "Variables" case .module: return "Modules" + case .extendedModule: return "Extended Modules" + case .extendedClass: return "Extended Classes" + case .extendedStructure: return "Extended Structures" + case .extendedEnumeration: return "Extended Enumerations" + case .extendedProtocol: return "Extended Protocols" default: return "Symbols" } } @@ -232,6 +237,12 @@ extension AutomaticCuration { .`typealias`, .`typeProperty`, .`typeMethod`, - .`enum` + .`enum`, + + .extendedModule, + .extendedClass, + .extendedProtocol, + .extendedStructure, + .extendedEnumeration, ] } diff --git a/Sources/SwiftDocC/Model/DocumentationNode.swift b/Sources/SwiftDocC/Model/DocumentationNode.swift index a1a1703b00..b7323cbe6e 100644 --- a/Sources/SwiftDocC/Model/DocumentationNode.swift +++ b/Sources/SwiftDocC/Model/DocumentationNode.swift @@ -467,8 +467,12 @@ public struct DocumentationNode { case .`typeSubscript`: return .typeSubscript case .`typealias`: return .typeAlias case .`var`: return .globalVariable - case .module: return .module + case .extendedModule: return .extendedModule + case .extendedStructure: return .extendedStructure + case .extendedClass: return .extendedClass + case .extendedEnumeration: return .extendedEnumeration + case .extendedProtocol: return .extendedProtocol default: return .unknown } } diff --git a/Sources/SwiftDocC/Model/Kind.swift b/Sources/SwiftDocC/Model/Kind.swift index 9be201f019..9260e1e8ee 100644 --- a/Sources/SwiftDocC/Model/Kind.swift +++ b/Sources/SwiftDocC/Model/Kind.swift @@ -155,6 +155,16 @@ extension DocumentationNode.Kind { public static let object = DocumentationNode.Kind(name: "Object", id: "org.swift.docc.kind.dictionary", isSymbol: true) /// A snippet. public static let snippet = DocumentationNode.Kind(name: "Snippet", id: "org.swift.docc.kind.snippet", isSymbol: true) + + public static let extendedModule = DocumentationNode.Kind(name: "Extended Module", id: "org.swift.docc.kind.extendedModule", isSymbol: true) + + public static let extendedStructure = DocumentationNode.Kind(name: "Extended Structure", id: "org.swift.docc.kind.extendedStructure", isSymbol: true) + + public static let extendedClass = DocumentationNode.Kind(name: "Extended Class", id: "org.swift.docc.kind.extendedClass", isSymbol: true) + + public static let extendedEnumeration = DocumentationNode.Kind(name: "Extended Enumeration", id: "org.swift.docc.kind.extendedEnumeration", isSymbol: true) + + public static let extendedProtocol = DocumentationNode.Kind(name: "Extended Protocol", id: "org.swift.docc.kind.extendedProtocol", isSymbol: true) /// The list of all known kinds of documentation nodes. /// - Note: The `unknown` value is not included. @@ -171,6 +181,8 @@ extension DocumentationNode.Kind { .enumerationCase, .initializer, .deinitializer, .instanceMethod, .instanceProperty, .instanceSubscript, .instanceVariable, .typeMethod, .typeProperty, .typeSubscript, // Data .buildSetting, .propertyListKey, + // Extended Symbols + .extendedModule, .extendedStructure, .extendedClass, .extendedEnumeration, .extendedProtocol, // Other .keyword, .restAPI, .tag, .propertyList, .object ] diff --git a/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift b/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift index 7f660b61d1..fc08aa252d 100644 --- a/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift +++ b/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift @@ -151,7 +151,7 @@ public class DocumentationContentRenderer { case .collectionGroup: return .collectionGroup case .technology, .technologyOverview: return .overview case .landingPage: return .article - case .module: return .collection + case .module, .extendedModule: return .collection case .onPageLandmark: return .pseudoSymbol case .root: return .collection case .sampleCode: return .sampleCode @@ -484,31 +484,10 @@ extension DocumentationContentRenderer { /// Applies Swift symbol navigator titles rules to a title. /// Will strip the typeIdentifier's precise identifier. static func navigatorTitle(for tokens: [DeclarationRenderSection.Token], symbolTitle: String) -> [DeclarationRenderSection.Token] { - guard tokens.count >= 3 else { - // Navigator title too short for a type symbol. - return tokens - } - // Replace kind "typeIdentifier" with "identifier" if the title matches the pattern: - // [keyword=class,protocol,enum,typealias,etc.][ ][typeIdentifier=Self] + // [keyword=class,protocol,enum,typealias,etc.][ ]([typeIdentifier=anchestor(Self)][.])*[typeIdentifier=Self] - if tokens[0].kind == DeclarationRenderSection.Token.Kind.keyword - && tokens[1].text == " " - && tokens[2].kind == DeclarationRenderSection.Token.Kind.typeIdentifier - && tokens[2].text == symbolTitle { - - // Replace the 2nd token with "identifier" kind. - return tokens.enumerated().map { pair -> DeclarationRenderSection.Token in - if pair.offset == 2 { - return DeclarationRenderSection.Token( - text: pair.element.text, - kind: .identifier - ) - } - return pair.element - } - } - return tokens + return tokens.mapNameFragmentsToIdentifierKind(matching: symbolTitle) } private static let initKeyword = DeclarationRenderSection.Token(text: "init", kind: .keyword) @@ -520,26 +499,9 @@ extension DocumentationContentRenderer { var tokens = tokens // 1. Replace kind "typeIdentifier" with "identifier" if the title matches the pattern: - // [keyword=class,protocol,enum,typealias,etc.][ ][typeIdentifier=Self] - if tokens.count >= 3 { - if tokens[0].kind == DeclarationRenderSection.Token.Kind.keyword - && tokens[1].text == " " - && tokens[2].kind == DeclarationRenderSection.Token.Kind.typeIdentifier - && tokens[2].text == symbolTitle { - - // Replace the 2nd token with "identifier" kind. - tokens = tokens.enumerated().map { pair -> DeclarationRenderSection.Token in - if pair.offset == 2 { - return DeclarationRenderSection.Token( - text: pair.element.text, - kind: .identifier, - preciseIdentifier: pair.element.preciseIdentifier - ) - } - return pair.element - } - } - } + // [keyword=class,protocol,enum,typealias,etc.][ ]([typeIdentifier=anchestor(Self)][.])*[typeIdentifier=Self] + tokens = tokens.mapNameFragmentsToIdentifierKind(matching: symbolTitle) + // 2. Map the first found "keyword=init" to an "identifier" kind to enable syntax highlighting. let parsedKind = SymbolGraph.Symbol.KindIdentifier(identifier: symbolKind) @@ -553,3 +515,82 @@ extension DocumentationContentRenderer { } } + +private extension Array where Element == DeclarationRenderSection.Token { + // Replaces kind "typeIdentifier" with "identifier" if the fragments matches the pattern: + // [keyword=class,protocol,enum,typealias,etc.][ ]([typeIdentifier=x_i)][.])*[typeIdentifier=x_i], + // where the x_i joined with separator "." equal the `symbolTitle` + func mapNameFragmentsToIdentifierKind(matching symbolTitle: String) -> Self { + let (includesTypeOrExtensionDeclaration, nameRange) = self.typeOrExtensionDeclaration() + + if includesTypeOrExtensionDeclaration + && self[nameRange].map(\.text).joined() == symbolTitle { + return self.enumerated().map { (index, token) -> DeclarationRenderSection.Token in + + if nameRange.contains(index) && token.kind == .typeIdentifier { + return DeclarationRenderSection.Token( + text: token.text, + kind: .identifier, + preciseIdentifier: token.preciseIdentifier + ) + } + + return token + } + } + + return self + } +} + +private extension Collection where Element == DeclarationRenderSection.Token, Index == Int { + func typeOrExtensionDeclaration() -> (includesTypeOrExtensionDeclaration: Bool, name: Range) { + self.reduce(into: TypeOrExtensionDeclarationNameExtractionSM(), { sm, token in sm.consume(token) }).result() + } +} + +private enum TypeOrExtensionDeclarationNameExtractionSM { + case initial + case illegal + case foundKeyword + case foundIdentifier(Int) + case expectIdentifier(Int) + case done(Range) + + init() { + self = .initial + } + + static let expectedNameStartIndex = 2 + + mutating func consume(_ token: DeclarationRenderSection.Token) { + switch (self, token.kind, token.text) { + case (.initial, .keyword, _): + self = .foundKeyword + case (.foundKeyword, .text, " "): + self = .expectIdentifier(Self.expectedNameStartIndex) + case let (.expectIdentifier(index), .identifier, _), + let (.expectIdentifier(index), .typeIdentifier, _): + self = .foundIdentifier(index+1) + case let (.foundIdentifier(index), .text, "."): + self = .expectIdentifier(index+1) + case let (.foundIdentifier(index), .text, _): + self = .done(.init(uncheckedBounds: (Self.expectedNameStartIndex, index))) + case let (.done(namerange), _, _): + self = .done(namerange) + default: + self = .illegal + } + } + + func result() -> (includesTypeOrExtensionDeclaration: Bool, name: Range) { + switch self { + case let .done(range): + return (true, range) + case let .foundIdentifier(index): + return (true, .init(uncheckedBounds: (Self.expectedNameStartIndex, index))) + default: + return (false, .init(uncheckedBounds: (0, 0))) + } + } +} diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator+Swift.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator+Swift.swift deleted file mode 100644 index 5b80484561..0000000000 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator+Swift.swift +++ /dev/null @@ -1,89 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 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 RenderNodeTranslator { - - /// Node translator extension with some exceptions to apply to Swift symbols. - enum Swift { - - /// Applies Swift symbol navigator titles rules to a title. - /// Will strip the typeIdentifier's precise identifier. - static func navigatorTitle(for tokens: [DeclarationRenderSection.Token], symbolTitle: String) -> [DeclarationRenderSection.Token] { - guard tokens.count >= 3 else { - // Navigator title too short for a type symbol. - return tokens - } - - // Replace kind "typeIdentifier" with "identifier" if the title matches the pattern: - // [keyword=class,protocol,enum,typealias,etc.][ ][typeIdentifier=Self] - - if tokens[0].kind == DeclarationRenderSection.Token.Kind.keyword - && tokens[1].text == " " - && tokens[2].kind == DeclarationRenderSection.Token.Kind.typeIdentifier - && tokens[2].text == symbolTitle { - - // Replace the 2nd token with "identifier" kind. - return tokens.enumerated().map { pair -> DeclarationRenderSection.Token in - if pair.offset == 2 { - return DeclarationRenderSection.Token( - text: pair.element.text, - kind: .identifier - ) - } - return pair.element - } - } - return tokens - } - - private static let initKeyword = DeclarationRenderSection.Token(text: "init", kind: .keyword) - private static let initIdentifier = DeclarationRenderSection.Token(text: "init", kind: .identifier) - - /// Applies Swift symbol subheading rules to a subheading. - /// Will preserve the typeIdentifier's precise identifier. - static func subHeading(for tokens: [DeclarationRenderSection.Token], symbolTitle: String, symbolKind: String) -> [DeclarationRenderSection.Token] { - var tokens = tokens - - // 1. Replace kind "typeIdentifier" with "identifier" if the title matches the pattern: - // [keyword=class,protocol,enum,typealias,etc.][ ][typeIdentifier=Self] - if tokens.count >= 3 { - if tokens[0].kind == DeclarationRenderSection.Token.Kind.keyword - && tokens[1].text == " " - && tokens[2].kind == DeclarationRenderSection.Token.Kind.typeIdentifier - && tokens[2].text == symbolTitle { - - // Replace the 2nd token with "identifier" kind. - tokens = tokens.enumerated().map { pair -> DeclarationRenderSection.Token in - if pair.offset == 2 { - return DeclarationRenderSection.Token( - text: pair.element.text, - kind: .identifier, - preciseIdentifier: pair.element.preciseIdentifier - ) - } - return pair.element - } - } - } - - // 2. Map the first found "keyword=init" to an "identifier" kind to enable syntax highlighting. - let parsedKind = SymbolGraph.Symbol.KindIdentifier(identifier: symbolKind) - if parsedKind == SymbolGraph.Symbol.KindIdentifier.`init`, - let initIndex = tokens.firstIndex(of: initKeyword) { - tokens[initIndex] = initIdentifier - } - - return tokens - } - } -} diff --git a/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift b/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift index 95a8dea9d7..33438cec5d 100644 --- a/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift @@ -16,13 +16,19 @@ import XCTest class AutomaticCurationTests: XCTestCase { func testAutomaticTopics() throws { // Create each kind of symbol and verify it gets its own topic group automatically + let decoder = JSONDecoder() + for kind in AutomaticCuration.groupKindOrder where kind != .module { + if !SymbolGraph.Symbol.KindIdentifier.allCases.contains(kind) { + decoder.register(symbolKinds: kind) + } + let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", excludingPaths: [], codeListings: [:], configureBundle: { url in let sidekitURL = url.appendingPathComponent("sidekit.symbols.json") let text = try String(contentsOf: sidekitURL) .replacingOccurrences(of: "\"identifier\" : \"swift.enum.case\"", with: "\"identifier\" : \"\(kind.identifier)\"") try text.write(to: sidekitURL, atomically: true, encoding: .utf8) - }) + }, decoder: decoder) let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) // Compile docs and verify the generated Topics section diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift index 46c305feab..5e89cdf608 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift @@ -1792,6 +1792,11 @@ let expected = """ text = text.replacingOccurrences(of: "\"relationships\" : [", with: """ "relationships" : [ + { + "source" : "s:7SideKit0A5ClassC10testSV", + "target" : "s:7SideKit0A5ClassC", + "kind" : "memberOf" + }, { "source" : "s:7SideKit0A5ClassC10testE", "target" : "s:7SideKit0A5ClassC", diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift index 15ca2d3185..c27d842f09 100644 --- a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift @@ -41,16 +41,13 @@ class SymbolGraphLoaderTests: XCTestCase { } var loader = try makeSymbolGraphLoader(symbolGraphURLs: symbolGraphURLs) - XCTAssertTrue(loader.symbolGraphs.isEmpty) + XCTAssertTrue(loader.unifiedGraphs.isEmpty) try loader.loadAll() var moduleNameFrequency = [String: Int]() - var isMainSymbolGraph = false - while let graph = try loader.next(isMainSymbolGraph: &isMainSymbolGraph) { - XCTAssertTrue(isMainSymbolGraph) - XCTAssertNotNil(graph) - moduleNameFrequency[graph.symbolGraph.module.name, default: 0] += 1 + for (_, graph) in loader.unifiedGraphs { + moduleNameFrequency[graph.moduleName, default: 0] += 1 } XCTAssertEqual(moduleNameFrequency, ["One": 1, "Two": 1, "Three": 1]) @@ -73,10 +70,8 @@ class SymbolGraphLoaderTests: XCTestCase { try loader.loadAll() var moduleNameFrequency = [String: Int]() - var isMainSymbolGraph = false - while let graph = try loader.next(isMainSymbolGraph: &isMainSymbolGraph) { - XCTAssertFalse(isMainSymbolGraph) - moduleNameFrequency[graph.symbolGraph.module.name, default: 0] += 1 + for (_, graph) in loader.unifiedGraphs { + moduleNameFrequency[graph.moduleName, default: 0] += 1 } // The loaded module should have the name of the module that was extended. @@ -109,12 +104,8 @@ class SymbolGraphLoaderTests: XCTestCase { try loader.loadAll() var moduleNameFrequency = [String: Int]() - var isMainSymbolGraph = false - while let graph = try loader.next(isMainSymbolGraph: &isMainSymbolGraph) { - XCTAssertNotNil(graph) - XCTAssertEqual(isMainSymbolGraph, !graph.url.lastPathComponent.contains("@")) - - moduleNameFrequency[graph.symbolGraph.module.name, default: 0] += 1 + for (_, graph) in loader.unifiedGraphs { + moduleNameFrequency[graph.moduleName, default: 0] += 1 } // All 4 modules should have different names @@ -141,13 +132,13 @@ class SymbolGraphLoaderTests: XCTestCase { try loader.loadAll() var loadedGraphs = 0 - var isMainSymbolGraph = false - while let graph = try loader.next(isMainSymbolGraph: &isMainSymbolGraph) { + + for (_, graph) in loader.unifiedGraphs { loadedGraphs += 1 - XCTAssertTrue(isMainSymbolGraph) - XCTAssertEqual(graph.symbolGraph.symbols.count, symbolGraph.symbols.count) - XCTAssertEqual(graph.symbolGraph.relationships.count, symbolGraph.relationships.count) + XCTAssertEqual(graph.symbols.count, symbolGraph.symbols.count) + XCTAssertEqual(graph.relationships.count, symbolGraph.relationships.count) } + XCTAssertEqual(loadedGraphs, 1000) } @@ -225,23 +216,23 @@ class SymbolGraphLoaderTests: XCTestCase { forResource: "MyKit@Foundation@_MyKit_Foundation.symbols", withExtension: "json", subdirectory: "Test Resources")! try FileManager.default.copyItem(at: bystanderSymbolGraphURL, to: url.appendingPathComponent("MyKit@Foundation@_MyKit_Foundation.symbols.json")) } - + var loader = try makeSymbolGraphLoader(symbolGraphURLs: bundle.symbolGraphURLs) try loader.loadAll() - - var isMainSymbolGraph = false - + // Verify both main and bystanders graphs are loaded - + var foundMainMyKitGraph = false var foundBystanderMyKitGraph = false - - while let graph = try loader.next(isMainSymbolGraph: &isMainSymbolGraph) { - if graph.symbolGraph.module.name == "MyKit" { - if graph.symbolGraph.module.bystanders == ["Foundation"] { - foundBystanderMyKitGraph = true - } else { - foundMainMyKitGraph = true + + for (_, graph) in loader.unifiedGraphs { + for (_, moduleData) in graph.moduleData { + if graph.moduleName == "MyKit" { + if moduleData.bystanders == ["Foundation"] { + foundBystanderMyKitGraph = true + } else { + foundMainMyKitGraph = true + } } } } @@ -259,14 +250,13 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertEqual(loader.decodingStrategy, .concurrentlyEachFileInBatches) - var isMainSymbolGraph = false - let symbolGraph = try loader.next(isMainSymbolGraph: &isMainSymbolGraph)!.symbolGraph + let symbolGraph = loader.unifiedGraphs.values.first! - XCTAssertEqual(symbolGraph.module.name, "AsyncMethods") + XCTAssertEqual(symbolGraph.moduleName, "AsyncMethods") XCTAssertEqual(symbolGraph.symbols.count, 1, "Only one of the symbols should be decoded") let symbol = try XCTUnwrap(symbolGraph.symbols.values.first) - let declaration = try XCTUnwrap(symbol.mixins[SymbolGraph.Symbol.DeclarationFragments.mixinKey] as? SymbolGraph.Symbol.DeclarationFragments) + let declaration = try XCTUnwrap(symbol.mixins.values.first?[SymbolGraph.Symbol.DeclarationFragments.mixinKey] as? SymbolGraph.Symbol.DeclarationFragments) XCTAssertEqual(shouldContainAsyncVariant, declaration.declarationFragments.contains(where: { fragment in fragment.kind == .keyword && fragment.spelling == "async" @@ -294,16 +284,15 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertEqual(loader.decodingStrategy, .concurrentlyEachFileInBatches) #endif - var isMainSymbolGraph = false var foundMainAsyncMethodsGraph = false - while let symbolGraph = try loader.next(isMainSymbolGraph: &isMainSymbolGraph)?.symbolGraph { - if symbolGraph.module.name == "AsyncMethods" { + for symbolGraph in loader.unifiedGraphs.values { + if symbolGraph.moduleName == "AsyncMethods" { foundMainAsyncMethodsGraph = true XCTAssertEqual(symbolGraph.symbols.count, 1, "Only one of the symbols should be decoded") let symbol = try XCTUnwrap(symbolGraph.symbols.values.first) - let declaration = try XCTUnwrap(symbol.mixins[SymbolGraph.Symbol.DeclarationFragments.mixinKey] as? SymbolGraph.Symbol.DeclarationFragments) + let declaration = try XCTUnwrap(symbol.mixins.values.first?[SymbolGraph.Symbol.DeclarationFragments.mixinKey] as? SymbolGraph.Symbol.DeclarationFragments) XCTAssertEqual(shouldContainAsyncVariant, declaration.declarationFragments.contains(where: { fragment in fragment.kind == .keyword && fragment.spelling == "async" diff --git a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslator+SwiftTests.swift b/Tests/SwiftDocCTests/Rendering/DocumentationContentRenderer+SwiftTests.swift similarity index 82% rename from Tests/SwiftDocCTests/Rendering/RenderNodeTranslator+SwiftTests.swift rename to Tests/SwiftDocCTests/Rendering/DocumentationContentRenderer+SwiftTests.swift index 0e3e3c4a00..b96e84a2fc 100644 --- a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslator+SwiftTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DocumentationContentRenderer+SwiftTests.swift @@ -12,7 +12,7 @@ import Foundation import XCTest @testable import SwiftDocC -class RenderNodeTranslator_SwiftTests: XCTestCase { +class DocumentationContentRenderer_SwiftTests: XCTestCase { // Tokens where the type name is incorrectly identified as "typeIdentifier" let typeIdentifierTokens: [DeclarationRenderSection.Token] = [ @@ -36,7 +36,7 @@ class RenderNodeTranslator_SwiftTests: XCTestCase { func testNavigatorTitle() { do { // Verify that the type's own name is mapped from "typeIdentifier" to "identifier" kind - let mapped = RenderNodeTranslator.Swift.navigatorTitle(for: typeIdentifierTokens, symbolTitle: "Test") + let mapped = DocumentationContentRenderer.Swift.navigatorTitle(for: typeIdentifierTokens, symbolTitle: "Test") XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .typeIdentifier]) XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "Test", " : ", "Object"]) @@ -44,7 +44,7 @@ class RenderNodeTranslator_SwiftTests: XCTestCase { do { // Verify that the type's own name is left as-is if the expect kind is vended - let mapped = RenderNodeTranslator.Swift.navigatorTitle(for: identifierTokens, symbolTitle: "Test") + let mapped = DocumentationContentRenderer.Swift.navigatorTitle(for: identifierTokens, symbolTitle: "Test") XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .typeIdentifier]) XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "Test", " : ", "Object"]) @@ -55,7 +55,7 @@ class RenderNodeTranslator_SwiftTests: XCTestCase { func testSubHeading() { do { // Verify that the type's own name is mapped from "typeIdentifier" to "identifier" kind - let mapped = RenderNodeTranslator.Swift.subHeading(for: typeIdentifierTokens, symbolTitle: "Test", symbolKind: "swift.class") + let mapped = DocumentationContentRenderer.Swift.subHeading(for: typeIdentifierTokens, symbolTitle: "Test", symbolKind: "swift.class") XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .typeIdentifier]) XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "Test", " : ", "Object"]) @@ -63,7 +63,7 @@ class RenderNodeTranslator_SwiftTests: XCTestCase { do { // Verify that the type's own name is not-mapped from "identifier" kind - let mapped = RenderNodeTranslator.Swift.subHeading(for: identifierTokens, symbolTitle: "Test", symbolKind: "swift.class") + let mapped = DocumentationContentRenderer.Swift.subHeading(for: identifierTokens, symbolTitle: "Test", symbolKind: "swift.class") XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .typeIdentifier]) XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "Test", " : ", "Object"]) @@ -90,7 +90,7 @@ class RenderNodeTranslator_SwiftTests: XCTestCase { func testSubHeadingInit() { do { // Verify that the "init" keyword is mapped to an identifier token to enable syntax highlight - let mapped = RenderNodeTranslator.Swift.subHeading(for: initAsKeywordTokens, symbolTitle: "Test", symbolKind: "swift.init") + let mapped = DocumentationContentRenderer.Swift.subHeading(for: initAsKeywordTokens, symbolTitle: "Test", symbolKind: "swift.init") XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text]) XCTAssertEqual(mapped.map { $0.text }, ["convenience", " ", "init", "()"]) @@ -98,7 +98,7 @@ class RenderNodeTranslator_SwiftTests: XCTestCase { do { // Verify that if the "init" has correct kind it is not mapped to another kind - let mapped = RenderNodeTranslator.Swift.subHeading(for: initAsIdentifierTokens, symbolTitle: "Test", symbolKind: "swift.init") + let mapped = DocumentationContentRenderer.Swift.subHeading(for: initAsIdentifierTokens, symbolTitle: "Test", symbolKind: "swift.init") XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text]) XCTAssertEqual(mapped.map { $0.text }, ["convenience", " ", "init", "()"]) diff --git a/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift b/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift index 945c73cd81..6b547f2454 100644 --- a/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift +++ b/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift @@ -15,12 +15,19 @@ import XCTest extension XCTestCase { /// Loads a documentation bundle from the given source URL and creates a documentation context. - func loadBundle(from bundleURL: URL, codeListings: [String : AttributedCodeListing] = [:], externalResolvers: [String: ExternalReferenceResolver] = [:], externalSymbolResolver: ExternalSymbolResolver? = nil, diagnosticFilterLevel: DiagnosticSeverity = .hint, configureContext: ((DocumentationContext) throws -> Void)? = nil) throws -> (URL, DocumentationBundle, DocumentationContext) { + func loadBundle(from bundleURL: URL, + codeListings: [String : AttributedCodeListing] = [:], + externalResolvers: [String: ExternalReferenceResolver] = [:], + externalSymbolResolver: ExternalSymbolResolver? = nil, + diagnosticFilterLevel: DiagnosticSeverity = .hint, + configureContext: ((DocumentationContext) throws -> Void)? = nil, + decoder: JSONDecoder = JSONDecoder()) throws -> (URL, DocumentationBundle, DocumentationContext) { let workspace = DocumentationWorkspace() let context = try DocumentationContext(dataProvider: workspace, diagnosticEngine: DiagnosticEngine(filterLevel: diagnosticFilterLevel)) context.externalReferenceResolvers = externalResolvers context.externalSymbolResolver = externalSymbolResolver context.externalMetadata.diagnosticLevel = diagnosticFilterLevel + context.decoder = decoder try configureContext?(context) // Load the bundle using automatic discovery let automaticDataProvider = try LocalFileSystemDataProvider(rootURL: bundleURL) @@ -32,7 +39,13 @@ extension XCTestCase { return (bundleURL, bundle, context) } - func testBundleAndContext(copying name: String, excludingPaths excludedPaths: [String] = [], codeListings: [String : AttributedCodeListing] = [:], externalResolvers: [BundleIdentifier : ExternalReferenceResolver] = [:], externalSymbolResolver: ExternalSymbolResolver? = nil, configureBundle: ((URL) throws -> Void)? = nil) throws -> (URL, DocumentationBundle, DocumentationContext) { + func testBundleAndContext(copying name: String, + excludingPaths excludedPaths: [String] = [], + codeListings: [String : AttributedCodeListing] = [:], + externalResolvers: [BundleIdentifier : ExternalReferenceResolver] = [:], + externalSymbolResolver: ExternalSymbolResolver? = nil, + configureBundle: ((URL) throws -> Void)? = nil, + decoder: JSONDecoder = JSONDecoder()) throws -> (URL, DocumentationBundle, DocumentationContext) { let sourceURL = try XCTUnwrap(Bundle.module.url( forResource: name, withExtension: "docc", subdirectory: "Test Bundles")) @@ -52,7 +65,7 @@ extension XCTestCase { // Do any additional setup to the custom bundle - adding, modifying files, etc try configureBundle?(bundleURL) - return try loadBundle(from: bundleURL, codeListings: codeListings, externalResolvers: externalResolvers, externalSymbolResolver: externalSymbolResolver) + return try loadBundle(from: bundleURL, codeListings: codeListings, externalResolvers: externalResolvers, externalSymbolResolver: externalSymbolResolver, decoder: decoder) } func testBundleAndContext(named name: String, codeListings: [String : AttributedCodeListing] = [:], externalResolvers: [String: ExternalReferenceResolver] = [:]) throws -> (DocumentationBundle, DocumentationContext) { From 16d1ae2d8bd0acd05384229033d23315bb5c8ae1 Mon Sep 17 00:00:00 2001 From: Max Obermeier Date: Thu, 28 Jul 2022 15:46:34 +0200 Subject: [PATCH 2/7] add tests relevant to handling extensions to external types - test detection of the Extension Block Symbol Graph Format and application of the ExtendedTypesFormatTransformation in SymbolGraphLoader - test the ExtendedTypesFormatTransformation - test handling of collisions resulting from extensions to nested external types in DocumentationContext - test tests for absolute/relative reference resolution for ambiguous relative references --- Package.resolved | 2 +- Package.swift | 4 +- .../Infrastructure/DocumentationContext.swift | 238 --------------- .../DocumentationCacheBasedLinkResolver.swift | 86 +++++- .../Link Resolution/PathHierarchy.swift | 108 +++++-- .../PathHierarchyBasedLinkResolver.swift | 2 +- .../ExtendedTypesFormatTransformation.swift | 2 +- .../SymbolGraphCreation.swift | 56 ++++ .../DocumentationContextTests.swift | 23 ++ .../ReferenceResolverTests.swift | 85 ++++++ ...tendedTypesFormatTransformationTests.swift | 288 ++++++++++++++++++ .../SymbolGraph/SymbolGraphLoaderTests.swift | 156 ++++++++-- ...WithCollisionBasedOnNestedTypeExtension.md | 5 + ...ionBasedOnNestedTypeExtension.symbols.json | 1 + ...sion@DependencyWithNestedType.symbols.json | 1 + .../Info.plist | 14 + .../BundleWithRelativePathAmbiguity.md | 67 ++++ ...ndleWithRelativePathAmbiguity.symbols.json | 1 + ...ativePathAmbiguity@Dependency.symbols.json | 1 + .../Dependency.symbols.json | 1 + .../Info.plist | 14 + 21 files changed, 862 insertions(+), 293 deletions(-) create mode 100644 Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift create mode 100644 Tests/SwiftDocCTests/Infrastructure/SymbolGraph/ExtendedTypesFormatTransformationTests.swift create mode 100644 Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension.md create mode 100644 Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension.symbols.json create mode 100644 Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension@DependencyWithNestedType.symbols.json create mode 100644 Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/Info.plist create mode 100644 Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.md create mode 100644 Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.symbols.json create mode 100644 Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity@Dependency.symbols.json create mode 100644 Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/Dependency.symbols.json create mode 100644 Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/Info.plist diff --git a/Package.resolved b/Package.resolved index 324fe64579..fb5f773a6f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -42,7 +42,7 @@ "repositoryURL": "https://github.com/themomax/swift-docc-symbolkit", "state": { "branch": "docc-extensions-to-external-types-base", - "revision": "dba9c2dc32a3c02d5be84b67e9b2c4af108d3ba0", + "revision": "0a67e26bb38d4f40c08bbf6a119affa3e02367ad", "version": null } }, diff --git a/Package.swift b/Package.swift index 266ba432c5..70c6f413c6 100644 --- a/Package.swift +++ b/Package.swift @@ -79,7 +79,9 @@ let package = Package( // Test utility library .target( name: "SwiftDocCTestUtilities", - dependencies: []), + dependencies: [ + "SymbolKit" + ]), // Command-line tool .executableTarget( diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index e90896f496..bb0fa6ea1b 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -1021,244 +1021,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { return ((reference, symbol.uniqueIdentifier, graphNode, documentation), []) } - - /// Returns a map between symbol identifiers and topic references. - /// - /// - Parameters: - /// - symbolGraph: The complete symbol graph to walk through. - /// - bundle: The bundle to use when creating symbol references. - func referencesForSymbols(in unifiedGraphs: [String: UnifiedSymbolGraph], bundle: DocumentationBundle) -> [SymbolGraph.Symbol.Identifier: [ResolvedTopicReference]] { - // The implementation of this function is fairly tricky because in most cases it has to preserve past behavior. - // - // This is because symbol references bake the disambiguators into the path, making it the only version of that - // path that resolves to that symbol. In other words, a reference with "too few" or "too many" disambiguators - // will fail to resolve. Changing what's considered the "correct" disambiguators for a symbol means that links - // that used to resolve will break with the new behavior. - // - // The tests in `SymbolDisambiguationTests` cover the known behaviors that should be preserved. - // - // The real solution to this problem is to allow symbol links to over-specify disambiguators and improve the - // diagnostics when symbol links are ambiguous. (rdar://78518537) - // That will allow for fixes to the least amount of disambiguation without breaking existing links. - - - // The current implementation works in 3 phases: - // - First, it computes the paths without disambiguators to identify colliding paths. - // - Second, it computes the "correct" disambiguators for each collision. - // - Lastly, it joins together the results in a stable order to avoid indeterministic behavior. - - - let totalSymbolCount = unifiedGraphs.values.map { $0.symbols.count }.reduce(0, +) - - /// Temporary data structure to hold input to compute paths with or without disambiguation. - struct PathCollisionInfo { - let symbol: UnifiedSymbolGraph.Symbol - let moduleName: String - var languages: Set - } - var pathCollisionInfo = [[String]: [PathCollisionInfo]]() - pathCollisionInfo.reserveCapacity(totalSymbolCount) - - // Group symbols by path from all of the available symbol graphs - for (moduleName, symbolGraph) in unifiedGraphs { - let symbols = Array(symbolGraph.symbols.values) - - let referenceMap = symbols.concurrentMap { symbol in - (symbol, referencesWithoutDisambiguationFor(symbol, moduleName: moduleName, bundle: bundle)) - }.reduce(into: [String: [SourceLanguage: ResolvedTopicReference]](), { result, next in - let (symbol, references) = next - for reference in references { - result[symbol.uniqueIdentifier, default: [:]][reference.sourceLanguage] = reference - } - }) - - let parentMap = symbolGraph.relationshipsByLanguage.reduce(into: [String: [SourceLanguage: String]](), { parentMap, next in - let (selector, relationships) = next - guard let language = SourceLanguage(knownLanguageIdentifier: selector.interfaceLanguage) else { - return - } - - for relationship in relationships { - switch relationship.kind { - case .memberOf, .requirementOf, .declaredIn: - parentMap[relationship.source, default: [:]][language] = relationship.target - default: - break - } - } - }) - - let pathsAndLanguages: [[([String], SourceLanguage)]] = symbols.concurrentMap { symbol in - guard let references = referenceMap[symbol.uniqueIdentifier] else { - return [] - } - - return references.map { language, reference in - var prefixLength: Int - if let parentId = parentMap[symbol.uniqueIdentifier]?[language], - let parentReference = referenceMap[parentId]?[language] ?? referenceMap[parentId]?.values.first { - // This is a child of some other symbol - prefixLength = parentReference.pathComponents.count - } else { - // This is a top-level symbol or another symbol without parent (e.g. default implementation) - prefixLength = reference.pathComponents.count-1 - } - - // PathComponents can have prefixes which are not known locally. In that case, - // the "empty" segments will be cut out later on. We follow the same logic here, as otherwise - // some collisions would not be detected. - // E.g. consider an extension to an external nested type `SomeModule.SomeStruct.SomeStruct`. The - // parent of this extended type symbol is `SomeModule`, however, the path for the extended type symbol - // is `SomeModule/SomeStruct/SomeStruct`, later on, this will change to `SomeModule/SomeStruct`. Now, if - // we also extend `SomeModule.SomeStruct`, the paths for both extensions could collide. To recognize (and resolve) - // the collision here, we work with the same, shortened paths. - return ((reference.pathComponents[0.. 1, disambiguationSuffix == (false, false) { - let symbolReference = SymbolReference( - collisionInfo.symbol.uniqueIdentifier, - interfaceLanguages: collisionInfo.symbol.sourceLanguages, - defaultSymbol: collisionInfo.symbol.defaultSymbol, - shouldAddHash: false, - shouldAddKind: false - ) - - resultGroups[collisionInfo.symbol.defaultIdentifier, default: []].append( - IntermediateResultGroup( - conflictingSymbolLanguage: language, - disambiguatedReferences:[ResolvedTopicReference(symbolReference: symbolReference, moduleName: collisionInfo.moduleName, bundle: bundle)] - ) - ) - continue - } - - // Emit the disambiguated references for all languages for this symbol's collision. - var symbolSelectors = [collisionInfo.symbol.defaultSelector!] - for selector in collisionInfo.symbol.mainGraphSelectors where !symbolSelectors.contains(selector) { - symbolSelectors.append(selector) - } - symbolSelectors = symbolSelectors.filter { collisionInfo.languages.contains(SourceLanguage(id: $0.interfaceLanguage)) } - - resultGroups[collisionInfo.symbol.defaultIdentifier, default: []].append( - IntermediateResultGroup( - conflictingSymbolLanguage: language, - disambiguatedReferences: symbolSelectors.map { selector in - let symbolReference = SymbolReference( - collisionInfo.symbol.uniqueIdentifier, - interfaceLanguages: collisionInfo.symbol.sourceLanguages, - defaultSymbol: collisionInfo.symbol.symbol(forSelector: selector), - shouldAddHash: disambiguationSuffix.shouldAddIdHash, - shouldAddKind: disambiguationSuffix.shouldAddKind - ) - return ResolvedTopicReference(symbolReference: symbolReference, moduleName: collisionInfo.moduleName, bundle: bundle) - } - ) - ) - } - } - - return resultGroups.mapValues({ - return $0.sorted(by: { lhs, rhs in - switch (lhs.conflictingSymbolLanguage, rhs.conflictingSymbolLanguage) { - // If only one result group is Swift, that comes before the other result. - case (.swift, let other) where other != .swift: - return true - case (let other, .swift) where other != .swift: - return false - - // Otherwise, compare the first path to ensure a deterministic order. - default: - return lhs.disambiguatedReferences[0].path < rhs.disambiguatedReferences[0].path - } - }).flatMap({ $0.disambiguatedReferences }) - }) - } - - private func referencesWithoutDisambiguationFor(_ symbol: UnifiedSymbolGraph.Symbol, moduleName: String, bundle: DocumentationBundle) -> [ResolvedTopicReference] { - if let pathComponents = knownDisambiguatedSymbolPathComponents?[symbol.uniqueIdentifier], - let componentsCount = symbol.defaultSymbol?.pathComponents.count, - pathComponents.count == componentsCount - { - let symbolReference = SymbolReference(pathComponents: pathComponents, interfaceLanguages: symbol.sourceLanguages) - return [ResolvedTopicReference(symbolReference: symbolReference, moduleName: moduleName, bundle: bundle)] - } - - // A unified symbol that exist in multiple languages may have multiple references. - - // Find all of the relevant selectors, starting with the `defaultSelector`. - // Any reference after the first is considered an alias/alternative to the first reference - // and will resolve to the first reference. - var symbolSelectors = [symbol.defaultSelector] - for selector in symbol.mainGraphSelectors where !symbolSelectors.contains(selector) { - symbolSelectors.append(selector) - } - - return symbolSelectors.map { selector in - let defaultSymbol = symbol.symbol(forSelector: selector)! - let symbolReference = SymbolReference( - symbol.uniqueIdentifier, - interfaceLanguages: symbol.sourceLanguages.filter { $0 == SourceLanguage(id: defaultSymbol.identifier.interfaceLanguage) }, - defaultSymbol: defaultSymbol, - shouldAddHash: false, - shouldAddKind: false - ) - return ResolvedTopicReference(symbolReference: symbolReference, moduleName: moduleName, bundle: bundle) - } - } private func parentChildRelationship(from edge: SymbolGraph.Relationship) -> (ResolvedTopicReference, ResolvedTopicReference)? { // Filter only parent <-> child edges diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift index 7c1ef5d3ca..89d713d503 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift @@ -78,8 +78,17 @@ final class DocumentationCacheBasedLinkResolver { func referenceFor(absoluteSymbolPath path: String, parent: ResolvedTopicReference) -> ResolvedTopicReference? { // Check if `destination` is a known absolute reference URL. if let match = referencesIndex[path] { return match } - - // Check if `destination` is a known absolute symbol path. + + // Check if `destination` is a known absolute symbol path... + if !path.hasPrefix("/") && parent.pathComponents.count > 2 { + // ...in the parent's module + let parentModule = parent.pathComponents[2] + let referenceURLString = "doc://\(parent.bundleIdentifier)/documentation/\(parentModule)/\(path)" + if let reference = referencesIndex[referenceURLString] { + return reference + } + } + // ...globally let referenceURLString = "doc://\(parent.bundleIdentifier)/documentation/\(path.hasPrefix("/") ? String(path.dropFirst()) : path)" return referencesIndex[referenceURLString] } @@ -299,8 +308,6 @@ final class DocumentationCacheBasedLinkResolver { } - // MARK: Symbol reference creation - /// Returns a map between symbol identifiers and topic references. /// /// - Parameters: @@ -324,7 +331,7 @@ final class DocumentationCacheBasedLinkResolver { // The current implementation works in 3 phases: // - First, it computes the paths without disambiguators to identify colliding paths. // - Second, it computes the "correct" disambiguators for each collision. - // - Lastly, it joins together the results in a stable order to avoid non-deterministic behavior. + // - Lastly, it joins together the results in a stable order to avoid indeterministic behavior. let totalSymbolCount = unifiedGraphs.values.map { $0.symbols.count }.reduce(0, +) @@ -335,15 +342,65 @@ final class DocumentationCacheBasedLinkResolver { let moduleName: String var languages: Set } - var pathCollisionInfo = [String: [PathCollisionInfo]]() + var pathCollisionInfo = [[String]: [PathCollisionInfo]]() pathCollisionInfo.reserveCapacity(totalSymbolCount) // Group symbols by path from all of the available symbol graphs for (moduleName, symbolGraph) in unifiedGraphs { let symbols = Array(symbolGraph.symbols.values) - let pathsAndLanguages: [[(String, SourceLanguage)]] = symbols.concurrentMap { referencesWithoutDisambiguationFor($0, moduleName: moduleName, bundle: bundle, context: context).map { - ($0.path.lowercased(), $0.sourceLanguage) - } } + + let referenceMap = symbols.concurrentMap { symbol in + (symbol, referencesWithoutDisambiguationFor(symbol, moduleName: moduleName, bundle: bundle, context: context)) + }.reduce(into: [String: [SourceLanguage: ResolvedTopicReference]](), { result, next in + let (symbol, references) = next + for reference in references { + result[symbol.uniqueIdentifier, default: [:]][reference.sourceLanguage] = reference + } + }) + + let parentMap = symbolGraph.relationshipsByLanguage.reduce(into: [String: [SourceLanguage: String]](), { parentMap, next in + let (selector, relationships) = next + guard let language = SourceLanguage(knownLanguageIdentifier: selector.interfaceLanguage) else { + return + } + + for relationship in relationships { + switch relationship.kind { + case .memberOf, .requirementOf, .declaredIn: + parentMap[relationship.source, default: [:]][language] = relationship.target + default: + break + } + } + }) + + let pathsAndLanguages: [[([String], SourceLanguage)]] = symbols.concurrentMap { symbol in + guard let references = referenceMap[symbol.uniqueIdentifier] else { + return [] + } + + return references.map { language, reference in + var prefixLength: Int + if let parentId = parentMap[symbol.uniqueIdentifier]?[language], + let parentReference = referenceMap[parentId]?[language] ?? referenceMap[parentId]?.values.first { + // This is a child of some other symbol + prefixLength = parentReference.pathComponents.count + } else { + // This is a top-level symbol or another symbol without parent (e.g. default implementation) + prefixLength = reference.pathComponents.count-1 + } + + // PathComponents can have prefixes which are not known locally. In that case, + // the "empty" segments will be cut out later on. We follow the same logic here, as otherwise + // some collisions would not be detected. + // E.g. consider an extension to an external nested type `SomeModule.SomeStruct.SomeStruct`. The + // parent of this extended type symbol is `SomeModule`, however, the path for the extended type symbol + // is `SomeModule/SomeStruct/SomeStruct`, later on, this will change to `SomeModule/SomeStruct`. Now, if + // we also extend `SomeModule.SomeStruct`, the paths for both extensions could collide. To recognize (and resolve) + // the collision here, we work with the same, shortened paths. + return ((reference.pathComponents[0.. 3, - // Fetch the symbol's parent - let parentReference = try symbolsURLHierarchy.parent(of: reference), - // If the parent path matches the current reference path, bail out - parentReference.pathComponents != reference.pathComponents.dropLast() + // Fetch the symbol's parent + let parentReference = try symbolsURLHierarchy.parent(of: reference), + // If the parent path matches the current reference path, bail out + parentReference.pathComponents != reference.pathComponents.dropLast(), + // If the parent is not from the same module (because we're dealing with a + // default implementation of an external protocol), bail out + parentReference.pathComponents[..<3] == reference.pathComponents[..<3] else { return reference } // Build an up to date reference path for the current node based on the parent path diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift index 754d8e592b..264c72b3f7 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift @@ -165,6 +165,17 @@ struct PathHierarchy { parent = child components = components.dropFirst() } + + // Symbols corresponding to nested types may appear outside of their original context, where + // their parent type may not be present. This happens e.g. for extensions to external nested + // types. In such cases, the nested type should be a direct child of whatever its parent is + // in this different context. Any other behavior would lead to truly empty pages. + var titlePrefix = node.symbol!.title.split(separator: ".").dropLast() + while !components.isEmpty && !titlePrefix.isEmpty && components.last! == titlePrefix.last! { + titlePrefix = titlePrefix.dropLast() + components = components.dropLast() + } + for component in components { let component = Self.parse(pathComponent: component[...]) let nodeWithoutSymbol = Node(name: component.name) @@ -294,17 +305,36 @@ struct PathHierarchy { } private func findNode(path rawPath: String, parent: ResolvedIdentifier?, onlyFindSymbols: Bool) throws -> Node { - // The search for a documentation element can be though of as 3 steps: // First, parse the path into structured path components. let (path, isAbsolute) = Self.parse(path: rawPath) guard !path.isEmpty else { throw Error.notFound(availableChildren: []) } - // Second, find the node to start the search relative to. - // This may consume or or more path components. See implementation for details. - var remaining = path[...] - var node = try findRoot(parentID: parent, remaining: &remaining, isAbsolute: isAbsolute, onlyFindSymbols: onlyFindSymbols) + // Second, we try to the node matching the path. This is done by first finding + // the root of the (possibly relative) path and then searching for the child + // from that root. + // A relative path could have multiple root candidates (where the first + // component is a match). We start searching at the `parent`, working + // our way up the tree, trying to find a child for each root candidate. + // This function reports all errors found on the way and - if successful - the + // matching node. + let (node, errors) = try searchForChildOnAllPossibleRoots(parentID: parent, path: path, isAbsolute: isAbsolute, onlyFindSymbols: onlyFindSymbols) + + if let node = node { + return node + } + + // Currently, we only report the first error, which corresponds to + // the root candidate closest to the `parent`. Aggregating errors could + // help giving more precise suggestions in the future. + throw errors.first! + } + + /// Tries to find a node in the subtree of `node` where `remaining` is the relative path from `node` to the child. + private func findChild(of node: Node, remaining: ArraySlice) throws -> Node { + var node = node + var remaining = remaining // Third, search for the match relative to the start node. if remaining.isEmpty { @@ -428,11 +458,13 @@ struct PathHierarchy { /// /// - Parameters: /// - parentID: An optional ID of the node to start the search relative to. - /// - remaining: The parsed path components. + /// - path: The parsed path components. /// - isAbsolute: If the parsed path represent an absolute documentation link. /// - onlyFindSymbols: If symbol results are required. /// - Returns: The node to start the relative search relative to. - private func findRoot(parentID: ResolvedIdentifier?, remaining: inout ArraySlice, isAbsolute: Bool, onlyFindSymbols: Bool) throws -> Node { + private func searchForChildOnAllPossibleRoots(parentID: ResolvedIdentifier?, path: [PathComponent], isAbsolute: Bool, onlyFindSymbols: Bool) throws -> (Node?, [Error]) { + var remaining = path[...] + // If the first path component is "tutorials" or "documentation" then that let isKnownTutorialPath = remaining.first!.full == "tutorials" let isKnownDocumentationPath = remaining.first!.full == "documentation" @@ -455,28 +487,28 @@ struct PathHierarchy { } } remaining = remaining.dropFirst() - return articlesContainer + return (try findChild(of: articlesContainer, remaining: remaining) , []) } else if articlesContainer.children.keys.contains(component.name) || articlesContainer.children.keys.contains(component.full) { - return articlesContainer + return (try findChild(of: articlesContainer, remaining: remaining) , []) } } if !isKnownDocumentationPath { if tutorialContainer.name == component.name || tutorialContainer.name == component.full { remaining = remaining.dropFirst() - return tutorialContainer + return (try findChild(of: tutorialContainer, remaining: remaining) , []) } else if tutorialContainer.children.keys.contains(component.name) || tutorialContainer.children.keys.contains(component.full) { - return tutorialContainer + return (try findChild(of: tutorialContainer, remaining: remaining) , []) } // The parent for tutorial overviews / technologies is "tutorials" which has already been removed above, so no need to check against that name. else if tutorialOverviewContainer.children.keys.contains(component.name) || tutorialOverviewContainer.children.keys.contains(component.full) { - return tutorialOverviewContainer + return (try findChild(of: tutorialOverviewContainer, remaining: remaining) , []) } } if !isKnownTutorialPath && isAbsolute { // If this is an absolute non-tutorial link, then the first component will be a module name. if let matched = modules[component.name] ?? modules[component.full] { remaining = remaining.dropFirst() - return matched + return (try findChild(of: matched, remaining: remaining) , []) } } } @@ -492,37 +524,73 @@ struct PathHierarchy { } if let parentID = parentID { + // We're dealing with a relative path, so search will be a bit more complicated. + // Starting from the parent, we ascend in the tree trying to find a node that matches + // our search path's first component. If we find one, we try to obtain the descendant + // matching the remainder of the search path using `findChild(of:remaining:)`. If that + // fails, we continue the search up the tree, after we've saved the error to be returned + // later. + + // Errors collected during the process + var errors: [Error] = [] + // 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. var parentNode = lookup[parentID]! let firstComponent = remaining.first! if matches(node: parentNode, component: firstComponent) { remaining = remaining.dropFirst() - return parentNode + do { + return (try findChild(of: parentNode, remaining: remaining), errors) + } catch let error as Error { + errors.append(error) + } } - while !parentNode.children.keys.contains(firstComponent.name) && !parentNode.children.keys.contains(firstComponent.full) { + while true { + if parentNode.children.keys.contains(firstComponent.name) || parentNode.children.keys.contains(firstComponent.full) { + do { + return (try findChild(of: parentNode, remaining: remaining), errors) + } catch let error as Error { + errors.append(error) + } + } + guard let parent = parentNode.parent else { if matches(node: parentNode, component: firstComponent){ remaining = remaining.dropFirst() - return parentNode + do { + return (try findChild(of: parentNode, remaining: remaining), errors) + } catch let error as Error { + errors.append(error) + } } if let matched = modules[component.name] ?? modules[component.full] { remaining = remaining.dropFirst() - return matched + do { + return (try findChild(of: matched, remaining: remaining), errors) + } catch let error as Error { + errors.append(error) + } } + // No node up the hierarchy from the provided parent has a child that matches the first path component. // Go back to the provided parent node for diagnostic information about its available children. parentNode = lookup[parentID]! - throw Error.partialResult(partialResult: parentNode, remainingSubpath: remaining.map({ $0.full }).joined(separator: "/"), availableChildren: parentNode.children.keys.sorted(by: availableChildNameIsBefore)) + + // We've reached the top of the tree...we return all the errors we obtained in the process along with + // the final error providing the partial result. + + errors.append(Error.partialResult(partialResult: parentNode, remainingSubpath: remaining.map({ $0.full }).joined(separator: "/"), availableChildren: parentNode.children.keys.sorted(by: availableChildNameIsBefore))) + return (nil, errors) } + parentNode = parent } - return parentNode } // If no parent ID was provided, check if the first path component is a module name. if let matched = modules[component.name] ?? modules[component.full] { remaining = remaining.dropFirst() - return matched + return (try findChild(of: matched, remaining: remaining) , []) } // No place to start the search from could be found. diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift index 6f45d4b60c..efc378efbd 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift @@ -209,7 +209,7 @@ final class PathHierarchyBasedLinkResolver { } do { - let parentID = resolvedReferenceMap[parent] + let parentID = unresolvedReference.path.hasPrefix("/") ? nil : resolvedReferenceMap[parent] let found = try pathHierarchy.find(path: Self.path(for: unresolvedReference), parent: parentID, onlyFindSymbols: isCurrentlyResolvingSymbolLink) let foundReference = resolvedReferenceMap[found]! diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift index c15954cbf2..f42cdc371e 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift @@ -75,7 +75,7 @@ extension ExtendedTypesFormatTransformation { var keyMap: [String: String] = [:] // choose canonical extended module symbol for each moduleName - for symbol in symbolGraph.symbols.values where symbol.kindIdentifier == "swift." + SymbolGraph.Symbol.KindIdentifier.extendedModule.identifier { + for symbol in symbolGraph.symbols.values.filter({symbol in symbol.kindIdentifier == "swift." + SymbolGraph.Symbol.KindIdentifier.extendedModule.identifier }).sorted(by: \.uniqueIdentifier) { if let canonical = canonicalSymbolByModuleName[symbol.title] { // merge accesslevel for (selector, level) in symbol.accessLevel { diff --git a/Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift b/Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift new file mode 100644 index 0000000000..713dad472c --- /dev/null +++ b/Sources/SwiftDocCTestUtilities/SymbolGraphCreation.swift @@ -0,0 +1,56 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 XCTest +import SymbolKit + +extension XCTestCase { + public func makeSymbolGraph(moduleName: String, symbols: [SymbolGraph.Symbol] = [], relationships: [SymbolGraph.Relationship] = []) -> SymbolGraph { + return SymbolGraph( + metadata: SymbolGraph.Metadata( + formatVersion: SymbolGraph.SemanticVersion(major: 0, minor: 6, patch: 0), + generator: "unit-test" + ), + module: SymbolGraph.Module( + name: moduleName, + platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: nil) + ), + symbols: symbols, + relationships: relationships + ) + } + + public func makeSymbolGraphString(moduleName: String, symbols: String = "", relationships: String = "") -> String { + return """ + { + "metadata": { + "formatVersion": { + "major": 0, + "minor": 6, + "patch": 0 + }, + "generator": "unit-test" + }, + "module": { + "name": "\(moduleName)", + "platform": { } + }, + "relationships" : [ + \(relationships) + ], + "symbols" : [ + \(symbols) + ] + } + """ + } +} diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift index 5e89cdf608..c75e2a2ac3 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift @@ -1719,6 +1719,29 @@ let expected = """ XCTAssertNoThrow(try context.entity(with: ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/SideKit/SideClass-swift.class/path", sourceLanguage: .swift))) XCTAssertNoThrow(try context.entity(with: ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/SideKit/sideClass-swift.var", sourceLanguage: .swift))) } + + /// Tests that collisions caused by contraction of path components in extensions to nested external types are detected. + /// + /// The external dependency defines the struct `CollidingName` and the nested struct `NonCollidingName.CollidingName`. + /// The main module extends both types with a property. Extended Type Symbols are always direct children of the respective Extended + /// Module Symbol. Thus, the extended symbol page for `NonCollidingName.CollidingName` does not contain the + /// `NonCollidingName` path component and collides with the top-level `CollidingName` struct. + func testCollisionFromExtensionToNestedExternalType() throws { + // Add some symbol collisions to graph + let (bundleURL, _, context) = try testBundleAndContext(copying: "BundleWithCollisionBasedOnNestedTypeExtension") + + defer { try? FileManager.default.removeItem(at: bundleURL) } + + // Verify the contraction-based collisions were resolved + XCTAssertNoThrow(try context.entity( + with: ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", + path: "/documentation/BundleWithCollisionBasedOnNestedTypeExtension/DependencyWithNestedType/CollidingName-813uu", + sourceLanguage: .swift))) + XCTAssertNoThrow(try context.entity( + with: ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", + path: "/documentation/BundleWithCollisionBasedOnNestedTypeExtension/DependencyWithNestedType/CollidingName-5fbpv", + sourceLanguage: .swift))) + } func testUnresolvedSidecarDiagnostics() throws { var unknownSymbolSidecarURL: URL! diff --git a/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift index 0aa49a12ae..23606b14fb 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift @@ -344,6 +344,39 @@ class ReferenceResolverTests: XCTestCase { XCTAssertEqual(referencingFileDiagnostics.filter({ $0.identifier == "org.swift.docc.unresolvedTopicReference" }).count, 1) } + func testAbsoluteAndRelativeReferencesToExternalAndExtensionSymbols() throws { + let (bundleURL, bundle, context) = try testBundleAndContext(copying: "BundleWithRelativePathAmbiguity") + + defer { try? FileManager.default.removeItem(at: bundleURL) } + + // Get a translated render node + let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/BundleWithRelativePathAmbiguity", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference, source: nil) + let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode + + let content = try XCTUnwrap(renderNode.primaryContentSections.first as? ContentRenderSection).content + + func assertListedReferencesInSectionMatchHeading(_ absoluteShorthandReference: String) throws { + let headingString = "`\(absoluteShorthandReference)`" + let absoluteReferenceString = "doc://org.swift.docc.example/documentation\(absoluteShorthandReference)" + + for listItem in content.contents(of: headingString).listItems() { + let reference = try XCTUnwrap(listItem.firstReference(), "found no reference for \(listItem)") + XCTAssertEqual(reference.identifier, absoluteReferenceString, "found mismatch for \(listItem)") + } + } + + try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity") + try assertListedReferencesInSectionMatchHeading("/Dependency") + try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity/Dependency") + try assertListedReferencesInSectionMatchHeading("/Dependency/AmbiguousType") + try assertListedReferencesInSectionMatchHeading("/Dependency/AmbiguousProtocol") + try assertListedReferencesInSectionMatchHeading("/Dependency/UnambiguousType") + try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType") + try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity/Dependency/AmbiguousProtocol") + try assertListedReferencesInSectionMatchHeading("/Dependency/AmbiguousType/unambiguousFunction()") + } + struct TestExternalReferenceResolver: ExternalReferenceResolver { var bundleIdentifier = "com.external.testbundle" var expectedReferencePath = "/externally/resolved/path" @@ -565,3 +598,55 @@ class ReferenceResolverTests: XCTestCase { private extension DocumentationDataVariantsTrait { static var objectiveC: DocumentationDataVariantsTrait { .init(interfaceLanguage: "occ") } } + +private extension Collection where Element == RenderBlockContent { + func contents(of heading: String) -> Slice { + var headingLevel: Int = 1 + + guard let headingIndex = self.firstIndex(where: { element in + if case let .heading(elementLevel, elementHeading, _) = element { + headingLevel = elementLevel + return heading == elementHeading + } + return false + }) else { + return Slice(base: self, bounds: self.startIndex.. [RenderBlockContent.ListItem] { + self.compactMap { block -> [RenderBlockContent.ListItem]? in + if case let .unorderedList(items) = block { + return items + } + return nil + }.flatMap({ $0 }) + } +} + +private extension RenderBlockContent.ListItem { + func firstReference() -> RenderReferenceIdentifier? { + self.content.compactMap { block in + guard case let .paragraph(inlineContent) = block else { + return nil + } + + return inlineContent.compactMap { content in + guard case let .reference(identifier, _, _, _) = content else { + return nil + } + + return identifier + }.first + }.first + } +} diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/ExtendedTypesFormatTransformationTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/ExtendedTypesFormatTransformationTests.swift new file mode 100644 index 0000000000..2fd99978b9 --- /dev/null +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/ExtendedTypesFormatTransformationTests.swift @@ -0,0 +1,288 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 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 XCTest +import SymbolKit +@testable import SwiftDocC + +class ExtendedTypesFormatTransformationTests: XCTestCase { + /// Tests the general transformation structure of ``ExtendedTypesFormatTransformation/transformExtensionBlockFormatToExtendedTypeFormat(_:)`` + /// including the edge case that one extension graph contains extensions for two modules. + func testExtendedTypesFormatStructure() throws { + let contents = twoExtensionBlockSymbolsExtendingSameType(extendedModule: "A", extendedType: "A", withExtensionMembers: true) + + twoExtensionBlockSymbolsExtendingSameType(extendedModule: "A", extendedType: "ATwo", withExtensionMembers: true) + + twoExtensionBlockSymbolsExtendingSameType(extendedModule: "B", extendedType: "B", withExtensionMembers: true) + + var graph = makeSymbolGraph(moduleName: "Module", + symbols: contents.symbols, + relationships: contents.relationships) + + // check the transformation recognizes the swift.extension symbols & transform + XCTAssert(try ExtendedTypesFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph)) + + // check the expected symbols exist + let extendedModuleA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedModule && symbol.title == "A" })) + let extendedModuleB = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedModule && symbol.title == "B" })) + + let extendedTypeA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "A" })) + let extendedTypeATwo = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "ATwo" })) + let extendedTypeB = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "B" })) + + let addedMemberSymbolsTypeA = graph.symbols.values.filter({ symbol in symbol.kind.identifier == .property && symbol.pathComponents[symbol.pathComponents.count-2] == "A" }) + XCTAssertEqual(addedMemberSymbolsTypeA.count, 2) + let addedMemberSymbolsTypeATwo = graph.symbols.values.filter({ symbol in symbol.kind.identifier == .property && symbol.pathComponents[symbol.pathComponents.count-2] == "ATwo" }) + XCTAssertEqual(addedMemberSymbolsTypeATwo.count, 2) + let addedMemberSymbolsTypeB = graph.symbols.values.filter({ symbol in symbol.kind.identifier == .property && symbol.pathComponents[symbol.pathComponents.count-2] == "B" }) + XCTAssertEqual(addedMemberSymbolsTypeB.count, 2) + + // check the symbols are connected as expected + [ + SymbolGraph.Relationship(source: addedMemberSymbolsTypeA[0].identifier.precise, target: extendedTypeA.identifier.precise, kind: .memberOf, targetFallback: nil), + SymbolGraph.Relationship(source: addedMemberSymbolsTypeA[1].identifier.precise, target: extendedTypeA.identifier.precise, kind: .memberOf, targetFallback: nil), + SymbolGraph.Relationship(source: addedMemberSymbolsTypeATwo[0].identifier.precise, target: extendedTypeATwo.identifier.precise, kind: .memberOf, targetFallback: nil), + SymbolGraph.Relationship(source: addedMemberSymbolsTypeATwo[1].identifier.precise, target: extendedTypeATwo.identifier.precise, kind: .memberOf, targetFallback: nil), + SymbolGraph.Relationship(source: addedMemberSymbolsTypeB[0].identifier.precise, target: extendedTypeB.identifier.precise, kind: .memberOf, targetFallback: nil), + SymbolGraph.Relationship(source: addedMemberSymbolsTypeB[1].identifier.precise, target: extendedTypeB.identifier.precise, kind: .memberOf, targetFallback: nil), + + SymbolGraph.Relationship(source: extendedTypeA.identifier.precise, target: extendedModuleA.identifier.precise, kind: .declaredIn, targetFallback: nil), + SymbolGraph.Relationship(source: extendedTypeATwo.identifier.precise, target: extendedModuleA.identifier.precise, kind: .declaredIn, targetFallback: nil), + SymbolGraph.Relationship(source: extendedTypeB.identifier.precise, target: extendedModuleB.identifier.precise, kind: .declaredIn, targetFallback: nil), + ].forEach { test in + XCTAssert(graph.relationships.contains(where: { sample in + sample.source == test.source && sample.target == test.target && sample.kind == test.kind + })) + } + + // check there are no additional elements + XCTAssertEqual(graph.symbols.count, 2 /* extended modules */ + 3 /* extended types */ + 6 /* added properties */) + XCTAssertEqual(graph.relationships.count, 3 /* .declaredIn */ + 6 /* .memberOf */) + + // check correct module name was prepended to pathComponents + ([extendedModuleA, extendedTypeA, extendedTypeATwo] + + addedMemberSymbolsTypeA + + addedMemberSymbolsTypeATwo).forEach { symbol in + XCTAssertEqual(symbol.pathComponents.first, "A") + } + + ([extendedModuleB, extendedTypeB] + + addedMemberSymbolsTypeB).forEach { symbol in + XCTAssertEqual(symbol.pathComponents.first, "B") + } + } + + /// Tests that an extended type symbol always uses the documentation comment with the highest number + /// of lines from the relevant extension block symbols. + /// + /// ```swift + /// /// This is shorter...won't be chosen. + /// extension A { /* ... */ } + /// + /// /// This is the longest as it + /// /// has two lines. It will be chosen. + /// extension A { /* ... */ } + /// ``` + func testDocumentationForExtendedTypeSymbolUsesLongestAvailableDocumenation() throws { + let content = twoExtensionBlockSymbolsExtendingSameType(sameDocCommentLength: false) + for permutation in allPermutations(of: content.symbols, and: content.relationships) { + var graph = makeSymbolGraph(moduleName: "Module", symbols: permutation.symbols, relationships: permutation.relationships) + _ = try ExtendedTypesFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph) + + let extendedTypeSymbol = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure })) + XCTAssertEqual(extendedTypeSymbol.docComment?.lines.count, 2) + } + } + + /// Tests that extended type symbols are always based on the same extension block symbol (if there is more than + /// one for the same type), which influences the extended type symbol's unique identifier. + func testBaseSymbolForExtendedTypeSymbolIsStable() throws { + let content = twoExtensionBlockSymbolsExtendingSameType() + for permutation in allPermutations(of: content.symbols, and: content.relationships) { + var graph = makeSymbolGraph(moduleName: "Module", symbols: permutation.symbols, relationships: permutation.relationships) + _ = try ExtendedTypesFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph) + + let extendedTypeSymbol = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure })) + XCTAssertEqual(extendedTypeSymbol.identifier.precise, "s:e:s:AAone") // one < two (alphabetically) + } + } + + /// Tests that extended module symbols are always based on the same extended type symbol (if there is more than + /// one for the same module), which influences the extended module symbol's unique identifier. + func testBaseSymbolForExtendedModuleSymbolIsStable() throws { + let content = twoExtensionBlockSymbolsExtendingSameType() + for permutation in allPermutations(of: content.symbols, and: content.relationships) { + var graph = makeSymbolGraph(moduleName: "Module", symbols: permutation.symbols, relationships: permutation.relationships) + _ = try ExtendedTypesFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph) + + let extendedModuleSymbol = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedModule })) + XCTAssertEqual(extendedModuleSymbol.identifier.precise, "s:m:s:e:s:AAone") // one < two (alphabetically) + } + } + + /// Tests that an extended type symbol always uses the same documentation comment if there is more than one relevant + /// extension block symbol that features the highest number of lines in its doc-comment. + func testDocumentationForExtendedTypeSymbolIsStable() throws { + let content = twoExtensionBlockSymbolsExtendingSameType(sameDocCommentLength: true) + for permutation in allPermutations(of: content.symbols, and: content.relationships) { + var graph = makeSymbolGraph(moduleName: "Module", symbols: permutation.symbols, relationships: permutation.relationships) + _ = try ExtendedTypesFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph) + + let extendedTypeSymbol = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure })) + XCTAssertEqual(extendedTypeSymbol.docComment?.lines.first?.text, "one line") // one < two (alphabetically) + } + } + + /// Tests that if a unified symbol graph contains more than one extended module symbols for the same module, these extended + /// module symbols are merged into one and that this symbol's identifier does not depend on the graph's order. + func testCrossModuleNestedTypeExtensionsHandling() throws { + let aAtB = (graph: makeSymbolGraph(moduleName: "A", symbols: [ + .init(identifier: .init(precise: "s:m:s:e:s:Bone", interfaceLanguage: "swift"), + names: .init(title: "B", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["B"], + docComment: nil, + accessLevel: .public, + kind: .init(parsedIdentifier: .extendedModule, displayName: "Extended Module"), + mixins: [:]) + ]), url: URL(fileURLWithPath: "A@B.symbols.json")) + + let aAtC = (graph: makeSymbolGraph(moduleName: "A", symbols: [ + .init(identifier: .init(precise: "s:m:s:e:s:Btwo", interfaceLanguage: "swift"), + names: .init(title: "B", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["B"], + docComment: nil, + accessLevel: .public, + kind: .init(parsedIdentifier: .extendedModule, displayName: "Extended Module"), + mixins: [:]), + .init(identifier: .init(precise: "s:m:s:e:s:C", interfaceLanguage: "swift"), + names: .init(title: "C", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["C"], + docComment: nil, + accessLevel: .public, + kind: .init(parsedIdentifier: .extendedModule, displayName: "Extended Module"), + mixins: [:]) + ]), url: URL(fileURLWithPath: "A@C.symbols.json")) + + for files in allPermutations(of: [aAtB, aAtC]) { + let unifiedGraph = try XCTUnwrap(UnifiedSymbolGraph(fromSingleGraph: makeSymbolGraph(moduleName: "A"), at: .init(fileURLWithPath: "A.symbols.json"))) + for file in files { + unifiedGraph.mergeGraph(graph: file.graph, at: file.url) + } + + ExtendedTypesFormatTransformation.mergeExtendedModuleSymbolsFromDifferentFiles(unifiedGraph) + + let extendedModuleSymbols = unifiedGraph.symbols.values.filter({ symbol in symbol.kindIdentifier == "swift." + SymbolGraph.Symbol.KindIdentifier.extendedModule.identifier }) + XCTAssertEqual(extendedModuleSymbols.count, 2) + + let extendedModuleSymbolForB = try XCTUnwrap(extendedModuleSymbols.first(where: { symbol in symbol.title == "B" })) + XCTAssertEqual(extendedModuleSymbolForB.uniqueIdentifier, "s:m:s:e:s:Bone") // one < two (alphabetically) + } + } + + // MARK: Helpers + + private struct SymbolGraphContents { + let symbols: [SymbolGraph.Symbol] + let relationships: [SymbolGraph.Relationship] + + static func +(lhs: Self, rhs: Self) -> Self { + SymbolGraphContents(symbols: lhs.symbols + rhs.symbols, relationships: lhs.relationships + rhs.relationships) + } + } + + private func twoExtensionBlockSymbolsExtendingSameType(extendedModule: String = "A", extendedType: String = "A", withExtensionMembers: Bool = false, sameDocCommentLength: Bool = true) -> SymbolGraphContents { + SymbolGraphContents(symbols: [.init(identifier: .init(precise: "s:e:s:\(extendedModule)\(extendedType)two", interfaceLanguage: "swift"), + names: .init(title: "\(extendedType)", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["\(extendedType)"], + docComment: .init([ + .init(text: "two", range: nil) + ] + (sameDocCommentLength ? [] : [.init(text: "lines", range: nil)])), + accessLevel: .public, + kind: .init(parsedIdentifier: .extension, displayName: "Extension"), + mixins: [ + SymbolGraph.Symbol.Swift.Extension.mixinKey: SymbolGraph.Symbol.Swift.Extension(extendedModule: "\(extendedModule)", typeKind: .struct, constraints: []) + ]), + .init(identifier: .init(precise: "s:e:s:\(extendedModule)\(extendedType)one", interfaceLanguage: "swift"), + names: .init(title: "\(extendedType)", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["\(extendedType)"], + docComment: .init([ + .init(text: "one line", range: nil) + ]), + accessLevel: .public, + kind: .init(parsedIdentifier: .extension, displayName: "Extension"), + mixins: [ + SymbolGraph.Symbol.Swift.Extension.mixinKey: SymbolGraph.Symbol.Swift.Extension(extendedModule: "\(extendedModule)", typeKind: .struct, constraints: []) + ]) + ] + (withExtensionMembers ? [ + .init(identifier: .init(precise: "s:\(extendedModule)\(extendedType)two", interfaceLanguage: "swift"), + names: .init(title: "two", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["\(extendedType)", "two"], + docComment: nil, + accessLevel: .public, + kind: .init(parsedIdentifier: .property, displayName: "Property"), + mixins: [ + SymbolGraph.Symbol.Swift.Extension.mixinKey: SymbolGraph.Symbol.Swift.Extension(extendedModule: "\(extendedModule)", typeKind: .struct, constraints: []) + ]), + .init(identifier: .init(precise: "s:\(extendedModule)\(extendedType)one", interfaceLanguage: "swift"), + names: .init(title: "one", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["\(extendedType)", "one"], + docComment: nil, + accessLevel: .public, + kind: .init(parsedIdentifier: .property, displayName: "Property"), + mixins: [ + SymbolGraph.Symbol.Swift.Extension.mixinKey: SymbolGraph.Symbol.Swift.Extension(extendedModule: "\(extendedModule)", typeKind: .struct, constraints: []) + ]) + ] : []) + , relationships: [ + .init(source: "s:e:s:\(extendedModule)\(extendedType)two", target: "s:\(extendedModule)\(extendedType)", kind: .extensionTo, targetFallback: "\(extendedModule).\(extendedType)"), + .init(source: "s:e:s:\(extendedModule)\(extendedType)one", target: "s:\(extendedModule)\(extendedType)", kind: .extensionTo, targetFallback: "\(extendedModule).\(extendedType)") + ] + (withExtensionMembers ? [ + .init(source: "s:\(extendedModule)\(extendedType)two", target: "s:e:s:\(extendedModule)\(extendedType)two", kind: .memberOf, targetFallback: "\(extendedModule).\(extendedType)"), + .init(source: "s:\(extendedModule)\(extendedType)one", target: "s:e:s:\(extendedModule)\(extendedType)one", kind: .memberOf, targetFallback: "\(extendedModule).\(extendedType)") + ] : [])) + } + + private func allPermutations(of symbols: [SymbolGraph.Symbol], and relationships: [SymbolGraph.Relationship]) -> [(symbols: [SymbolGraph.Symbol], relationships: [SymbolGraph.Relationship])] { + let symbolPermutations = allPermutations(of: symbols) + let relationshipPermutations = allPermutations(of: relationships) + + var permutations: [([SymbolGraph.Symbol], [SymbolGraph.Relationship])] = [] + + for sp in symbolPermutations { + for rp in relationshipPermutations { + permutations.append((sp, rp)) + } + } + + return permutations + } + + private func allPermutations(of a: C) -> [[C.Element]] { + var a = Array(a) + var p: [[C.Element]] = [] + p.reserveCapacity(Int(pow(Double(2), Double(a.count)))) + permutations(a.count, &a, calling: { p.append($0) }) + return p + } + + // https://en.wikipedia.org/wiki/Heap's_algorithm + private func permutations(_ n:Int, _ a: inout C, calling report: (C) -> Void) where C.Index == Int { + if n == 1 { + report(a) + return + } + for i in 0.. SymbolGraph { - return SymbolGraph( - metadata: SymbolGraph.Metadata( - formatVersion: SymbolGraph.SemanticVersion(major: 1, minor: 1, patch: 1), - generator: "unit-test" - ), - module: SymbolGraph.Module( - name: moduleName, - platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: nil) - ), - symbols: [], - relationships: [] - ) + func testInputWithMixedGraphFormats() throws { + let tempURL = try createTemporaryDirectory() + + let mainGraph = (url: tempURL.appendingPathComponent("A.symbols.json"), + content: makeSymbolGraphString(moduleName: "A")) + + let emptyExtensionGraph = (url: tempURL.appendingPathComponent("A@Empty.symbols.json"), + content: makeSymbolGraphString(moduleName: "A")) + + let extensionBlockFormatExtensionGraph = (url: tempURL.appendingPathComponent("A@EBF.symbols.json"), + content: makeSymbolGraphString(moduleName: "A", symbols: """ + { + "kind": { + "identifier": "swift.extension", + "displayName": "Extension" + }, + "identifier": { + "precise": "s:e:s:EBFfunction", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "EBF" + ], + "names": { + "title": "EBF", + }, + "swiftExtension": { + "extendedModule": "A", + "typeKind": "struct" + }, + "accessLevel": "public" + }, + { + "kind": { + "identifier": "swift.func", + "displayName": "Function" + }, + "identifier": { + "precise": "s:EBFfunction", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "EBF", + "function" + ], + "names": { + "title": "function", + }, + "swiftExtension": { + "extendedModule": "A", + "typeKind": "struct" + }, + "accessLevel": "public" + } + """, relationships: """ + { + "kind": "memberOf", + "source": "s:EBFfunction", + "target": "s:e:s:EBFfunction", + "targetFallback": "A.EBF" + }, + { + "kind": "extensionTo", + "source": "s:e:s:EBFfunction", + "target": "s:EBF", + "targetFallback": "A.EBF" + } + """)) + + let noExtensionBlockFormatExtensionGraph = (url: tempURL.appendingPathComponent("A@NEBF.symbols.json"), + content: makeSymbolGraphString(moduleName: "A", symbols: """ + { + "kind": { + "identifier": "swift.func", + "displayName": "Function" + }, + "identifier": { + "precise": "s:EBFfunction", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "EBF", + "function" + ], + "names": { + "title": "function", + }, + "swiftExtension": { + "extendedModule": "A", + "typeKind": "struct" + }, + "accessLevel": "public" + } + """, relationships: """ + { + "kind": "memberOf", + "source": "s:EBFfunction", + "target": "s:EBF", + "targetFallback": "A.EBF" + } + """)) + + let allGraphs = [mainGraph, emptyExtensionGraph, extensionBlockFormatExtensionGraph, noExtensionBlockFormatExtensionGraph] + + for graph in allGraphs { + try XCTUnwrap(graph.content.data(using: .utf8)).write(to: graph.url) + } + + let validUndetermined = [mainGraph, emptyExtensionGraph] + var loader = try makeSymbolGraphLoader(symbolGraphURLs: validUndetermined.map(\.url)) + try loader.loadAll() + // by default, extension graphs should be associated with the extended graph + XCTAssertEqual(loader.unifiedGraphs.count, 2) + + let validEBF = [mainGraph, emptyExtensionGraph, extensionBlockFormatExtensionGraph] + loader = try makeSymbolGraphLoader(symbolGraphURLs: validEBF.map(\.url)) + try loader.loadAll() + // found extension block symbols; extension graphs should be associated with the extending graph + XCTAssertEqual(loader.unifiedGraphs.count, 1) + + let validNEBF = [mainGraph, emptyExtensionGraph, noExtensionBlockFormatExtensionGraph] + loader = try makeSymbolGraphLoader(symbolGraphURLs: validNEBF.map(\.url)) + try loader.loadAll() + // found no extension block symbols; extension graphs should be associated with the extended graph + XCTAssertEqual(loader.unifiedGraphs.count, 3) + + let invalid = allGraphs + loader = try makeSymbolGraphLoader(symbolGraphURLs: invalid.map(\.url)) + // found non-empty extension graphs with and without extension block symbols -> should throw + do { + try loader.loadAll() + XCTFail("SymbolGraphLoader should throw when encountering a collection of symbol graph files, where some do and some don't use the extension block format") + } catch {} } + // MARK: - Helpers + private func makeSymbolGraphLoader(symbolGraphURLs: [URL]) throws -> SymbolGraphLoader { let workspace = DocumentationWorkspace() let bundle = DocumentationBundle( diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension.md b/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension.md new file mode 100644 index 0000000000..ebf2a8ef83 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension.md @@ -0,0 +1,5 @@ +# ``BundleWithCollisionBasedOnNestedTypeExtension`` + +This bundle contains collisions caused by contraction of path components in extensions to nested external types. + + diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension.symbols.json b/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension.symbols.json new file mode 100644 index 0000000000..bb522d06ce --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension.symbols.json @@ -0,0 +1 @@ +{"metadata":{"formatVersion":{"major":0,"minor":6,"patch":0},"generator":"Swift version 5.8-dev (LLVM c44b030a65f0a4d, Swift 27003e37fd4aa55)"},"module":{"name":"BundleWithCollisionBasedOnNestedTypeExtension","platform":{"architecture":"arm64","vendor":"apple","operatingSystem":{"name":"macosx","minimumVersion":{"major":10,"minor":10,"patch":0}}}},"symbols":[],"relationships":[]} \ No newline at end of file diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension@DependencyWithNestedType.symbols.json b/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension@DependencyWithNestedType.symbols.json new file mode 100644 index 0000000000..f9406e3188 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/BundleWithCollisionBasedOnNestedTypeExtension@DependencyWithNestedType.symbols.json @@ -0,0 +1 @@ +{"metadata":{"formatVersion":{"major":0,"minor":6,"patch":0},"generator":"Swift version 5.8-dev (LLVM c44b030a65f0a4d, Swift 27003e37fd4aa55)"},"module":{"name":"BundleWithCollisionBasedOnNestedTypeExtension","platform":{"architecture":"arm64","vendor":"apple","operatingSystem":{"name":"macosx","minimumVersion":{"major":10,"minor":10,"patch":0}}}},"symbols":[{"kind":{"identifier":"swift.extension","displayName":"Extension"},"identifier":{"precise":"s:e:s:24DependencyWithNestedType16NonCollidingNameV0fG0V06Bundleb16CollisionBasedOncD9ExtensionE03nonfG0yyF","interfaceLanguage":"swift"},"pathComponents":["NonCollidingName","CollidingName"],"names":{"title":"NonCollidingName.CollidingName","navigator":[{"kind":"identifier","spelling":"CollidingName"}],"subHeading":[{"kind":"keyword","spelling":"extension"},{"kind":"text","spelling":" "},{"kind":"typeIdentifier","spelling":"NonCollidingName","preciseIdentifier":"s:24DependencyWithNestedType16NonCollidingNameV"},{"kind":"text","spelling":"."},{"kind":"typeIdentifier","spelling":"CollidingName","preciseIdentifier":"s:24DependencyWithNestedType16NonCollidingNameV0fG0V"}]},"swiftExtension":{"extendedModule":"DependencyWithNestedType","typeKind":"struct"},"declarationFragments":[{"kind":"keyword","spelling":"extension"},{"kind":"text","spelling":" "},{"kind":"typeIdentifier","spelling":"NonCollidingName","preciseIdentifier":"s:24DependencyWithNestedType16NonCollidingNameV"},{"kind":"text","spelling":"."},{"kind":"typeIdentifier","spelling":"CollidingName","preciseIdentifier":"s:24DependencyWithNestedType16NonCollidingNameV0fG0V"}],"accessLevel":"public","location":{"uri":"file:///Users/themomax/Development/local/DocC/BundleWithCollisionBasedOnNestedTypeExtension/Sources/BundleWithCollisionBasedOnNestedTypeExtension/BundleWithCollisionBasedOnNestedTypeExtension.swift","position":{"line":6,"character":7}}},{"kind":{"identifier":"swift.method","displayName":"Instance Method"},"identifier":{"precise":"s:24DependencyWithNestedType13CollidingNameV06Bundleb16CollisionBasedOncD9ExtensionE03noneF0yyF","interfaceLanguage":"swift"},"pathComponents":["CollidingName","nonCollidingName()"],"names":{"title":"nonCollidingName()","subHeading":[{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"nonCollidingName"},{"kind":"text","spelling":"()"}]},"functionSignature":{"returns":[{"kind":"text","spelling":"()"}]},"swiftExtension":{"extendedModule":"DependencyWithNestedType","typeKind":"struct"},"declarationFragments":[{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"nonCollidingName"},{"kind":"text","spelling":"()"}],"accessLevel":"public","location":{"uri":"file:///Users/themomax/Development/local/DocC/BundleWithCollisionBasedOnNestedTypeExtension/Sources/BundleWithCollisionBasedOnNestedTypeExtension/BundleWithCollisionBasedOnNestedTypeExtension.swift","position":{"line":3,"character":9}}},{"kind":{"identifier":"swift.extension","displayName":"Extension"},"identifier":{"precise":"s:e:s:24DependencyWithNestedType13CollidingNameV06Bundleb16CollisionBasedOncD9ExtensionE03noneF0yyF","interfaceLanguage":"swift"},"pathComponents":["CollidingName"],"names":{"title":"CollidingName","navigator":[{"kind":"identifier","spelling":"CollidingName"}],"subHeading":[{"kind":"keyword","spelling":"extension"},{"kind":"text","spelling":" "},{"kind":"typeIdentifier","spelling":"CollidingName","preciseIdentifier":"s:24DependencyWithNestedType13CollidingNameV"}]},"swiftExtension":{"extendedModule":"DependencyWithNestedType","typeKind":"struct"},"declarationFragments":[{"kind":"keyword","spelling":"extension"},{"kind":"text","spelling":" "},{"kind":"typeIdentifier","spelling":"CollidingName","preciseIdentifier":"s:24DependencyWithNestedType13CollidingNameV"}],"accessLevel":"public","location":{"uri":"file:///Users/themomax/Development/local/DocC/BundleWithCollisionBasedOnNestedTypeExtension/Sources/BundleWithCollisionBasedOnNestedTypeExtension/BundleWithCollisionBasedOnNestedTypeExtension.swift","position":{"line":2,"character":7}}},{"kind":{"identifier":"swift.method","displayName":"Instance Method"},"identifier":{"precise":"s:24DependencyWithNestedType16NonCollidingNameV0fG0V06Bundleb16CollisionBasedOncD9ExtensionE03nonfG0yyF","interfaceLanguage":"swift"},"pathComponents":["NonCollidingName","CollidingName","nonCollidingName()"],"names":{"title":"nonCollidingName()","subHeading":[{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"nonCollidingName"},{"kind":"text","spelling":"()"}]},"functionSignature":{"returns":[{"kind":"text","spelling":"()"}]},"swiftExtension":{"extendedModule":"DependencyWithNestedType","typeKind":"struct"},"declarationFragments":[{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"nonCollidingName"},{"kind":"text","spelling":"()"}],"accessLevel":"public","location":{"uri":"file:///Users/themomax/Development/local/DocC/BundleWithCollisionBasedOnNestedTypeExtension/Sources/BundleWithCollisionBasedOnNestedTypeExtension/BundleWithCollisionBasedOnNestedTypeExtension.swift","position":{"line":7,"character":9}}}],"relationships":[{"kind":"extensionTo","source":"s:e:s:24DependencyWithNestedType13CollidingNameV06Bundleb16CollisionBasedOncD9ExtensionE03noneF0yyF","target":"s:24DependencyWithNestedType13CollidingNameV","targetFallback":"DependencyWithNestedType.CollidingName"},{"kind":"memberOf","source":"s:24DependencyWithNestedType13CollidingNameV06Bundleb16CollisionBasedOncD9ExtensionE03noneF0yyF","target":"s:e:s:24DependencyWithNestedType13CollidingNameV06Bundleb16CollisionBasedOncD9ExtensionE03noneF0yyF","targetFallback":"DependencyWithNestedType.CollidingName"},{"kind":"memberOf","source":"s:24DependencyWithNestedType16NonCollidingNameV0fG0V06Bundleb16CollisionBasedOncD9ExtensionE03nonfG0yyF","target":"s:e:s:24DependencyWithNestedType16NonCollidingNameV0fG0V06Bundleb16CollisionBasedOncD9ExtensionE03nonfG0yyF","targetFallback":"DependencyWithNestedType.NonCollidingName.CollidingName"},{"kind":"extensionTo","source":"s:e:s:24DependencyWithNestedType16NonCollidingNameV0fG0V06Bundleb16CollisionBasedOncD9ExtensionE03nonfG0yyF","target":"s:24DependencyWithNestedType16NonCollidingNameV0fG0V","targetFallback":"DependencyWithNestedType.NonCollidingName.CollidingName"}]} \ No newline at end of file diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/Info.plist b/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/Info.plist new file mode 100644 index 0000000000..d4ccbd35f8 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithCollisionBasedOnNestedTypeExtension.docc/Info.plist @@ -0,0 +1,14 @@ + + + + + CFBundleVersion + 0.1.0 + CFBundleIdentifier + org.swift.docc.example + CFBundleDisplayName + Bundle with Collision Based on Nested-Type Extension + CFBundleName + BundleWithCollisionBasedOnNestedTypeExtension + + diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.md b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.md new file mode 100644 index 0000000000..7ca5c66caf --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.md @@ -0,0 +1,67 @@ +# ``BundleWithRelativePathAmbiguity`` + +This bundle contains external symbols of the dependency module and local extensions to external symbols where some cannot be referenced unambigously. + +## Overview + +This bundle tests path resolution in a combined documentation archive of the module ``BundleWithRelativePathAmbiguity`` and its ``/Dependency``. The main bundle ``BundleWithRelativePathAmbiguity`` extends its ``/Dependency``, thus many of the types from ``/Dependency`` have Extended Type Pages in ``BundleWithRelativePathAmbiguity``. Since this document is part of ``BundleWithRelativePathAmbiguity``, ambiguous relative paths should always resolve to the Extended Type Pages and not the original type pages from the external module's documentation. + +Absolute references can be used to unambigously refer to the pages in the external module's documentation in ambiguous situations. While one could also use the fully qualified URI including the bundle identifier, the shorthand syntax with a leading slash is the preferred way to go. + +### Module Pages + +#### `/BundleWithRelativePathAmbiguity` + +``BundleWithRelativePathAmbiguity`` is the main module and can therefore be referenced using all of the following: +- ``BundleWithRelativePathAmbiguity`` (relative) +- ``/BundleWithRelativePathAmbiguity`` (absolute, shorthand) +- ``doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity`` (absolute, fully qualified) + +#### `/Dependency` + +``/Dependency`` is the original module page in the dependency's documentation and can therefore only be referenced absolutely: +- ``/Dependency`` (absolute, shorthand) +- ``doc://org.swift.docc.example/documentation/Dependency`` (absolute, fully qualified) + +### Extended Module Pages + +#### `/BundleWithRelativePathAmbiguity/Dependency` + +``Dependency`` is the Extended Module Page for the dependency module in the main module's documentation. As it is the local type, it can be referenced with a relative address: +- ``Dependency`` (relative) +- ``/BundleWithRelativePathAmbiguity/Dependency`` (absolute, shorthand) +- ``doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency`` (absolute, fully qualified) + + +### Type Pages + +#### `/Dependency/AmbiguousType` + +- ``/Dependency/AmbiguousType`` (absolute, shorthand) +- ``doc://org.swift.docc.example/documentation/Dependency/AmbiguousType`` (absolute, fully qualified) + +#### `/Dependency/UnambiguousType` + +It should be possible to reference `/Dependency/UnambiguousType` relatively, even from `documentation/BundleWithRelativePathAmbiguity`. This way, the relative link switches to the extended type page automatically when the type gets extended. + +- ``Dependency/UnambiguousType`` (relative) +- ``/Dependency/UnambiguousType`` (absolute, shorthand) +- ``doc://org.swift.docc.example/documentation/Dependency/UnambiguousType`` (absolute, fully qualified) + +### Extended Type Pages + +#### `/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType` + +- ``Dependency/AmbiguousType`` (relative) +- ``/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType`` (absolute, shorthand) +- ``doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType`` (absolute, fully qualified) + +### Member Pages + +#### `/Dependency/AmbiguousType/unambiguousFunction()` + +- ``Dependency/AmbiguousType/unambiguousFunction()`` (relative) +- ``/Dependency/AmbiguousType/unambiguousFunction()`` (absolute, shorthand) +- ``doc://org.swift.docc.example/documentation/Dependency/AmbiguousType/unambiguousFunction()`` (absolute, fully qualified) + + diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.symbols.json b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.symbols.json new file mode 100644 index 0000000000..00314755b1 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.symbols.json @@ -0,0 +1 @@ +{"metadata":{"formatVersion":{"major":0,"minor":6,"patch":0},"generator":"Swift version 5.8-dev (LLVM c44b030a65f0a4d, Swift fb9ecb25924353e)"},"module":{"name":"BundleWithRelativePathAmbiguity","platform":{"architecture":"arm64","vendor":"apple","operatingSystem":{"name":"macosx","minimumVersion":{"major":10,"minor":10,"patch":0}}}},"symbols":[],"relationships":[]} \ No newline at end of file diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity@Dependency.symbols.json b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity@Dependency.symbols.json new file mode 100644 index 0000000000..8ac9afddda --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity@Dependency.symbols.json @@ -0,0 +1 @@ +{"metadata":{"formatVersion":{"major":0,"minor":6,"patch":0},"generator":"Swift version 5.8-dev (LLVM c44b030a65f0a4d, Swift fb9ecb25924353e)"},"module":{"name":"BundleWithRelativePathAmbiguity","platform":{"architecture":"arm64","vendor":"apple","operatingSystem":{"name":"macosx","minimumVersion":{"major":10,"minor":10,"patch":0}}}},"symbols":[{"kind":{"identifier":"swift.method","displayName":"Instance Method"},"identifier":{"precise":"s:10Dependency13AmbiguousTypeV31BundleWithRelativePathAmbiguityE3fooyyF","interfaceLanguage":"swift"},"pathComponents":["AmbiguousType","foo()"],"names":{"title":"foo()","subHeading":[{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"foo"},{"kind":"text","spelling":"()"}]},"functionSignature":{"returns":[{"kind":"text","spelling":"()"}]},"swiftExtension":{"extendedModule":"Dependency","typeKind":"struct"},"declarationFragments":[{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"foo"},{"kind":"text","spelling":"()"}],"accessLevel":"public","location":{"uri":"file:///Users/themomax/Development/local/DocC/BundleWithRelativePathAmbiguity/Sources/BundleWithRelativePathAmbiguity/BundleWithRelativePathAmbiguity.swift","position":{"line":3,"character":9}}},{"kind":{"identifier":"swift.extension","displayName":"Extension"},"identifier":{"precise":"s:e:s:10Dependency13AmbiguousTypeV31BundleWithRelativePathAmbiguityE3fooyyF","interfaceLanguage":"swift"},"pathComponents":["AmbiguousType"],"names":{"title":"AmbiguousType","navigator":[{"kind":"identifier","spelling":"AmbiguousType"}],"subHeading":[{"kind":"keyword","spelling":"extension"},{"kind":"text","spelling":" "},{"kind":"typeIdentifier","spelling":"AmbiguousType","preciseIdentifier":"s:10Dependency13AmbiguousTypeV"}]},"swiftExtension":{"extendedModule":"Dependency","typeKind":"struct"},"declarationFragments":[{"kind":"keyword","spelling":"extension"},{"kind":"text","spelling":" "},{"kind":"typeIdentifier","spelling":"AmbiguousType","preciseIdentifier":"s:10Dependency13AmbiguousTypeV"}],"accessLevel":"public","location":{"uri":"file:///Users/themomax/Development/local/DocC/BundleWithRelativePathAmbiguity/Sources/BundleWithRelativePathAmbiguity/BundleWithRelativePathAmbiguity.swift","position":{"line":2,"character":7}}}],"relationships":[{"kind":"memberOf","source":"s:10Dependency13AmbiguousTypeV31BundleWithRelativePathAmbiguityE3fooyyF","target":"s:e:s:10Dependency13AmbiguousTypeV31BundleWithRelativePathAmbiguityE3fooyyF","targetFallback":"Dependency.AmbiguousType"},{"kind":"extensionTo","source":"s:e:s:10Dependency13AmbiguousTypeV31BundleWithRelativePathAmbiguityE3fooyyF","target":"s:10Dependency13AmbiguousTypeV","targetFallback":"Dependency.AmbiguousType"}]} \ No newline at end of file diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/Dependency.symbols.json b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/Dependency.symbols.json new file mode 100644 index 0000000000..5a985929a3 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/Dependency.symbols.json @@ -0,0 +1 @@ +{"metadata":{"formatVersion":{"major":0,"minor":6,"patch":0},"generator":"Swift version 5.8-dev (LLVM c44b030a65f0a4d, Swift fb9ecb25924353e)"},"module":{"name":"Dependency","platform":{"architecture":"arm64","vendor":"apple","operatingSystem":{"name":"macosx","minimumVersion":{"major":10,"minor":10,"patch":0}}}},"symbols":[{"kind":{"identifier":"swift.struct","displayName":"Structure"},"identifier":{"precise":"s:10Dependency13AmbiguousTypeV","interfaceLanguage":"swift"},"pathComponents":["AmbiguousType"],"names":{"title":"AmbiguousType","navigator":[{"kind":"identifier","spelling":"AmbiguousType"}],"subHeading":[{"kind":"keyword","spelling":"struct"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"AmbiguousType"}]},"declarationFragments":[{"kind":"keyword","spelling":"struct"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"AmbiguousType"}],"accessLevel":"public","location":{"uri":"file:///Users/themomax/Development/local/DocC/BundleWithRelativePathAmbiguity/Sources/Dependency/Dependency.swift","position":{"line":0,"character":14}}},{"kind":{"identifier":"swift.method","displayName":"Instance Method"},"identifier":{"precise":"s:10Dependency13AmbiguousTypeV19unambiguousFunctionyyF","interfaceLanguage":"swift"},"pathComponents":["AmbiguousType","unambiguousFunction()"],"names":{"title":"unambiguousFunction()","subHeading":[{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"unambiguousFunction"},{"kind":"text","spelling":"()"}]},"functionSignature":{"returns":[{"kind":"text","spelling":"()"}]},"declarationFragments":[{"kind":"keyword","spelling":"func"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"unambiguousFunction"},{"kind":"text","spelling":"()"}],"accessLevel":"public","location":{"uri":"file:///Users/themomax/Development/local/DocC/BundleWithRelativePathAmbiguity/Sources/Dependency/Dependency.swift","position":{"line":1,"character":16}}},{"kind":{"identifier":"swift.struct","displayName":"Structure"},"identifier":{"precise":"s:10Dependency15UnambiguousTypeV","interfaceLanguage":"swift"},"pathComponents":["UnambiguousType"],"names":{"title":"UnambiguousType","navigator":[{"kind":"identifier","spelling":"UnambiguousType"}],"subHeading":[{"kind":"keyword","spelling":"struct"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"UnambiguousType"}]},"declarationFragments":[{"kind":"keyword","spelling":"struct"},{"kind":"text","spelling":" "},{"kind":"identifier","spelling":"UnambiguousType"}],"accessLevel":"public","location":{"uri":"file:///Users/themomax/Development/local/DocC/BundleWithRelativePathAmbiguity/Sources/Dependency/Dependency.swift","position":{"line":4,"character":14}}}],"relationships":[{"kind":"memberOf","source":"s:10Dependency13AmbiguousTypeV19unambiguousFunctionyyF","target":"s:10Dependency13AmbiguousTypeV"}]} \ No newline at end of file diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/Info.plist b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/Info.plist new file mode 100644 index 0000000000..8f5d9bdf27 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/Info.plist @@ -0,0 +1,14 @@ + + + + + CFBundleVersion + 0.1.0 + CFBundleIdentifier + org.swift.docc.example + CFBundleDisplayName + Bundle with Relative Path Ambiguity + CFBundleName + BundleWithRelativePathAmbiguity + + From 3ae5f3b0a3af8ed32d3d6c69ddf90938cb6706ce Mon Sep 17 00:00:00 2001 From: Max Obermeier Date: Wed, 10 Aug 2022 09:43:54 +0200 Subject: [PATCH 3/7] document link resolution rules relevant in the context of extending external types --- .../SwiftDocC/LinkResolution.md | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md b/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md index 51f717a0ae..a4be0e3fdb 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md +++ b/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md @@ -1,4 +1,4 @@ -# Linking between documentation +# Linking Between Documentation Connect documentation pages with documentation links. @@ -22,13 +22,25 @@ doc://com.example/path/to/documentation/page#optional-heading bundle ID path in docs hierarchy heading name ``` -## Resolving a documentation link +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`` `. -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. +## Resolving a Documentation Link -These relative documentation links can specify path components from higher up in the documentation hierarchy to reference container symbols or container pages. +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. -### Handling ambiguous links +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. + +```swift +struct Container { + struct Container { + /// ``Container`` links to `Container.Container` + /// ``/MyModule/Container`` links to `Container` + func foo() { } + } +} +``` + +### Handling Ambiguous Links It's possible for collisions to occur in documentation links (symbol links or otherwise) where more than one page are represented by the same path. A common cause for documentation link collisions are function overloads (functions with the same name but different arguments or different return values). It's also possible to have documentation link collisions in conceptual content if an article file name is the same as a tutorial file name (excluding the file extension in both cases). @@ -52,7 +64,37 @@ If two or more symbol results have the same kind, then that information doesn't Links with added disambiguation information is both harder read and harder write so DocC aims to require as little disambiguation as possible. -## Resolving links outside the documentation catalog +### Handling Type Aliases + +Members defined on a `typealias` cannot be linked to using the type alias' name, but must use the original name instead. Only the declaration of the `typealias` itself uses the alias' name. + +```swift +struct A {} + +/// This is referred to as ``B`` +typealias B = A + +extension B { + /// This can only be referred to as ``A/foo()``, not `B/foo()` + func foo() { } +} +``` + +### Handling Nested Types + +Sometimes it can happen that a symbol appears in your documentation catalog, but one or more of its original anchestors do not. This happens, for example, when extending a nested type from a different module. In those cases, the path components representing the missing anchestors are not part of the symbol page's link. + +Assuming the `Outer` and `Inner` types were defined in a different module called `ExternalModule`, this example shows the correct reference usage: + +```swift +/// This is referred to as ``ExternalModule/Inner`` +extension Outer.Inner { + /// This is referred to as ``ExternalModule/Inner/foo()`` + func foo() { } +} +``` + +## Resolving Links Outside the Documentation Catalog If a ``DocumentationContext`` is configured with one or more ``DocumentationContext/externalReferenceResolvers`` it is capable of resolving links general documentation links via that ``ExternalReferenceResolver``. External documentation links need to be written with a bundle ID in the URI to identify which external resolver should handle the request. From c8428da9df6ce467e3cc944ac1c58604b8b7b1a0 Mon Sep 17 00:00:00 2001 From: Max Obermeier Date: Wed, 10 Aug 2022 11:32:02 +0200 Subject: [PATCH 4/7] shift handling of absolute path syntax from PathHierarchyBasedLinkResolver to underlying PathHierarchy --- .../Link Resolution/PathHierarchy.swift | 17 +++++++++++------ .../PathHierarchyBasedLinkResolver.swift | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift index 264c72b3f7..72f149edd3 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift @@ -504,12 +504,17 @@ struct PathHierarchy { return (try findChild(of: tutorialOverviewContainer, remaining: remaining) , []) } } - if !isKnownTutorialPath && isAbsolute { - // If this is an absolute non-tutorial link, then the first component will be a module name. - if let matched = modules[component.name] ?? modules[component.full] { - remaining = remaining.dropFirst() - return (try findChild(of: matched, remaining: remaining) , []) - } + } + + if !isKnownTutorialPath && isAbsolute { + // If this is an absolute non-tutorial link, then the first component will be a module name. + if let matched = modules[component.name] ?? modules[component.full] { + remaining = remaining.dropFirst() + return (try findChild(of: matched, remaining: remaining) , []) + } else { + // This is an absolute path that doesn't start with a valid module. Don't continue the search + // in relative mode. + throw Error.notFound(availableChildren: Array(modules.keys)) } } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift index efc378efbd..6f45d4b60c 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift @@ -209,7 +209,7 @@ final class PathHierarchyBasedLinkResolver { } do { - let parentID = unresolvedReference.path.hasPrefix("/") ? nil : resolvedReferenceMap[parent] + let parentID = resolvedReferenceMap[parent] let found = try pathHierarchy.find(path: Self.path(for: unresolvedReference), parent: parentID, onlyFindSymbols: isCurrentlyResolvingSymbolLink) let foundReference = resolvedReferenceMap[found]! From afe3c497841cdc573a832023fcddd4a222ae5bd8 Mon Sep 17 00:00:00 2001 From: Max Obermeier Date: Sat, 13 Aug 2022 10:41:21 +0200 Subject: [PATCH 5/7] revert comment phrasing after accidental commit --- .../Link Resolution/DocumentationCacheBasedLinkResolver.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift index 89d713d503..bc898eb0d8 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift @@ -331,7 +331,7 @@ final class DocumentationCacheBasedLinkResolver { // The current implementation works in 3 phases: // - First, it computes the paths without disambiguators to identify colliding paths. // - Second, it computes the "correct" disambiguators for each collision. - // - Lastly, it joins together the results in a stable order to avoid indeterministic behavior. + // - Lastly, it joins together the results in a stable order to avoid non-deterministic behavior. let totalSymbolCount = unifiedGraphs.values.map { $0.symbols.count }.reduce(0, +) From 4b52aefb65a739cdf16d49300b7d592d7e8882a5 Mon Sep 17 00:00:00 2001 From: Max Obermeier Date: Sat, 13 Aug 2022 12:13:48 +0200 Subject: [PATCH 6/7] fix ambiguous relative links example --- .../SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md b/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md index a4be0e3fdb..b5a44f6a9f 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md +++ b/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md @@ -28,18 +28,20 @@ Both types of links can be used in a relative or absolute way. Absolute symbol l 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. -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. +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. ```swift struct Container { struct Container { /// ``Container`` links to `Container.Container` - /// ``/MyModule/Container`` links to `Container` + /// ``MyModule/Container`` links to the outer `Container` func foo() { } } } ``` +> Note: If `MyModule` were to be named `Container` too, only the absolute link `/Container/Container` could be used to refer to the outer `Container`. + ### Handling Ambiguous Links It's possible for collisions to occur in documentation links (symbol links or otherwise) where more than one page are represented by the same path. A common cause for documentation link collisions are function overloads (functions with the same name but different arguments or different return values). It's also possible to have documentation link collisions in conceptual content if an article file name is the same as a tutorial file name (excluding the file extension in both cases). From 7d34951c1c67c4e2e70c8c40af76f128fea94df2 Mon Sep 17 00:00:00 2001 From: Max Obermeier Date: Wed, 17 Aug 2022 14:42:28 +0200 Subject: [PATCH 7/7] remove changes relevant to absolute paths and shadowing --- .../DocumentationCacheBasedLinkResolver.swift | 15 +-- .../Link Resolution/PathHierarchy.swift | 112 ++++-------------- .../ExtendedTypesFormatTransformation.swift | 4 +- .../SwiftDocC/LinkResolution.md | 18 +-- .../ReferenceResolverTests.swift | 68 ++++++++--- .../BundleWithRelativePathAmbiguity.md | 60 +--------- 6 files changed, 83 insertions(+), 194 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift index bc898eb0d8..c6dae2b0ee 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift @@ -78,17 +78,8 @@ final class DocumentationCacheBasedLinkResolver { func referenceFor(absoluteSymbolPath path: String, parent: ResolvedTopicReference) -> ResolvedTopicReference? { // Check if `destination` is a known absolute reference URL. if let match = referencesIndex[path] { return match } - - // Check if `destination` is a known absolute symbol path... - if !path.hasPrefix("/") && parent.pathComponents.count > 2 { - // ...in the parent's module - let parentModule = parent.pathComponents[2] - let referenceURLString = "doc://\(parent.bundleIdentifier)/documentation/\(parentModule)/\(path)" - if let reference = referencesIndex[referenceURLString] { - return reference - } - } - // ...globally + + // Check if `destination` is a known absolute symbol path. let referenceURLString = "doc://\(parent.bundleIdentifier)/documentation/\(path.hasPrefix("/") ? String(path.dropFirst()) : path)" return referencesIndex[referenceURLString] } @@ -308,6 +299,8 @@ final class DocumentationCacheBasedLinkResolver { } + // MARK: Symbol reference creation + /// Returns a map between symbol identifiers and topic references. /// /// - Parameters: diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift index 72f149edd3..3864eb9b92 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift @@ -305,36 +305,17 @@ struct PathHierarchy { } private func findNode(path rawPath: String, parent: ResolvedIdentifier?, onlyFindSymbols: Bool) throws -> Node { + // The search for a documentation element can be though of as 3 steps: // First, parse the path into structured path components. let (path, isAbsolute) = Self.parse(path: rawPath) guard !path.isEmpty else { throw Error.notFound(availableChildren: []) } - // Second, we try to the node matching the path. This is done by first finding - // the root of the (possibly relative) path and then searching for the child - // from that root. - // A relative path could have multiple root candidates (where the first - // component is a match). We start searching at the `parent`, working - // our way up the tree, trying to find a child for each root candidate. - // This function reports all errors found on the way and - if successful - the - // matching node. - let (node, errors) = try searchForChildOnAllPossibleRoots(parentID: parent, path: path, isAbsolute: isAbsolute, onlyFindSymbols: onlyFindSymbols) - - if let node = node { - return node - } - - // Currently, we only report the first error, which corresponds to - // the root candidate closest to the `parent`. Aggregating errors could - // help giving more precise suggestions in the future. - throw errors.first! - } - - /// Tries to find a node in the subtree of `node` where `remaining` is the relative path from `node` to the child. - private func findChild(of node: Node, remaining: ArraySlice) throws -> Node { - var node = node - var remaining = remaining + // Second, find the node to start the search relative to. + // This may consume or or more path components. See implementation for details. + var remaining = path[...] + var node = try findRoot(parentID: parent, remaining: &remaining, isAbsolute: isAbsolute, onlyFindSymbols: onlyFindSymbols) // Third, search for the match relative to the start node. if remaining.isEmpty { @@ -458,13 +439,11 @@ struct PathHierarchy { /// /// - Parameters: /// - parentID: An optional ID of the node to start the search relative to. - /// - path: The parsed path components. + /// - remaining: The parsed path components. /// - isAbsolute: If the parsed path represent an absolute documentation link. /// - onlyFindSymbols: If symbol results are required. /// - Returns: The node to start the relative search relative to. - private func searchForChildOnAllPossibleRoots(parentID: ResolvedIdentifier?, path: [PathComponent], isAbsolute: Bool, onlyFindSymbols: Bool) throws -> (Node?, [Error]) { - var remaining = path[...] - + private func findRoot(parentID: ResolvedIdentifier?, remaining: inout ArraySlice, isAbsolute: Bool, onlyFindSymbols: Bool) throws -> Node { // If the first path component is "tutorials" or "documentation" then that let isKnownTutorialPath = remaining.first!.full == "tutorials" let isKnownDocumentationPath = remaining.first!.full == "documentation" @@ -487,34 +466,29 @@ struct PathHierarchy { } } remaining = remaining.dropFirst() - return (try findChild(of: articlesContainer, remaining: remaining) , []) + return articlesContainer } else if articlesContainer.children.keys.contains(component.name) || articlesContainer.children.keys.contains(component.full) { - return (try findChild(of: articlesContainer, remaining: remaining) , []) + return articlesContainer } } if !isKnownDocumentationPath { if tutorialContainer.name == component.name || tutorialContainer.name == component.full { remaining = remaining.dropFirst() - return (try findChild(of: tutorialContainer, remaining: remaining) , []) + return tutorialContainer } else if tutorialContainer.children.keys.contains(component.name) || tutorialContainer.children.keys.contains(component.full) { - return (try findChild(of: tutorialContainer, remaining: remaining) , []) + return tutorialContainer } // The parent for tutorial overviews / technologies is "tutorials" which has already been removed above, so no need to check against that name. else if tutorialOverviewContainer.children.keys.contains(component.name) || tutorialOverviewContainer.children.keys.contains(component.full) { - return (try findChild(of: tutorialOverviewContainer, remaining: remaining) , []) + return tutorialOverviewContainer } } - } - - if !isKnownTutorialPath && isAbsolute { - // If this is an absolute non-tutorial link, then the first component will be a module name. - if let matched = modules[component.name] ?? modules[component.full] { - remaining = remaining.dropFirst() - return (try findChild(of: matched, remaining: remaining) , []) - } else { - // This is an absolute path that doesn't start with a valid module. Don't continue the search - // in relative mode. - throw Error.notFound(availableChildren: Array(modules.keys)) + if !isKnownTutorialPath && isAbsolute { + // If this is an absolute non-tutorial link, then the first component will be a module name. + if let matched = modules[component.name] ?? modules[component.full] { + remaining = remaining.dropFirst() + return matched + } } } @@ -529,73 +503,37 @@ struct PathHierarchy { } if let parentID = parentID { - // We're dealing with a relative path, so search will be a bit more complicated. - // Starting from the parent, we ascend in the tree trying to find a node that matches - // our search path's first component. If we find one, we try to obtain the descendant - // matching the remainder of the search path using `findChild(of:remaining:)`. If that - // fails, we continue the search up the tree, after we've saved the error to be returned - // later. - - // Errors collected during the process - var errors: [Error] = [] - // 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. var parentNode = lookup[parentID]! let firstComponent = remaining.first! if matches(node: parentNode, component: firstComponent) { remaining = remaining.dropFirst() - do { - return (try findChild(of: parentNode, remaining: remaining), errors) - } catch let error as Error { - errors.append(error) - } + return parentNode } - while true { - if parentNode.children.keys.contains(firstComponent.name) || parentNode.children.keys.contains(firstComponent.full) { - do { - return (try findChild(of: parentNode, remaining: remaining), errors) - } catch let error as Error { - errors.append(error) - } - } - + while !parentNode.children.keys.contains(firstComponent.name) && !parentNode.children.keys.contains(firstComponent.full) { guard let parent = parentNode.parent else { if matches(node: parentNode, component: firstComponent){ remaining = remaining.dropFirst() - do { - return (try findChild(of: parentNode, remaining: remaining), errors) - } catch let error as Error { - errors.append(error) - } + return parentNode } if let matched = modules[component.name] ?? modules[component.full] { remaining = remaining.dropFirst() - do { - return (try findChild(of: matched, remaining: remaining), errors) - } catch let error as Error { - errors.append(error) - } + return matched } - // No node up the hierarchy from the provided parent has a child that matches the first path component. // Go back to the provided parent node for diagnostic information about its available children. parentNode = lookup[parentID]! - - // We've reached the top of the tree...we return all the errors we obtained in the process along with - // the final error providing the partial result. - - errors.append(Error.partialResult(partialResult: parentNode, remainingSubpath: remaining.map({ $0.full }).joined(separator: "/"), availableChildren: parentNode.children.keys.sorted(by: availableChildNameIsBefore))) - return (nil, errors) + throw Error.partialResult(partialResult: parentNode, remainingSubpath: remaining.map({ $0.full }).joined(separator: "/"), availableChildren: parentNode.children.keys.sorted(by: availableChildNameIsBefore)) } - parentNode = parent } + return parentNode } // If no parent ID was provided, check if the first path component is a module name. if let matched = modules[component.name] ?? modules[component.full] { remaining = remaining.dropFirst() - return (try findChild(of: matched, remaining: remaining) , []) + return matched } // No place to start the search from could be found. diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift index f42cdc371e..a7f30349eb 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypesFormatTransformation.swift @@ -120,8 +120,8 @@ extension ExtendedTypesFormatTransformation { /// If it finds such symbols, it applies the actual transformation. Refer to the sections below to find /// out how the two formats differ. /// - /// In addition, the transformation prepends the given `moduleName` to the `pathComponents` of all - /// symbols in the graph. + /// In addition, the transformation prepends each symbol's `swiftExtension.extendedModule` + /// name to its `pathComponents`. /// /// ### The Extension Block Symbol Format /// diff --git a/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md b/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md index b5a44f6a9f..d4a3d56470 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md +++ b/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md @@ -22,25 +22,11 @@ doc://com.example/path/to/documentation/page#optional-heading bundle ID path in docs hierarchy heading name ``` -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`` `. - ## Resolving a Documentation Link -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. - -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. - -```swift -struct Container { - struct Container { - /// ``Container`` links to `Container.Container` - /// ``MyModule/Container`` links to the outer `Container` - func foo() { } - } -} -``` +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. -> Note: If `MyModule` were to be named `Container` too, only the absolute link `/Container/Container` could be used to refer to the outer `Container`. +These relative documentation links can specify path components from higher up in the documentation hierarchy to reference container symbols or container pages. ### Handling Ambiguous Links diff --git a/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift index 23606b14fb..84e410a068 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift @@ -344,37 +344,67 @@ class ReferenceResolverTests: XCTestCase { XCTAssertEqual(referencingFileDiagnostics.filter({ $0.identifier == "org.swift.docc.unresolvedTopicReference" }).count, 1) } - func testAbsoluteAndRelativeReferencesToExternalAndExtensionSymbols() throws { - let (bundleURL, bundle, context) = try testBundleAndContext(copying: "BundleWithRelativePathAmbiguity") + func testRelativeReferencesToExtensionSymbols() throws { + let (bundleURL, bundle, context) = try testBundleAndContext(copying: "BundleWithRelativePathAmbiguity") { root in + // We don't want the external target to be part of the archive as that is not + // officially supported yet. + try FileManager.default.removeItem(at: root.appendingPathComponent("Dependency.symbols.json")) + + try """ + # ``BundleWithRelativePathAmbiguity/Dependency`` + + ## Overview + + ### Module Scope Links + + - ``BundleWithRelativePathAmbiguity/Dependency`` + - ``BundleWithRelativePathAmbiguity/Dependency/AmbiguousType`` + - ``BundleWithRelativePathAmbiguity/Dependency/AmbiguousType/foo()`` + + ### Extended Module Scope Links + + - ``Dependency`` + - ``Dependency/AmbiguousType`` + - ``Dependency/AmbiguousType/foo()`` + + ### Local Scope Links + + - ``Dependency`` + - ``AmbiguousType`` + - ``AmbiguousType/foo()`` + """.write(to: root.appendingPathComponent("Article.md"), atomically: true, encoding: .utf8) + } defer { try? FileManager.default.removeItem(at: bundleURL) } // Get a translated render node - let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/BundleWithRelativePathAmbiguity", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/BundleWithRelativePathAmbiguity/Dependency", sourceLanguage: .swift)) var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference, source: nil) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode let content = try XCTUnwrap(renderNode.primaryContentSections.first as? ContentRenderSection).content - func assertListedReferencesInSectionMatchHeading(_ absoluteShorthandReference: String) throws { - let headingString = "`\(absoluteShorthandReference)`" - let absoluteReferenceString = "doc://org.swift.docc.example/documentation\(absoluteShorthandReference)" + let expectedReferences = [ + "doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency", + "doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType", + "doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType/foo()", + ] + + let sectionContents = [ + content.contents(of: "Module Scope Links"), + content.contents(of: "Extended Module Scope Links"), + content.contents(of: "Local Scope Links"), + ] + + let sectionReferences = try sectionContents.map { sectionContent in + try sectionContent.listItems().map { item in try XCTUnwrap(item.firstReference(), "found no reference for \(item)") } + } - for listItem in content.contents(of: headingString).listItems() { - let reference = try XCTUnwrap(listItem.firstReference(), "found no reference for \(listItem)") - XCTAssertEqual(reference.identifier, absoluteReferenceString, "found mismatch for \(listItem)") + for resolvedReferencesOfSection in sectionReferences { + zip(resolvedReferencesOfSection, expectedReferences).forEach { resolved, expected in + XCTAssertEqual(resolved.identifier, expected) } } - - try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity") - try assertListedReferencesInSectionMatchHeading("/Dependency") - try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity/Dependency") - try assertListedReferencesInSectionMatchHeading("/Dependency/AmbiguousType") - try assertListedReferencesInSectionMatchHeading("/Dependency/AmbiguousProtocol") - try assertListedReferencesInSectionMatchHeading("/Dependency/UnambiguousType") - try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType") - try assertListedReferencesInSectionMatchHeading("/BundleWithRelativePathAmbiguity/Dependency/AmbiguousProtocol") - try assertListedReferencesInSectionMatchHeading("/Dependency/AmbiguousType/unambiguousFunction()") } struct TestExternalReferenceResolver: ExternalReferenceResolver { diff --git a/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.md b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.md index 7ca5c66caf..9dc7a4a856 100644 --- a/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.md +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.md @@ -4,64 +4,6 @@ This bundle contains external symbols of the dependency module and local extensi ## Overview -This bundle tests path resolution in a combined documentation archive of the module ``BundleWithRelativePathAmbiguity`` and its ``/Dependency``. The main bundle ``BundleWithRelativePathAmbiguity`` extends its ``/Dependency``, thus many of the types from ``/Dependency`` have Extended Type Pages in ``BundleWithRelativePathAmbiguity``. Since this document is part of ``BundleWithRelativePathAmbiguity``, ambiguous relative paths should always resolve to the Extended Type Pages and not the original type pages from the external module's documentation. - -Absolute references can be used to unambigously refer to the pages in the external module's documentation in ambiguous situations. While one could also use the fully qualified URI including the bundle identifier, the shorthand syntax with a leading slash is the preferred way to go. - -### Module Pages - -#### `/BundleWithRelativePathAmbiguity` - -``BundleWithRelativePathAmbiguity`` is the main module and can therefore be referenced using all of the following: -- ``BundleWithRelativePathAmbiguity`` (relative) -- ``/BundleWithRelativePathAmbiguity`` (absolute, shorthand) -- ``doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity`` (absolute, fully qualified) - -#### `/Dependency` - -``/Dependency`` is the original module page in the dependency's documentation and can therefore only be referenced absolutely: -- ``/Dependency`` (absolute, shorthand) -- ``doc://org.swift.docc.example/documentation/Dependency`` (absolute, fully qualified) - -### Extended Module Pages - -#### `/BundleWithRelativePathAmbiguity/Dependency` - -``Dependency`` is the Extended Module Page for the dependency module in the main module's documentation. As it is the local type, it can be referenced with a relative address: -- ``Dependency`` (relative) -- ``/BundleWithRelativePathAmbiguity/Dependency`` (absolute, shorthand) -- ``doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency`` (absolute, fully qualified) - - -### Type Pages - -#### `/Dependency/AmbiguousType` - -- ``/Dependency/AmbiguousType`` (absolute, shorthand) -- ``doc://org.swift.docc.example/documentation/Dependency/AmbiguousType`` (absolute, fully qualified) - -#### `/Dependency/UnambiguousType` - -It should be possible to reference `/Dependency/UnambiguousType` relatively, even from `documentation/BundleWithRelativePathAmbiguity`. This way, the relative link switches to the extended type page automatically when the type gets extended. - -- ``Dependency/UnambiguousType`` (relative) -- ``/Dependency/UnambiguousType`` (absolute, shorthand) -- ``doc://org.swift.docc.example/documentation/Dependency/UnambiguousType`` (absolute, fully qualified) - -### Extended Type Pages - -#### `/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType` - -- ``Dependency/AmbiguousType`` (relative) -- ``/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType`` (absolute, shorthand) -- ``doc://org.swift.docc.example/documentation/BundleWithRelativePathAmbiguity/Dependency/AmbiguousType`` (absolute, fully qualified) - -### Member Pages - -#### `/Dependency/AmbiguousType/unambiguousFunction()` - -- ``Dependency/AmbiguousType/unambiguousFunction()`` (relative) -- ``/Dependency/AmbiguousType/unambiguousFunction()`` (absolute, shorthand) -- ``doc://org.swift.docc.example/documentation/Dependency/AmbiguousType/unambiguousFunction()`` (absolute, fully qualified) +This bundle tests path resolution in a combined documentation archive of the module ``BundleWithRelativePathAmbiguity`` and its `Dependency`. The main bundle ``BundleWithRelativePathAmbiguity`` extends its `Dependency`, thus many of the types from `Dependency` have Extended Type Pages in ``BundleWithRelativePathAmbiguity/Dependency``.