diff --git a/Package.resolved b/Package.resolved index a96e4455b1..f08609a7a2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -42,7 +42,7 @@ "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", "state": { "branch": "main", - "revision": "da6cedd103e0e08a2bc7b14869ec37fba4db72d9", + "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", "version": null } }, diff --git a/Package.swift b/Package.swift index 0900ffdeb1..210a470953 100644 --- a/Package.swift +++ b/Package.swift @@ -79,7 +79,9 @@ let package = Package( // Test utility library .target( name: "SwiftDocCTestUtilities", - dependencies: []), + dependencies: [ + "SymbolKit" + ]), // Command-line tool .executableTarget( diff --git a/Sources/SwiftDocC/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..34fef44a21 100644 --- a/Sources/SwiftDocC/Infrastructure/CoverageDataEntry.swift +++ b/Sources/SwiftDocC/Infrastructure/CoverageDataEntry.swift @@ -243,7 +243,12 @@ extension CoverageDataEntry { .protocol, .typeAlias, .associatedType, - .typeDef: + .typeDef, + .extendedClass, + .extendedStructure, + .extendedEnumeration, + .extendedProtocol, + .unknownExtendedType: self = .types case .localVariable, .instanceProperty, @@ -256,7 +261,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 +302,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 524af65995..1b9898d036 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -277,6 +277,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. @@ -1035,7 +1038,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, .inContextOf: guard let parentRef = symbolIndex[edge.target]?.reference, let childRef = symbolIndex[edge.source]?.reference else { return nil } @@ -1933,7 +1936,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..d3f51e1090 100644 --- a/Sources/SwiftDocC/Infrastructure/External Data/ExternalSymbolResolver+SymbolKind.swift +++ b/Sources/SwiftDocC/Infrastructure/External Data/ExternalSymbolResolver+SymbolKind.swift @@ -66,6 +66,18 @@ 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 + case .unknownExtendedType: + symbolKind = .unknownExtendedType // 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..652c06429b 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/DocumentationCacheBasedLinkResolver.swift @@ -495,10 +495,13 @@ final class DocumentationCacheBasedLinkResolver { // therefore for the currently processed symbol to be a child of a re-written symbol it needs to have // at least 3 components. It's a fair optimization to make since graphs will include a lot of root level symbols. guard reference.pathComponents.count > 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/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/ExtendedTypeFormatExtension.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatExtension.swift new file mode 100644 index 0000000000..db0345810b --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatExtension.swift @@ -0,0 +1,104 @@ +/* + 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 { + /// This relationship connects top-level extended type symbols the + /// respective extended module symbol. + static let declaredIn = Self(rawValue: "declaredIn") + + /// This relationship markes a parent-child hierarchy between a nested + /// extended type symbol and its parent extended type symbol. It mirrors the + /// `memberOf` relationship between the two respective original type symbols. + static let inContextOf = Self(rawValue: "inContextOf") +} + +// 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 unknownExtendedType = Self(rawValue: "unknown.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 unknownExtendedType + } + } + + static let unknownExtendedType = Self(parsedIdentifier: .unknownExtendedType, 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/ExtendedTypeFormatTransformation.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift new file mode 100644 index 0000000000..482b165120 --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift @@ -0,0 +1,576 @@ +/* + 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 ExtendedTypeFormatTransformation { } + +extension ExtendedTypeFormatTransformation { + /// Transforms the extension symbol graph file to better match the hierarchical symbol structure that DocC uses when processing and rendering documentation. + /// + /// ## Discussion + /// + /// Performing this transformation before the symbols are registered allows the handling of extensions to be centralized in one place. + /// + /// ### Extensions in Symbol Graph Files + /// + /// Extension symbol graph files, i.e. such that are named `ExtendingModule@ExtendedModule.symbols.json`, + /// use the Extension Block Symbol Format to store information about extensions to types from the respective extended module. + /// + /// - Note: The emission of extension information to extension symbol graph files can be disabled on the Swift compiler. If such graphs + /// are encountered, this function returns `false` and does not modify the `symbolGraph`. + /// + /// When using the Extension Block Symbol Format in the symbol graph file, each [extension declaration](extension-decl), + /// i.e. `extension X { ... }`, has one corresponding symbol with a `extension` kind. Each extension declaration symbol + /// has one `extensionTo` relationship to the symbol that it extends. + /// + /// ```swift + /// extension ExtendedSymbol { ... } + /// // │ ▲ + /// // ╰──────────╯ extensionTo + /// ``` + /// + /// Each symbol that's defined in the extension declaration has a `memberOf` relationship to the extension declaration symbol. + /// + /// ```swift + /// extension ExtendedSymbol { + /// // ▲ + /// // ╰────────╮ memberOf + /// func addedSymbol() { ... } + /// } + /// ``` + /// + /// If the extension adds protocol conformances, the extension declaration symbol has `conformsTo` relationships for each adopted protocol. + /// + /// ```swift + /// extension ExtendedSymbol: AdoptedProtocol { ... } + /// // │ ▲ + /// // ╰───────────────────────────╯ conformsTo + /// ``` + /// + /// ### Transformation + /// + /// The Extension Block Symbol Format is designed to capture all information and directly reflect the declarations in your code. However, + /// when reading documentation, we have slightly different demands. Therefore, we transform the Extension Block Symbol Format into the + /// Extended Type Format, which aggregates and structures the extensions' contents. + /// + /// #### Extended Type Symbols + /// + /// For each extended symbol, all extension declarations are combined into a single "extended type" symbol with the combined + /// `memberOf` and `conformsTo` relationships for those extension declarations. The extended symbol has the most visible + /// access off all of the extensions and the longest documentation comment of all the extensions. + /// + /// ```swift + /// /// Long comment that // The combined "ExtendedSymbol" extended type + /// /// spans two lines. // symbol gets its documentation comment from + /// internal extension ExtendedSymbol { ... } // ◀─── this extension declaration + /// // .. + /// /// Short single-line comment. // and its "public" access control level from + /// public extension ExtendedSymbol { ... } // ◀─── this extension declaration + /// ``` + /// + /// The kind of the extended type symbol include information about the extended symbol's kind: + /// + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedStruct`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedClass`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedEnum`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedProtocol`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/unknownExtendedType`` + /// + /// #### Documentation Hierarchy + /// + /// For the extended module, a new "extended module" symbol is created. Each top-level extended symbol has a `declaredIn` relationship to the extended module symbol: + /// + /// ``` + /// ┌─────────────┐ ┌──────────────────┐ + /// │ Extended │ declaredIn │ "ExtendedSymbol" │ + /// │ Module │◀────────────│ Extended Type │ + /// └─────────────┘ └──────────────────┘ + /// ``` + /// + /// For extension declarations where the extended type is nested within another type, an "extended type" symbol is created for each symbol + /// in the hierarchy. + /// + /// ```swift + /// extension Outer.Inner { ... } + /// ``` + /// + /// Each nested "extended type" symbol has a `inContextOf` relationship to its "extended type" parent symbol. + /// + /// ``` + /// ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + /// │ Extended │ declaredIn │ "Outer" │ inContextOf │ "Inner" │ + /// │ Module │◀────────────│Extended Type│◀─────────────│Extended Type│ + /// └─────────────┘ └─────────────┘ └─────────────┘ + /// ``` + /// + /// [extension-decl]: https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID378 + /// + /// #### Path Components + /// + /// The URLs for symbols declared in extensions include both the extend_ed_ and extend_ing_ module names: + /// + /// ``` + /// /ExtendingModule/ExtendedModule/Path/To/ExtendedSymbol/NewSymbol + /// ``` + /// + /// To accomplish this, all extended type symbols' path components are prefixed with the extended module. + /// + /// After transforming the extension symbol graph, the extended symbols are to be merged with the extending + /// module's main symbol graph. + /// + /// - Parameter symbolGraph: An (extension) symbol graph that should use the Extension Block Symbol Format. + /// - Parameter moduleName: The name of the extended module all top-level symbols in this symbol graph belong to. + /// - Returns: Returns whether the transformation was applied or not. The transformation is applied if `symbolGraph` is an extension graph + /// in the Extended Type Symbol Format. + static func transformExtensionBlockFormatToExtendedTypeFormat(_ symbolGraph: inout SymbolGraph, moduleName: String) throws -> Bool { + var extensionBlockSymbols = extractExtensionBlockSymbols(from: &symbolGraph) + + guard !extensionBlockSymbols.isEmpty else { + return false + } + + prependModuleNameToPathComponents(&symbolGraph.symbols.values, moduleName: moduleName) + prependModuleNameToPathComponents(&extensionBlockSymbols.values, moduleName: moduleName) + + var (extensionToRelationships, + memberOfRelationships, + conformsToRelationships) = extractRelationshipsTouchingExtensionBlockSymbols(from: &symbolGraph, using: extensionBlockSymbols) + + var (extendedTypeSymbols, + extensionBlockToExtendedTypeMapping, + extendedTypeToExtensionBlockMapping) = synthesizePrimaryExtendedTypeSymbols(using: extensionBlockSymbols, extensionToRelationships) + + let contextOfRelationships = synthesizeSecondaryExtendedTypeSymbols(&extendedTypeSymbols) + + 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 + // across 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) + symbolGraph.relationships.append(contentsOf: contextOfRelationships) + extendedTypeSymbols.values.forEach { symbol in symbolGraph.symbols[symbol.identifier.precise] = symbol } + + try synthesizeExtendedModuleSymbolAndDeclaredInRelationships(on: &symbolGraph, + using: extendedTypeSymbols.values.filter { symbol in symbol.pathComponents.count == 2 }.map(\.identifier.precise), + moduleName: moduleName) + + return true + } + + /// Tries to obtain `docComment`s for all `targets` and copies the documentation 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, moduleName: String) where S.Element == SymbolGraph.Symbol { + for i in symbols.indices { + let symbol = symbols[i] + + symbols[i] = symbol.replacing(\.pathComponents, with: [moduleName] + 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 following kinds: + /// + /// - `.extensionTo`: 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 `extensionToRelationships`. + /// + /// Creates symbols of the following kinds: + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedStruct`` + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedClass`` + /// - ``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 synthesizePrimaryExtendedTypeSymbols(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]] = [:] + var pathComponentToExtendedTypeMapping: [ArraySlice: String] = [:] + + extensionBlockToExtendedTypeMapping.reserveCapacity(extensionBlockSymbols.count) + + let createExtendedTypeSymbolAndAnchestors = { (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 fragment in declarationFragments[3...] { + switch (fragment.kind, fragment.spelling) { + case (.typeIdentifier, _), + (.identifier, _), + (.text, "."): + prefixWithoutWhereClause.append(fragment) + 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 + // across 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) + } ?? createExtendedTypeSymbolAndAnchestors(extensionBlockSymbol, extendedSymbolId) + + pathComponentToExtendedTypeMapping[symbol.pathComponents[...]] = symbol.identifier.precise + + extendedTypeSymbols[symbol.identifier.precise] = symbol + + extensionBlockToExtendedTypeMapping[extensionTo.source] = symbol.identifier.precise + extendedTypeToExtensionBlockMapping[symbol.identifier.precise, default: []] += [extensionBlockSymbol.identifier.precise] + } + + return (extendedTypeSymbols, extensionBlockToExtendedTypeMapping, extendedTypeToExtensionBlockMapping) + } + + /// Synthesizes missing ancestor extended type symbols for any nested types among the `extendedTypeSymbols` and + /// creates the relevant ``SymbolKit/SymbolGraph/Relationship/inContextOf`` relationships. + /// + /// Creates symbols of the following kinds: + /// - ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/unknownExtendedType`` + /// + /// If a nested type is extended, but its parent (or another ancestor) is not, this ancestor is not part of the + /// extension block symbol format. In that case, a extended type symbol of unknown kind is synthesized by + /// this function. However, if the ancestor symbol is extended, the `extendedTypeSymbols` should + /// already contain the respective symbol. In that case, the ``SymbolKit/SymbolGraph/Relationship/inContextOf`` + /// is attached to the existing symbol. + /// + /// - Returns: the ``SymbolKit/SymbolGraph/Relationship/inContextOf`` relationships between the + /// relevant extended type symbols + private static func synthesizeSecondaryExtendedTypeSymbols(_ extendedTypeSymbols: inout [String: SymbolGraph.Symbol]) -> [SymbolGraph.Relationship] { + let sortedKeys: [(pathComponents: [String], preciseId: String)] = extendedTypeSymbols.map { key, value in + (value.pathComponents, key) + }.sorted(by: { a, b in a.pathComponents.count <= b.pathComponents.count && a.preciseId < b.preciseId }) + + var pathComponentsToSymbolIds: [ArraySlice: String] = [:] + pathComponentsToSymbolIds.reserveCapacity(extendedTypeSymbols.count) + for (key, symbol) in extendedTypeSymbols { + pathComponentsToSymbolIds[symbol.pathComponents[...]] = key + } + + func lookupSymbol(_ pathComponents: ArraySlice) -> SymbolGraph.Symbol? { + guard let id = pathComponentsToSymbolIds[pathComponents] else { + return nil + } + + return extendedTypeSymbols[id] + } + + var relationships = [SymbolGraph.Relationship]() + var symbolIsConnectedToParent = [String: Bool]() + symbolIsConnectedToParent.reserveCapacity(extendedTypeSymbols.count) + + for (pathComponents, preciseId) in sortedKeys { + guard var symbol = extendedTypeSymbols[preciseId] else { + continue + } + + var pathComponents = pathComponents[0..(_ 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 the extended module symbol and declaredIn relationships on the given `symbolGraph` based on the given `extendedTypeSymbolIds`. + /// + /// Creates one symbol of kind ``SymbolKit/SymbolGraph/Symbol/KindIdentifier/extendedModule`` with the given name. + /// The extended type symbols are connected with the extended module symbol using relationships of kind + /// ``SymbolKit/SymbolGraph/Relationship/declaredIn``. + private static func synthesizeExtendedModuleSymbolAndDeclaredInRelationships(on symbolGraph: inout SymbolGraph, using extendedTypeSymbolIds: S, moduleName: String) throws + where S.Element == String { + var extendedModuleId: String? + + // we sort the symbols here because their order is not guaranteed to stay the same + // across 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 + } + + let id = extendedModuleId ?? "s:m:" + extendedTypeSymbol.identifier.precise + extendedModuleId = 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: moduleName, navigator: nil, subHeading: nil, prose: nil), + pathComponents: [moduleName], + 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 closure: (Self) -> V) -> Self { + var new = self + new[keyPath: keyPath] = closure(self) + return new + } +} + +private extension SymbolGraph.Relationship { + func replacing(_ keyPath: WritableKeyPath, with value: V) -> Self { + var new = self + new[keyPath: keyPath] = value + return new + } +} + +private extension String { + func asDeclarationFragment(_ kind: SymbolGraph.Symbol.DeclarationFragments.Fragment.Kind) -> [SymbolGraph.Symbol.DeclarationFragments.Fragment] { + [.init(kind: kind, spelling: self, preciseIdentifier: nil)] + } +} + +private extension Dictionary { + func keeping(_ keys: Key...) -> Self { + var new = Self() + + for key in keys { + new[key] = self[key] + } + + return new + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphConcurrentDecoder.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphConcurrentDecoder.swift index d7ab90a023..d3a3484a79 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphConcurrentDecoder.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphConcurrentDecoder.swift @@ -54,7 +54,9 @@ enum SymbolGraphConcurrentDecoder { /// it made sense to spread the work equally (in other words to decode each N-th symbol per worker) /// so that we can get the best performance out of the concurrent work. - static func decode(_ data: Data, concurrentBatches: Int = 4) throws -> SymbolGraph { + static func decode(_ data: Data, concurrentBatches: Int = 4, using decoder: JSONDecoder = JSONDecoder()) throws -> SymbolGraph { + + var symbolGraph: SymbolGraph! let decodeError = Synchronized(nil) @@ -67,7 +69,7 @@ enum SymbolGraphConcurrentDecoder { group.async(queue: queue) { do { // Decode the symbol graph bar the symbol list. - symbolGraph = try JSONDecoder().decode(SymbolGraphWithoutSymbols.self, from: data).symbolGraph + symbolGraph = try JSONDecoder(like: decoder).decode(SymbolGraphWithoutSymbols.self, from: data).symbolGraph } catch { decodeError.sync({ $0 = error }) } @@ -75,7 +77,7 @@ enum SymbolGraphConcurrentDecoder { // Concurrently decode each batch of symbols in the graph. (0.. (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 30b40fc282..1237f6a090 100644 --- a/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift +++ b/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift @@ -212,6 +212,12 @@ 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" + case .unknownExtendedType: return "Extended Types" default: return "Symbols" } } @@ -240,6 +246,13 @@ extension AutomaticCuration { .`typealias`, .`typeProperty`, .`typeMethod`, - .`enum` + .`enum`, + + .extendedModule, + .extendedClass, + .extendedProtocol, + .extendedStructure, + .extendedEnumeration, + .unknownExtendedType, ] } diff --git a/Sources/SwiftDocC/Model/DocumentationNode.swift b/Sources/SwiftDocC/Model/DocumentationNode.swift index af686f773c..84b289ac44 100644 --- a/Sources/SwiftDocC/Model/DocumentationNode.swift +++ b/Sources/SwiftDocC/Model/DocumentationNode.swift @@ -489,8 +489,13 @@ 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 + case .unknownExtendedType: return .unknownExtendedType default: return .unknown } } diff --git a/Sources/SwiftDocC/Model/Kind.swift b/Sources/SwiftDocC/Model/Kind.swift index 9be201f019..5f2cf7619f 100644 --- a/Sources/SwiftDocC/Model/Kind.swift +++ b/Sources/SwiftDocC/Model/Kind.swift @@ -155,6 +155,18 @@ 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) + + public static let unknownExtendedType = DocumentationNode.Kind(name: "Extended Type", id: "org.swift.docc.kind.unknownExtendedType", isSymbol: true) /// The list of all known kinds of documentation nodes. /// - Note: The `unknown` value is not included. @@ -171,6 +183,8 @@ extension DocumentationNode.Kind { .enumerationCase, .initializer, .deinitializer, .instanceMethod, .instanceProperty, .instanceSubscript, .instanceVariable, .typeMethod, .typeProperty, .typeSubscript, // Data .buildSetting, .propertyListKey, + // Extended Symbols + .extendedModule, .extendedStructure, .extendedClass, .extendedEnumeration, .extendedProtocol, .unknownExtendedType, // Other .keyword, .restAPI, .tag, .propertyList, .object ] diff --git a/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift b/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift index 885108d6c1..79dfb3d5a0 100644 --- a/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift +++ b/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift @@ -155,7 +155,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 @@ -515,31 +515,7 @@ 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] - - 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) @@ -550,27 +526,9 @@ extension DocumentationContentRenderer { 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 - } - } - } + // 1. Map typeIdenifier tokens to identifier tokens where applicable + 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) @@ -584,3 +542,49 @@ extension DocumentationContentRenderer { } } + +private extension Array where Element == DeclarationRenderSection.Token { + // Replaces kind "typeIdentifier" with "identifier" if the fragments matches the pattern: + // [keyword=_] [text=" "] [(typeIdentifier|identifier)=Name_0] ( [text="."] [typeIdentifier=Name_i] )* + // where the Name_i from typeIdentifier tokens joined with separator "." equal the `symbolTitle` + func mapNameFragmentsToIdentifierKind(matching symbolTitle: String) -> Self { + // Check that the first 3 tokens are: [keyword=_] [text=" "] [(typeIdentifier|identifier)=_] + guard count >= 3, + self[0].kind == .keyword, + self[1].kind == .text, self[1].text == " ", + self[2].kind == .typeIdentifier || self[2].kind == .identifier + else { return self } + + // If the first named token belongs to an identifier, this is a module prefix. + // We store it for later comparison with the `combinedName` + let modulePrefix = self[2].kind == .identifier ? self[2].text + "." : "" + + var combinedName = self[2].text + + var finalTypeIdentifierIndex = 2 + var remainder = self.dropFirst(3) + // Continue checking for pairs of "." text tokens and typeIdentifier tokens: ( [text="."] [typeIdentifier=Name_i] )* + while remainder.count >= 2 { + let separator = remainder.removeFirst() + guard separator.kind == .text, separator.text == "." else { break } + let next = remainder.removeFirst() + guard next.kind == .typeIdentifier else { break } + + finalTypeIdentifierIndex += 2 + combinedName += "." + next.text + } + + guard combinedName == modulePrefix + symbolTitle else { return self } + + var mapped = self + for index in stride(from: 2, to: finalTypeIdentifierIndex+1, by: 2) { + let token = self[index] + mapped[index] = DeclarationRenderSection.Token( + text: token.text, + kind: .identifier, + preciseIdentifier: token.preciseIdentifier + ) + } + return mapped + } +} 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..825657ad34 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). @@ -50,9 +50,25 @@ If two or more symbol results have the same kind, then that information doesn't /path/to/someFunction-def456 ``` -Links with added disambiguation information is both harder read and harder write so DocC aims to require as little disambiguation as possible. +Links with added disambiguation information is both harder to read and harder to 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() { } +} +``` + +## 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..1dc8f6f808 100644 --- a/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift @@ -16,13 +16,22 @@ 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 { + // TODO: Synthesize appropriate `swift.extension` symbols that get transformed into + // the respective internal symbol kinds as defined by `ExtendedTypeFormatTransformation` + // and remove decoder injection logic from `DocumentationContext` and `SymbolGraphLoader` + 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 9d321da7b3..b7e6b4bf4a 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift @@ -1792,6 +1792,11 @@ let expected = """ text = text.replacingOccurrences(of: "\"relationships\" : [", with: """ "relationships" : [ + { + "source" : "s:7SideKit0A5ClassC10testSV", + "target" : "s:7SideKit0A5ClassC", + "kind" : "memberOf" + }, { "source" : "s:7SideKit0A5ClassC10testE", "target" : "s:7SideKit0A5ClassC", diff --git a/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift index bdbc0a0334..a9996245bc 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift @@ -344,6 +344,67 @@ 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) + } + + // 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 +626,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(value) = element { + headingLevel = value.level + return heading == value.text + } + return false + }) else { + return Slice(base: self, bounds: self.startIndex.. [RenderBlockContent.ListItem] { + self.compactMap { block -> [RenderBlockContent.ListItem]? in + if case let .unorderedList(value) = block { + return value.items + } + return nil + }.flatMap({ $0 }) + } +} + +private extension RenderBlockContent.ListItem { + func firstReference() -> RenderReferenceIdentifier? { + self.content.compactMap { block in + guard case let .paragraph(value) = block else { + return nil + } + + return value.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..af69d13941 --- /dev/null +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/ExtendedTypesFormatTransformationTests.swift @@ -0,0 +1,299 @@ +/* + 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 ExtendedTypeFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph, moduleName: "A")) + + // check the expected symbols exist + let extendedModuleA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedModule && symbol.title == "A" })) + + 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: extendedModuleA.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, 1 /* 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, extendedTypeB] + + addedMemberSymbolsTypeA + + addedMemberSymbolsTypeATwo + + addedMemberSymbolsTypeB).forEach { symbol in + XCTAssertEqual(symbol.pathComponents.first, "A") + } + } + + /// Tests that the transformation synthesizes ancestor extended type symbols if the extended type is a nested type + /// and that these synthesized ancestors are merged with pre-existing extended type symbols where applicable. + func testExtendedNestedTypeHierarchySynthesis() throws { + let contents = twoExtensionBlockSymbolsExtendingSameType(extendedModule: "A", extendedType: "A", withExtensionMembers: false, pathPrefix: ["Unextended", "Extended", "UnextendedInner"]) + + twoExtensionBlockSymbolsExtendingSameType(extendedModule: "A", extendedType: "Extended", withExtensionMembers: false, pathPrefix: ["Unextended"]) + + var graph = makeSymbolGraph(moduleName: "Module", + symbols: contents.symbols, + relationships: contents.relationships) + + // check the transformation recognizes the swift.extension symbols & transform + XCTAssert(try ExtendedTypeFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph, moduleName: "A")) + + // check the expected symbols exist + let extendedModuleA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedModule && symbol.title == "A" })) + + let extendedTypeUnextended = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .unknownExtendedType && symbol.title == "Unextended" })) + + // this ancestor is also extended so its kind should be known + let extendedTypeUnextendedDotExtended = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "Unextended.Extended" })) + + let extendedTypeUnextendedDotExtendedDotUnextendedInner = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .unknownExtendedType && symbol.title == "Unextended.Extended.UnextendedInner" })) + + let extendedTypeUnextendedDotExtendedDotUnextendedInnerDotA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "Unextended.Extended.UnextendedInner.A" })) + + // check the expected relationships exist + XCTAssertNotNil(graph.relationships.first(where: { relationship in + relationship.kind == .declaredIn + && relationship.target == extendedModuleA.identifier.precise + && relationship.source == extendedTypeUnextended.identifier.precise + })) + + XCTAssertNotNil(graph.relationships.first(where: { relationship in + relationship.kind == .inContextOf + && relationship.target == extendedTypeUnextended.identifier.precise + && relationship.source == extendedTypeUnextendedDotExtended.identifier.precise + })) + + XCTAssertNotNil(graph.relationships.first(where: { relationship in + relationship.kind == .inContextOf + && relationship.target == extendedTypeUnextendedDotExtended.identifier.precise + && relationship.source == extendedTypeUnextendedDotExtendedDotUnextendedInner.identifier.precise + })) + + XCTAssertNotNil(graph.relationships.first(where: { relationship in + relationship.kind == .inContextOf + && relationship.target == extendedTypeUnextendedDotExtendedDotUnextendedInner.identifier.precise + && relationship.source == extendedTypeUnextendedDotExtendedDotUnextendedInnerDotA.identifier.precise + })) + + // check there are no additional elements + XCTAssertEqual(graph.symbols.count, 5) + XCTAssertEqual(graph.relationships.count, 4) + } + + /// 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 ExtendedTypeFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph, moduleName: "A") + + 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 ExtendedTypeFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph, moduleName: "A") + + 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 ExtendedTypeFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph, moduleName: "A") + + 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 ExtendedTypeFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph, moduleName: "A") + + 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) + } + } + + // 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, + pathPrefix: [String] = []) -> SymbolGraphContents { + let titlePrefix = pathPrefix.joined(separator: ".") + (pathPrefix.isEmpty ? "" : ".") + + return SymbolGraphContents(symbols: [ + SymbolKit.SymbolGraph.Symbol(identifier: .init(precise: "s:e:s:\(extendedModule)\(pathPrefix.joined())\(extendedType)two", interfaceLanguage: "swift"), + names: .init(title: "\(titlePrefix)\(extendedType)", navigator: nil, subHeading: nil, prose: nil), + pathComponents: pathPrefix + ["\(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: []) + ]), + SymbolKit.SymbolGraph.Symbol(identifier: .init(precise: "s:e:s:\(extendedModule)\(pathPrefix.joined())\(extendedType)one", interfaceLanguage: "swift"), + names: .init(title: "\(titlePrefix)\(extendedType)", navigator: nil, subHeading: nil, prose: nil), + pathComponents: pathPrefix + ["\(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 ? [ + SymbolKit.SymbolGraph.Symbol(identifier: .init(precise: "s:\(extendedModule)\(pathPrefix.joined())\(extendedType)two", interfaceLanguage: "swift"), + names: .init(title: "two", navigator: nil, subHeading: nil, prose: nil), + pathComponents: pathPrefix + ["\(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: []) + ]), + SymbolKit.SymbolGraph.Symbol(identifier: .init(precise: "s:\(extendedModule)\(pathPrefix.joined())\(extendedType)one", interfaceLanguage: "swift"), + names: .init(title: "one", navigator: nil, subHeading: nil, prose: nil), + pathComponents: pathPrefix + ["\(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)\(pathPrefix.joined())\(extendedType)two", target: "s:\(extendedModule)\(pathPrefix.joined())\(extendedType)", kind: .extensionTo, targetFallback: "\(extendedModule).\(titlePrefix)\(extendedType)"), + .init(source: "s:e:s:\(extendedModule)\(pathPrefix.joined())\(extendedType)one", target: "s:\(extendedModule)\(pathPrefix.joined())\(extendedType)", kind: .extensionTo, targetFallback: "\(extendedModule).\(titlePrefix)\(extendedType)") + ] + (withExtensionMembers ? [ + .init(source: "s:\(extendedModule)\(pathPrefix.joined())\(extendedType)two", target: "s:e:s:\(extendedModule)\(pathPrefix.joined())\(extendedType)two", kind: .memberOf, targetFallback: "\(extendedModule).\(titlePrefix)\(extendedType)"), + .init(source: "s:\(extendedModule)\(pathPrefix.joined())\(extendedType)one", target: "s:e:s:\(extendedModule)\(pathPrefix.joined())\(extendedType)one", kind: .memberOf, targetFallback: "\(extendedModule).\(titlePrefix)\(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 + XCTAssertThrowsError(try loader.loadAll(), + "SymbolGraphLoader should throw when encountering a collection of symbol graph files, where some do and some don't use the extension block format") { error in + XCTAssertFalse(error.localizedDescription.contains(mainGraph.url.absoluteString)) + XCTAssertFalse(error.localizedDescription.contains(emptyExtensionGraph.url.absoluteString)) + XCTAssert(error.localizedDescription.contains(""" + Symbol graph files with extension declarations: + \(extensionBlockFormatExtensionGraph.url.absoluteString) + """)) + XCTAssert(error.localizedDescription.contains(""" + Symbol graph files without extension declarations: + \(noExtensionBlockFormatExtensionGraph.url.absoluteString) + """)) + } } + // 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 60% rename from Tests/SwiftDocCTests/Rendering/RenderNodeTranslator+SwiftTests.swift rename to Tests/SwiftDocCTests/Rendering/DocumentationContentRenderer+SwiftTests.swift index 0e3e3c4a00..cd867a05c2 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] = [ @@ -23,6 +23,18 @@ class RenderNodeTranslator_SwiftTests: XCTestCase { .init(text: "Object", kind: .typeIdentifier), ] + // Tokens where the type name is incorrectly identified as "typeIdentifier", which + // are additionally prefixed by a module name + let typeIdentifierTokensWithModule: [DeclarationRenderSection.Token] = [ + .init(text: "class", kind: .keyword), + .init(text: " ", kind: .text), + .init(text: "MyModule", kind: .identifier), + .init(text: ".", kind: .text), + .init(text: "Test", kind: .typeIdentifier), + .init(text: " : ", kind: .text), + .init(text: "Object", kind: .typeIdentifier), + ] + // Tokens where the type name is correctly identified as "identifier" let identifierTokens: [DeclarationRenderSection.Token] = [ .init(text: "class", kind: .keyword), @@ -36,7 +48,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,18 +56,27 @@ 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"]) } + + do { + // Verify that the type's own name is mapped from "typeIdentifier" to "identifier" kind even when prefixed with + // a module "identifier". + let mapped = DocumentationContentRenderer.Swift.navigatorTitle(for: typeIdentifierTokensWithModule, symbolTitle: "Test") + + XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .identifier, .text, .typeIdentifier]) + XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "MyModule", ".", "Test", " : ", "Object"]) + } } /// Test whether we map to "identifier" if we find an unexpected "typeIdentifier" token kind 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,11 +84,20 @@ 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"]) } + + do { + // Verify that the type's own name is mapped from "typeIdentifier" to "identifier" kind even when prefixed with + // a module "identifier". + let mapped = DocumentationContentRenderer.Swift.subHeading(for: typeIdentifierTokensWithModule, symbolTitle: "Test", symbolKind: "swift.class") + + XCTAssertEqual(mapped.map { $0.kind }, [.keyword, .text, .identifier, .text, .identifier, .text, .typeIdentifier]) + XCTAssertEqual(mapped.map { $0.text }, ["class", " ", "MyModule", ".", "Test", " : ", "Object"]) + } } // Tokens for an "init" symbol @@ -90,7 +120,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 +128,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 cfd28057a1..4e9a75b48b 100644 --- a/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift +++ b/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift @@ -16,12 +16,19 @@ import Markdown 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) @@ -33,7 +40,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")) @@ -53,7 +66,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) {