diff --git a/Package.resolved b/Package.resolved index a96e4455b1..fb5f773a6f 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": "0a67e26bb38d4f40c08bbf6a119affa3e02367ad", "version": null } }, diff --git a/Package.swift b/Package.swift index 0900ffdeb1..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( @@ -124,7 +126,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..bb0fa6ea1b 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. @@ -1022,7 +1025,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { 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 +1923,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/Link Resolution/DocumentationCacheBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift index 7c1ef5d3ca..c6dae2b0ee 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift @@ -335,15 +335,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..3864eb9b92 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) 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..a7f30349eb --- /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.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 { + 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 each symbol's `swiftExtension.extendedModule` + /// name to its `pathComponents`. + /// + /// ### 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/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md b/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/LinkResolution.md index 51f717a0ae..d4a3d56470 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,13 @@ doc://com.example/path/to/documentation/page#optional-heading bundle ID path in docs hierarchy heading name ``` -## Resolving a documentation link +## Resolving a Documentation Link 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. 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 +### 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 +52,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. 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/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..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! @@ -1792,6 +1815,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/ReferenceResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift index 0aa49a12ae..84e410a068 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift @@ -344,6 +344,69 @@ class ReferenceResolverTests: XCTestCase { XCTAssertEqual(referencingFileDiagnostics.filter({ $0.identifier == "org.swift.docc.unresolvedTopicReference" }).count, 1) } + 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/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 + + 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 resolvedReferencesOfSection in sectionReferences { + zip(resolvedReferencesOfSection, expectedReferences).forEach { resolved, expected in + XCTAssertEqual(resolved.identifier, expected) + } + } + } + struct TestExternalReferenceResolver: ExternalReferenceResolver { var bundleIdentifier = "com.external.testbundle" var expectedReferencePath = "/externally/resolved/path" @@ -565,3 +628,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/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/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..9dc7a4a856 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/BundleWithRelativePathAmbiguity.docc/BundleWithRelativePathAmbiguity.md @@ -0,0 +1,9 @@ +# ``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/Dependency``. + + 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 + + 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) {