diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000..fa036ee10c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,44 @@ +#[[ +This source file is part of the Swift open source project + +Copyright © 2014 - 2025 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 +#]] + +cmake_minimum_required(VERSION 3.24) + +# FIXME: The C language is enabled as `GNUInstallDirs` requires the language to +# function properly. We must further enable the language in the initial set to +# ensure that the correct linker is detected. +project(SwiftDocC + LANGUAGES C Swift) + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/swift) +set(CMAKE_Swift_COMPILE_OPTIONS_MSVC_RUNTIME_LIBRARY MultiThreadedDLL) +set(CMAKE_Swift_LANGUAGE_VERSION 5) + +include(GNUInstallDirs) + +# NOTE(compnerd) workaround CMake issues +add_compile_options("$<$:SHELL:-swift-version 5>") +add_compile_options("$<$:SHELL:-enable-upcoming-feature ConciseMagicFile>") +add_compile_options("$<$:SHELL:-enable-upcoming-feature ExistentialAny>") +add_compile_options("$<$:SHELL:-enable-upcoming-feature InternalImportsByDefault>") + +find_package(ArgumentParser) +find_package(SwiftASN1) +find_package(SwiftCrypto) +find_package(SwiftMarkdown) +find_package(LMDB) +find_package(SymbolKit) +find_package(cmark-gfm) + +add_compile_options("$<$:-package-name;SwiftDocC>") + +add_subdirectory(Sources) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b72111d846..da6d76fb8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -350,7 +350,32 @@ by running the test suite in a Docker environment that simulates Swift on Linux. cd swift-docc swift run docc ``` - + +## Updating Build Rules + +In order to build DocC as part of the Windows toolchain distribution uniformly, +a parallel CMake based build exists. Note that this is **not** supported for +development purposes (you cannot execute the test suite with this build). + +CMake requires that the full file list is kept up-to-date. When adding or +removing files in a given module, the `CMakeLists.txt` list must be updated to +the file list. + +The 1-line script below lists all the Swift files in the current directory tree. +You can use the script's output to replace the list of files in the CMakeLists.txt file for each target that you added files to or removed files from. + +```bash +python -c "print('\n'.join((f'{chr(34)}{path}{chr(34)}' if ' ' in path else path) for path in sorted(str(path) for path in __import__('pathlib').Path('.').rglob('*.swift'))))" +``` + +This should provide the listing of files in the module that can be used to +update the `CMakeLists.txt` associated with the target. + +In the case that a new target is added to the project, the new directory would +need to add the new library or executable target (`add_library` and +`add_executable` respectively) and the new target subdirectory must be listed in +the `Sources/CMakeLists.txt` (via `add_subdirectory`). + ## Continuous Integration Swift-DocC uses [swift-ci](https://ci.swift.org) infrastructure for its continuous integration diff --git a/Package.resolved b/Package.resolved index ffb8d1e20d..ca13d9b29a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -69,7 +69,7 @@ "location" : "https://github.com/swiftlang/swift-docc-symbolkit.git", "state" : { "branch" : "main", - "revision" : "96bce1cfad4f4d7e265c1eb46729ebf8a7695f4b" + "revision" : "65fa99b0b84db5c743415a00ee7f0099837aae1b" } }, { diff --git a/Package.swift b/Package.swift index 2381b5dfeb..3ceb324be8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 /* This source file is part of the Swift.org open source project @@ -15,6 +15,8 @@ import class Foundation.ProcessInfo let swiftSettings: [SwiftSetting] = [ .unsafeFlags(["-Xfrontend", "-warn-long-expression-type-checking=1000"], .when(configuration: .debug)), + .swiftLanguageMode(.v5), + .enableUpcomingFeature("ConciseMagicFile"), // SE-0274: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0274-magic-file.md .enableUpcomingFeature("ExistentialAny"), // SE-0335: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0335-existential-any.md .enableUpcomingFeature("InternalImportsByDefault"), // SE-0409: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0409-access-level-on-imports.md @@ -23,8 +25,8 @@ let swiftSettings: [SwiftSetting] = [ let package = Package( name: "SwiftDocC", platforms: [ - .macOS(.v12), - .iOS(.v15) + .macOS(.v13), + .iOS(.v16) ], products: [ .library( @@ -41,17 +43,20 @@ let package = Package( .target( name: "SwiftDocC", dependencies: [ + .target(name: "DocCCommon"), .product(name: "Markdown", package: "swift-markdown"), .product(name: "SymbolKit", package: "swift-docc-symbolkit"), .product(name: "CLMDB", package: "swift-lmdb"), .product(name: "Crypto", package: "swift-crypto"), ], + exclude: ["CMakeLists.txt"], swiftSettings: swiftSettings ), .testTarget( name: "SwiftDocCTests", dependencies: [ .target(name: "SwiftDocC"), + .target(name: "DocCCommon"), .target(name: "SwiftDocCTestUtilities"), ], resources: [ @@ -67,9 +72,11 @@ let package = Package( name: "SwiftDocCUtilities", dependencies: [ .target(name: "SwiftDocC"), + .target(name: "DocCCommon"), .product(name: "NIOHTTP1", package: "swift-nio", condition: .when(platforms: [.macOS, .iOS, .linux, .android])), .product(name: "ArgumentParser", package: "swift-argument-parser") ], + exclude: ["CMakeLists.txt"], swiftSettings: swiftSettings ), .testTarget( @@ -77,6 +84,7 @@ let package = Package( dependencies: [ .target(name: "SwiftDocCUtilities"), .target(name: "SwiftDocC"), + .target(name: "DocCCommon"), .target(name: "SwiftDocCTestUtilities"), ], resources: [ @@ -91,6 +99,7 @@ let package = Package( name: "SwiftDocCTestUtilities", dependencies: [ .target(name: "SwiftDocC"), + .target(name: "DocCCommon"), .product(name: "SymbolKit", package: "swift-docc-symbolkit"), ], swiftSettings: swiftSettings @@ -102,8 +111,28 @@ let package = Package( dependencies: [ .target(name: "SwiftDocCUtilities"), ], + exclude: ["CMakeLists.txt"], swiftSettings: swiftSettings ), + + // A few common types and core functionality that's useable by all other targets. + .target( + name: "DocCCommon", + dependencies: [ + // This target shouldn't have any local dependencies so that all other targets can depend on it. + // We can add dependencies on SymbolKit and Markdown here but they're not needed yet. + ], + swiftSettings: [.swiftLanguageMode(.v6)] + ), + + .testTarget( + name: "DocCCommonTests", + dependencies: [ + .target(name: "DocCCommon"), + .target(name: "SwiftDocCTestUtilities"), + ], + swiftSettings: [.swiftLanguageMode(.v6)] + ), // Test app for SwiftDocCUtilities .executableTarget( @@ -123,7 +152,6 @@ let package = Package( ], swiftSettings: swiftSettings ), - ] ) diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt new file mode 100644 index 0000000000..840812426a --- /dev/null +++ b/Sources/CMakeLists.txt @@ -0,0 +1,13 @@ +#[[ +This source file is part of the Swift open source project + +Copyright © 2014 - 2025 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 +#]] + +add_subdirectory(DocCCommon) +add_subdirectory(SwiftDocC) +add_subdirectory(SwiftDocCUtilities) +add_subdirectory(docc) diff --git a/Sources/DocCCommon/CMakeLists.txt b/Sources/DocCCommon/CMakeLists.txt new file mode 100644 index 0000000000..9ea7088e8a --- /dev/null +++ b/Sources/DocCCommon/CMakeLists.txt @@ -0,0 +1,13 @@ +#[[ +This source file is part of the Swift open source project + +Copyright © 2014 - 2025 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 +#]] + +add_library(DocCCommon STATIC + FixedSizeBitSet.swift + Mutex.swift + SourceLanguage.swift) diff --git a/Sources/DocCCommon/FixedSizeBitSet.swift b/Sources/DocCCommon/FixedSizeBitSet.swift new file mode 100644 index 0000000000..9ef13a9bb6 --- /dev/null +++ b/Sources/DocCCommon/FixedSizeBitSet.swift @@ -0,0 +1,294 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024-2025 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 +*/ + +/// A fixed size bit set, used for storing very small amounts of small integer values. +/// +/// This type can only store values that are `0 ..< Storage.bitWidth` which makes it _unsuitable_ as a general purpose set-algebra type. +/// However, in specialized cases where the caller can guarantee that all values are in bounds, this type can offer a memory and performance improvement. +package struct _FixedSizeBitSet: Sendable { + package typealias Element = Int + + package init() {} + + @usableFromInline + private(set) var storage: Storage = 0 + + @inlinable + init(storage: Storage) { + self.storage = storage + } +} + +// MARK: Set Algebra + +extension _FixedSizeBitSet: SetAlgebra { + private static func mask(_ number: Int) -> Storage { + precondition(number < Storage.bitWidth, "Number \(number) is out of bounds (0..<\(Storage.bitWidth))") + return 1 &<< number + } + + @inlinable + @discardableResult + mutating package func insert(_ member: Int) -> (inserted: Bool, memberAfterInsert: Int) { + let newStorage = storage | _FixedSizeBitSet.mask(member) + defer { + storage = newStorage + } + return (newStorage != storage, member) + } + + @inlinable + @discardableResult + mutating package func remove(_ member: Int) -> Int? { + let newStorage = storage & ~_FixedSizeBitSet.mask(member) + defer { + storage = newStorage + } + return newStorage != storage ? member : nil + } + + @inlinable + @discardableResult + mutating package func update(with member: Int) -> Int? { + let (inserted, _) = insert(member) + return inserted ? nil : member + } + + @inlinable + package func contains(_ member: Int) -> Bool { + storage & _FixedSizeBitSet.mask(member) != 0 + } + + @inlinable + package func isSuperset(of other: Self) -> Bool { + (storage & other.storage) == other.storage + } + + @inlinable + package func union(_ other: Self) -> Self { + .init(storage: storage | other.storage) + } + + @inlinable + package func intersection(_ other: Self) -> Self { + .init(storage: storage & other.storage) + } + + @inlinable + package func symmetricDifference(_ other: Self) -> Self { + .init(storage: storage ^ other.storage) + } + + @inlinable + mutating package func formUnion(_ other: Self) { + storage |= other.storage + } + + @inlinable + mutating package func formIntersection(_ other: Self) { + storage &= other.storage + } + + @inlinable + mutating package func formSymmetricDifference(_ other: Self) { + storage ^= other.storage + } + + @inlinable + package var isEmpty: Bool { + storage == 0 + } +} + +// MARK: Sequence + +extension _FixedSizeBitSet: Sequence { + @inlinable + package func makeIterator() -> some IteratorProtocol { + _Iterator(set: self) + } + + private struct _Iterator: IteratorProtocol { + typealias Element = Int + + private var storage: Storage + private var current: Int = -1 + + @inlinable + init(set: _FixedSizeBitSet) { + self.storage = set.storage + } + + @inlinable + mutating func next() -> Int? { + guard storage != 0 else { + return nil + } + // If the set is somewhat sparse, we can find the next element faster by shifting to the next value. + // This saves needing to do `contains()` checks for all the numbers since the previous element. + let amountToShift = storage.trailingZeroBitCount &+ 1 + storage &>>= amountToShift + + current &+= amountToShift + return current + } + } +} + +// MARK: Collection + +extension _FixedSizeBitSet: Collection { + // Collection conformance requires an `Index` type, that the collection can advance, and `startIndex` and `endIndex` accessors that follow certain requirements. + // + // For this design, as a hidden implementation detail, the `Index` holds the bit offset to the element. + + @inlinable + package subscript(position: Index) -> Int { + precondition(position.bit < Storage.bitWidth, "Index \(position.bit) out of bounds") + // Because the index stores the bit offset, which is also the value, we can simply return the value without accessing the storage. + return Int(position.bit) + } + + package struct Index: Comparable { + // The bit offset into the storage to the value + fileprivate var bit: UInt8 + + package static func < (lhs: Self, rhs: Self) -> Bool { + lhs.bit < rhs.bit + } + } + + @inlinable + package var startIndex: Index { + // This is the index (bit offset) to the smallest value in the bit set. + Index(bit: UInt8(storage.trailingZeroBitCount)) + } + + @inlinable + package var endIndex: Index { + // For a valid collection, the end index is required to be _exactly_ one past the last in-bounds index, meaning; `index(after: LAST_IN-BOUNDS_INDEX)` + // If the collection implementation doesn't satisfy this requirement, it will have an infinitely long `indices` collection. + // This either results in infinite implementations or hits internal preconditions in other Swift types that that collection has more elements than its `count`. + + // See `index(after:)` below for explanation of how the index after is calculated. + let lastInBoundsBit = UInt8(Storage.bitWidth &- storage.leadingZeroBitCount) + return Index(bit: lastInBoundsBit &+ UInt8((storage &>> lastInBoundsBit).trailingZeroBitCount)) + } + + @inlinable + package func index(after currentIndex: Index) -> Index { + // To advance the index we have to find the next 1 bit _after_ the current bit. + // For example, consider the following 16 bits, where values are represented from right to left: + // 0110 0010 0110 0010 + // + // To go from the first index to the second index, we need to count the number of 0 bits between it and the next 1 bit. + // We get this value by shifting the bits by one past the current index: + // 0110 0010 0110 0010 + // ╰╴current index + // 0001 1000 1001 1000 + // ~~~ 3 trailing zero bits + // + // The second index's absolute value is the one past the first index's value plus the number of trailing zero bits in the shifted value. + // + // For the third index we repeat the same process, starting by shifting the bits by one past second index: + // 0110 0010 0110 0010 + // ╰╴current index + // 0000 0001 1000 1001 + // 0 trailing zero bits + // + // This time there are no trailing zero bits in the shifted value, so the third index's absolute value is just one past the second index. + let shift = currentIndex.bit &+ 1 + return Index(bit: shift &+ UInt8((storage &>> shift).trailingZeroBitCount)) + } + + @inlinable + package func formIndex(after index: inout Index) { + // See `index(after:)` above for explanation. + index.bit &+= 1 + index.bit &+= UInt8((storage &>> index.bit).trailingZeroBitCount) + } + + @inlinable + package func distance(from start: Index, to end: Index) -> Int { + // To compute the distance between two indices we have to find the number of 1 bits from the start index to (but excluding) the end index. + // For example, consider the following 16 bits, where values are represented from right to left: + // 0110 0010 0110 0010 + // end╶╯ ╰╴start + // + // To find the distance between the second index and the fourth index, we need to count the number of 0 bits between it and the next 1 bit. + // We limit the calculation to this range in two steps. + // + // First, we mask out all the bits above the end index: + // end╶╮ ╭╴start + // 0110 0010 0110 0010 + // 0000 0011 1111 1111 mask + // + // Because collections can have end indices that extend out-of-bounds we need to clamp the mask from a larger integer type to avoid it wrapping around to 0. + let mask = Storage(clamping: (1 &<< UInt(end.bit)) &- 1) + var distance = storage & mask + + // Then, we shift away all the bits below the start index: + // end╶╮ ╭╴start + // 0000 0010 0110 0010 + // 0000 0000 0000 1001 + distance &>>= start.bit + + // The distance from start to end is the number of 1 bits in this number. + return distance.nonzeroBitCount + } + + @inlinable + package var first: Element? { + isEmpty ? nil : storage.trailingZeroBitCount + } + + @inlinable + package func min() -> Element? { + first // The elements are already sorted + } + + @inlinable + package func sorted() -> [Element] { + Array(self) // The elements are already sorted + } + + @inlinable + package var count: Int { + storage.nonzeroBitCount + } +} + +// MARK: Hashable + +extension _FixedSizeBitSet: Hashable {} + +// MARK: Combinations + +extension _FixedSizeBitSet { + /// Returns a list of all possible combinations of the elements in the set, in order of increasing number of elements. + package func allCombinationsOfValues() -> [Self] { + // Leverage the fact that bits of an Int represent the possible combinations. + let smallest = storage.trailingZeroBitCount + + var combinations: [Self] = [] + combinations.reserveCapacity((1 &<< count /*known to be less than Storage.bitWidth */) - 1) + + for raw in 1 ... storage &>> smallest { + let combination = Self(storage: Storage(raw &<< smallest)) + + // Filter out any combinations that include columns that are the same for all overloads + guard self.isSuperset(of: combination) else { continue } + + combinations.append(combination) + } + // The bits of larger and larger Int values won't be in order of number of bits set, so we sort them. + return combinations.sorted(by: { $0.count < $1.count }) + } +} diff --git a/Sources/DocCCommon/Mutex.swift b/Sources/DocCCommon/Mutex.swift new file mode 100644 index 0000000000..27453ce1db --- /dev/null +++ b/Sources/DocCCommon/Mutex.swift @@ -0,0 +1,46 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 +*/ + +#if os(macOS) || os(iOS) +import Darwin + +// This type is designed to have the same API surface as 'Synchronization.Mutex'. +// It's different from 'SwiftDocC.Synchronized' which requires that the wrapped value is `Copyable`. +// +// When we can require macOS 15.0 we can remove this custom type and use 'Synchronization.Mutex' directly on all platforms. +struct Mutex: ~Copyable, @unchecked Sendable { + private var value: UnsafeMutablePointer + private var lock: UnsafeMutablePointer + + init(_ initialValue: consuming sending Value) { + value = UnsafeMutablePointer.allocate(capacity: 1) + value.initialize(to: initialValue) + + lock = UnsafeMutablePointer.allocate(capacity: 1) + lock.initialize(to: os_unfair_lock()) + } + + deinit { + value.deallocate() + lock.deallocate() + } + + borrowing func withLock(_ body: (inout sending Value) throws(E) -> sending Result) throws(E) -> sending Result { + os_unfair_lock_lock(lock) + defer { os_unfair_lock_unlock(lock) } + + return try body(&value.pointee) + } +} +#else +import Synchronization + +typealias Mutex = Synchronization.Mutex +#endif diff --git a/Sources/DocCCommon/SourceLanguage.swift b/Sources/DocCCommon/SourceLanguage.swift new file mode 100644 index 0000000000..3de76d214a --- /dev/null +++ b/Sources/DocCCommon/SourceLanguage.swift @@ -0,0 +1,546 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021-2025 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 +*/ + +/// A programming language, for example "Swift" or "Objective-C". +public struct SourceLanguage: Hashable, Codable, Comparable, Sendable { + /// Using only an 8-bit value as an identifier technically limits a single DocC execution to 256 different languages. + /// This may sound like a significant limitation. However, in practice almost all content deals with either 1 or 2 languages. + /// There is some known content with 3 languages but beyond that 4 or 5 or more languages is increasingly less common/realistic. + /// + /// Thus, in practice it's deemed unrealistic that any content would ever represent symbols from 256 different programming languages. + /// Note that this limitation only applies to the languages within a single DocC execution and not globally. + /// Two different DocC executions can each represent 200+ different languages, resulting in a total of 400+ languages together. + /// + /// When DocC works with programming languages it's very common to work with a set of languages. + /// For example, a set can represent a page's list of supported language or it can represent a filter of common languages between to pages. + /// Because each DocC execution only involves very few unique languages in practice, + /// having a very small private identifier type allows DocC to pack all the languages it realistically needs into a small (inlineable) value. + fileprivate var _id: UInt8 // this is fileprivate so that SmallSourceLanguageSet (below) can access it +} + +/// The private type that holds the information for each source language +private struct _SourceLanguageInformation: Equatable { + var name: String + var id: String + var idAliases: [String] = [] + var linkDisambiguationID: String + + init(name: String, id: String, idAliases: [String] = [], linkDisambiguationID: String? = nil) { + self.name = name + self.id = id + self.idAliases = idAliases + self.linkDisambiguationID = linkDisambiguationID ?? id + } +} + +// MARK: Known Languages + +private let _knownLanguages = [ + // NOTE: The known languages have identifiers that is also their sort order when there are no unknown languages + + // Swift + _SourceLanguageInformation(name: "Swift", id: "swift"), + + // Miscellaneous data, that's not a programming language. + _SourceLanguageInformation(name: "Data", id: "data"), + + // JavaScript or another language that conforms to the ECMAScript specification. + _SourceLanguageInformation(name: "JavaScript", id: "javascript"), + + // The Metal programming language. + _SourceLanguageInformation(name: "Metal", id: "metal"), + + // Objective-C, C, and C++ + _SourceLanguageInformation( + name: "Objective-C", + id: "occ", + idAliases: [ + "objective-c", + "objc", + "c", // FIXME: DocC should display C as its own language (https://github.com/swiftlang/swift-docc/issues/169). + "c++", // FIXME: DocC should display C++ and Objective-C++ as their own languages (https://github.com/swiftlang/swift-docc/issues/767) + "objective-c++", + "objc++", + "occ++", + ], + linkDisambiguationID: "c" + ), +] + +private extension SourceLanguage { + private var _isKnownLanguage: Bool { + Self._isKnownLanguageID(_id) + } + private static func _isKnownLanguageID(_ id: UInt8) -> Bool { + id < _numberOfKnownLanguages + } + + private static let _numberOfKnownLanguages = UInt8(_knownLanguages.count) + private static let _maximumNumberOfUnknownLanguages: UInt8 = .max - _numberOfKnownLanguages +} + +// Public accessors for known languages +public extension SourceLanguage { + // NOTE: The known languages have identifiers that is also their sort order when there are no unknown languages + + /// The Swift programming language. + static let swift = SourceLanguage(_id: 0) + + /// Miscellaneous data, that's not a programming language. + /// + /// For example, use this to represent JSON or XML content. + static let data = SourceLanguage(_id: 1) + /// The JavaScript programming language or another language that conforms to the ECMAScript specification. + static let javaScript = SourceLanguage(_id: 2) + /// The Metal programming language. + static let metal = SourceLanguage(_id: 3) + + /// The Objective-C programming language. + static let objectiveC = SourceLanguage(_id: 4) + + /// The list of programming languages that are known to DocC. + static let knownLanguages: [SourceLanguage] = [.swift, .objectiveC, .javaScript, .data, .metal] +} + +private let _unknownLanguages = Mutex([_SourceLanguageInformation]()) + +// MARK: Language properties + +private extension SourceLanguage { + private func _accessInfo() -> _SourceLanguageInformation { + Self._accessInfo(id: _id) + } + + private static func _accessInfo(id: UInt8) -> _SourceLanguageInformation { + let (unknownIndex, isKnownLanguage) = id.subtractingReportingOverflow(SourceLanguage._numberOfKnownLanguages) + return if isKnownLanguage { + _knownLanguages[Int(id)] + } else { + _unknownLanguages.withLock { $0[Int(unknownIndex)] } + } + } + + private func _accessInfo(withUnlockedUnknownLanguages unknownLanguages: borrowing [_SourceLanguageInformation]) -> _SourceLanguageInformation { + let (unknownIndex, isKnownLanguage) = _id.subtractingReportingOverflow(SourceLanguage._numberOfKnownLanguages) + return if isKnownLanguage { + _knownLanguages[Int(_id)] + } else { + unknownLanguages[Int(unknownIndex)] + } + } + + private mutating func _addOrFindExisting(unknownLanguage: _SourceLanguageInformation, withUnlockedUnknownLanguages unknownLanguages: inout [_SourceLanguageInformation]) { + self._id = Self._addingOrFindingExisting(unknownLanguageInfo: unknownLanguage, withUnlockedUnknownLanguages: &unknownLanguages) + } + + private static func _addingOrFindingExisting(unknownLanguageInfo: _SourceLanguageInformation, withUnlockedUnknownLanguages unknownLanguages: inout [_SourceLanguageInformation]) -> UInt8 { + if let existingIndex = unknownLanguages.firstIndex(of: unknownLanguageInfo) { + return _languageID(unknownLanguageIndex: existingIndex) + } else { + unknownLanguages.append(unknownLanguageInfo) + return _languageID(unknownLanguageIndex: unknownLanguages.count - 1) + } + } + + private static func _languageID(unknownLanguageIndex: Int) -> UInt8 { + precondition(unknownLanguageIndex < _maximumNumberOfUnknownLanguages, """ + Unexpectedly created more than 256 different programming languages in a single DocC execution. \ + This is considered highly unlikely in real content and is possibly caused by some programming bug that is frequently modifying existing source languages. + """) + return _numberOfKnownLanguages + UInt8(clamping: unknownLanguageIndex) + } +} + +// Public accessors for each language property +public extension SourceLanguage { + /// The display name of the programming language. + var name: String { + get { _accessInfo().name } + @available(*, deprecated, message: "Create a new source language using 'init(name:id:idAliases:linkDisambiguationID:)' instead. This deprecated API will be removed after 6.4 is released.") + set { + // Modifying a language in any way create a new entry. This is generally discouraged because it easily creates a situation where language ID strings aren't globally unique anymore + _unknownLanguages.withLock { unknownLanguages in + var copy = _accessInfo(withUnlockedUnknownLanguages: unknownLanguages) + copy.name = newValue + _addOrFindExisting(unknownLanguage: copy, withUnlockedUnknownLanguages: &unknownLanguages) + } + } + } + /// A globally unique identifier for the language. + var id: String { + get { _accessInfo().id } + @available(*, deprecated, message: "Create a new source language using 'init(name:id:idAliases:linkDisambiguationID:)' instead. This deprecated API will be removed after 6.4 is released.") + set { + // Modifying a language in any way create a new entry. This is generally discouraged because it easily creates a situation where language ID strings aren't globally unique anymore + _unknownLanguages.withLock { unknownLanguages in + var copy = _accessInfo(withUnlockedUnknownLanguages: unknownLanguages) + copy.id = newValue + _addOrFindExisting(unknownLanguage: copy, withUnlockedUnknownLanguages: &unknownLanguages) + } + } + } + /// Aliases for the language's identifier. + var idAliases: [String] { + get { _accessInfo().idAliases } + @available(*, deprecated, message: "Create a new source language using 'init(name:id:idAliases:linkDisambiguationID:)' instead. This deprecated API will be removed after 6.4 is released.") + set { + // Modifying a language in any way create a new entry. This is generally discouraged because it easily creates a situation where language ID strings aren't globally unique anymore + _unknownLanguages.withLock { unknownLanguages in + var copy = _accessInfo(withUnlockedUnknownLanguages: unknownLanguages) + copy.idAliases = newValue + _addOrFindExisting(unknownLanguage: copy, withUnlockedUnknownLanguages: &unknownLanguages) + } + } + } + /// The identifier to use for link disambiguation purposes. + var linkDisambiguationID: String { + get { _accessInfo().linkDisambiguationID } + @available(*, deprecated, message: "Create a new source language using 'init(name:id:idAliases:linkDisambiguationID:)' instead. This deprecated API will be removed after 6.4 is released.") + set { + // Modifying a language in any way create a new entry. This is generally discouraged because it easily creates a situation where language ID strings aren't globally unique anymore + _unknownLanguages.withLock { unknownLanguages in + var copy = _accessInfo(withUnlockedUnknownLanguages: unknownLanguages) + copy.linkDisambiguationID = newValue + _addOrFindExisting(unknownLanguage: copy, withUnlockedUnknownLanguages: &unknownLanguages) + } + } + } +} + +// MARK: Creating languages + +// Public initializers +public extension SourceLanguage { + /// Creates a new language with a given name and identifier. + /// - Parameters: + /// - name: The display name of the programming language. + /// - id: A globally unique identifier for the language. + /// - idAliases: Aliases for the language's identifier. + /// - linkDisambiguationID: The identifier to use for link disambiguation purposes. + init(name: String, id: String, idAliases: [String] = [], linkDisambiguationID: String? = nil) { + let newInfo = _SourceLanguageInformation(name: name, id: id, idAliases: idAliases, linkDisambiguationID: linkDisambiguationID) + + // Before creating a new language, check if there is one that matches all the information + if let existing = Self._knownLanguage(withIdentifier: id), newInfo == Self._accessInfo(id: existing._id) { + self = existing + return + } + + self._id = _unknownLanguages.withLock { unknownLanguages in + Self._addingOrFindingExisting( + unknownLanguageInfo: .init(name: name, id: id, idAliases: idAliases, linkDisambiguationID: linkDisambiguationID), + withUnlockedUnknownLanguages: &unknownLanguages + ) + } + } + + /// Finds the programming language that matches a given identifier, or creates a new one if it finds no existing language. + /// - Parameter id: The identifier of the programming language. + init(id: String) { + if let known = Self._knownLanguage(withIdentifier: id) { + self = known + } else { + self._id = _unknownLanguages.withLock { unknownLanguages in + Self._addingOrFindingExisting(unknownLanguageInfo: .init(name: id, id: id), withUnlockedUnknownLanguages: &unknownLanguages) + } + } + } + + /// Finds the programming language that matches a given display name, or creates a new one if it finds no existing language. + /// + /// - Parameter name: The display name of the programming language. + init(name: String) { + let id = name.lowercased() + if let knownLanguage = Self.knownLanguage(withName: name) ?? Self._knownLanguage(withIdentifier: id) { + self = knownLanguage + } else { + self._id = _unknownLanguages.withLock { unknownLanguages in + Self._addingOrFindingExisting(unknownLanguageInfo: .init(name: name, id: id), withUnlockedUnknownLanguages: &unknownLanguages) + } + } + } + + /// Finds the programming language that matches a given display name. + /// + /// If the language name doesn't match any known language, this initializer returns `nil`. + /// + /// - Parameter knownLanguageName: The display name of the programming language. + init?(knownLanguageName: String) { + if let knownLanguage = Self.knownLanguage(withName: knownLanguageName) { + self = knownLanguage + } else { + return nil + } + } + + /// Finds the programming language that matches a given identifier. + /// + /// If the language identifier doesn't match any known language, this initializer returns `nil`. + /// + /// - Parameter knownLanguageIdentifier: The identifier name of the programming language. + init?(knownLanguageIdentifier: String) { + if let knownLanguage = Self._knownLanguage(withIdentifier: knownLanguageIdentifier) { + self = knownLanguage + } else { + return nil + } + } + + private static func knownLanguage(withName name: String) -> SourceLanguage? { + switch name.lowercased() { + case "swift": .swift + case "objective-c": .objectiveC + case "javascript": .javaScript + case "data": .data + case "metal": .metal + default: nil + } + } + + private static func _knownLanguage(withIdentifier id: String) -> SourceLanguage? { + switch id.lowercased() { + case "swift": .swift + case "occ", "objc", "objective-c", + "c", // FIXME: DocC should display C as its own language (https://github.com/swiftlang/swift-docc/issues/169). + "occ++", "objc++", "objective-c++", "c++": // FIXME: DocC should display C++ and Objective-C++ as their own languages (https://github.com/swiftlang/swift-docc/issues/767) + .objectiveC + case "javascript": .javaScript + case "data": .data + case "metal": .metal + default: nil + } + } +} + +// MARK: Conformances + +extension SourceLanguage { + private enum CodingKeys: CodingKey { + case name, id, idAliases, linkDisambiguationID + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let name = try container.decode(String.self, forKey: .name) + let id = try container.decode(String.self, forKey: .id) + let idAliases = try container.decodeIfPresent([String].self, forKey: .idAliases) ?? [] + let linkDisambiguationID = try container.decodeIfPresent(String.self, forKey: .linkDisambiguationID) + + self.init(name: name, id: id, idAliases: idAliases, linkDisambiguationID: linkDisambiguationID) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let info = _accessInfo() + + try container.encode(info.name, forKey: .name) + try container.encode(info.id, forKey: .id) + if !info.idAliases.isEmpty { + try container.encode(info.idAliases, forKey: .idAliases) + } + try container.encode(info.linkDisambiguationID, forKey: .linkDisambiguationID) + } +} + +public extension SourceLanguage { + static func == (lhs: SourceLanguage, rhs: SourceLanguage) -> Bool { + lhs._id == rhs._id || lhs._accessInfo() == rhs._accessInfo() + } +} + +public extension SourceLanguage { + static func < (lhs: SourceLanguage, rhs: SourceLanguage) -> Bool { + if lhs._isKnownLanguage, rhs._isKnownLanguage { + // If both languages are known, their `_id` is also their sort order + lhs._id < rhs._id + } + + // Sort Swift before other languages. + else if lhs == .swift { + true + } else if rhs == .swift { + false + } else { + // Otherwise, sort by ID (a string) for a stable order. + lhs.id < rhs.id + } + } +} + +// MARK: SourceLanguage Set + +package struct SmallSourceLanguageSet: Sendable, Hashable, SetAlgebra, ExpressibleByArrayLiteral, Sequence, Collection { + // There are a few different valid ways that we could implement this, each with their own tradeoffs. + // + // The current implementation uses a single fixed size 64-value bit set to store the private `SourceLanguage._id` values. + // The primary benefit of this design is that it's easy to implement, very fast, and uses the same logic for both known and unknown source languages. + // The tradeoff is that it "only" supports 64 different programming languages at once and that it "only" supports the first 59 unknown/custom source languages that a single DocC build creates. + // This may sound like a significant limitation. However, in practice almost all content deals with either 1 or 2 languages. + // There is some known content with 3 languages but beyond that; as little as 4 or 5 or more languages is increasingly less common/realistic. + // A single project with >64 languages is considered so _extremely_ unlikely that it's considered an unrealistic hypothetical. + // + // Another way that we could implement this within 64 bits could be to store 8 separate UInt8 values. + // This would limit the numbers of source languages in a single set to 8 but would enable a project to use create 251 different unknown/custom source languages. + // This would make it a harder to implement the SetAlgebra, Sequence, and Collection conformances. + // Because `InlineArray` requires Swift 6.2, this design would need to use 8 separate properties or an 8 element tuple, making most operations _O(n)_ (where _n_ is <= 8). + // + // We could also combine the two designs above to use a smaller bit set for some values, and a series of separate UInt8 values. + // This would enable a project to use create 251 different unknown/custom source languages at the cost of supporting fewer simultaneous values in the set. + // This would also have a _greatly_ increased implementation complexity; because we would need both the bit-set-implementation and the separate-UInt8-values-implementation and we would need to dynamically switch between them throughout the entire implementation. + // Depending on the size of the bit set and the number of additional UInt8 values, we could achieve different balances between total number of supported values in the set and number of unknown/custom languages. + // For example, an 8-value bit set for known languages and 7 UInt8 properties for unknown languages would allow the set to contains 12 languages (the 5 known and 7 unknown). + // Alternatively, a 32-value bit set for both known and unknown languages and 4 additional UInt8 properties for unknown languages with a high `_id` could support up to 36 different values. + // That said, storing unknown languages in both the bit set and the additional UInt8 properties would have an _even_ greater implementation complexity. + // Because the very high implementation complexity of these various mixed-implementation designs, we shouldn't try to implement any of them until we know for certain that it's necessary. + // + // We _could_ use an enum to switch between an inline fixed size value and a dynamic resizable value. + // However, the 1 bit for the two enum cases would double the `stride` of the memory layout, resulting in 63 unused "wasted" bits. + + private var bitSet: _FixedSizeBitSet + private init(storage: _FixedSizeBitSet) { + self.bitSet = storage + } + + @inlinable + package init() { + bitSet = .init() + } + + // SetAlgebra + + package typealias Element = SourceLanguage + + @inlinable + package func contains(_ member: SourceLanguage) -> Bool { + bitSet.contains(Int(member._id)) + } + @inlinable + package func union(_ other: SmallSourceLanguageSet) -> SmallSourceLanguageSet { + Self(storage: bitSet.union(other.bitSet)) + } + @inlinable + package func intersection(_ other: SmallSourceLanguageSet) -> SmallSourceLanguageSet { + Self(storage: bitSet.intersection(other.bitSet)) + } + @inlinable + package func symmetricDifference(_ other: SmallSourceLanguageSet) -> SmallSourceLanguageSet { + Self(storage: bitSet.symmetricDifference(other.bitSet)) + } + @inlinable + @discardableResult + package mutating func insert(_ newMember: SourceLanguage) -> (inserted: Bool, memberAfterInsert: SourceLanguage) { + (bitSet.insert(Int(newMember._id)).inserted, newMember) + } + @inlinable + @discardableResult + package mutating func remove(_ member: SourceLanguage) -> SourceLanguage? { + bitSet.remove(Int(member._id)).map { SourceLanguage(_id: UInt8($0)) } + } + @inlinable + @discardableResult + package mutating func update(with newMember: SourceLanguage) -> SourceLanguage? { + bitSet.update(with: Int(newMember._id)).map { SourceLanguage(_id: UInt8($0)) } + } + @inlinable + package mutating func formUnion(_ other: SmallSourceLanguageSet) { + bitSet.formUnion(other.bitSet) + } + @inlinable + package mutating func formIntersection(_ other: SmallSourceLanguageSet) { + bitSet.formIntersection(other.bitSet) + } + @inlinable + package mutating func formSymmetricDifference(_ other: SmallSourceLanguageSet) { + bitSet.formSymmetricDifference(other.bitSet) + } + + // ExpressibleByArrayLiteral + + @inlinable + package init(arrayLiteral elements: SourceLanguage...) { + bitSet = .init() + for language in elements { + bitSet.insert(Int(language._id)) + } + } + + // Sequence + + @inlinable + package func makeIterator() -> some IteratorProtocol { + _Iterator(wrapped: bitSet.makeIterator()) + } + + private struct _Iterator>: IteratorProtocol { + typealias Element = SourceLanguage + + fileprivate var wrapped: Wrapped + + @inlinable + mutating func next() -> SourceLanguage? { + wrapped.next().map { SourceLanguage(_id: UInt8($0) )} + } + } + + // Collection + + package typealias Index = _FixedSizeBitSet.Index + @inlinable + package var startIndex: Index { + bitSet.startIndex + } + @inlinable + package var endIndex: Index { + bitSet.endIndex + } + @inlinable + package subscript(position: Index) -> SourceLanguage { + SourceLanguage(_id: UInt8(bitSet[position])) + } + @inlinable + package func index(after currentIndex: Index) -> Index { + bitSet.index(after: currentIndex) + } + + private var containsUnknownLanguages: Bool { + // There are 5 known languages, representing the trailing 5 bits of the bit set + let unknownLanguagesMask: UInt64 = 0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11100000 + return (bitSet.storage & unknownLanguagesMask) != 0 + } + + package func min() -> SourceLanguage? { + guard containsUnknownLanguages else { + // Known languages are trivially sortable by their `_id` + return bitSet.min().map { SourceLanguage(_id: UInt8($0) )} + } + + return Array(bitSet).map { SourceLanguage(_id: UInt8($0) )}.min() + } + + package func sorted() -> [SourceLanguage] { + guard containsUnknownLanguages else { + // Known languages are trivially sortable by their `_id` + return bitSet.sorted().map { SourceLanguage(_id: UInt8($0) )} + } + + return Array(bitSet).map { SourceLanguage(_id: UInt8($0) )}.sorted() + } + + @inlinable + package var isEmpty: Bool { + bitSet.isEmpty + } + + @inlinable + package var count: Int { + bitSet.count + } +} diff --git a/Sources/SwiftDocC/CMakeLists.txt b/Sources/SwiftDocC/CMakeLists.txt new file mode 100644 index 0000000000..c5e6ef0e26 --- /dev/null +++ b/Sources/SwiftDocC/CMakeLists.txt @@ -0,0 +1,483 @@ +#[[ +This source file is part of the Swift open source project + +Copyright © 2014 - 2025 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 +#]] + +add_library(SwiftDocC + Benchmark/Benchmark.swift + Benchmark/BenchmarkResults.swift + Benchmark/Metrics.swift + Benchmark/Metrics/Duration.swift + Benchmark/Metrics/ExternalTopicsHash.swift + Benchmark/Metrics/OutputSize.swift + Benchmark/Metrics/PeakMemory.swift + Benchmark/Metrics/TopicAnchorHash.swift + Benchmark/Metrics/TopicGraphHash.swift + "Catalog Processing/GeneratedCurationWriter.swift" + Checker/Checker.swift + Checker/Checkers/AbstractContainsFormattedTextOnly.swift + Checker/Checkers/DuplicateTopicsSection.swift + Checker/Checkers/InvalidAdditionalTitle.swift + Checker/Checkers/InvalidCodeBlockOption.swift + Checker/Checkers/MissingAbstract.swift + Checker/Checkers/NonInclusiveLanguageChecker.swift + Checker/Checkers/NonOverviewHeadingChecker.swift + Checker/Checkers/SeeAlsoInTopicsHeadingChecker.swift + Converter/DocumentationContextConverter.swift + Converter/DocumentationNodeConverter.swift + Converter/RenderNode+Coding.swift + Converter/Rewriter/RemoveHierarchyTransformation.swift + Converter/Rewriter/RemoveUnusedReferencesTransformation.swift + Converter/Rewriter/RenderNodeTransformationComposition.swift + Converter/Rewriter/RenderNodeTransformationContext.swift + Converter/Rewriter/RenderNodeTransformer.swift + Converter/Rewriter/RenderNodeTransforming.swift + Converter/TopicRenderReferenceEncoder.swift + Coverage/DocumentationCoverageOptions.swift + DocumentationService/Convert/ConvertService+DataProvider.swift + DocumentationService/Convert/ConvertService.swift + "DocumentationService/Convert/Fallback Link Resolution/ConvertServiceFallbackResolver.swift" + "DocumentationService/Convert/Symbol Link Resolution/LinkCompletionTools.swift" + DocumentationService/DocumentationServer+createDefaultServer.swift + DocumentationService/ExternalReferenceResolverServiceClient.swift + DocumentationService/Models/DocumentationServer+Message.swift + DocumentationService/Models/DocumentationServer+MessageType.swift + DocumentationService/Models/DocumentationServer.swift + DocumentationService/Models/DocumentationServerError.swift + DocumentationService/Models/DocumentationServerProtocol.swift + DocumentationService/Models/DocumentationService.swift + DocumentationService/Models/Services/Convert/ConvertRequest.swift + DocumentationService/Models/Services/Convert/ConvertRequestContextWrapper.swift + DocumentationService/Models/Services/Convert/ConvertResponse.swift + DocumentationService/Models/Services/Convert/ConvertServiceError.swift + Indexing/Indexable.swift + Indexing/IndexingError.swift + Indexing/IndexingRecord.swift + Indexing/Navigator/AvailabilityIndex+Ext.swift + Indexing/Navigator/AvailabilityIndex.swift + Indexing/Navigator/NavigatorIndex.swift + Indexing/Navigator/NavigatorItem.swift + Indexing/Navigator/NavigatorTree.swift + Indexing/Navigator/RenderNode+NavigatorIndex.swift + Indexing/RenderBlockContent+TextIndexing.swift + Indexing/RenderIndexJSON/RenderIndex.swift + Indexing/RenderInlineContent+TextIndexing.swift + Indexing/RenderNode+Indexable.swift + Indexing/RenderNode+Relationships.swift + Indexing/RenderSection+TextIndexing.swift + Indexing/TutorialSectionsRenderSection+Indexable.swift + "Infrastructure/Bundle Assets/BundleData.swift" + "Infrastructure/Bundle Assets/DataAssetManager.swift" + "Infrastructure/Bundle Assets/SVGIDExtractor.swift" + "Infrastructure/Communication/Code colors/CodeColors.swift" + "Infrastructure/Communication/Code colors/CodeColorsPreferenceKey.swift" + "Infrastructure/Communication/Code colors/SRGBColor.swift" + Infrastructure/Communication/CommunicationBridge.swift + Infrastructure/Communication/Foundation/AnyCodable.swift + Infrastructure/Communication/Foundation/JSON.swift + Infrastructure/Communication/Message+Codable.swift + Infrastructure/Communication/Message.swift + Infrastructure/Communication/MessageType.swift + Infrastructure/Communication/WebKitCommunicationBridge.swift + Infrastructure/ContentCache.swift + Infrastructure/Context/DocumentationContext+Configuration.swift + Infrastructure/ConvertActionConverter.swift + Infrastructure/ConvertOutputConsumer.swift + Infrastructure/CoverageDataEntry.swift + Infrastructure/Diagnostics/ANSIAnnotation.swift + Infrastructure/Diagnostics/Diagnostic.swift + Infrastructure/Diagnostics/DiagnosticConsoleWriter.swift + Infrastructure/Diagnostics/DiagnosticConsumer.swift + Infrastructure/Diagnostics/DiagnosticEngine.swift + Infrastructure/Diagnostics/DiagnosticFile.swift + Infrastructure/Diagnostics/DiagnosticFileWriter.swift + Infrastructure/Diagnostics/DiagnosticFormattingOptions.swift + Infrastructure/Diagnostics/DiagnosticNote.swift + Infrastructure/Diagnostics/DiagnosticSeverity.swift + Infrastructure/Diagnostics/ParseDirectiveArguments.swift + Infrastructure/Diagnostics/Problem.swift + Infrastructure/Diagnostics/Replacement.swift + Infrastructure/Diagnostics/Solution.swift + Infrastructure/Diagnostics/SourcePosition.swift + Infrastructure/Diagnostics/TerminalHelper.swift + Infrastructure/DocumentationBundle+Identifier.swift + Infrastructure/DocumentationBundle.swift + Infrastructure/DocumentationBundleFileTypes.swift + Infrastructure/DocumentationContext+Breadcrumbs.swift + Infrastructure/DocumentationContext.swift + Infrastructure/DocumentationCurator.swift + Infrastructure/Extensions/KindIdentifier+Curation.swift + Infrastructure/Extensions/SymbolGraph.Symbol.Location+URL.swift + "Infrastructure/External Data/ExternalDocumentationSource.swift" + "Infrastructure/External Data/ExternalMetadata.swift" + "Infrastructure/External Data/GlobalExternalSymbolResolver.swift" + "Infrastructure/External Data/OutOfProcessReferenceResolver+Communication.swift" + "Infrastructure/External Data/OutOfProcessReferenceResolver+DeprecatedCommunication.swift" + "Infrastructure/External Data/OutOfProcessReferenceResolver.swift" + "Infrastructure/Input Discovery/DataProvider.swift" + "Infrastructure/Input Discovery/DocumentationInputsProvider.swift" + "Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift" + "Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift" + "Infrastructure/Link Resolution/LinkResolver.swift" + "Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift" + "Infrastructure/Link Resolution/PathHierarchy+Dump.swift" + "Infrastructure/Link Resolution/PathHierarchy+Error.swift" + "Infrastructure/Link Resolution/PathHierarchy+Find.swift" + "Infrastructure/Link Resolution/PathHierarchy+PathComponent.swift" + "Infrastructure/Link Resolution/PathHierarchy+Serialization.swift" + "Infrastructure/Link Resolution/PathHierarchy+TypeSignature.swift" + "Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift" + "Infrastructure/Link Resolution/PathHierarchy.swift" + "Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver+Breadcrumbs.swift" + "Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver+Overloads.swift" + "Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift" + "Infrastructure/Link Resolution/SnippetResolver.swift" + Infrastructure/NodeURLGenerator.swift + "Infrastructure/Symbol Graph/AccessControl+Comparable.swift" + "Infrastructure/Symbol Graph/ExtendedTypeFormatExtension.swift" + "Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift" + "Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift" + "Infrastructure/Symbol Graph/ResolvedTopicReference+Symbol.swift" + "Infrastructure/Symbol Graph/SymbolGraphConcurrentDecoder.swift" + "Infrastructure/Symbol Graph/SymbolGraphLoader.swift" + "Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift" + "Infrastructure/Symbol Graph/SymbolReference.swift" + "Infrastructure/Symbol Graph/UnresolvedTopicReference+Symbol.swift" + "Infrastructure/Topic Graph/AutomaticCuration.swift" + "Infrastructure/Topic Graph/TopicGraph.swift" + Infrastructure/Workspace/DefaultAvailability.swift + Infrastructure/Workspace/DocumentationBundle+Info.swift + Infrastructure/Workspace/DocumentationWorkspaceDataProvider.swift + Infrastructure/Workspace/FeatureFlags+Info.swift + LinkTargets/LinkDestinationSummary.swift + Model/AnchorSection.swift + Model/AvailabilityParser.swift + Model/BuildMetadata.swift + Model/DocumentationMarkup.swift + Model/DocumentationNode.swift + Model/Identifier.swift + Model/Kind.swift + Model/Markup+parsing.swift + Model/Name.swift + Model/ParametersAndReturnValidator.swift + Model/Rendering/Content/Extensions/RenderTermLists.swift + Model/Rendering/Content/RenderBlockContent+Capitalization.swift + Model/Rendering/Content/RenderBlockContent.swift + Model/Rendering/Content/RenderContentMetadata.swift + Model/Rendering/Content/RenderInlineContent.swift + Model/Rendering/Diffing/AnyRenderReference.swift + Model/Rendering/Diffing/AnyRenderSection.swift + Model/Rendering/Diffing/DifferenceBuilder.swift + Model/Rendering/Diffing/Differences.swift + Model/Rendering/Diffing/RenderNode+Diffable.swift + Model/Rendering/DocumentationContentRenderer.swift + Model/Rendering/LinkTitleResolver.swift + "Model/Rendering/Navigation Tree/RenderHierarchy.swift" + "Model/Rendering/Navigation Tree/RenderHierarchyChapter.swift" + "Model/Rendering/Navigation Tree/RenderHierarchyLandmark.swift" + "Model/Rendering/Navigation Tree/RenderHierarchyTranslator.swift" + "Model/Rendering/Navigation Tree/RenderHierarchyTutorial.swift" + "Model/Rendering/Navigation Tree/RenderReferenceHierarchy.swift" + "Model/Rendering/Navigation Tree/RenderTutorialsHierarchy.swift" + Model/Rendering/PresentationURLGenerator.swift + Model/Rendering/References/AssetReferences.swift + Model/Rendering/References/FileReference.swift + Model/Rendering/References/ImageReference.swift + Model/Rendering/References/LinkReference.swift + Model/Rendering/References/MediaReference.swift + Model/Rendering/References/RenderReference.swift + Model/Rendering/References/TopicColor.swift + Model/Rendering/References/TopicImage.swift + Model/Rendering/References/TopicRenderReference.swift + Model/Rendering/References/UnresolvedReference.swift + Model/Rendering/References/VideoReference.swift + Model/Rendering/RenderContentCompiler.swift + Model/Rendering/RenderContentConvertible.swift + Model/Rendering/RenderContext.swift + Model/Rendering/RenderNode.Tag.swift + Model/Rendering/RenderNode.swift + Model/Rendering/RenderNodeTranslator.swift + Model/Rendering/RenderNodeVariant.swift + Model/Rendering/RenderNode/AnyMetadata.swift + Model/Rendering/RenderNode/CodableContentSection.swift + Model/Rendering/RenderNode/CodableRenderReference.swift + Model/Rendering/RenderNode/CodableRenderSection.swift + Model/Rendering/RenderNode/RenderMetadata.swift + Model/Rendering/RenderNode/RenderNode+Codable.swift + Model/Rendering/RenderReferenceStore.swift + Model/Rendering/RenderSection.swift + Model/Rendering/RenderSectionTranslator/AttributesSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/DictionaryKeysSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/DiscussionSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/HTTPBodySectionTranslator.swift + Model/Rendering/RenderSectionTranslator/HTTPEndpointSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/HTTPParametersSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/HTTPResponsesSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/MentionsSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/ParametersSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/PlistDetailsSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/PossibleValuesSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/RenderSectionTranslator.swift + Model/Rendering/RenderSectionTranslator/ReturnsSectionTranslator.swift + Model/Rendering/SemanticVersion.swift + Model/Rendering/Symbol/AttributesRenderSection.swift + Model/Rendering/Symbol/AvailabilityRenderMetadataItem.swift + Model/Rendering/Symbol/AvailabilitySortOrder.swift + Model/Rendering/Symbol/ConformanceSection.swift + Model/Rendering/Symbol/ContentRenderSection.swift + Model/Rendering/Symbol/DeclarationRenderSection+SymbolGraph.swift + Model/Rendering/Symbol/DeclarationsRenderSection.swift + Model/Rendering/Symbol/DiffAvailability.swift + Model/Rendering/Symbol/MentionsRenderSection.swift + Model/Rendering/Symbol/ParameterRenderSection.swift + Model/Rendering/Symbol/PossibleValuesRenderSection.swift + Model/Rendering/Symbol/PropertiesRenderSection.swift + Model/Rendering/Symbol/PropertyListDetailsRenderSection.swift + Model/Rendering/Symbol/RESTBodyRenderSection.swift + Model/Rendering/Symbol/RESTEndpointRenderSection.swift + Model/Rendering/Symbol/RESTExampleRenderSection.swift + Model/Rendering/Symbol/RESTParametersRenderSection.swift + Model/Rendering/Symbol/RESTResponseRenderSection.swift + Model/Rendering/Symbol/RelationshipsRenderSection.swift + Model/Rendering/Symbol/SampleDownloadSection.swift + Model/Rendering/Symbol/TaskGroupRenderSection.swift + Model/Rendering/TopicsSectionStyle.swift + "Model/Rendering/Tutorial Article/TutorialArticleSection.swift" + Model/Rendering/Tutorial/LineHighlighter.swift + Model/Rendering/Tutorial/References/DownloadReference.swift + Model/Rendering/Tutorial/References/XcodeRequirementReference.swift + Model/Rendering/Tutorial/Sections/IntroRenderSection.swift + Model/Rendering/Tutorial/Sections/TutorialAssessmentsRenderSection.swift + Model/Rendering/Tutorial/Sections/TutorialSectionsRenderSection.swift + "Model/Rendering/Tutorials Overview/Resources/RenderTile.swift" + "Model/Rendering/Tutorials Overview/Sections/CallToActionSection.swift" + "Model/Rendering/Tutorials Overview/Sections/ContentAndMediaGroupSection.swift" + "Model/Rendering/Tutorials Overview/Sections/ContentAndMediaSection.swift" + "Model/Rendering/Tutorials Overview/Sections/ResourcesRenderSection.swift" + "Model/Rendering/Tutorials Overview/Sections/VolumeRenderSection.swift" + Model/Rendering/Variants/JSONPatchApplier.swift + Model/Rendering/Variants/JSONPatchOperation.swift + Model/Rendering/Variants/JSONPointer.swift + Model/Rendering/Variants/PatchOperation.swift + Model/Rendering/Variants/RenderNodeVariantOverridesApplier.swift + Model/Rendering/Variants/VariantCollection+Coding.swift + Model/Rendering/Variants/VariantCollection+Symbol.swift + Model/Rendering/Variants/VariantCollection+Variant.swift + Model/Rendering/Variants/VariantCollection.swift + Model/Rendering/Variants/VariantContainer.swift + Model/Rendering/Variants/VariantOverride.swift + Model/Rendering/Variants/VariantOverrides.swift + Model/Rendering/Variants/VariantPatchOperation.swift + Model/Section/Section.swift + Model/Section/Sections/Abstract.swift + Model/Section/Sections/AutomaticTaskGroupSection.swift + Model/Section/Sections/DefaultImplementations.swift + Model/Section/Sections/DeprecatedSection.swift + Model/Section/Sections/DictionaryKeysSection.swift + Model/Section/Sections/Discussion.swift + Model/Section/Sections/GroupedSection.swift + Model/Section/Sections/HTTPBodySection.swift + Model/Section/Sections/HTTPEndpointSection.swift + Model/Section/Sections/HTTPParametersSection.swift + Model/Section/Sections/HTTPResponsesSection.swift + Model/Section/Sections/ParametersSection.swift + Model/Section/Sections/PropertyListPossibleValuesSection.swift + Model/Section/Sections/Relationships.swift + Model/Section/Sections/ReturnsSection.swift + Model/Section/Sections/SeeAlso.swift + Model/Section/Sections/Topics.swift + Model/Semantics/DictionaryKey.swift + Model/Semantics/HTTPBody.swift + Model/Semantics/HTTPParameter.swift + Model/Semantics/HTTPResponse.swift + Model/Semantics/LegacyTag.swift + Model/Semantics/Parameter.swift + Model/Semantics/Return.swift + Model/Semantics/Throw.swift + Model/TaskGroup.swift + Semantics/Abstracted.swift + Semantics/Article/Article.swift + Semantics/Article/ArticleSymbolMentions.swift + Semantics/Article/MarkupConvertible.swift + Semantics/Comment.swift + Semantics/ContentAndMedia.swift + Semantics/DirectiveConvertable.swift + Semantics/DirectiveInfrastructure/AutomaticDirectiveConvertible.swift + Semantics/DirectiveInfrastructure/ChildDirectiveWrapper.swift + Semantics/DirectiveInfrastructure/ChildMarkdownWrapper.swift + Semantics/DirectiveInfrastructure/DirectiveArgumentValueConvertible.swift + Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift + Semantics/DirectiveInfrastructure/DirectiveIndex.swift + Semantics/DirectiveInfrastructure/DirectiveMirror.swift + Semantics/DirectiveInfrastructure/MarkupContaining.swift + Semantics/DirectiveParser.swift + Semantics/ExternalLinks/ExternalMarkupReferenceWalker.swift + Semantics/ExternalLinks/ExternalReferenceWalker.swift + "Semantics/General Purpose Analyses/DeprecatedArgument.swift" + "Semantics/General Purpose Analyses/Extract.swift" + "Semantics/General Purpose Analyses/HasArgumentOfType.swift" + "Semantics/General Purpose Analyses/HasAtLeastOne.swift" + "Semantics/General Purpose Analyses/HasAtMostOne.swift" + "Semantics/General Purpose Analyses/HasContent.swift" + "Semantics/General Purpose Analyses/HasExactlyOne.swift" + "Semantics/General Purpose Analyses/HasOnlyKnownArguments.swift" + "Semantics/General Purpose Analyses/HasOnlyKnownDirectives.swift" + "Semantics/General Purpose Analyses/HasOnlySequentialHeadings.swift" + Semantics/Graph/SymbolKind.Swift.swift + Semantics/Intro.swift + Semantics/Landmark.swift + Semantics/Layout.swift + Semantics/MarkupContainer.swift + Semantics/MarkupReferenceResolver.swift + Semantics/Media/ImageMedia.swift + Semantics/Media/Media.swift + Semantics/Media/VideoMedia.swift + Semantics/Metadata/AlternateRepresentation.swift + Semantics/Metadata/Availability.swift + Semantics/Metadata/CallToAction.swift + Semantics/Metadata/CustomMetadata.swift + Semantics/Metadata/DisplayName.swift + Semantics/Metadata/DocumentationExtension.swift + Semantics/Metadata/Metadata.swift + Semantics/Metadata/PageColor.swift + Semantics/Metadata/PageImage.swift + Semantics/Metadata/PageKind.swift + Semantics/Metadata/SupportedLanguage.swift + Semantics/Metadata/TechnologyRoot.swift + Semantics/Metadata/TitleHeading.swift + Semantics/Options/AutomaticArticleSubheading.swift + Semantics/Options/AutomaticSeeAlso.swift + Semantics/Options/AutomaticTitleHeading.swift + Semantics/Options/Options.swift + Semantics/Options/TopicsVisualStyle.swift + Semantics/Redirect.swift + Semantics/Redirected.swift + Semantics/ReferenceResolver.swift + Semantics/Reference/Links.swift + Semantics/Reference/Row.swift + Semantics/Reference/Small.swift + Semantics/Reference/TabNavigator.swift + Semantics/Semantic.swift + Semantics/SemanticAnalyzer.swift + Semantics/Snippets/Snippet.swift + Semantics/Symbol/DeprecationSummary.swift + Semantics/Symbol/DocumentationDataVariants+SymbolGraphSymbol.swift + Semantics/Symbol/DocumentationDataVariants.swift + Semantics/Symbol/PlatformName.swift + Semantics/Symbol/Relationship.swift + Semantics/Symbol/Symbol.swift + Semantics/Symbol/UnifiedSymbol+Extensions.swift + Semantics/Technology/Resources/Resources.swift + Semantics/Technology/Resources/Tile.swift + Semantics/Technology/TutorialTableOfContents.swift + Semantics/Technology/Volume/Chapter/Chapter.swift + Semantics/Technology/Volume/Chapter/TutorialReference.swift + Semantics/Technology/Volume/Volume.swift + Semantics/Timed.swift + Semantics/Titled.swift + Semantics/TutorialArticle/Stack.swift + Semantics/TutorialArticle/TutorialArticle.swift + Semantics/Tutorial/Assessments/Assessments.swift + "Semantics/Tutorial/Assessments/Multiple Choice/Choice/Choice.swift" + "Semantics/Tutorial/Assessments/Multiple Choice/Choice/Justification.swift" + "Semantics/Tutorial/Assessments/Multiple Choice/MultipleChoice.swift" + Semantics/Tutorial/Tasks/Steps/Code.swift + Semantics/Tutorial/Tasks/Steps/Step.swift + Semantics/Tutorial/Tasks/Steps/Steps.swift + Semantics/Tutorial/Tasks/TutorialSection.swift + Semantics/Tutorial/Tutorial.swift + Semantics/Tutorial/XcodeRequirement.swift + Semantics/Visitor/SemanticVisitor.swift + Semantics/Walker/SemanticWalker.swift + Semantics/Walker/Walkers/SemanticTreeDumper.swift + Servers/DocumentationSchemeHandler.swift + Servers/FileServer.swift + SourceRepository/SourceRepository.swift + Utility/Checksum.swift + Utility/Collection+ConcurrentPerform.swift + Utility/CollectionChanges.swift + Utility/CommonTypeExports.swift + Utility/DataStructures/BidirectionalMap.swift + Utility/DataStructures/GroupedSequence.swift + Utility/DispatchGroup+Async.swift + Utility/Errors/DescribedError.swift + Utility/Errors/ErrorWithProblems.swift + Utility/ExternalIdentifier.swift + Utility/FeatureFlags.swift + Utility/FileManagerProtocol+FilesSequence.swift + Utility/FileManagerProtocol.swift + Utility/FoundationExtensions/Array+baseType.swift + Utility/FoundationExtensions/AutoreleasepoolShim.swift + Utility/FoundationExtensions/CharacterSet.swift + Utility/FoundationExtensions/Collection+indexed.swift + Utility/FoundationExtensions/Dictionary+TypedValues.swift + Utility/FoundationExtensions/NoOpSignposterShim.swift + Utility/FoundationExtensions/Optional+baseType.swift + Utility/FoundationExtensions/PlainTextShim.swift + Utility/FoundationExtensions/RangeReplaceableCollection+Group.swift + Utility/FoundationExtensions/SendableMetatypeShim.swift + Utility/FoundationExtensions/Sequence+Categorize.swift + Utility/FoundationExtensions/Sequence+FirstMap.swift + Utility/FoundationExtensions/Sequence+RenderBlockContentElemenet.swift + Utility/FoundationExtensions/SortByKeyPath.swift + Utility/FoundationExtensions/String+Capitalization.swift + Utility/FoundationExtensions/String+Hashing.swift + Utility/FoundationExtensions/String+Path.swift + Utility/FoundationExtensions/String+SingleQuoted.swift + Utility/FoundationExtensions/String+Splitting.swift + Utility/FoundationExtensions/String+Whitespace.swift + Utility/FoundationExtensions/StringCollection+List.swift + Utility/FoundationExtensions/URL+IsAbsoluteWebURL.swift + Utility/FoundationExtensions/URL+Relative.swift + Utility/FoundationExtensions/URL+WithFragment.swift + Utility/FoundationExtensions/URL+WithoutHostAndPortAndScheme.swift + Utility/Graphs/DirectedGraph+Cycles.swift + Utility/Graphs/DirectedGraph+Paths.swift + Utility/Graphs/DirectedGraph+Traversal.swift + Utility/Graphs/DirectedGraph.swift + Utility/LMDB/LMDB+Database.swift + Utility/LMDB/LMDB+Environment.swift + Utility/LMDB/LMDB+Error.swift + Utility/LMDB/LMDB+Transaction.swift + Utility/LMDB/LMDB.swift + Utility/Language/EnglishLanguage.swift + Utility/Language/NativeLanguage.swift + Utility/ListItemUpdatable.swift + Utility/LogHandle.swift + Utility/MarkupExtensions/AnyLink.swift + Utility/MarkupExtensions/BlockDirectiveExtensions.swift + Utility/MarkupExtensions/DocumentExtensions.swift + Utility/MarkupExtensions/ImageExtensions.swift + Utility/MarkupExtensions/ListItemExtractor.swift + Utility/MarkupExtensions/MarkupChildrenExtensions.swift + Utility/MarkupExtensions/SourceRangeExtensions.swift + Utility/NearMiss.swift + Utility/RenderNodeDataExtractor.swift + Utility/SemanticVersion+Comparable.swift + Utility/SymbolGraphAvailability+Filter.swift + Utility/Synchronization.swift + Utility/ValidatedURL.swift + Utility/Version.swift) +target_link_libraries(SwiftDocC PRIVATE + DocCCommon) +target_link_libraries(SwiftDocC PUBLIC + SwiftMarkdown::Markdown + DocC::SymbolKit + LMDB::CLMDB + Crypto) +# FIXME(compnerd) workaround leaking dependencies +target_link_libraries(SwiftDocC PUBLIC + libcmark-gfm + libcmark-gfm-extensions) + +if(BUILD_SHARED_LIBS) + install(TARGETS SwiftDocC + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif() diff --git a/Sources/SwiftDocC/Catalog Processing/GeneratedCurationWriter.swift b/Sources/SwiftDocC/Catalog Processing/GeneratedCurationWriter.swift index a700bde21a..fd8e7f4ef1 100644 --- a/Sources/SwiftDocC/Catalog Processing/GeneratedCurationWriter.swift +++ b/Sources/SwiftDocC/Catalog Processing/GeneratedCurationWriter.swift @@ -185,7 +185,7 @@ public struct GeneratedCurationWriter { let languagesToCurate = node.availableSourceLanguages.sorted() var topicsByLanguage = [SourceLanguage: [AutomaticCuration.TaskGroup]]() for language in languagesToCurate { - topicsByLanguage[language] = try? AutomaticCuration.topics(for: node, withTraits: [.init(interfaceLanguage: language.id)], context: context) + topicsByLanguage[language] = try? AutomaticCuration.topics(for: node, withTraits: [.init(sourceLanguage: language)], context: context) } guard topicsByLanguage.count > 1 else { diff --git a/Sources/SwiftDocC/Checker/Checkers/AbstractContainsFormattedTextOnly.swift b/Sources/SwiftDocC/Checker/Checkers/AbstractContainsFormattedTextOnly.swift index 05f30ea575..f41e467cf6 100644 --- a/Sources/SwiftDocC/Checker/Checkers/AbstractContainsFormattedTextOnly.swift +++ b/Sources/SwiftDocC/Checker/Checkers/AbstractContainsFormattedTextOnly.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -14,6 +14,7 @@ public import Markdown /** A document's abstract may only contain formatted text. Images and links are not allowed. */ +@available(*, deprecated, message: "This check is no longer applicable. This deprecated API will be removed after 6.4 is released") public struct AbstractContainsFormattedTextOnly: Checker { public var problems: [Problem] = [Problem]() private var sourceFile: URL? diff --git a/Sources/SwiftDocC/Checker/Checkers/InvalidCodeBlockOption.swift b/Sources/SwiftDocC/Checker/Checkers/InvalidCodeBlockOption.swift new file mode 100644 index 0000000000..e483fa76c7 --- /dev/null +++ b/Sources/SwiftDocC/Checker/Checkers/InvalidCodeBlockOption.swift @@ -0,0 +1,97 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 +*/ + +internal import Foundation +internal import Markdown + +/** + Code blocks can have a `nocopy` option after the \`\`\`, in the language line. +`nocopy` can be immediately after the \`\`\` or after a specified language and a comma (`,`). + */ +internal struct InvalidCodeBlockOption: Checker { + var problems = [Problem]() + + /// Parsing options for code blocks + private let knownOptions = RenderBlockContent.CodeBlockOptions.knownOptions + + private var sourceFile: URL? + + /// Creates a new checker that detects documents with multiple titles. + /// + /// - Parameter sourceFile: The URL to the documentation file that the checker checks. + init(sourceFile: URL?) { + self.sourceFile = sourceFile + } + + mutating func visitCodeBlock(_ codeBlock: CodeBlock) { + let (lang, tokens) = RenderBlockContent.CodeBlockOptions.tokenizeLanguageString(codeBlock.language) + + func matches(token: RenderBlockContent.CodeBlockOptions.OptionName, value: String?) { + guard token == .unknown, let value = value else { return } + + let matches = NearMiss.bestMatches(for: knownOptions, against: value) + + if !matches.isEmpty { + let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Unknown option \(value.singleQuoted) in code block.") + let possibleSolutions = matches.map { candidate in + Solution( + summary: "Replace \(value.singleQuoted) with \(candidate.singleQuoted).", + replacements: [] + ) + } + problems.append(Problem(diagnostic: diagnostic, possibleSolutions: possibleSolutions)) + } else if lang == nil { + let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Unknown option \(value.singleQuoted) in code block.") + let possibleSolutions = + Solution( + summary: "If \(value.singleQuoted) is the language for this code block, then write \(value.singleQuoted) as the first option.", + replacements: [] + ) + problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [possibleSolutions])) + } + } + + func validateArrayIndices(token: RenderBlockContent.CodeBlockOptions.OptionName, value: String?) { + guard token == .highlight || token == .strikeout, let value = value else { return } + // code property ends in a newline. this gives us a bogus extra line. + let lineCount: Int = codeBlock.code.split(omittingEmptySubsequences: false, whereSeparator: { $0.isNewline }).count - 1 + + let indices = RenderBlockContent.CodeBlockOptions.parseCodeBlockOptionsArray(value) + + if !value.isEmpty, indices.isEmpty { + let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Could not parse \(token.rawValue.singleQuoted) indices from \(value.singleQuoted). Expected an integer (e.g. 3) or an array (e.g. [1, 3, 5])") + problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [])) + return + } + + let invalid = indices.filter { $0 < 1 || $0 > lineCount } + guard !invalid.isEmpty else { return } + + let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: codeBlock.range, identifier: "org.swift.docc.InvalidCodeBlockOption", summary: "Invalid \(token.rawValue.singleQuoted) index\(invalid.count == 1 ? "" : "es") in \(value.singleQuoted) for a code block with \(lineCount) line\(lineCount == 1 ? "" : "s"). Valid range is 1...\(lineCount).") + let solutions: [Solution] = { + if invalid.contains(where: {$0 == lineCount + 1}) { + return [Solution( + summary: "If you intended the last line, change '\(lineCount + 1)' to \(lineCount).", + replacements: [] + )] + } + return [] + }() + problems.append(Problem(diagnostic: diagnostic, possibleSolutions: solutions)) + } + + for (token, value) in tokens { + matches(token: token, value: value) + validateArrayIndices(token: token, value: value) + } + // check if first token (lang) might be a typo + matches(token: .unknown, value: lang) + } +} diff --git a/Sources/SwiftDocC/Converter/DocumentationContextConverter.swift b/Sources/SwiftDocC/Converter/DocumentationContextConverter.swift index 72e23665bb..ff9c438239 100644 --- a/Sources/SwiftDocC/Converter/DocumentationContextConverter.swift +++ b/Sources/SwiftDocC/Converter/DocumentationContextConverter.swift @@ -20,9 +20,6 @@ public class DocumentationContextConverter { /// The context the converter uses to resolve references it finds in the documentation node's content. let context: DocumentationContext - /// The bundle that contains the content from which the documentation node originated. - let bundle: DocumentationBundle - /// A context that contains common pre-rendered pieces of content. let renderContext: RenderContext @@ -43,12 +40,11 @@ public class DocumentationContextConverter { /// The remote source control repository where the documented module's source is hosted. let sourceRepository: SourceRepository? - /// Creates a new node converter for the given bundle and context. + /// Creates a new node converter for the given context. /// /// The converter uses bundle and context to resolve references to other documentation and describe the documentation hierarchy. /// /// - Parameters: - /// - bundle: The bundle that contains the content from which the documentation node originated. /// - context: The context that the converter uses to to resolve references it finds in the documentation node's content. /// - renderContext: A context that contains common pre-rendered pieces of content. /// - emitSymbolSourceFileURIs: Whether the documentation converter should include @@ -61,7 +57,6 @@ public class DocumentationContextConverter { /// - sourceRepository: The source repository where the documentation's sources are hosted. /// - symbolIdentifiersWithExpandedDocumentation: A list of symbol IDs that have version of their documentation page with more content that a renderer can link to. public init( - bundle: DocumentationBundle, context: DocumentationContext, renderContext: RenderContext, emitSymbolSourceFileURIs: Bool = false, @@ -69,7 +64,6 @@ public class DocumentationContextConverter { sourceRepository: SourceRepository? = nil, symbolIdentifiersWithExpandedDocumentation: [String]? = nil ) { - self.bundle = bundle self.context = context self.renderContext = renderContext self.shouldEmitSymbolSourceFileURIs = emitSymbolSourceFileURIs @@ -77,6 +71,25 @@ public class DocumentationContextConverter { self.sourceRepository = sourceRepository self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation } + @available(*, deprecated, renamed: "init(context:renderContext:emitSymbolSourceFileURIs:emitSymbolAccessLevels:sourceRepository:symbolIdentifiersWithExpandedDocumentation:)", message: "Use 'init(context:renderContext:emitSymbolSourceFileURIs:emitSymbolAccessLevels:sourceRepository:symbolIdentifiersWithExpandedDocumentation:)' instead. This deprecated API will be removed after 6.4 is released.") + public convenience init( + bundle _: DocumentationBundle, + context: DocumentationContext, + renderContext: RenderContext, + emitSymbolSourceFileURIs: Bool = false, + emitSymbolAccessLevels: Bool = false, + sourceRepository: SourceRepository? = nil, + symbolIdentifiersWithExpandedDocumentation: [String]? = nil + ) { + self.init( + context: context, + renderContext: renderContext, + emitSymbolSourceFileURIs: emitSymbolSourceFileURIs, + emitSymbolAccessLevels: emitSymbolAccessLevels, + sourceRepository: sourceRepository, + symbolIdentifiersWithExpandedDocumentation: symbolIdentifiersWithExpandedDocumentation + ) + } /// Converts a documentation node to a render node. /// @@ -91,7 +104,6 @@ public class DocumentationContextConverter { var translator = RenderNodeTranslator( context: context, - bundle: bundle, identifier: node.reference, renderContext: renderContext, emitSymbolSourceFileURIs: shouldEmitSymbolSourceFileURIs, diff --git a/Sources/SwiftDocC/Converter/DocumentationNodeConverter.swift b/Sources/SwiftDocC/Converter/DocumentationNodeConverter.swift index 77804359e8..51c19137b6 100644 --- a/Sources/SwiftDocC/Converter/DocumentationNodeConverter.swift +++ b/Sources/SwiftDocC/Converter/DocumentationNodeConverter.swift @@ -15,20 +15,19 @@ public struct DocumentationNodeConverter { /// The context the converter uses to resolve references it finds in the documentation node's content. let context: DocumentationContext - /// The bundle that contains the content from which the documentation node originated. - let bundle: DocumentationBundle - - /// Creates a new node converter for the given bundle and context. + /// Creates a new node converter for the given context. /// - /// The converter uses bundle and context to resolve references to other documentation and describe the documentation hierarchy. + /// The converter uses context to resolve references to other documentation and describe the documentation hierarchy. /// /// - Parameters: - /// - bundle: The bundle that contains the content from which the documentation node originated. /// - context: The context that the converter uses to to resolve references it finds in the documentation node's content. - public init(bundle: DocumentationBundle, context: DocumentationContext) { - self.bundle = bundle + public init(context: DocumentationContext) { self.context = context } + @available(*, deprecated, renamed: "init(context:)", message: "Use 'init(context:)' instead. This deprecated API will be removed after 6.4 is released.") + public init(bundle _: DocumentationBundle, context: DocumentationContext) { + self.init(context: context) + } /// Converts a documentation node to a render node. /// @@ -37,7 +36,7 @@ public struct DocumentationNodeConverter { /// - node: The documentation node to convert. /// - Returns: The render node representation of the documentation node. public func convert(_ node: DocumentationNode) -> RenderNode { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) return translator.visit(node.semantic) as! RenderNode } } diff --git a/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift b/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift index 3be90ed79e..f55a9fe80b 100644 --- a/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift +++ b/Sources/SwiftDocC/DocumentationService/Convert/ConvertService.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -44,68 +44,68 @@ public struct ConvertService: DocumentationService { _ message: DocumentationServer.Message, completion: @escaping (DocumentationServer.Message) -> () ) { - let conversionResult = retrievePayload(message) - .flatMap(decodeRequest) - .flatMap(convert) - .flatMap(encodeResponse) - - switch conversionResult { - case .success(let response): - completion( - DocumentationServer.Message( - type: Self.convertResponseMessageType, - identifier: "\(message.identifier)-response", - payload: response + Task { + let result = await process(message) + completion(result) + } + } + + public func process(_ message: DocumentationServer.Message) async -> DocumentationServer.Message { + func makeErrorResponse(_ error: ConvertServiceError) -> DocumentationServer.Message { + DocumentationServer.Message( + type: Self.convertResponseErrorMessageType, + identifier: "\(message.identifier)-response-error", + + // Force trying because encoding known messages should never fail. + payload: try! JSONEncoder().encode(error) + ) + } + + guard let payload = message.payload else { + return makeErrorResponse(.missingPayload()) + } + + let request: ConvertRequest + do { + request = try JSONDecoder().decode(ConvertRequest.self, from: payload) + } catch { + return makeErrorResponse(.invalidRequest(underlyingError: error.localizedDescription)) + } + + let renderNodes: [RenderNode] + let renderReferenceStore: RenderReferenceStore? + do { + (renderNodes, renderReferenceStore) = try await convert(request: request, messageIdentifier: message.identifier) + } catch { + return makeErrorResponse(.conversionError(underlyingError: error.localizedDescription)) + } + + do { + let encoder = JSONEncoder() + let encodedResponse = try encoder.encode( + try ConvertResponse( + renderNodes: renderNodes.map(encoder.encode), + renderReferenceStore: renderReferenceStore.map(encoder.encode) ) ) - case .failure(let error): - completion( - DocumentationServer.Message( - type: Self.convertResponseErrorMessageType, - identifier: "\(message.identifier)-response-error", - - // Force trying because encoding known messages should never fail. - payload: try! JSONEncoder().encode(error) - ) + return DocumentationServer.Message( + type: Self.convertResponseMessageType, + identifier: "\(message.identifier)-response", + payload: encodedResponse ) - } - } - - /// Attempts to retrieve the payload from the given message, returning a failure if the payload is missing. - /// - /// - Returns: A result with the message's payload if present, otherwise a ``ConvertServiceError/missingPayload`` - /// failure. - private func retrievePayload( - _ message: DocumentationServer.Message - ) -> Result<(payload: Data, messageIdentifier: String), ConvertServiceError> { - message.payload.map { .success(($0, message.identifier)) } ?? .failure(.missingPayload()) - } - - /// Attempts to decode the given request, returning a failure if decoding failed. - /// - /// - Returns: A result with the decoded request if the decoding succeeded, otherwise a - /// ``ConvertServiceError/invalidRequest`` failure. - private func decodeRequest( - data: Data, - messageIdentifier: String - ) -> Result<(request: ConvertRequest, messageIdentifier: String), ConvertServiceError> { - Result { - return (try JSONDecoder().decode(ConvertRequest.self, from: data), messageIdentifier) - }.mapErrorToConvertServiceError { - .invalidRequest(underlyingError: $0.localizedDescription) + } catch { + return makeErrorResponse(.invalidResponseMessage(underlyingError: error.localizedDescription)) } } /// Attempts to process the given convert request, returning a failure if the conversion failed. /// - /// - Returns: A result with the produced render nodes if the conversion was successful, otherwise a - /// ``ConvertServiceError/conversionError`` failure. + /// - Returns: A result with the produced render nodes if the conversion was successful private func convert( request: ConvertRequest, messageIdentifier: String - ) -> Result<([RenderNode], RenderReferenceStore?), ConvertServiceError> { - Result { + ) async throws -> ([RenderNode], RenderReferenceStore?) { // Update DocC's current feature flags based on the ones provided // in the request. FeatureFlags.current = request.featureFlags @@ -155,10 +155,10 @@ public struct ConvertService: DocumentationService { (bundle, dataProvider) = Self.makeBundleAndInMemoryDataProvider(request) } - let context = try DocumentationContext(bundle: bundle, dataProvider: dataProvider, configuration: configuration) + let context = try await DocumentationContext(bundle: bundle, dataProvider: dataProvider, configuration: configuration) // Precompute the render context - let renderContext = RenderContext(documentationContext: context, bundle: bundle) + let renderContext = RenderContext(documentationContext: context) let symbolIdentifiersMeetingRequirementsForExpandedDocumentation: [String]? = request.symbolIdentifiersWithExpandedDocumentation?.compactMap { identifier, expandedDocsRequirement in guard let documentationNode = context.documentationCache[identifier] else { @@ -168,7 +168,6 @@ public struct ConvertService: DocumentationService { return documentationNode.meetsExpandedDocumentationRequirements(expandedDocsRequirement) ? identifier : nil } let converter = DocumentationContextConverter( - bundle: bundle, context: context, renderContext: renderContext, emitSymbolSourceFileURIs: request.emitSymbolSourceFileURIs, @@ -228,30 +227,6 @@ public struct ConvertService: DocumentationService { } return (renderNodes, referenceStore) - }.mapErrorToConvertServiceError { - .conversionError(underlyingError: $0.localizedDescription) - } - } - - /// Encodes a conversion response to send to the client. - /// - /// - Parameter renderNodes: The render nodes that were produced as part of the conversion. - private func encodeResponse( - renderNodes: [RenderNode], - renderReferenceStore: RenderReferenceStore? - ) -> Result { - Result { - let encoder = JSONEncoder() - - return try encoder.encode( - try ConvertResponse( - renderNodes: renderNodes.map(encoder.encode), - renderReferenceStore: renderReferenceStore.map(encoder.encode) - ) - ) - }.mapErrorToConvertServiceError { - .invalidResponseMessage(underlyingError: $0.localizedDescription) - } } /// Takes a base reference store and adds uncurated article references and documentation extensions. @@ -267,11 +242,11 @@ public struct ConvertService: DocumentationService { .compactMap { (value, isDocumentationExtensionContent) -> (ResolvedTopicReference, RenderReferenceStore.TopicContent)? in let (topicReference, article) = value - guard let bundle = context.bundle, bundle.id == topicReference.bundleID else { return nil } - let renderer = DocumentationContentRenderer(documentationContext: context, bundle: bundle) + guard context.inputs.id == topicReference.bundleID else { return nil } + let renderer = DocumentationContentRenderer(context: context) let documentationNodeKind: DocumentationNode.Kind = isDocumentationExtensionContent ? .unknownSymbol : .article - let overridingDocumentationNode = DocumentationContext.documentationNodeAndTitle(for: article, kind: documentationNodeKind, in: bundle)?.node + let overridingDocumentationNode = DocumentationContext.documentationNodeAndTitle(for: article, kind: documentationNodeKind, in: context.inputs)?.node var dependencies = RenderReferenceDependencies() let renderReference = renderer.renderReference(for: topicReference, with: overridingDocumentationNode, dependencies: &dependencies) @@ -297,25 +272,6 @@ public struct ConvertService: DocumentationService { } } -extension Result { - /// Returns a new result, mapping any failure value using the given transformation if the error is not a conversion error. - /// - /// If the error value is a ``ConvertServiceError``, it is returned as-is. If it's not, the given transformation is called on the - /// error. - /// - /// - Parameter transform: A closure that takes the failure value of the instance. - func mapErrorToConvertServiceError( - _ transform: (any Error) -> ConvertServiceError - ) -> Result { - mapError { error in - switch error { - case let error as ConvertServiceError: return error - default: return transform(error) - } - } - } -} - private extension SymbolGraph.LineList.Line { /// Creates a line given a convert request line. init(_ line: ConvertRequest.Line) { @@ -336,3 +292,11 @@ private extension SymbolGraph.LineList.Line { ) } } + +private extension DocumentationNode { + func meetsExpandedDocumentationRequirements(_ requirements: ConvertRequest.ExpandedDocumentationRequirements) -> Bool { + guard let symbol else { return false } + + return requirements.accessControlLevels.contains(symbol.accessLevel.rawValue) && (!symbol.names.title.starts(with: "_") || requirements.canBeUnderscored) + } +} diff --git a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/AbsoluteSymbolLink.swift b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/AbsoluteSymbolLink.swift deleted file mode 100644 index e4d962db07..0000000000 --- a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/AbsoluteSymbolLink.swift +++ /dev/null @@ -1,343 +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 - -/// An absolute link to a symbol. -/// -/// You can use this model to validate a symbol link and access its different parts. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -public struct AbsoluteSymbolLink: CustomStringConvertible { - /// The identifier for the documentation bundle this link is from. - public let bundleID: String - - /// The name of the module that contains this symbol link. - /// - Note: This could be a link to the module itself. - public let module: String - - /// The top level symbol in this documentation link. - /// - /// If this symbol represents a module (see ``representsModule``), then - /// this is just the module and can be ignored. Otherwise, it's the top level symbol within - /// the module. - public let topLevelSymbol: LinkComponent - - /// The ordered path components, excluding the module and top level symbol. - public let basePathComponents: [LinkComponent] - - /// A Boolean value that is true if this is a link to a module. - public let representsModule: Bool - - /// Create a new documentation symbol link from a path. - /// - /// Expects an absolute symbol link structured like one of the following: - /// - doc://org.swift.docc.example/documentation/ModuleName - /// - doc://org.swift.docc.example/documentation/ModuleName/TypeName - /// - doc://org.swift.docc.example/documentation/ModuleName/ParentType/Test-swift.class/testFunc()-k2k9d - /// - doc://org.swift.docc.example/documentation/ModuleName/ClassName/functionName(firstParameter:secondParameter:) - public init?(string: String) { - // Begin by constructing a validated URL from the given string. - // Normally symbol links would be validated with `init(symbolPath:)` but since this is expected - // to be an absolute URL we parse it with `init(parsing:)` instead. - guard let validatedURL = ValidatedURL(parsingExact: string)?.requiring(scheme: ResolvedTopicReference.urlScheme) else { - return nil - } - - // All absolute documentation links include the bundle identifier as their host. - guard let bundleID = validatedURL.components.host, !bundleID.isEmpty else { - return nil - } - self.bundleID = bundleID - - var pathComponents = validatedURL.url.pathComponents - - // Swift's URL interprets the following link "doc://org.swift.docc.example/documentation/ModuleName" - // to have a sole "/" as its first path component. We'll just remove it if it's there. - if pathComponents.first == "/" { - pathComponents.removeFirst() - } - - // Swift-DocC requires absolute symbol links to be prepended with "documentation" - guard pathComponents.first == NodeURLGenerator.Path.documentationFolderName else { - return nil - } - - // Swift-DocC requires absolute symbol links to be prepended with "documentation" - // as their first path component but that's not actually part of the symbol's path - // so we drop it here. - pathComponents.removeFirst() - - // Now that we've cleaned up the link, confirm that it's non-empty - guard !pathComponents.isEmpty else { - return nil - } - - // Validate and construct the link component that represents the module - guard let moduleLinkComponent = LinkComponent(string: pathComponents.removeFirst()) else { - return nil - } - - // We don't allow modules to have disambiguation suffixes - guard !moduleLinkComponent.hasDisambiguationSuffix else { - return nil - } - self.module = moduleLinkComponent.name - - // Next we'll attempt to construct the link component for the top level symbol - // within the module. - if pathComponents.isEmpty { - // There are no more path components, so this is a link to the module itself - self.topLevelSymbol = moduleLinkComponent - self.representsModule = true - } else if let topLevelSymbol = LinkComponent(string: pathComponents.removeFirst()) { - // We were able to build a valid link component for the next path component - // so we'll set the top level symbol value and indicate that this is not a link - // to the module - self.topLevelSymbol = topLevelSymbol - self.representsModule = false - } else { - return nil - } - - // Finally we transform the remaining path components into link components - basePathComponents = pathComponents.compactMap { componentName in - LinkComponent(string: componentName) - } - - // If any of the path components were invalid, we want to mark the entire link as invalid - guard basePathComponents.count == pathComponents.count else { - return nil - } - } - - public var description: String { - """ - { - bundleID: \(bundleID.singleQuoted), - module: \(module.singleQuoted), - topLevelSymbol: \(topLevelSymbol), - representsModule: \(representsModule), - basePathComponents: [\(basePathComponents.map(\.description).joined(separator: ", "))] - } - """ - } -} - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension AbsoluteSymbolLink { - /// A component of a symbol link. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - public struct LinkComponent: CustomStringConvertible { - /// The name of the symbol represented by the link component. - public let name: String - - /// The suffix used to disambiguate this symbol from other symbol's - /// that share the same name. - public let disambiguationSuffix: DisambiguationSuffix - - var hasDisambiguationSuffix: Bool { - disambiguationSuffix != .none - } - - init(name: String, disambiguationSuffix: DisambiguationSuffix) { - self.name = name - self.disambiguationSuffix = disambiguationSuffix - } - - /// Creates an absolute symbol component from a raw string. - /// - /// For example, the input string can be `"foo-swift.var"`. - public init?(string: String) { - // Check to see if this component includes a disambiguation suffix - if string.contains("-") { - // Split the path component into its name and disambiguation suffix. - // It's important to limit to a single split since the disambiguation - // suffix itself could also contain a '-'. - var splitPathComponent = string.split(separator: "-", maxSplits: 1) - guard splitPathComponent.count == 2 else { - // This catches a trailing '-' in the suffix (which we don't allow) - // since a split would then result in a single path component. - return nil - } - - // Set the name from the first half of the split - name = String(splitPathComponent.removeFirst()) - - // The disambiguation suffix is formed from the second half - let disambiguationSuffixString = String(splitPathComponent.removeLast()) - - // Attempt to parse and validate a disambiguation suffix - guard let disambiguationSuffix = DisambiguationSuffix( - string: disambiguationSuffixString - ) else { - // Invalid disambiguation suffix, so we just return nil - return nil - } - - guard disambiguationSuffix != .none else { - // Since a "-" was included, we expect the disambiguation - // suffix to be non-nil. - return nil - } - - self.disambiguationSuffix = disambiguationSuffix - } else { - // The path component had no "-" so we just set the name - // as the entire path component - name = string - disambiguationSuffix = .none - } - } - - public var description: String { - """ - (name: \(name.singleQuoted), suffix: \(disambiguationSuffix)) - """ - } - - /// A string representation of this link component. - public var asLinkComponentString: String { - "\(name)\(disambiguationSuffix.asLinkSuffixString)" - } - } -} - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension AbsoluteSymbolLink.LinkComponent { - /// A suffix attached to a documentation link to disambiguate it from other symbols - /// that share the same base name. - public enum DisambiguationSuffix: Equatable, CustomStringConvertible { - /// The link is not disambiguated. - case none - - /// The symbol's kind. - case kindIdentifier(String) - - /// A hash of the symbol's precise identifier. - case preciseIdentifierHash(String) - - /// The symbol's kind and precise identifier. - /// - /// See ``kindIdentifier(_:)`` and ``preciseIdentifierHash(_:)`` for details. - case kindAndPreciseIdentifier( - kindIdentifier: String, preciseIdentifierHash: String - ) - - private static func isKnownSymbolKindIdentifier(identifier: String) -> Bool { - return SymbolGraph.Symbol.KindIdentifier.isKnownIdentifier(identifier) - } - - /// Creates a disambiguation suffix based on the given kind and precise - /// identifiers. - init(kindIdentifier: String?, preciseIdentifier: String?) { - if let kindIdentifier, let preciseIdentifier { - self = .kindAndPreciseIdentifier( - kindIdentifier: kindIdentifier, - preciseIdentifierHash: preciseIdentifier.stableHashString - ) - } else if let kindIdentifier { - self = .kindIdentifier(kindIdentifier) - } else if let preciseIdentifier { - self = .preciseIdentifierHash(preciseIdentifier.stableHashString) - } else { - self = .none - } - } - - /// Creates a symbol path component disambiguation suffix from the given string. - init?(string: String) { - guard !string.isEmpty else { - self = .none - return - } - - // We begin by splitting the given string in - // case this disambiguation suffix includes both an id hash - // and a kind identifier. - let splitSuffix = string.split(separator: "-") - - if splitSuffix.count == 1 && splitSuffix[0] == string { - // The string didn't contain a "-" so now we check - // to see if the hash is a known symbol kind identifier. - if Self.isKnownSymbolKindIdentifier(identifier: string) { - self = .kindIdentifier(string) - } else { - // Since we've confirmed that it's not a symbol kind identifier - // we now assume its a hash of the symbol's precise identifier. - self = .preciseIdentifierHash(string) - } - } else if splitSuffix.count == 2 { - // The string did contain a "-" so we now know the exact format - // it should be in. - // - // We expect the symbol kind identifier to come first, followed - // by a hash of the symbol's precise identifier. - if Self.isKnownSymbolKindIdentifier(identifier: String(splitSuffix[0])) { - self = .kindAndPreciseIdentifier( - kindIdentifier: String(splitSuffix[0]), - preciseIdentifierHash: String(splitSuffix[1]) - ) - } else { - // We were unable to validate the given symbol kind identifier - // so this is an invalid format for a disambiguation suffix. - return nil - } - } else { - // Unexpected number or configuration of "-" in the given string - // so we just return nil. - return nil - } - } - - public var description: String { - switch self { - case .none: - return "(none)" - case .kindIdentifier(let kindIdentifier): - return "(kind: \(kindIdentifier.singleQuoted))" - case .preciseIdentifierHash(let preciseIdentifierHash): - return "(idHash: \(preciseIdentifierHash.singleQuoted))" - case .kindAndPreciseIdentifier( - kindIdentifier: let kindIdentifier, - preciseIdentifierHash: let preciseIdentifierHash - ): - return "(kind: \(kindIdentifier.singleQuoted), idHash: \(preciseIdentifierHash.singleQuoted))" - } - } - - /// A string representation of the given disambiguation suffix. - /// - /// This value will include the preceding "-" character if necessary. - /// For example, if this is a ``kindAndPreciseIdentifier(kindIdentifier:preciseIdentifierHash:)`` value, - /// the following might be returned: - /// - /// ``` - /// -swift.var-h73kj - /// ``` - /// - /// However, if this is a ``none``, an empty string will be returned. - public var asLinkSuffixString: String { - switch self { - case .none: - return "" - case .kindIdentifier(let kindIdentifier): - return "-\(kindIdentifier)" - case .preciseIdentifierHash(let preciseIdentifierHash): - return "-\(preciseIdentifierHash)" - case .kindAndPreciseIdentifier( - kindIdentifier: let kindIdentifier, - preciseIdentifierHash: let preciseIdentifierHash - ): - return "-\(kindIdentifier)-\(preciseIdentifierHash)" - } - } - } -} diff --git a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/DocCSymbolRepresentable.swift b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/DocCSymbolRepresentable.swift deleted file mode 100644 index a31c77b110..0000000000 --- a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/DocCSymbolRepresentable.swift +++ /dev/null @@ -1,232 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2024 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 -public import SymbolKit - -/// A type that can be converted to a DocC symbol. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -public protocol DocCSymbolRepresentable: Equatable { - /// A namespaced, unique identifier for the kind of symbol. - /// - /// For example, a Swift class might use `swift.class`. - var kindIdentifier: String? { get } - - /// A unique identifier for this symbol. - /// - /// For Swift, this is the USR. - var preciseIdentifier: String? { get } - - /// The case-sensitive title of this symbol as would be used in documentation. - /// - /// > Note: DocC embeds function parameter information directly in the title. - /// > For example: `functionName(parameterName:secondParameter)` - /// > or `functionName(_:firstNamedParameter)`. - var title: String { get } -} - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -public extension DocCSymbolRepresentable { - /// The given symbol information as a symbol link component. - /// - /// The component will include a disambiguation suffix - /// based on the included information in the symbol. For example, if the symbol - /// includes a kind identifier and a precise identifier, both - /// will be represented in the link component. - var asLinkComponent: AbsoluteSymbolLink.LinkComponent { - AbsoluteSymbolLink.LinkComponent( - name: title, - disambiguationSuffix: .init( - kindIdentifier: kindIdentifier, - preciseIdentifier: preciseIdentifier - ) - ) - } -} - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension AbsoluteSymbolLink.LinkComponent { - /// Given an array of symbols that are overloads for the symbol represented - /// by this link component, returns those that are precisely identified by the component. - /// - /// If the link is not specific enough to disambiguate between the given symbols, - /// this function will return an empty array. - public func disambiguateBetweenOverloadedSymbols( - _ overloadedSymbols: [SymbolType] - ) -> [SymbolType] { - // First confirm that we were given symbols to disambiguate - guard !overloadedSymbols.isEmpty else { - return [] - } - - // Pair each overloaded symbol with its required disambiguation - // suffix. This will tell us what kind of disambiguation suffix the - // link should have. - let overloadedSymbolsWithSuffixes = zip( - overloadedSymbols, overloadedSymbols.requiredDisambiguationSuffixes - ) - - // Next we filter the given symbols for those that are precise matches - // for the component. - let matchingSymbols = overloadedSymbolsWithSuffixes.filter { (symbol, _) in - // Filter the results by those that are fully represented by the element. - // This includes checking case sensitivity and disambiguation suffix. - // This _should_ always return a single element but we can't be entirely sure. - return fullyRepresentsSymbol(symbol) - } - - // We now check all the returned matching symbols to confirm that - // the current link has the correct disambiguation suffix - for (_, (shouldAddIdHash, shouldAddKind)) in matchingSymbols { - if shouldAddIdHash && shouldAddKind { - guard case .kindAndPreciseIdentifier = disambiguationSuffix else { - return [] - } - } else if shouldAddIdHash { - guard case .preciseIdentifierHash = disambiguationSuffix else { - return [] - } - } else if shouldAddKind { - guard case .kindIdentifier = disambiguationSuffix else { - return [] - } - } else { - guard case .none = disambiguationSuffix else { - return [] - } - } - } - - // Since we've validated that the link has the correct - // disambiguation suffix, we now return all matching symbols. - return matchingSymbols.map(\.0) - } - - /// Returns true if the given symbol is fully represented by the - /// symbol link. - /// - /// This means that the element has the same name (case-sensitive) - /// and, if the symbol link has a disambiguation suffix, the given element has the same - /// type or usr. - private func fullyRepresentsSymbol( - _ symbol: some DocCSymbolRepresentable - ) -> Bool { - guard name == symbol.title else { - return false - } - - switch self.disambiguationSuffix { - case .none: - return true - case .kindIdentifier(let kindIdentifier): - return symbol.kindIdentifier == kindIdentifier - case .preciseIdentifierHash(let preciseIdentifierHash): - return symbol.preciseIdentifier?.stableHashString == preciseIdentifierHash - case .kindAndPreciseIdentifier( - kindIdentifier: let kindIdentifier, - preciseIdentifierHash: let preciseIdentifierHash): - return symbol.preciseIdentifier?.stableHashString == preciseIdentifierHash - && symbol.kindIdentifier == kindIdentifier - } - } -} - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -public extension Collection where Element: DocCSymbolRepresentable { - /// Given a collection of colliding symbols, returns the disambiguation suffix required - /// for each symbol to disambiguate it from the others in the collection. - var requiredDisambiguationSuffixes: [(shouldAddIdHash: Bool, shouldAddKind: Bool)] { - guard let first else { - return [] - } - - guard count > 1 else { - // There are no path collisions - return Array(repeating: (shouldAddIdHash: false, shouldAddKind: false), count: count) - } - - if allSatisfy({ symbol in symbol.kindIdentifier == first.kindIdentifier }) { - // All collisions are the same symbol kind. - return Array(repeating: (shouldAddIdHash: true, shouldAddKind: false), count: count) - } else { - // Disambiguate by kind - return map { currentSymbol in - let kindCount = filter { $0.kindIdentifier == currentSymbol.kindIdentifier }.count - return ( - shouldAddIdHash: kindCount > 1, - shouldAddKind: kindCount == 1 - ) - } - } - } -} - -#if compiler(>=6) -// DocCSymbolRepresentable inherits from Equatable. If SymbolKit added Equatable conformance in the future, this could behave differently. -// It's reasonable to expect that symbols with the same unique ID would be equal but it's possible that SymbolKit's implementation would consider more symbol properties. -// -// In the long term we should try to phase out DocCSymbolRepresentable since it doesn't reflect how DocC resolves links or disambiguated symbols in links. -extension SymbolGraph.Symbol: @retroactive Equatable {} -extension UnifiedSymbolGraph.Symbol: @retroactive Equatable {} -#endif - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension SymbolGraph.Symbol: DocCSymbolRepresentable { - public var preciseIdentifier: String? { - self.identifier.precise - } - - public var title: String { - self.names.title - } - - public var kindIdentifier: String? { - "\(self.identifier.interfaceLanguage).\(self.kind.identifier.identifier)" - } - - public static func == (lhs: SymbolGraph.Symbol, rhs: SymbolGraph.Symbol) -> Bool { - lhs.identifier.precise == rhs.identifier.precise - } -} - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension UnifiedSymbolGraph.Symbol: DocCSymbolRepresentable { - public var preciseIdentifier: String? { - self.uniqueIdentifier - } - - public var title: String { - guard let selector = self.defaultSelector else { - fatalError(""" - Failed to find a supported default selector. \ - Language unsupported or corrupt symbol graph provided. - """ - ) - } - - return self.names[selector]!.title - } - - public var kindIdentifier: String? { - guard let selector = self.defaultSelector else { - fatalError(""" - Failed to find a supported default selector. \ - Language unsupported or corrupt symbol graph provided. - """ - ) - } - - return "\(selector.interfaceLanguage).\(self.kind[selector]!.identifier.identifier)" - } - - public static func == (lhs: UnifiedSymbolGraph.Symbol, rhs: UnifiedSymbolGraph.Symbol) -> Bool { - lhs.uniqueIdentifier == rhs.uniqueIdentifier - } -} diff --git a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/LinkCompletionTools.swift b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/LinkCompletionTools.swift index 7ed5ac3d52..ac1a36f20c 100644 --- a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/LinkCompletionTools.swift +++ b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/LinkCompletionTools.swift @@ -9,6 +9,7 @@ */ import Foundation +public import SymbolKit /// A collection of API for link completion. /// @@ -20,7 +21,6 @@ import Foundation /// - Third, determine the minimal unique disambiguation for each completion suggestion using ``suggestedDisambiguation(forCollidingSymbols:)`` /// /// > Tip: You can use ``SymbolInformation/hash(uniqueSymbolID:)`` to compute the hashed symbol identifiers needed for steps 2 and 3 above. -@_spi(LinkCompletion) // LinkCompletionTools isn't stable API yet public enum LinkCompletionTools { // MARK: Parsing @@ -131,8 +131,8 @@ public enum LinkCompletionTools { node, kind: symbol.kind, hash: symbol.symbolIDHash, - parameterTypes: symbol.parameterTypes, - returnTypes: symbol.returnTypes + parameterTypes: symbol.parameterTypes?.map { $0.withoutWhitespace() }, + returnTypes: symbol.returnTypes?.map { $0.withoutWhitespace() } ) } @@ -184,6 +184,15 @@ public enum LinkCompletionTools { self.parameterTypes = parameterTypes self.returnTypes = returnTypes } + + public init(symbol: SymbolGraph.Symbol) { + self.kind = symbol.kind.identifier.identifier + self.symbolIDHash = Self.hash(uniqueSymbolID: symbol.identifier.precise) + if let signature = PathHierarchy.functionSignatureTypeNames(for: symbol) { + self.parameterTypes = signature.parameterTypeNames + self.returnTypes = signature.returnTypeNames + } + } /// Creates a hashed representation of a symbol's unique identifier. /// @@ -236,3 +245,9 @@ private extension PathHierarchy.PathComponent.Disambiguation { } } } + +private extension String { + func withoutWhitespace() -> String { + filter { !$0.isWhitespace } + } +} diff --git a/Sources/SwiftDocC/DocumentationService/ExternalReferenceResolverServiceClient.swift b/Sources/SwiftDocC/DocumentationService/ExternalReferenceResolverServiceClient.swift index 6fd3fd51ad..5fcc59cdaf 100644 --- a/Sources/SwiftDocC/DocumentationService/ExternalReferenceResolverServiceClient.swift +++ b/Sources/SwiftDocC/DocumentationService/ExternalReferenceResolverServiceClient.swift @@ -42,7 +42,7 @@ class ExternalReferenceResolverServiceClient { self.convertRequestIdentifier = convertRequestIdentifier } - func sendAndWait(_ request: some Codable) throws -> Data { + func sendAndWait(_ request: some Codable & SendableMetatype) throws -> Data { let resultGroup = DispatchGroup() var result: Result? diff --git a/Sources/SwiftDocC/Indexing/Navigator/AvailabilityIndex+Ext.swift b/Sources/SwiftDocC/Indexing/Navigator/AvailabilityIndex+Ext.swift index 1309a484c7..1cf55b1e8a 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/AvailabilityIndex+Ext.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/AvailabilityIndex+Ext.swift @@ -83,7 +83,7 @@ public struct InterfaceLanguage: Hashable, CustomStringConvertible, Codable, Equ /// > ``from(string:)`` function. public let id: String - /// A mask to use to identify the interface language.. + /// A mask to use to identify the interface language. public let mask: ID diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift deleted file mode 100644 index d3834d1e1e..0000000000 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift +++ /dev/null @@ -1,75 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2025 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 - -/** - This class provides a simple way to transform a `FileSystemProvider` into a `RenderNodeProvider` to feed an index builder. - The data from the disk is fetched and processed in an efficient way to build a navigator index. - */ -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") -public class FileSystemRenderNodeProvider: RenderNodeProvider { - - /// The internal `FileSystemProvider` reference. - private let dataProvider: any FileSystemProvider - - /// The list of problems the provider encountered during the process. - private var problems = [Problem]() - - /// The enqueued file system nodes. - private var queue = [FSNode]() - - /** - Initialize an instance to provide `RenderNode` instances from a give `FileSystemProvider`. - */ - public init(fileSystemProvider: any FileSystemProvider) { - dataProvider = fileSystemProvider - - // Insert the first node in the queue - queue.append(fileSystemProvider.fileSystem) - } - - /// Returns a render node that can be processed by an index creator, for example. - public func getRenderNode() -> RenderNode? { - var renderNode: RenderNode? = nil - - while let next = queue.first, renderNode == nil { - switch next { - case .directory(let dir): - queue.append(contentsOf: dir.children) - case .file(let file): - // we need to process JSON files only - if file.url.pathExtension.lowercased() == "json" { - do { - let data = try Data(contentsOf: file.url) - renderNode = try RenderNode.decode(fromJSON: data) - } catch { - let diagnostic = Diagnostic(source: file.url, - severity: .warning, - range: nil, - identifier: "org.swift.docc", - summary: "Invalid file found while indexing content: \(error.localizedDescription)") - let problem = Problem(diagnostic: diagnostic, possibleSolutions: []) - problems.append(problem) - } - } - } - queue.removeFirst() - } - - return renderNode - } - - /// Get the problems that happened during the process. - /// - Returns: An array with the problems encountered during the filesystem read of render nodes. - public func getProblems() -> [Problem] { - return problems - } -} diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift index 53b4e99829..a712b13520 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,17 +11,6 @@ public import Foundation import Crypto -/// A protocol to provide data to be indexed. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") -public protocol RenderNodeProvider { - /// Get an instance of `RenderNode` to be processed by the index. - /// - Note: Returning `nil` will end the indexing process. - func getRenderNode() -> RenderNode? - - /// Returns an array of `Problem` indicating which problems the `Provider` encountered. - func getProblems() -> [Problem] -} - /** A `NavigatorIndex` contains all the necessary information to display the data inside a navigator. The data ranges from the tree to the necessary pieces of information to filter the content and perform actions in a fast way. @@ -230,12 +219,12 @@ public class NavigatorIndex { } /** - Initialize an `NavigatorIndex` from a given path with an empty tree. + Initialize a `NavigatorIndex` from a given path with an empty tree. - Parameter url: The URL pointing to the path from which the index should be read. - Parameter bundleIdentifier: The name of the bundle the index is referring to. - - Note: Don't exposed this initializer as it's used **ONLY** for building an index. + - Note: Don't expose this initializer as it's used **ONLY** for building an index. */ fileprivate init(withEmptyTree url: URL, bundleIdentifier: String) throws { self.url = url @@ -375,14 +364,14 @@ public class NavigatorIndex { Read a tree on disk from a given path. The read is atomically performed, which means it reads all the content of the file from the disk and process the tree from loaded data. The queue is used to load the data for a given timeout period, after that, the queue is used to schedule another read after a given delay. - This approach ensures that the used queue doesn't stall while loading the content from the disk keeping the used queue responsive. + This approach ensures that the used queue doesn't stall while loading the content from the disk keeping the used queue responsive. - Parameters: - - timeout: The amount of time we can load a batch of items from data, once the timeout time pass, + - timeout: The duration for which we can load a batch of items from data. Once the timeout duration passes, the reading process will reschedule asynchronously using the given queue. - - delay: The delay to wait before schedule the next read. Default: 0.01 seconds. + - delay: The duration to wait for before scheduling the next read. Default: 0.01 seconds. - queue: The queue to use. - - broadcast: The callback to update get updates of the current process. + - broadcast: The callback to receive updates on the status of the current process. - Note: Do not access the navigator tree root node or the map from identifier to node from a different thread than the one the queue is using while the read is performed, this may cause data inconsistencies. For that please use the broadcast callback that notifies which items have been loaded. @@ -466,6 +455,17 @@ extension NavigatorIndex { self.fragment = fragment self.languageIdentifier = languageIdentifier } + + /// Compare an identifier with another one, ignoring the identifier language. + /// + /// Used when curating cross-language references in multi-language frameworks. + /// + /// - Parameter other: The other identifier to compare with. + func isEquivalentIgnoringLanguage(to other: Identifier) -> Bool { + return self.bundleIdentifier == other.bundleIdentifier && + self.path == other.path && + self.fragment == other.fragment + } } /** @@ -477,14 +477,6 @@ extension NavigatorIndex { */ open class Builder { - /// The data provider. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - public var renderNodeProvider: (any RenderNodeProvider)? { - _renderNodeProvider as! (any RenderNodeProvider)? - } - // This property only exist to be able to assign `nil` to `renderNodeProvider` in the new initializer without causing a deprecation warning. - private let _renderNodeProvider: Any? - /// The documentation archive to build an index from. public let archiveURL: URL? @@ -579,20 +571,6 @@ extension NavigatorIndex { /// - usePageTitle: Configure the builder to use the "page title" instead of the "navigator title" as the title for each entry. public init(archiveURL: URL? = nil, outputURL: URL, bundleIdentifier: String, sortRootChildrenByName: Bool = false, groupByLanguage: Bool = false, writePathsOnDisk: Bool = true, usePageTitle: Bool = false) { self.archiveURL = archiveURL - self._renderNodeProvider = nil - self.outputURL = outputURL - self.bundleIdentifier = bundleIdentifier - self.sortRootChildrenByName = sortRootChildrenByName - self.groupByLanguage = groupByLanguage - self.writePathsOnDisk = writePathsOnDisk - self.usePageTitle = usePageTitle - } - - @available(*, deprecated, renamed: "init(archiveURL:outputURL:bundleIdentifier:sortRootChildrenByName:groupByLanguage:writePathsOnDisk:usePageTitle:)", message: "Use 'init(archiveURL:outputURL:bundleIdentifier:sortRootChildrenByName:groupByLanguage:writePathsOnDisk:usePageTitle:)' instead. This deprecated API will be removed after 6.2 is released") - @_disfavoredOverload - public init(renderNodeProvider: (any RenderNodeProvider)? = nil, outputURL: URL, bundleIdentifier: String, sortRootChildrenByName: Bool = false, groupByLanguage: Bool = false, writePathsOnDisk: Bool = true, usePageTitle: Bool = false) { - self._renderNodeProvider = renderNodeProvider - self.archiveURL = nil self.outputURL = outputURL self.bundleIdentifier = bundleIdentifier self.sortRootChildrenByName = sortRootChildrenByName @@ -630,6 +608,28 @@ extension NavigatorIndex { } } + /// Index a single render `ExternalRenderNode`. + /// - Parameter renderNode: The render node to be indexed. + package func index(renderNode: ExternalRenderNode, ignoringLanguage: Bool = false) throws { + let navigatorRenderNode = NavigatorExternalRenderNode(renderNode: renderNode) + _ = try index(navigatorRenderNode, traits: nil, isExternal: true) + guard renderNode.identifier.sourceLanguage != .objectiveC else { + return + } + // Check if the render node has an Objective-C representation + guard let objCVariantTrait = renderNode.variants?.flatMap(\.traits).first(where: { trait in + switch trait { + case .interfaceLanguage(let language): + return InterfaceLanguage.from(string: language) == .objc + } + }) else { + return + } + // If this external render node has a variant, we create a "view" into its Objective-C specific data and index that. + let objVariantView = NavigatorExternalRenderNode(renderNode: renderNode, trait: objCVariantTrait) + _ = try index(objVariantView, traits: [objCVariantTrait], isExternal: true) + } + /// Index a single render `RenderNode`. /// - Parameter renderNode: The render node to be indexed. /// - Parameter ignoringLanguage: Whether language variants should be ignored when indexing this render node. @@ -684,7 +684,7 @@ extension NavigatorIndex { } // The private index implementation which indexes a given render node representation - private func index(_ renderNode: any NavigatorIndexableRenderNodeRepresentation, traits: [RenderNode.Variant.Trait]?) throws -> InterfaceLanguage? { + private func index(_ renderNode: any NavigatorIndexableRenderNodeRepresentation, traits: [RenderNode.Variant.Trait]?, isExternal external: Bool = false) throws -> InterfaceLanguage? { guard let navigatorIndex else { throw Error.navigatorIndexIsNil } @@ -781,7 +781,9 @@ extension NavigatorIndex { title: title, platformMask: platformID, availabilityID: UInt64(availabilityID), - icon: renderNode.icon + icon: renderNode.icon, + isExternal: external, + isBeta: renderNode.metadata.isBeta ) navigationItem.path = identifierPath @@ -812,7 +814,8 @@ extension NavigatorIndex { languageID: language.mask, title: title, platformMask: platformID, - availabilityID: UInt64(Self.availabilityIDWithNoAvailabilities) + availabilityID: UInt64(Self.availabilityIDWithNoAvailabilities), + isExternal: external ) groupItem.path = identifier.path + "#" + fragment @@ -892,7 +895,7 @@ extension NavigatorIndex { /// - emitJSONRepresentation: Whether or not a JSON representation of the index should /// be written to disk. /// - /// Defaults to `false`. + /// Defaults to `true`. /// /// - emitLMDBRepresentation: Whether or not an LMDB representation of the index should /// written to disk. @@ -925,7 +928,7 @@ extension NavigatorIndex { let (nodeID, parent) = nodesMultiCurated[index] let placeholders = identifierToChildren[nodeID]! for reference in placeholders { - if let child = identifierToNode[reference] { + if let child = identifierToNode[reference] ?? externalNonSymbolNode(for: reference) { parent.add(child: child) pendingUncuratedReferences.remove(reference) if !multiCurated.keys.contains(reference) && reference.fragment == nil { @@ -946,7 +949,7 @@ extension NavigatorIndex { for (nodeIdentifier, placeholders) in identifierToChildren { for reference in placeholders { let parent = identifierToNode[nodeIdentifier]! - if let child = identifierToNode[reference] { + if let child = identifierToNode[reference] ?? externalNonSymbolNode(for: reference) { let needsCopy = multiCurated[reference] != nil parent.add(child: (needsCopy) ? child.copy() : child) pendingUncuratedReferences.remove(reference) @@ -976,14 +979,12 @@ extension NavigatorIndex { // curation, then they should not be in the navigator. In addition, treat unknown // page types as symbol nodes on the assumption that an unknown page type is a // symbol kind added in a future version of Swift-DocC. - if let node = identifierToNode[nodeID], PageType(rawValue: node.item.pageType)?.isSymbolKind == false { + // Finally, don't add external references to the root; if they are not referenced within the navigation tree, they should be dropped altogether. + if let node = identifierToNode[nodeID], PageType(rawValue: node.item.pageType)?.isSymbolKind == false, !node.item.isExternal { // If an uncurated page has been curated in another language, don't add it to the top-level. if curatedReferences.contains(where: { curatedNodeID in - // Compare all the identifier's properties for equality, except for its language. - curatedNodeID.bundleIdentifier == nodeID.bundleIdentifier - && curatedNodeID.path == nodeID.path - && curatedNodeID.fragment == nodeID.fragment + curatedNodeID.isEquivalentIgnoringLanguage(to: nodeID) }) { continue } @@ -1263,21 +1264,31 @@ extension NavigatorIndex { problem = Problem(diagnostic: diagnostic, possibleSolutions: []) problems.append(problem) } - + + /// Find an external node for the reference that is not of a symbol kind. The source language + /// of the reference is ignored during this lookup since the reference assumes the target node + /// to be of the same language as the page that it is curated in. This may or may not be true + /// since non-symbol kinds (articles, tutorials, etc.) are not tied to a language. + // This is a workaround for https://github.com/swiftlang/swift-docc/issues/240. + // FIXME: This should ideally be solved by making the article language-agnostic rather + // than accomodating the "Swift" language and special-casing for non-symbol nodes. + func externalNonSymbolNode(for reference: NavigatorIndex.Identifier) -> NavigatorTree.Node? { + identifierToNode + .first { identifier, node in + identifier.isEquivalentIgnoringLanguage(to: reference) + && PageType.init(rawValue: node.item.pageType)?.isSymbolKind == false + && node.item.isExternal + }?.value + } /// Build the index using the render nodes files in the provided documentation archive. /// - Returns: A list containing all the errors encountered during indexing. - /// - Precondition: Either ``archiveURL`` or ``renderNodeProvider`` is set. + /// - Precondition: ``archiveURL`` is set. public func build() -> [Problem] { - if let archiveURL { - return _build(archiveURL: archiveURL) - } else { - return (self as (any _DeprecatedRenderNodeProviderAccess))._legacyBuild() + guard let archiveURL else { + fatalError("Calling `build()` requires that `archiveURL` is set.") } - } - - // After 6.2 is released, move this into `build()`. - private func _build(archiveURL: URL) -> [Problem] { + setup() let dataDirectory = archiveURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName, isDirectory: true) @@ -1298,27 +1309,6 @@ extension NavigatorIndex { return problems } - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - fileprivate func _legacyBuild() -> [Problem] { - precondition(renderNodeProvider != nil, "Calling `build()` without an `archiveURL` or `renderNodeProvider` set is not permitted.") - - setup() - - while let renderNode = renderNodeProvider!.getRenderNode() { - do { - try index(renderNode: renderNode) - } catch { - problems.append(error.problem(source: renderNode.identifier.url, - severity: .warning, - summaryPrefix: "RenderNode indexing process failed")) - } - } - - finalize() - - return problems - } - func availabilityEntryIDs(for availabilityID: UInt64) -> [Int]? { return availabilityIDs[Int(availabilityID)] } @@ -1391,9 +1381,3 @@ enum PathHasher: String { } } } - -private protocol _DeprecatedRenderNodeProviderAccess { - // This private function accesses the deprecated RenderNodeProvider - func _legacyBuild() -> [Problem] -} -extension NavigatorIndex.Builder: _DeprecatedRenderNodeProviderAccess {} diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorItem.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorItem.swift index 78d281dd04..8107665a89 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorItem.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorItem.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -49,6 +49,16 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString var icon: RenderReferenceIdentifier? = nil + /// A value that indicates whether this item is built for a beta platform. + /// + /// This value is `false` if the referenced item is not a symbol. + var isBeta: Bool = false + + /// Whether the item has originated from an external reference. + /// + /// Used for determining whether stray navigation items should remain part of the final navigator. + var isExternal: Bool = false + /** Initialize a `NavigatorItem` with the given data. @@ -61,7 +71,7 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString - path: The path to load the content. - icon: A reference to a custom image for this navigator item. */ - init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, path: String, icon: RenderReferenceIdentifier? = nil) { + init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, path: String, icon: RenderReferenceIdentifier? = nil, isExternal: Bool = false, isBeta: Bool = false) { self.pageType = pageType self.languageID = languageID self.title = title @@ -69,6 +79,8 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString self.availabilityID = availabilityID self.path = path self.icon = icon + self.isExternal = isExternal + self.isBeta = isBeta } /** @@ -81,14 +93,18 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString - platformMask: The mask indicating for which platform the page is available. - availabilityID: The identifier of the availability information of the page. - icon: A reference to a custom image for this navigator item. + - isExternal: A flag indicating whether the navigator item belongs to an external documentation archive. + - isBeta: A flag indicating whether the navigator item is in beta. */ - public init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, icon: RenderReferenceIdentifier? = nil) { + public init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, icon: RenderReferenceIdentifier? = nil, isExternal: Bool = false, isBeta: Bool = false) { self.pageType = pageType self.languageID = languageID self.title = title self.platformMask = platformMask self.availabilityID = availabilityID self.icon = icon + self.isExternal = isExternal + self.isBeta = isBeta } // MARK: - Serialization and Deserialization @@ -96,7 +112,7 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString /** Initialize a `NavigatorItem` using raw data. - - Parameters rawValue: The `Data` from which the instance should be deserialized from. + - Parameter rawValue: The `Data` from which the instance should be deserialized from. */ required public init?(rawValue: Data) { let data = rawValue @@ -130,8 +146,27 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString let pathData = data[cursor...stride + // To ensure backwards compatibility, handle both when `isBeta` and `isExternal` has been encoded and when it hasn't + if cursor < data.count { + // Encoded `isBeta` + assert(cursor + length <= data.count, "The serialized data is malformed: `isBeta` value should not extend past the end of the data") + let betaValue: UInt8 = unpackedValueFromData(data[cursor.. = [ .framework, .class, .structure, .enumeration, .protocol, .typeAlias, .associatedType, .extension ] diff --git a/Sources/SwiftDocC/Indexing/RenderIndexJSON/RenderIndex.swift b/Sources/SwiftDocC/Indexing/RenderIndexJSON/RenderIndex.swift index 060c204b86..19e4d476c8 100644 --- a/Sources/SwiftDocC/Indexing/RenderIndexJSON/RenderIndex.swift +++ b/Sources/SwiftDocC/Indexing/RenderIndexJSON/RenderIndex.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022-2024 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -86,7 +86,16 @@ public struct RenderIndex: Codable, Equatable { /// - Parameter named: The name of the new root node public mutating func insertRoot(named: String) { for (languageID, nodes) in interfaceLanguages { - let root = Node(title: named, path: "/documentation", pageType: .framework, isDeprecated: false, children: nodes, icon: nil) + let root = Node( + title: named, + path: "/documentation", + pageType: .framework, + isDeprecated: false, + isExternal: false, + isBeta: false, + children: nodes, + icon: nil + ) interfaceLanguages[languageID] = [root] } } @@ -214,7 +223,7 @@ extension RenderIndex { public init( title: String, path: String?, - type: String, + type: String?, children: [Node]?, isDeprecated: Bool, isExternal: Bool, @@ -236,6 +245,8 @@ extension RenderIndex { path: String, pageType: NavigatorIndex.PageType?, isDeprecated: Bool, + isExternal: Bool, + isBeta: Bool, children: [Node], icon: RenderReferenceIdentifier? ) { @@ -243,12 +254,9 @@ extension RenderIndex { self.children = children.isEmpty ? nil : children self.isDeprecated = isDeprecated - - // Currently Swift-DocC doesn't support resolving links to external DocC archives - // so we default to `false` here. - self.isExternal = false - - self.isBeta = false + self.isExternal = isExternal + self.isBeta = isBeta + self.icon = icon guard let pageType else { @@ -318,6 +326,8 @@ extension RenderIndex.Node { path: node.item.path, pageType: NavigatorIndex.PageType(rawValue: node.item.pageType), isDeprecated: isDeprecated, + isExternal: node.item.isExternal, + isBeta: node.item.isBeta, children: node.children.map { RenderIndex.Node.fromNavigatorTreeNode($0, in: navigatorIndex, with: builder) }, diff --git a/Sources/SwiftDocC/Infrastructure/Bundle Assets/DataAssetManager.swift b/Sources/SwiftDocC/Infrastructure/Bundle Assets/DataAssetManager.swift index ac932b1a36..c36f2542dc 100644 --- a/Sources/SwiftDocC/Infrastructure/Bundle Assets/DataAssetManager.swift +++ b/Sources/SwiftDocC/Infrastructure/Bundle Assets/DataAssetManager.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -346,10 +346,6 @@ fileprivate extension NSRegularExpression { public struct AssetReference: Hashable, Codable { /// The name of the asset. public var assetName: String - @available(*, deprecated, renamed: "bundleID", message: "Use 'bundleID' instead. This deprecated API will be removed after 6.2 is released") - public var bundleIdentifier: String { - bundleID.rawValue - } /// The identifier of the bundle the asset is apart of. public let bundleID: DocumentationBundle.Identifier @@ -359,11 +355,4 @@ public struct AssetReference: Hashable, Codable { self.assetName = assetName self.bundleID = bundleID } - @available(*, deprecated, renamed: "init(assetName:bundleID:)", message: "Use 'init(assetName:bundleID:)' instead. This deprecated API will be removed after 6.2 is released") - public init(assetName: String, bundleIdentifier: String) { - self.init( - assetName: assetName, - bundleID: .init(rawValue: bundleIdentifier) - ) - } } diff --git a/Sources/SwiftDocC/Infrastructure/Context/Deprecated/DocumentationContext+Deprecated.swift b/Sources/SwiftDocC/Infrastructure/Context/Deprecated/DocumentationContext+Deprecated.swift deleted file mode 100644 index 97685f8a19..0000000000 --- a/Sources/SwiftDocC/Infrastructure/Context/Deprecated/DocumentationContext+Deprecated.swift +++ /dev/null @@ -1,47 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2024 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 -*/ - -extension DocumentationContext { - - @available(*, deprecated, renamed: "configuration.externalMetadata", message: "Use 'configuration.externalMetadata' instead. This deprecated API will be removed after Swift 6.2 is released.") - public var externalMetadata: ExternalMetadata { - get { configuration.externalMetadata } - set { configuration.externalMetadata = newValue } - } - - @available(*, deprecated, renamed: "configuration.externalDocumentationConfiguration.sources", message: "Use 'configuration.externalDocumentationConfiguration.sources' instead. This deprecated API will be removed after Swift 6.2 is released.") - public var externalDocumentationSources: [BundleIdentifier: any ExternalDocumentationSource] { - get { - var result = [BundleIdentifier: any ExternalDocumentationSource]() - for (key, value) in configuration.externalDocumentationConfiguration.sources { - result[key.rawValue] = value - } - return result - } - set { - configuration.externalDocumentationConfiguration.sources.removeAll() - for (key, value) in newValue { - configuration.externalDocumentationConfiguration.sources[.init(rawValue: key)] = value - } - } - } - - @available(*, deprecated, renamed: "configuration.externalDocumentationConfiguration.globalSymbolResolver", message: "Use 'configuration.externalDocumentationConfiguration.globalSymbolResolver' instead. This deprecated API will be removed after Swift 6.2 is released.") - public var globalExternalSymbolResolver: (any GlobalExternalSymbolResolver)? { - get { configuration.externalDocumentationConfiguration.globalSymbolResolver } - set { configuration.externalDocumentationConfiguration.globalSymbolResolver = newValue } - } - - @available(*, deprecated, renamed: "configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences", message: "Use 'configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences' instead. This deprecated API will be removed after Swift 6.2 is released.") - public var shouldStoreManuallyCuratedReferences: Bool { - get { configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences } - set { configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences = newValue } - } -} diff --git a/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift b/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift index 2d348d425d..c18d64004b 100644 --- a/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift +++ b/Sources/SwiftDocC/Infrastructure/ConvertActionConverter.swift @@ -21,10 +21,9 @@ package enum ConvertActionConverter { static package let signposter = NoOpSignposterShim() #endif - /// Converts the documentation bundle in the given context and passes its output to a given consumer. + /// Converts the documentation in the given context and passes its output to a given consumer. /// /// - Parameters: - /// - bundle: The documentation bundle to convert. /// - context: The context that the bundle is a part of. /// - outputConsumer: The consumer that the conversion passes outputs of the conversion to. /// - sourceRepository: The source repository where the documentation's sources are hosted. @@ -32,9 +31,8 @@ package enum ConvertActionConverter { /// - documentationCoverageOptions: The level of experimental documentation coverage information that the conversion should pass to the consumer. /// - Returns: A list of problems that occurred during the conversion (excluding the problems that the context already encountered). package static func convert( - bundle: DocumentationBundle, context: DocumentationContext, - outputConsumer: some ConvertOutputConsumer, + outputConsumer: some ConvertOutputConsumer & ExternalNodeConsumer, sourceRepository: SourceRepository?, emitDigest: Bool, documentationCoverageOptions: DocumentationCoverageOptions @@ -61,15 +59,14 @@ package enum ConvertActionConverter { // Precompute the render context let renderContext = signposter.withIntervalSignpost("Build RenderContext", id: signposter.makeSignpostID()) { - RenderContext(documentationContext: context, bundle: bundle) + RenderContext(documentationContext: context) } try outputConsumer.consume(renderReferenceStore: renderContext.store) // Copy images, sample files, and other static assets. - try outputConsumer.consume(assetsInBundle: bundle) + try outputConsumer.consume(assetsInBundle: context.inputs) let converter = DocumentationContextConverter( - bundle: bundle, context: context, renderContext: renderContext, sourceRepository: sourceRepository @@ -103,7 +100,7 @@ package enum ConvertActionConverter { let resultsSyncQueue = DispatchQueue(label: "Convert Serial Queue", qos: .unspecified, attributes: []) let resultsGroup = DispatchGroup() - + let renderSignpostHandle = signposter.beginInterval("Render", id: signposter.makeSignpostID(), "Render \(context.knownPages.count) pages") var conversionProblems: [Problem] = context.knownPages.concurrentPerform { identifier, results in @@ -166,7 +163,27 @@ package enum ConvertActionConverter { signposter.endInterval("Render", renderSignpostHandle) guard !Task.isCancelled else { return [] } - + + // Consumes all external links and adds them into the sidebar. + // This consumes all external links referenced across all content, and indexes them so they're available for reference in the navigator. + // This is not ideal as it means that links outside of the Topics section can impact the content of the navigator. + // TODO: It would be more correct to only index external links which have been curated as part of the Topics section. + // + // This has to run after all local nodes have been indexed because we're associating the external node with the **local** documentation's identifier, + // which makes it possible for there be clashes between local and external render nodes. + // When there are duplicate nodes, only the first one will be indexed, + // so in order to prefer local entities whenever there are any clashes, we have to index external nodes second. + // TODO: External render nodes should be associated with the correct documentation identifier. + try signposter.withIntervalSignpost("Index external links", id: signposter.makeSignpostID()) { + for externalLink in context.externalCache { + // Here we're associating the external node with the **local** documentation's identifier. + // This is needed because nodes are only considered children if the parent and child's identifier match. + // Otherwise, the node will be considered as a separate root node and displayed separately. + let externalRenderNode = ExternalRenderNode(externalEntity: externalLink.value, bundleIdentifier: context.inputs.id) + try outputConsumer.consume(externalRenderNode: externalRenderNode) + } + } + // Write various metadata if emitDigest { signposter.withIntervalSignpost("Emit digest", id: signposter.makeSignpostID()) { @@ -183,7 +200,7 @@ package enum ConvertActionConverter { if FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled { signposter.withIntervalSignpost("Serialize link hierarchy", id: signposter.makeSignpostID()) { do { - let serializableLinkInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: bundle.id) + let serializableLinkInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: context.inputs.id) try outputConsumer.consume(linkResolutionInformation: serializableLinkInformation) if !emitDigest { @@ -216,7 +233,7 @@ package enum ConvertActionConverter { break } - try outputConsumer.consume(buildMetadata: BuildMetadata(bundleDisplayName: bundle.displayName, bundleID: bundle.id)) + try outputConsumer.consume(buildMetadata: BuildMetadata(bundleDisplayName: context.inputs.displayName, bundleID: context.inputs.id)) // Log the finalized topic graph checksum. benchmark(add: Benchmark.TopicGraphHash(context: context)) diff --git a/Sources/SwiftDocC/Infrastructure/ConvertOutputConsumer.swift b/Sources/SwiftDocC/Infrastructure/ConvertOutputConsumer.swift index afe7a82720..f5e1ebd432 100644 --- a/Sources/SwiftDocC/Infrastructure/ConvertOutputConsumer.swift +++ b/Sources/SwiftDocC/Infrastructure/ConvertOutputConsumer.swift @@ -16,8 +16,8 @@ import Foundation /// or store them in memory. public protocol ConvertOutputConsumer { /// Consumes an array of problems that were generated during a conversion. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - func consume(problems: [Problem]) throws + @available(*, deprecated, message: "This deprecated API will be removed after 6.3 is released") + func _deprecated_consume(problems: [Problem]) throws /// Consumes a render node that was generated during a conversion. /// > Warning: This method might be called concurrently. @@ -62,7 +62,7 @@ public extension ConvertOutputConsumer { // Default implementation so that conforming types don't need to implement deprecated API. public extension ConvertOutputConsumer { - func consume(problems: [Problem]) throws {} + func _deprecated_consume(problems: [Problem]) throws {} } // A package-internal protocol that callers can cast to when they need to call `_consume(problems:)` for backwards compatibility (until `consume(problems:)` is removed). @@ -84,7 +84,7 @@ package struct _Deprecated: _DeprecatedConsumeP } // This needs to be deprecated to be able to call `consume(problems:)` without a deprecation warning. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") + @available(*, deprecated, message: "This deprecated API will be removed after 6.3 is released") package func _consume(problems: [Problem]) throws { var problems = problems @@ -94,7 +94,7 @@ package struct _Deprecated: _DeprecatedConsumeP severity: .warning, identifier: "org.swift.docc.DeprecatedDiagnosticsDigets", summary: """ - The 'diagnostics.json' digest file is deprecated and will be removed after 6.2 is released. \ + The 'diagnostics.json' digest file is deprecated and will be removed after 6.3 is released. \ Pass a `--diagnostics-file ` to specify a custom location where DocC will write a diagnostics JSON file with more information. """) ), @@ -102,6 +102,15 @@ package struct _Deprecated: _DeprecatedConsumeP ) } - try consumer.consume(problems: problems) + try consumer._deprecated_consume(problems: problems) } } + +/// A consumer for nodes generated from external references. +/// +/// Types that conform to this protocol manage what to do with external references, for example index them. +package protocol ExternalNodeConsumer { + /// Consumes a external render node that was generated during a conversion. + /// > Warning: This method might be called concurrently. + func consume(externalRenderNode: ExternalRenderNode) throws +} diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationBundle.swift b/Sources/SwiftDocC/Infrastructure/DocumentationBundle.swift index 353559d890..11cc3ef426 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationBundle.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationBundle.swift @@ -57,25 +57,10 @@ public struct DocumentationBundle { info.displayName } - @available(*, deprecated, renamed: "id", message: "Use 'id' instead. This deprecated API will be removed after 6.2 is released") - public var identifier: String { - id.rawValue - } - /// The documentation bundle's stable and locally unique identifier. public var id: DocumentationBundle.Identifier { info.id } - - /** - The documentation bundle's version. - - It's not safe to make computations based on assumptions about the format of bundle's version. The version can be in any format. - */ - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - public var version: String? { - info.version - } /// Symbol graph JSON input files for the module that's represented by this unit of documentation. /// @@ -152,19 +137,9 @@ public struct DocumentationBundle { /// Default path to resolve symbol links. public private(set) var documentationRootReference: ResolvedTopicReference - @available(*, deprecated, renamed: "tutorialTableOfContentsContainer", message: "Use 'tutorialTableOfContentsContainer' instead. This deprecated API will be removed after 6.2 is released") - public var tutorialsRootReference: ResolvedTopicReference { - tutorialTableOfContentsContainer - } - /// Default path to resolve tutorial table-of-contents links. public var tutorialTableOfContentsContainer: ResolvedTopicReference - @available(*, deprecated, renamed: "tutorialsContainerReference", message: "Use 'tutorialsContainerReference' instead. This deprecated API will be removed after 6.2 is released") - public var technologyTutorialsRootReference: ResolvedTopicReference { - tutorialsContainerReference - } - /// Default path to resolve tutorial links. public var tutorialsContainerReference: ResolvedTopicReference diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index 224bd5f287..dd0c3dc5c7 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -12,56 +12,6 @@ public import Foundation import Markdown import SymbolKit -/// A type that provides information about documentation bundles and their content. -@available(*, deprecated, message: "Pass the context its inputs at initialization instead. This deprecated API will be removed after 6.2 is released") -public protocol DocumentationContextDataProvider { - /// An object to notify when bundles are added or removed. - var delegate: (any DocumentationContextDataProviderDelegate)? { get set } - - /// The documentation bundles that this data provider provides. - var bundles: [BundleIdentifier: DocumentationBundle] { get } - - /// Returns the data for the specified `url` in the provided `bundle`. - /// - /// - Parameters: - /// - url: The URL of the file to read. - /// - bundle: The bundle that the file is a part of. - /// - /// - Throws: When the file cannot be found in the workspace. - func contentsOfURL(_ url: URL, in bundle: DocumentationBundle) throws -> Data -} - -/// An object that responds to changes in available documentation bundles for a specific provider. -@available(*, deprecated, message: "Pass the context its inputs at initialization instead. This deprecated API will be removed after 6.2 is released") -public protocol DocumentationContextDataProviderDelegate: AnyObject { - - /// Called when the `dataProvider` has added a new documentation bundle to its list of `bundles`. - /// - /// - Parameters: - /// - dataProvider: The provider that added this bundle. - /// - bundle: The bundle that was added. - /// - /// - Note: This method is called after the `dataProvider` has been added the bundle to its `bundles` property. - func dataProvider(_ dataProvider: any DocumentationContextDataProvider, didAddBundle bundle: DocumentationBundle) throws - - /// Called when the `dataProvider` has removed a documentation bundle from its list of `bundles`. - /// - /// - Parameters: - /// - dataProvider: The provider that removed this bundle. - /// - bundle: The bundle that was removed. - /// - /// - Note: This method is called after the `dataProvider` has been removed the bundle from its `bundles` property. - func dataProvider(_ dataProvider: any DocumentationContextDataProvider, didRemoveBundle bundle: DocumentationBundle) throws -} - -/// Documentation bundles use a string value as a unique identifier. -/// -/// This value is typically a reverse host name, for example: `com..`. -/// -/// Documentation links may include the bundle identifier---as a host component of the URL---to reference content in a specific documentation bundle. -@available(*, deprecated, renamed: "DocumentationBundle.Identifier", message: "Use 'DocumentationBundle.Identifier' instead. This deprecated API will be removed after 6.2 is released") -public typealias BundleIdentifier = String - /// The documentation context manages the in-memory model for the built documentation. /// /// A ``DocumentationWorkspace`` discovers serialized documentation bundles from a variety of sources (files on disk, databases, or web services), provides them to the `DocumentationContext`, @@ -116,68 +66,30 @@ public class DocumentationContext { /// A class that resolves documentation links by orchestrating calls to other link resolver implementations. public var linkResolver: LinkResolver - - private enum _Provider { - @available(*, deprecated, message: "Use 'DataProvider' instead. This deprecated API will be removed after 6.2 is released") - case legacy(any DocumentationContextDataProvider) - case new(any DataProvider) - } - private var dataProvider: _Provider - /// The provider of documentation bundles for this context. - @available(*, deprecated, message: "Use 'DataProvider' instead. This deprecated API will be removed after 6.2 is released") - private var _legacyDataProvider: (any DocumentationContextDataProvider)! { - get { - switch dataProvider { - case .legacy(let legacyDataProvider): - legacyDataProvider - case .new: - nil - } - } - set { - dataProvider = .legacy(newValue) - } - } - - func contentsOfURL(_ url: URL, in bundle: DocumentationBundle) throws -> Data { - switch dataProvider { - case .legacy(let legacyDataProvider): - return try legacyDataProvider.contentsOfURL(url, in: bundle) - case .new(let dataProvider): - assert(self.bundle?.id == bundle.id, "New code shouldn't pass unknown bundle identifiers to 'DocumentationContext.bundle(identifier:)'.") - return try dataProvider.contents(of: url) - } - } + /// The data provider that the context can use to read the contents of files that belong to ``bundle``. + let dataProvider: any DataProvider - /// The documentation bundle that is registered with the context. - var bundle: DocumentationBundle? + /// The collection of input files that the context was created from. + let inputs: DocumentationContext.Inputs /// A collection of configuration for this context. - public package(set) var configuration: Configuration { - get { _configuration } - @available(*, deprecated, message: "Pass a configuration at initialization. This property will become read-only after Swift 6.2 is released.") - set { _configuration = newValue } - } - // Having a deprecated setter above requires a computed property. - private var _configuration: Configuration + public let configuration: Configuration /// The graph of all the documentation content and their relationships to each other. /// /// > Important: The topic graph has no awareness of source language specific edges. var topicGraph = TopicGraph() + /// Will be assigned during context initialization + var snippetResolver: SnippetResolver! + /// User-provided global options for this documentation conversion. var options: Options? /// The set of all manually curated references if `shouldStoreManuallyCuratedReferences` was true at the time of processing and has remained `true` since.. Nil if curation has not been processed yet. public private(set) var manuallyCuratedReferences: Set? - @available(*, deprecated, renamed: "tutorialTableOfContentsReferences", message: "Use 'tutorialTableOfContentsReferences' This deprecated API will be removed after 6.2 is released") - public var rootTechnologies: [ResolvedTopicReference] { - tutorialTableOfContentsReferences - } - /// The tutorial table-of-contents nodes in the topic graph. public var tutorialTableOfContentsReferences: [ResolvedTopicReference] { return topicGraph.nodes.values.compactMap { node in @@ -285,31 +197,6 @@ public class DocumentationContext { /// Mentions of symbols within articles. var articleSymbolMentions = ArticleSymbolMentions() - /// Initializes a documentation context with a given `dataProvider` and registers all the documentation bundles that it provides. - /// - /// - Parameters: - /// - dataProvider: The data provider to register bundles from. - /// - diagnosticEngine: The pre-configured engine that will collect problems encountered during compilation. - /// - configuration: A collection of configuration for the created context. - /// - Throws: If an error is encountered while registering a documentation bundle. - @available(*, deprecated, message: "Pass the context its inputs at initialization instead. This deprecated API will be removed after 6.2 is released") - public init( - dataProvider: any DocumentationContextDataProvider, - diagnosticEngine: DiagnosticEngine = .init(), - configuration: Configuration = .init() - ) throws { - self.dataProvider = .legacy(dataProvider) - self.diagnosticEngine = diagnosticEngine - self._configuration = configuration - self.linkResolver = LinkResolver(dataProvider: FileManager.default) - - _legacyDataProvider.delegate = self - - for bundle in dataProvider.bundles.values { - try register(bundle) - } - } - /// Initializes a documentation context with a given `bundle`. /// /// - Parameters: @@ -319,83 +206,19 @@ public class DocumentationContext { /// - configuration: A collection of configuration for the created context. /// - Throws: If an error is encountered while registering a documentation bundle. package init( - bundle: DocumentationBundle, + bundle inputs: DocumentationBundle, dataProvider: any DataProvider, diagnosticEngine: DiagnosticEngine = .init(), configuration: Configuration = .init() - ) throws { - self.bundle = bundle - self.dataProvider = .new(dataProvider) + ) async throws { + self.inputs = inputs + self.dataProvider = dataProvider self.diagnosticEngine = diagnosticEngine - self._configuration = configuration + self.configuration = configuration self.linkResolver = LinkResolver(dataProvider: dataProvider) - ResolvedTopicReference.enableReferenceCaching(for: bundle.id) - try register(bundle) - } - - /// Respond to a new `bundle` being added to the `dataProvider` by registering it. - /// - /// - Parameters: - /// - dataProvider: The provider that added this bundle. - /// - bundle: The bundle that was added. - @available(*, deprecated, message: "Pass the context its inputs at initialization instead. This deprecated API will be removed after 6.2 is released") - public func dataProvider(_ dataProvider: any DocumentationContextDataProvider, didAddBundle bundle: DocumentationBundle) throws { - try benchmark(wrap: Benchmark.Duration(id: "bundle-registration")) { - // Enable reference caching for this documentation bundle. - ResolvedTopicReference.enableReferenceCaching(for: bundle.id) - - try self.register(bundle) - } - } - - /// Respond to a new `bundle` being removed from the `dataProvider` by unregistering it. - /// - /// - Parameters: - /// - dataProvider: The provider that removed this bundle. - /// - bundle: The bundle that was removed. - @available(*, deprecated, message: "Pass the context its inputs at initialization instead. This deprecated API will be removed after 6.2 is released") - public func dataProvider(_ dataProvider: any DocumentationContextDataProvider, didRemoveBundle bundle: DocumentationBundle) throws { - linkResolver.localResolver?.unregisterBundle(identifier: bundle.id) - - // Purge the reference cache for this bundle and disable reference caching for - // this bundle moving forward. - ResolvedTopicReference.purgePool(for: bundle.id) - - unregister(bundle) - } - - /// The documentation bundles that are currently registered with the context. - @available(*, deprecated, message: "Use 'bundle' instead. This deprecated API will be removed after 6.2 is released") - public var registeredBundles: some Collection { - _registeredBundles - } - - /// Returns the `DocumentationBundle` with the given `identifier` if it's registered with the context, otherwise `nil`. - @available(*, deprecated, message: "Use 'bundle' instead. This deprecated API will be removed after 6.2 is released") - public func bundle(identifier: String) -> DocumentationBundle? { - _bundle(identifier: identifier) - } - - // Remove these when removing `registeredBundles` and `bundle(identifier:)`. - // These exist so that internal code that need to be compatible with legacy data providers can access the bundles without deprecation warnings. - var _registeredBundles: [DocumentationBundle] { - switch dataProvider { - case .legacy(let legacyDataProvider): - Array(legacyDataProvider.bundles.values) - case .new: - bundle.map { [$0] } ?? [] - } - } - - func _bundle(identifier: String) -> DocumentationBundle? { - switch dataProvider { - case .legacy(let legacyDataProvider): - return legacyDataProvider.bundles[identifier] - case .new: - assert(bundle?.id.rawValue == identifier, "New code shouldn't pass unknown bundle identifiers to 'DocumentationContext.bundle(identifier:)'.") - return bundle?.id.rawValue == identifier ? bundle : nil - } + ResolvedTopicReference.enableReferenceCaching(for: inputs.id) + try register() } /// Perform semantic analysis on a given `document` at a given `source` location and append any problems found to `problems`. @@ -403,11 +226,10 @@ public class DocumentationContext { /// - Parameters: /// - document: The document to analyze. /// - source: The location of the document. - /// - bundle: The bundle that the document belongs to. /// - problems: A mutable collection of problems to update with any problem encountered during the semantic analysis. /// - Returns: The result of the semantic analysis. - private func analyze(_ document: Document, at source: URL, in bundle: DocumentationBundle, engine: DiagnosticEngine) -> Semantic? { - var analyzer = SemanticAnalyzer(source: source, bundle: bundle) + private func analyze(_ document: Document, at source: URL, engine: DiagnosticEngine) -> Semantic? { + var analyzer = SemanticAnalyzer(source: source, bundle: inputs) let result = analyzer.visit(document) engine.emit(analyzer.problems) return result @@ -432,12 +254,12 @@ public class DocumentationContext { /// - source: The location of the document. private func check(_ document: Document, at source: URL) { var checker = CompositeChecker([ - AbstractContainsFormattedTextOnly(sourceFile: source).any(), DuplicateTopicsSections(sourceFile: source).any(), InvalidAdditionalTitle(sourceFile: source).any(), MissingAbstract(sourceFile: source).any(), NonOverviewHeadingChecker(sourceFile: source).any(), SeeAlsoInTopicsHeadingChecker(sourceFile: source).any(), + InvalidCodeBlockOption(sourceFile: source).any(), ]) checker.visit(document) diagnosticEngine.emit(checker.problems) @@ -483,12 +305,11 @@ public class DocumentationContext { /// /// - Parameters: /// - references: A list of references to local nodes to visit to collect links. - /// - localBundleID: The local bundle ID, used to identify and skip absolute fully qualified local links. - private func preResolveExternalLinks(references: [ResolvedTopicReference], localBundleID: DocumentationBundle.Identifier) { + private func preResolveExternalLinks(references: [ResolvedTopicReference]) { preResolveExternalLinks(semanticObjects: references.compactMap({ reference -> ReferencedSemanticObject? in guard let node = try? entity(with: reference), let semantic = node.semantic else { return nil } return (reference: reference, semantic: semantic) - }), localBundleID: localBundleID) + })) } /// A tuple of a semantic object and its reference in the topic graph. @@ -505,8 +326,7 @@ public class DocumentationContext { /// /// - Parameters: /// - semanticObjects: A list of semantic objects to visit to collect links. - /// - localBundleID: The local bundle ID, used to identify and skip absolute fully qualified local links. - private func preResolveExternalLinks(semanticObjects: [ReferencedSemanticObject], localBundleID: DocumentationBundle.Identifier) { + private func preResolveExternalLinks(semanticObjects: [ReferencedSemanticObject]) { // If there are no external resolvers added we will not resolve any links. guard !configuration.externalDocumentationConfiguration.sources.isEmpty else { return } @@ -514,7 +334,7 @@ public class DocumentationContext { semanticObjects.concurrentPerform { _, semantic in autoreleasepool { // Walk the node and extract external link references. - var externalLinksCollector = ExternalReferenceWalker(localBundleID: localBundleID) + var externalLinksCollector = ExternalReferenceWalker(localBundleID: inputs.id) externalLinksCollector.visit(semantic) // Avoid any synchronization overhead if there are no references to add. @@ -563,7 +383,7 @@ public class DocumentationContext { /** Attempt to resolve links in curation-only documentation, converting any ``TopicReferences`` from `.unresolved` to `.resolved` where possible. */ - private func resolveLinks(curatedReferences: Set, bundle: DocumentationBundle) { + private func resolveLinks(curatedReferences: Set) { let signpostHandle = signposter.beginInterval("Resolve links", id: signposter.makeSignpostID()) defer { signposter.endInterval("Resolve links", signpostHandle) @@ -609,7 +429,7 @@ public class DocumentationContext { return } - var resolver = ReferenceResolver(context: self, bundle: bundle, rootReference: reference, inheritanceParentReference: symbolOriginReference) + var resolver = ReferenceResolver(context: self, rootReference: reference, inheritanceParentReference: symbolOriginReference) // Update the node with the markup that contains resolved references instead of authored links. documentationNode.semantic = autoreleasepool { @@ -623,7 +443,7 @@ public class DocumentationContext { for alternateRepresentation in alternateRepresentations { let resolutionResult = resolver.resolve( alternateRepresentation.reference, - in: bundle.rootReference, + in: inputs.rootReference, range: alternateRepresentation.originalMarkup.range, severity: .warning ) @@ -720,12 +540,10 @@ public class DocumentationContext { /// - tutorialTableOfContentsResults: The list of temporary 'tutorial table-of-contents' pages. /// - tutorials: The list of temporary 'tutorial' pages. /// - tutorialArticles: The list of temporary 'tutorialArticle' pages. - /// - bundle: The bundle to resolve links against. private func resolveLinks( tutorialTableOfContents tutorialTableOfContentsResults: [SemanticResult], tutorials: [SemanticResult], - tutorialArticles: [SemanticResult], - bundle: DocumentationBundle + tutorialArticles: [SemanticResult] ) { let signpostHandle = signposter.beginInterval("Resolve links", id: signposter.makeSignpostID()) defer { @@ -739,7 +557,7 @@ public class DocumentationContext { for tableOfContentsResult in tutorialTableOfContentsResults { autoreleasepool { let url = tableOfContentsResult.source - var resolver = ReferenceResolver(context: self, bundle: bundle) + var resolver = ReferenceResolver(context: self) let tableOfContents = resolver.visit(tableOfContentsResult.value) as! TutorialTableOfContents diagnosticEngine.emit(resolver.problems) @@ -811,7 +629,7 @@ public class DocumentationContext { autoreleasepool { let url = tutorialResult.source let unresolvedTutorial = tutorialResult.value - var resolver = ReferenceResolver(context: self, bundle: bundle) + var resolver = ReferenceResolver(context: self) let tutorial = resolver.visit(unresolvedTutorial) as! Tutorial diagnosticEngine.emit(resolver.problems) @@ -845,7 +663,7 @@ public class DocumentationContext { autoreleasepool { let url = articleResult.source let unresolvedTutorialArticle = articleResult.value - var resolver = ReferenceResolver(context: self, bundle: bundle) + var resolver = ReferenceResolver(context: self) let article = resolver.visit(unresolvedTutorialArticle) as! TutorialArticle diagnosticEngine.emit(resolver.problems) @@ -876,7 +694,7 @@ public class DocumentationContext { // Articles are resolved in a separate pass } - private func registerDocuments(from bundle: DocumentationBundle) throws -> ( + private func registerDocuments() throws -> ( tutorialTableOfContentsResults: [SemanticResult], tutorials: [SemanticResult], tutorialArticles: [SemanticResult], @@ -896,11 +714,11 @@ public class DocumentationContext { let decodeError = Synchronized<(any Error)?>(nil) // Load and analyze documents concurrently - let analyzedDocuments: [(URL, Semantic)] = bundle.markupURLs.concurrentPerform { url, results in + let analyzedDocuments: [(URL, Semantic)] = inputs.markupURLs.concurrentPerform { url, results in guard decodeError.sync({ $0 == nil }) else { return } do { - let data = try contentsOfURL(url, in: bundle) + let data = try dataProvider.contents(of: url) let source = String(decoding: data, as: UTF8.self) let document = Document(parsing: source, source: url, options: [.parseBlockDirectives, .parseSymbolLinks]) @@ -911,7 +729,7 @@ public class DocumentationContext { diagnosticEngine.emit(langChecker.problems) } - guard let analyzed = analyze(document, at: url, in: bundle, engine: diagnosticEngine) else { + guard let analyzed = analyze(document, at: url, engine: diagnosticEngine) else { return } @@ -938,8 +756,8 @@ public class DocumentationContext { // Store the references we encounter to ensure they're unique. The file name is currently the only part of the URL considered for the topic reference, so collisions may occur. let (url, analyzed) = analyzedDocument - let path = NodeURLGenerator.pathForSemantic(analyzed, source: url, bundle: bundle) - let reference = ResolvedTopicReference(bundleID: bundle.id, path: path, sourceLanguage: .swift) + let path = NodeURLGenerator.pathForSemantic(analyzed, source: url, bundle: inputs) + var reference = ResolvedTopicReference(bundleID: inputs.id, path: path, sourceLanguage: .swift) // Since documentation extensions' filenames have no impact on the URL of pages, there is no need to enforce unique filenames for them. // At this point we consider all articles with an H1 containing link a "documentation extension." @@ -993,7 +811,11 @@ public class DocumentationContext { insertLandmarks(tutorialArticle.landmarks, from: topicGraphNode, source: url) } else if let article = analyzed as? Article { - + // If the article contains any `@SupportedLanguage` directives in the metadata, + // include those languages in the set of source languages for the reference. + if let supportedLanguages = article.supportedLanguages { + reference = reference.withSourceLanguages(supportedLanguages) + } // Here we create a topic graph node with the prepared data but we don't add it to the topic graph just yet // because we don't know where in the hierarchy the article belongs, we will add it later when crawling the manual curation via Topics task groups. let topicGraphNode = TopicGraph.Node(reference: reference, kind: .article, source: .file(url: url), title: article.title!.plainText) @@ -1063,8 +885,7 @@ public class DocumentationContext { private func nodeWithInitializedContent( reference: ResolvedTopicReference, - match foundDocumentationExtension: DocumentationContext.SemanticResult
?, - bundle: DocumentationBundle + match foundDocumentationExtension: DocumentationContext.SemanticResult
? ) -> DocumentationNode { guard var updatedNode = documentationCache[reference] else { fatalError("A topic reference that has already been resolved should always exist in the cache.") @@ -1074,7 +895,7 @@ public class DocumentationContext { updatedNode.initializeSymbolContent( documentationExtension: foundDocumentationExtension?.value, engine: diagnosticEngine, - bundle: bundle + bundle: inputs ) // After merging the documentation extension into the symbol, warn about deprecation summary for non-deprecated symbols. @@ -1165,12 +986,8 @@ public class DocumentationContext { diagnosticEngine.emit(result.problems) } - /// Loads all graph files from a given `bundle` and merges them together while building the symbol relationships and loading any available markdown documentation for those symbols. - /// - /// - Parameter bundle: The bundle to load symbol graph files from. - /// - Returns: A pair of the references to all loaded modules and the hierarchy of all the loaded symbol's references. + /// Loads all graph files from the context's inputs and merges them together while building the symbol relationships and loading any available markdown documentation for those symbols. private func registerSymbols( - from bundle: DocumentationBundle, symbolGraphLoader: SymbolGraphLoader, documentationExtensions: [SemanticResult
] ) throws { @@ -1192,7 +1009,7 @@ public class DocumentationContext { // Build references for all symbols in all of this module's symbol graphs. let symbolReferences = signposter.withIntervalSignpost("Disambiguate references") { - linkResolver.localResolver.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self) + linkResolver.localResolver.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, context: self) } // Set the index and cache storage capacity to avoid ad-hoc storage resizing. @@ -1247,7 +1064,7 @@ public class DocumentationContext { // Use the default module kind for this bundle if one was provided, // otherwise fall back to 'Framework' - let moduleKindDisplayName = bundle.info.defaultModuleKind ?? "Framework" + let moduleKindDisplayName = inputs.info.defaultModuleKind ?? "Framework" let moduleSymbol = SymbolGraph.Symbol( identifier: moduleIdentifier, names: SymbolGraph.Symbol.Names(title: moduleName, navigator: nil, subHeading: nil, prose: nil), @@ -1257,7 +1074,7 @@ public class DocumentationContext { kind: SymbolGraph.Symbol.Kind(parsedIdentifier: .module, displayName: moduleKindDisplayName), mixins: [:]) let moduleSymbolReference = SymbolReference(moduleName, interfaceLanguages: moduleInterfaceLanguages, defaultSymbol: moduleSymbol) - moduleReference = ResolvedTopicReference(symbolReference: moduleSymbolReference, moduleName: moduleName, bundle: bundle) + moduleReference = ResolvedTopicReference(symbolReference: moduleSymbolReference, moduleName: moduleName, bundle: inputs) signposter.withIntervalSignpost("Add symbols to topic graph", id: signposter.makeSignpostID()) { addSymbolsToTopicGraph(symbolGraph: unifiedSymbolGraph, url: fileURL, symbolReferences: symbolReferences, moduleReference: moduleReference) @@ -1348,7 +1165,7 @@ public class DocumentationContext { // FIXME: Resolve the link relative to the module https://github.com/swiftlang/swift-docc/issues/516 let reference = TopicReference.unresolved(.init(topicURL: url)) - switch resolve(reference, in: bundle.rootReference, fromSymbolLink: true) { + switch resolve(reference, in: inputs.rootReference, fromSymbolLink: true) { case .success(let resolved): if let existing = uncuratedDocumentationExtensions[resolved] { if symbolsWithMultipleDocumentationExtensionMatches[resolved] == nil { @@ -1377,7 +1194,7 @@ public class DocumentationContext { bundleID: reference.bundleID, path: symbolPath, fragment: nil, - sourceLanguages: reference.sourceLanguages + sourceLanguages: reference._sourceLanguages ) if let existing = uncuratedDocumentationExtensions[symbolReference] { @@ -1406,27 +1223,19 @@ public class DocumentationContext { symbolsWithMultipleDocumentationExtensionMatches.removeAll() // Create inherited API collections - try GeneratedDocumentationTopics.createInheritedSymbolsAPICollections( - relationships: uniqueRelationships, - context: self, - bundle: bundle - ) + try GeneratedDocumentationTopics.createInheritedSymbolsAPICollections(relationships: uniqueRelationships, context: self) // Parse and prepare the nodes' content concurrently. let updatedNodes = signposter.withIntervalSignpost("Parse symbol markup", id: signposter.makeSignpostID()) { Array(documentationCache.symbolReferences).concurrentMap { finalReference in // Match the symbol's documentation extension and initialize the node content. let match = uncuratedDocumentationExtensions[finalReference] - let updatedNode = nodeWithInitializedContent( - reference: finalReference, - match: match, - bundle: bundle - ) + let updatedNode = nodeWithInitializedContent(reference: finalReference, match: match) - return (( + return ( node: updatedNode, matchedArticleURL: match?.source - )) + ) } } @@ -1452,14 +1261,14 @@ public class DocumentationContext { } // Resolve any external references first - preResolveExternalLinks(references: Array(moduleReferences.values) + combinedSymbols.keys.compactMap({ documentationCache.reference(symbolID: $0) }), localBundleID: bundle.id) + preResolveExternalLinks(references: Array(moduleReferences.values) + combinedSymbols.keys.compactMap({ documentationCache.reference(symbolID: $0) })) // Look up and add symbols that are _referenced_ in the symbol graph but don't exist in the symbol graph. try resolveExternalSymbols(in: combinedSymbols, relationships: combinedRelationshipsBySelector) for (selector, relationships) in combinedRelationshipsBySelector { // Build relationships in the completed graph - buildRelationships(relationships, selector: selector, bundle: bundle) + buildRelationships(relationships, selector: selector) // Merge into target symbols the member symbols that get rendered on the same page as target. populateOnPageMemberRelationships(from: relationships, selector: selector) } @@ -1468,25 +1277,17 @@ public class DocumentationContext { private func shouldContinueRegistration() throws { try Task.checkCancellation() - guard isRegistrationEnabled.sync({ $0 }) else { - throw ContextError.registrationDisabled - } } /// Builds in-memory relationships between symbols based on the relationship information in a given symbol graph file. /// /// - Parameters: /// - symbolGraph: The symbol graph whose symbols to add in-memory relationships to. - /// - bundle: The bundle that the symbols belong to. - /// - problems: A mutable collection of problems to update with any problem encountered while building symbol relationships. + /// - selector: The platform and language selector to build relationships for. /// /// ## See Also /// - ``SymbolGraphRelationshipsBuilder`` - func buildRelationships( - _ relationships: Set, - selector: UnifiedSymbolGraph.Selector, - bundle: DocumentationBundle - ) { + func buildRelationships(_ relationships: Set, selector: UnifiedSymbolGraph.Selector) { // Find all of the relationships which refer to an extended module. let extendedModuleRelationships = ExtendedTypeFormatTransformation.collapsedExtendedModuleRelationships(from: relationships) @@ -1505,7 +1306,7 @@ public class DocumentationContext { SymbolGraphRelationshipsBuilder.addConformanceRelationship( edge: edge, selector: selector, - in: bundle, + in: inputs, localCache: documentationCache, externalCache: externalCache, engine: diagnosticEngine @@ -1515,7 +1316,7 @@ public class DocumentationContext { SymbolGraphRelationshipsBuilder.addImplementationRelationship( edge: edge, selector: selector, - in: bundle, + in: inputs, context: self, localCache: documentationCache, engine: diagnosticEngine @@ -1525,7 +1326,7 @@ public class DocumentationContext { SymbolGraphRelationshipsBuilder.addInheritanceRelationship( edge: edge, selector: selector, - in: bundle, + in: inputs, localCache: documentationCache, externalCache: externalCache, engine: diagnosticEngine @@ -1817,9 +1618,9 @@ public class DocumentationContext { } } - private func registerMiscResources(from bundle: DocumentationBundle) throws { - let miscResources = Set(bundle.miscResourceURLs) - try assetManagers[bundle.id, default: DataAssetManager()].register(data: miscResources) + private func registerMiscResources() throws { + let miscResources = Set(inputs.miscResourceURLs) + try assetManagers[inputs.id, default: DataAssetManager()].register(data: miscResources) } private func registeredAssets(withExtensions extensions: Set? = nil, inContexts contexts: [DataAsset.Context] = DataAsset.Context.allCases, forBundleID bundleID: DocumentationBundle.Identifier) -> [DataAsset] { @@ -1847,11 +1648,6 @@ public class DocumentationContext { registeredAssets(withExtensions: DocumentationContext.supportedImageExtensions, forBundleID: bundleID) } - @available(*, deprecated, renamed: "registeredImageAssets(for:)", message: "registeredImageAssets(for:)' instead. This deprecated API will be removed after 6.2 is released") - public func registeredImageAssets(forBundleID bundleIdentifier: BundleIdentifier) -> [DataAsset] { - registeredImageAssets(for: DocumentationBundle.Identifier(rawValue: bundleIdentifier)) - } - /// Returns a list of all the video assets that registered for a given `bundleIdentifier`. /// /// - Parameter bundleID: The identifier of the bundle to return video assets for. @@ -1860,11 +1656,6 @@ public class DocumentationContext { registeredAssets(withExtensions: DocumentationContext.supportedVideoExtensions, forBundleID: bundleID) } - @available(*, deprecated, renamed: "registeredVideoAssets(for:)", message: "registeredImageAssets(for:)' instead. This deprecated API will be removed after 6.2 is released") - public func registeredVideoAssets(forBundleID bundleIdentifier: BundleIdentifier) -> [DataAsset] { - registeredVideoAssets(for: DocumentationBundle.Identifier(rawValue: bundleIdentifier)) - } - /// Returns a list of all the download assets that registered for a given `bundleIdentifier`. /// /// - Parameter bundleID: The identifier of the bundle to return download assets for. @@ -1873,11 +1664,6 @@ public class DocumentationContext { registeredAssets(inContexts: [DataAsset.Context.download], forBundleID: bundleID) } - @available(*, deprecated, renamed: "registeredDownloadsAssets(for:)", message: "registeredDownloadsAssets(for:)' instead. This deprecated API will be removed after 6.2 is released") - public func registeredDownloadsAssets(forBundleID bundleIdentifier: BundleIdentifier) -> [DataAsset] { - registeredDownloadsAssets(for: DocumentationBundle.Identifier(rawValue: bundleIdentifier)) - } - typealias Articles = [DocumentationContext.SemanticResult
] private typealias ArticlesTuple = (articles: Articles, rootPageArticles: Articles) @@ -1891,11 +1677,11 @@ public class DocumentationContext { } } - private func registerRootPages(from articles: Articles, in bundle: DocumentationBundle) { + private func registerRootPages(from articles: Articles) { // Create a root leaf node for all root page articles for article in articles { // Create the documentation data - guard let (documentation, title) = DocumentationContext.documentationNodeAndTitle(for: article, kind: .collection, in: bundle) else { continue } + guard let (documentation, title) = Self.documentationNodeAndTitle(for: article, kind: .collection, in: inputs) else { continue } let reference = documentation.reference // Create the documentation node @@ -1915,18 +1701,6 @@ public class DocumentationContext { } } - /// When `true` bundle registration will be cancelled asap. - private var isRegistrationEnabled = Synchronized(true) - - /// Enables or disables bundle registration. - /// - /// When given `false` the context will try to cancel as quick as possible - /// any ongoing bundle registrations. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - public func setRegistrationEnabled(_ value: Bool) { - isRegistrationEnabled.sync({ $0 = value }) - } - /// Adds articles that are not root pages to the documentation cache. /// /// This method adds all of the `articles` to the documentation cache and inserts a node representing @@ -1936,27 +1710,42 @@ public class DocumentationContext { /// /// - Parameters: /// - articles: Articles to register with the documentation cache. - /// - bundle: The bundle containing the articles. /// - Returns: The articles that were registered, with their topic graph node updated to what's been added to the topic graph. - private func registerArticles( - _ articles: DocumentationContext.Articles, - in bundle: DocumentationBundle - ) -> DocumentationContext.Articles { + private func registerArticles(_ articles: DocumentationContext.Articles) -> DocumentationContext.Articles { articles.map { article in - guard let (documentation, title) = DocumentationContext.documentationNodeAndTitle( + guard let (documentation, title) = Self.documentationNodeAndTitle( for: article, // By default, articles are available in the languages the module that's being documented // is available in. It's possible to override that behavior using the `@SupportedLanguage` // directive though; see its documentation for more details. availableSourceLanguages: soleRootModuleReference.map { sourceLanguages(for: $0) }, kind: .article, - in: bundle + in: inputs ) else { return article } let reference = documentation.reference - documentationCache[reference] = documentation + if let existing = documentationCache[reference], existing.kind.isSymbol { + // By the time we get here it's already to late to fix the collision. All we can do is make the author aware of it and handle the collision deterministically. + // rdar://79745455 and https://github.com/swiftlang/swift-docc/issues/593 tracks fixing the root cause of this issue, avoiding the collision and allowing the article and symbol to both exist. + diagnosticEngine.emit( + Problem( + diagnostic: Diagnostic(source: article.source, severity: .warning, identifier: "org.swift.docc.articleCollisionProblem", summary: """ + Article '\(article.source.lastPathComponent)' (\(title)) would override \(existing.kind.name.lowercased()) '\(existing.name.description)'. + """, explanation: """ + DocC computes unique URLs for symbols, even if they have the same name, but doesn't account for article filenames that collide with symbols because of a bug. + Until rdar://79745455 (issue #593) is fixed, DocC favors the symbol in this collision and drops the article to have deterministic behavior. + """), + possibleSolutions: [ + Solution(summary: "Rename '\(article.source.lastPathComponent)'", replacements: [ /* Renaming a file isn't something that we can represent with a replacement */ ]) + ] + ) + ) + return article // Don't continue processing this article + } else { + documentationCache[reference] = documentation + } documentLocationMap[article.source] = reference let graphNode = TopicGraph.Node(reference: reference, kind: .article, source: .file(url: article.source), title: title) @@ -1980,9 +1769,8 @@ public class DocumentationContext { /// /// - Parameters: /// - articles: On input, a list of articles. If an article is used as a root it is removed from this list. - /// - bundle: The bundle containing the articles. - private func synthesizeArticleOnlyRootPage(articles: inout [DocumentationContext.SemanticResult
], bundle: DocumentationBundle) { - let title = bundle.displayName + private func synthesizeArticleOnlyRootPage(articles: inout [DocumentationContext.SemanticResult
]) { + let title = inputs.displayName // An inner helper function to register a new root node from an article func registerAsNewRootNode(_ articleResult: SemanticResult
) { @@ -1990,7 +1778,7 @@ public class DocumentationContext { let title = articleResult.source.deletingPathExtension().lastPathComponent // Create a new root-looking reference let reference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: inputs.id, path: NodeURLGenerator.Path.documentation(path: title).stringValue, sourceLanguages: [DocumentationContext.defaultLanguage(in: nil /* article-only content has no source language information */)] ) @@ -2009,13 +1797,13 @@ public class DocumentationContext { } let article = Article( markup: articleResult.value.markup, - metadata: Metadata(from: metadataMarkup, for: bundle), + metadata: Metadata(from: metadataMarkup, for: inputs), redirects: articleResult.value.redirects, options: articleResult.value.options ) let graphNode = TopicGraph.Node(reference: reference, kind: .module, source: articleResult.topicGraphNode.source, title: title) - registerRootPages(from: [.init(value: article, source: articleResult.source, topicGraphNode: graphNode)], in: bundle) + registerRootPages(from: [.init(value: article, source: articleResult.source, topicGraphNode: graphNode)]) } if articles.count == 1 { @@ -2029,7 +1817,7 @@ public class DocumentationContext { let path = NodeURLGenerator.Path.documentation(path: title).stringValue let sourceLanguage = DocumentationContext.defaultLanguage(in: []) - let reference = ResolvedTopicReference(bundleID: bundle.id, path: path, sourceLanguages: [sourceLanguage]) + let reference = ResolvedTopicReference(bundleID: inputs.id, path: path, sourceLanguages: [sourceLanguage]) let graphNode = TopicGraph.Node(reference: reference, kind: .module, source: .external, title: title) topicGraph.addNode(graphNode) @@ -2042,7 +1830,7 @@ public class DocumentationContext { Heading(level: 1, Text(title)), metadataDirectiveMarkup ) - let metadata = Metadata(from: metadataDirectiveMarkup, for: bundle) + let metadata = Metadata(from: metadataDirectiveMarkup, for: inputs) let article = Article(markup: markup, metadata: metadata, redirects: nil, options: [:]) let documentationNode = DocumentationNode( reference: reference, @@ -2062,39 +1850,29 @@ public class DocumentationContext { /// - Parameters: /// - article: The article that will be used to create the returned documentation node. /// - kind: The kind that should be used to create the returned documentation node. - /// - bundle: The documentation bundle this article belongs to. + /// - inputs: The collection of inputs files that the article belongs to. /// - Returns: A documentation node and title for the given article semantic result. static func documentationNodeAndTitle( for article: DocumentationContext.SemanticResult
, availableSourceLanguages: Set? = nil, kind: DocumentationNode.Kind, - in bundle: DocumentationBundle + in inputs: DocumentationBundle ) -> (node: DocumentationNode, title: String)? { guard let articleMarkup = article.value.markup else { return nil } - let path = NodeURLGenerator.pathForSemantic(article.value, source: article.source, bundle: bundle) + let path = NodeURLGenerator.pathForSemantic(article.value, source: article.source, bundle: inputs) // Use the languages specified by the `@SupportedLanguage` directives if present. - let availableSourceLanguages = article.value - .metadata - .flatMap { metadata in - let languages = Set( - metadata.supportedLanguages - .map(\.language) - ) - - return languages.isEmpty ? nil : languages - } - ?? availableSourceLanguages + let availableSourceLanguages = article.value.supportedLanguages ?? availableSourceLanguages // If available source languages are provided and it contains Swift, use Swift as the default language of // the article. let defaultSourceLanguage = defaultLanguage(in: availableSourceLanguages) let reference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: inputs.id, path: path, sourceLanguages: availableSourceLanguages // FIXME: Pages in article-only catalogs should not be inferred as "Swift" as a fallback @@ -2160,7 +1938,7 @@ public class DocumentationContext { // for each language it's available in. if let symbol = node.semantic as? Symbol { for sourceLanguage in node.availableSourceLanguages { - symbol.automaticTaskGroupsVariants[.init(interfaceLanguage: sourceLanguage.id)] = [automaticTaskGroup] + symbol.automaticTaskGroupsVariants[.init(sourceLanguage: sourceLanguage)] = [automaticTaskGroup] } } else if var taskGroupProviding = node.semantic as? (any AutomaticTaskGroupsProviding) { taskGroupProviding.automaticTaskGroups = [automaticTaskGroup] @@ -2172,11 +1950,11 @@ public class DocumentationContext { /** Register a documentation bundle with this context. */ - private func register(_ bundle: DocumentationBundle) throws { + private func register() throws { try shouldContinueRegistration() let currentFeatureFlags: FeatureFlags? - if let bundleFlags = bundle.info.featureFlags { + if let bundleFlags = inputs.info.featureFlags { currentFeatureFlags = FeatureFlags.current FeatureFlags.current.loadFlagsFromBundle(bundleFlags) @@ -2219,8 +1997,8 @@ public class DocumentationContext { discoveryGroup.async(queue: discoveryQueue) { [unowned self] in symbolGraphLoader = SymbolGraphLoader( - bundle: bundle, - dataLoader: { try self.contentsOfURL($0, in: $1) }, + bundle: inputs, + dataProvider: dataProvider, symbolGraphTransformer: configuration.convertServiceConfiguration.symbolGraphTransformer ) @@ -2231,10 +2009,12 @@ public class DocumentationContext { hierarchyBasedResolver = signposter.withIntervalSignpost("Build PathHierarchy", id: signposter.makeSignpostID()) { PathHierarchyBasedLinkResolver(pathHierarchy: PathHierarchy( symbolGraphLoader: symbolGraphLoader, - bundleName: urlReadablePath(bundle.displayName), + bundleName: urlReadablePath(inputs.displayName), knownDisambiguatedPathComponents: configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents )) } + + self.snippetResolver = SnippetResolver(symbolGraphLoader: symbolGraphLoader) } catch { // Pipe the error out of the dispatch queue. discoveryError.sync({ @@ -2247,7 +2027,7 @@ public class DocumentationContext { discoveryGroup.async(queue: discoveryQueue) { [unowned self] in do { try signposter.withIntervalSignpost("Load resources", id: signposter.makeSignpostID()) { - try self.registerMiscResources(from: bundle) + try self.registerMiscResources() } } catch { // Pipe the error out of the dispatch queue. @@ -2273,7 +2053,7 @@ public class DocumentationContext { discoveryGroup.async(queue: discoveryQueue) { [unowned self] in do { result = try signposter.withIntervalSignpost("Load documents", id: signposter.makeSignpostID()) { - try self.registerDocuments(from: bundle) + try self.registerDocuments() } } catch { // Pipe the error out of the dispatch queue. @@ -2347,7 +2127,7 @@ public class DocumentationContext { } self.linkResolver.localResolver = hierarchyBasedResolver - hierarchyBasedResolver.addMappingForRoots(bundle: bundle) + hierarchyBasedResolver.addMappingForRoots(bundle: inputs) for tutorial in tutorials { hierarchyBasedResolver.addTutorial(tutorial) } @@ -2358,8 +2138,8 @@ public class DocumentationContext { hierarchyBasedResolver.addTutorialTableOfContents(tutorialTableOfContents) } - registerRootPages(from: rootPageArticles, in: bundle) - try registerSymbols(from: bundle, symbolGraphLoader: symbolGraphLoader, documentationExtensions: documentationExtensions) + registerRootPages(from: rootPageArticles) + try registerSymbols(symbolGraphLoader: symbolGraphLoader, documentationExtensions: documentationExtensions) // We don't need to keep the loader in memory after we've registered all symbols. symbolGraphLoader = nil @@ -2369,7 +2149,7 @@ public class DocumentationContext { !otherArticles.isEmpty, !configuration.convertServiceConfiguration.allowsRegisteringArticlesWithoutTechnologyRoot { - synthesizeArticleOnlyRootPage(articles: &otherArticles, bundle: bundle) + synthesizeArticleOnlyRootPage(articles: &otherArticles) } // Keep track of the root modules registered from symbol graph files, we'll need them to automatically @@ -2384,7 +2164,7 @@ public class DocumentationContext { // Articles that will be automatically curated can be resolved but they need to be pre registered before resolving links. let rootNodeForAutomaticCuration = soleRootModuleReference.flatMap(topicGraph.nodeWithReference(_:)) if configuration.convertServiceConfiguration.allowsRegisteringArticlesWithoutTechnologyRoot || rootNodeForAutomaticCuration != nil { - otherArticles = registerArticles(otherArticles, in: bundle) + otherArticles = registerArticles(otherArticles) try shouldContinueRegistration() } @@ -2392,14 +2172,12 @@ public class DocumentationContext { preResolveExternalLinks(semanticObjects: tutorialTableOfContentsResults.map(referencedSemanticObject) + tutorials.map(referencedSemanticObject) + - tutorialArticles.map(referencedSemanticObject), - localBundleID: bundle.id) + tutorialArticles.map(referencedSemanticObject)) resolveLinks( tutorialTableOfContents: tutorialTableOfContentsResults, tutorials: tutorials, - tutorialArticles: tutorialArticles, - bundle: bundle + tutorialArticles: tutorialArticles ) // After the resolving links in tutorial content all the local references are known and can be added to the referenceIndex for fast lookup. @@ -2412,7 +2190,7 @@ public class DocumentationContext { } try shouldContinueRegistration() - var allCuratedReferences = try crawlSymbolCuration(in: linkResolver.localResolver.topLevelSymbols(), bundle: bundle) + var allCuratedReferences = try crawlSymbolCuration(in: linkResolver.localResolver.topLevelSymbols()) // Store the list of manually curated references if doc coverage is on. if configuration.experimentalCoverageConfiguration.shouldStoreManuallyCuratedReferences { @@ -2427,13 +2205,13 @@ public class DocumentationContext { } // Crawl the rest of the symbols that haven't been crawled so far in hierarchy pre-order. - allCuratedReferences = try crawlSymbolCuration(in: automaticallyCurated.map(\.symbol), bundle: bundle, initial: allCuratedReferences) + allCuratedReferences = try crawlSymbolCuration(in: automaticallyCurated.map(\.symbol), initial: allCuratedReferences) // Automatically curate articles that haven't been manually curated // Article curation is only done automatically if there is only one root module if let rootNode = rootNodeForAutomaticCuration { let articleReferences = try autoCurateArticles(otherArticles, startingFrom: rootNode) - allCuratedReferences = try crawlSymbolCuration(in: articleReferences, bundle: bundle, initial: allCuratedReferences) + allCuratedReferences = try crawlSymbolCuration(in: articleReferences, initial: allCuratedReferences) } // Remove curation paths that have been created automatically above @@ -2451,14 +2229,14 @@ public class DocumentationContext { linkResolver.localResolver.addAnchorForSymbols(localCache: documentationCache) // Fifth, resolve links in nodes that are added solely via curation - preResolveExternalLinks(references: Array(allCuratedReferences), localBundleID: bundle.id) - resolveLinks(curatedReferences: allCuratedReferences, bundle: bundle) + preResolveExternalLinks(references: Array(allCuratedReferences)) + resolveLinks(curatedReferences: allCuratedReferences) if configuration.convertServiceConfiguration.fallbackResolver != nil { // When the ``ConvertService`` builds documentation for a single page there won't be a module or root // reference to auto-curate the page under, so the regular local link resolution code path won't visit // the single page. To ensure that links are resolved, explicitly visit all pages. - resolveLinks(curatedReferences: Set(knownPages), bundle: bundle) + resolveLinks(curatedReferences: Set(knownPages)) } // We should use a read-only context during render time (rdar://65130130). @@ -2650,32 +2428,22 @@ public class DocumentationContext { } } } - /// A closure type getting the information about a reference in a context and returns any possible problems with it. public typealias ReferenceCheck = (DocumentationContext, ResolvedTopicReference) -> [Problem] - /// Adds new checks to be run during the global topic analysis; after a bundle has been fully registered and its topic graph has been fully built. - /// - /// - Parameter newChecks: The new checks to add. - @available(*, deprecated, message: "Use 'TopicAnalysisConfiguration.additionalChecks' instead. This deprecated API will be removed after 6.2 is released") - public func addGlobalChecks(_ newChecks: [ReferenceCheck]) { - configuration.topicAnalysisConfiguration.additionalChecks.append(contentsOf: newChecks) - } - /// Crawls the hierarchy of the given list of nodes, adding relationships in the topic graph for all resolvable task group references. /// - Parameters: /// - references: A list of references to crawl. - /// - bundle: A documentation bundle. /// - initial: A list of references to skip when crawling. /// - Returns: The references of all the symbols that were curated. @discardableResult - func crawlSymbolCuration(in references: [ResolvedTopicReference], bundle: DocumentationBundle, initial: Set = []) throws -> Set { + func crawlSymbolCuration(in references: [ResolvedTopicReference], initial: Set = []) throws -> Set { let signpostHandle = signposter.beginInterval("Curate symbols", id: signposter.makeSignpostID()) defer { signposter.endInterval("Curate symbols", signpostHandle) } - var crawler = DocumentationCurator(in: self, bundle: bundle, initial: initial) + var crawler = DocumentationCurator(in: self, initial: initial) for reference in references { try crawler.crawlChildren( @@ -2844,21 +2612,6 @@ public class DocumentationContext { analyzeTopicGraph() } - /** - Unregister a documentation bundle with this context and clear any cached resources associated with it. - */ - private func unregister(_ bundle: DocumentationBundle) { - let referencesToRemove = topicGraph.nodes.keys.filter { - $0.bundleID == bundle.id - } - - for reference in referencesToRemove { - topicGraph.edges[reference]?.removeAll(where: { $0.bundleID == bundle.id }) - topicGraph.reverseEdges[reference]?.removeAll(where: { $0.bundleID == bundle.id }) - topicGraph.nodes[reference] = nil - } - } - // MARK: - Getting documentation relationships /** @@ -2871,15 +2624,13 @@ public class DocumentationContext { - Throws: ``ContextError/notFound(_:)` if a resource with the given was not found. */ public func resource(with identifier: ResourceReference, trait: DataTraitCollection = .init()) throws -> Data { - guard let bundle, - let assetManager = assetManagers[identifier.bundleID], - let asset = assetManager.allData(named: identifier.path) else { + guard let asset = assetManagers[identifier.bundleID]?.allData(named: identifier.path) else { throw ContextError.notFound(identifier.url) } let resource = asset.data(bestMatching: trait) - return try contentsOfURL(resource.url, in: bundle) + return try dataProvider.contents(of: resource.url) } /// Returns true if a resource with the given identifier exists in the registered bundle. @@ -2910,7 +2661,7 @@ public class DocumentationContext { - Returns: A ``DocumentationNode`` with the given identifier. - Throws: ``ContextError/notFound(_:)`` if a documentation node with the given identifier was not found. */ - public func entity(with reference: ResolvedTopicReference) throws -> DocumentationNode { + public func entity(with reference: ResolvedTopicReference) throws(ContextError) -> DocumentationNode { if let cached = documentationCache[reference] { return cached } @@ -2946,7 +2697,7 @@ public class DocumentationContext { knownEntityValue( reference: reference, valueInLocalEntity: \.availableSourceLanguages, - valueInExternalEntity: \.sourceLanguages + valueInExternalEntity: \.availableLanguages ) } @@ -2954,9 +2705,14 @@ public class DocumentationContext { func isSymbol(reference: ResolvedTopicReference) -> Bool { knownEntityValue( reference: reference, - valueInLocalEntity: { node in node.kind.isSymbol }, - valueInExternalEntity: { entity in entity.topicRenderReference.kind == .symbol } - ) + valueInLocalEntity: \.kind, + valueInExternalEntity: \.kind + ).isSymbol + } + + /// Returns whether the given reference resolves to an external entity. + func isExternal(reference: ResolvedTopicReference) -> Bool { + externalCache[reference] != nil } // MARK: - Relationship queries @@ -3194,14 +2950,7 @@ extension DocumentationContext { var problems = [Problem]() func listSourceLanguages(_ sourceLanguages: Set) -> String { - sourceLanguages.sorted(by: { language1, language2 in - // Emit Swift first, then alphabetically. - switch (language1, language2) { - case (.swift, _): return true - case (_, .swift): return false - default: return language1.id < language2.id - } - }).map(\.name).list(finalConjunction: .and) + sourceLanguages.sorted().map(\.name).list(finalConjunction: .and) } func removeAlternateRepresentationSolution(_ alternateRepresentation: AlternateRepresentation) -> [Solution] { [Solution( @@ -3349,6 +3098,3 @@ extension DataAsset { } } } - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension DocumentationContext: DocumentationContextDataProviderDelegate {} diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift b/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift deleted file mode 100644 index 9b0cd3a86e..0000000000 --- a/Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift +++ /dev/null @@ -1,486 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2025 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 -*/ - -public import Foundation - -/// A converter from a documentation bundle to an output that can be consumed by a renderer. -/// -/// This protocol is primarily used for injecting mock documentation converters during testing. -/// -/// ## See Also -/// -/// - ``DocumentationConverter`` -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -public protocol DocumentationConverterProtocol { - /// Converts documentation, outputting products using the given output consumer. - /// - Parameter outputConsumer: The output consumer for content produced during conversion. - /// - Returns: The problems emitted during analysis of the documentation bundle and during conversion. - /// - Throws: Throws an error if the conversion process was not able to start at all, for example if the bundle could not be read. - /// Partial failures, such as failing to consume a single render node, are returned in the `conversionProblems` component - /// of the returned tuple. - mutating func convert( - outputConsumer: some ConvertOutputConsumer - ) throws -> (analysisProblems: [Problem], conversionProblems: [Problem]) -} - -/// A converter from a documentation bundle to an output that can be consumed by a renderer. -/// -/// A documentation converter analyzes a documentation bundle and converts it to products that can be used by a documentation -/// renderer to render documentation. The output format of the conversion is controlled by a ``ConvertOutputConsumer``, which -/// determines what to do with the conversion products, for example, write them to disk. -/// -/// You can also configure the documentation converter to emit extra metadata such as linkable entities and indexing records -/// information. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -public struct DocumentationConverter: DocumentationConverterProtocol { - let rootURL: URL? - let emitDigest: Bool - let documentationCoverageOptions: DocumentationCoverageOptions - let bundleDiscoveryOptions: BundleDiscoveryOptions - let diagnosticEngine: DiagnosticEngine - - private(set) var context: DocumentationContext - private let workspace: DocumentationWorkspace - private var currentDataProvider: (any DocumentationWorkspaceDataProvider)? - private var dataProvider: any DocumentationWorkspaceDataProvider - - /// An optional closure that sets up a context before the conversion begins. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - public var setupContext: ((inout DocumentationContext) -> Void)? - - /// Conversion batches should be big enough to keep all cores busy but small enough not to keep - /// around too many async blocks that update the conversion results. After running some tests it - /// seems that more than couple hundred of a batch size doesn't bring more performance CPU-wise - /// and it's a fair amount of async tasks to keep in memory before draining the results queue - /// after the batch is converted. - var batchNodeCount = 1 - - /// The external IDs of the symbols to convert. - /// - /// Use this property to indicate what symbol documentation nodes should be converted. When ``externalIDsToConvert`` - /// and ``documentationPathsToConvert`` are both set, the documentation nodes that are in either arrays will be - /// converted. - /// - /// If you want all the symbol render nodes to be returned as part of the conversion's response, set this property to `nil`. - /// For Swift, the external ID of the symbol is its USR. - var externalIDsToConvert: [String]? - - /// The paths of the documentation nodes to convert. - /// - /// Use this property to indicate what documentation nodes should be converted. When ``externalIDsToConvert`` - /// and ``documentationPathsToConvert`` are both set, the documentation nodes that are in either arrays will be - /// converted. - /// - /// If you want all the render nodes to be returned as part of the conversion's response, set this property to `nil`. - var documentPathsToConvert: [String]? - - /// Whether the documentation converter should include source file - /// location metadata in any render nodes representing symbols it creates. - /// - /// Before setting this value to `true` please confirm that your use case doesn't include - /// public distribution of any created render nodes as there are filesystem privacy and security - /// concerns with distributing this data. - var shouldEmitSymbolSourceFileURIs: Bool - - /// Whether the documentation converter should include access level information for symbols. - var shouldEmitSymbolAccessLevels: Bool - - /// The source repository where the documentation's sources are hosted. - var sourceRepository: SourceRepository? - - /// Whether the documentation converter should write documentation extension files containing markdown representations of DocC's automatic curation into the source documentation catalog. - var experimentalModifyCatalogWithGeneratedCuration: Bool - - /// The identifiers and access level requirements for symbols that have an expanded version of their documentation page if the requirements are met - var symbolIdentifiersWithExpandedDocumentation: [String: ConvertRequest.ExpandedDocumentationRequirements]? = nil - - /// `true` if the conversion is cancelled. - private var isCancelled: Synchronized? = nil - - private var processingDurationMetric: Benchmark.Duration? - - /// Creates a documentation converter given a documentation bundle's URL. - /// - /// - Parameters: - /// - documentationBundleURL: The root URL of the documentation bundle to convert. - /// - emitDigest: Whether the conversion should create metadata files, such as linkable entities information. - /// - documentationCoverageOptions: What level of documentation coverage output should be emitted. - /// - currentPlatforms: The current version and beta information for platforms that may be encountered while processing symbol graph files. - /// - workspace: A provided documentation workspace. Creates a new empty workspace if value is `nil`. - /// - context: A provided documentation context. - /// - dataProvider: A data provider to use when registering bundles. - /// - externalIDsToConvert: The external IDs of the documentation nodes to convert. - /// - documentPathsToConvert: The paths of the documentation nodes to convert. - /// - bundleDiscoveryOptions: Options to configure how the converter discovers documentation bundles. - /// - emitSymbolSourceFileURIs: Whether the documentation converter should include - /// source file location metadata in any render nodes representing symbols it creates. - /// - /// Before passing `true` please confirm that your use case doesn't include public - /// distribution of any created render nodes as there are filesystem privacy and security - /// concerns with distributing this data. - /// - emitSymbolAccessLevels: Whether the documentation converter should include access level information for symbols. - /// - sourceRepository: The source repository where the documentation's sources are hosted. - /// - isCancelled: A wrapped boolean value used for the caller to cancel converting the documentation. - /// that have an expanded version of their documentation page if the access level requirement is met. - /// - diagnosticEngine: The diagnostic engine that collects any problems encountered from converting the documentation. - /// - symbolIdentifiersWithExpandedDocumentation: Identifiers and access level requirements for symbols - /// - experimentalModifyCatalogWithGeneratedCuration: Whether the documentation converter should write documentation extension files containing markdown representations of DocC's automatic curation into the source documentation catalog. - public init( - documentationBundleURL: URL?, - emitDigest: Bool, - documentationCoverageOptions: DocumentationCoverageOptions, - currentPlatforms: [String : PlatformVersion]?, - workspace: DocumentationWorkspace, - context: DocumentationContext, - dataProvider: any DocumentationWorkspaceDataProvider, - externalIDsToConvert: [String]? = nil, - documentPathsToConvert: [String]? = nil, - bundleDiscoveryOptions: BundleDiscoveryOptions, - emitSymbolSourceFileURIs: Bool = false, - emitSymbolAccessLevels: Bool = false, - sourceRepository: SourceRepository? = nil, - isCancelled: Synchronized? = nil, - diagnosticEngine: DiagnosticEngine = .init(), - symbolIdentifiersWithExpandedDocumentation: [String: ConvertRequest.ExpandedDocumentationRequirements]? = nil, - experimentalModifyCatalogWithGeneratedCuration: Bool = false - ) { - self.rootURL = documentationBundleURL - self.emitDigest = emitDigest - self.documentationCoverageOptions = documentationCoverageOptions - self.workspace = workspace - self.context = context - self.dataProvider = dataProvider - self.externalIDsToConvert = externalIDsToConvert - self.documentPathsToConvert = documentPathsToConvert - self.bundleDiscoveryOptions = bundleDiscoveryOptions - self.shouldEmitSymbolSourceFileURIs = emitSymbolSourceFileURIs - self.shouldEmitSymbolAccessLevels = emitSymbolAccessLevels - self.sourceRepository = sourceRepository - self.isCancelled = isCancelled - self.diagnosticEngine = diagnosticEngine - self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation - self.experimentalModifyCatalogWithGeneratedCuration = experimentalModifyCatalogWithGeneratedCuration - } - - /// Returns the first bundle in the source directory, if any. - /// > Note: The result of this function is not cached, it reads the source directory and finds all bundles. - public func firstAvailableBundle() -> DocumentationBundle? { - return (try? dataProvider.bundles(options: bundleDiscoveryOptions)).map(sorted(bundles:))?.first - } - - /// Sorts a list of bundles by the bundle identifier. - private func sorted(bundles: [DocumentationBundle]) -> [DocumentationBundle] { - return bundles.sorted(by: \.identifier) - } - - mutating public func convert( - outputConsumer: some ConvertOutputConsumer - ) throws -> (analysisProblems: [Problem], conversionProblems: [Problem]) { - defer { - diagnosticEngine.flush() - } - - // Unregister the current file data provider and all its bundles - // when running repeated conversions. - if let dataProvider = self.currentDataProvider { - try workspace.unregisterProvider(dataProvider) - } - - let context = self.context - - // Start bundle registration - try workspace.registerProvider(dataProvider, options: bundleDiscoveryOptions) - self.currentDataProvider = dataProvider - - // If cancelled, return early before we emit diagnostics. - func isConversionCancelled() -> Bool { - Task.isCancelled || isCancelled?.sync({ $0 }) == true - } - guard !isConversionCancelled() else { return ([], []) } - - processingDurationMetric = benchmark(begin: Benchmark.Duration(id: "documentation-processing")) - - let bundles = try sorted(bundles: dataProvider.bundles(options: bundleDiscoveryOptions)) - guard !bundles.isEmpty else { - if let rootURL { - throw Error.doesNotContainBundle(url: rootURL) - } else { - try (_Deprecated(outputConsumer) as (any _DeprecatedConsumeProblemsAccess))._consume(problems: context.problems) - throw GeneratedDataProvider.Error.notEnoughDataToGenerateBundle(options: bundleDiscoveryOptions, underlyingError: nil) - } - } - - // For now, we only support one bundle. - let bundle = bundles.first! - - if experimentalModifyCatalogWithGeneratedCuration, let catalogURL = rootURL { - let writer = GeneratedCurationWriter(context: context, catalogURL: catalogURL, outputURL: catalogURL) - let curation = try writer.generateDefaultCurationContents() - for (url, updatedContent) in curation { - guard let data = updatedContent.data(using: .utf8) else { continue } - try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) - try? data.write(to: url, options: .atomic) - } - } - - guard !context.problems.containsErrors else { - if emitDigest { - try (_Deprecated(outputConsumer) as (any _DeprecatedConsumeProblemsAccess))._consume(problems: context.problems) - } - return (analysisProblems: context.problems, conversionProblems: []) - } - - // Precompute the render context - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - - try outputConsumer.consume(renderReferenceStore: renderContext.store) - - // Copy images, sample files, and other static assets. - try outputConsumer.consume(assetsInBundle: bundle) - - let symbolIdentifiersMeetingRequirementsForExpandedDocumentation: [String]? = symbolIdentifiersWithExpandedDocumentation?.compactMap { (identifier, expandedDocsRequirement) -> String? in - guard let documentationNode = context.documentationCache[identifier] else { - return nil - } - - return documentationNode.meetsExpandedDocumentationRequirements(expandedDocsRequirement) ? identifier : nil - } - - let converter = DocumentationContextConverter( - bundle: bundle, - context: context, - renderContext: renderContext, - emitSymbolSourceFileURIs: shouldEmitSymbolSourceFileURIs, - emitSymbolAccessLevels: shouldEmitSymbolAccessLevels, - sourceRepository: sourceRepository, - symbolIdentifiersWithExpandedDocumentation: symbolIdentifiersMeetingRequirementsForExpandedDocumentation - ) - - var indexingRecords = [IndexingRecord]() - var linkSummaries = [LinkDestinationSummary]() - var assets = [RenderReferenceType : [any RenderReference]]() - - let references = context.knownPages - let resultsSyncQueue = DispatchQueue(label: "Convert Serial Queue", qos: .unspecified, attributes: []) - let resultsGroup = DispatchGroup() - - var coverageInfo = [CoverageDataEntry]() - // No need to generate this closure more than once. - let coverageFilterClosure = documentationCoverageOptions.generateFilterClosure() - - // Process render nodes in batches allowing us to release memory and sync after each batch - // Keep track of any problems in case emitDigest == true - var conversionProblems: [Problem] = references.concurrentPerform { identifier, results in - // If cancelled skip all concurrent conversion work in this block. - guard !isConversionCancelled() else { return } - - // Wrap JSON encoding in an autorelease pool to avoid retaining the autoreleased ObjC objects returned by `JSONSerialization` - autoreleasepool { - do { - let entity = try context.entity(with: identifier) - - guard shouldConvertEntity(entity: entity, identifier: identifier) else { - return - } - - guard let renderNode = converter.renderNode(for: entity) else { - // No render node was produced for this entity, so just skip it. - return - } - - try outputConsumer.consume(renderNode: renderNode) - - switch documentationCoverageOptions.level { - case .detailed, .brief: - let coverageEntry = try CoverageDataEntry( - documentationNode: entity, - renderNode: renderNode, - context: context - ) - if coverageFilterClosure(coverageEntry) { - resultsGroup.async(queue: resultsSyncQueue) { - coverageInfo.append(coverageEntry) - } - } - case .none: - break - } - - if emitDigest { - let nodeLinkSummaries = entity.externallyLinkableElementSummaries(context: context, renderNode: renderNode, includeTaskGroups: true) - let nodeIndexingRecords = try renderNode.indexingRecords(onPage: identifier) - - resultsGroup.async(queue: resultsSyncQueue) { - assets.merge(renderNode.assetReferences, uniquingKeysWith: +) - linkSummaries.append(contentsOf: nodeLinkSummaries) - indexingRecords.append(contentsOf: nodeIndexingRecords) - } - } else if FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled { - let nodeLinkSummaries = entity.externallyLinkableElementSummaries(context: context, renderNode: renderNode, includeTaskGroups: false) - - resultsGroup.async(queue: resultsSyncQueue) { - linkSummaries.append(contentsOf: nodeLinkSummaries) - } - } - } catch { - recordProblem(from: error, in: &results, withIdentifier: "render-node") - } - } - } - - // Wait for any concurrent updates to complete. - resultsGroup.wait() - - // If cancelled, return before producing outputs. - guard !isConversionCancelled() else { return ([], []) } - - // Write various metadata - if emitDigest { - do { - try outputConsumer.consume(linkableElementSummaries: linkSummaries) - try outputConsumer.consume(indexingRecords: indexingRecords) - try outputConsumer.consume(assets: assets) - } catch { - recordProblem(from: error, in: &conversionProblems, withIdentifier: "metadata") - } - } - - if FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled { - do { - let serializableLinkInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: bundle.id) - try outputConsumer.consume(linkResolutionInformation: serializableLinkInformation) - - if !emitDigest { - try outputConsumer.consume(linkableElementSummaries: linkSummaries) - } - } catch { - recordProblem(from: error, in: &conversionProblems, withIdentifier: "link-resolver") - } - } - - if emitDigest { - do { - try (_Deprecated(outputConsumer) as (any _DeprecatedConsumeProblemsAccess))._consume(problems: context.problems + conversionProblems) - } catch { - recordProblem(from: error, in: &conversionProblems, withIdentifier: "problems") - } - } - - switch documentationCoverageOptions.level { - case .detailed, .brief: - do { - try outputConsumer.consume(documentationCoverageInfo: coverageInfo) - } catch { - recordProblem(from: error, in: &conversionProblems, withIdentifier: "coverage") - } - case .none: - break - } - - try outputConsumer.consume( - buildMetadata: BuildMetadata( - bundleDisplayName: bundle.displayName, - bundleIdentifier: bundle.identifier - ) - ) - - // Log the duration of the processing (after the bundle content finished registering). - benchmark(end: processingDurationMetric) - // Log the finalized topic graph checksum. - benchmark(add: Benchmark.TopicGraphHash(context: context)) - // Log the finalized list of topic anchor sections. - benchmark(add: Benchmark.TopicAnchorHash(context: context)) - // Log the finalized external topics checksum. - benchmark(add: Benchmark.ExternalTopicsHash(context: context)) - // Log the peak memory. - benchmark(add: Benchmark.PeakMemory()) - - return (analysisProblems: context.problems, conversionProblems: conversionProblems) - } - - /// Whether the given entity should be converted to a render node. - private func shouldConvertEntity( - entity: DocumentationNode, - identifier: ResolvedTopicReference - ) -> Bool { - let isDocumentPathToConvert: Bool - if let documentPathsToConvert { - isDocumentPathToConvert = documentPathsToConvert.contains(identifier.path) - } else { - isDocumentPathToConvert = true - } - - let isExternalIDToConvert: Bool - if let externalIDsToConvert { - isExternalIDToConvert = entity.symbol.map { - externalIDsToConvert.contains($0.identifier.precise) - } == true - } else { - isExternalIDToConvert = true - } - - // If the identifier of the entity is neither in `documentPathsToConvert` - // nor `externalIDsToConvert`, we don't convert it to a render node. - return isDocumentPathToConvert || isExternalIDToConvert - } - - /// Record a problem from the given error in the given problem array. - /// - /// Creates a ``Problem`` from the given `Error` and identifier, emits it to the - /// ``DocumentationConverter``'s ``DiagnosticEngine``, and appends it to the given - /// problem array. - /// - /// - Parameters: - /// - error: The error that describes the problem. - /// - problems: The array that the created problem should be appended to. - /// - identifier: A unique identifier the problem. - private func recordProblem( - from error: any Swift.Error, - in problems: inout [Problem], - withIdentifier identifier: String - ) { - let singleDiagnostic = Diagnostic( - source: nil, - severity: .error, - range: nil, - identifier: "org.swift.docc.documentation-converter.\(identifier)", - summary: error.localizedDescription - ) - let problem = Problem(diagnostic: singleDiagnostic, possibleSolutions: []) - - diagnosticEngine.emit(problem) - problems.append(problem) - } - - enum Error: DescribedError, Equatable { - case doesNotContainBundle(url: URL) - - var errorDescription: String { - switch self { - case .doesNotContainBundle(let url): - return """ - The directory at '\(url)' and its subdirectories do not contain at least one \ - valid documentation bundle. A documentation bundle is a directory ending in \ - `.docc`. - Pass `--allow-arbitrary-catalog-directories` flag to convert a directory \ - without a `.docc` extension. - """ - } - } - } -} - -extension DocumentationNode { - func meetsExpandedDocumentationRequirements(_ requirements: ConvertRequest.ExpandedDocumentationRequirements) -> Bool { - guard let symbol else { return false } - - return requirements.accessControlLevels.contains(symbol.accessLevel.rawValue) && (!symbol.names.title.starts(with: "_") || requirements.canBeUnderscored) - } -} diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift b/Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift index b310940a53..f430ab01eb 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -17,14 +17,10 @@ struct DocumentationCurator { /// The documentation context to crawl. private let context: DocumentationContext - /// The current bundle. - private let bundle: DocumentationBundle - private(set) var problems = [Problem]() - init(in context: DocumentationContext, bundle: DocumentationBundle, initial: Set = []) { + init(in context: DocumentationContext, initial: Set = []) { self.context = context - self.bundle = bundle self.curatedNodes = initial } @@ -90,13 +86,26 @@ struct DocumentationCurator { } // Try extracting an article from the cache - let articleFilename = unresolved.topicURL.components.path.components(separatedBy: "/").last! - let sourceArticlePath = NodeURLGenerator.Path.article(bundleName: bundle.displayName, articleName: articleFilename).stringValue - + let sourceArticlePath: String = { + let path = unresolved.topicURL.components.path.removingLeadingSlash + + // The article path can either be written as + // - "ArticleName" + // - "CatalogName/ArticleName" + // - "documentation/CatalogName/ArticleName" + switch path.components(separatedBy: "/").count { + case 0,1: + return NodeURLGenerator.Path.article(bundleName: context.inputs.displayName, articleName: path).stringValue + case 2: + return "\(NodeURLGenerator.Path.documentationFolder)/\(path)" + default: + return path.prependingLeadingSlash + } + }() let reference = ResolvedTopicReference( bundleID: resolved.bundleID, path: sourceArticlePath, - sourceLanguages: resolved.sourceLanguages) + sourceLanguages: resolved._sourceLanguages) guard let currentArticle = self.context.uncuratedArticles[reference], let documentationNode = try? DocumentationNode(reference: reference, article: currentArticle.value) else { return nil } @@ -115,6 +124,7 @@ struct DocumentationCurator { context.topicGraph.addNode(curatedNode) // Move the article from the article cache to the documentation + let articleFilename = reference.url.pathComponents.last! context.linkResolver.localResolver.addArticle(filename: articleFilename, reference: reference, anchorSections: documentationNode.anchorSections) context.documentationCache[reference] = documentationNode diff --git a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+Communication.swift b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+Communication.swift new file mode 100644 index 0000000000..4f621ab942 --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+Communication.swift @@ -0,0 +1,226 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 +*/ + +extension OutOfProcessReferenceResolver { + // MARK: Capabilities + + /// A set of optional capabilities that either DocC or your external link resolver declares that it supports. + /// + /// ## Supported messages + /// + /// If your external link resolver declares none of the optional capabilities, then DocC will only send it the following messages: + /// - ``RequestV2/link(_:)`` + /// - ``RequestV2/symbol(_:)`` + public struct Capabilities: OptionSet, Codable { + public let rawValue: Int + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + rawValue = try container.decode(Int.self) + } + } + + // MARK: Request & Response + + /// Request messages that DocC sends to the configured external link resolver. + /// + /// ## Topics + /// ### Base requests + /// + /// Your external link resolver always needs to handle the following requests regardless of its declared capabilities: + /// + /// - ``link(_:)`` + /// - ``symbol(_:)`` + public enum RequestV2: Codable { + /// A request to resolve a link + /// + /// DocC omits the "doc:\/\/" and identifier prefix from the link string because it would be the same for every link request. + /// For example: if your resolver registers itself for the `"your.resolver.id"` identifier---by sending it in the ``ResponseV2/identifierAndCapabilities(_:_:)`` handshake message--- + /// and DocC encounters a `doc://your.resolver.id/path/to/some-page#some-fragment` link in any documentation content, DocC sends the `"/path/to/some-page#some-fragment"` link to your resolver. + /// + /// Your external resolver should respond with either: + /// - a ``ResponseV2/resolved(_:)`` message, with information about the requested link. + /// - a ``ResponseV2/failure(_:)`` message, with human-readable information about the problem that the external link resolver encountered while resolving the link. + case link(String) + /// A request to resolve a symbol based on its precise identifier. + /// + /// Your external resolver should respond with either: + /// - a ``ResponseV2/resolved(_:)`` message, with information about the requested symbol. + /// - an empty ``ResponseV2/failure(_:)`` message. + /// DocC doesn't display any diagnostics about failed symbol requests because they wouldn't be actionable; + /// because they're entirely automatic---based on unique identifiers in the symbol graph files---rather than authored references in the content. + case symbol(String) + + // This empty-marker case is here because non-frozen enums are only available when Library Evolution is enabled, + // which is not available to Swift Packages without unsafe flags (rdar://78773361). + // This can be removed once that is available and applied to Swift-DocC (rdar://89033233). + @available(*, deprecated, message: """ + This enum is non-frozen and may be expanded in the future; add a `default` case, and do nothing in it, instead of matching this one. + Your external link resolver won't be passed new messages that it hasn't declared the corresponding capability for. + """) + case _nonFrozenEnum_useDefaultCase + + private enum CodingKeys: CodingKey { + case link, symbol // Default requests keys + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .link(let link): try container.encode(link, forKey: .link) + case .symbol(let id): try container.encode(id, forKey: .symbol) + + case ._nonFrozenEnum_useDefaultCase: + fatalError("Never use '_nonFrozenEnum_useDefaultCase' as a real case.") + } + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self = switch container.allKeys.first { + case .link?: .link( try container.decode(String.self, forKey: .link)) + case .symbol?: .symbol(try container.decode(String.self, forKey: .symbol)) + case nil: throw OutOfProcessReferenceResolver.Error.unknownTypeOfRequest + } + } + } + + /// Response messages that the external link resolver sends back to DocC for each received request.. + /// + /// If your external resolver sends a response that's associated with a capability that DocC hasn't declared support for, then DocC will fail to handle the response. + public enum ResponseV2: Codable { + /// The initial identifier-and-capabilities message. + /// + /// Your external link resolver should send this message, exactly once, after it has launched to signal that its ready to receive requests. + /// + /// The capabilities that your external link resolver declares in this message determines which optional request messages that DocC will send. + /// If your resolver doesn't declare _any_ capabilities it only needs to handle the 3 default requests. See . + case identifierAndCapabilities(DocumentationBundle.Identifier, Capabilities) + /// A response with human-readable information about the problem that the external link resolver encountered while resolving the requested link or symbol. + /// + /// - Note: DocC doesn't display any diagnostics about failed ``RequestV2/symbol(_:)`` requests because they wouldn't be actionable; + /// because they're entirely automatic---based on unique identifiers in the symbol graph files---rather than authored references in the content. + /// + /// Your external link resolver still needs to send a response to each request but it doesn't need to include details about the failure when responding to a ``RequestV2/symbol(_:)`` request. + case failure(DiagnosticInformation) + /// A response with the resolved information about the requested link or symbol. + /// + /// The ``LinkDestinationSummary/referenceURL`` can have a "path" and "fragment" components that are different from what DocC sent in the ``RequestV2/link(_:)`` request. + /// For example; if your resolver supports different spellings of the link---corresponding to a page's different names in different language representations---you can return the common reference URL identifier for that page for all link spellings. + /// + /// - Note: DocC expects the resolved ``LinkDestinationSummary/referenceURL`` to have a "host" component that matches the ``DocumentationBundle/Identifier`` that your resolver provided in its initial ``identifierAndCapabilities(_:_:)`` handshake message. + /// Responding with a ``LinkDestinationSummary/referenceURL`` that doesn't match the resolver's provided ``DocumentationBundle/Identifier`` is undefined behavior. + case resolved(LinkDestinationSummary) + + // This empty-marker case is here because non-frozen enums are only available when Library Evolution is enabled, + // which is not available to Swift Packages without unsafe flags (rdar://78773361). + // This can be removed once that is available and applied to Swift-DocC (rdar://89033233). + @available(*, deprecated, message: """ + This enum is non-frozen and may be expanded in the future; add a `default` case, and do nothing in it, instead of matching this one. + Your external link resolver won't be passed new messages that it hasn't declared the corresponding capability for. + """) + case _nonFrozenEnum_useDefaultCase + + private enum CodingKeys: String, CodingKey { + // Default response keys + case identifier, capabilities + case failure + case resolved + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self = switch container.allKeys.first { + case .identifier?, .capabilities?: + .identifierAndCapabilities( + try container.decode(DocumentationBundle.Identifier.self, forKey: .identifier), + try container.decode(Capabilities.self, forKey: .capabilities) + ) + case .failure?: + .failure(try container.decode(DiagnosticInformation.self, forKey: .failure)) + case .resolved?: + .resolved(try container.decode(LinkDestinationSummary.self, forKey: .resolved)) + case nil: + throw OutOfProcessReferenceResolver.Error.invalidResponseKindFromClient + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .identifierAndCapabilities(let identifier, let capabilities): + try container.encode(identifier, forKey: .identifier) + try container.encode(capabilities, forKey: .capabilities) + + case .failure(errorMessage: let diagnosticInfo): + try container.encode(diagnosticInfo, forKey: .failure) + + case .resolved(let summary): + try container.encode(summary, forKey: .resolved) + + case ._nonFrozenEnum_useDefaultCase: + fatalError("Never use '_nonFrozenEnum_useDefaultCase' for anything.") + } + } + } +} + +extension OutOfProcessReferenceResolver.ResponseV2 { + /// Information about why the external resolver failed to resolve the `link(_:)`, or `symbol(_:)` request. + /// + /// - Note: DocC doesn't display any diagnostics about failed ``RequestV2/symbol(_:)`` requests because they wouldn't be actionable; + /// because they're entirely automatic---based on unique identifiers in the symbol graph files---rather than authored references in the content. + /// + /// Your external link resolver still needs to send a response to each request but it doesn't need to include details about the failure when responding to a ``RequestV2/symbol(_:)`` request. + public struct DiagnosticInformation: Codable { + /// A brief user-facing summary of the issue that caused the external resolver to fail. + public var summary: String + + /// A list of possible suggested solutions that can address the failure. + public var solutions: [Solution]? + + /// Creates a new value with information about why the external resolver failed to resolve the `link(_:)`, or `symbol(_:)` request. + /// - Parameters: + /// - summary: A brief user-facing summary of the issue that caused the external resolver to fail. + /// - solutions: Possible possible suggested solutions that can address the failure. + public init( + summary: String, + solutions: [Solution]? + ) { + self.summary = summary + self.solutions = solutions + } + + /// A possible solution to an external resolver issue. + public struct Solution: Codable { + /// A brief user-facing description of what the solution is. + public var summary: String + /// A full replacement of the link. + public var replacement: String? + + /// Creates a new solution to an external resolver issue + /// - Parameters: + /// - summary: A brief user-facing description of what the solution is. + /// - replacement: A full replacement of the link. + public init(summary: String, replacement: String?) { + self.summary = summary + self.replacement = replacement + } + } + } +} diff --git a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+DeprecatedCommunication.swift b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+DeprecatedCommunication.swift new file mode 100644 index 0000000000..153a9aa72d --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+DeprecatedCommunication.swift @@ -0,0 +1,350 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 +*/ + +public import Foundation +public import SymbolKit + +extension OutOfProcessReferenceResolver { + + // MARK: Request & Response + + /// An outdated version of a request message to send to the external link resolver. + /// + /// This can either be a request to resolve a topic URL or to resolve a symbol based on its precise identifier. + /// + /// @DeprecationSummary { + /// This version of the communication protocol is no longer recommended. Update to ``RequestV2`` and ``ResponseV2`` instead. + /// + /// The new version of the communication protocol both has a mechanism for expanding functionality in the future (through common ``Capabilities`` between DocC and the external resolver) and supports richer responses for both successful and and failed requests. + /// } + @available(*, deprecated, message: "This version of the communication protocol is no longer recommended. Update to `RequestV2` and `ResponseV2` instead.") + public typealias Request = _DeprecatedRequestV1 + + // Note this type isn't formally deprecated to avoid warnings in the ConvertService, which still _implicitly_ require this version of requests and responses. + public enum _DeprecatedRequestV1: Codable, CustomStringConvertible { + /// A request to resolve a topic URL + case topic(URL) + /// A request to resolve a symbol based on its precise identifier. + case symbol(String) + /// A request to resolve an asset. + case asset(AssetReference) + + private enum CodingKeys: CodingKey { + case topic + case symbol + case asset + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .topic(let url): + try container.encode(url, forKey: .topic) + case .symbol(let identifier): + try container.encode(identifier, forKey: .symbol) + case .asset(let assetReference): + try container.encode(assetReference, forKey: .asset) + } + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + switch container.allKeys.first { + case .topic?: + self = .topic(try container.decode(URL.self, forKey: .topic)) + case .symbol?: + self = .symbol(try container.decode(String.self, forKey: .symbol)) + case .asset?: + self = .asset(try container.decode(AssetReference.self, forKey: .asset)) + case nil: + throw OutOfProcessReferenceResolver.Error.unknownTypeOfRequest + } + } + + /// A plain text representation of the request message. + public var description: String { + switch self { + case .topic(let url): + return "topic: \(url.absoluteString.singleQuoted)" + case .symbol(let identifier): + return "symbol: \(identifier.singleQuoted)" + case .asset(let asset): + return "asset with name: \(asset.assetName), bundle identifier: \(asset.bundleID)" + } + } + } + + /// An outdated version of a response message from the external link resolver. + /// + /// @DeprecationSummary { + /// This version of the communication protocol is no longer recommended. Update to ``RequestV2`` and ``ResponseV2`` instead. + /// + /// The new version of the communication protocol both has a mechanism for expanding functionality in the future (through common ``Capabilities`` between DocC and the external resolver) and supports richer responses for both successful and and failed requests. + /// } + @available(*, deprecated, message: "This version of the communication protocol is no longer recommended. Update to `RequestV2` and `ResponseV2` instead.") + public typealias Response = _DeprecatedResponseV1 + + @available(*, deprecated, message: "This version of the communication protocol is no longer recommended. Update to `RequestV2` and `ResponseV2` instead.") + public enum _DeprecatedResponseV1: Codable { + /// A bundle identifier response. + /// + /// This message should only be sent once, after the external link resolver has launched. + case bundleIdentifier(String) + /// The error message of the problem that the external link resolver encountered while resolving the requested topic or symbol. + case errorMessage(String) + /// A response with the resolved information about the requested topic or symbol. + case resolvedInformation(ResolvedInformation) + /// A response with information about the resolved asset. + case asset(DataAsset) + + enum CodingKeys: String, CodingKey { + case bundleIdentifier + case errorMessage + case resolvedInformation + case asset + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + switch container.allKeys.first { + case .bundleIdentifier?: + self = .bundleIdentifier(try container.decode(String.self, forKey: .bundleIdentifier)) + case .errorMessage?: + self = .errorMessage(try container.decode(String.self, forKey: .errorMessage)) + case .resolvedInformation?: + self = .resolvedInformation(try container.decode(ResolvedInformation.self, forKey: .resolvedInformation)) + case .asset?: + self = .asset(try container.decode(DataAsset.self, forKey: .asset)) + case nil: + throw OutOfProcessReferenceResolver.Error.invalidResponseKindFromClient + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .bundleIdentifier(let bundleIdentifier): + try container.encode(bundleIdentifier, forKey: .bundleIdentifier) + case .errorMessage(let errorMessage): + try container.encode(errorMessage, forKey: .errorMessage) + case .resolvedInformation(let resolvedInformation): + try container.encode(resolvedInformation, forKey: .resolvedInformation) + case .asset(let assetReference): + try container.encode(assetReference, forKey: .asset) + } + } + } + + // MARK: Resolved Information + + /// A type used to transfer information about a resolved reference in the outdated and no longer recommended version of the external resolver communication protocol. + @available(*, deprecated, message: "This type is only used in the outdated, and no longer recommended, version of the out-of-process external resolver communication protocol.") + public struct ResolvedInformation: Codable { + /// Information about the resolved kind. + public let kind: DocumentationNode.Kind + /// Information about the resolved URL. + public let url: URL + /// Information about the resolved title. + public let title: String // DocumentationNode.Name + /// Information about the resolved abstract. + public let abstract: String // Markup + /// Information about the resolved language. + public let language: SourceLanguage + /// Information about the languages where the resolved node is available. + public let availableLanguages: Set + /// Information about the platforms and their versions where the resolved node is available, if any. + public let platforms: [PlatformAvailability]? + /// Information about the resolved declaration fragments, if any. + /// + /// This is expected to be an abbreviated declaration for the symbol. + public let declarationFragments: DeclarationFragments? + + // We use the real types here because they're Codable and don't have public member-wise initializers. + + /// Platform availability for a resolved symbol reference. + public typealias PlatformAvailability = AvailabilityRenderItem + + /// The declaration fragments for a resolved symbol reference. + public typealias DeclarationFragments = SymbolGraph.Symbol.DeclarationFragments + + /// The platform names, derived from the platform availability. + public var platformNames: Set? { + return platforms.map { platforms in Set(platforms.compactMap { $0.name }) } + } + + /// Images that are used to represent the summarized element. + public var topicImages: [TopicImage]? + + /// References used in the content of the summarized element. + public var references: [any RenderReference]? + + /// The variants of content (kind, url, title, abstract, language, declaration) for this resolver information. + public var variants: [Variant]? + + /// A value that indicates whether this symbol is under development and likely to change. + var isBeta: Bool { + guard let platforms, !platforms.isEmpty else { + return false + } + + return platforms.allSatisfy { $0.isBeta == true } + } + + /// Creates a new resolved information value with all its values. + /// + /// - Parameters: + /// - kind: The resolved kind. + /// - url: The resolved URL. + /// - title: The resolved title + /// - abstract: The resolved (plain text) abstract. + /// - language: The resolved language. + /// - availableLanguages: The languages where the resolved node is available. + /// - platforms: The platforms and their versions where the resolved node is available, if any. + /// - declarationFragments: The resolved declaration fragments, if any. This is expected to be an abbreviated declaration for the symbol. + /// - topicImages: Images that are used to represent the summarized element. + /// - references: References used in the content of the summarized element. + /// - variants: The variants of content for this resolver information. + public init( + kind: DocumentationNode.Kind, + url: URL, + title: String, + abstract: String, + language: SourceLanguage, + availableLanguages: Set, + platforms: [PlatformAvailability]? = nil, + declarationFragments: DeclarationFragments? = nil, + topicImages: [TopicImage]? = nil, + references: [any RenderReference]? = nil, + variants: [Variant]? = nil + ) { + self.kind = kind + self.url = url + self.title = title + self.abstract = abstract + self.language = language + self.availableLanguages = availableLanguages + self.platforms = platforms + self.declarationFragments = declarationFragments + self.topicImages = topicImages + self.references = references + self.variants = variants + } + + /// A variant of content for the resolved information. + /// + /// - Note: All properties except for ``traits`` are optional. If a property is `nil` it means that the value is the same as the resolved information's value. + public struct Variant: Codable { + /// The traits of the variant. + public let traits: [RenderNode.Variant.Trait] + + /// A wrapper for variant values that can either be specified, meaning the variant has a custom value, or not, meaning the variant has the same value as the resolved information. + /// + /// This alias is used to make the property declarations more explicit while at the same time offering the convenient syntax of optionals. + public typealias VariantValue = Optional + + /// The kind of the variant or `nil` if the kind is the same as the resolved information. + public let kind: VariantValue + /// The url of the variant or `nil` if the url is the same as the resolved information. + public let url: VariantValue + /// The title of the variant or `nil` if the title is the same as the resolved information. + public let title: VariantValue + /// The abstract of the variant or `nil` if the abstract is the same as the resolved information. + public let abstract: VariantValue + /// The language of the variant or `nil` if the language is the same as the resolved information. + public let language: VariantValue + /// The declaration fragments of the variant or `nil` if the declaration is the same as the resolved information. + /// + /// This is expected to be an abbreviated declaration for the symbol. + /// + /// If the resolver information has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. + public let declarationFragments: VariantValue + + /// Creates a new resolved information variant with the values that are different from the resolved information values. + /// + /// - Parameters: + /// - traits: The traits of the variant. + /// - kind: The resolved kind. + /// - url: The resolved URL. + /// - title: The resolved title + /// - abstract: The resolved (plain text) abstract. + /// - language: The resolved language. + /// - declarationFragments: The resolved declaration fragments, if any. This is expected to be an abbreviated declaration for the symbol. + public init( + traits: [RenderNode.Variant.Trait], + kind: VariantValue = nil, + url: VariantValue = nil, + title: VariantValue = nil, + abstract: VariantValue = nil, + language: VariantValue = nil, + declarationFragments: VariantValue = nil + ) { + self.traits = traits + self.kind = kind + self.url = url + self.title = title + self.abstract = abstract + self.language = language + self.declarationFragments = declarationFragments + } + } + } +} + +@available(*, deprecated, message: "This type is only used in the outdates, and no longer recommended, version of the out-of-process external resolver communication protocol.") +extension OutOfProcessReferenceResolver.ResolvedInformation { + enum CodingKeys: CodingKey { + case kind + case url + case title + case abstract + case language + case availableLanguages + case platforms + case declarationFragments + case topicImages + case references + case variants + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + kind = try container.decode(DocumentationNode.Kind.self, forKey: .kind) + url = try container.decode(URL.self, forKey: .url) + title = try container.decode(String.self, forKey: .title) + abstract = try container.decode(String.self, forKey: .abstract) + language = try container.decode(SourceLanguage.self, forKey: .language) + availableLanguages = try container.decode(Set.self, forKey: .availableLanguages) + platforms = try container.decodeIfPresent([OutOfProcessReferenceResolver.ResolvedInformation.PlatformAvailability].self, forKey: .platforms) + declarationFragments = try container.decodeIfPresent(OutOfProcessReferenceResolver.ResolvedInformation.DeclarationFragments.self, forKey: .declarationFragments) + topicImages = try container.decodeIfPresent([TopicImage].self, forKey: .topicImages) + references = try container.decodeIfPresent([CodableRenderReference].self, forKey: .references).map { decodedReferences in + decodedReferences.map(\.reference) + } + variants = try container.decodeIfPresent([OutOfProcessReferenceResolver.ResolvedInformation.Variant].self, forKey: .variants) + + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.kind, forKey: .kind) + try container.encode(self.url, forKey: .url) + try container.encode(self.title, forKey: .title) + try container.encode(self.abstract, forKey: .abstract) + try container.encode(self.language, forKey: .language) + try container.encode(self.availableLanguages, forKey: .availableLanguages) + try container.encodeIfPresent(self.platforms, forKey: .platforms) + try container.encodeIfPresent(self.declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(self.topicImages, forKey: .topicImages) + try container.encodeIfPresent(references?.map { CodableRenderReference($0) }, forKey: .references) + try container.encodeIfPresent(self.variants, forKey: .variants) + } +} diff --git a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift index fa3666edde..25523e6520 100644 --- a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -9,59 +9,94 @@ */ public import Foundation -import Markdown -public import SymbolKit +private import Markdown /// A reference resolver that launches and interactively communicates with another process or service to resolve links. /// /// If your external reference resolver or an external symbol resolver is implemented in another executable, you can use this object /// to communicate between DocC and the `docc` executable. /// -/// The launched executable is expected to follow the flow outlined below, sending ``OutOfProcessReferenceResolver/Request`` -/// and ``OutOfProcessReferenceResolver/Response`` values back and forth: +/// ## Launching and responding to requests /// -/// │ -/// 1 ▼ -/// ┌──────────────────┐ -/// │ Output bundle ID │ -/// └──────────────────┘ -/// │ -/// 2 ▼ -/// ┌──────────────────┐ -/// │ Wait for input │◀───┐ -/// └──────────────────┘ │ -/// │ │ -/// 3 ▼ │ repeat -/// ┌──────────────────┐ │ -/// │ Output resolved │ │ -/// │ information │────┘ -/// └──────────────────┘ +/// When creating an out-of-process resolver using ``init(processLocation:errorOutputHandler:)`` to communicate with another executable; +/// DocC launches your link resolver executable and declares _its_ own ``Capabilities`` as a raw value passed via the `--capabilities` option. +/// Your link resolver executable is expected to respond with a ``ResponseV2/identifierAndCapabilities(_:_:)`` message that declares: +/// - The documentation bundle identifier that the executable can to resolve links for. +/// - The capabilities that the resolver supports. /// -/// When resolving against a server, the server is expected to be able to handle messages of type "resolve-reference" with a -/// ``OutOfProcessReferenceResolver/Request`` payload and respond with messages of type "resolved-reference-response" -/// with a ``OutOfProcessReferenceResolver/Response`` payload. +/// After this "handshake" your link resolver executable is expected to wait for ``RequestV2`` messages from DocC and respond with exactly one ``ResponseV2`` per message. +/// A visual representation of this flow of execution can be seen in the diagram below: +/// +/// DocC link resolver executable +/// ┌─┐ ╎ +/// │ ├─────────── Launch ──────────▶┴┐ +/// │ │ --capabilities │ │ +/// │ │ │ │ +/// │ ◀───────── Handshake ─────────┤ │ +/// │ │ { "identifier" : ... , │ │ +/// │ │ "capabilities" : ... } │ │ +/// ┏ loop ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +/// ┃ │ │ │ │ ┃ +/// ┃ │ ├────────── Request ──────────▶ │ ┃ +/// ┃ │ │ { "link" : ... } OR │ │ ┃ +/// ┃ │ │ { "symbol" : ... } │ │ ┃ +/// ┃ │ │ │ │ ┃ +/// ┃ │ ◀────────── Response ─────────┤ │ ┃ +/// ┃ │ │ { "resolved" : ... } OR │ │ ┃ +/// ┃ │ │ { "failure" : ... } │ │ ┃ +/// ┃ │ │ │ │ ┃ +/// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +/// │ │ └─┘ +/// │ │ ╎ +/// +/// ## Interacting with a Convert Service +/// +/// When creating an out-of-process resolver using ``init(bundleID:server:convertRequestIdentifier:)`` to communicate with another process using a ``ConvertService``; +/// DocC sends that service `"resolve-reference"` messages with a``OutOfProcessReferenceResolver/Request`` payload and expects a `"resolved-reference-response"` responses with a ``OutOfProcessReferenceResolver/Response`` payload. +/// +/// Because the ``ConvertService`` messages are _implicitly_ tied to these outdated—and no longer recommended—request and response types, the richness of its responses is limited. +/// +/// - Note: when interacting with a ``ConvertService`` your service also needs to handle "asset" requests (``OutOfProcessReferenceResolver/Request/asset(_:)`` and responses that (``OutOfProcessReferenceResolver/Response/asset(_:)``) that link resolver executables don't need to handle. +/// +/// ## Topics +/// +/// ### Messages +/// +/// Requests that DocC sends to your link resolver executable and the responses that it should send back. +/// +/// - ``RequestV2`` +/// - ``ResponseV2`` +/// +/// ### Finding common capabilities +/// +/// Ways that your link resolver executable can signal any optional capabilities that it supports. +/// +/// - ``ResponseV2/identifierAndCapabilities(_:_:)`` +/// - ``Capabilities`` +/// +/// ### Deprecated messages +/// +/// - ``Request`` +/// - ``Response`` /// /// ## See Also -/// - ``ExternalDocumentationSource`` -/// - ``GlobalExternalSymbolResolver`` /// - ``DocumentationContext/externalDocumentationSources`` /// - ``DocumentationContext/globalExternalSymbolResolver`` -/// - ``Request`` -/// - ``Response`` public class OutOfProcessReferenceResolver: ExternalDocumentationSource, GlobalExternalSymbolResolver { - private let externalLinkResolvingClient: any ExternalLinkResolving + private var implementation: any _Implementation - @available(*, deprecated, renamed: "id", message: "Use 'id' instead. This deprecated API will be removed after 6.2 is released") - public var bundleIdentifier: String { - bundleID.rawValue + /// The bundle identifier for the reference resolver in the other process. + public var bundleID: DocumentationBundle.Identifier { + implementation.bundleID } - /// The bundle identifier for the reference resolver in the other process. - public let bundleID: DocumentationBundle.Identifier + // This variable is used below for the `ConvertServiceFallbackResolver` conformance. + private var assetCache: [AssetReference: DataAsset] = [:] /// Creates a new reference resolver that interacts with another executable. /// /// Initializing the resolver will also launch the other executable. The other executable will remain running for the lifetime of this object. + /// This and the rest of the communication between DocC and the link resolver executable is described in /// /// - Parameters: /// - processLocation: The location of the other executable. @@ -78,19 +113,12 @@ public class OutOfProcessReferenceResolver: ExternalDocumentationSource, GlobalE let longRunningProcess = try LongRunningProcess(location: processLocation, errorOutputHandler: errorOutputHandler) - guard case let .bundleIdentifier(decodedBundleIdentifier) = try longRunningProcess.sendAndWait(request: nil as Request?) as Response else { + guard let handshake: InitialHandshakeMessage = try? longRunningProcess.readInitialHandshakeMessage() else { throw Error.invalidBundleIdentifierOutputFromExecutable(processLocation) } - self.bundleID = .init(rawValue: decodedBundleIdentifier) - self.externalLinkResolvingClient = longRunningProcess - } - - @available(*, deprecated, renamed: "init(bundleID:server:convertRequestIdentifier:)", message: "Use 'init(bundleID:server:convertRequestIdentifier:)' instead. This deprecated API will be removed after 6.2 is released") - public init(bundleIdentifier: String, server: DocumentationServer, convertRequestIdentifier: String?) throws { - self.bundleID = .init(rawValue: bundleIdentifier) - self.externalLinkResolvingClient = LongRunningService( - server: server, convertRequestIdentifier: convertRequestIdentifier) + // This private type and protocol exist to silence deprecation warnings + self.implementation = (_ImplementationProvider() as (any _ImplementationProviding)).makeImplementation(for: handshake, longRunningProcess: longRunningProcess) } /// Creates a new reference resolver that interacts with a documentation service. @@ -102,174 +130,372 @@ public class OutOfProcessReferenceResolver: ExternalDocumentationSource, GlobalE /// - server: The server to send link resolution requests to. /// - convertRequestIdentifier: The identifier that the resolver will use for convert requests that it sends to the server. public init(bundleID: DocumentationBundle.Identifier, server: DocumentationServer, convertRequestIdentifier: String?) throws { - self.bundleID = bundleID - self.externalLinkResolvingClient = LongRunningService( - server: server, convertRequestIdentifier: convertRequestIdentifier) + self.implementation = (_ImplementationProvider() as any _ImplementationProviding).makeImplementation( + for: .init(identifier: bundleID, capabilities: nil /* always use the V1 implementation */), + longRunningProcess: LongRunningService(server: server, convertRequestIdentifier: convertRequestIdentifier) + ) } - // MARK: External Reference Resolver - - public func resolve(_ reference: TopicReference) -> TopicReferenceResolutionResult { - switch reference { - case .resolved(let resolved): - return resolved + fileprivate struct InitialHandshakeMessage: Decodable { + var identifier: DocumentationBundle.Identifier + var capabilities: Capabilities? // The old V1 handshake didn't include this but the V2 requires it. + + init(identifier: DocumentationBundle.Identifier, capabilities: OutOfProcessReferenceResolver.Capabilities?) { + self.identifier = identifier + self.capabilities = capabilities + } + + private enum CodingKeys: CodingKey { + case bundleIdentifier // Legacy V1 handshake + case identifier, capabilities // V2 handshake + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) - case let .unresolved(unresolvedReference): - guard unresolvedReference.bundleID == bundleID else { - fatalError(""" - Attempted to resolve a local reference externally: \(unresolvedReference.description.singleQuoted). - DocC should never pass a reference to an external resolver unless it matches that resolver's bundle identifier. - """) - } - do { - guard let unresolvedTopicURL = unresolvedReference.topicURL.components.url else { - // Return the unresolved reference if the underlying URL is not valid - return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo("URL \(unresolvedReference.topicURL.absoluteString.singleQuoted) is not valid.")) - } - let resolvedInformation = try resolveInformationForTopicURL(unresolvedTopicURL) - return .success( resolvedReference(for: resolvedInformation) ) - } catch let error { - return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo(error)) + guard container.contains(.identifier) || container.contains(.bundleIdentifier) else { + throw DecodingError.keyNotFound(CodingKeys.identifier, .init(codingPath: decoder.codingPath, debugDescription: """ + Initial handshake message includes neither a '\(CodingKeys.identifier.stringValue)' key nor a '\(CodingKeys.bundleIdentifier.stringValue)' key. + """)) } + + self.identifier = try container.decodeIfPresent(DocumentationBundle.Identifier.self, forKey: .identifier) + ?? container.decode(DocumentationBundle.Identifier.self, forKey: .bundleIdentifier) + + self.capabilities = try container.decodeIfPresent(Capabilities.self, forKey: .capabilities) } } + // MARK: External Reference Resolver + + public func resolve(_ reference: TopicReference) -> TopicReferenceResolutionResult { + implementation.resolve(reference) + } + @_spi(ExternalLinks) // LinkResolver.ExternalEntity isn't stable API yet public func entity(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity { - guard let resolvedInformation = referenceCache[reference.url] else { - fatalError("A topic reference that has already been resolved should always exist in the cache.") - } - return makeEntity(with: resolvedInformation, reference: reference.absoluteString) + implementation.entity(with: reference) } @_spi(ExternalLinks) // LinkResolver.ExternalEntity isn't stable API yet public func symbolReferenceAndEntity(withPreciseIdentifier preciseIdentifier: String) -> (ResolvedTopicReference, LinkResolver.ExternalEntity)? { - guard let resolvedInformation = try? resolveInformationForSymbolIdentifier(preciseIdentifier) else { return nil } - - let reference = ResolvedTopicReference( - bundleID: "com.externally.resolved.symbol", - path: "/\(preciseIdentifier)", - sourceLanguages: sourceLanguages(for: resolvedInformation) - ) - let entity = makeEntity(with: resolvedInformation, reference: reference.absoluteString) - return (reference, entity) + implementation.symbolReferenceAndEntity(withPreciseIdentifier: preciseIdentifier) } +} + +// MARK: Implementations + +private protocol _Implementation: ExternalDocumentationSource, GlobalExternalSymbolResolver { + var bundleID: DocumentationBundle.Identifier { get } + var longRunningProcess: any ExternalLinkResolving { get } - private func makeEntity(with resolvedInformation: ResolvedInformation, reference: String) -> LinkResolver.ExternalEntity { - let (kind, role) = DocumentationContentRenderer.renderKindAndRole(resolvedInformation.kind, semantic: nil) - - var renderReference = TopicRenderReference( - identifier: .init(reference), - title: resolvedInformation.title, - // The resolved information only stores the plain text abstract https://github.com/swiftlang/swift-docc/issues/802 - abstract: [.text(resolvedInformation.abstract)], - url: resolvedInformation.url.path, - kind: kind, - role: role, - fragments: resolvedInformation.declarationFragments?.declarationFragments.map { DeclarationRenderSection.Token(fragment: $0, identifier: nil) }, - isBeta: resolvedInformation.isBeta, - isDeprecated: (resolvedInformation.platforms ?? []).contains(where: { $0.deprecated != nil }), - images: resolvedInformation.topicImages ?? [] - ) - for variant in resolvedInformation.variants ?? [] { - if let title = variant.title { - renderReference.titleVariants.variants.append( - .init(traits: variant.traits, patch: [.replace(value: title)]) - ) - } - if let abstract = variant.abstract { - renderReference.abstractVariants.variants.append( - .init(traits: variant.traits, patch: [.replace(value: [.text(abstract)])]) - ) + // + func resolve(unresolvedReference: UnresolvedTopicReference) throws -> TopicReferenceResolutionResult +} + +private extension _Implementation { + // Avoid some common boilerplate between implementations. + func resolve(_ reference: TopicReference) -> TopicReferenceResolutionResult { + switch reference { + case .resolved(let resolved): + return resolved + + case let .unresolved(unresolvedReference): + guard unresolvedReference.bundleID == bundleID else { + fatalError(""" + Attempted to resolve a local reference externally: \(unresolvedReference.description.singleQuoted). + DocC should never pass a reference to an external resolver unless it matches that resolver's bundle identifier. + """) + } + do { + // This is where each implementation differs + return try resolve(unresolvedReference: unresolvedReference) + } catch let error { + return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo(error)) + } + } + } +} + +// This private protocol allows the out-of-process resolver to create ImplementationV1 without deprecation warnings +private protocol _ImplementationProviding { + func makeImplementation(for handshake: OutOfProcessReferenceResolver.InitialHandshakeMessage, longRunningProcess: any ExternalLinkResolving) -> any _Implementation +} + +private extension OutOfProcessReferenceResolver { + // A concrete type with a deprecated implementation that can be cast to `_ImplementationProviding` to avoid deprecation warnings. + struct _ImplementationProvider: _ImplementationProviding { + @available(*, deprecated) // The V1 implementation is built around several now-deprecated types. This deprecation silences those depreciation warnings. + func makeImplementation(for handshake: OutOfProcessReferenceResolver.InitialHandshakeMessage, longRunningProcess: any ExternalLinkResolving) -> any _Implementation { + if let capabilities = handshake.capabilities { + return ImplementationV2(longRunningProcess: longRunningProcess, bundleID: handshake.identifier, executableCapabilities: capabilities) + } else { + return ImplementationV1(longRunningProcess: longRunningProcess, bundleID: handshake.identifier) } - if let declarationFragments = variant.declarationFragments { - renderReference.fragmentsVariants.variants.append( - .init(traits: variant.traits, patch: [.replace(value: declarationFragments?.declarationFragments.map { DeclarationRenderSection.Token(fragment: $0, identifier: nil) })]) - ) + } + } +} + +// MARK: Version 1 (deprecated) + +extension OutOfProcessReferenceResolver { + /// The original—no longer recommended—version of the out-of-process resolver implementation. + /// + /// This implementation uses ``Request`` and ``Response`` which aren't extensible and have restrictions on the details of the response payloads. + @available(*, deprecated) // The V1 implementation is built around several now-deprecated types. This deprecation silences those depreciation warnings. + private final class ImplementationV1: _Implementation { + let bundleID: DocumentationBundle.Identifier + let longRunningProcess: any ExternalLinkResolving + + init(longRunningProcess: any ExternalLinkResolving, bundleID: DocumentationBundle.Identifier) { + self.longRunningProcess = longRunningProcess + self.bundleID = bundleID + } + + // This is fileprivate so that the ConvertService conformance below can access it. + fileprivate private(set) var referenceCache: [URL: ResolvedInformation] = [:] + private var symbolCache: [String: ResolvedInformation] = [:] + + func resolve(unresolvedReference: UnresolvedTopicReference) throws -> TopicReferenceResolutionResult { + guard let unresolvedTopicURL = unresolvedReference.topicURL.components.url else { + // Return the unresolved reference if the underlying URL is not valid + return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo("URL \(unresolvedReference.topicURL.absoluteString.singleQuoted) is not valid.")) } + let resolvedInformation = try resolveInformationForTopicURL(unresolvedTopicURL) + return .success( resolvedReference(for: resolvedInformation) ) } - let dependencies = RenderReferenceDependencies( - topicReferences: [], - linkReferences: (resolvedInformation.references ?? []).compactMap { $0 as? LinkReference }, - imageReferences: (resolvedInformation.references ?? []).compactMap { $0 as? ImageReference } - ) - return LinkResolver.ExternalEntity(topicRenderReference: renderReference, renderReferenceDependencies: dependencies, sourceLanguages: resolvedInformation.availableLanguages) - } - - // MARK: Implementation - - private var referenceCache: [URL: ResolvedInformation] = [:] - private var symbolCache: [String: ResolvedInformation] = [:] - private var assetCache: [AssetReference: DataAsset] = [:] - - /// Makes a call to the other process to resolve information about a page based on its URL. - func resolveInformationForTopicURL(_ topicURL: URL) throws -> ResolvedInformation { - if let cachedInformation = referenceCache[topicURL] { - return cachedInformation + func entity(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity { + guard let resolvedInformation = referenceCache[reference.url] else { + fatalError("A topic reference that has already been resolved should always exist in the cache.") + } + return makeEntity(with: resolvedInformation, reference: reference.absoluteString) } - let response: Response = try externalLinkResolvingClient.sendAndWait(request: Request.topic(topicURL)) + func symbolReferenceAndEntity(withPreciseIdentifier preciseIdentifier: String) -> (ResolvedTopicReference, LinkResolver.ExternalEntity)? { + guard let resolvedInformation = try? resolveInformationForSymbolIdentifier(preciseIdentifier) else { return nil } + + let reference = ResolvedTopicReference( + bundleID: "com.externally.resolved.symbol", + path: "/\(preciseIdentifier)", + sourceLanguages: sourceLanguages(for: resolvedInformation) + ) + let entity = makeEntity(with: resolvedInformation, reference: reference.absoluteString) + return (reference, entity) + } - switch response { - case .bundleIdentifier: - throw Error.executableSentBundleIdentifierAgain + /// Makes a call to the other process to resolve information about a page based on its URL. + private func resolveInformationForTopicURL(_ topicURL: URL) throws -> ResolvedInformation { + if let cachedInformation = referenceCache[topicURL] { + return cachedInformation + } - case .errorMessage(let errorMessage): - throw Error.forwardedErrorFromClient(errorMessage: errorMessage) + let response: Response = try longRunningProcess.sendAndWait(request: Request.topic(topicURL)) + + switch response { + case .bundleIdentifier: + throw Error.executableSentBundleIdentifierAgain + + case .errorMessage(let errorMessage): + throw Error.forwardedErrorFromClient(errorMessage: errorMessage) + + case .resolvedInformation(let resolvedInformation): + // Cache the information for the resolved reference, that's what's will be used when returning the entity later. + let resolvedReference = resolvedReference(for: resolvedInformation) + referenceCache[resolvedReference.url] = resolvedInformation + return resolvedInformation + + default: + throw Error.unexpectedResponse(response: response, requestDescription: "topic URL") + } + } + + /// Makes a call to the other process to resolve information about a symbol based on its precise identifier. + private func resolveInformationForSymbolIdentifier(_ preciseIdentifier: String) throws -> ResolvedInformation { + if let cachedInformation = symbolCache[preciseIdentifier] { + return cachedInformation + } - case .resolvedInformation(let resolvedInformation): - // Cache the information for the resolved reference, that's what's will be used when returning the entity later. - let resolvedReference = resolvedReference(for: resolvedInformation) - referenceCache[resolvedReference.url] = resolvedInformation - return resolvedInformation + let response: Response = try longRunningProcess.sendAndWait(request: Request.symbol(preciseIdentifier)) - default: - throw Error.unexpectedResponse(response: response, requestDescription: "topic URL") + switch response { + case .bundleIdentifier: + throw Error.executableSentBundleIdentifierAgain + + case .errorMessage(let errorMessage): + throw Error.forwardedErrorFromClient(errorMessage: errorMessage) + + case .resolvedInformation(let resolvedInformation): + symbolCache[preciseIdentifier] = resolvedInformation + return resolvedInformation + + default: + throw Error.unexpectedResponse(response: response, requestDescription: "symbol ID") + } + } + + private func resolvedReference(for resolvedInformation: ResolvedInformation) -> ResolvedTopicReference { + return ResolvedTopicReference( + bundleID: bundleID, + path: resolvedInformation.url.path, + fragment: resolvedInformation.url.fragment, + sourceLanguages: sourceLanguages(for: resolvedInformation) + ) + } + + private func sourceLanguages(for resolvedInformation: ResolvedInformation) -> Set { + // It is expected that the available languages contains the main language + return resolvedInformation.availableLanguages.union(CollectionOfOne(resolvedInformation.language)) + } + + private func makeEntity(with resolvedInformation: ResolvedInformation, reference: String) -> LinkResolver.ExternalEntity { + return LinkResolver.ExternalEntity( + kind: resolvedInformation.kind, + language: resolvedInformation.language, + relativePresentationURL: resolvedInformation.url.withoutHostAndPortAndScheme(), + referenceURL: URL(string: reference)!, + title: resolvedInformation.title, + // The resolved information only stores the plain text abstract and can't be changed. Use the version 2 communication protocol to support rich abstracts. + abstract: [.text(resolvedInformation.abstract)], + availableLanguages: resolvedInformation.availableLanguages, + platforms: resolvedInformation.platforms, + taskGroups: nil, + usr: nil, + declarationFragments: resolvedInformation.declarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, + redirects: nil, + topicImages: resolvedInformation.topicImages, + references: resolvedInformation.references, + variants: (resolvedInformation.variants ?? []).map { variant in + .init( + traits: variant.traits, + kind: variant.kind, + language: variant.language, + relativePresentationURL: variant.url?.withoutHostAndPortAndScheme(), + title: variant.title, + abstract: variant.abstract.map { [.text($0)] }, + taskGroups: nil, + usr: nil, + declarationFragments: variant.declarationFragments.map { fragments in + fragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) } + } + ) + } + ) } } - - /// Makes a call to the other process to resolve information about a symbol based on its precise identifier. - private func resolveInformationForSymbolIdentifier(_ preciseIdentifier: String) throws -> ResolvedInformation { - if let cachedInformation = symbolCache[preciseIdentifier] { - return cachedInformation +} + +// MARK: Version 2 + +extension OutOfProcessReferenceResolver { + private final class ImplementationV2: _Implementation { + let longRunningProcess: any ExternalLinkResolving + let bundleID: DocumentationBundle.Identifier + let executableCapabilities: Capabilities + + init( + longRunningProcess: any ExternalLinkResolving, + bundleID: DocumentationBundle.Identifier, + executableCapabilities: Capabilities + ) { + self.longRunningProcess = longRunningProcess + self.bundleID = bundleID + self.executableCapabilities = executableCapabilities + } + + private var linkCache: [String /* either a USR or an absolute UnresolvedTopicReference */: LinkDestinationSummary] = [:] + + func resolve(unresolvedReference: UnresolvedTopicReference) throws -> TopicReferenceResolutionResult { + let unresolvedReferenceString = unresolvedReference.topicURL.absoluteString + if let cachedSummary = linkCache[unresolvedReferenceString] { + return .success( makeReference(for: cachedSummary) ) + } + + let linkString = String( + unresolvedReferenceString.dropFirst(6) // "doc://" + .drop(while: { $0 != "/" }) // the known identifier (host component) + ) + let response: ResponseV2 = try longRunningProcess.sendAndWait(request: RequestV2.link(linkString)) + + switch response { + case .identifierAndCapabilities: + throw Error.executableSentBundleIdentifierAgain + + case .failure(let diagnosticMessage): + let prefixLength = 2 /* for "//" */ + bundleID.rawValue.utf8.count + let solutions: [Solution] = (diagnosticMessage.solutions ?? []).map { + Solution(summary: $0.summary, replacements: $0.replacement.map { replacement in + [Replacement( + // The replacement ranges are relative to the link itself. + // To replace only the path and fragment portion of the link, we create a range from 0 to the relative link string length, both offset by the bundle ID length + range: SourceLocation(line: 0, column: prefixLength, source: nil) ..< SourceLocation(line: 0, column: linkString.utf8.count + prefixLength, source: nil), + replacement: replacement + )] + } ?? []) + } + return .failure( + unresolvedReference, + TopicReferenceResolutionErrorInfo(diagnosticMessage.summary, solutions: solutions) + ) + + case .resolved(let linkSummary): + // Cache the information for the original authored link + linkCache[unresolvedReferenceString] = linkSummary + // Cache the information for the resolved reference. That's what's will be used when returning the entity later. + let reference = makeReference(for: linkSummary) + linkCache[reference.absoluteString] = linkSummary + if let usr = linkSummary.usr { + // If the page is a symbol, cache its information for the USR as well. + linkCache[usr] = linkSummary + } + return .success(reference) + + default: + throw Error.unexpectedResponse(response: response, requestDescription: "topic link") + } } - let response: Response = try externalLinkResolvingClient.sendAndWait(request: Request.symbol(preciseIdentifier)) + func entity(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity { + guard let linkSummary = linkCache[reference.url.standardized.absoluteString] else { + fatalError("A topic reference that has already been resolved should always exist in the cache.") + } + return linkSummary + } - switch response { - case .bundleIdentifier: - throw Error.executableSentBundleIdentifierAgain + func symbolReferenceAndEntity(withPreciseIdentifier preciseIdentifier: String) -> (ResolvedTopicReference, LinkResolver.ExternalEntity)? { + if let cachedSummary = linkCache[preciseIdentifier] { + return (makeReference(for: cachedSummary), cachedSummary) + } - case .errorMessage(let errorMessage): - throw Error.forwardedErrorFromClient(errorMessage: errorMessage) + guard case ResponseV2.resolved(let linkSummary)? = try? longRunningProcess.sendAndWait(request: RequestV2.symbol(preciseIdentifier)) else { + return nil + } + + // Cache the information for the USR + linkCache[preciseIdentifier] = linkSummary - case .resolvedInformation(let resolvedInformation): - symbolCache[preciseIdentifier] = resolvedInformation - return resolvedInformation + // Cache the information for the resolved reference. + let reference = makeReference(for: linkSummary) + linkCache[reference.absoluteString] = linkSummary - default: - throw Error.unexpectedResponse(response: response, requestDescription: "symbol ID") + return (reference, linkSummary) + } + + private func makeReference(for linkSummary: LinkDestinationSummary) -> ResolvedTopicReference { + ResolvedTopicReference( + bundleID: linkSummary.referenceURL.host.map { .init(rawValue: $0) } ?? "unknown", + path: linkSummary.referenceURL.path, + fragment: linkSummary.referenceURL.fragment, + sourceLanguages: linkSummary.availableLanguages + ) } - } - - private func resolvedReference(for resolvedInformation: ResolvedInformation) -> ResolvedTopicReference { - return ResolvedTopicReference( - bundleID: bundleID, - path: resolvedInformation.url.path, - fragment: resolvedInformation.url.fragment, - sourceLanguages: sourceLanguages(for: resolvedInformation) - ) - } - - private func sourceLanguages(for resolvedInformation: ResolvedInformation) -> Set { - // It is expected that the available languages contains the main language - return resolvedInformation.availableLanguages.union(CollectionOfOne(resolvedInformation.language)) } } +// MARK: Cross process communication + private protocol ExternalLinkResolving { - func sendAndWait(request: Request?) throws -> Response + func sendAndWait(request: Request) throws -> Response } private class LongRunningService: ExternalLinkResolving { @@ -280,7 +506,7 @@ private class LongRunningService: ExternalLinkResolving { server: server, convertRequestIdentifier: convertRequestIdentifier) } - func sendAndWait(request: Request?) throws -> Response { + func sendAndWait(request: Request) throws -> Response { let responseData = try client.sendAndWait(request) return try JSONDecoder().decode(Response.self, from: responseData) } @@ -297,6 +523,7 @@ private class LongRunningProcess: ExternalLinkResolving { init(location: URL, errorOutputHandler: @escaping (String) -> Void) throws { let process = Process() process.executableURL = location + process.arguments = ["--capabilities", "\(OutOfProcessReferenceResolver.Capabilities().rawValue)"] process.standardInput = input process.standardOutput = output @@ -308,7 +535,7 @@ private class LongRunningProcess: ExternalLinkResolving { errorReadSource.setEventHandler { [errorOutput] in let data = errorOutput.fileHandleForReading.availableData let errorMessage = String(data: data, encoding: .utf8) - ?? "<\(ByteCountFormatter.string(fromByteCount: Int64(data.count), countStyle: .memory)) of non-utf8 data>" + ?? "<\(ByteCountFormatter.string(fromByteCount: Int64(data.count), countStyle: .memory)) of non-utf8 data>" errorOutputHandler(errorMessage) } @@ -326,16 +553,25 @@ private class LongRunningProcess: ExternalLinkResolving { private let output = Pipe() private let errorOutput = Pipe() private let errorReadSource: any DispatchSourceRead - - func sendAndWait(request: Request?) throws -> Response { - if let request { - guard let requestString = String(data: try JSONEncoder().encode(request), encoding: .utf8)?.appending("\n"), - let requestData = requestString.data(using: .utf8) - else { - throw OutOfProcessReferenceResolver.Error.unableToEncodeRequestToClient(requestDescription: request.description) - } - input.fileHandleForWriting.write(requestData) + + func readInitialHandshakeMessage() throws -> Response { + return try _readResponse() + } + + func sendAndWait(request: Request) throws -> Response { + // Send + guard let requestString = String(data: try JSONEncoder().encode(request), encoding: .utf8)?.appending("\n"), + let requestData = requestString.data(using: .utf8) + else { + throw OutOfProcessReferenceResolver.Error.unableToEncodeRequestToClient(requestDescription: "\(request)") } + input.fileHandleForWriting.write(requestData) + + // Receive + return try _readResponse() + } + + private func _readResponse() throws -> Response { var response = output.fileHandleForReading.availableData guard !response.isEmpty else { throw OutOfProcessReferenceResolver.Error.processDidExit(code: Int(process.terminationStatus)) @@ -348,8 +584,8 @@ private class LongRunningProcess: ExternalLinkResolving { // To avoid blocking forever we check if the response can be decoded after each chunk of data. return try JSONDecoder().decode(Response.self, from: response) } catch { - if case DecodingError.dataCorrupted = error, // If the data wasn't valid JSON, read more data and try to decode it again. - response.count.isMultiple(of: Int(PIPE_BUF)) // To reduce the risk of deadlocking, check that bytes so far is a multiple of the pipe buffer size. + if case DecodingError.dataCorrupted = error, // If the data wasn't valid JSON, read more data and try to decode it again. + response.count.isMultiple(of: Int(PIPE_BUF)) // To reduce the risk of deadlocking, check that bytes so far is a multiple of the pipe buffer size. { let moreResponseData = output.fileHandleForReading.availableData guard !moreResponseData.isEmpty else { @@ -358,7 +594,7 @@ private class LongRunningProcess: ExternalLinkResolving { response += moreResponseData continue } - + // Other errors are re-thrown as wrapped errors. throw OutOfProcessReferenceResolver.Error.unableToDecodeResponseFromClient(response, error) } @@ -371,13 +607,19 @@ private class LongRunningProcess: ExternalLinkResolving { fatalError("Cannot initialize an out of process resolver outside of macOS or Linux platforms.") } - func sendAndWait(request: Request?) throws -> Response { + func readInitialHandshakeMessage() throws -> Response { + fatalError("Cannot call sendAndWait in non macOS/Linux platform.") + } + + func sendAndWait(request: Request) throws -> Response { fatalError("Cannot call sendAndWait in non macOS/Linux platform.") } #endif } +// MARK: Error + extension OutOfProcessReferenceResolver { /// Errors that may occur when communicating with an external reference resolver. enum Error: Swift.Error, DescribedError { @@ -407,7 +649,7 @@ extension OutOfProcessReferenceResolver { /// The request type was not known (neither 'topic' nor 'symbol'). case unknownTypeOfRequest /// Received an unknown type of response to sent request. - case unexpectedResponse(response: Response, requestDescription: String) + case unexpectedResponse(response: Any, requestDescription: String) /// A plain text representation of the error message. var errorDescription: String { @@ -442,360 +684,46 @@ extension OutOfProcessReferenceResolver { } } -extension OutOfProcessReferenceResolver { - - // MARK: Request & Response - - /// A request message to send to the external link resolver. - /// - /// This can either be a request to resolve a topic URL or to resolve a symbol based on its precise identifier. - public enum Request: Codable, CustomStringConvertible { - /// A request to resolve a topic URL - case topic(URL) - /// A request to resolve a symbol based on its precise identifier. - case symbol(String) - /// A request to resolve an asset. - case asset(AssetReference) - - private enum CodingKeys: CodingKey { - case topic - case symbol - case asset - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case .topic(let url): - try container.encode(url, forKey: .topic) - case .symbol(let identifier): - try container.encode(identifier, forKey: .symbol) - case .asset(let assetReference): - try container.encode(assetReference, forKey: .asset) - } - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - switch container.allKeys.first { - case .topic?: - self = .topic(try container.decode(URL.self, forKey: .topic)) - case .symbol?: - self = .symbol(try container.decode(String.self, forKey: .symbol)) - case .asset?: - self = .asset(try container.decode(AssetReference.self, forKey: .asset)) - case nil: - throw OutOfProcessReferenceResolver.Error.unknownTypeOfRequest - } - } - - /// A plain text representation of the request message. - public var description: String { - switch self { - case .topic(let url): - return "topic: \(url.absoluteString.singleQuoted)" - case .symbol(let identifier): - return "symbol: \(identifier.singleQuoted)" - case .asset(let asset): - return "asset with name: \(asset.assetName), bundle identifier: \(asset.bundleID)" - } - } - } - - /// A response message from the external link resolver. - public enum Response: Codable { - /// A bundle identifier response. - /// - /// This message should only be sent once, after the external link resolver has launched. - case bundleIdentifier(String) - /// The error message of the problem that the external link resolver encountered while resolving the requested topic or symbol. - case errorMessage(String) - /// A response with the resolved information about the requested topic or symbol. - case resolvedInformation(ResolvedInformation) - /// A response with information about the resolved asset. - case asset(DataAsset) - - enum CodingKeys: String, CodingKey { - case bundleIdentifier - case errorMessage - case resolvedInformation - case asset - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - switch container.allKeys.first { - case .bundleIdentifier?: - self = .bundleIdentifier(try container.decode(String.self, forKey: .bundleIdentifier)) - case .errorMessage?: - self = .errorMessage(try container.decode(String.self, forKey: .errorMessage)) - case .resolvedInformation?: - self = .resolvedInformation(try container.decode(ResolvedInformation.self, forKey: .resolvedInformation)) - case .asset?: - self = .asset(try container.decode(DataAsset.self, forKey: .asset)) - case nil: - throw OutOfProcessReferenceResolver.Error.invalidResponseKindFromClient - } - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case .bundleIdentifier(let bundleIdentifier): - try container.encode(bundleIdentifier, forKey: .bundleIdentifier) - case .errorMessage(let errorMessage): - try container.encode(errorMessage, forKey: .errorMessage) - case .resolvedInformation(let resolvedInformation): - try container.encode(resolvedInformation, forKey: .resolvedInformation) - case .asset(let assetReference): - try container.encode(assetReference, forKey: .asset) - } - } - } - - // MARK: Resolved Information - - /// A type used to transfer information about a resolved reference to DocC from from a reference resolver in another executable. - public struct ResolvedInformation: Codable { - // This type is duplicating the information from LinkDestinationSummary with some minor differences. - // Changes generally need to be made in both places. It would be good to replace this with LinkDestinationSummary. - // FIXME: https://github.com/swiftlang/swift-docc/issues/802 - - /// Information about the resolved kind. - public let kind: DocumentationNode.Kind - /// Information about the resolved URL. - public let url: URL - /// Information about the resolved title. - public let title: String // DocumentationNode.Name - /// Information about the resolved abstract. - public let abstract: String // Markup - /// Information about the resolved language. - public let language: SourceLanguage - /// Information about the languages where the resolved node is available. - public let availableLanguages: Set - /// Information about the platforms and their versions where the resolved node is available, if any. - public let platforms: [PlatformAvailability]? - /// Information about the resolved declaration fragments, if any. - public let declarationFragments: DeclarationFragments? - - // We use the real types here because they're Codable and don't have public member-wise initializers. - - /// Platform availability for a resolved symbol reference. - public typealias PlatformAvailability = AvailabilityRenderItem - - /// The declaration fragments for a resolved symbol reference. - public typealias DeclarationFragments = SymbolGraph.Symbol.DeclarationFragments - - /// The platform names, derived from the platform availability. - public var platformNames: Set? { - return platforms.map { platforms in Set(platforms.compactMap { $0.name }) } - } - - /// Images that are used to represent the summarized element. - public var topicImages: [TopicImage]? - - /// References used in the content of the summarized element. - public var references: [any RenderReference]? - - /// The variants of content (kind, url, title, abstract, language, declaration) for this resolver information. - public var variants: [Variant]? - - /// A value that indicates whether this symbol is under development and likely to change. - var isBeta: Bool { - guard let platforms, !platforms.isEmpty else { - return false - } - - return platforms.allSatisfy { $0.isBeta == true } - } - - /// Creates a new resolved information value with all its values. - /// - /// - Parameters: - /// - kind: The resolved kind. - /// - url: The resolved URL. - /// - title: The resolved title - /// - abstract: The resolved (plain text) abstract. - /// - language: The resolved language. - /// - availableLanguages: The languages where the resolved node is available. - /// - platforms: The platforms and their versions where the resolved node is available, if any. - /// - declarationFragments: The resolved declaration fragments, if any. - /// - topicImages: Images that are used to represent the summarized element. - /// - references: References used in the content of the summarized element. - /// - variants: The variants of content for this resolver information. - public init( - kind: DocumentationNode.Kind, - url: URL, - title: String, - abstract: String, - language: SourceLanguage, - availableLanguages: Set, - platforms: [PlatformAvailability]? = nil, - declarationFragments: DeclarationFragments? = nil, - topicImages: [TopicImage]? = nil, - references: [any RenderReference]? = nil, - variants: [Variant]? = nil - ) { - self.kind = kind - self.url = url - self.title = title - self.abstract = abstract - self.language = language - self.availableLanguages = availableLanguages - self.platforms = platforms - self.declarationFragments = declarationFragments - self.topicImages = topicImages - self.references = references - self.variants = variants - } - - /// A variant of content for the resolved information. - /// - /// - Note: All properties except for ``traits`` are optional. If a property is `nil` it means that the value is the same as the resolved information's value. - public struct Variant: Codable { - /// The traits of the variant. - public let traits: [RenderNode.Variant.Trait] - - /// A wrapper for variant values that can either be specified, meaning the variant has a custom value, or not, meaning the variant has the same value as the resolved information. - /// - /// This alias is used to make the property declarations more explicit while at the same time offering the convenient syntax of optionals. - public typealias VariantValue = Optional - - /// The kind of the variant or `nil` if the kind is the same as the resolved information. - public let kind: VariantValue - /// The url of the variant or `nil` if the url is the same as the resolved information. - public let url: VariantValue - /// The title of the variant or `nil` if the title is the same as the resolved information. - public let title: VariantValue - /// The abstract of the variant or `nil` if the abstract is the same as the resolved information. - public let abstract: VariantValue - /// The language of the variant or `nil` if the language is the same as the resolved information. - public let language: VariantValue - /// The declaration fragments of the variant or `nil` if the declaration is the same as the resolved information. - /// - /// If the resolver information has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. - public let declarationFragments: VariantValue - - /// Creates a new resolved information variant with the values that are different from the resolved information values. - /// - /// - Parameters: - /// - traits: The traits of the variant. - /// - kind: The resolved kind. - /// - url: The resolved URL. - /// - title: The resolved title - /// - abstract: The resolved (plain text) abstract. - /// - language: The resolved language. - /// - declarationFragments: The resolved declaration fragments, if any. - public init( - traits: [RenderNode.Variant.Trait], - kind: VariantValue = nil, - url: VariantValue = nil, - title: VariantValue = nil, - abstract: VariantValue = nil, - language: VariantValue = nil, - declarationFragments: VariantValue = nil - ) { - self.traits = traits - self.kind = kind - self.url = url - self.title = title - self.abstract = abstract - self.language = language - self.declarationFragments = declarationFragments - } - } - } -} - -extension OutOfProcessReferenceResolver.ResolvedInformation { - enum CodingKeys: CodingKey { - case kind - case url - case title - case abstract - case language - case availableLanguages - case platforms - case declarationFragments - case topicImages - case references - case variants - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - kind = try container.decode(DocumentationNode.Kind.self, forKey: .kind) - url = try container.decode(URL.self, forKey: .url) - title = try container.decode(String.self, forKey: .title) - abstract = try container.decode(String.self, forKey: .abstract) - language = try container.decode(SourceLanguage.self, forKey: .language) - availableLanguages = try container.decode(Set.self, forKey: .availableLanguages) - platforms = try container.decodeIfPresent([OutOfProcessReferenceResolver.ResolvedInformation.PlatformAvailability].self, forKey: .platforms) - declarationFragments = try container.decodeIfPresent(OutOfProcessReferenceResolver.ResolvedInformation.DeclarationFragments.self, forKey: .declarationFragments) - topicImages = try container.decodeIfPresent([TopicImage].self, forKey: .topicImages) - references = try container.decodeIfPresent([CodableRenderReference].self, forKey: .references).map { decodedReferences in - decodedReferences.map(\.reference) - } - variants = try container.decodeIfPresent([OutOfProcessReferenceResolver.ResolvedInformation.Variant].self, forKey: .variants) - - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(self.kind, forKey: .kind) - try container.encode(self.url, forKey: .url) - try container.encode(self.title, forKey: .title) - try container.encode(self.abstract, forKey: .abstract) - try container.encode(self.language, forKey: .language) - try container.encode(self.availableLanguages, forKey: .availableLanguages) - try container.encodeIfPresent(self.platforms, forKey: .platforms) - try container.encodeIfPresent(self.declarationFragments, forKey: .declarationFragments) - try container.encodeIfPresent(self.topicImages, forKey: .topicImages) - try container.encodeIfPresent(references?.map { CodableRenderReference($0) }, forKey: .references) - try container.encodeIfPresent(self.variants, forKey: .variants) - } -} +// MARK: Convert Service extension OutOfProcessReferenceResolver: ConvertServiceFallbackResolver { @_spi(ExternalLinks) + @available(*, deprecated, message: "The ConvertService is implicitly reliant on the deprecated `Request` and `Response` types.") public func entityIfPreviouslyResolved(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity? { - guard referenceCache.keys.contains(reference.url) else { return nil } + guard let implementation = implementation as? ImplementationV1 else { + assertionFailure("ConvertServiceFallbackResolver expects V1 requests and responses") + return nil + } + + guard implementation.referenceCache.keys.contains(reference.url) else { return nil } var entity = entity(with: reference) // The entity response doesn't include the assets that it references. // Before returning the entity, make sure that its references assets are included among the image dependencies. - for image in entity.topicRenderReference.images { + var references = entity.references ?? [] + + for image in entity.topicImages ?? [] { if let asset = resolve(assetNamed: image.identifier.identifier) { - entity.renderReferenceDependencies.imageReferences.append(ImageReference(identifier: image.identifier, imageAsset: asset)) + references.append(ImageReference(identifier: image.identifier, imageAsset: asset)) } } + if !references.isEmpty { + entity.references = references + } + return entity } + @available(*, deprecated, message: "The ConvertService is implicitly reliant on the deprecated `Request` and `Response` types.") func resolve(assetNamed assetName: String) -> DataAsset? { - return try? resolveInformationForAsset(named: assetName) - } - - func resolveInformationForAsset(named assetName: String) throws -> DataAsset { let assetReference = AssetReference(assetName: assetName, bundleID: bundleID) if let asset = assetCache[assetReference] { return asset } - let response = try externalLinkResolvingClient.sendAndWait( - request: Request.asset(AssetReference(assetName: assetName, bundleID: bundleID)) - ) as Response - - switch response { - case .asset(let asset): - assetCache[assetReference] = asset - return asset - case .errorMessage(let errorMessage): - throw Error.forwardedErrorFromClient(errorMessage: errorMessage) - default: - throw Error.unexpectedResponse(response: response, requestDescription: "asset") + guard case .asset(let asset)? = try? implementation.longRunningProcess.sendAndWait(request: Request.asset(assetReference)) as Response else { + return nil } + return asset } } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift index 0d145a597d..44477ca526 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -44,12 +44,10 @@ final class ExternalPathHierarchyResolver { } return .success(foundReference) - } catch let error as PathHierarchy.Error { + } catch { return .failure(unresolvedReference, error.makeTopicReferenceResolutionErrorInfo() { collidingNode in self.fullName(of: collidingNode) // If the link was ambiguous, determine the full name of each colliding node to be presented in the link diagnostic. }) - } catch { - fatalError("Only PathHierarchy.Error errors are raised from the symbol link resolution code above.") } } @@ -58,13 +56,13 @@ final class ExternalPathHierarchyResolver { return collidingNode.name } if let symbolID = collidingNode.symbol?.identifier { - if symbolID.interfaceLanguage == summary.language.id, let fragments = summary.declarationFragments { - return fragments.plainTextDeclaration() + if symbolID.interfaceLanguage == summary.language.id, let plainTextDeclaration = summary.plainTextDeclaration { + return plainTextDeclaration } if let variant = summary.variants.first(where: { $0.traits.contains(.interfaceLanguage(symbolID.interfaceLanguage)) }), - let fragments = variant.declarationFragments ?? summary.declarationFragments + let plainTextDeclaration = variant.plainTextDeclaration ?? summary.plainTextDeclaration { - return fragments.plainTextDeclaration() + return plainTextDeclaration } } return summary.title @@ -87,30 +85,10 @@ final class ExternalPathHierarchyResolver { /// /// - Precondition: The `reference` was previously resolved by this resolver. func entity(_ reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity { - guard let resolvedInformation = content[reference] else { + guard let alreadyResolvedSummary = content[reference] else { fatalError("The resolver should only be asked for entities that it resolved.") } - - let topicReferences: [ResolvedTopicReference] = (resolvedInformation.references ?? []).compactMap { - guard let renderReference = $0 as? TopicRenderReference, - let url = URL(string: renderReference.identifier.identifier), - let bundleID = url.host - else { - return nil - } - return ResolvedTopicReference(bundleID: .init(rawValue: bundleID), path: url.path, fragment: url.fragment, sourceLanguage: .swift) - } - let dependencies = RenderReferenceDependencies( - topicReferences: topicReferences, - linkReferences: (resolvedInformation.references ?? []).compactMap { $0 as? LinkReference }, - imageReferences: (resolvedInformation.references ?? []).compactMap { $0 as? ImageReference } - ) - - return .init( - topicRenderReference: resolvedInformation.topicRenderReference(), - renderReferenceDependencies: dependencies, - sourceLanguages: resolvedInformation.availableLanguages - ) + return alreadyResolvedSummary } // MARK: Deserialization @@ -173,17 +151,11 @@ final class ExternalPathHierarchyResolver { } } -private extension Sequence { - func plainTextDeclaration() -> String { - return self.map(\.text).joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ") - } -} - // MARK: ExternalEntity -private extension LinkDestinationSummary { +extension LinkDestinationSummary { /// A value that indicates whether this symbol is under development and likely to change. - var isBeta: Bool { + private var isBeta: Bool { guard let platforms, !platforms.isEmpty else { return false } @@ -192,12 +164,13 @@ private extension LinkDestinationSummary { } /// Create a topic render render reference for this link summary and its content variants. - func topicRenderReference() -> TopicRenderReference { + func makeTopicRenderReference() -> TopicRenderReference { let (kind, role) = DocumentationContentRenderer.renderKindAndRole(kind, semantic: nil) var titleVariants = VariantCollection(defaultValue: title) var abstractVariants = VariantCollection(defaultValue: abstract ?? []) - var fragmentVariants = VariantCollection(defaultValue: declarationFragments) + var fragmentVariants = VariantCollection(defaultValue: subheadingDeclarationFragments) + var navigatorTitleVariants = VariantCollection(defaultValue: navigatorDeclarationFragments) for variant in variants { let traits = variant.traits @@ -207,21 +180,24 @@ private extension LinkDestinationSummary { if let abstract = variant.abstract { abstractVariants.variants.append(.init(traits: traits, patch: [.replace(value: abstract ?? [])])) } - if let fragment = variant.declarationFragments { + if let fragment = variant.subheadingDeclarationFragments { fragmentVariants.variants.append(.init(traits: traits, patch: [.replace(value: fragment)])) } + if let navigatorTitle = variant.navigatorDeclarationFragments { + navigatorTitleVariants.variants.append(.init(traits: traits, patch: [.replace(value: navigatorTitle)])) + } } return TopicRenderReference( identifier: .init(referenceURL.absoluteString), titleVariants: titleVariants, abstractVariants: abstractVariants, - url: relativePresentationURL.absoluteString, + url: absolutePresentationURL?.absoluteString ?? relativePresentationURL.absoluteString, kind: kind, required: false, role: role, fragmentsVariants: fragmentVariants, - navigatorTitleVariants: .init(defaultValue: nil), + navigatorTitleVariants: navigatorTitleVariants, estimatedTime: nil, conformance: nil, isBeta: isBeta, diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift new file mode 100644 index 0000000000..2bb01f5abe --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift @@ -0,0 +1,166 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 rendering-friendly representation of a external node. +package struct ExternalRenderNode { + private var entity: LinkResolver.ExternalEntity + private var topicRenderReference: TopicRenderReference + + /// The bundle identifier for this external node. + private var bundleIdentifier: DocumentationBundle.Identifier + + // This type is designed to misrepresent external content as local content to fit in with the navigator. + // This spreads the issue to more code rather than fixing it, which adds technical debt and can be fragile. + // + // At the time of writing this comment, this type and the issues it comes with has spread to 6 files (+ 3 test files). + // Luckily, none of that code is public API so we can modify or even remove it without compatibility restrictions. + init(externalEntity: LinkResolver.ExternalEntity, bundleIdentifier: DocumentationBundle.Identifier) { + self.entity = externalEntity + self.bundleIdentifier = bundleIdentifier + self.topicRenderReference = externalEntity.makeTopicRenderReference() + } + + /// The identifier of the external render node. + package var identifier: ResolvedTopicReference { + ResolvedTopicReference( + bundleID: bundleIdentifier, + path: entity.referenceURL.path, + fragment: entity.referenceURL.fragment, + sourceLanguages: entity.availableLanguages + ) + } + + /// The kind of this documentation node. + var kind: RenderNode.Kind { + topicRenderReference.kind + } + + /// The symbol kind of this documentation node. + /// + /// This value is `nil` if the referenced page is not a symbol. + var symbolKind: SymbolGraph.Symbol.KindIdentifier? { + DocumentationNode.symbolKind(for: entity.kind) + } + + /// The additional "role" assigned to the symbol, if any + /// + /// This value is `nil` if the referenced page is not a symbol. + var role: String? { + topicRenderReference.role + } + + /// The variants of the title. + var titleVariants: VariantCollection { + topicRenderReference.titleVariants + } + + /// The variants of the abbreviated declaration of the symbol to display in navigation. + var navigatorTitleVariants: VariantCollection<[DeclarationRenderSection.Token]?> { + topicRenderReference.navigatorTitleVariants + } + + /// The variants of the abbreviated declaration of the symbol to display in links and fall-back to in navigation. + /// + /// This value is `nil` if the referenced page is not a symbol. + var fragmentsVariants: VariantCollection<[DeclarationRenderSection.Token]?> { + topicRenderReference.fragmentsVariants + } + + /// Author provided images that represent this page. + var images: [TopicImage] { + entity.topicImages ?? [] + } + + /// The identifier of the external reference. + var externalIdentifier: RenderReferenceIdentifier { + topicRenderReference.identifier + } + + /// List of variants of the same external node for various languages. + var variants: [RenderNode.Variant]? { + entity.availableLanguages.map { + RenderNode.Variant(traits: [.interfaceLanguage($0.id)], paths: [topicRenderReference.url]) + } + } + + /// A value that indicates whether this symbol is built for a beta platform + /// + /// This value is `false` if the referenced page is not a symbol. + var isBeta: Bool { + topicRenderReference.isBeta + } +} + +/// A language specific representation of an external render node value for building a navigator index. +struct NavigatorExternalRenderNode: NavigatorIndexableRenderNodeRepresentation { + private var _identifier: ResolvedTopicReference + var identifier: ResolvedTopicReference { + _identifier + } + var kind: RenderNode.Kind + var metadata: ExternalRenderNodeMetadataRepresentation + + // Values that don't affect how the node is rendered in the sidebar. + // These are needed to conform to the navigator indexable protocol. + var references: [String : any RenderReference] = [:] + var sections: [any RenderSection] = [] + var topicSections: [TaskGroupRenderSection] = [] + var defaultImplementationsSections: [TaskGroupRenderSection] = [] + + init(renderNode: ExternalRenderNode, trait: RenderNode.Variant.Trait? = nil) { + // Compute the source language of the node based on the trait to know which variant to apply. + let traitLanguage = if case .interfaceLanguage(let id) = trait { + SourceLanguage(id: id) + } else { + renderNode.identifier.sourceLanguage + } + let traits = trait.map { [$0] } ?? [] + + self._identifier = renderNode.identifier.withSourceLanguages([traitLanguage]) + self.kind = renderNode.kind + + self.metadata = ExternalRenderNodeMetadataRepresentation( + title: renderNode.titleVariants.value(for: traits), + navigatorTitle: renderNode.navigatorTitleVariants.value(for: traits), + externalID: renderNode.externalIdentifier.identifier, + role: renderNode.role, + symbolKind: renderNode.symbolKind?.renderingIdentifier, + images: renderNode.images, + isBeta: renderNode.isBeta, + fragments: renderNode.fragmentsVariants.value(for: traits) + ) + } +} + +/// A language specific representation of a render metadata value for building an external navigator index. +struct ExternalRenderNodeMetadataRepresentation: NavigatorIndexableRenderMetadataRepresentation { + var title: String? + var navigatorTitle: [DeclarationRenderSection.Token]? + var externalID: String? + var role: String? + var symbolKind: String? + var images: [TopicImage] + var isBeta: Bool + var fragments: [DeclarationRenderSection.Token]? + + // Values that we have insufficient information to derive. + // These are needed to conform to the navigator indexable metadata protocol. + // + // The role heading is used to identify Property Lists. + // The value being missing is used for computing the final navigator title. + // + // The platforms are used for generating the availability index, + // but doesn't affect how the node is rendered in the sidebar. + var roleHeading: String? = nil + var platforms: [AvailabilityRenderItem]? = nil +} diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift index 0ec34d9b4d..10b3fc5057 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -9,7 +9,6 @@ */ import Foundation -import SymbolKit /// A class that resolves documentation links by orchestrating calls to other link resolver implementations. public class LinkResolver { @@ -42,40 +41,7 @@ public class LinkResolver { /// The minimal information about an external entity necessary to render links to it on another page. @_spi(ExternalLinks) // This isn't stable API yet. - public struct ExternalEntity { - /// Creates a new external entity. - /// - Parameters: - /// - topicRenderReference: The render reference for this external topic. - /// - renderReferenceDependencies: Any dependencies for the render reference. - /// - sourceLanguages: The different source languages for which this page is available. - @_spi(ExternalLinks) - public init(topicRenderReference: TopicRenderReference, renderReferenceDependencies: RenderReferenceDependencies, sourceLanguages: Set) { - self.topicRenderReference = topicRenderReference - self.renderReferenceDependencies = renderReferenceDependencies - self.sourceLanguages = sourceLanguages - } - - /// The render reference for this external topic. - var topicRenderReference: TopicRenderReference - /// Any dependencies for the render reference. - /// - /// For example, if the external content contains links or images, those are included here. - var renderReferenceDependencies: RenderReferenceDependencies - /// The different source languages for which this page is available. - var sourceLanguages: Set - - /// Creates a pre-render new topic content value to be added to a render context's reference store. - func topicContent() -> RenderReferenceStore.TopicContent { - return .init( - renderReference: topicRenderReference, - canonicalPath: nil, - taskGroups: nil, - source: nil, - isDocumentationExtensionContent: false, - renderReferenceDependencies: renderReferenceDependencies - ) - } - } + public typealias ExternalEntity = LinkDestinationSummary // Currently we use the same format as DocC outputs for its own pages. That may change depending on what information we need here. /// Attempts to resolve an unresolved reference. /// @@ -93,14 +59,15 @@ public class LinkResolver { // Check if this is a link to an external documentation source that should have previously been resolved in `DocumentationContext.preResolveExternalLinks(...)` if let bundleID = unresolvedReference.bundleID, - !context._registeredBundles.contains(where: { $0.id == bundleID || urlReadablePath($0.displayName) == bundleID.rawValue }) + context.inputs.id != bundleID, + urlReadablePath(context.inputs.displayName) != bundleID.rawValue { return .failure(unresolvedReference, TopicReferenceResolutionErrorInfo("No external resolver registered for '\(bundleID)'.")) } do { return try localResolver.resolve(unresolvedReference, in: parent, fromSymbolLink: isCurrentlyResolvingSymbolLink) - } catch let error as PathHierarchy.Error { + } catch { // Check if there's a known external resolver for this module. if case .moduleNotFound(_, let remainingPathComponents, _) = error, let resolver = externalResolvers[remainingPathComponents.first!.full] { let result = resolver.resolve(unresolvedReference, fromSymbolLink: isCurrentlyResolvingSymbolLink) @@ -119,8 +86,6 @@ public class LinkResolver { } else { return .failure(unresolvedReference, error.makeTopicReferenceResolutionErrorInfo() { localResolver.fullName(of: $0, in: context) }) } - } catch { - fatalError("Only SymbolPathTree.Error errors are raised from the symbol link resolution code above.") } } @@ -171,9 +136,8 @@ private final class FallbackResolverBasedLinkResolver { // Check if a fallback reference resolver should resolve this let referenceBundleID = unresolvedReference.bundleID ?? parent.bundleID guard let fallbackResolver = context.configuration.convertServiceConfiguration.fallbackResolver, - // This uses an underscored internal variant of `registeredBundles` to avoid deprecation warnings and remain compatible with legacy data providers. - let knownBundleID = context._registeredBundles.first(where: { $0.id == referenceBundleID || urlReadablePath($0.displayName) == referenceBundleID.rawValue })?.id, - fallbackResolver.bundleID == knownBundleID + fallbackResolver.bundleID == context.inputs.id, + context.inputs.id == referenceBundleID || urlReadablePath(context.inputs.displayName) == referenceBundleID.rawValue else { return nil } @@ -187,21 +151,20 @@ private final class FallbackResolverBasedLinkResolver { bundleID: referenceBundleID, path: unresolvedReference.path.prependingLeadingSlash, fragment: unresolvedReference.topicURL.components.fragment, - sourceLanguages: parent.sourceLanguages + sourceLanguages: parent._sourceLanguages ) allCandidateURLs.append(alreadyResolved.url) - // This uses an underscored internal variant of `bundle(identifier:)` to avoid deprecation warnings and remain compatible with legacy data providers. - let currentBundle = context._bundle(identifier: knownBundleID.rawValue)! + let currentInputs = context.inputs if !isCurrentlyResolvingSymbolLink { // First look up articles path allCandidateURLs.append(contentsOf: [ // First look up articles path - currentBundle.articlesDocumentationRootReference.url.appendingPathComponent(unresolvedReference.path), + currentInputs.articlesDocumentationRootReference.url.appendingPathComponent(unresolvedReference.path), // Then technology tutorials root path (for individual tutorial pages) - currentBundle.tutorialsContainerReference.url.appendingPathComponent(unresolvedReference.path), + currentInputs.tutorialsContainerReference.url.appendingPathComponent(unresolvedReference.path), // Then tutorials root path (for tutorial table of contents pages) - currentBundle.tutorialTableOfContentsContainer.url.appendingPathComponent(unresolvedReference.path), + currentInputs.tutorialTableOfContentsContainer.url.appendingPathComponent(unresolvedReference.path), ]) } // Try resolving in the local context (as child) @@ -216,8 +179,8 @@ private final class FallbackResolverBasedLinkResolver { // Check that the parent is not an article (ignoring if absolute or relative link) // because we cannot resolve in the parent context if it's not a symbol. - if parent.path.hasPrefix(currentBundle.documentationRootReference.path) && parentPath.count > 2 { - let rootPath = currentBundle.documentationRootReference.appendingPath(parentPath[2]) + if parent.path.hasPrefix(currentInputs.documentationRootReference.path) && parentPath.count > 2 { + let rootPath = currentInputs.documentationRootReference.appendingPath(parentPath[2]) let resolvedInRoot = rootPath.url.appendingPathComponent(unresolvedReference.path) // Confirm here that we we're not already considering this link. We only need to specifically @@ -227,7 +190,7 @@ private final class FallbackResolverBasedLinkResolver { } } - allCandidateURLs.append(currentBundle.documentationRootReference.url.appendingPathComponent(unresolvedReference.path)) + allCandidateURLs.append(currentInputs.documentationRootReference.url.appendingPathComponent(unresolvedReference.path)) for candidateURL in allCandidateURLs { guard let candidateReference = ValidatedURL(candidateURL).map({ UnresolvedTopicReference(topicURL: $0) }) else { @@ -256,3 +219,42 @@ private final class FallbackResolverBasedLinkResolver { return nil } } + +extension LinkResolver.ExternalEntity { + /// Creates a pre-render new topic content value to be added to a render context's reference store. + func makeTopicContent() -> RenderReferenceStore.TopicContent { + .init( + renderReference: makeTopicRenderReference(), + canonicalPath: nil, + taskGroups: nil, + source: nil, + isDocumentationExtensionContent: false, + renderReferenceDependencies: makeRenderDependencies() + ) + } + + func makeRenderDependencies() -> RenderReferenceDependencies { + guard let references else { return .init() } + + return .init( + topicReferences: references.compactMap { ($0 as? TopicRenderReference)?.topicReference(languages: availableLanguages) }, + linkReferences: references.compactMap { $0 as? LinkReference }, + imageReferences: references.compactMap { $0 as? ImageReference } + ) + } +} + +private extension TopicRenderReference { + func topicReference(languages: Set) -> ResolvedTopicReference? { + guard let url = URL(string: identifier.identifier), let rawBundleID = url.host else { + return nil + } + return ResolvedTopicReference( + bundleID: .init(rawValue: rawBundleID), + path: url.path, + fragment: url.fragment, + // TopicRenderReference doesn't have language information. Also, the reference's languages _doesn't_ specify the languages of the linked entity. + sourceLanguages: languages + ) + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Error.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Error.swift index 67851a2126..e02a6f4c00 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Error.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Error.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -285,7 +285,7 @@ private extension PathHierarchy.Node { } } -private extension SourceRange { +extension SourceRange { static func makeRelativeRange(startColumn: Int, endColumn: Int) -> SourceRange { return SourceLocation(line: 0, column: startColumn, source: nil) ..< SourceLocation(line: 0, column: endColumn, source: nil) } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift index 0ead6bbe16..3f7351b44f 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -20,11 +20,11 @@ extension PathHierarchy { /// - onlyFindSymbols: Whether or not only symbol matches should be found. /// - Returns: Returns the unique identifier for the found match or raises an error if no match can be found. /// - Throws: Raises a ``PathHierarchy/Error`` if no match can be found. - func find(path rawPath: String, parent: ResolvedIdentifier? = nil, onlyFindSymbols: Bool) throws -> ResolvedIdentifier { + func find(path rawPath: String, parent: ResolvedIdentifier? = nil, onlyFindSymbols: Bool) throws(Error) -> ResolvedIdentifier { return try findNode(path: rawPath, parentID: parent, onlyFindSymbols: onlyFindSymbols).identifier } - private func findNode(path rawPath: String, parentID: ResolvedIdentifier?, onlyFindSymbols: Bool) throws -> Node { + private func findNode(path rawPath: String, parentID: ResolvedIdentifier?, onlyFindSymbols: Bool) throws(Error) -> Node { // The search for a documentation element can be though of as 3 steps: // - First, parse the path into structured path components. // - Second, find nodes that match the beginning of the path as starting points for the search @@ -79,15 +79,17 @@ extension PathHierarchy { } // A function to avoid repeating the - func searchForNodeInModules() throws -> Node { + func searchForNodeInModules() throws(Error) -> Node { // Note: This captures `parentID`, `remaining`, and `rawPathForError`. if let moduleMatch = modules.first(where: { $0.matches(firstComponent) }) { return try searchForNode(descendingFrom: moduleMatch, pathComponents: remaining.dropFirst(), onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPath) } - if modules.count == 1 { + // For absolute links, only use the single-module fallback if the first component doesn't match + // any module name + if modules.count == 1 && !isAbsolute { do { return try searchForNode(descendingFrom: modules.first!, pathComponents: remaining, onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPath) - } catch let error as PathHierarchy.Error { + } catch { switch error { case .notFound: // Ignore this error and raise an error about not finding the module instead. @@ -129,7 +131,7 @@ extension PathHierarchy { } // A recursive function to traverse up the path hierarchy searching for the matching node - func searchForNodeUpTheHierarchy(from startingPoint: Node?, path: ArraySlice) throws -> Node { + func searchForNodeUpTheHierarchy(from startingPoint: Node?, path: ArraySlice) throws(Error) -> Node { guard let possibleStartingPoint = startingPoint else { // If the search has reached the top of the hierarchy, check the modules as a base case to break the recursion. do { @@ -147,7 +149,7 @@ extension PathHierarchy { let firstComponent = path.first! // Keep track of the inner most error and raise that if no node is found. - var innerMostError: (any Swift.Error)? + var innerMostError: Error? // If the starting point's children match this component, descend the path hierarchy from there. if possibleStartingPoint.anyChildMatches(firstComponent) { @@ -195,7 +197,7 @@ extension PathHierarchy { startingPoint = counterpoint default: // Only symbols have counterpoints which means that each node should always have at least one language - if counterpoint.languages.map(\.id).min()! < startingPoint.languages.map(\.id).min()! { + if counterpoint.languages.min()! < startingPoint.languages.min()! { startingPoint = counterpoint } } @@ -211,7 +213,7 @@ extension PathHierarchy { pathComponents: ArraySlice, onlyFindSymbols: Bool, rawPathForError: String - ) throws -> Node { + ) throws(Error) -> Node { // All code paths through this function wants to perform extra verification on the return value before returning it to the caller. // To accomplish that, the core implementation happens in `_innerImplementation`, which is called once, right below its definition. @@ -220,7 +222,7 @@ extension PathHierarchy { pathComponents: ArraySlice, onlyFindSymbols: Bool, rawPathForError: String - ) throws -> Node { + ) throws(Error) -> Node { var node = startingPoint var remaining = pathComponents[...] @@ -234,21 +236,13 @@ extension PathHierarchy { while true { let (children, pathComponent) = try findChildContainer(node: &node, remaining: remaining, rawPathForError: rawPathForError) + let child: PathHierarchy.Node? do { - guard let child = try children.find(pathComponent.disambiguation) else { - // The search has ended with a node that doesn't have a child matching the next path component. - throw makePartialResultError(node: node, remaining: remaining, rawPathForError: rawPathForError) - } - node = child - remaining = remaining.dropFirst() - if remaining.isEmpty { - // If all path components are consumed, then the match is found. - return child - } - } catch DisambiguationContainer.Error.lookupCollision(let collisions) { - func handleWrappedCollision() throws -> Node { - let match = try handleCollision(node: node, remaining: remaining, collisions: collisions, onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPathForError) - return match + child = try children.find(pathComponent.disambiguation) + } catch { + let collisions = error.collisions + func handleWrappedCollision() throws(Error) -> Node { + try handleCollision(node: node, remaining: remaining, collisions: collisions, onlyFindSymbols: onlyFindSymbols, rawPathForError: rawPathForError) } // When there's a collision, use the remaining path components to try and narrow down the possible collisions. @@ -314,6 +308,17 @@ extension PathHierarchy { // Couldn't resolve the collision by look ahead. return try handleWrappedCollision() } + + guard let child else { + // The search has ended with a node that doesn't have a child matching the next path component. + throw makePartialResultError(node: node, remaining: remaining, rawPathForError: rawPathForError) + } + node = child + remaining = remaining.dropFirst() + if remaining.isEmpty { + // If all path components are consumed, then the match is found. + return child + } } } @@ -336,7 +341,7 @@ extension PathHierarchy { collisions: [(node: PathHierarchy.Node, disambiguation: String)], onlyFindSymbols: Bool, rawPathForError: String - ) throws -> Node { + ) throws(Error) -> Node { if let favoredMatch = collisions.singleMatch({ !$0.node.isDisfavoredInLinkCollisions }) { return favoredMatch.node } @@ -421,7 +426,7 @@ extension PathHierarchy { node: inout Node, remaining: ArraySlice, rawPathForError: String - ) throws -> (DisambiguationContainer, PathComponent) { + ) throws(Error) -> (DisambiguationContainer, PathComponent) { var pathComponent = remaining.first! if let match = node.children[pathComponent.full] { // The path component parsing may treat dash separated words as disambiguation information. @@ -439,12 +444,10 @@ extension PathHierarchy { // MARK: Disambiguation Container extension PathHierarchy.DisambiguationContainer { - /// Errors finding values in the disambiguation tree - enum Error: Swift.Error { - /// Multiple matches found. - /// - /// Includes a list of values paired with their missing disambiguation suffixes. - case lookupCollision([(node: PathHierarchy.Node, disambiguation: String)]) + /// Multiple matches found. + struct LookupCollisionError: Swift.Error { + /// A list of values paired with their missing disambiguation suffixes. + let collisions: [(node: PathHierarchy.Node, disambiguation: String)] } /// Attempts to find the only element in the disambiguation container without using any disambiguation information. @@ -464,7 +467,7 @@ extension PathHierarchy.DisambiguationContainer { /// - No match is found; indicated by a `nil` return value. /// - Exactly one match is found; indicated by a non-nil return value. /// - More than one match is found; indicated by a raised error listing the matches and their missing disambiguation. - func find(_ disambiguation: PathHierarchy.PathComponent.Disambiguation?) throws -> PathHierarchy.Node? { + func find(_ disambiguation: PathHierarchy.PathComponent.Disambiguation?) throws(LookupCollisionError) -> PathHierarchy.Node? { if disambiguation == nil, let match = singleMatch() { return match } @@ -478,13 +481,13 @@ extension PathHierarchy.DisambiguationContainer { let matches = storage.filter({ $0.kind == kind }) guard matches.count <= 1 else { // Suggest not only hash disambiguation, but also type signature disambiguation. - throw Error.lookupCollision(Self.disambiguatedValues(for: matches).map { ($0.value, $0.disambiguation.makeSuffix()) }) + throw LookupCollisionError(collisions: Self.disambiguatedValues(for: matches).map { ($0.value, $0.disambiguation.makeSuffix()) }) } return matches.first?.node case (nil, let hash?): let matches = storage.filter({ $0.hash == hash }) guard matches.count <= 1 else { - throw Error.lookupCollision(matches.map { ($0.node, "-" + $0.kind!) }) // An element wouldn't match if it didn't have kind disambiguation. + throw LookupCollisionError(collisions: matches.map { ($0.node, "-" + $0.kind!) }) // An element wouldn't match if it didn't have kind disambiguation. } return matches.first?.node case (nil, nil): @@ -498,13 +501,13 @@ extension PathHierarchy.DisambiguationContainer { case (let parameterTypes?, nil): let matches = storage.filter({ typesMatch(provided: parameterTypes, actual: $0.parameterTypes) }) guard matches.count <= 1 else { - throw Error.lookupCollision(matches.map { ($0.node, "->" + formattedTypes($0.parameterTypes)!) }) // An element wouldn't match if it didn't have parameter type disambiguation. + throw LookupCollisionError(collisions: matches.map { ($0.node, "->" + formattedTypes($0.parameterTypes)!) }) // An element wouldn't match if it didn't have parameter type disambiguation. } return matches.first?.node case (nil, let returnTypes?): let matches = storage.filter({ typesMatch(provided: returnTypes, actual: $0.returnTypes) }) guard matches.count <= 1 else { - throw Error.lookupCollision(matches.map { ($0.node, "-" + formattedTypes($0.returnTypes)!) }) // An element wouldn't match if it didn't have return type disambiguation. + throw LookupCollisionError(collisions: matches.map { ($0.node, "-" + formattedTypes($0.returnTypes)!) }) // An element wouldn't match if it didn't have return type disambiguation. } return matches.first?.node case (nil, nil): @@ -515,7 +518,7 @@ extension PathHierarchy.DisambiguationContainer { } // Disambiguate by a mix of kinds and USRs - throw Error.lookupCollision(self.disambiguatedValues().map { ($0.value, $0.disambiguation.makeSuffix()) }) + throw LookupCollisionError(collisions: self.disambiguatedValues().map { ($0.value, $0.disambiguation.makeSuffix()) }) } } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+PathComponent.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+PathComponent.swift index 531ad22f76..f067c735b9 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+PathComponent.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+PathComponent.swift @@ -210,7 +210,7 @@ extension PathHierarchy.PathParser { } } -private struct PathComponentScanner { +private struct PathComponentScanner: ~Copyable { private var remaining: Substring static let separator: Character = "/" diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignature.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignature.swift index 52af84c174..860df9ebde 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignature.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignature.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -20,7 +20,7 @@ extension PathHierarchy { return nil } - let isSwift = symbol.identifier.interfaceLanguage == SourceLanguage.swift.id + let isSwift = symbol.identifier.interfaceLanguage == "swift" return ( signature.parameters.map { parameterTypeSpelling(for: $0.declarationFragments, isSwift: isSwift) }, returnTypeSpellings(for: signature.returns, isSwift: isSwift) @@ -45,7 +45,7 @@ extension PathHierarchy { } let spelling = utf8TypeSpelling(for: fragments, isSwift: isSwift) - guard isSwift, spelling[...].isTuple() else { + guard isSwift, spelling[...].shapeOfSwiftTypeSpelling() == .tuple else { return [String(decoding: spelling, as: UTF8.self)] } @@ -136,6 +136,11 @@ extension PathHierarchy { // For example: "[", "?", "<", "...", ",", "(", "->" etc. contribute to the type spellings like // `[Name]`, `Name?`, "Name", "Name...", "()", "(Name, Name)", "(Name)->Name" and more. let utf8Spelling = fragment.spelling.utf8 + guard !utf8Spelling.elementsEqual(".Type".utf8) else { + // Once exception to that is "Name.Type" which is different from just "Name" (and we don't want a trailing ".") + accumulated.append(contentsOf: utf8Spelling) + continue + } for index in utf8Spelling.indices { let char = utf8Spelling[index] switch char { @@ -189,14 +194,14 @@ extension PathHierarchy { } // Check if the type names are wrapped in redundant parenthesis and remove them - if accumulated.first == openParen, accumulated.last == closeParen, !accumulated[...].isTuple() { + if accumulated.first == openParen, accumulated.last == closeParen, accumulated[...].shapeOfSwiftTypeSpelling() == .scalar { // In case there are multiple // Use a temporary slice until all the layers of redundant parenthesis have been removed. var temp = accumulated[...] repeat { temp = temp.dropFirst().dropLast() - } while temp.first == openParen && temp.last == closeParen && !temp.isTuple() + } while temp.first == openParen && temp.last == closeParen && temp.shapeOfSwiftTypeSpelling() == .scalar // Adjust the markers so that they align with the expected characters let difference = (accumulated.count - temp.count) / 2 @@ -225,7 +230,7 @@ extension PathHierarchy { } /// A small helper type that tracks the scope of nested brackets; `()`, `[]`, or `<>`. - private struct SwiftBracketsStack { + private struct SwiftBracketsStack: ~Copyable { enum Bracket { case angle // <> case square // [] @@ -277,26 +282,48 @@ private let question = UTF8.CodeUnit(ascii: "?") private let colon = UTF8.CodeUnit(ascii: ":") private let hyphen = UTF8.CodeUnit(ascii: "-") +/// A guesstimate of the "shape" of a Swift type based on its spelling. +private enum ShapeOfSwiftTypeSpelling { + /// This type spelling looks like a scalar. + /// + /// For example `Name` or `(Name)`. + /// - Note: We treat `(Name)` as a non-tuple so that we can remove the redundant leading and trailing parenthesis. + case scalar + /// This type spelling looks like a tuple. + /// + /// For example `(First, Second)`. + case tuple + /// This type spelling looks like a closure. + /// + /// For example `(First)->Second` or `(First, Second)->()` or `()->()`. + case closure +} + private extension ContiguousArray.SubSequence { - /// Checks if the UTF-8 string looks like a tuple with comma separated values. + /// Checks if the UTF-8 string looks like a tuple, scalar, or closure. /// /// This is used to remove redundant parenthesis around expressions. - func isTuple() -> Bool { - guard first == openParen, last == closeParen else { return false } + func shapeOfSwiftTypeSpelling() -> ShapeOfSwiftTypeSpelling { + guard first == openParen, last == closeParen else { return .scalar } var depth = 0 - for char in self { - switch char { + for index in indices { + switch self[index] { case openParen: depth += 1 case closeParen: depth -= 1 case comma where depth == 1: - return true + // If we find "," in one level of parenthesis, we've found a tuple. + return .tuple + case closeAngle where depth == 0 && index > startIndex && self[index - 1] == hyphen: + // If we find "->" outside any parentheses, we've found a closure. + return .closure default: continue } } - return false + // If we traversed the entire type name without finding a tuple or a closure we treat the type name as a scalar. + return .scalar } } @@ -495,32 +522,66 @@ extension PathHierarchy.PathParser { // MARK: Scanning a substring -private struct StringScanner { +/// A file-private, low-level string scanner type that's only designed for parsing type signature based disambiguation suffixes in authored links. +/// +/// ## Correct usage +/// +/// The higher level methods like ``scanReturnTypes()``, ``scanArguments()``, ``scanTuple()``, or ``scanValue()`` makes assumptions about the scanners content and current state. +/// For example: +/// - ``scanReturnTypes()`` knows that return types are specified after any parameter types and requires that the caller has already scanned the parameter types and advanced past the `"->"` separator. +/// It's the caller's (`parseTypeSignatureDisambiguation(pathComponent:)` above) responsibility to do these things correctly. +/// Similarly, it's the caller's responsibility to advance past the `"-"` prefix verify that the scanner points to an open parenthesis character (`(`) that before calling ``scanArguments()`` to scan the parameter types. +/// Failing to do either of these things will result in unexpected parsed disambiguation that DocC will fail to find a match for. +/// - Both ``scanArguments()``, or ``scanTuple()`` expects that the disambiguation portion of the authored link has a balanced number of open and closer parenthesis (`(` and `)`). +/// If the authored link contains unbalanced parenthesis then disambiguation isn't valid and the scanner will return a parsed value that DocC will fail to find a match for. +/// - ``scanValue()`` expects that the disambiguation portion of the authored link has a balanced number of open and closer angle brackets (`<` and `>`). +/// If the authored link contains unbalanced angle brackets then disambiguation isn't valid and the scanner will return a parsed value that DocC will fail to find a match for. +private struct StringScanner: ~Copyable { private var remaining: Substring init(_ original: Substring) { remaining = original } - func peek() -> Character? { + /// Returns the next character _without_ advancing the scanner + private func peek() -> Character? { remaining.first } - mutating func take() -> Character { + /// Advances the scanner and returns the scanned character. + private mutating func take() -> Character { remaining.removeFirst() } + /// Advances the scanner by `count` elements and returns the scanned substring. mutating func take(_ count: Int) -> Substring { defer { remaining = remaining.dropFirst(count) } return remaining.prefix(count) } - mutating func takeAll() -> Substring { + /// Advances the scanner to the end and returns the scanned substring. + private mutating func takeAll() -> Substring { defer { remaining.removeAll() } return remaining } - mutating func scan(until predicate: (Character) -> Bool) -> Substring? { + /// Advances the scanner up to the first character that satisfies the given `predicate` and returns the scanned substring. + /// + /// If the scanner doesn't contain any characters that satisfy the given `predicate`, then this method returns `nil` _without_ advancing the scanner. + /// + /// For example, consider a scanner that has already advanced 4 characters into the string `"One,Two,Three"` + /// ``` + /// One,Two,Three + /// ^ + /// ``` + /// Calling `scanner.scan(until: \.isNumber)` returns `nil` without advancing the scanner because none of the (remaining) characters is a number. + /// + /// Calling `scanner.scan(until: { $0 == "," })` advances the scanner by 3 additional characters, returning the scanned `"Two"` substring. + /// ``` + /// One,Two,Three + /// ^ + /// ``` + private mutating func scan(until predicate: (Character) -> Bool) -> Substring? { guard let index = remaining.firstIndex(where: predicate) else { return nil } @@ -528,16 +589,54 @@ private struct StringScanner { return remaining[.. Bool) -> Substring? { + guard let beforeIndex = remaining.firstIndex(where: predicate) else { + return nil + } + let index = remaining.index(after: beforeIndex) + defer { remaining = remaining[index...] } + return remaining[.. Bool { remaining.hasPrefix(prefix) } // MARK: Parsing argument types by scanning + /// Scans the remainder of the scanner's contents as the individual elements of a tuple return type, + /// or as a single return type if the scanners current position isn't an open parenthesis (`(`) + /// + /// For example, consider a scanner that has already advanced 8 characters into the string `"-(One)->(Two,Three)"` + /// ``` + /// -(One)->(Two, Three) + /// ^ + /// ``` + /// Because the scanner's current position is an open parenthesis (`(`), the scanner advances all the way to the end and returns `["Two", "Three"]` representing two elements in the tuple return value. + /// + /// - Note: The scanner expects that the caller has already scanned any parameter types and advanced past the `"->"` separator. mutating func scanReturnTypes() -> [Substring] { if peek() == "(" { _ = take() // the leading parenthesis @@ -546,7 +645,20 @@ private struct StringScanner { return [takeAll()] } } - + + /// Scans the list of individual parameter type names as if the scanner's current position was 1 past the open parenthesis (`(`) or a tuple. + /// + /// For example, consider a scanner that has already advanced 2 characters into the string `"-(One,(A,B))->(Two)"` + /// ``` + /// -(One,(A,B))->(Two) + /// ^ + /// ``` + /// The scanner parses two parameter return types---`"One"` and `"(A,B)"`---before the parenthesis balance out, advancing its position to one after the arguments list's closing parenthesis (`)`). + /// ``` + /// -(One,(A,B))->(Two) + /// ^ + /// ``` + /// - Note: The scanner expects that the caller has already advanced past the open parenthesis (`(`) that begins the list of parameter types. mutating func scanArguments() -> [Substring] { guard peek() != ")" else { _ = take() // drop the ")" @@ -564,11 +676,23 @@ private struct StringScanner { return arguments } - mutating func scanArgument() -> Substring? { + /// Scans a single type name, representing either a scalar value (such as `One`) or a nested tuple (such as `(A,B)`). + /// + /// For example, consider a scanner that has already advanced 6 characters into the string `"-(One,(A,B))->(Two)"` + /// ``` + /// -(One,(A,B))->(Two) + /// ^ + /// ``` + /// Because the value starts with an opening parenthesis (`(`), the scanner advances until the parenthesis balance out, returning `"(A,B)"`. + /// ``` + /// -(One,(A,B))->(Two) + /// ^ + /// ``` + private mutating func scanArgument() -> Substring? { guard peek() == "(" else { // If the argument doesn't start with "(" it can't be neither a tuple nor a closure type. // In this case, scan until the next argument (",") or the end of the arguments (")") - return scan(until: { $0 == "," || $0 == ")" }) ?? takeAll() + return scanValue() ?? takeAll() } guard var argumentString = scanTuple() else { @@ -584,7 +708,7 @@ private struct StringScanner { guard peek() == "(" else { // This closure type has a simple return type. - guard let returnValue = scan(until: { $0 == "," || $0 == ")" }) else { + guard let returnValue = scanValue() else { return nil } return argumentString + returnValue @@ -595,7 +719,20 @@ private struct StringScanner { return argumentString + returnValue } - mutating func scanTuple() -> Substring? { + /// Scans a nested tuple as a single substring. + /// + /// For example, consider a scanner that has already advanced 6 character into the string `"-(One,(A,B))->(Two)"` + /// ``` + /// -(One,(A,B))->(Two) + /// ^ + /// ``` + /// Because the value starts with an opening parenthesis (`(`), the scanner advances until the parenthesis balance out, returning `"(A,B)"`. + /// ``` + /// -(One,(A,B))->(Two) + /// ^ + /// ``` + /// - Note: The scanner expects that the caller has already advanced to the open parenthesis (`(`) that's the start of the nested tuple. + private mutating func scanTuple() -> Substring? { assert(peek() == "(", "The caller should have checked that this is a tuple") // The tuple may contain any number of nested tuples. Keep track of the open and close parenthesis while scanning. @@ -605,13 +742,41 @@ private struct StringScanner { depth += 1 return false // keep scanning } - if depth > 0 { - if $0 == ")" { - depth -= 1 - } + else if $0 == ")" { + depth -= 1 + return depth == 0 // stop only if we've reached a balanced number of parenthesis + } + return false // keep scanning + } + + return scan(past: predicate) + } + + /// Scans a single type name. + /// + /// For example, consider a scanner that has already advanced 2 character into the string `"-(One,Two)"` + /// ``` + /// -(One,Two) + /// ^ + /// ``` + /// Because the value contains generics (``), the scanner advances until the angle brackets balance out, returning `"One"`. + /// ``` + /// -(One,Two) + /// ^ + /// ``` + private mutating func scanValue() -> Substring? { + // The value may contain any number of nested generics. Keep track of the open and close angle brackets while scanning. + var depth = 0 + let predicate: (Character) -> Bool = { + if $0 == "<" { + depth += 1 + return false // keep scanning + } + else if $0 == ">" { + depth -= 1 return false // keep scanning } - return $0 == "," || $0 == ")" + return depth == 0 && ($0 == "," || $0 == ")") } return scan(until: predicate) } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift index 336e4d853e..2871130a10 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignatureDisambiguation.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -9,6 +9,7 @@ */ import Foundation +import DocCCommon extension PathHierarchy.DisambiguationContainer { @@ -63,8 +64,14 @@ extension PathHierarchy.DisambiguationContainer { } } - private static func _minimalSuggestedDisambiguationForFewParameters(typeNames: Table) -> [[String]?] { - typealias IntSet = _TinySmallValueIntSet + private static func _minimalSuggestedDisambiguationForFewParameters(typeNames: consuming Table) -> [[String]?] { + /// A specialized set-algebra type that only stores the possible values `0 ..< 64`. + /// + /// This specialized implementation is _not_ suitable as a general purpose set-algebra type. + /// However, because the code in this file only works with consecutive sequences of very small integers (most likely `0 ..< 16` and increasingly less likely the higher the number), + /// and because the the sets of those integers is frequently accessed in loops, a specialized implementation addresses bottlenecks in `_minimalSuggestedDisambiguation(...)`. + typealias IntSet = _FixedSizeBitSet + // We find the minimal suggested type-signature disambiguation in two steps. // // First, we compute which type names occur in which overloads. @@ -136,7 +143,7 @@ extension PathHierarchy.DisambiguationContainer { } // Create a sequence of type name combinations with increasing number of type names in each combination. - let typeNameCombinationsToCheck = typeNameIndicesToCheck.combinationsToCheck() + let typeNameCombinationsToCheck = typeNameIndicesToCheck.allCombinationsOfValues() return typeNames.rowIndices.map { row in var shortestDisambiguationSoFar: (indicesToInclude: IntSet, length: Int)? = nil @@ -216,7 +223,7 @@ extension PathHierarchy.DisambiguationContainer { } } - private static func _minimalSuggestedDisambiguationForManyParameters(typeNames: Table) -> [[String]?] { + private static func _minimalSuggestedDisambiguationForManyParameters(typeNames: consuming Table) -> [[String]?] { // If there are more than 64 parameters or more than 64 overloads we only try to disambiguate by a single type name. // // In practice, the number of parameters goes down rather quickly. @@ -259,166 +266,10 @@ extension PathHierarchy.DisambiguationContainer { } } -// MARK: Int Set - -/// A specialized set-algebra type that only stores the possible values `0 ..< 64`. -/// -/// This specialized implementation is _not_ suitable as a general purpose set-algebra type. -/// However, because the code in this file only works with consecutive sequences of very small integers (most likely `0 ..< 16` and increasingly less likely the higher the number), -/// and because the the sets of those integers is frequently accessed in loops, a specialized implementation addresses bottlenecks in `_minimalSuggestedDisambiguation(...)`. -/// -/// > Important: -/// > This type is thought of as file private but it made internal so that it can be tested. -struct _TinySmallValueIntSet: SetAlgebra { - typealias Element = Int - - init() {} - - @usableFromInline - private(set) var storage: UInt64 = 0 - - @inlinable - init(storage: UInt64) { - self.storage = storage - } - - private static func mask(_ number: Int) -> UInt64 { - precondition(number < 64, "Number \(number) is out of bounds (0..<64)") - return 1 << number - } - - @inlinable - @discardableResult - mutating func insert(_ member: Int) -> (inserted: Bool, memberAfterInsert: Int) { - let newStorage = storage | Self.mask(member) - defer { - storage = newStorage - } - return (newStorage != storage, member) - } - - @inlinable - @discardableResult - mutating func remove(_ member: Int) -> Int? { - let newStorage = storage & ~Self.mask(member) - defer { - storage = newStorage - } - return newStorage != storage ? member : nil - } - - @inlinable - @discardableResult - mutating func update(with member: Int) -> Int? { - let (inserted, _) = insert(member) - return inserted ? nil : member - } - - @inlinable - func contains(_ member: Int) -> Bool { - storage & Self.mask(member) != 0 - } - - @inlinable - var count: Int { - storage.nonzeroBitCount - } - - @inlinable - func isSuperset(of other: Self) -> Bool { - // Provide a custom implementation since this is called frequently in `combinationsToCheck()` - (storage & other.storage) == other.storage - } - - @inlinable - func union(_ other: Self) -> Self { - .init(storage: storage | other.storage) - } - - @inlinable - func intersection(_ other: Self) -> Self { - .init(storage: storage & other.storage) - } - - @inlinable - func symmetricDifference(_ other: Self) -> Self { - .init(storage: storage ^ other.storage) - } - - @inlinable - mutating func formUnion(_ other: Self) { - storage |= other.storage - } - - @inlinable - mutating func formIntersection(_ other: Self) { - storage &= other.storage - } - - @inlinable - mutating func formSymmetricDifference(_ other: Self) { - storage ^= other.storage - } -} - -extension _TinySmallValueIntSet: Sequence { - func makeIterator() -> Iterator { - Iterator(set: self) - } - - struct Iterator: IteratorProtocol { - typealias Element = Int - - private var storage: UInt64 - private var current: Int = -1 - - @inlinable - init(set: _TinySmallValueIntSet) { - self.storage = set.storage - } - - @inlinable - mutating func next() -> Int? { - guard storage != 0 else { - return nil - } - // If the set is somewhat sparse, we can find the next element faster by shifting to the next value. - // This saves needing to do `contains()` checks for all the numbers since the previous element. - let amountToShift = storage.trailingZeroBitCount + 1 - storage >>= amountToShift - - current += amountToShift - return current - } - } -} - -extension _TinySmallValueIntSet { - /// All possible combinations of values to check in order of increasing number of values. - func combinationsToCheck() -> [Self] { - // For `_TinySmallValueIntSet`, leverage the fact that bits of an Int represent the possible combinations. - let smallest = storage.trailingZeroBitCount - - var combinations: [Self] = [] - combinations.reserveCapacity((1 << count /*known to be <64 */) - 1) - - for raw in 1 ... storage >> smallest { - let combination = Self(storage: UInt64(raw << smallest)) - - // Filter out any combinations that include columns that are the same for all overloads - guard self.isSuperset(of: combination) else { continue } - - combinations.append(combination) - } - // The bits of larger and larger Int values won't be in order of number of bits set, so we sort them. - return combinations.sorted(by: { $0.count < $1.count }) - } -} - // MARK: Table /// A fixed-size grid of elements. -private struct Table { +private struct Table: ~Copyable { typealias Size = (width: Int, height: Int) @usableFromInline let size: Size diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift index be9144cc7e..36d5db5372 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift @@ -10,6 +10,7 @@ import Foundation import SymbolKit +import DocCCommon /// An opaque identifier that uniquely identifies a resolved entry in the path hierarchy, /// @@ -149,6 +150,7 @@ struct PathHierarchy { // If there are multiple symbol graphs (for example for different source languages or platforms) then the nodes may have already been added to the hierarchy. var topLevelCandidates = nodes.filter { _, node in node.parent == nil } + let graphLanguageID = language?.id for relationship in graph.relationships where relationship.kind.formsHierarchy { guard let sourceNode = nodes[relationship.source], let expectedContainerName = sourceNode.symbol?.pathComponents.dropLast().last else { continue @@ -188,7 +190,7 @@ struct PathHierarchy { } // Prefer the symbol that matches the relationship's language. - if let targetNode = targetNodes.first(where: { $0.symbol!.identifier.interfaceLanguage == language?.id }) { + if let targetNode = targetNodes.first(where: { $0.symbol!.identifier.interfaceLanguage == graphLanguageID }) { targetNode.add(symbolChild: sourceNode) } else { // It's not clear which target to add the source to, so we add it to all of them. @@ -516,7 +518,7 @@ extension PathHierarchy { /// The symbol, if a node has one. fileprivate(set) var symbol: SymbolGraph.Symbol? /// The languages where this node's symbol is represented. - fileprivate(set) var languages: Set = [] + fileprivate(set) var languages = SmallSourceLanguageSet() /// The other language representation of this symbol. /// /// > Note: Swift currently only supports one other language representation (either Objective-C or C++ but not both). @@ -574,7 +576,7 @@ extension PathHierarchy { fileprivate func deepClone( separating separatedLanguage: SourceLanguage, - keeping otherLanguages: Set, + keeping otherLanguages: SmallSourceLanguageSet, symbolsByUSR: borrowing [String: SymbolGraph.Symbol], didCloneNode: (Node, SymbolGraph.Symbol) -> Void ) -> Node { @@ -718,23 +720,6 @@ extension PathHierarchy { } } -// MARK: Removing nodes - -extension PathHierarchy { - // When unregistering a documentation bundle from a context, entries for that bundle should no longer be findable. - // The below implementation marks nodes as "not findable" while leaving them in the hierarchy so that they can be - // traversed. - // This would be problematic if it happened repeatedly but in practice the path hierarchy will only be in this state - // after unregistering a data provider until a new data provider is registered. - - /// Removes a node from the path hierarchy so that it can no longer be found. - /// - Parameter id: The unique identifier for the node. - mutating func removeNodeWithID(_ id: ResolvedIdentifier) { - // Remove the node from the lookup and unset its identifier - lookup.removeValue(forKey: id)!.identifier = nil - } -} - // MARK: Disambiguation container extension PathHierarchy { diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift index 3c70f61124..1bac256aab 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022-2024 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -10,6 +10,7 @@ import Foundation import SymbolKit +import DocCCommon /// A type that encapsulates resolving links by searching a hierarchy of path components. final class PathHierarchyBasedLinkResolver { @@ -24,19 +25,6 @@ final class PathHierarchyBasedLinkResolver { self.pathHierarchy = pathHierarchy } - /// Remove all matches from a given documentation bundle from the link resolver. - func unregisterBundle(identifier: DocumentationBundle.Identifier) { - var newMap = BidirectionalMap() - for (id, reference) in resolvedReferenceMap { - if reference.bundleID == identifier { - pathHierarchy.removeNodeWithID(id) - } else { - newMap[id] = reference - } - } - resolvedReferenceMap = newMap - } - /// Creates a path string---that can be used to find documentation in the path hierarchy---from an unresolved topic reference, private static func path(for unresolved: UnresolvedTopicReference) -> String { guard let fragment = unresolved.fragment else { @@ -71,7 +59,7 @@ final class PathHierarchyBasedLinkResolver { /// - reference: The identifier of the page whose descendants to return. /// - languagesFilter: A set of source languages to filter descendants against. /// - Returns: The references of each direct descendant that has a language representation in at least one of the given languages. - func directDescendants(of reference: ResolvedTopicReference, languagesFilter: Set) -> Set { + func directDescendants(of reference: ResolvedTopicReference, languagesFilter: SmallSourceLanguageSet) -> Set { guard let id = resolvedReferenceMap[reference] else { return [] } let node = pathHierarchy.lookup[id]! @@ -241,7 +229,7 @@ final class PathHierarchyBasedLinkResolver { /// - isCurrentlyResolvingSymbolLink: Whether or not the documentation link is a symbol link. /// - context: The documentation context to resolve the link in. /// - Returns: The result of resolving the reference. - func resolve(_ unresolvedReference: UnresolvedTopicReference, in parent: ResolvedTopicReference, fromSymbolLink isCurrentlyResolvingSymbolLink: Bool) throws -> TopicReferenceResolutionResult { + func resolve(_ unresolvedReference: UnresolvedTopicReference, in parent: ResolvedTopicReference, fromSymbolLink isCurrentlyResolvingSymbolLink: Bool) throws(PathHierarchy.Error) -> TopicReferenceResolutionResult { let parentID = resolvedReferenceMap[parent] let found = try pathHierarchy.find(path: Self.path(for: unresolvedReference), parent: parentID, onlyFindSymbols: isCurrentlyResolvingSymbolLink) guard let foundReference = resolvedReferenceMap[found] else { @@ -277,8 +265,8 @@ final class PathHierarchyBasedLinkResolver { /// /// - Parameters: /// - symbolGraph: The complete symbol graph to walk through. - /// - bundle: The bundle to use when creating symbol references. - func referencesForSymbols(in unifiedGraphs: [String: UnifiedSymbolGraph], bundle: DocumentationBundle, context: DocumentationContext) -> [SymbolGraph.Symbol.Identifier: ResolvedTopicReference] { + /// - context: The context that the symbols are a part of. + func referencesForSymbols(in unifiedGraphs: [String: UnifiedSymbolGraph], context: DocumentationContext) -> [SymbolGraph.Symbol.Identifier: ResolvedTopicReference] { let disambiguatedPaths = pathHierarchy.caseInsensitiveDisambiguatedPaths(includeDisambiguationForUnambiguousChildren: true, includeLanguage: true, allowAdvancedDisambiguation: false) var result: [SymbolGraph.Symbol.Identifier: ResolvedTopicReference] = [:] @@ -293,7 +281,7 @@ final class PathHierarchyBasedLinkResolver { pathComponents.count == componentsCount { let symbolReference = SymbolReference(pathComponents: pathComponents, interfaceLanguages: symbol.sourceLanguages) - return ResolvedTopicReference(symbolReference: symbolReference, moduleName: moduleName, bundle: bundle) + return ResolvedTopicReference(symbolReference: symbolReference, moduleName: moduleName, bundle: context.inputs) } guard let path = disambiguatedPaths[uniqueIdentifier] else { @@ -301,7 +289,7 @@ final class PathHierarchyBasedLinkResolver { } return ResolvedTopicReference( - bundleID: bundle.documentationRootReference.bundleID, + bundleID: context.inputs.documentationRootReference.bundleID, path: NodeURLGenerator.Path.documentationFolder + path, sourceLanguages: symbol.sourceLanguages ) @@ -352,7 +340,7 @@ private let whitespaceAndDashes = CharacterSet.whitespaces .union(CharacterSet(charactersIn: "-–—")) // hyphen, en dash, em dash private extension PathHierarchy.Node { - func matches(languagesFilter: Set) -> Bool { + func matches(languagesFilter: SmallSourceLanguageSet) -> Bool { languagesFilter.isEmpty || !self.languages.isDisjoint(with: languagesFilter) } } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/SnippetResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/SnippetResolver.swift new file mode 100644 index 0000000000..4c98f7354d --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/SnippetResolver.swift @@ -0,0 +1,156 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 +import Markdown + +/// A type that resolves snippet paths. +final class SnippetResolver { + typealias SnippetMixin = SymbolKit.SymbolGraph.Symbol.Snippet + typealias Explanation = Markdown.Document + + /// Information about a resolved snippet + struct ResolvedSnippet { + fileprivate var path: String // For use in diagnostics + var mixin: SnippetMixin + var explanation: Explanation? + } + /// A snippet that has been resolved, either successfully or not. + enum SnippetResolutionResult { + case success(ResolvedSnippet) + case failure(TopicReferenceResolutionErrorInfo) + } + + private var snippets: [String: ResolvedSnippet] = [:] + + init(symbolGraphLoader: SymbolGraphLoader) { + var snippets: [String: ResolvedSnippet] = [:] + + for graph in symbolGraphLoader.snippetSymbolGraphs.values { + for symbol in graph.symbols.values { + guard let snippetMixin = symbol[mixin: SnippetMixin.self] else { continue } + + let path: String = if symbol.pathComponents.first == "Snippets" { + symbol.pathComponents.dropFirst().joined(separator: "/") + } else { + symbol.pathComponents.joined(separator: "/") + } + + snippets[path] = .init(path: path, mixin: snippetMixin, explanation: symbol.docComment.map { + Document(parsing: $0.lines.map(\.text).joined(separator: "\n"), options: .parseBlockDirectives) + }) + } + } + + self.snippets = snippets + } + + func resolveSnippet(path authoredPath: String) -> SnippetResolutionResult { + // Snippet paths are relative to the root of the Swift Package. + // The first two components are always the same (the package name followed by "Snippets"). + // The later components can either be subdirectories of the "Snippets" directory or the base name of a snippet '.swift' file (without the extension). + + // Drop the common package name + "Snippets" prefix (that's always the same), if the authored path includes it. + // This enables the author to omit this prefix (but include it for backwards compatibility with older DocC versions). + var components = authoredPath.split(separator: "/", omittingEmptySubsequences: true) + + // It's possible that the package name is "Snippets", resulting in two identical components. Skip until the last of those two. + if let snippetsPrefixIndex = components.prefix(2).lastIndex(of: "Snippets"), + // Don't search for an empty string if the snippet happens to be named "Snippets" + let relativePathStart = components.index(snippetsPrefixIndex, offsetBy: 1, limitedBy: components.endIndex - 1) + { + components.removeFirst(relativePathStart) + } + + let path = components.joined(separator: "/") + if let found = snippets[path] { + return .success(found) + } else { + let replacementRange = SourceRange.makeRelativeRange(startColumn: authoredPath.utf8.count - path.utf8.count, length: path.utf8.count) + + let nearMisses = NearMiss.bestMatches(for: snippets.keys, against: path) + let solutions = nearMisses.map { candidate in + Solution(summary: "\(Self.replacementOperationDescription(from: path, to: candidate))", replacements: [ + Replacement(range: replacementRange, replacement: candidate) + ]) + } + + return .failure(.init("Snippet named '\(path)' couldn't be found", solutions: solutions, rangeAdjustment: replacementRange)) + } + } + + func validate(slice: String, for resolvedSnippet: ResolvedSnippet) -> TopicReferenceResolutionErrorInfo? { + guard resolvedSnippet.mixin.slices[slice] == nil else { + return nil + } + let replacementRange = SourceRange.makeRelativeRange(startColumn: 0, length: slice.utf8.count) + + let nearMisses = NearMiss.bestMatches(for: resolvedSnippet.mixin.slices.keys, against: slice) + let solutions = nearMisses.map { candidate in + Solution(summary: "\(Self.replacementOperationDescription(from: slice, to: candidate))", replacements: [ + Replacement(range: replacementRange, replacement: candidate) + ]) + } + + return .init("Slice named '\(slice)' doesn't exist in snippet '\(resolvedSnippet.path)'", solutions: solutions) + } +} + +// MARK: Diagnostics + +extension SnippetResolver { + static func unknownSnippetSliceProblem(source: URL?, range: SourceRange?, errorInfo: TopicReferenceResolutionErrorInfo) -> Problem { + _problem(source: source, range: range, errorInfo: errorInfo, id: "org.swift.docc.unknownSnippetPath") + } + + static func unresolvedSnippetPathProblem(source: URL?, range: SourceRange?, errorInfo: TopicReferenceResolutionErrorInfo) -> Problem { + _problem(source: source, range: range, errorInfo: errorInfo, id: "org.swift.docc.unresolvedSnippetPath") + } + + private static func _problem(source: URL?, range: SourceRange?, errorInfo: TopicReferenceResolutionErrorInfo, id: String) -> Problem { + var solutions: [Solution] = [] + var notes: [DiagnosticNote] = [] + if let range { + if let note = errorInfo.note, let source { + notes.append(DiagnosticNote(source: source, range: range, message: note)) + } + + solutions.append(contentsOf: errorInfo.solutions(referenceSourceRange: range)) + } + + let diagnosticRange: SourceRange? + if var rangeAdjustment = errorInfo.rangeAdjustment, let range { + rangeAdjustment.offsetWithRange(range) + assert(rangeAdjustment.lowerBound.column >= 0, """ + Unresolved snippet reference range adjustment created range with negative column. + Source: \(source?.absoluteString ?? "nil") + Range: \(rangeAdjustment.lowerBound.description):\(rangeAdjustment.upperBound.description) + Summary: \(errorInfo.message) + """) + diagnosticRange = rangeAdjustment + } else { + diagnosticRange = range + } + + let diagnostic = Diagnostic(source: source, severity: .warning, range: diagnosticRange, identifier: id, summary: errorInfo.message, notes: notes) + return Problem(diagnostic: diagnostic, possibleSolutions: solutions) + } + + private static func replacementOperationDescription(from: some StringProtocol, to: some StringProtocol) -> String { + if from.isEmpty { + return "Insert \(to.singleQuoted)" + } + if to.isEmpty { + return "Remove \(from.singleQuoted)" + } + return "Replace \(from.singleQuoted) with \(to.singleQuoted)" + } +} diff --git a/Sources/SwiftDocC/Infrastructure/NodeURLGenerator.swift b/Sources/SwiftDocC/Infrastructure/NodeURLGenerator.swift index cac3ab219c..f58a4a32cc 100644 --- a/Sources/SwiftDocC/Infrastructure/NodeURLGenerator.swift +++ b/Sources/SwiftDocC/Infrastructure/NodeURLGenerator.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -55,8 +55,6 @@ public struct NodeURLGenerator { case documentationCuration(parentPath: String, articleName: String) case article(bundleName: String, articleName: String) case tutorialTableOfContents(name: String) - @available(*, deprecated, renamed: "tutorialTableOfContents(name:)", message: "Use 'tutorialTableOfContents(name:)' instead. This deprecated API will be removed after 6.2 is released") - case technology(technologyName: String) case tutorial(bundleName: String, tutorialName: String) /// A URL safe path under the given root path. @@ -94,8 +92,7 @@ public struct NodeURLGenerator { isDirectory: false ) .path - case .technology(let name), - .tutorialTableOfContents(let name): + case .tutorialTableOfContents(let name): // Format: "/tutorials/Name" return Self.tutorialsFolderURL .appendingPathComponent( diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift index e9d2e42e73..680d8e666b 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -569,7 +569,7 @@ extension ExtendedTypeFormatTransformation { // MARK: Apply Mappings to SymbolGraph private extension SymbolGraph { - mutating func apply(compactMap include: (SymbolGraph.Symbol) throws -> SymbolGraph.Symbol?) rethrows { + mutating func apply(compactMap include: (SymbolGraph.Symbol) throws(Error) -> SymbolGraph.Symbol?) throws(Error) { for (key, symbol) in self.symbols { self.symbols.removeValue(forKey: key) if let newSymbol = try include(symbol) { diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift index 003f6b64aa..d90a10aa73 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,6 +11,7 @@ import Foundation import SymbolKit import Markdown +import DocCCommon /// A collection of APIs to generate documentation topics. enum GeneratedDocumentationTopics { @@ -27,8 +28,6 @@ enum GeneratedDocumentationTopics { struct APICollection { /// The title of the collection. var title: String - /// A reference to the parent of the collection. - let parentReference: ResolvedTopicReference /// A list of topic references for the collection. var identifiers = [ResolvedTopicReference]() } @@ -39,8 +38,7 @@ enum GeneratedDocumentationTopics { /// - childReference: The inherited symbol reference. /// - reference: The parent type reference. /// - originDisplayName: The origin display name as provided by the symbol graph. - /// - extendedModuleName: Extended module name. - mutating func add(_ childReference: ResolvedTopicReference, to reference: ResolvedTopicReference, childSymbol: SymbolGraph.Symbol, originDisplayName: String, originSymbol: SymbolGraph.Symbol?, extendedModuleName: String) throws { + mutating func add(_ childReference: ResolvedTopicReference, to reference: ResolvedTopicReference, childSymbol: SymbolGraph.Symbol, originDisplayName: String, originSymbol: SymbolGraph.Symbol?) throws { let fromType: String let typeSimpleName: String if let originSymbol, originSymbol.pathComponents.count > 1 { @@ -89,7 +87,7 @@ enum GeneratedDocumentationTopics { // Create a new default implementations provider, if needed. if !implementingTypes[reference]!.inheritedFromTypeName.keys.contains(fromType) { - implementingTypes[reference]!.inheritedFromTypeName[fromType] = Collections.APICollection(title: "\(typeSimpleName) Implementations", parentReference: reference) + implementingTypes[reference]!.inheritedFromTypeName[fromType] = Collections.APICollection(title: "\(typeSimpleName) Implementations") } // Add the default implementation. @@ -99,15 +97,13 @@ enum GeneratedDocumentationTopics { private static let defaultImplementationGroupTitle = "Default Implementations" - private static func createCollectionNode(parent: ResolvedTopicReference, title: String, identifiers: [ResolvedTopicReference], context: DocumentationContext, bundle: DocumentationBundle) throws { - let automaticCurationSourceLanguage: SourceLanguage - let automaticCurationSourceLanguages: Set - automaticCurationSourceLanguage = identifiers.first?.sourceLanguage ?? .swift - automaticCurationSourceLanguages = Set(identifiers.flatMap { identifier in context.sourceLanguages(for: identifier) }) + private static func createCollectionNode(parent: ResolvedTopicReference, title: String, identifiers: [ResolvedTopicReference], context: DocumentationContext) throws { + let automaticCurationSourceLanguage = identifiers.first?.sourceLanguage ?? .swift + let automaticCurationSourceLanguages = SmallSourceLanguageSet(identifiers.flatMap { identifier in context.sourceLanguages(for: identifier) }) // Create the collection topic reference let collectionReference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: NodeURLGenerator.Path.documentationCuration( parentPath: parent.path, articleName: title @@ -124,8 +120,8 @@ enum GeneratedDocumentationTopics { let node = try context.entity(with: parent) if let symbol = node.semantic as? Symbol { for trait in node.availableVariantTraits { - guard let language = trait.interfaceLanguage, - automaticCurationSourceLanguages.lazy.map(\.id).contains(language) + guard let language = trait.sourceLanguage, + automaticCurationSourceLanguages.contains(language) else { // If the collection is not available in this trait, don't curate it in this symbol's variant. continue @@ -193,7 +189,7 @@ enum GeneratedDocumentationTopics { reference: collectionReference, kind: .collectionGroup, sourceLanguage: automaticCurationSourceLanguage, - availableSourceLanguages: automaticCurationSourceLanguages, + availableSourceLanguages: Set(automaticCurationSourceLanguages), name: DocumentationNode.Name.conceptual(title: title), markup: Document(parsing: ""), semantic: collectionArticle @@ -230,8 +226,7 @@ enum GeneratedDocumentationTopics { /// - relationships: A set of relationships to inspect. /// - symbolsURLHierarchy: A symbol graph hierarchy as created during symbol registration. /// - context: A documentation context to update. - /// - bundle: The current documentation bundle. - static func createInheritedSymbolsAPICollections(relationships: Set, context: DocumentationContext, bundle: DocumentationBundle) throws { + static func createInheritedSymbolsAPICollections(relationships: Set, context: DocumentationContext) throws { var inheritanceIndex = InheritedSymbols() // Walk the symbol graph relationships and look for parent <-> child links that stem in a different module. @@ -247,13 +242,13 @@ enum GeneratedDocumentationTopics { let child = context.documentationCache[relationship.source], // Get the child symbol let childSymbol = child.symbol, - // Get the swift extension data - let extends = childSymbol[mixin: SymbolGraph.Symbol.Swift.Extension.self] + // Check that there is Swift extension information + childSymbol[mixin: SymbolGraph.Symbol.Swift.Extension.self] != nil { let originSymbol = context.documentationCache[origin.identifier]?.symbol // Add the inherited symbol to the index. - try inheritanceIndex.add(child.reference, to: parent.reference, childSymbol: childSymbol, originDisplayName: origin.displayName, originSymbol: originSymbol, extendedModuleName: extends.extendedModule) + try inheritanceIndex.add(child.reference, to: parent.reference, childSymbol: childSymbol, originDisplayName: origin.displayName, originSymbol: originSymbol) } } @@ -261,7 +256,7 @@ enum GeneratedDocumentationTopics { for (typeReference, collections) in inheritanceIndex.implementingTypes where !collections.inheritedFromTypeName.isEmpty { for (_, collection) in collections.inheritedFromTypeName where !collection.identifiers.isEmpty { // Create a collection for the given provider type's inherited symbols - try createCollectionNode(parent: typeReference, title: collection.title, identifiers: collection.identifiers, context: context, bundle: bundle) + try createCollectionNode(parent: typeReference, title: collection.title, identifiers: collection.identifiers, context: context) } } } diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift index 50baa2529c..620d184122 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphLoader.swift @@ -17,25 +17,25 @@ import SymbolKit /// which makes detecting symbol collisions and overloads easier. struct SymbolGraphLoader { private(set) var symbolGraphs: [URL: SymbolKit.SymbolGraph] = [:] + private(set) var snippetSymbolGraphs: [URL: SymbolKit.SymbolGraph] = [:] private(set) var unifiedGraphs: [String: SymbolKit.UnifiedSymbolGraph] = [:] private(set) var graphLocations: [String: [SymbolKit.GraphCollector.GraphKind]] = [:] - // FIXME: After 6.2, when we no longer have `DocumentationContextDataProvider` we can simply this code to not use a closure to read data. - private var dataLoader: (URL, DocumentationBundle) throws -> Data - private var bundle: DocumentationBundle - private var symbolGraphTransformer: ((inout SymbolGraph) -> ())? = nil + private let dataProvider: any DataProvider + private let bundle: DocumentationBundle + private let symbolGraphTransformer: ((inout SymbolGraph) -> ())? /// Creates a new symbol graph loader /// - Parameters: /// - bundle: The documentation bundle from which to load symbol graphs. - /// - dataLoader: A closure that the loader uses to read symbol graph data. + /// - dataProvider: A provider that the loader uses to read symbol graph data. /// - symbolGraphTransformer: An optional closure that transforms the symbol graph after the loader decodes it. init( bundle: DocumentationBundle, - dataLoader: @escaping (URL, DocumentationBundle) throws -> Data, + dataProvider: any DataProvider, symbolGraphTransformer: ((inout SymbolGraph) -> ())? = nil ) { self.bundle = bundle - self.dataLoader = dataLoader + self.dataProvider = dataProvider self.symbolGraphTransformer = symbolGraphTransformer } @@ -58,16 +58,16 @@ struct SymbolGraphLoader { let loadingLock = Lock() - var loadedGraphs = [URL: (usesExtensionSymbolFormat: Bool?, graph: SymbolKit.SymbolGraph)]() + var loadedGraphs = [URL: (usesExtensionSymbolFormat: Bool?, isSnippetGraph: Bool, graph: SymbolKit.SymbolGraph)]() var loadError: (any Error)? - let loadGraphAtURL: (URL) -> Void = { [dataLoader, bundle] symbolGraphURL in + let loadGraphAtURL: (URL) -> Void = { [dataProvider] symbolGraphURL in // Bail out in case a symbol graph has already errored guard loadingLock.sync({ loadError == nil }) else { return } do { // Load and decode a single symbol graph file - let data = try dataLoader(symbolGraphURL, bundle) + let data = try dataProvider.contents(of: symbolGraphURL) var symbolGraph: SymbolGraph @@ -99,9 +99,13 @@ struct SymbolGraphLoader { usesExtensionSymbolFormat = symbolGraph.symbols.isEmpty ? nil : containsExtensionSymbols } + // If the graph doesn't have any symbols we treat it as a regular, but empty, graph. + // v + let isSnippetGraph = symbolGraph.symbols.values.first?.kind.identifier.isSnippetKind == true + // Store the decoded graph in `loadedGraphs` loadingLock.sync { - loadedGraphs[symbolGraphURL] = (usesExtensionSymbolFormat, symbolGraph) + loadedGraphs[symbolGraphURL] = (usesExtensionSymbolFormat, isSnippetGraph, symbolGraph) } } catch { // If the symbol graph was invalid, store the error @@ -141,8 +145,9 @@ struct SymbolGraphLoader { let mergeSignpostHandle = signposter.beginInterval("Build unified symbol graph", id: signposter.makeSignpostID()) let graphLoader = GraphCollector(extensionGraphAssociationStrategy: usingExtensionSymbolFormat ? .extendingGraph : .extendedGraph) - // feed the loaded graphs into the `graphLoader` - for (url, (_, graph)) in loadedGraphs { + + // feed the loaded non-snippet graphs into the `graphLoader` + for (url, (_, isSnippets, graph)) in loadedGraphs where !isSnippets { graphLoader.mergeSymbolGraph(graph, at: url) } @@ -152,7 +157,8 @@ struct SymbolGraphLoader { throw loadError } - self.symbolGraphs = loadedGraphs.mapValues(\.graph) + self.symbolGraphs = loadedGraphs.compactMapValues({ _, isSnippets, graph in isSnippets ? nil : graph }) + self.snippetSymbolGraphs = loadedGraphs.compactMapValues({ _, isSnippets, graph in isSnippets ? graph : nil }) (self.unifiedGraphs, self.graphLocations) = graphLoader.finishLoading( createOverloadGroups: FeatureFlags.current.isExperimentalOverloadedSymbolPresentationEnabled ) @@ -188,33 +194,7 @@ struct SymbolGraphLoader { } // Alias to declutter code - typealias AvailabilityItem = SymbolGraph.Symbol.Availability.AvailabilityItem - - /// Cache default availability items as we create them on demand. - private var cachedAvailabilityItems = [DefaultAvailability.ModuleAvailability: AvailabilityItem]() - - /// Returns a symbol graph availability item, given a module availability. - /// - returns: An availability item, or `nil` if the input data is invalid. - private func availabilityItem(for defaultAvailability: DefaultAvailability.ModuleAvailability) -> AvailabilityItem? { - if let cached = cachedAvailabilityItems[defaultAvailability] { - return cached - } - return AvailabilityItem(defaultAvailability) - } - - private func loadSymbolGraph(at url: URL) throws -> (SymbolGraph, isMainSymbolGraph: Bool) { - // This is a private method, the `url` key is known to exist - var symbolGraph = symbolGraphs[url]! - let (moduleName, isMainSymbolGraph) = Self.moduleNameFor(symbolGraph, at: url) - - if !isMainSymbolGraph && symbolGraph.module.bystanders == nil { - // If this is an extending another module, change the module name to match the extended module. - // This makes the symbols in this graph have a path that starts with the extended module's name. - symbolGraph.module.name = moduleName - } - - return (symbolGraph, isMainSymbolGraph) - } + private typealias AvailabilityItem = SymbolGraph.Symbol.Availability.AvailabilityItem /// Adds the missing fallback and default availability information to the unified symbol graph /// in case it didn't exists in the loaded symbol graphs. @@ -546,3 +526,9 @@ private extension SymbolGraph.Symbol.Availability.AvailabilityItem { domain?.rawValue.lowercased() == platform.rawValue.lowercased() } } + +extension SymbolGraph.Symbol.KindIdentifier { + var isSnippetKind: Bool { + self == .snippet || self == .snippetGroup + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift index 749ff60ce1..836b84775d 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/SymbolGraphRelationshipsBuilder.swift @@ -346,7 +346,13 @@ struct SymbolGraphRelationshipsBuilder { assertionFailure(AssertionMessages.sourceNotFound(edge)) return } - requiredSymbol.isRequired = required + // If both requirementOf and optionalRequirementOf relationships exist + // for the same symbol, let the optional relationship take precedence. + // Optional protocol requirements sometimes appear with both relationships, + // but non-optional requirements do not. + if !required || requiredSymbol.isRequiredVariants.isEmpty { + requiredSymbol.isRequired = required + } } /// Sets a node in the context as an inherited symbol. diff --git a/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift b/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift index be5516727a..f15c4323ff 100644 --- a/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift +++ b/Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,7 +11,7 @@ import Foundation import Markdown import SymbolKit - +import DocCCommon private let automaticSeeAlsoLimit: Int = { ProcessInfo.processInfo.environment["DOCC_AUTOMATIC_SEE_ALSO_LIMIT"].flatMap { Int($0) } ?? 15 @@ -56,9 +56,7 @@ public struct AutomaticCuration { withTraits variantsTraits: Set, context: DocumentationContext ) throws -> [TaskGroup] { - let languagesFilter = Set(variantsTraits.compactMap { - $0.interfaceLanguage.map { SourceLanguage(id: $0) } - }) + let languagesFilter = SmallSourceLanguageSet(variantsTraits.compactMap(\.sourceLanguage)) // Because the `TopicGraph` uses the same nodes for both language representations and doesn't have awareness of language specific edges, // it can't correctly determine language specific automatic curation. Instead we ask the `PathHierarchy` which is source-language-aware. @@ -155,7 +153,6 @@ public struct AutomaticCuration { for node: DocumentationNode, withTraits variantsTraits: Set, context: DocumentationContext, - bundle: DocumentationBundle, renderContext: RenderContext?, renderer: DocumentationContentRenderer ) -> TaskGroup? { @@ -172,9 +169,7 @@ public struct AutomaticCuration { return nil } - let variantLanguages = Set(variantsTraits.compactMap { traits in - traits.interfaceLanguage.map { SourceLanguage(id: $0) } - }) + let variantLanguages = SmallSourceLanguageSet(variantsTraits.compactMap(\.sourceLanguage)) func isRelevant(_ filteredGroup: DocumentationContentRenderer.ReferenceGroup) -> Bool { // Check if the task group is filtered to a subset of languages diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift index d9b806b61a..28d4490c80 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationBundle+Info.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -18,18 +18,9 @@ extension DocumentationBundle { /// The display name of the bundle. public var displayName: String - @available(*, deprecated, renamed: "id", message: "Use 'id' instead. This deprecated API will be removed after 6.2 is released") - public var identifier: String { - id.rawValue - } - /// The unique identifier of the bundle. public var id: DocumentationBundle.Identifier - /// The version of the bundle. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - public var version: String? - /// The default language identifier for code listings in the bundle. public var defaultCodeListingLanguage: String? @@ -109,23 +100,6 @@ extension DocumentationBundle { ) } - @available(*, deprecated, renamed: "init(displayName:id:defaultCodeListingLanguage:defaultAvailability:defaultModuleKind:)", message: "Use 'Info.init(displayName:id:defaultCodeListingLanguage:defaultAvailability:defaultModuleKind:)' instead. This deprecated API will be removed after 6.2 is released") - public init( - displayName: String, - identifier: String, - defaultCodeListingLanguage: String?, - defaultAvailability: DefaultAvailability?, - defaultModuleKind: String? - ) { - self.init( - displayName: displayName, - id: .init(rawValue: identifier), - defaultCodeListingLanguage: defaultCodeListingLanguage, - defaultAvailability: defaultAvailability, - defaultModuleKind: defaultModuleKind - ) - } - /// Creates documentation bundle information from the given Info.plist data, falling back to the values /// in the given bundle discovery options if necessary. init( @@ -330,26 +304,6 @@ extension BundleDiscoveryOptions { additionalSymbolGraphFiles: additionalSymbolGraphFiles ) } - - @available(*, deprecated, renamed: "init(fallbackDisplayName:fallbackIdentifier:fallbackDefaultCodeListingLanguage:fallbackDefaultModuleKind:fallbackDefaultAvailability:additionalSymbolGraphFiles:)", message: "Use 'init(fallbackDisplayName:fallbackIdentifier:fallbackDefaultCodeListingLanguage:fallbackDefaultModuleKind:fallbackDefaultAvailability:additionalSymbolGraphFiles:)' instead. This deprecated API will be removed after 6.2 is released") - public init( - fallbackDisplayName: String? = nil, - fallbackIdentifier: String? = nil, - fallbackVersion: String?, - fallbackDefaultCodeListingLanguage: String? = nil, - fallbackDefaultModuleKind: String? = nil, - fallbackDefaultAvailability: DefaultAvailability? = nil, - additionalSymbolGraphFiles: [URL] = [] - ) { - self.init( - fallbackDisplayName: fallbackDisplayName, - fallbackIdentifier: fallbackIdentifier, - fallbackDefaultCodeListingLanguage: fallbackDefaultCodeListingLanguage, - fallbackDefaultModuleKind: fallbackDefaultModuleKind, - fallbackDefaultAvailability: fallbackDefaultAvailability, - additionalSymbolGraphFiles:additionalSymbolGraphFiles - ) - } } private extension CodingUserInfoKey { @@ -358,23 +312,3 @@ private extension CodingUserInfoKey { /// A user info key to store derived display name in the decoder. static let derivedDisplayName = CodingUserInfoKey(rawValue: "derivedDisplayName")! } - -extension DocumentationBundle.Info { - @available(*, deprecated, renamed: "init(displayName:identifier:defaultCodeListingLanguage:defaultAvailability:defaultModuleKind:)", message: "Use 'init(displayName:identifier:defaultCodeListingLanguage:defaultAvailability:defaultModuleKind:)' instead. This deprecated API will be removed after 6.2 is released") - public init( - displayName: String, - identifier: String, - version: String?, - defaultCodeListingLanguage: String?, - defaultAvailability: DefaultAvailability?, - defaultModuleKind: String? - ) { - self.init( - displayName: displayName, - identifier: identifier, - defaultCodeListingLanguage: defaultCodeListingLanguage, - defaultAvailability: defaultAvailability, - defaultModuleKind: defaultModuleKind - ) - } -} diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationWorkspace.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationWorkspace.swift deleted file mode 100644 index 874bbeb47e..0000000000 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationWorkspace.swift +++ /dev/null @@ -1,148 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2024 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 -*/ - -public import Foundation - -/// The documentation workspace provides a unified interface for accessing serialized documentation bundles and their files, from a variety of sources. -/// -/// The ``DocumentationContext`` and the workspace that the context is operating in are connected in two ways: -/// - The workspace is the context's data provider. -/// - The context is the workspace's ``DocumentationContextDataProviderDelegate``. -/// -/// The first lets the workspace multiplex the bundles from any number of data providers (``DocumentationWorkspaceDataProvider``) into a single list of -/// ``DocumentationContextDataProvider/bundles`` and allows the context to access the contents of the various bundles without knowing any specifics -/// of its source (files on disk, a database, or a web services). -/// -/// The second lets the workspace notify the context when bundles are added or removed so that the context stays up to date, even after the context is created. -/// -/// ``` -/// ┌─────┐ -/// ┌────────────────────────────────│ IDE │─────────────────────────────┐ -/// ┌──────────┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ └─────┘ │ -/// │FileSystem│─▶ WorkspaceDataProvider ─┐ │ │ -/// └──────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ │ -/// │ │ │ -/// │ │ │ -/// ┌──────────┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌───────────┐ Read-only ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌─────────┐ -/// │WebService│─▶ WorkspaceDataProvider ─┼─▶│ Workspace │◀────interface───── ContextDataProvider ◀────get data────│ Context │ -/// └──────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ └───────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ └─────────┘ -/// │ │ ▲ -/// │ │ │ -/// ┌────────────────┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ │ -/// │MyCustomDatabase│─▶ WorkspaceDataProvider ─┘ │ Bundle or ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ Event push │ -/// └────────────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ └───────file ───────▶ ContextDataProviderDelegate ─────interface─────┘ -/// change └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ -/// ``` -/// -/// > Note: Each data provider is treated as a separate file system. A single documentation bundle may not span multiple data providers. -/// -/// ## Topics -/// -/// ### Data Providers -/// -/// - ``DocumentationWorkspaceDataProvider`` -/// - ``LocalFileSystemDataProvider`` -/// - ``PrebuiltLocalFileSystemDataProvider`` -/// -/// ## See Also -/// -/// - ``DocumentationContext`` -/// - ``DocumentationContextDataProvider`` -/// - ``DocumentationContextDataProviderDelegate`` -/// -@available(*, deprecated, message: "Pass the context its inputs at initialization instead. This deprecated API will be removed after 6.2 is released") -public class DocumentationWorkspace: DocumentationContextDataProvider { - /// An error when requesting information from a workspace. - public enum WorkspaceError: DescribedError { - /// A bundle with the provided ID wasn't found in the workspace. - case unknownBundle(id: String) - /// A data provider with the provided ID wasn't found in the workspace. - case unknownProvider(id: String) - - /// A plain-text description of the error. - public var errorDescription: String { - switch self { - case .unknownBundle(let id): - return "The requested data could not be located because a containing bundle with id '\(id)' could not be found in the workspace." - case .unknownProvider(let id): - return "The requested data could not be located because a containing data provider with id '\(id)' could not be found in the workspace." - } - } - } - - /// Reads the data for a given file in a given documentation bundle. - /// - /// - Parameters: - /// - url: The URL of the file to read. - /// - bundle: The documentation bundle that the file belongs to. - /// - Throws: A ``WorkspaceError/unknownBundle(id:)`` error if the bundle doesn't exist in the workspace or - /// a ``WorkspaceError/unknownProvider(id:)`` error if the bundle's data provider doesn't exist in the workspace. - /// - Returns: The raw data for the given file. - public func contentsOfURL(_ url: URL, in bundle: DocumentationBundle) throws -> Data { - guard let providerID = bundleToProvider[bundle.identifier] else { - throw WorkspaceError.unknownBundle(id: bundle.identifier) - } - - guard let provider = providers[providerID] else { - throw WorkspaceError.unknownProvider(id: providerID) - } - - return try provider.contentsOfURL(url) - } - - /// A map of bundle identifiers to documentation bundles. - public var bundles: [String: DocumentationBundle] = [:] - /// A map of provider identifiers to data providers. - private var providers: [String: any DocumentationWorkspaceDataProvider] = [:] - /// A map of bundle identifiers to provider identifiers (in other words, a map from a bundle to the provider that vends the bundle). - private var bundleToProvider: [String: String] = [:] - /// The delegate to notify when documentation bundles are added or removed from this workspace. - public weak var delegate: (any DocumentationContextDataProviderDelegate)? - /// Creates a new, empty documentation workspace. - public init() {} - - /// Adds a new data provider to the workspace. - /// - /// Adding a data provider also adds the documentation bundles that it provides, and notifies the ``delegate`` of the added bundles. - /// - /// - Parameters: - /// - provider: The workspace data provider to add to the workspace. - /// - options: The options that the data provider uses to discover documentation bundles that it provides to the delegate. - public func registerProvider(_ provider: any DocumentationWorkspaceDataProvider, options: BundleDiscoveryOptions = .init()) throws { - // We must add the provider before adding the bundle so that the delegate - // may start making requests immediately. - providers[provider.identifier] = provider - - for bundle in try provider.bundles(options: options) { - bundles[bundle.identifier] = bundle - bundleToProvider[bundle.identifier] = provider.identifier - try delegate?.dataProvider(self, didAddBundle: bundle) - } - } - - /// Removes a given data provider from the workspace. - /// - /// Removing a data provider also removes all its provided documentation bundles and notifies the ``delegate`` of the removed bundles. - /// - /// - Parameters: - /// - provider: The workspace data provider to remove from the workspace. - /// - options: The options that the data provider uses to discover documentation bundles that it removes from the delegate. - public func unregisterProvider(_ provider: any DocumentationWorkspaceDataProvider, options: BundleDiscoveryOptions = .init()) throws { - for bundle in try provider.bundles(options: options) { - bundles[bundle.identifier] = nil - bundleToProvider[bundle.identifier] = nil - try delegate?.dataProvider(self, didRemoveBundle: bundle) - } - - // The provider must be removed after removing the bundle so that the delegate - // may continue making requests as part of removing the bundle. - providers[provider.identifier] = nil - } -} diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationWorkspaceDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationWorkspaceDataProvider.swift index dbf5c03772..e068262975 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationWorkspaceDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/DocumentationWorkspaceDataProvider.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -10,42 +10,6 @@ public import Foundation -/// A type that vends bundles and responds to requests for data. -@available(*, deprecated, message: "Pass the context its inputs at initialization instead. This deprecated API will be removed after 6.2 is released") -public protocol DocumentationWorkspaceDataProvider { - /// A string that uniquely identifies this data provider. - /// - /// Unless your implementation needs a stable identifier to associate with an external system, it's reasonable to - /// use `UUID().uuidString` for the provider's identifier. - var identifier: String { get } - - /// Returns the data backing one of the files that this data provider provides. - /// - /// Your implementation can expect to only receive URLs that it provides. It's acceptable to assert if you receive - /// a URL that wasn't provided by your data provider, because this indicates a bug in the ``DocumentationWorkspace``. - /// - /// - Parameter url: The URL of a file to return the backing data for. - func contentsOfURL(_ url: URL) throws -> Data - - /// Returns the documentation bundles that your data provider provides. - /// - /// - Parameter options: Configuration that controls how the provider discovers documentation bundles. - /// - /// If your data provider also conforms to ``FileSystemProvider``, there is a default implementation of this method - /// that traverses the ``FileSystemProvider/fileSystem`` to find all documentation bundles in it. - func bundles(options: BundleDiscoveryOptions) throws -> [DocumentationBundle] -} - -@available(*, deprecated, message: "Pass the context its inputs at initialization instead. This deprecated API will be removed after 6.2 is released") -public extension DocumentationWorkspaceDataProvider { - /// Returns the documentation bundles that your data provider provides; discovered with the default options. - /// - /// If your data provider also conforms to ``FileSystemProvider``, there is a default implementation of this method - /// that traverses the ``FileSystemProvider/fileSystem`` to find all documentation bundles in it. - func bundles() throws -> [DocumentationBundle] { - return try bundles(options: BundleDiscoveryOptions()) - } -} /// Options to configure the discovery of documentation bundles public struct BundleDiscoveryOptions { diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/FeatureFlags+Info.swift b/Sources/SwiftDocC/Infrastructure/Workspace/FeatureFlags+Info.swift index dd62465ddd..4f95feb9b2 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/FeatureFlags+Info.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/FeatureFlags+Info.swift @@ -37,11 +37,20 @@ extension DocumentationBundle.Info { self.unknownFeatureFlags = [] } + /// This feature flag corresponds to ``FeatureFlags/isExperimentalCodeBlockAnnotationsEnabled``. + public var experimentalCodeBlockAnnotations: Bool? + + public init(experimentalCodeBlockAnnotations: Bool? = nil) { + self.experimentalCodeBlockAnnotations = experimentalCodeBlockAnnotations + self.unknownFeatureFlags = [] + } + /// A list of decoded feature flag keys that didn't match a known feature flag. public let unknownFeatureFlags: [String] enum CodingKeys: String, CodingKey, CaseIterable { case experimentalOverloadedSymbolPresentation = "ExperimentalOverloadedSymbolPresentation" + case experimentalCodeBlockAnnotations = "ExperimentalCodeBlockAnnotations" } struct AnyCodingKeys: CodingKey { @@ -66,6 +75,9 @@ extension DocumentationBundle.Info { switch codingKey { case .experimentalOverloadedSymbolPresentation: self.experimentalOverloadedSymbolPresentation = try values.decode(Bool.self, forKey: flagName) + + case .experimentalCodeBlockAnnotations: + self.experimentalCodeBlockAnnotations = try values.decode(Bool.self, forKey: flagName) } } else { unknownFeatureFlags.append(flagName.stringValue) @@ -79,6 +91,7 @@ extension DocumentationBundle.Info { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(experimentalOverloadedSymbolPresentation, forKey: .experimentalOverloadedSymbolPresentation) + try container.encode(experimentalCodeBlockAnnotations, forKey: .experimentalCodeBlockAnnotations) } } } diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift deleted file mode 100644 index 2fb4fa60f9..0000000000 --- a/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift +++ /dev/null @@ -1,66 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2024 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 -*/ - -public import Foundation - -/// A type that vends a tree of virtual filesystem objects. -@available(*, deprecated, message: "Use 'FileManagerProtocol.recursiveFiles(startingPoint:)' instead. This deprecated API will be removed after 6.2 is released.") -public protocol FileSystemProvider { - /// The organization of the files that this provider provides. - var fileSystem: FSNode { get } -} - -/// An element in a virtual filesystem. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") -public enum FSNode { - /// A file in a filesystem. - case file(File) - /// A directory in a filesystem. - case directory(Directory) - - /// A file in a virtual file system - public struct File { - /// The URL to this file. - public var url: URL - - /// Creates a new virtual file with a given URL - /// - Parameter url: The URL to this file. - public init(url: URL) { - self.url = url - } - } - - /// A directory in a virtual file system. - public struct Directory { - /// The URL to this directory. - public var url: URL - /// The contents of this directory. - public var children: [FSNode] - - /// Creates a new virtual directory with a given URL and contents. - /// - Parameters: - /// - url: The URL to this directory. - /// - children: The contents of this directory. - public init(url: URL, children: [FSNode]) { - self.url = url - self.children = children - } - } - - /// The URL for the node in the filesystem. - public var url: URL { - switch self { - case .file(let file): - return file.url - case .directory(let directory): - return directory.url - } - } -} diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/GeneratedDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/GeneratedDataProvider.swift deleted file mode 100644 index f345158b56..0000000000 --- a/Sources/SwiftDocC/Infrastructure/Workspace/GeneratedDataProvider.swift +++ /dev/null @@ -1,149 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2022 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 -*/ - -public import Foundation -import SymbolKit - -/// A type that provides documentation bundles that it discovers by traversing the local file system. -@available(*, deprecated, message: "Use 'DocumentationContext.InputProvider' instead. This deprecated API will be removed after 6.2 is released") -public class GeneratedDataProvider: DocumentationWorkspaceDataProvider { - public var identifier: String = UUID().uuidString - - public typealias SymbolGraphDataLoader = (URL) -> Data? - private let symbolGraphDataLoader: SymbolGraphDataLoader - private var generatedMarkdownFiles: [String: Data] = [:] - - /// Creates a new provider that generates documentation bundles from the ``BundleDiscoveryOptions`` it is passed in ``bundles(options:)``. - /// - /// - Parameters: - /// - symbolGraphDataLoader: A closure that loads the raw data for a symbol graph file at a given URL. - public init(symbolGraphDataLoader: @escaping SymbolGraphDataLoader) { - self.symbolGraphDataLoader = symbolGraphDataLoader - } - - public func bundles(options: BundleDiscoveryOptions) throws -> [DocumentationBundle] { - // Find all the unique module names from the symbol graph files and generate a top level module page for each of them. - var moduleNames = Set() - for url in options.additionalSymbolGraphFiles { - guard let data = symbolGraphDataLoader(url) else { - throw Error.unableToLoadSymbolGraphData(url: url) - } - let container = try JSONDecoder().decode(SymbolGraphModuleContainer.self, from: data) - moduleNames.insert(container.module.name) - } - let info: DocumentationBundle.Info - do { - let derivedDisplayName: String? - if moduleNames.count == 1, let moduleName = moduleNames.first { - derivedDisplayName = moduleName - } else { - derivedDisplayName = nil - } - info = try DocumentationBundle.Info( - bundleDiscoveryOptions: options, - derivedDisplayName: derivedDisplayName - ) - } catch { - throw Error.notEnoughDataToGenerateBundle(options: options, underlyingError: error) - } - - guard !options.additionalSymbolGraphFiles.isEmpty else { - return [] - } - - if moduleNames.count == 1, let moduleName = moduleNames.first, moduleName != info.displayName { - generatedMarkdownFiles[moduleName] = Data(""" - # ``\(moduleName)`` - - @Metadata { - @DisplayName("\(info.displayName)") - } - """.utf8) - } else { - for moduleName in moduleNames { - generatedMarkdownFiles[moduleName] = Data("# ``\(moduleName)``".utf8) - } - } - - let topLevelPages = generatedMarkdownFiles.keys.map { URL(string: $0 + ".md")! } - - return [ - DocumentationBundle( - info: info, - symbolGraphURLs: options.additionalSymbolGraphFiles, - markupURLs: topLevelPages, - miscResourceURLs: [] - ) - ] - } - - enum Error: DescribedError { - case unableToLoadSymbolGraphData(url: URL) - case notEnoughDataToGenerateBundle(options: BundleDiscoveryOptions, underlyingError: (any Swift.Error)?) - - var errorDescription: String { - switch self { - case .unableToLoadSymbolGraphData(let url): - return "Unable to load data for symbol graph file at \(url.path.singleQuoted)" - case .notEnoughDataToGenerateBundle(let options, let underlyingError): - var symbolGraphFileList = options.additionalSymbolGraphFiles.reduce("") { $0 + "\n\t" + $1.path } - if !symbolGraphFileList.isEmpty { - symbolGraphFileList += "\n" - } - - var errorMessage = """ - The information provided as command line arguments is not enough to generate a documentation bundle: - """ - - if let underlyingError { - errorMessage += """ - \((underlyingError as? (any DescribedError))?.errorDescription ?? underlyingError.localizedDescription) - - """ - } else { - errorMessage += """ - \(options.infoPlistFallbacks.sorted(by: { lhs, rhs in lhs.key < rhs.key }).map { "\($0.key) : '\($0.value)'" }.joined(separator: "\n")) - Additional symbol graph files: [\(symbolGraphFileList)] - - """ - } - - return errorMessage - } - } - } - - public func contentsOfURL(_ url: URL) throws -> Data { - if DocumentationBundleFileTypes.isMarkupFile(url), let content = generatedMarkdownFiles[url.deletingPathExtension().lastPathComponent] { - return content - } else if DocumentationBundleFileTypes.isSymbolGraphFile(url) { - guard let data = symbolGraphDataLoader(url) else { - throw Error.unableToLoadSymbolGraphData(url: url) - } - return data - } else { - preconditionFailure("Unexpected url '\(url)'.") - } - } -} - -/// A wrapper type that decodes only the module in the symbol graph. -private struct SymbolGraphModuleContainer: Decodable { - /// The decoded symbol graph module. - let module: SymbolGraph.Module - - typealias CodingKeys = SymbolGraph.CodingKeys - - public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.module = try container.decode(SymbolGraph.Module.self, forKey: .module) - } -} diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift deleted file mode 100644 index 9101f51ec1..0000000000 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift +++ /dev/null @@ -1,180 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2024 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 - -@available(*, deprecated, message: "Use 'DocumentationContext.InputProvider' instead. This deprecated API will be removed after 6.2 is released") -extension LocalFileSystemDataProvider: DocumentationWorkspaceDataProvider { - public func bundles(options: BundleDiscoveryOptions) throws -> [DocumentationBundle] { - var bundles = try bundlesInTree(fileSystem, options: options) - - guard case .directory(let rootDirectory) = fileSystem else { - preconditionFailure("Expected directory object at path '\(fileSystem.url.absoluteString)'.") - } - - // If no bundles were found in the root directory, assume that the directory itself is a bundle. - if bundles.isEmpty && self.allowArbitraryCatalogDirectories { - bundles.append(try createBundle(rootDirectory, rootDirectory.children, options: options)) - } - - return bundles - } - - /// Recursively traverses the file system, searching for documentation bundles. - /// - /// - Parameters: - /// - root: The directory in which to search for documentation bundles. - /// - options: Configuration that controls how the provider discovers documentation bundles. - /// - Throws: A ``WorkspaceError`` if one of the found documentation bundle directories is an invalid documentation bundle. - /// - Returns: A list of all the bundles that the provider discovered in the file system. - private func bundlesInTree(_ root: FSNode, options: BundleDiscoveryOptions) throws -> [DocumentationBundle] { - var bundles: [DocumentationBundle] = [] - - guard case .directory(let rootDirectory) = root else { - preconditionFailure("Expected directory object at path '\(root.url.absoluteString)'.") - } - - if DocumentationBundleFileTypes.isDocumentationCatalog(rootDirectory.url) { - bundles.append(try createBundle(rootDirectory, rootDirectory.children, options: options)) - } else { - // Recursively descend when the current root directory isn't a documentation bundle. - for child in rootDirectory.children { - if case .directory = child { - try bundles.append(contentsOf: bundlesInTree(child, options: options)) - } - } - } - - return bundles - } - - /// Creates a documentation bundle from the content in a given documentation bundle directory. - /// - Parameters: - /// - directory: The documentation bundle directory. - /// - bundleChildren: The top-level files and directories in the documentation bundle directory. - /// - options: Configuration that controls how the provider discovers documentation bundles. - /// - Throws: A ``WorkspaceError`` if the content is an invalid documentation bundle or - /// a ``DocumentationBundle/PropertyListError`` error if the bundle's Info.plist file is invalid. - /// - Returns: The new documentation bundle. - private func createBundle(_ directory: FSNode.Directory, _ bundleChildren: [FSNode], options: BundleDiscoveryOptions) throws -> DocumentationBundle { - let infoPlistData: Data? - if let infoPlistRef = findInfoPlist(bundleChildren) { - infoPlistData = try contentsOfURL(infoPlistRef.url) - } else { - infoPlistData = nil - } - let info = try DocumentationBundle.Info( - from: infoPlistData, - bundleDiscoveryOptions: options, - derivedDisplayName: directory.url.deletingPathExtension().lastPathComponent - ) - - let markupFiles = findMarkupFiles(bundleChildren, recursive: true).map { $0.url } - let miscResources = findNonMarkupFiles(bundleChildren, recursive: true).map { $0.url } - let symbolGraphFiles = findSymbolGraphFiles(bundleChildren, recursive: true).map { $0.url } + options.additionalSymbolGraphFiles - - let customHeader = findCustomHeader(bundleChildren)?.url - let customFooter = findCustomFooter(bundleChildren)?.url - let themeSettings = findThemeSettings(bundleChildren)?.url - - return DocumentationBundle( - info: info, - symbolGraphURLs: symbolGraphFiles, - markupURLs: markupFiles, - miscResourceURLs: miscResources, - customHeader: customHeader, - customFooter: customFooter, - themeSettings: themeSettings - ) - } - - /// Performs a shallow search for the first Info.plist file in the given list of files and directories. - /// - Parameter bundleChildren: The list of files and directories to check. - /// - Returns: The first Info.plist file, or `nil` if none of the files is an Info.plist file. - private func findInfoPlist(_ bundleChildren: [FSNode]) -> FSNode.File? { - return bundleChildren.firstFile { DocumentationBundleFileTypes.isInfoPlistFile($0.url) } - } - - /// Finds all the symbol-graph files in the given list of files and directories. - /// - Parameters: - /// - bundleChildren: The list of files and directories to check. - /// - recursive: If `true`, this function will recursively check the files of all directories in the array. If `false`, it will ignore all directories. - /// - Returns: A list of all the symbol-graph files. - private func findSymbolGraphFiles(_ bundleChildren: [FSNode], recursive: Bool) -> [FSNode.File] { - return bundleChildren.files(recursive: recursive) { DocumentationBundleFileTypes.isSymbolGraphFile($0.url) } - } - - /// Finds all the markup files in the given list of files and directories. - /// - Parameters: - /// - bundleChildren: The list of files and directories to check. - /// - recursive: If `true`, this function will recursively check the files of all directories in the array. If `false`, it will ignore all directories. - /// - Returns: A list of all the markup files. - private func findMarkupFiles(_ bundleChildren: [FSNode], recursive: Bool) -> [FSNode.File] { - return bundleChildren.files(recursive: recursive) { DocumentationBundleFileTypes.isMarkupFile($0.url) } - } - - /// Finds all the non-markup files in the given list of files and directories. - /// - Parameters: - /// - bundleChildren: The list of files and directories to check. - /// - recursive: If `true`, this function will recursively check the files of all directories in the array. If `false`, it will ignore all directories. - /// - Returns: A list of all the non-markup files. - private func findNonMarkupFiles(_ bundleChildren: [FSNode], recursive: Bool) -> [FSNode.File] { - bundleChildren.files(recursive: recursive) { !DocumentationBundleFileTypes.isMarkupFile($0.url) && !DocumentationBundleFileTypes.isSymbolGraphFile($0.url) } - } - - private func findCustomHeader(_ bundleChildren: [FSNode]) -> FSNode.File? { - return bundleChildren.firstFile { DocumentationBundleFileTypes.isCustomHeader($0.url) } - } - - private func findCustomFooter(_ bundleChildren: [FSNode]) -> FSNode.File? { - return bundleChildren.firstFile { DocumentationBundleFileTypes.isCustomFooter($0.url) } - } - - private func findThemeSettings(_ bundleChildren: [FSNode]) -> FSNode.File? { - return bundleChildren.firstFile { DocumentationBundleFileTypes.isThemeSettingsFile($0.url) } - } -} - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") -fileprivate extension [FSNode] { - /// Returns the first file that matches a given predicate. - /// - Parameter predicate: A closure that takes a file as its argument and returns a Boolean value indicating whether the file should be returned from this function. - /// - Throws: Any error that the predicate closure raises. - /// - Returns: The first file that matches the predicate. - func firstFile(where predicate: (FSNode.File) throws -> Bool) rethrows -> FSNode.File? { - for case .file(let file) in self where try predicate(file) { - return file - } - return nil - } - - /// Returns all the files that match s given predicate. - /// - Parameters: - /// - recursive: If `true`, this function will recursively check the files of all directories in the array. If `false`, it will ignore all directories in the array. - /// - predicate: A closure that takes a file as its argument and returns a Boolean value indicating whether the file should be included in the returned array. - /// - Throws: Any error that the predicate closure raises. - /// - Returns: The first file that matches the predicate. - func files(recursive: Bool, where predicate: (FSNode.File) throws -> Bool) rethrows -> [FSNode.File] { - var matches: [FSNode.File] = [] - for node in self { - switch node { - case .directory(let directory): - guard recursive else { break } - try matches.append(contentsOf: directory.children.files(recursive: true, where: predicate)) - case .file(let file) where try predicate(file): - matches.append(file) - case .file: - break - } - } - - return matches - } -} diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift deleted file mode 100644 index a24c75444c..0000000000 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift +++ /dev/null @@ -1,57 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2024 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 -*/ - -public import Foundation - -/// A type that provides documentation bundles that it discovers by traversing the local file system. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") -public struct LocalFileSystemDataProvider: FileSystemProvider { - public var identifier: String = UUID().uuidString - - /// The location that this provider searches for documentation bundles in. - public var rootURL: URL - - public var fileSystem: FSNode - - /// Whether to consider the root location as a documentation bundle if the data provider doesn't discover another bundle in the hierarchy from the root location. - public let allowArbitraryCatalogDirectories: Bool - - /// Creates a new provider that recursively traverses the content of the given root URL to discover documentation bundles. - /// - Parameters: - /// - rootURL: The location that this provider searches for documentation bundles in. - /// - allowArbitraryCatalogDirectories: Configures the data provider to consider the root location as a documentation bundle if it doesn't discover another bundle. - public init(rootURL: URL, allowArbitraryCatalogDirectories: Bool = false) throws { - self.rootURL = rootURL - self.allowArbitraryCatalogDirectories = allowArbitraryCatalogDirectories - fileSystem = try LocalFileSystemDataProvider.buildTree(root: rootURL) - } - - /// Builds a virtual file system hierarchy from the contents of a root URL in the local file system. - /// - Parameter root: The location from which to descend to build the virtual file system. - /// - Returns: A virtual file system that describe the file and directory structure within the given URL. - private static func buildTree(root: URL) throws -> FSNode { - var children: [FSNode] = [] - let childURLs = try FileManager.default.contentsOfDirectory(at: root, includingPropertiesForKeys: [URLResourceKey.isDirectoryKey], options: .skipsHiddenFiles) - - for url in childURLs { - if FileManager.default.directoryExists(atPath: url.path) { - children.append(try buildTree(root: url)) - } else { - children.append(FSNode.file(FSNode.File(url: url))) - } - } - return FSNode.directory(FSNode.Directory(url: root, children: children)) - } - - public func contentsOfURL(_ url: URL) throws -> Data { - precondition(url.isFileURL, "Unexpected non-file url '\(url)'.") - return try Data(contentsOf: url) - } -} diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift deleted file mode 100644 index 00c3231d35..0000000000 --- a/Sources/SwiftDocC/Infrastructure/Workspace/PrebuiltLocalFileSystemDataProvider.swift +++ /dev/null @@ -1,35 +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 -*/ - -public import Foundation - -/// A data provider that provides existing in-memory documentation bundles with files on the local filesystem. -@available(*, deprecated, message: "Use 'DocumentationContext.InputProvider' instead. This deprecated API will be removed after 6.2 is released") -public struct PrebuiltLocalFileSystemDataProvider: DocumentationWorkspaceDataProvider { - public var identifier: String = UUID().uuidString - - private var _bundles: [DocumentationBundle] - public func bundles(options: BundleDiscoveryOptions) throws -> [DocumentationBundle] { - // Ignore the bundle discovery options, these bundles are already built. - return _bundles - } - - /// Creates a new provider to provide the given documentation bundles. - /// - Parameter bundles: The existing documentation bundles for this provider to provide. - public init(bundles: [DocumentationBundle]) { - _bundles = bundles - } - - public func contentsOfURL(_ url: URL) throws -> Data { - precondition(url.isFileURL, "Unexpected non-file url '\(url)'.") - return try Data(contentsOf: url) - } -} - diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 0a901bab56..e0f64d733f 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -83,6 +83,11 @@ public struct LinkDestinationSummary: Codable, Equatable { /// The relative presentation URL for this element. public let relativePresentationURL: URL + /// The absolute presentation URL for this element, or `nil` if only the _relative_ presentation URL is known. + /// + /// - Note: The absolute presentation URL (if one exists) and the relative presentation URL will always have the same path and fragment components. + let absolutePresentationURL: URL? + /// The resolved topic reference URL to this element. public var referenceURL: URL @@ -107,7 +112,8 @@ public struct LinkDestinationSummary: Codable, Equatable { // so that external documentation sources don't need to provide that data. // Adding new required properties is considered breaking change since existing external documentation sources // wouldn't necessarily meet these new requirements. - + // Make sure to update the encoding, decoding and Equatable implementations when adding new properties. + /// A collection of identifiers that all relate to some common task, as described by the title. public struct TaskGroup: Codable, Equatable { /// The title of this task group @@ -135,11 +141,32 @@ public struct LinkDestinationSummary: Codable, Equatable { /// The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. public let usr: String? + /// The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. + public let plainTextDeclaration: String? + /// The rendered fragments of a symbol's declaration. public typealias DeclarationFragments = [DeclarationRenderSection.Token] - /// The fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. - public let declarationFragments: DeclarationFragments? + /// The simplified "subheading" declaration fragments for this symbol, or `nil` if the summarized element isn't a symbol. + /// + /// These subheading fragments are suitable to use to refer to a symbol that's linked to in a topic group. + /// + /// - Note: The subheading fragments do not represent the symbol's full declaration. + /// Different overloads may have indistinguishable subheading fragments. + public let subheadingDeclarationFragments: DeclarationFragments? + @available(*, deprecated, renamed: "subheadingDeclarationFragments", message: "Use 'subheadingDeclarationFragments' instead. This deprecated API will be removed after 6.3 is released.") + public var declarationFragments: DeclarationFragments? { + subheadingDeclarationFragments + } + + /// The simplified "navigator" declaration fragments for this symbol, or `nil` if the summarized element isn't a symbol. + /// + /// These navigator fragments are suitable to use to refer to a symbol that's linked to in a navigator. + /// + /// - Note: The navigator title does not represent the symbol's full declaration. + /// Different overloads may have indistinguishable navigator fragments. + public let navigatorDeclarationFragments: DeclarationFragments? + /// Any previous URLs for this element. /// /// A web server can use this list of URLs to redirect to the current URL. @@ -193,15 +220,35 @@ public struct LinkDestinationSummary: Codable, Equatable { /// If the summarized element has a precise symbol identifier but the variant doesn't, this property will be `Optional.some(nil)`. public let usr: VariantValue - /// The declaration of the variant or `nil` if the declaration is the same as the summarized element. + /// The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. + /// + /// If the summarized element has a plain text declaration but the variant doesn't, this property will be `Optional.some(nil)`. + public let plainTextDeclaration: VariantValue + + /// The simplified "subheading" declaration fragments for this symbol, or `nil` if the declaration is the same as the summarized element. + /// + /// These subheading fragments are suitable to use to refer to a symbol that's linked to in a topic group. /// /// If the summarized element has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. - public let declarationFragments: VariantValue + public let subheadingDeclarationFragments: VariantValue + @available(*, deprecated, renamed: "subheadingDeclarationFragments", message: "Use 'subheadingDeclarationFragments' instead. This deprecated API will be removed after 6.4 is released.") + public var declarationFragments: VariantValue { + subheadingDeclarationFragments + } + + /// The simplified "navigator" declaration fragments for this symbol, or `nil` if the navigator title is the same as the summarized element. + /// + /// These navigator fragments are suitable to use to refer to a symbol that's linked to in a navigator. + /// + /// If the summarized element has a navigator title but the variant doesn't, this property will be `Optional.some(nil)`. + public let navigatorDeclarationFragments: VariantValue + /// Images that are used to represent the summarized element or `nil` if the images are the same as the summarized element. /// /// If the summarized element has an image but the variant doesn't, this property will be `Optional.some(nil)`. - public let topicImages: VariantValue<[TopicImage]?> + @available(*, deprecated, message: "`TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.4 is released") + public let topicImages: VariantValue<[TopicImage]?> = nil /// Creates a new summary variant with the values that are different from the main summarized values. /// @@ -214,8 +261,9 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - abstract: The abstract of the variant or `nil` if the abstract is the same as the summarized element. /// - taskGroups: The taskGroups of the variant or `nil` if the taskGroups is the same as the summarized element. /// - usr: The precise symbol identifier of the variant or `nil` if the precise symbol identifier is the same as the summarized element. - /// - declarationFragments: The declaration of the variant or `nil` if the declaration is the same as the summarized element. - /// - topicImages: Images that are used to represent the summarized element or `nil` if the images are the same as the summarized element. + /// - plainTextDeclaration: The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. + /// - subheadingDeclarationFragments: The simplified "subheading" declaration fragments for this symbol, to display in topic groups, or `nil` if the declaration is the same as the summarized element. + /// - navigatorDeclarationFragments: The simplified "navigator" declaration fragments for this symbol, to display in navigation, or `nil` if the declaration is the same as the summarized element. public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -225,8 +273,9 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: VariantValue = nil, taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, - declarationFragments: VariantValue = nil, - topicImages: VariantValue<[TopicImage]?> = nil + plainTextDeclaration: VariantValue = nil, + subheadingDeclarationFragments: VariantValue = nil, + navigatorDeclarationFragments: VariantValue = nil ) { self.traits = traits self.kind = kind @@ -236,8 +285,39 @@ public struct LinkDestinationSummary: Codable, Equatable { self.abstract = abstract self.taskGroups = taskGroups self.usr = usr - self.declarationFragments = declarationFragments - self.topicImages = topicImages + self.plainTextDeclaration = plainTextDeclaration + self.subheadingDeclarationFragments = subheadingDeclarationFragments + self.navigatorDeclarationFragments = navigatorDeclarationFragments + } + + @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorDeclarationFragments:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorDeclarationFragments:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.4 is released") + public init( + traits: [RenderNode.Variant.Trait], + kind: VariantValue = nil, + language: VariantValue = nil, + relativePresentationURL: VariantValue = nil, + title: VariantValue = nil, + abstract: VariantValue = nil, + taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, + usr: VariantValue = nil, + plainTextDeclaration: VariantValue = nil, + declarationFragments: VariantValue = nil, + navigatorDeclarationFragments: VariantValue = nil, + topicImages: VariantValue<[TopicImage]?> = nil + ) { + self.init( + traits: traits, + kind: kind, + language: language, + relativePresentationURL: relativePresentationURL, + title: title, + abstract: abstract, + taskGroups: taskGroups, + usr: usr, + plainTextDeclaration: plainTextDeclaration, + subheadingDeclarationFragments: declarationFragments, + navigatorDeclarationFragments: navigatorDeclarationFragments + ) } } @@ -257,7 +337,9 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - platforms: Information about the platforms for which the summarized element is available. /// - taskGroups: The reference URLs of the summarized element's children, grouped by their task groups. /// - usr: The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. - /// - declarationFragments: The fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. + /// - plainTextDeclaration: The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. + /// - subheadingDeclarationFragments: The simplified "subheading" fragments for this symbol, to display in topic groups, or `nil` if the summarized element isn't a symbol. + /// - navigatorDeclarationFragments: The simplified "subheading" declaration fragments for this symbol, to display in navigation, or `nil` if the summarized element isn't a symbol. /// - redirects: Any previous URLs for this element, or `nil` if this element has no previous URLs. /// - topicImages: Images that are used to represent the summarized element, or `nil` if this element has no topic images. /// - references: References used in the content of the summarized element, or `nil` if this element has no references to other content. @@ -272,7 +354,9 @@ public struct LinkDestinationSummary: Codable, Equatable { platforms: [LinkDestinationSummary.PlatformAvailability]? = nil, taskGroups: [LinkDestinationSummary.TaskGroup]? = nil, usr: String? = nil, - declarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, + plainTextDeclaration: String? = nil, + subheadingDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, + navigatorDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, redirects: [URL]? = nil, topicImages: [TopicImage]? = nil, references: [any RenderReference]? = nil, @@ -281,6 +365,7 @@ public struct LinkDestinationSummary: Codable, Equatable { self.kind = kind self.language = language self.relativePresentationURL = relativePresentationURL + self.absolutePresentationURL = nil self.referenceURL = referenceURL self.title = title self.abstract = abstract @@ -288,12 +373,54 @@ public struct LinkDestinationSummary: Codable, Equatable { self.platforms = platforms self.taskGroups = taskGroups self.usr = usr - self.declarationFragments = declarationFragments + self.plainTextDeclaration = plainTextDeclaration + self.subheadingDeclarationFragments = subheadingDeclarationFragments + self.navigatorDeclarationFragments = navigatorDeclarationFragments self.redirects = redirects self.topicImages = topicImages self.references = references self.variants = variants } + + @available(*, deprecated, renamed: "init(kind:language:relativePresentationURL:referenceURL:title:abstract:availableLanguages:platforms:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorDeclarationFragments:redirects:topicImages:references:variants:)", message: "Use `init(kind:language:relativePresentationURL:referenceURL:title:abstract:availableLanguages:platforms:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorDeclarationFragments:redirects:topicImages:references:variants:)` instead. This property will be removed after 6.4 is released") + public init( + kind: DocumentationNode.Kind, + language: SourceLanguage, + relativePresentationURL: URL, + referenceURL: URL, title: String, + abstract: LinkDestinationSummary.Abstract? = nil, + availableLanguages: Set, + platforms: [LinkDestinationSummary.PlatformAvailability]? = nil, + taskGroups: [LinkDestinationSummary.TaskGroup]? = nil, + usr: String? = nil, + plainTextDeclaration: String? = nil, + declarationFragments: LinkDestinationSummary.DeclarationFragments?, + navigatorDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, + redirects: [URL]? = nil, + topicImages: [TopicImage]? = nil, + references: [any RenderReference]? = nil, + variants: [LinkDestinationSummary.Variant] + ) { + self.init( + kind: kind, + language: language, + relativePresentationURL: relativePresentationURL, + referenceURL: referenceURL, + title: title, + abstract: abstract, + availableLanguages: availableLanguages, + platforms: platforms, + taskGroups: taskGroups, + usr: usr, + plainTextDeclaration: plainTextDeclaration, + subheadingDeclarationFragments: declarationFragments, + navigatorDeclarationFragments: navigatorDeclarationFragments, + redirects: redirects, + topicImages: topicImages, + references: references, + variants: variants + ) + } } // MARK: - Accessing the externally linkable elements @@ -311,14 +438,14 @@ public extension DocumentationNode { renderNode: RenderNode, includeTaskGroups: Bool = true ) -> [LinkDestinationSummary] { - guard let bundle = context.bundle, bundle.id == reference.bundleID else { + guard context.inputs.id == reference.bundleID else { // Don't return anything for external references that don't have a bundle in the context. return [] } - let urlGenerator = PresentationURLGenerator(context: context, baseURL: bundle.baseURL) + let urlGenerator = PresentationURLGenerator(context: context, baseURL: context.inputs.baseURL) let relativePresentationURL = urlGenerator.presentationURLForReference(reference).withoutHostAndPortAndScheme() - var compiler = RenderContentCompiler(context: context, bundle: bundle, identifier: reference) + var compiler = RenderContentCompiler(context: context, identifier: reference) let platforms = renderNode.metadata.platforms @@ -330,7 +457,6 @@ public extension DocumentationNode { let taskGroups: [LinkDestinationSummary.TaskGroup]? if includeTaskGroups { switch kind { - case ._technologyOverview: fallthrough // This case is deprecated and will be removed after 6.2 is released. case .tutorial, .tutorialArticle, .tutorialTableOfContents, .chapter, .volume, .onPageLandmark: taskGroups = [.init(title: nil, identifiers: context.children(of: reference).map { $0.reference.absoluteString })] default: @@ -401,7 +527,7 @@ extension LinkDestinationSummary { let topicImages = renderNode.metadata.images let referenceIdentifiers = topicImages.map(\.identifier) - guard let symbol = documentationNode.semantic as? Symbol, let summaryTrait = documentationNode.availableVariantTraits.first(where: { $0.interfaceLanguage == documentationNode.sourceLanguage.id }) else { + guard let symbol = documentationNode.semantic as? Symbol, let summaryTrait = documentationNode.availableVariantTraits.first(where: { $0.sourceLanguage == documentationNode.sourceLanguage }) else { // Only symbol documentation currently support multi-language variants (rdar://86580915) let references = referenceIdentifiers .compactMap { renderNode.references[$0.identifier] } @@ -418,7 +544,7 @@ extension LinkDestinationSummary { platforms: platforms, taskGroups: taskGroups, usr: nil, - declarationFragments: nil, + subheadingDeclarationFragments: nil, redirects: redirects, topicImages: topicImages.nilIfEmpty, references: references.nilIfEmpty, @@ -442,35 +568,49 @@ extension LinkDestinationSummary { let abstract = renderSymbolAbstract(symbol.abstractVariants[summaryTrait] ?? symbol.abstract) let usr = symbol.externalIDVariants[summaryTrait] ?? symbol.externalID - let declaration = (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() + let plainTextDeclaration = symbol.plainTextDeclaration(for: summaryTrait) let language = documentationNode.sourceLanguage - + // If no abbreviated declaration fragments are available, use the full declaration fragments instead. + // In this case, they are assumed to be the same. + let subheadingDeclarationFragments = renderNode.metadata.fragmentsVariants.value(for: language) ?? (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() + let navigatorDeclarationFragments = renderNode.metadata.navigatorTitleVariants.value(for: language) + let variants: [Variant] = documentationNode.availableVariantTraits.compactMap { trait in // Skip the variant for the summarized elements source language. - guard let interfaceLanguage = trait.interfaceLanguage, interfaceLanguage != documentationNode.sourceLanguage.id else { + guard let sourceLanguage = trait.sourceLanguage, sourceLanguage != documentationNode.sourceLanguage else { return nil } - let declarationVariant = symbol.declarationVariants[trait]?.renderDeclarationTokens() - let abstractVariant: Variant.VariantValue = symbol.abstractVariants[trait].map { renderSymbolAbstract($0) } func nilIfEqual(main: Value, variant: Value?) -> Value? { return main == variant ? nil : variant } - let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage(interfaceLanguage)] + let plainTextDeclarationVariant = symbol.plainTextDeclaration(for: trait) + let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage(sourceLanguage.id)] + + // Use the abbreviated declaration fragments instead of the full declaration fragments. + // These have been derived from the symbol's subheading declaration fragments as part of rendering. + // We only want an abbreviated version of the declaration in the link summary (for display in Topic sections, the navigator, etc.). + // Otherwise, the declaration would be too verbose. + // + // However if no abbreviated declaration fragments are available, use the full declaration fragments instead. + // In this case, they are assumed to be the same. + let subheadingDeclarationFragmentsVariant = renderNode.metadata.fragmentsVariants.value(for: variantTraits) ?? symbol.declarationVariants[trait]?.renderDeclarationTokens() + let navigatorDeclarationFragmentsVariant = renderNode.metadata.navigatorTitleVariants.value(for: variantTraits) return Variant( traits: variantTraits, kind: nilIfEqual(main: kind, variant: symbol.kindVariants[trait].map { DocumentationNode.kind(forKind: $0.identifier) }), - language: nilIfEqual(main: language, variant: SourceLanguage(knownLanguageIdentifier: interfaceLanguage)), + language: nilIfEqual(main: language, variant: sourceLanguage), relativePresentationURL: nil, // The symbol variant uses the same relative path title: nilIfEqual(main: title, variant: symbol.titleVariants[trait]), abstract: nilIfEqual(main: abstract, variant: abstractVariant), taskGroups: nilIfEqual(main: taskGroups, variant: taskGroupVariants[variantTraits]), usr: nil, // The symbol variant uses the same USR - declarationFragments: nilIfEqual(main: declaration, variant: declarationVariant), - topicImages: nil // The symbol variant doesn't currently have their own images + plainTextDeclaration: nilIfEqual(main: plainTextDeclaration, variant: plainTextDeclarationVariant), + subheadingDeclarationFragments: nilIfEqual(main: subheadingDeclarationFragments, variant: subheadingDeclarationFragmentsVariant), + navigatorDeclarationFragments: nilIfEqual(main: navigatorDeclarationFragments, variant: navigatorDeclarationFragmentsVariant) ) } @@ -489,7 +629,9 @@ extension LinkDestinationSummary { platforms: platforms, taskGroups: taskGroups, usr: usr, - declarationFragments: declaration, + plainTextDeclaration: plainTextDeclaration, + subheadingDeclarationFragments: subheadingDeclarationFragments, + navigatorDeclarationFragments: navigatorDeclarationFragments, redirects: redirects, topicImages: topicImages.nilIfEmpty, references: references.nilIfEmpty, @@ -557,7 +699,7 @@ extension LinkDestinationSummary { platforms: platforms, taskGroups: [], // Landmarks have no children usr: nil, // Only symbols have a USR - declarationFragments: nil, // Only symbols have declarations + subheadingDeclarationFragments: nil, // Only symbols have declarations redirects: (landmark as? (any Redirected))?.redirects?.map { $0.oldPath }, topicImages: nil, // Landmarks doesn't have topic images references: nil, // Landmarks have no references, since only topic image references is currently supported @@ -571,24 +713,42 @@ extension LinkDestinationSummary { // Add Codable methods—which include an initializer—in an extension so that it doesn't override the member-wise initializer. extension LinkDestinationSummary { enum CodingKeys: String, CodingKey { - case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants + case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, plainTextDeclaration case relativePresentationURL = "path" - case declarationFragments = "fragments" + case subheadingDeclarationFragments = "fragments" + case navigatorDeclarationFragments = "navigatorFragments" } public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(kind.id, forKey: .kind) - try container.encode(relativePresentationURL, forKey: .relativePresentationURL) + if DocumentationNode.Kind.allKnownValues.contains(kind) { + try container.encode(kind.id, forKey: .kind) + } else { + try container.encode(kind, forKey: .kind) + } + try container.encode(absolutePresentationURL ?? relativePresentationURL, forKey: .relativePresentationURL) try container.encode(referenceURL, forKey: .referenceURL) try container.encode(title, forKey: .title) try container.encodeIfPresent(abstract, forKey: .abstract) - try container.encode(language.id, forKey: .language) - try container.encode(availableLanguages.map { $0.id }, forKey: .availableLanguages) + if SourceLanguage.knownLanguages.contains(language) { + try container.encode(language.id, forKey: .language) + } else { + try container.encode(language, forKey: .language) + } + var languagesContainer = container.nestedUnkeyedContainer(forKey: .availableLanguages) + for language in availableLanguages.sorted() { + if SourceLanguage.knownLanguages.contains(language) { + try languagesContainer.encode(language.id) + } else { + try languagesContainer.encode(language) + } + } try container.encodeIfPresent(platforms, forKey: .platforms) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) try container.encodeIfPresent(usr, forKey: .usr) - try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) + try container.encodeIfPresent(subheadingDeclarationFragments, forKey: .subheadingDeclarationFragments) + try container.encodeIfPresent(navigatorDeclarationFragments, forKey: .navigatorDeclarationFragments) try container.encodeIfPresent(redirects, forKey: .redirects) try container.encodeIfPresent(topicImages, forKey: .topicImages) try container.encodeIfPresent(references?.map { CodableRenderReference($0) }, forKey: .references) @@ -600,32 +760,55 @@ extension LinkDestinationSummary { public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let kindID = try container.decode(String.self, forKey: .kind) - guard let foundKind = DocumentationNode.Kind.allKnownValues.first(where: { $0.id == kindID }) else { - throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown DocumentationNode.Kind identifier: '\(kindID)'.") + // Kind can either be a known identifier or a full structure + do { + let kindID = try container.decode(String.self, forKey: .kind) + guard let foundKind = DocumentationNode.Kind.allKnownValues.first(where: { $0.id == kindID }) else { + throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown DocumentationNode.Kind identifier: '\(kindID)'.") + } + kind = foundKind + } catch { + kind = try container.decode(DocumentationNode.Kind.self, forKey: .kind) } - kind = foundKind - relativePresentationURL = try container.decode(URL.self, forKey: .relativePresentationURL) + let decodedURL = try container.decode(URL.self, forKey: .relativePresentationURL) + (relativePresentationURL, absolutePresentationURL) = Self.checkIfDecodedURLWasAbsolute(decodedURL) + referenceURL = try container.decode(URL.self, forKey: .referenceURL) title = try container.decode(String.self, forKey: .title) abstract = try container.decodeIfPresent(Abstract.self, forKey: .abstract) - let languageID = try container.decode(String.self, forKey: .language) - guard let foundLanguage = SourceLanguage.knownLanguages.first(where: { $0.id == languageID }) else { - throw DecodingError.dataCorruptedError(forKey: .language, in: container, debugDescription: "Unknown SourceLanguage identifier: '\(languageID)'.") - } - language = foundLanguage - - let availableLanguageIDs = try container.decode([String].self, forKey: .availableLanguages) - availableLanguages = try Set(availableLanguageIDs.map { languageID in + // Language can either be an identifier of a known language or a full structure + do { + let languageID = try container.decode(String.self, forKey: .language) guard let foundLanguage = SourceLanguage.knownLanguages.first(where: { $0.id == languageID }) else { - throw DecodingError.dataCorruptedError(forKey: .availableLanguages, in: container, debugDescription: "Unknown SourceLanguage identifier: '\(languageID)'.") + throw DecodingError.dataCorruptedError(forKey: .language, in: container, debugDescription: "Unknown SourceLanguage identifier: '\(languageID)'.") + } + language = foundLanguage + } catch DecodingError.typeMismatch { + language = try container.decode(SourceLanguage.self, forKey: .language) + } + + // The set of languages can be a mix of identifiers and full structure + var languagesContainer = try container.nestedUnkeyedContainer(forKey: .availableLanguages) + var decodedLanguages = Set() + while !languagesContainer.isAtEnd { + do { + let languageID = try languagesContainer.decode(String.self) + guard let foundLanguage = SourceLanguage.knownLanguages.first(where: { $0.id == languageID }) else { + throw DecodingError.dataCorruptedError(forKey: .availableLanguages, in: container, debugDescription: "Unknown SourceLanguage identifier: '\(languageID)'.") + } + decodedLanguages.insert( foundLanguage ) + } catch DecodingError.typeMismatch { + decodedLanguages.insert( try languagesContainer.decode(SourceLanguage.self) ) } - return foundLanguage - }) + } + availableLanguages = decodedLanguages + platforms = try container.decodeIfPresent([AvailabilityRenderItem].self, forKey: .platforms) taskGroups = try container.decodeIfPresent([TaskGroup].self, forKey: .taskGroups) usr = try container.decodeIfPresent(String.self, forKey: .usr) - declarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .declarationFragments) + plainTextDeclaration = try container.decodeIfPresent(String.self, forKey: .plainTextDeclaration) + subheadingDeclarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .subheadingDeclarationFragments) + navigatorDeclarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .navigatorDeclarationFragments) redirects = try container.decodeIfPresent([URL].self, forKey: .redirects) topicImages = try container.decodeIfPresent([TopicImage].self, forKey: .topicImages) references = try container.decodeIfPresent([CodableRenderReference].self, forKey: .references).map { decodedReferences in @@ -634,56 +817,95 @@ extension LinkDestinationSummary { variants = try container.decodeIfPresent([Variant].self, forKey: .variants) ?? [] } + + private static func checkIfDecodedURLWasAbsolute(_ decodedURL: URL) -> (relative: URL, absolute: URL?) { + guard decodedURL.isAbsoluteWebURL, + var components = URLComponents(url: decodedURL, resolvingAgainstBaseURL: false) + else { + // If the decoded URL isn't an absolute web URL that's valid according to RFC 3986, then treat it as relative. + return (relative: decodedURL, absolute: nil) + } + + // Remove the scheme, user, port, and host to create a relative URL. + components.scheme = nil + components.user = nil + components.host = nil + components.port = nil + + guard let relativeURL = components.url else { + // If we can't create a relative URL that's valid according to RFC 3986, then treat the original as relative. + return (relative: decodedURL, absolute: nil) + } + + return (relative: relativeURL, absolute: decodedURL) + } } extension LinkDestinationSummary.Variant { enum CodingKeys: String, CodingKey { - case traits, kind, title, abstract, language, usr, taskGroups, topicImages + case traits, kind, title, abstract, language, usr, taskGroups, plainTextDeclaration case relativePresentationURL = "path" case declarationFragments = "fragments" + case navigatorDeclarationFragments = "navigatorFragments" } public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(traits, forKey: .traits) - try container.encodeIfPresent(kind?.id, forKey: .kind) + if let kind { + if DocumentationNode.Kind.allKnownValues.contains(kind) { + try container.encode(kind.id, forKey: .kind) + } else { + try container.encode(kind, forKey: .kind) + } + } try container.encodeIfPresent(relativePresentationURL, forKey: .relativePresentationURL) try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(abstract, forKey: .abstract) - try container.encodeIfPresent(language?.id, forKey: .language) + if let language { + if SourceLanguage.knownLanguages.contains(language) { + try container.encode(language.id, forKey: .language) + } else { + try container.encode(language, forKey: .language) + } + } try container.encodeIfPresent(usr, forKey: .usr) - try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) + try container.encodeIfPresent(subheadingDeclarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(navigatorDeclarationFragments, forKey: .navigatorDeclarationFragments) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) - try container.encodeIfPresent(topicImages, forKey: .topicImages) } public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + traits = try container.decode([RenderNode.Variant.Trait].self, forKey: .traits) - let traits = try container.decode([RenderNode.Variant.Trait].self, forKey: .traits) - for case .interfaceLanguage(let languageID) in traits { - guard SourceLanguage.knownLanguages.contains(where: { $0.id == languageID }) else { - throw DecodingError.dataCorruptedError(forKey: .traits, in: container, debugDescription: "Unknown SourceLanguage identifier: '\(languageID)'.") - } - } - self.traits = traits - - let kindID = try container.decodeIfPresent(String.self, forKey: .kind) - if let kindID { - guard let foundKind = DocumentationNode.Kind.allKnownValues.first(where: { $0.id == kindID }) else { - throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown DocumentationNode.Kind identifier: '\(kindID)'.") + if container.contains(.kind) { + // The kind can either be a known identifier or a full structure + do { + let kindID = try container.decode(String.self, forKey: .kind) + guard let foundKind = DocumentationNode.Kind.allKnownValues.first(where: { $0.id == kindID }) else { + throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown DocumentationNode.Kind identifier: '\(kindID)'.") + } + kind = foundKind + } catch { + kind = try container.decode(DocumentationNode.Kind.self, forKey: .kind) } - kind = foundKind } else { kind = nil } - let languageID = try container.decodeIfPresent(String.self, forKey: .language) - if let languageID { - guard let foundLanguage = SourceLanguage.knownLanguages.first(where: { $0.id == languageID }) else { - throw DecodingError.dataCorruptedError(forKey: .language, in: container, debugDescription: "Unknown SourceLanguage identifier: '\(languageID)'.") + if container.contains(.language) { + // Language can either be an identifier of a known language or a full structure + do { + let languageID = try container.decode(String.self, forKey: .language) + guard let foundLanguage = SourceLanguage.knownLanguages.first(where: { $0.id == languageID }) else { + throw DecodingError.dataCorruptedError(forKey: .language, in: container, debugDescription: "Unknown SourceLanguage identifier: '\(languageID)'.") + } + language = foundLanguage + } catch DecodingError.typeMismatch { + language = try container.decode(SourceLanguage.self, forKey: .language) } - language = foundLanguage } else { language = nil } @@ -691,9 +913,11 @@ extension LinkDestinationSummary.Variant { title = try container.decodeIfPresent(String.self, forKey: .title) abstract = try container.decodeIfPresent(LinkDestinationSummary.Abstract?.self, forKey: .abstract) usr = try container.decodeIfPresent(String?.self, forKey: .usr) - declarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) + plainTextDeclaration = try container.decodeIfPresent(String?.self, forKey: .plainTextDeclaration) + subheadingDeclarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) + navigatorDeclarationFragments = try container + .decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .navigatorDeclarationFragments) taskGroups = try container.decodeIfPresent([LinkDestinationSummary.TaskGroup]?.self, forKey: .taskGroups) - topicImages = try container.decodeIfPresent([TopicImage]?.self, forKey: .topicImages) } } @@ -712,12 +936,15 @@ extension LinkDestinationSummary { guard lhs.kind == rhs.kind else { return false } guard lhs.language == rhs.language else { return false } guard lhs.relativePresentationURL == rhs.relativePresentationURL else { return false } + guard lhs.absolutePresentationURL == rhs.absolutePresentationURL else { return false } guard lhs.title == rhs.title else { return false } guard lhs.abstract == rhs.abstract else { return false } guard lhs.availableLanguages == rhs.availableLanguages else { return false } guard lhs.platforms == rhs.platforms else { return false } guard lhs.taskGroups == rhs.taskGroups else { return false } - guard lhs.declarationFragments == rhs.declarationFragments else { return false } + guard lhs.plainTextDeclaration == rhs.plainTextDeclaration else { return false } + guard lhs.subheadingDeclarationFragments == rhs.subheadingDeclarationFragments else { return false } + guard lhs.navigatorDeclarationFragments == rhs.navigatorDeclarationFragments else { return false } guard lhs.redirects == rhs.redirects else { return false } guard lhs.topicImages == rhs.topicImages else { return false } guard lhs.variants == rhs.variants else { return false } @@ -790,7 +1017,18 @@ private extension DocumentationNode { // specialized articles, like sample code pages, that benefit from being treated as articles in // some parts of the compilation process (like curation) but not others (like link destination // summary creation and render node translation). - return metadata?.pageKind?.kind.documentationNodeKind ?? kind + let baseKind = metadata?.pageKind?.kind.documentationNodeKind ?? kind + + // For articles, check if they should be treated as API Collections (collectionGroup). + // This ensures that linkable entities have the same kind detection logic as the rendering system, + // fixing cross-framework references where API Collections were incorrectly showing as articles. + if baseKind == .article, + let article = semantic as? Article, + DocumentationContentRenderer.roleForArticle(article, nodeKind: kind) == .collectionGroup { + return .collectionGroup + } + + return baseKind } } @@ -801,3 +1039,10 @@ private extension Collection { isEmpty ? nil : self } } + +private extension Symbol { + func plainTextDeclaration(for trait: DocumentationDataVariantsTrait) -> String? { + guard let fullDeclaration = (self.declarationVariants[trait] ?? self.declaration).mainRenderFragments() else { return nil } + return fullDeclaration.declarationFragments.map(\.spelling).joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ") + } +} diff --git a/Sources/SwiftDocC/Model/BuildMetadata.swift b/Sources/SwiftDocC/Model/BuildMetadata.swift index abe6e57e0c..39a83aa468 100644 --- a/Sources/SwiftDocC/Model/BuildMetadata.swift +++ b/Sources/SwiftDocC/Model/BuildMetadata.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -23,11 +23,6 @@ public struct BuildMetadata: Codable { /// The display name of the documentation bundle that DocC built. public var bundleDisplayName: String - @available(*, deprecated, renamed: "bundleID", message: "Use 'bundleID' instead. This deprecated API will be removed after 6.2 is released") - public var bundleIdentifier: String { - bundleID.rawValue - } - /// The bundle identifier of the documentation bundle that DocC built. public let bundleID: DocumentationBundle.Identifier @@ -40,12 +35,4 @@ public struct BuildMetadata: Codable { self.bundleDisplayName = bundleDisplayName self.bundleID = bundleID } - - @available(*, deprecated, renamed: "init(bundleDisplayName:bundleID:)", message: "Use 'init(bundleDisplayName:bundleID:)' instead. This deprecated API will be removed after 6.2 is released") - public init(bundleDisplayName: String, bundleIdentifier: String) { - self.init( - bundleDisplayName: bundleDisplayName, - bundleID: .init(rawValue: bundleIdentifier) - ) - } } diff --git a/Sources/SwiftDocC/Model/DocumentationNode.swift b/Sources/SwiftDocC/Model/DocumentationNode.swift index 3aca3e3e26..b84d5474ef 100644 --- a/Sources/SwiftDocC/Model/DocumentationNode.swift +++ b/Sources/SwiftDocC/Model/DocumentationNode.swift @@ -31,11 +31,7 @@ public struct DocumentationNode { /// All of the traits that make up the different variants of this node. public var availableVariantTraits: Set { - return Set( - availableSourceLanguages - .map(\.id) - .map(DocumentationDataVariantsTrait.init(interfaceLanguage:)) - ) + Set(availableSourceLanguages.map(DocumentationDataVariantsTrait.init(sourceLanguage:))) } /// The names of the platforms for which the node is available. @@ -238,7 +234,7 @@ public struct DocumentationNode { Symbol.Overloads(references: [], displayIndex: overloadData.overloadGroupIndex) }) - var languages = Set([reference.sourceLanguage]) + var languages = reference.sourceLanguages var operatingSystemName = platformName.map({ Set([$0]) }) ?? [] for (_, symbolAvailability) in symbolAvailabilityVariants.allValues { @@ -674,6 +670,7 @@ public struct DocumentationNode { case .union: return .union case .`var`: return .globalVariable case .module: return .module + case .extension: return .extension case .extendedModule: return .extendedModule case .extendedStructure: return .extendedStructure case .extendedClass: return .extendedClass @@ -684,6 +681,55 @@ public struct DocumentationNode { } } + /// Returns a symbol kind for the given documentation node. + /// - Parameter symbol: A documentation node kind. + /// - Returns: A symbol graph symbol. + static func symbolKind(for kind: Kind) -> SymbolGraph.Symbol.KindIdentifier? { + switch kind { + case .associatedType: return .`associatedtype` + case .class: return .`class` + case .deinitializer: return .`deinit` + case .dictionary, .object: return .dictionary + case .dictionaryKey: return .dictionaryKey + case .enumeration: return .`enum` + case .enumerationCase: return .`case` + case .function: return .`func` + case .httpRequest: return .httpRequest + case .httpParameter: return .httpParameter + case .httpBody: return .httpBody + case .httpResponse: return .httpResponse + case .operator: return .`operator` + case .initializer: return .`init` + case .instanceVariable: return .ivar + case .macro: return .macro + case .instanceMethod: return .`method` + case .namespace: return .namespace + case .instanceProperty: return .`property` + case .protocol: return .`protocol` + case .snippet: return .snippet + case .structure: return .`struct` + case .instanceSubscript: return .`subscript` + case .typeMethod: return .`typeMethod` + case .typeProperty, .typeConstant: return .`typeProperty` + case .typeSubscript: return .`typeSubscript` + case .typeAlias, .typeDef: return .`typealias` + case .union: return .union + case .globalVariable, .localVariable: return .`var` + case .module: return .module + case .extension: return .extension + 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: + // For non-symbol kinds (like .article, .tutorial, etc.), + // return nil since these don't have corresponding SymbolGraph.Symbol.KindIdentifier values + return nil + } + } + /// Initializes a documentation node to represent a symbol from a symbol graph. /// /// - Parameters: @@ -726,7 +772,7 @@ public struct DocumentationNode { let symbolAvailability = symbol.mixins[SymbolGraph.Symbol.Availability.mixinKey] as? SymbolGraph.Symbol.Availability - var languages = Set([reference.sourceLanguage]) + var languages = reference.sourceLanguages var operatingSystemName = platformName.map({ Set([$0]) }) ?? [] let availabilityDomains = symbolAvailability?.availability.compactMap({ $0.domain?.rawValue }) @@ -809,7 +855,7 @@ public struct DocumentationNode { self.semantic = article self.sourceLanguage = reference.sourceLanguage self.name = .conceptual(title: article.title?.title ?? "") - self.availableSourceLanguages = [reference.sourceLanguage] + self.availableSourceLanguages = reference.sourceLanguages self.docChunks = [DocumentationChunk(source: .documentationExtension, markup: articleMarkup)] self.markup = articleMarkup self.isVirtual = false @@ -853,7 +899,7 @@ private extension BlockDirective { } } -extension [String] { +extension Collection { /// Strip the minimum leading whitespace from all the strings in this array, as follows: /// - Find the line with least amount of leading whitespace. Ignore blank lines during this search. diff --git a/Sources/SwiftDocC/Model/Identifier.swift b/Sources/SwiftDocC/Model/Identifier.swift index d6e980784a..b6f66561f2 100644 --- a/Sources/SwiftDocC/Model/Identifier.swift +++ b/Sources/SwiftDocC/Model/Identifier.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2023 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,6 +11,7 @@ public import Foundation import SymbolKit public import Markdown +public import DocCCommon /// A resolved or unresolved reference to a piece of documentation. /// @@ -144,7 +145,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString private struct ReferenceKey: Hashable { var path: String var fragment: String? - var sourceLanguages: Set + var sourceLanguages: SmallSourceLanguageSet } /// A synchronized reference cache to store resolved references. @@ -176,11 +177,6 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString /// The storage for the resolved topic reference's state. let _storage: Storage - @available(*, deprecated, renamed: "bundleID", message: "Use 'bundleID' instead. This deprecated API will be removed after 6.2 is released") - public var bundleIdentifier: String { - bundleID.rawValue - } - /// The identifier of the bundle that owns this documentation topic. public var bundleID: DocumentationBundle.Identifier { _storage.bundleID @@ -199,7 +195,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString /// The source language for which this topic is relevant. public var sourceLanguage: SourceLanguage { // Return Swift by default to maintain backwards-compatibility. - return sourceLanguages.contains(.swift) ? .swift : sourceLanguages.first! + _sourceLanguages.min()! } /// The source languages for which this topic is relevant. @@ -208,7 +204,11 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString /// corresponding ``DocumentationNode``. If you need to query the source languages associated with a documentation node, use /// ``DocumentationContext/sourceLanguages(for:)`` instead. public var sourceLanguages: Set { - return _storage.sourceLanguages + Set(_sourceLanguages) + } + + var _sourceLanguages: SmallSourceLanguageSet { + _storage.sourceLanguages } /// - Note: The `path` parameter is escaped to a path readable string. @@ -216,7 +216,12 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString self.init(bundleID: bundleID, path: path, fragment: fragment, sourceLanguages: [sourceLanguage]) } + @_disfavoredOverload public init(bundleID: DocumentationBundle.Identifier, path: String, fragment: String? = nil, sourceLanguages: Set) { + self.init(bundleID: bundleID, path: path, fragment: fragment, sourceLanguages: .init(sourceLanguages)) + } + + init(bundleID: DocumentationBundle.Identifier, path: String, fragment: String? = nil, sourceLanguages: SmallSourceLanguageSet) { self.init( bundleID: bundleID, urlReadablePath: urlReadablePath(path), @@ -224,16 +229,8 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString sourceLanguages: sourceLanguages ) } - @available(*, deprecated, renamed: "init(id:path:fragment:sourceLanguage:)", message: "Use 'init(id:path:fragment:sourceLanguage:)' instead. This deprecated API will be removed after 6.2 is released") - public init(bundleIdentifier: String, path: String, fragment: String? = nil, sourceLanguage: SourceLanguage) { - self.init(bundleIdentifier: bundleIdentifier, path: path, fragment: fragment, sourceLanguages: [sourceLanguage]) - } - @available(*, deprecated, renamed: "init(id:path:fragment:sourceLanguages:)", message: "Use 'init(id:path:fragment:sourceLanguages:)' instead. This deprecated API will be removed after 6.2 is released") - public init(bundleIdentifier: String, path: String, fragment: String? = nil, sourceLanguages: Set) { - self.init(bundleID: .init(rawValue: bundleIdentifier), path: path, fragment: fragment, sourceLanguages: sourceLanguages) - } - private init(bundleID: DocumentationBundle.Identifier, urlReadablePath: String, urlReadableFragment: String? = nil, sourceLanguages: Set) { + private init(bundleID: DocumentationBundle.Identifier, urlReadablePath: String, urlReadableFragment: String? = nil, sourceLanguages: SmallSourceLanguageSet) { precondition(!sourceLanguages.isEmpty, "ResolvedTopicReference.sourceLanguages cannot be empty") // Check for a cached instance of the reference let key = ReferenceKey(path: urlReadablePath, fragment: urlReadableFragment, sourceLanguages: sourceLanguages) @@ -319,8 +316,8 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString let newReference = ResolvedTopicReference( bundleID: bundleID, path: path, - fragment: fragment.map(urlReadableFragment), - sourceLanguages: sourceLanguages + fragment: fragment, // The internal initializer implementation ensures that the fragment is URL readable + sourceLanguages: _sourceLanguages ) return newReference @@ -336,7 +333,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString let newReference = ResolvedTopicReference( bundleID: bundleID, urlReadablePath: url.appendingPathComponent(urlReadablePath(path), isDirectory: false).path, - sourceLanguages: sourceLanguages + sourceLanguages: _sourceLanguages ) return newReference } @@ -358,7 +355,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString bundleID: bundleID, urlReadablePath: newPath, urlReadableFragment: reference.fragment.map(urlReadableFragment), - sourceLanguages: sourceLanguages + sourceLanguages: _sourceLanguages ) return newReference } @@ -370,7 +367,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString bundleID: bundleID, urlReadablePath: newPath, urlReadableFragment: fragment, - sourceLanguages: sourceLanguages + sourceLanguages: _sourceLanguages ) return newReference } @@ -379,10 +376,12 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString /// /// If the current topic reference already includes the given source languages, this returns /// the original topic reference. - public func addingSourceLanguages(_ sourceLanguages: Set) -> ResolvedTopicReference { - let combinedSourceLanguages = self.sourceLanguages.union(sourceLanguages) - - guard combinedSourceLanguages != self.sourceLanguages else { + public func addingSourceLanguages(_ sourceLanguages: some Sequence) -> ResolvedTopicReference { + var updatedLanguages = _sourceLanguages + for language in sourceLanguages { + updatedLanguages.insert(language) + } + guard updatedLanguages != _sourceLanguages else { return self } @@ -390,7 +389,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString bundleID: bundleID, urlReadablePath: path, urlReadableFragment: fragment, - sourceLanguages: combinedSourceLanguages + sourceLanguages: updatedLanguages ) } @@ -399,7 +398,8 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString /// If the current topic reference's source languages equal the given source languages, /// this returns the original topic reference. public func withSourceLanguages(_ sourceLanguages: Set) -> ResolvedTopicReference { - guard sourceLanguages != self.sourceLanguages else { + let newSourceLanguages = SmallSourceLanguageSet(sourceLanguages) + guard newSourceLanguages != _sourceLanguages else { return self } @@ -407,7 +407,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString bundleID: bundleID, urlReadablePath: path, urlReadableFragment: fragment, - sourceLanguages: sourceLanguages + sourceLanguages: newSourceLanguages ) } @@ -423,8 +423,8 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString let sourceLanguageIDVariants = DocumentationDataVariants( values: [DocumentationDataVariantsTrait: String]( - uniqueKeysWithValues: sourceLanguages.map { language in - (DocumentationDataVariantsTrait(interfaceLanguage: language.id), language.id) + uniqueKeysWithValues: _sourceLanguages.map { language in + (DocumentationDataVariantsTrait(sourceLanguage: language), language.id) } ) ) @@ -456,7 +456,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString let bundleID: DocumentationBundle.Identifier let path: String let fragment: String? - let sourceLanguages: Set + let sourceLanguages: SmallSourceLanguageSet let url: URL @@ -468,7 +468,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString bundleID: DocumentationBundle.Identifier, path: String, fragment: String? = nil, - sourceLanguages: Set + sourceLanguages: SmallSourceLanguageSet ) { self.bundleID = bundleID self.path = path @@ -541,11 +541,6 @@ public struct UnresolvedTopicReference: Hashable, CustomStringConvertible { /// The URL as originally spelled. public let topicURL: ValidatedURL - @available(*, deprecated, renamed: "bundleID", message: "Use 'bundleID' instead. This deprecated API will be removed after 6.2 is released") - public var bundleIdentifier: String? { - bundleID?.rawValue - } - /// The bundle identifier, if one was provided in the host name component of the original URL. public var bundleID: DocumentationBundle.Identifier? { topicURL.components.host.map { .init(rawValue: $0) } @@ -607,11 +602,6 @@ public struct UnresolvedTopicReference: Hashable, CustomStringConvertible { /// A reference to an auxiliary resource such as an image. public struct ResourceReference: Hashable { - @available(*, deprecated, renamed: "bundleID", message: "Use 'bundleID' instead. This deprecated API will be removed after 6.2 is released") - public var bundleIdentifier: String { - bundleID.rawValue - } - /// The documentation bundle identifier for the bundle in which this resource resides. public let bundleID: DocumentationBundle.Identifier diff --git a/Sources/SwiftDocC/Model/Kind.swift b/Sources/SwiftDocC/Model/Kind.swift index c6d5abf621..f241c753d0 100644 --- a/Sources/SwiftDocC/Model/Kind.swift +++ b/Sources/SwiftDocC/Model/Kind.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2023 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -203,9 +203,6 @@ extension DocumentationNode.Kind { .extendedModule, .extendedStructure, .extendedClass, .extendedEnumeration, .extendedProtocol, .unknownExtendedType, // Other .keyword, .restAPI, .tag, .propertyList, .object - - // Deprecated, to be removed after 6.2 is released. - , _technologyOverview, ] /// Returns whether this symbol kind is a synthetic "Extended Symbol" symbol kind. @@ -218,12 +215,3 @@ extension DocumentationNode.Kind { } } } - -extension DocumentationNode.Kind { - @available(*, deprecated, renamed: "tutorialTableOfContents", message: "Use 'tutorialTableOfContents' This deprecated API will be removed after 6.2 is released") - public static var technology: Self { tutorialTableOfContents } - - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - public static var technologyOverview: Self { _technologyOverview } - static let _technologyOverview = DocumentationNode.Kind(name: "Technology (Overview)", id: "org.swift.docc.kind.technology.overview", isSymbol: false) -} diff --git a/Sources/SwiftDocC/Model/ParametersAndReturnValidator.swift b/Sources/SwiftDocC/Model/ParametersAndReturnValidator.swift index efa43e189b..4df4d1c174 100644 --- a/Sources/SwiftDocC/Model/ParametersAndReturnValidator.swift +++ b/Sources/SwiftDocC/Model/ParametersAndReturnValidator.swift @@ -296,7 +296,7 @@ struct ParametersAndReturnValidator { var traitsWithNonVoidReturnValues = Set(signatures.keys) for (trait, signature) in signatures { - let language = trait.interfaceLanguage.flatMap(SourceLanguage.init(knownLanguageIdentifier:)) + let language = trait.sourceLanguage // The function signature for Swift initializers indicate a Void return type. // However, initializers have a _conceptual_ return value that's sometimes worth documenting (rdar://131913065). diff --git a/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift b/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift index 7c4695f2a6..ad1827d012 100644 --- a/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift +++ b/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift @@ -124,12 +124,229 @@ public enum RenderBlockContent: Equatable { public var code: [String] /// Additional metadata for this code block. public var metadata: RenderContentMetadata? + /// Annotations for code blocks + public var options: CodeBlockOptions? /// Make a new `CodeListing` with the given data. - public init(syntax: String?, code: [String], metadata: RenderContentMetadata?) { + public init(syntax: String?, code: [String], metadata: RenderContentMetadata?, options: CodeBlockOptions?) { self.syntax = syntax self.code = code self.metadata = metadata + self.options = options + } + } + + public struct CodeBlockOptions: Equatable { + public var language: String? + public var copyToClipboard: Bool + public var showLineNumbers: Bool + public var wrap: Int + public var lineAnnotations: [LineAnnotation] + + public struct Position: Equatable, Comparable, Codable { + public static func < (lhs: RenderBlockContent.CodeBlockOptions.Position, rhs: RenderBlockContent.CodeBlockOptions.Position) -> Bool { + if lhs.line == rhs.line, let lhsCharacter = lhs.character, let rhsCharacter = rhs.character { + return lhsCharacter < rhsCharacter + } + return lhs.line < rhs.line + } + + public init(line: Int, character: Int? = nil) { + self.line = line + self.character = character + } + + public var line: Int + public var character: Int? + } + + public struct LineAnnotation: Equatable, Codable { + public var style: String + public var range: Range + + public init(style: String, range: Range) { + self.style = style + self.range = range + } + } + + public enum OptionName: String, CaseIterable { + case _nonFrozenEnum_useDefaultCase + case nocopy + case wrap + case highlight + case showLineNumbers + case strikeout + case unknown + + init?(caseInsensitive raw: some StringProtocol) { + self.init(rawValue: raw.lowercased()) + } + } + + public static var knownOptions: Set { + Set(OptionName.allCases.map(\.rawValue)) + } + + // empty initializer with default values + public init() { + self.language = "" + self.copyToClipboard = FeatureFlags.current.isExperimentalCodeBlockAnnotationsEnabled + self.showLineNumbers = false + self.wrap = 0 + self.lineAnnotations = [] + } + + public init(parsingLanguageString language: String?) { + let (lang, tokens) = Self.tokenizeLanguageString(language) + + self.language = lang + self.copyToClipboard = !tokens.contains { $0.name == .nocopy } + self.showLineNumbers = tokens.contains { $0.name == .showLineNumbers } + + if let wrapString = tokens.first(where: { $0.name == .wrap })?.value, + let wrapValue = Int(wrapString) { + self.wrap = wrapValue + } else { + self.wrap = 0 + } + + var annotations: [LineAnnotation] = [] + + if let highlightString = tokens.first(where: { $0.name == .highlight })?.value { + let highlightValue = Self.parseCodeBlockOptionsArray(highlightString) + for line in highlightValue { + let pos = Position(line: line, character: nil) + let range = pos.. [Int] { + guard var s = value?.trimmingCharacters(in: .whitespaces), !s.isEmpty else { return [] } + + if s.hasPrefix("[") && s.hasSuffix("]") { + s.removeFirst() + s.removeLast() + } + + return s.split(separator: ",").compactMap { Int($0.trimmingCharacters(in: .whitespaces)) } + } + + /// A function that parses the language line options on code blocks, returning the language and tokens, an array of OptionName and option values + static internal func tokenizeLanguageString(_ input: String?) -> (lang: String?, tokens: [(name: OptionName, value: String?)]) { + guard let input else { return (lang: nil, tokens: []) } + + let parts = parseLanguageString(input) + var tokens: [(OptionName, String?)] = [] + var lang: String? = nil + + for (index, part) in parts.enumerated() { + if let eq = part.firstIndex(of: "=") { + let key = part[.. [Substring] { + + guard let input else { return [] } + var parts: [Substring] = [] + var start = input.startIndex + var i = input.startIndex + + var bracketDepth = 0 + + while i < input.endIndex { + let c = input[i] + + if c == "[" { bracketDepth += 1 } + else if c == "]" { bracketDepth = max(0, bracketDepth - 1) } + else if c == "," && bracketDepth == 0 { + let seq = input[start.. ConformanceSection? { - guard let node = try? documentationContext.entity(with: reference), + guard let node = try? context.entity(with: reference), let symbol = node.symbol else { // Couldn't find the node for this reference return nil @@ -193,8 +194,8 @@ public class DocumentationContentRenderer { } let isLeaf = SymbolReference.isLeaf(symbol) - let parentName = documentationContext.parents(of: reference).first - .flatMap { try? documentationContext.entity(with: $0).symbol?.names.title } + let parentName = context.parents(of: reference).first + .flatMap { try? context.entity(with: $0).symbol?.names.title } let options = ConformanceSection.ConstraintRenderOptions( isLeaf: isLeaf, @@ -212,7 +213,7 @@ public class DocumentationContentRenderer { // We verify that this is a symbol with defined availability // and that we're feeding in a current set of platforms to the context. guard let symbol = node.semantic as? Symbol, - let currentPlatforms = documentationContext.configuration.externalMetadata.currentPlatforms, + let currentPlatforms = context.configuration.externalMetadata.currentPlatforms, !currentPlatforms.isEmpty, let symbolAvailability = symbol.availability?.availability.filter({ !$0.isUnconditionallyUnavailable }), // symbol that's unconditionally unavailable in all the platforms can't be in beta. !symbolAvailability.isEmpty // A symbol without availability items can't be in beta. @@ -231,7 +232,7 @@ public class DocumentationContentRenderer { guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }), // Use the display name of the platform when looking up the current platforms // as we expect that form on the command line. - let current = documentationContext.configuration.externalMetadata.currentPlatforms?[name.displayName] + let current = context.configuration.externalMetadata.currentPlatforms?[name.displayName] else { return false } @@ -288,14 +289,14 @@ public class DocumentationContentRenderer { /// /// - Returns: The rendered documentation node. func renderReference(for reference: ResolvedTopicReference, with overridingDocumentationNode: DocumentationNode? = nil, dependencies: inout RenderReferenceDependencies) -> TopicRenderReference { - let resolver = LinkTitleResolver(context: documentationContext, source: reference.url) + let resolver = LinkTitleResolver(context: context, source: reference.url) let titleVariants: DocumentationDataVariants - let node = try? overridingDocumentationNode ?? documentationContext.entity(with: reference) + let node = try? overridingDocumentationNode ?? context.entity(with: reference) if let node, let resolvedTitle = resolver.title(for: node) { titleVariants = resolvedTitle - } else if let anchorSection = documentationContext.nodeAnchorSections[reference] { + } else if let anchorSection = context.nodeAnchorSections[reference] { // No need to continue, return a section topic reference return TopicRenderReference( identifier: RenderReferenceIdentifier(reference.absoluteString), @@ -305,16 +306,18 @@ public class DocumentationContentRenderer { kind: .section, estimatedTime: nil ) - } else if let topicGraphOnlyNode = documentationContext.topicGraph.nodeWithReference(reference) { + } else if let topicGraphOnlyNode = context.topicGraph.nodeWithReference(reference) { // Some nodes are artificially inserted into the topic graph, // try resolving that way as a fallback after looking up `documentationCache`. titleVariants = .init(defaultVariantValue: topicGraphOnlyNode.title) - } else if let external = documentationContext.externalCache[reference] { - dependencies.topicReferences.append(contentsOf: external.renderReferenceDependencies.topicReferences) - dependencies.linkReferences.append(contentsOf: external.renderReferenceDependencies.linkReferences) - dependencies.imageReferences.append(contentsOf: external.renderReferenceDependencies.imageReferences) + } else if let external = context.externalCache[reference] { + let renderDependencies = external.makeRenderDependencies() + + dependencies.topicReferences.append(contentsOf: renderDependencies.topicReferences) + dependencies.linkReferences.append(contentsOf: renderDependencies.linkReferences) + dependencies.imageReferences.append(contentsOf: renderDependencies.imageReferences) - return external.topicRenderReference + return external.makeTopicRenderReference() } else { titleVariants = .init(defaultVariantValue: reference.absoluteString) } @@ -325,7 +328,7 @@ public class DocumentationContentRenderer { // Topic render references require the URLs to be relative, even if they're external. let presentationURL = urlGenerator.presentationURLForReference(reference) - var contentCompiler = RenderContentCompiler(context: documentationContext, bundle: bundle, identifier: reference) + var contentCompiler = RenderContentCompiler(context: context, identifier: reference) let abstractContent: VariantCollection<[RenderInlineContent]> var abstractedNode = node @@ -334,9 +337,9 @@ public class DocumentationContentRenderer { let containerReference = ResolvedTopicReference( bundleID: reference.bundleID, path: reference.path, - sourceLanguages: reference.sourceLanguages + sourceLanguages: reference._sourceLanguages ) - abstractedNode = try? documentationContext.entity(with: containerReference) + abstractedNode = try? context.entity(with: containerReference) } func extractAbstract(from paragraph: Paragraph?) -> [RenderInlineContent] { @@ -396,13 +399,13 @@ public class DocumentationContentRenderer { renderReference.images = node?.metadata?.pageImages.compactMap { pageImage -> TopicImage? in guard let image = TopicImage( pageImage: pageImage, - with: documentationContext, + with: context, in: reference ) else { return nil } - guard let asset = documentationContext.resolveAsset( + guard let asset = context.resolveAsset( named: image.identifier.identifier, in: reference ) else { @@ -459,7 +462,7 @@ public class DocumentationContentRenderer { var result = [RenderNode.Tag]() /// Add an SPI tag to SPI symbols. - if let node = try? documentationContext.entity(with: reference), + if let node = try? context.entity(with: reference), let symbol = node.semantic as? Symbol, symbol.isSPI { result.append(.spi) @@ -478,7 +481,7 @@ public class DocumentationContentRenderer { /// Returns the task groups for a given node reference. func taskGroups(for reference: ResolvedTopicReference) -> [ReferenceGroup]? { - guard let node = try? documentationContext.entity(with: reference) else { return nil } + guard let node = try? context.entity(with: reference) else { return nil } let groups: [TaskGroup]? switch node.semantic { @@ -504,7 +507,7 @@ public class DocumentationContentRenderer { // For external links, verify they've resolved successfully and return `nil` otherwise. if linkHost != reference.bundleID.rawValue { - if let url = ValidatedURL(destination), case .success(let externalReference) = documentationContext.externallyResolvedLinks[url] { + if let url = ValidatedURL(destination), case .success(let externalReference) = context.externallyResolvedLinks[url] { return externalReference } return nil @@ -517,7 +520,7 @@ public class DocumentationContentRenderer { } let supportedLanguages = group.directives[SupportedLanguage.directiveName]?.compactMap { - SupportedLanguage(from: $0, source: nil, for: bundle)?.language + SupportedLanguage(from: $0, source: nil, for: context.inputs)?.language } return ReferenceGroup( diff --git a/Sources/SwiftDocC/Model/Rendering/LinkTitleResolver.swift b/Sources/SwiftDocC/Model/Rendering/LinkTitleResolver.swift index bf8838c25f..2aceb26ecf 100644 --- a/Sources/SwiftDocC/Model/Rendering/LinkTitleResolver.swift +++ b/Sources/SwiftDocC/Model/Rendering/LinkTitleResolver.swift @@ -27,17 +27,15 @@ struct LinkTitleResolver { /// - Parameter page: The page for which to resolve the title. /// - Returns: The variants of the link title for this page, or `nil` if the page doesn't exist in the context. func title(for page: DocumentationNode) -> DocumentationDataVariants? { - if let bundle = context.bundle, - let directive = page.markup.child(at: 0) as? BlockDirective { - + if let directive = page.markup.child(at: 0) as? BlockDirective { var problems = [Problem]() switch directive.name { case Tutorial.directiveName: - if let tutorial = Tutorial(from: directive, source: source, for: bundle, problems: &problems) { + if let tutorial = Tutorial(from: directive, source: source, for: context.inputs, problems: &problems) { return .init(defaultVariantValue: tutorial.intro.title) } case TutorialTableOfContents.directiveName: - if let overview = TutorialTableOfContents(from: directive, source: source, for: bundle, problems: &problems) { + if let overview = TutorialTableOfContents(from: directive, source: source, for: context.inputs, problems: &problems) { return .init(defaultVariantValue: overview.name) } default: break diff --git a/Sources/SwiftDocC/Model/Rendering/Navigation Tree/RenderHierarchyTranslator.swift b/Sources/SwiftDocC/Model/Rendering/Navigation Tree/RenderHierarchyTranslator.swift index 25c6708333..1dd1e512cd 100644 --- a/Sources/SwiftDocC/Model/Rendering/Navigation Tree/RenderHierarchyTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/Navigation Tree/RenderHierarchyTranslator.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -13,18 +13,15 @@ import Foundation /// A hierarchy translator that converts a part of the topic graph into a hierarchy tree. struct RenderHierarchyTranslator { var context: DocumentationContext - var bundle: DocumentationBundle var collectedTopicReferences = Set() var linkReferences = [String: LinkReference]() - /// Creates a new translator for the given bundle in the given context. + /// Creates a new translator for the given context. /// - Parameters: /// - context: The documentation context for the conversion. - /// - bundle: The documentation bundle for the conversion. - init(context: DocumentationContext, bundle: DocumentationBundle) { + init(context: DocumentationContext) { self.context = context - self.bundle = bundle } static let assessmentsAnchor = urlReadableFragment(TutorialAssessmentsRenderSection.title) @@ -164,7 +161,7 @@ struct RenderHierarchyTranslator { let assessmentReference = ResolvedTopicReference(bundleID: tutorialReference.bundleID, path: tutorialReference.path, fragment: RenderHierarchyTranslator.assessmentsAnchor, sourceLanguage: .swift) renderHierarchyTutorial.landmarks.append(RenderHierarchyLandmark(reference: RenderReferenceIdentifier(assessmentReference.absoluteString), kind: .assessment)) - let urlGenerator = PresentationURLGenerator(context: context, baseURL: bundle.baseURL) + let urlGenerator = PresentationURLGenerator(context: context, baseURL: context.inputs.baseURL) let assessmentLinkReference = LinkReference( identifier: RenderReferenceIdentifier(assessmentReference.absoluteString), title: "Check Your Understanding", @@ -206,7 +203,7 @@ struct RenderHierarchyTranslator { defaultValue: mainPathReferences.map(makeHierarchy) // It's possible that the symbol only has a language representation in a variant language ) - for language in symbolReference.sourceLanguages where language != symbolReference.sourceLanguage { + for language in symbolReference._sourceLanguages where language != symbolReference.sourceLanguage { guard let variantPathReferences = context.linkResolver.localResolver.breadcrumbs(of: symbolReference, in: language), variantPathReferences != mainPathReferences else { diff --git a/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift b/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift index 58fabccede..2f7ddd4d31 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -20,7 +20,6 @@ extension RenderInlineContent: RenderContent {} struct RenderContentCompiler: MarkupVisitor { var context: DocumentationContext - var bundle: DocumentationBundle var identifier: ResolvedTopicReference var imageReferences: [String: ImageReference] = [:] var videoReferences: [String: VideoReference] = [:] @@ -28,9 +27,8 @@ struct RenderContentCompiler: MarkupVisitor { var collectedTopicReferences = GroupedSequence { $0.absoluteString } var linkReferences: [String: LinkReference] = [:] - init(context: DocumentationContext, bundle: DocumentationBundle, identifier: ResolvedTopicReference) { + init(context: DocumentationContext, identifier: ResolvedTopicReference) { self.context = context - self.bundle = bundle self.identifier = identifier } @@ -46,8 +44,19 @@ struct RenderContentCompiler: MarkupVisitor { } mutating func visitCodeBlock(_ codeBlock: CodeBlock) -> [any RenderContent] { - // Default to the bundle's code listing syntax if one is not explicitly declared in the code block. - return [RenderBlockContent.codeListing(.init(syntax: codeBlock.language ?? bundle.info.defaultCodeListingLanguage, code: codeBlock.code.splitByNewlines, metadata: nil))] + if FeatureFlags.current.isExperimentalCodeBlockAnnotationsEnabled { + let codeBlockOptions = RenderBlockContent.CodeBlockOptions(parsingLanguageString: codeBlock.language) + let listing = RenderBlockContent.CodeListing( + syntax: codeBlockOptions.language ?? context.inputs.info.defaultCodeListingLanguage, + code: codeBlock.code.splitByNewlines, + metadata: nil, + options: codeBlockOptions + ) + + return [RenderBlockContent.codeListing(listing)] + } else { + return [RenderBlockContent.codeListing(.init(syntax: codeBlock.language ?? context.inputs.info.defaultCodeListingLanguage, code: codeBlock.code.splitByNewlines, metadata: nil, options: nil))] + } } mutating func visitHeading(_ heading: Heading) -> [any RenderContent] { diff --git a/Sources/SwiftDocC/Model/Rendering/RenderContentConvertible.swift b/Sources/SwiftDocC/Model/Rendering/RenderContentConvertible.swift index 6e7325c800..18e358d94c 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderContentConvertible.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderContentConvertible.swift @@ -24,7 +24,7 @@ extension RenderableDirectiveConvertible { _ blockDirective: BlockDirective, with contentCompiler: inout RenderContentCompiler ) -> [any RenderContent] { - guard let directive = Self.init(from: blockDirective, for: contentCompiler.bundle) else { + guard let directive = Self.init(from: blockDirective, for: contentCompiler.context.inputs) else { return [] } diff --git a/Sources/SwiftDocC/Model/Rendering/RenderContext.swift b/Sources/SwiftDocC/Model/Rendering/RenderContext.swift index dade49b7a0..d6e60d1cec 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderContext.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderContext.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -17,21 +17,23 @@ import SymbolKit /// converting nodes in bulk, i.e. when converting a complete documentation model for example. public struct RenderContext { let documentationContext: DocumentationContext - let bundle: DocumentationBundle let renderer: DocumentationContentRenderer /// Creates a new render context. /// - Warning: Creating a render context pre-renders all content that the context provides. /// - Parameters: /// - documentationContext: A documentation context. - /// - bundle: A documentation bundle. - public init(documentationContext: DocumentationContext, bundle: DocumentationBundle) { + public init(documentationContext: DocumentationContext) { self.documentationContext = documentationContext - self.bundle = bundle - self.renderer = DocumentationContentRenderer(documentationContext: documentationContext, bundle: bundle) + self.renderer = DocumentationContentRenderer(context: documentationContext) createRenderedContent() } + @available(*, deprecated, renamed: "init(context:)", message: "Use 'init(context:)' instead. This deprecated API will be removed after 6.4 is released.") + public init(documentationContext: DocumentationContext, bundle _: DocumentationBundle) { + self.init(documentationContext: documentationContext) + } + /// The pre-rendered content per node reference. private(set) public var store = RenderReferenceStore() @@ -40,10 +42,8 @@ public struct RenderContext { private mutating func createRenderedContent() { let references = documentationContext.knownIdentifiers var topics = [ResolvedTopicReference: RenderReferenceStore.TopicContent]() - let renderer = self.renderer - let documentationContext = self.documentationContext - let renderContentFor: (ResolvedTopicReference) -> RenderReferenceStore.TopicContent = { reference in + let renderContentFor: (ResolvedTopicReference) -> RenderReferenceStore.TopicContent = { [renderer, documentationContext] reference in var dependencies = RenderReferenceDependencies() let renderReference = renderer.renderReference(for: reference, dependencies: &dependencies) let canonicalPath = documentationContext.shortestFinitePath(to: reference).flatMap { $0.isEmpty ? nil : $0 } @@ -90,7 +90,24 @@ public struct RenderContext { // Add all the external content to the topic store for (reference, entity) in documentationContext.externalCache { - topics[reference] = entity.topicContent() + topics[reference] = entity.makeTopicContent() + + // Also include transitive dependencies in the store, so that the external entity can reference them. + for case let dependency as TopicRenderReference in (entity.references ?? []) { + guard let url = URL(string: dependency.identifier.identifier), let rawBundleID = url.host else { + // This dependency doesn't have a valid topic reference, skip adding it to the render context. + continue + } + + let dependencyReference = ResolvedTopicReference( + bundleID: .init(rawValue: rawBundleID), + path: url.path, + fragment: url.fragment, + // TopicRenderReference doesn't have language information. Also, the reference's languages _doesn't_ specify the languages of the linked entity. + sourceLanguages: reference._sourceLanguages + ) + topics[dependencyReference] = .init(renderReference: dependency, canonicalPath: nil, taskGroups: nil, source: nil, isDocumentationExtensionContent: false) + } } self.store = RenderReferenceStore(topics: topics, assets: assets) diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNode.swift b/Sources/SwiftDocC/Model/Rendering/RenderNode.swift index 973ff1f74e..520eb5e8a0 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNode.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNode.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -125,12 +125,6 @@ public struct RenderNode: VariantContainer { /// /// The key for each reference is the ``RenderReferenceIdentifier/identifier`` of the reference's ``RenderReference/identifier``. public var references: [String: any RenderReference] = [:] - - @available(*, deprecated, message: "Use 'hierarchyVariants' instead. This deprecated API will be removed after 6.2 is released") - public var hierarchy: RenderHierarchy? { - get { hierarchyVariants.defaultValue } - set { hierarchyVariants.defaultValue = newValue } - } /// Hierarchy information about the context in which this documentation node is placed. public var hierarchyVariants: VariantCollection = .init(defaultValue: nil) diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNode/CodableContentSection.swift b/Sources/SwiftDocC/Model/Rendering/RenderNode/CodableContentSection.swift index 6358e9b309..44b11c4f57 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNode/CodableContentSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNode/CodableContentSection.swift @@ -15,7 +15,11 @@ import Foundation /// This allows decoding a ``RenderSection`` into its appropriate concrete type, based on the section's /// ``RenderSection/kind``. public struct CodableContentSection: Codable, Equatable { - var section: any RenderSection { + public var section: any RenderSection { + @storageRestrictions(initializes: typeErasedSection) + init(initialValue) { + typeErasedSection = AnyRenderSection(initialValue) + } get { typeErasedSection.value } @@ -27,7 +31,6 @@ public struct CodableContentSection: Codable, Equatable { /// Creates a codable content section from the given section. public init(_ section: any RenderSection) { - self.typeErasedSection = AnyRenderSection(section) self.section = section } @@ -35,7 +38,6 @@ public struct CodableContentSection: Codable, Equatable { let container = try decoder.container(keyedBy: CodingKeys.self) let kind = try container.decode(RenderSectionKind.self, forKey: .kind) - self.typeErasedSection = AnyRenderSection(ContentRenderSection(kind: .content, content: [])) switch kind { case .discussion: section = try ContentRenderSection(from: decoder) diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index 108de80729..443eaf0b3d 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -11,6 +11,7 @@ public import Foundation public import Markdown import SymbolKit +import DocCCommon /// A visitor which converts a semantic model into a render node. /// @@ -134,7 +135,7 @@ public struct RenderNodeTranslator: SemanticVisitor { public mutating func visitTutorial(_ tutorial: Tutorial) -> (any RenderTree)? { var node = RenderNode(identifier: identifier, kind: .tutorial) - var hierarchyTranslator = RenderHierarchyTranslator(context: context, bundle: bundle) + var hierarchyTranslator = RenderHierarchyTranslator(context: context) if let hierarchy = hierarchyTranslator.visitTutorialTableOfContentsNode(identifier) { let tutorialTableOfContents = try! context.entity(with: hierarchy.tutorialTableOfContents).semantic as! TutorialTableOfContents @@ -315,7 +316,7 @@ public struct RenderNodeTranslator: SemanticVisitor { // Visits a container and expects the elements to be block level elements public mutating func visitMarkupContainer(_ markupContainer: MarkupContainer) -> (any RenderTree)? { - var contentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: identifier) + var contentCompiler = RenderContentCompiler(context: context, identifier: identifier) let content = markupContainer.elements.reduce(into: [], { result, item in result.append(contentsOf: contentCompiler.visit(item))}) as! [RenderBlockContent] collectedTopicReferences.append(contentsOf: contentCompiler.collectedTopicReferences) // Copy all the image references found in the markup container. @@ -327,7 +328,7 @@ public struct RenderNodeTranslator: SemanticVisitor { // Visits a collection of inline markup elements. public mutating func visitMarkup(_ markup: [any Markup]) -> (any RenderTree)? { - var contentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: identifier) + var contentCompiler = RenderContentCompiler(context: context, identifier: identifier) let content = markup.reduce(into: [], { result, item in result.append(contentsOf: contentCompiler.visit(item))}) as! [RenderInlineContent] collectedTopicReferences.append(contentsOf: contentCompiler.collectedTopicReferences) // Copy all the image references. @@ -401,7 +402,7 @@ public struct RenderNodeTranslator: SemanticVisitor { node.sections.append(visitResources(resources) as! ResourcesRenderSection) } - var hierarchyTranslator = RenderHierarchyTranslator(context: context, bundle: bundle) + var hierarchyTranslator = RenderHierarchyTranslator(context: context) if let (hierarchyVariants, _) = hierarchyTranslator.visitTutorialTableOfContentsNode(identifier, omittingChapters: true) { node.hierarchyVariants = hierarchyVariants collectedTopicReferences.append(contentsOf: hierarchyTranslator.collectedTopicReferences) @@ -419,7 +420,7 @@ public struct RenderNodeTranslator: SemanticVisitor { private mutating func createTopicRenderReferences() -> [String: any RenderReference] { var renderReferences: [String: any RenderReference] = [:] - let renderer = DocumentationContentRenderer(documentationContext: context, bundle: bundle) + let renderer = DocumentationContentRenderer(context: context) for reference in collectedTopicReferences { var renderReference: TopicRenderReference @@ -530,7 +531,7 @@ public struct RenderNodeTranslator: SemanticVisitor { } public mutating func visitTutorialReference(_ tutorialReference: TutorialReference) -> (any RenderTree)? { - switch context.resolve(tutorialReference.topic, in: bundle.rootReference) { + switch context.resolve(tutorialReference.topic, in: context.inputs.rootReference) { case let .failure(reference, _): return RenderReferenceIdentifier(reference.topicURL.absoluteString) case let .success(resolved): @@ -600,7 +601,7 @@ public struct RenderNodeTranslator: SemanticVisitor { public mutating func visitArticle(_ article: Article) -> (any RenderTree)? { var node = RenderNode(identifier: identifier, kind: .article) // Contains symbol references declared in the Topics section. - var topicSectionContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: identifier) + var topicSectionContentCompiler = RenderContentCompiler(context: context, identifier: identifier) node.metadata.title = article.title!.plainText @@ -624,17 +625,16 @@ public struct RenderNodeTranslator: SemanticVisitor { let documentationNode = try! context.entity(with: identifier) - var hierarchyTranslator = RenderHierarchyTranslator(context: context, bundle: bundle) + var hierarchyTranslator = RenderHierarchyTranslator(context: context) let hierarchyVariants = hierarchyTranslator.visitArticle(identifier) collectedTopicReferences.append(contentsOf: hierarchyTranslator.collectedTopicReferences) node.hierarchyVariants = hierarchyVariants // Emit variants only if we're not compiling an article-only catalog to prevent renderers from - // advertising the page as "Swift", which is the language DocC assigns to pages in article only pages. + // advertising the page as "Swift", which is the language DocC assigns to pages in article only catalogs. // (github.com/swiftlang/swift-docc/issues/240). - if let topLevelModule = context.soleRootModuleReference, - try! context.entity(with: topLevelModule).kind.isSymbol - { + let isArticleOnlyCatalog = context.rootModules.allSatisfy { !context.isSymbol(reference: $0) } + if !isArticleOnlyCatalog { node.variants = variants(for: documentationNode) } @@ -806,7 +806,6 @@ public struct RenderNodeTranslator: SemanticVisitor { for: documentationNode, withTraits: allowedTraits, context: context, - bundle: bundle, renderContext: renderContext, renderer: contentRenderer ) { @@ -878,7 +877,7 @@ public struct RenderNodeTranslator: SemanticVisitor { public mutating func visitTutorialArticle(_ article: TutorialArticle) -> (any RenderTree)? { var node = RenderNode(identifier: identifier, kind: .article) - var hierarchyTranslator = RenderHierarchyTranslator(context: context, bundle: bundle) + var hierarchyTranslator = RenderHierarchyTranslator(context: context) guard let hierarchy = hierarchyTranslator.visitTutorialTableOfContentsNode(identifier) else { // This tutorial article is not curated, so we don't generate a render node. // We've warned about this during semantic analysis. @@ -1029,7 +1028,7 @@ public struct RenderNodeTranslator: SemanticVisitor { ) -> [TaskGroupRenderSection] { return topics.taskGroups.compactMap { group in let supportedLanguages = group.directives[SupportedLanguage.directiveName]?.compactMap { - SupportedLanguage(from: $0, source: nil, for: bundle)?.language + SupportedLanguage(from: $0, source: nil, for: context.inputs)?.language } // If the task group has a set of supported languages, see if it should render for the allowed traits. @@ -1054,17 +1053,20 @@ public struct RenderNodeTranslator: SemanticVisitor { return true } - guard context.isSymbol(reference: reference) else { - // If the reference corresponds to any kind except Symbol - // (e.g., Article, Tutorial, SampleCode...), allow the topic - // to appear independently of the source language it belongs to. + // If this is a reference to a non-symbol kind (article, tutorial, sample code, etc.), + // and is external to the bundle, then curate the topic irrespective of the source + // language of the page or reference, since non-symbol kinds are not tied to a language. + // This is a workaround for https://github.com/swiftlang/swift-docc/issues/240. + // FIXME: This should ideally be solved by making the article language-agnostic rather + // than accomodating the "Swift" language and special-casing for non-symbol nodes. + if !context.isSymbol(reference: reference) && context.isExternal(reference: reference) { return true } - let referenceSourceLanguageIDs = Set(context.sourceLanguages(for: reference).map(\.id)) + let referenceSourceLanguages = SmallSourceLanguageSet(context.sourceLanguages(for: reference)) - let availableSourceLanguageTraits = Set(availableTraits.compactMap(\.interfaceLanguage)) - if availableSourceLanguageTraits.isDisjoint(with: referenceSourceLanguageIDs) { + let availableSourceLanguageTraits = SmallSourceLanguageSet(availableTraits.compactMap(\.sourceLanguage)) + if availableSourceLanguageTraits.isDisjoint(with: referenceSourceLanguages) { // The set of available source language traits has no members in common with the // set of source languages the given reference is available in. // @@ -1073,10 +1075,8 @@ public struct RenderNodeTranslator: SemanticVisitor { return true } - return referenceSourceLanguageIDs.contains { sourceLanguageID in - allowedTraits.contains { trait in - trait.interfaceLanguage == sourceLanguageID - } + return allowedTraits.contains { trait in + trait.sourceLanguage.map { referenceSourceLanguages.contains($0) } ?? false } } @@ -1201,7 +1201,7 @@ public struct RenderNodeTranslator: SemanticVisitor { let identifier = identifier.addingSourceLanguages(documentationNode.availableSourceLanguages) var node = RenderNode(identifier: identifier, kind: .symbol) - var contentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: identifier) + var contentCompiler = RenderContentCompiler(context: context, identifier: identifier) /* FIXME: We shouldn't be doing this kind of crawling here. @@ -1241,7 +1241,7 @@ public struct RenderNodeTranslator: SemanticVisitor { node.metadata.extendedModuleVariants = VariantCollection(from: symbol.extendedModuleVariants) - let defaultAvailability = defaultAvailability(for: bundle, moduleName: moduleName.symbolName, currentPlatforms: context.configuration.externalMetadata.currentPlatforms)? + let defaultAvailability = defaultAvailability(moduleName: moduleName.symbolName, currentPlatforms: context.configuration.externalMetadata.currentPlatforms)? .filter { $0.unconditionallyUnavailable != true } .sorted(by: AvailabilityRenderOrder.compare) @@ -1257,13 +1257,14 @@ public struct RenderNodeTranslator: SemanticVisitor { if availability.obsoletedVersion != nil { return nil } - guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }), - let currentPlatform = context.configuration.externalMetadata.currentPlatforms?[name.displayName] - else { + // Filter out this availability item if it has a missing or invalid domain. + guard let name = availability.domain.map({ PlatformName(operatingSystemName: $0.rawValue) }) else { + return nil + } + guard let currentPlatform = context.configuration.externalMetadata.currentPlatforms?[name.displayName] else { // No current platform provided by the context return AvailabilityRenderItem(availability, current: nil) } - return AvailabilityRenderItem(availability, current: currentPlatform) } .filter { $0.unconditionallyUnavailable != true } @@ -1329,10 +1330,10 @@ public struct RenderNodeTranslator: SemanticVisitor { collectedTopicReferences.append(identifier) - let contentRenderer = DocumentationContentRenderer(documentationContext: context, bundle: bundle) + let contentRenderer = DocumentationContentRenderer(context: context) node.metadata.tags = contentRenderer.tags(for: identifier) - var hierarchyTranslator = RenderHierarchyTranslator(context: context, bundle: bundle) + var hierarchyTranslator = RenderHierarchyTranslator(context: context) let hierarchyVariants = hierarchyTranslator.visitSymbol(identifier) collectedTopicReferences.append(contentsOf: hierarchyTranslator.collectedTopicReferences) node.hierarchyVariants = hierarchyVariants @@ -1477,7 +1478,7 @@ public struct RenderNodeTranslator: SemanticVisitor { } } else if let entity = context.externalCache[resolved] { collectedTopicReferences.append(resolved) - destinationsMap[destination] = entity.topicRenderReference.title + destinationsMap[destination] = entity.title } else { fatalError("A successfully resolved reference should have either local or external content.") } @@ -1647,7 +1648,6 @@ public struct RenderNodeTranslator: SemanticVisitor { for: documentationNode, withTraits: allowedTraits, context: context, - bundle: bundle, renderContext: renderContext, renderer: contentRenderer ), !seeAlso.references.isEmpty { @@ -1755,7 +1755,7 @@ public struct RenderNodeTranslator: SemanticVisitor { let downloadReference: DownloadReference do { let downloadURL = resolvedAssets.variants.first!.value - let downloadData = try context.contentsOfURL(downloadURL, in: bundle) + let downloadData = try context.dataProvider.contents(of: downloadURL) downloadReference = DownloadReference(identifier: mediaReference, renderURL: downloadURL, checksum: Checksum.sha512(of: downloadData)) @@ -1773,7 +1773,6 @@ public struct RenderNodeTranslator: SemanticVisitor { } var context: DocumentationContext - var bundle: DocumentationBundle var identifier: ResolvedTopicReference var imageReferences: [String: ImageReference] = [:] var videoReferences: [String: VideoReference] = [:] @@ -1807,8 +1806,8 @@ public struct RenderNodeTranslator: SemanticVisitor { } /// The default availability for modules in a given bundle and module. - mutating func defaultAvailability(for bundle: DocumentationBundle, moduleName: String, currentPlatforms: [String: PlatformVersion]?) -> [AvailabilityRenderItem]? { - let identifier = BundleModuleIdentifier(bundle: bundle, moduleName: moduleName) + private mutating func defaultAvailability(moduleName: String, currentPlatforms: [String: PlatformVersion]?) -> [AvailabilityRenderItem]? { + let identifier = BundleModuleIdentifier(bundle: context.inputs, moduleName: moduleName) // Cached availability if let availability = bundleAvailability[identifier] { @@ -1816,7 +1815,7 @@ public struct RenderNodeTranslator: SemanticVisitor { } // Find default module availability if existing - guard let bundleDefaultAvailability = bundle.info.defaultAvailability, + guard let bundleDefaultAvailability = context.inputs.info.defaultAvailability, let moduleAvailability = bundleDefaultAvailability.modules[moduleName] else { return nil } @@ -1850,7 +1849,7 @@ public struct RenderNodeTranslator: SemanticVisitor { } private func variants(for documentationNode: DocumentationNode) -> [RenderNode.Variant] { - let generator = PresentationURLGenerator(context: context, baseURL: bundle.baseURL) + let generator = PresentationURLGenerator(context: context, baseURL: context.inputs.baseURL) var allVariants: [SourceLanguage: ResolvedTopicReference] = documentationNode.availableSourceLanguages.reduce(into: [:]) { partialResult, language in partialResult[language] = identifier @@ -1869,7 +1868,7 @@ public struct RenderNodeTranslator: SemanticVisitor { // Symbols can only specify custom alternate language representations for languages that the documented symbol doesn't already have a representation for. // If the current symbol and its custom alternate representation share language representations, the custom language representation is ignored. allVariants.merge( - alternateRepresentationReference.sourceLanguages.map { ($0, alternateRepresentationReference) } + alternateRepresentationReference._sourceLanguages.map { ($0, alternateRepresentationReference) } ) { existing, _ in existing } } } @@ -2001,7 +2000,6 @@ public struct RenderNodeTranslator: SemanticVisitor { init( context: DocumentationContext, - bundle: DocumentationBundle, identifier: ResolvedTopicReference, renderContext: RenderContext? = nil, emitSymbolSourceFileURIs: Bool = false, @@ -2010,10 +2008,9 @@ public struct RenderNodeTranslator: SemanticVisitor { symbolIdentifiersWithExpandedDocumentation: [String]? = nil ) { self.context = context - self.bundle = bundle self.identifier = identifier self.renderContext = renderContext - self.contentRenderer = DocumentationContentRenderer(documentationContext: context, bundle: bundle) + self.contentRenderer = DocumentationContentRenderer(context: context) self.shouldEmitSymbolSourceFileURIs = emitSymbolSourceFileURIs self.shouldEmitSymbolAccessLevels = emitSymbolAccessLevels self.sourceRepository = sourceRepository @@ -2055,13 +2052,8 @@ extension ContentRenderSection: RenderTree {} private extension Sequence { func matchesOneOf(traits: Set) -> Bool { - traits.contains(where: { - guard let languageID = $0.interfaceLanguage, - let traitLanguage = SourceLanguage(knownLanguageIdentifier: languageID) - else { - return false - } - return self.contains(traitLanguage) + traits.contains(where: { trait in + trait.sourceLanguage.map { self.contains($0) } ?? false }) } } diff --git a/Sources/SwiftDocC/Model/Rendering/RenderReferenceStore.swift b/Sources/SwiftDocC/Model/Rendering/RenderReferenceStore.swift index b84ffa7616..d095303fc5 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderReferenceStore.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderReferenceStore.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -41,11 +41,6 @@ public struct RenderReferenceStore: Codable { public func content(forAssetNamed assetName: String, bundleID: DocumentationBundle.Identifier) -> DataAsset? { assets[AssetReference(assetName: assetName, bundleID: bundleID)] } - - @available(*, deprecated, renamed: "content(forAssetNamed:bundleID:)", message: "Use 'content(forAssetNamed:bundleID:)' instead. This deprecated API will be removed after 6.2 is released") - public func content(forAssetNamed assetName: String, bundleIdentifier: String) -> DataAsset? { - content(forAssetNamed: assetName, bundleID: .init(rawValue: bundleIdentifier)) - } } public extension RenderReferenceStore { diff --git a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift index cc88210346..5cacc05eab 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DeclarationsSectionTranslator.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -44,13 +44,17 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { /// Fetch the common fragments for the given references, or compute it if necessary. func commonFragments( for mainDeclaration: OverloadDeclaration, - overloadDeclarations: [OverloadDeclaration] + overloadDeclarations: [OverloadDeclaration], + mainDeclarationIndex: Int ) -> [SymbolGraph.Symbol.DeclarationFragments.Fragment] { if let fragments = commonFragments(for: mainDeclaration.reference) { return fragments } - let preProcessedDeclarations = [mainDeclaration.declaration] + overloadDeclarations.map(\.declaration) + var preProcessedDeclarations = overloadDeclarations.map(\.declaration) + // Insert the main declaration according to the display index so the ordering is consistent + // between overloaded symbols + preProcessedDeclarations.insert(mainDeclaration.declaration, at: mainDeclarationIndex) // Collect the "common fragments" so we can highlight the ones that are different // in each declaration @@ -183,21 +187,45 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { return declarations } - func sortPlatformNames(_ platforms: [PlatformName?]) -> [PlatformName?] { - platforms.sorted { (lhs, rhs) -> Bool in - guard let lhsValue = lhs, let rhsValue = rhs else { - return lhs == nil - } - return lhsValue.rawValue < rhsValue.rawValue + /// Returns the given platforms with any missing fallback platforms added. + /// + /// This function uses the centralized `DefaultAvailability.fallbackPlatforms` mapping to ensure + /// consistency with platform expansion logic used throughout the codebase. + /// + /// For example, when iOS is present in the platforms array, this function adds iPadOS and Mac Catalyst + /// if they are not already included. + /// + /// - Parameter platforms: The original platforms array. + /// - Returns: The platforms array with fallback platforms added where applicable. + func expandPlatformsWithFallbacks(_ platforms: [PlatformName?]) -> [PlatformName?] { + guard !platforms.isEmpty else { return platforms } + + // Add fallback platforms if their primary platform is present but the fallback is missing + let fallbacks = DefaultAvailability.fallbackPlatforms.compactMap { fallback, primary in + platforms.contains(primary) && !platforms.contains(fallback) ? fallback : nil + } + return platforms + fallbacks + } + + func comparePlatformNames(_ lhs: PlatformName?, _ rhs: PlatformName?) -> Bool { + guard let lhsValue = lhs, let rhsValue = rhs else { + return lhs == nil } + return lhsValue.rawValue < rhsValue.rawValue + } + + func sortPlatformNames(_ platforms: [PlatformName?]) -> [PlatformName?] { + platforms.sorted(by: comparePlatformNames(_:_:)) } var declarations: [DeclarationRenderSection] = [] - let languages = [ + let renderLanguageIDs = [ trait.interfaceLanguage ?? renderNodeTranslator.identifier.sourceLanguage.id ] for pair in declaration { let (platforms, declaration) = pair + let expandedPlatforms = expandPlatformsWithFallbacks(platforms) + let platformNames = sortPlatformNames(expandedPlatforms) let renderedTokens: [DeclarationRenderSection.Token] let otherDeclarations: DeclarationRenderSection.OtherDeclarations? @@ -216,7 +244,9 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { // in each declaration let commonFragments = commonFragments( for: (mainDeclaration, renderNode.identifier, nil), - overloadDeclarations: processedOverloadDeclarations) + overloadDeclarations: processedOverloadDeclarations, + mainDeclarationIndex: overloads.displayIndex + ) renderedTokens = translateDeclaration( mainDeclaration, @@ -233,8 +263,8 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { declarations.append( DeclarationRenderSection( - languages: languages, - platforms: sortPlatformNames(platforms), + languages: renderLanguageIDs, + platforms: platformNames, tokens: renderedTokens, otherDeclarations: otherDeclarations ) @@ -244,13 +274,14 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { if let alternateDeclarations = symbol.alternateDeclarationVariants[trait] { for pair in alternateDeclarations { let (platforms, decls) = pair - let platformNames = sortPlatformNames(platforms) + let expandedPlatforms = expandPlatformsWithFallbacks(platforms) + let platformNames = sortPlatformNames(expandedPlatforms) for alternateDeclaration in decls { let renderedTokens = alternateDeclaration.declarationFragments.map(translateFragment) declarations.append( DeclarationRenderSection( - languages: languages, + languages: renderLanguageIDs, platforms: platformNames, tokens: renderedTokens ) @@ -259,6 +290,15 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator { } } + declarations.sort { (lhs, rhs) -> Bool in + // We only need to compare the first platform in each list against the + // first platform in any other list, so pull them out here + guard let lhsPlatform = lhs.platforms.first, let rhsPlatform = rhs.platforms.first else { + return lhs.platforms.isEmpty + } + return comparePlatformNames(lhsPlatform, rhsPlatform) + } + return DeclarationsRenderSection(declarations: declarations) } } diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/AttributesRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/AttributesRenderSection.swift index d3f84bd3d6..18faf1e46c 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/AttributesRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/AttributesRenderSection.swift @@ -14,9 +14,9 @@ import Foundation public struct AttributesRenderSection: RenderSection, Equatable { public var kind: RenderSectionKind = .attributes /// The section title. - public let title: String + public var title: String /// The list of attributes in this section. - public let attributes: [RenderAttribute]? + public var attributes: [RenderAttribute]? /// Creates a new attributes section. /// - Parameter title: The section title. diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/ConformanceSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/ConformanceSection.swift index 7048c13d3a..73ca49b210 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/ConformanceSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/ConformanceSection.swift @@ -20,6 +20,7 @@ extension Constraint.Kind { case .conformance: return "conforms to" case .sameType: return "is" case .superclass: return "inherits" + case .sameShape: return "is the same shape as" } } } diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/ParameterRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/ParameterRenderSection.swift index 572e27c7bc..7a7f93eaa5 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/ParameterRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/ParameterRenderSection.swift @@ -12,7 +12,7 @@ public struct ParametersRenderSection: RenderSection, Equatable { public var kind: RenderSectionKind = .parameters /// The list of parameter sub-sections. - public let parameters: [ParameterRenderSection] + public var parameters: [ParameterRenderSection] /// Creates a new parameters section with the given list. public init(parameters: [ParameterRenderSection]) { diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/PossibleValuesRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/PossibleValuesRenderSection.swift index c5e0938448..5bedd1d523 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/PossibleValuesRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/PossibleValuesRenderSection.swift @@ -29,9 +29,9 @@ public struct PossibleValuesRenderSection: RenderSection, Equatable { public var kind: RenderSectionKind = .possibleValues /// The title for the section, `nil` by default. - public let title: String? + public var title: String? /// The list of named values. - public let values: [NamedValue] + public var values: [NamedValue] /// Creates a new possible values section. /// - Parameter title: The section title. diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/PropertiesRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/PropertiesRenderSection.swift index 527d042289..2be88815da 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/PropertiesRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/PropertiesRenderSection.swift @@ -16,9 +16,9 @@ import Foundation public struct PropertiesRenderSection: RenderSection { public var kind: RenderSectionKind = .properties /// The title for this section. - public let title: String + public var title: String /// The list of properties. - public let items: [RenderProperty] + public var items: [RenderProperty] /// Creates a new property-list section. /// - Parameters: @@ -60,9 +60,9 @@ public struct RenderProperty: Codable, TextIndexing, Equatable { /// The list of possible type declarations for the property's value including additional details, if available. public let typeDetails: [TypeDetails]? /// Additional details about the property, if available. - public let content: [RenderBlockContent]? + public var content: [RenderBlockContent]? /// Additional list of attributes, if any. - public let attributes: [RenderAttribute]? + public var attributes: [RenderAttribute]? /// A mime-type associated with the property, if applicable. public let mimeType: String? /// If true, the property is required in its containing context. diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/RESTBodyRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/RESTBodyRenderSection.swift index 5d62c5e98e..173ef737e3 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/RESTBodyRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/RESTBodyRenderSection.swift @@ -14,7 +14,7 @@ import Foundation public struct RESTBodyRenderSection: RenderSection, Equatable { public var kind: RenderSectionKind = .restBody /// A title for the section. - public let title: String + public var title: String /// Content encoding MIME type for the request body. public let mimeType: String @@ -23,14 +23,14 @@ public struct RESTBodyRenderSection: RenderSection, Equatable { public let bodyContentType: [DeclarationRenderSection.Token] /// Details about the request body, if available. - public let content: [RenderBlockContent]? + public var content: [RenderBlockContent]? /// A list of request parameters, if applicable. /// /// If the body content is `multipart/form-data` encoded, it contains a list /// of parameters. Each of these parameters is a ``RESTParameter`` /// and it has its own value-content encoding, name, type, and description. - public let parameters: [RenderProperty]? + public var parameters: [RenderProperty]? /// Creates a new REST body section. /// - Parameters: diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/RESTEndpointRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/RESTEndpointRenderSection.swift index b6e72e4a20..f727375d7c 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/RESTEndpointRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/RESTEndpointRenderSection.swift @@ -50,7 +50,7 @@ public struct RESTEndpointRenderSection: RenderSection, Equatable { } /// The title for the section. - public let title: String + public var title: String /// The list of tokens. public let tokens: [Token] diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/RESTParametersRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/RESTParametersRenderSection.swift index d2102b257f..ea7aefbadd 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/RESTParametersRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/RESTParametersRenderSection.swift @@ -29,9 +29,9 @@ public enum RESTParameterSource: String, Codable { public struct RESTParametersRenderSection: RenderSection, Equatable { public var kind: RenderSectionKind = .restParameters /// The title for the section. - public let title: String + public var title: String /// The list of REST parameters. - public let parameters: [RenderProperty] + public var parameters: [RenderProperty] /// The kind of listed parameters. public let source: RESTParameterSource diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/RESTResponseRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/RESTResponseRenderSection.swift index 332a9d81bf..826fef56d7 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/RESTResponseRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/RESTResponseRenderSection.swift @@ -14,9 +14,9 @@ import Foundation public struct RESTResponseRenderSection: RenderSection, Equatable { public var kind: RenderSectionKind = .restResponses /// The title for the section. - public let title: String + public var title: String /// The list of possible REST responses. - public let responses: [RESTResponse] + public var responses: [RESTResponse] enum CodingKeys: String, CodingKey { case kind @@ -70,7 +70,7 @@ public struct RESTResponse: Codable, TextIndexing, Equatable { /// A type declaration of the response's content. public let type: [DeclarationRenderSection.Token] /// Response details, if any. - public let content: [RenderBlockContent]? + public var content: [RenderBlockContent]? /// Creates a new REST response section. /// - Parameters: diff --git a/Sources/SwiftDocC/Model/Rendering/Symbol/TaskGroupRenderSection.swift b/Sources/SwiftDocC/Model/Rendering/Symbol/TaskGroupRenderSection.swift index 0c56f378b5..cb944da1a8 100644 --- a/Sources/SwiftDocC/Model/Rendering/Symbol/TaskGroupRenderSection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Symbol/TaskGroupRenderSection.swift @@ -13,9 +13,9 @@ public struct TaskGroupRenderSection: RenderSection, Equatable { public let kind: RenderSectionKind = .taskGroup /// An optional title for the section. - public let title: String? + public var title: String? /// An optional abstract summary for the section. - public let abstract: [RenderInlineContent]? + public var abstract: [RenderInlineContent]? /// An optional discussion for the section. public var discussion: (any RenderSection)? { get { diff --git a/Sources/SwiftDocC/Model/Rendering/Tutorial/LineHighlighter.swift b/Sources/SwiftDocC/Model/Rendering/Tutorial/LineHighlighter.swift index d88fa5bab6..a4aca9faf7 100644 --- a/Sources/SwiftDocC/Model/Rendering/Tutorial/LineHighlighter.swift +++ b/Sources/SwiftDocC/Model/Rendering/Tutorial/LineHighlighter.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2023 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -30,23 +30,6 @@ import Foundation ``` */ public struct LineHighlighter { - /** - A local utility type to hold incremental results. - */ - private struct IncrementalResult { - /// The previous ``Code`` element to compare. - /// If this is the first ``Step``'s ``Code``, this will be `nil`. - let previousCode: Code? - - /// The highlight results accumulated so far. - let results: [Result] - - init(previousCode: Code? = nil, results: [Result] = []) { - self.previousCode = previousCode - self.results = results - } - } - /** The final resulting highlights for a given file. */ @@ -100,7 +83,7 @@ public struct LineHighlighter { } /// The lines in the `resource` file. - private func lines(of resource: ResourceReference) -> [String]? { + private func lines(of resource: borrowing ResourceReference) -> [String]? { let fileContent: String? // Check if the file is a local asset that can be read directly from the context if let fileData = try? context.resource(with: resource) { @@ -118,7 +101,7 @@ public struct LineHighlighter { } /// Returns the line highlights between two files. - private func lineHighlights(old: ResourceReference, new: ResourceReference) -> Result { + private func lineHighlights(old: borrowing ResourceReference, new: ResourceReference) -> Result { // Retrieve the contents of the current file and the file we're comparing against. guard let oldLines = lines(of: old), let newLines = lines(of: new) else { return Result(file: new, highlights: []) @@ -138,7 +121,7 @@ public struct LineHighlighter { } /// Returns the line highlights between two ``Code`` elements. - private func lineHighlights(old: Code?, new: Code) -> Result { + private func lineHighlights(old: consuming Code?, new: borrowing Code) -> Result { if let previousFileOverride = new.previousFileReference { guard !new.shouldResetDiff else { return Result(file: new.fileReference, highlights: []) @@ -157,11 +140,17 @@ public struct LineHighlighter { /// The highlights to apply for the given ``TutorialSection``. var highlights: [Result] { - return tutorialSection.stepsContent?.steps - .compactMap { $0.code } - .reduce(IncrementalResult(), { (incrementalResult, newCode) -> IncrementalResult in - let result = lineHighlights(old: incrementalResult.previousCode, new: newCode) - return IncrementalResult(previousCode: newCode, results: incrementalResult.results + [result]) - }).results ?? [] + guard let steps = tutorialSection.stepsContent?.steps else { return [] } + + var previousCode: Code? = nil + var results: [Result] = [] + + for step in steps { + guard let newCode = step.code else { continue } + results.append(lineHighlights(old: previousCode, new: newCode)) + previousCode = newCode + } + + return results } } diff --git a/Sources/SwiftDocC/Model/Section/Sections/DefaultImplementations.swift b/Sources/SwiftDocC/Model/Section/Sections/DefaultImplementations.swift index 3898796a6c..bcc78a18fa 100644 --- a/Sources/SwiftDocC/Model/Section/Sections/DefaultImplementations.swift +++ b/Sources/SwiftDocC/Model/Section/Sections/DefaultImplementations.swift @@ -65,7 +65,7 @@ public struct DefaultImplementationsSection { return ImplementationsGroup( heading: "\(groupName)Implementations", - references: grouped[name]!.map { $0.reference } + references: grouped[name]!.map { $0.reference }.sorted(by: \.description) ) } } diff --git a/Sources/SwiftDocC/Model/SourceLanguage.swift b/Sources/SwiftDocC/Model/SourceLanguage.swift deleted file mode 100644 index 2866213456..0000000000 --- a/Sources/SwiftDocC/Model/SourceLanguage.swift +++ /dev/null @@ -1,174 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2023 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 -*/ - -/// A programming language. -public struct SourceLanguage: Hashable, Codable, Comparable { - /// The display name of the programming language. - public var name: String - /// A globally unique identifier for the language. - public var id: String - /// Aliases for the language's identifier. - public var idAliases: [String] = [] - /// The identifier to use for link disambiguation purposes. - public var linkDisambiguationID: String - - /// Creates a new language with a given name and identifier. - /// - Parameters: - /// - name: The display name of the programming language. - /// - id: A globally unique identifier for the language. - /// - idAliases: Aliases for the language's identifier. - /// - linkDisambiguationID: The identifier to use for link disambiguation purposes. - public init(name: String, id: String, idAliases: [String] = [], linkDisambiguationID: String? = nil) { - self.name = name - self.id = id - self.idAliases = idAliases - self.linkDisambiguationID = linkDisambiguationID ?? id - } - - /// Finds the programming language that matches a given identifier, or creates a new one if it finds no existing language. - /// - Parameter id: The identifier of the programming language. - public init(id: String) { - switch id { - case "swift": self = .swift - case "occ", "objc", "objective-c", "c": self = .objectiveC - // FIXME: DocC should display C++ and Objective-C++ as their own languages (https://github.com/swiftlang/swift-docc/issues/767) - case "occ++", "objc++", "objective-c++", "c++": self = .objectiveC - case "javascript": self = .javaScript - case "data": self = .data - case "metal": self = .metal - default: - self.name = id - self.id = id - self.linkDisambiguationID = id - } - } - - /// Finds the programming language that matches a given display name, or creates a new one if it finds no existing language. - /// - /// - Parameter name: The display name of the programming language. - public init(name: String) { - if let knownLanguage = SourceLanguage.firstKnownLanguage(withName: name) { - self = knownLanguage - } else { - self.name = name - - let id = name.lowercased() - self.id = id - self.linkDisambiguationID = id - } - } - - /// Finds the programming language that matches a given display name. - /// - /// If the language name doesn't match any known language, this initializer returns `nil`. - /// - /// - Parameter knownLanguageName: The display name of the programming language. - public init?(knownLanguageName: String) { - if let knownLanguage = SourceLanguage.firstKnownLanguage(withName: knownLanguageName) { - self = knownLanguage - } else { - return nil - } - } - - /// Finds the programming language that matches a given identifier. - /// - /// If the language identifier doesn't match any known language, this initializer returns `nil`. - /// - /// - Parameter knownLanguageIdentifier: The identifier name of the programming language. - public init?(knownLanguageIdentifier: String) { - if let knownLanguage = SourceLanguage.firstKnownLanguage(withIdentifier: knownLanguageIdentifier) { - self = knownLanguage - } else { - return nil - } - } - - private static func firstKnownLanguage(withName name: String) -> SourceLanguage? { - SourceLanguage.knownLanguages.first { $0.name.lowercased() == name.lowercased() } - } - - private static func firstKnownLanguage(withIdentifier id: String) -> SourceLanguage? { - SourceLanguage.knownLanguages.first { knownLanguage in - ([knownLanguage.id] + knownLanguage.idAliases) - .map { $0.lowercased() } - .contains(id) - } - } - - /// The Swift programming language. - public static let swift = SourceLanguage(name: "Swift", id: "swift") - - /// The Objective-C programming language. - public static let objectiveC = SourceLanguage( - name: "Objective-C", - id: "occ", - idAliases: [ - "objective-c", - "objc", - "c", // FIXME: DocC should display C as its own language (github.com/swiftlang/swift-docc/issues/169). - "c++", // FIXME: DocC should display C++ and Objective-C++ as their own languages (https://github.com/swiftlang/swift-docc/issues/767) - "objective-c++", - "objc++", - "occ++", - ], - linkDisambiguationID: "c" - ) - - /// The JavaScript programming language or another language that conforms to the ECMAScript specification. - public static let javaScript = SourceLanguage(name: "JavaScript", id: "javascript") - /// Miscellaneous data, that's not a programming language. - /// - /// For example, use this to represent JSON or XML content. - public static let data = SourceLanguage(name: "Data", id: "data") - /// The Metal programming language. - public static let metal = SourceLanguage(name: "Metal", id: "metal") - - /// The list of programming languages that are known to DocC. - public static var knownLanguages: [SourceLanguage] = [.swift, .objectiveC, .javaScript, .data, .metal] - - enum CodingKeys: CodingKey { - case name - case id - case idAliases - case linkDisambiguationID - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: SourceLanguage.CodingKeys.self) - - let name = try container.decode(String.self, forKey: SourceLanguage.CodingKeys.name) - let id = try container.decode(String.self, forKey: SourceLanguage.CodingKeys.id) - let idAliases = try container.decodeIfPresent([String].self, forKey: SourceLanguage.CodingKeys.idAliases) ?? [] - let linkDisambiguationID = try container.decodeIfPresent(String.self, forKey: SourceLanguage.CodingKeys.linkDisambiguationID) - - self.init(name: name, id: id, idAliases: idAliases, linkDisambiguationID: linkDisambiguationID) - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: SourceLanguage.CodingKeys.self) - - try container.encode(self.name, forKey: SourceLanguage.CodingKeys.name) - try container.encode(self.id, forKey: SourceLanguage.CodingKeys.id) - try container.encodeIfNotEmpty(self.idAliases, forKey: SourceLanguage.CodingKeys.idAliases) - try container.encode(self.linkDisambiguationID, forKey: SourceLanguage.CodingKeys.linkDisambiguationID) - } - - public static func < (lhs: SourceLanguage, rhs: SourceLanguage) -> Bool { - // Sort Swift before other languages. - if lhs == .swift { - return true - } else if rhs == .swift { - return false - } - // Otherwise, sort by ID for a stable order. - return lhs.id < rhs.id - } -} diff --git a/Sources/SwiftDocC/Semantics/Article/Article.swift b/Sources/SwiftDocC/Semantics/Article/Article.swift index aec17835f9..42195c74c4 100644 --- a/Sources/SwiftDocC/Semantics/Article/Article.swift +++ b/Sources/SwiftDocC/Semantics/Article/Article.swift @@ -66,6 +66,18 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected, return abstractSection?.paragraph } + /// The list of supported languages for the article, if present. + /// + /// This information is available via `@SupportedLanguage` in the `@Metadata` directive. + public var supportedLanguages: Set? { + guard let metadata = self.metadata else { + return nil + } + + let langs = metadata.supportedLanguages.map(\.language) + return langs.isEmpty ? nil : Set(langs) + } + /// An optional custom deprecation summary for a deprecated symbol. private(set) public var deprecationSummary: MarkupContainer? diff --git a/Sources/SwiftDocC/Semantics/Article/MarkupConvertible.swift b/Sources/SwiftDocC/Semantics/Article/MarkupConvertible.swift index 5c3bdfbb76..2eace344cc 100644 --- a/Sources/SwiftDocC/Semantics/Article/MarkupConvertible.swift +++ b/Sources/SwiftDocC/Semantics/Article/MarkupConvertible.swift @@ -21,19 +21,4 @@ public protocol MarkupConvertible { /// - bundle: The documentation bundle that the source file belongs to. /// - problems: A mutable collection of problems to update with any problem encountered while initializing the element. init?(from markup: any Markup, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) - - @available(*, deprecated, renamed: "init(from:source:for:problems:)", message: "Use 'init(from:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - init?(from markup: any Markup, source: URL?, for bundle: DocumentationBundle, in context: DocumentationContext, problems: inout [Problem]) -} - -public extension MarkupConvertible { - // Default implementation to avoid source breaking changes. Remove this after 6.2 is released. - init?(from markup: any Markup, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) { - fatalError("Markup convertible type doesn't implement either 'init(from:source:for:problems:)' or 'init(from:source:for:in:problems:)'") - } - - // Default implementation to new types don't need to implement a deprecated initializer. Remove this after 6.2 is released. - init?(from markup: any Markup, source: URL?, for bundle: DocumentationBundle, in context: DocumentationContext, problems: inout [Problem]) { - self.init(from: markup, source: source, for: bundle, problems: &problems) - } } diff --git a/Sources/SwiftDocC/Semantics/DirectiveConvertable.swift b/Sources/SwiftDocC/Semantics/DirectiveConvertable.swift index b5407c0a93..fb2d4d6f9d 100644 --- a/Sources/SwiftDocC/Semantics/DirectiveConvertable.swift +++ b/Sources/SwiftDocC/Semantics/DirectiveConvertable.swift @@ -38,9 +38,6 @@ public protocol DirectiveConvertible { /// - problems: An inout array of ``Problem`` to be collected for later diagnostic reporting. init?(from directive: BlockDirective, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) - @available(*, deprecated, renamed: "init(from:source:for:problems:)", message: "Use 'init(from:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - init?(from directive: BlockDirective, source: URL?, for bundle: DocumentationBundle, in context: DocumentationContext, problems: inout [Problem]) - /// Returns a Boolean value indicating whether the `DirectiveConvertible` recognizes the given directive. /// /// - Parameter directive: The directive to check for conversion compatibility. @@ -54,15 +51,5 @@ public extension DirectiveConvertible { static func canConvertDirective(_ directive: BlockDirective) -> Bool { directiveName == directive.name } - - // Default implementation to avoid source breaking changes. Remove this after 6.2 is released. - init?(from directive: BlockDirective, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) { - fatalError("Directive named \(directive.name) doesn't implement either 'init(from:source:for:problems:)' or 'init(from:source:for:in:problems:)'") - } - - // Default implementation to new types don't need to implement a deprecated initializer. Remove this after 6.2 is released. - init?(from directive: BlockDirective, source: URL?, for bundle: DocumentationBundle, in context: DocumentationContext, problems: inout [Problem]) { - self.init(from: directive, source: source, for: bundle, problems: &problems) - } } diff --git a/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/AutomaticDirectiveConvertible.swift b/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/AutomaticDirectiveConvertible.swift index d6e172c990..bfd1ab53de 100644 --- a/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/AutomaticDirectiveConvertible.swift +++ b/Sources/SwiftDocC/Semantics/DirectiveInfrastructure/AutomaticDirectiveConvertible.swift @@ -111,11 +111,6 @@ extension AutomaticDirectiveConvertible { ) } - @available(*, deprecated, renamed: "init(from:source:for:)", message: "Use 'init(from:source:for:)' instead. This deprecated API will be removed after 6.2 is released") - public init?(from directive: BlockDirective, source: URL? = nil, for bundle: DocumentationBundle, in _: DocumentationContext) { - self.init(from: directive, source: source, for: bundle) - } - public init?( from directive: BlockDirective, source: URL?, diff --git a/Sources/SwiftDocC/Semantics/ExternalLinks/ExternalReferenceWalker.swift b/Sources/SwiftDocC/Semantics/ExternalLinks/ExternalReferenceWalker.swift index 5e15a2d062..b1f2021f4a 100644 --- a/Sources/SwiftDocC/Semantics/ExternalLinks/ExternalReferenceWalker.swift +++ b/Sources/SwiftDocC/Semantics/ExternalLinks/ExternalReferenceWalker.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -110,11 +110,6 @@ struct ExternalReferenceWalker: SemanticVisitor { visitMarkupContainer(MarkupContainer(markup)) } - @available(*, deprecated) // This is a deprecated protocol requirement. Remove after 6.2 is released - mutating func visitTechnology(_ technology: TutorialTableOfContents) { - visitTutorialTableOfContents(technology) - } - mutating func visitTutorialTableOfContents(_ tutorialTableOfContents: TutorialTableOfContents) -> Void { visit(tutorialTableOfContents.intro) tutorialTableOfContents.volumes.forEach { visit($0) } diff --git a/Sources/SwiftDocC/Semantics/General Purpose Analyses/Extract.swift b/Sources/SwiftDocC/Semantics/General Purpose Analyses/Extract.swift index 9d063731ad..38b3611ea0 100644 --- a/Sources/SwiftDocC/Semantics/General Purpose Analyses/Extract.swift +++ b/Sources/SwiftDocC/Semantics/General Purpose Analyses/Extract.swift @@ -18,11 +18,6 @@ extension Semantic.Analyses { public struct ExtractAll { public init() {} - @available(*, deprecated, renamed: "analyze(_:children:source:for:problems:)", message: "Use 'analyze(_:children:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> ([Child], remainder: MarkupContainer) { - analyze(directive, children: children, source: source, for: bundle, problems: &problems) - } - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) -> ([Child], remainder: MarkupContainer) { return Semantic.Analyses.extractAll( childType: Child.self, @@ -66,11 +61,6 @@ extension Semantic.Analyses { } return (matches, remainder: MarkupContainer(remainder)) } - - @available(*, deprecated, renamed: "analyze(_:children:source:problems:)", message: "Use 'analyze(_:children:source:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for _: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> ([Child], remainder: MarkupContainer) { - analyze(directive, children: children, source: source, problems: &problems) - } } } diff --git a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasAtLeastOne.swift b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasAtLeastOne.swift index ba20a20496..7c8172f0d1 100644 --- a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasAtLeastOne.swift +++ b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasAtLeastOne.swift @@ -21,11 +21,6 @@ extension Semantic.Analyses { self.severityIfNotFound = severityIfNotFound } - @available(*, deprecated, renamed: "analyze(_:children:source:for:problems:)", message: "Use 'analyze(_:children:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> ([Child], remainder: MarkupContainer) { - analyze(directive, children: children, source: source, for: bundle, problems: &problems) - } - public func analyze( _ directive: BlockDirective, children: some Sequence, diff --git a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasAtMostOne.swift b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasAtMostOne.swift index 9c987c89dd..93765fd5d3 100644 --- a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasAtMostOne.swift +++ b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasAtMostOne.swift @@ -16,12 +16,6 @@ extension Semantic.Analyses { Checks to see if a parent directive has at most one child directive of a specified type. If so, return that child and the remainder. */ public struct HasAtMostOne { - - @available(*, deprecated, renamed: "analyze(_:children:source:for:problems:)", message: "Use 'analyze(_:children:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> (Child?, remainder: MarkupContainer) { - analyze(directive, children: children, source: source, for: bundle, problems: &problems) - } - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) -> (Child?, remainder: MarkupContainer) { return Semantic.Analyses.extractAtMostOne( childType: Child.self, diff --git a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasContent.swift b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasContent.swift index 8c0879ec21..4badc40152 100644 --- a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasContent.swift +++ b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasContent.swift @@ -36,10 +36,5 @@ extension Semantic.Analyses { problems.append(Problem(diagnostic: diagnostic, possibleSolutions: [])) return MarkupContainer() } - - @available(*, deprecated, renamed: "analyze(_:children:source:problems:)", message: "Use 'analyze(_:children:source:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for _: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> MarkupContainer { - analyze(directive, children: children, source: source, problems: &problems) - } } } diff --git a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasExactlyOne.swift b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasExactlyOne.swift index 1385323c14..aa05a6dd63 100644 --- a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasExactlyOne.swift +++ b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasExactlyOne.swift @@ -21,11 +21,6 @@ extension Semantic.Analyses { self.severityIfNotFound = severityIfNotFound } - @available(*, deprecated, renamed: "analyze(_:children:source:for:problems:)", message: "Use 'analyze(_:children:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> (Child?, remainder: MarkupContainer) { - analyze(directive, children: children, source: source, for: bundle, problems: &problems) - } - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) -> (Child?, remainder: MarkupContainer) { return Semantic.Analyses.extractExactlyOne( childType: Child.self, @@ -104,11 +99,6 @@ extension Semantic.Analyses { self.severityIfNotFound = severityIfNotFound } - @available(*, deprecated, renamed: "analyze(_:children:source:for:problems:)", message: "Use 'analyze(_:children:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> (Child1?, Child2?, remainder: MarkupContainer) { - analyze(directive, children: children, source: source, for: bundle, problems: &problems) - } - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) -> (Child1?, Child2?, remainder: MarkupContainer) { let (candidates, remainder) = children.categorize { child -> BlockDirective? in guard let childDirective = child as? BlockDirective else { @@ -159,11 +149,6 @@ extension Semantic.Analyses { self.severityIfNotFound = severityIfNotFound } - @available(*, deprecated, renamed: "analyze(_:children:source:for:problems:)", message: "Use 'analyze(_:children:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> ((any Media)?, remainder: MarkupContainer) { - analyze(directive, children: children, source: source, for: bundle, problems: &problems) - } - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) -> ((any Media)?, remainder: MarkupContainer) { let (foundImage, foundVideo, remainder) = HasExactlyOneOf(severityIfNotFound: severityIfNotFound).analyze(directive, children: children, source: source, for: bundle, problems: &problems) return (foundImage ?? foundVideo, remainder) @@ -177,11 +162,6 @@ extension Semantic.Analyses { self.severityIfNotFound = severityIfNotFound } - @available(*, deprecated, renamed: "analyze(_:children:source:for:problems:)", message: "Use 'analyze(_:children:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> ((any Media)?, remainder: MarkupContainer) { - analyze(directive, children: children, source: source, for: bundle, problems: &problems) - } - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, problems: inout [Problem]) -> ((any Media)?, remainder: MarkupContainer) { let (mediaDirectives, remainder) = children.categorize { child -> BlockDirective? in guard let childDirective = child as? BlockDirective else { @@ -279,11 +259,6 @@ extension Semantic.Analyses { return validElements } - @available(*, deprecated, renamed: "analyze(_:children:source:problems:)", message: "Use 'analyze(_:children:source:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for _: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> [ListElement]? { - analyze(directive, children: children, source: source, problems: &problems) - } - func firstChildElement(in markup: any Markup) -> ListElement? { return markup // ListItem .child(at: 0)? // Paragraph diff --git a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlyKnownArguments.swift b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlyKnownArguments.swift index 5ca45fae53..25da33e704 100644 --- a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlyKnownArguments.swift +++ b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlyKnownArguments.swift @@ -42,11 +42,6 @@ extension Semantic.Analyses { } return arguments } - - @available(*, deprecated, renamed: "analyze(_:children:source:problems:)", message: "Use 'analyze(_:children:source:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for _: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> [String: Markdown.DirectiveArgument] { - analyze(directive, children: children, source: source, problems: &problems) - } } } diff --git a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlyKnownDirectives.swift b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlyKnownDirectives.swift index d346015f53..ec54d1a2f3 100644 --- a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlyKnownDirectives.swift +++ b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlyKnownDirectives.swift @@ -75,11 +75,6 @@ extension Semantic.Analyses { } } } - - @available(*, deprecated, renamed: "analyze(_:children:source:problems:)", message: "Use 'analyze(_:children:source:problems:)' instead. This deprecated API will be removed after 6.2 is released") - public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for _: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) { - analyze(directive, children: children, source: source, problems: &problems) - } } } diff --git a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlySequentialHeadings.swift b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlySequentialHeadings.swift index addf05361a..b7515b4efa 100644 --- a/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlySequentialHeadings.swift +++ b/Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlySequentialHeadings.swift @@ -35,11 +35,6 @@ extension Semantic.Analyses { self.startingFromLevel = startingFromLevel } - @available(*, deprecated, renamed: "analyze(_:children:source:for:problems:)", message: "Use 'analyze(_:children:source:for:problems:)' instead. This deprecated API will be removed after 6.2 is released") - @discardableResult public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, in _: DocumentationContext, problems: inout [Problem]) -> [Heading] { - analyze(directive, children: children, source: source, for: bundle, problems: &problems) - } - /// Returns all valid headings. @discardableResult public func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for _: DocumentationBundle, problems: inout [Problem]) -> [Heading] { var currentHeadingLevel = startingFromLevel diff --git a/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift b/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift index 8f34a30d46..f70557855c 100644 --- a/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift +++ b/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift @@ -21,11 +21,6 @@ private func disabledLinkDestinationProblem(reference: ResolvedTopicReference, r return Problem(diagnostic: Diagnostic(source: range?.source, severity: severity, range: range, identifier: "org.swift.docc.disabledLinkDestination", summary: "The topic \(reference.path.singleQuoted) cannot be linked to."), possibleSolutions: []) } -private func unknownSnippetSliceProblem(snippetPath: String, slice: String, range: SourceRange?) -> Problem { - let diagnostic = Diagnostic(source: range?.source, severity: .warning, range: range, identifier: "org.swift.docc.unknownSnippetSlice", summary: "Snippet slice \(slice.singleQuoted) does not exist in snippet \(snippetPath.singleQuoted); this directive will be ignored") - return Problem(diagnostic: diagnostic, possibleSolutions: []) -} - private func removedLinkDestinationProblem(reference: ResolvedTopicReference, range: SourceRange?, severity: DiagnosticSeverity) -> Problem { var solutions = [Solution]() if let range, reference.pathComponents.count > 3 { @@ -44,13 +39,11 @@ private func removedLinkDestinationProblem(reference: ResolvedTopicReference, ra */ struct MarkupReferenceResolver: MarkupRewriter { var context: DocumentationContext - var bundle: DocumentationBundle var problems = [Problem]() var rootReference: ResolvedTopicReference - init(context: DocumentationContext, bundle: DocumentationBundle, rootReference: ResolvedTopicReference) { + init(context: DocumentationContext, rootReference: ResolvedTopicReference) { self.context = context - self.bundle = bundle self.rootReference = rootReference } @@ -84,14 +77,14 @@ struct MarkupReferenceResolver: MarkupRewriter { return nil } - let uncuratedArticleMatch = context.uncuratedArticles[bundle.articlesDocumentationRootReference.appendingPathOfReference(unresolved)]?.source + let uncuratedArticleMatch = context.uncuratedArticles[context.inputs.articlesDocumentationRootReference.appendingPathOfReference(unresolved)]?.source problems.append(unresolvedReferenceProblem(source: range?.source, range: range, severity: severity, uncuratedArticleMatch: uncuratedArticleMatch, errorInfo: error, fromSymbolLink: fromSymbolLink)) return nil } } mutating func visitImage(_ image: Image) -> (any Markup)? { - if let reference = image.reference(in: bundle), !context.resourceExists(with: reference) { + if let reference = image.reference(in: context.inputs), !context.resourceExists(with: reference) { problems.append(unresolvedResourceProblem(resource: reference, source: image.range?.source, range: image.range, severity: .warning)) } @@ -171,28 +164,25 @@ struct MarkupReferenceResolver: MarkupRewriter { let source = blockDirective.range?.source switch blockDirective.name { case Snippet.directiveName: - var problems = [Problem]() - guard let snippet = Snippet(from: blockDirective, source: source, for: bundle, problems: &problems) else { + var ignoredParsingProblems = [Problem]() // Any argument parsing problems have already been reported elsewhere + guard let snippet = Snippet(from: blockDirective, source: source, for: context.inputs, problems: &ignoredParsingProblems) else { return blockDirective } - if let resolved = resolveAbsoluteSymbolLink(unresolvedDestination: snippet.path, elementRange: blockDirective.range) { - var argumentText = "path: \"\(resolved.absoluteString)\"" + switch context.snippetResolver.resolveSnippet(path: snippet.path) { + case .success(let resolvedSnippet): if let requestedSlice = snippet.slice, - let snippetMixin = try? context.entity(with: resolved).symbol? - .mixins[SymbolGraph.Symbol.Snippet.mixinKey] as? SymbolGraph.Symbol.Snippet { - guard snippetMixin.slices[requestedSlice] != nil else { - problems.append(unknownSnippetSliceProblem(snippetPath: snippet.path, slice: requestedSlice, range: blockDirective.nameRange)) - return blockDirective - } - argumentText.append(", slice: \"\(requestedSlice)\"") + let errorInfo = context.snippetResolver.validate(slice: requestedSlice, for: resolvedSnippet) + { + problems.append(SnippetResolver.unknownSnippetSliceProblem(source: source, range: blockDirective.arguments()["slice"]?.valueRange, errorInfo: errorInfo)) } - return BlockDirective(name: Snippet.directiveName, argumentText: argumentText, children: []) - } else { + return blockDirective + case .failure(let errorInfo): + problems.append(SnippetResolver.unresolvedSnippetPathProblem(source: source, range: blockDirective.arguments()["path"]?.valueRange, errorInfo: errorInfo)) return blockDirective } case ImageMedia.directiveName: - guard let imageMedia = ImageMedia(from: blockDirective, source: source, for: bundle) else { + guard let imageMedia = ImageMedia(from: blockDirective, source: source, for: context.inputs) else { return blockDirective } @@ -210,7 +200,7 @@ struct MarkupReferenceResolver: MarkupRewriter { return blockDirective case VideoMedia.directiveName: - guard let videoMedia = VideoMedia(from: blockDirective, source: source, for: bundle) else { + guard let videoMedia = VideoMedia(from: blockDirective, source: source, for: context.inputs) else { return blockDirective } @@ -248,4 +238,3 @@ struct MarkupReferenceResolver: MarkupRewriter { } } } - diff --git a/Sources/SwiftDocC/Semantics/Metadata/AlternateRepresentation.swift b/Sources/SwiftDocC/Semantics/Metadata/AlternateRepresentation.swift index de96860517..520f5d7245 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/AlternateRepresentation.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/AlternateRepresentation.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -18,7 +18,7 @@ public import Markdown /// /// Whenever possible, prefer to define alternative language representations for a symbol by using in-source annotations /// such as the `@objc` and `@_objcImplementation` attributes in Swift, -/// or the `NS_SWIFT_NAME` macro in Objective C. +/// or the `NS_SWIFT_NAME` macro in Objective-C. /// /// If your source language doesn’t have a mechanism for specifying alternate representations or if your intended alternate representation isn't compatible with those attributes, /// you can use the `@AlternateRepresentation` directive to specify another symbol that should be considered an alternate representation of the documented symbol. diff --git a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift index 4f767e38a0..ca3ae0c296 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift @@ -105,7 +105,7 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { func validate(source: URL?, problems: inout [Problem]) -> Bool { // Check that something is configured in the metadata block - if documentationOptions == nil && technologyRoot == nil && displayName == nil && pageImages.isEmpty && customMetadata.isEmpty && callToAction == nil && availability.isEmpty && pageKind == nil && pageColor == nil && titleHeading == nil && redirects == nil && alternateRepresentations.isEmpty { + if documentationOptions == nil && technologyRoot == nil && displayName == nil && pageImages.isEmpty && customMetadata.isEmpty && callToAction == nil && availability.isEmpty && pageKind == nil && pageColor == nil && supportedLanguages.isEmpty && titleHeading == nil && redirects == nil && alternateRepresentations.isEmpty { let diagnostic = Diagnostic( source: source, severity: .information, @@ -200,49 +200,64 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { /// Validates the use of this Metadata directive in a documentation comment. /// - /// Some configuration options of Metadata are invalid in documentation comments. This function - /// emits warnings for illegal uses and sets their values to `nil`. - func validateForUseInDocumentationComment( - symbolSource: URL?, - problems: inout [Problem] - ) { - let invalidDirectives: [(any AutomaticDirectiveConvertible)?] = [ - documentationOptions, - technologyRoot, - displayName, - callToAction, - pageKind, - _pageColor, - titleHeading, - ] + (redirects ?? []) - + supportedLanguages - + pageImages - - let namesAndRanges = invalidDirectives - .compactMap { $0 } - .map { (type(of: $0).directiveName, $0.originalMarkup.range) } + /// Some configuration options of Metadata are not supported in documentation comments. + /// This function emits warnings for unsupported uses and resets their values (to `nil` or `[]`) . + func validateForUseInDocumentationComment(symbolSource: URL?, problems: inout [Problem]) { + func validateUnsupportedMetadataDirective(for directives: [Directive]?) { + for directive in directives ?? [] { + validateUnsupportedMetadataDirective(for: directive) + } + } - problems.append( - contentsOf: namesAndRanges.map { (name, range) in - Problem( - diagnostic: Diagnostic( - source: symbolSource, - severity: .warning, - range: range, - identifier: "org.swift.docc.\(Metadata.directiveName).Invalid\(name)InDocumentationComment", - summary: "Invalid use of \(name.singleQuoted) directive in documentation comment; configuration will be ignored", - explanation: "Specify this configuration in a documentation extension file" - - // TODO: It would be nice to offer a solution here that removes the directive for you (#1111, rdar://140846407) - ) - ) + func validateUnsupportedMetadataDirective(for directive: Directive?) { + guard let directive else { + return } - ) + + let name = Directive.directiveName + let range = directive.originalMarkup.range + + let diagnostic = Diagnostic( + source: symbolSource, + severity: .warning, + range: range, + identifier: "org.swift.docc.\(Metadata.directiveName).Invalid\(name)InDocumentationComment", + summary: "Invalid use of \(name.singleQuoted) directive in documentation comment; configuration will be ignored", + explanation: "Specify this configuration in a documentation extension file" + ) + + let solutions: [Solution] = range.map { range in + [Solution( + summary: "Remove invalid \(name.singleQuoted) directive", + replacements: [ + Replacement(range: range, replacement: "") + ] + )] + } ?? [] + + problems.append(Problem(diagnostic: diagnostic, possibleSolutions: solutions)) + } + + validateUnsupportedMetadataDirective(for: documentationOptions) + validateUnsupportedMetadataDirective(for: technologyRoot) + validateUnsupportedMetadataDirective(for: displayName) + validateUnsupportedMetadataDirective(for: callToAction) + validateUnsupportedMetadataDirective(for: pageKind) + validateUnsupportedMetadataDirective(for: _pageColor) + validateUnsupportedMetadataDirective(for: titleHeading) + validateUnsupportedMetadataDirective(for: redirects) + validateUnsupportedMetadataDirective(for: supportedLanguages) + validateUnsupportedMetadataDirective(for: pageImages) documentationOptions = nil - technologyRoot = nil - displayName = nil - pageKind = nil - _pageColor = nil + technologyRoot = nil + displayName = nil + callToAction = nil + pageKind = nil + _pageColor = nil + titleHeading = nil + redirects = nil + supportedLanguages = [] + pageImages = [] } } diff --git a/Sources/SwiftDocC/Semantics/Metadata/PageImage.swift b/Sources/SwiftDocC/Semantics/Metadata/PageImage.swift index aae9c3d916..88451fdec3 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/PageImage.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/PageImage.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022-2024 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -14,9 +14,19 @@ public import Markdown /// Associates an image with a page. /// /// You can use this directive to set the image used when rendering a user-interface element representing the page. -/// For example, use the page image directive to customize the icon used to represent this page in the navigation sidebar, -/// or the card image used to represent this page when using the ``Links`` directive and the ``Links/VisualStyle/detailedGrid`` -/// visual style. +/// +/// Use the "icon" purpose to customize the icon that DocC uses to represent this page in the navigation sidebar. +/// For article pages, DocC also uses this icon to represent the article in topics sections and in ``Links`` directives that use the `list` visual style. +/// +/// > Tip: Page images with the "icon" purpose work best when they're square and when they have good visual clarity at small sizes (less than 20×20 points). +/// +/// Use the "card" purpose to customize the image that DocC uses to represent this page inside ``Links`` directives that use the either the `detailedGrid` or the `compactGrid` visual style. +/// For article pages, DocC also incorporates a partially faded out version of the card image in the background of the page itself. +/// +/// > Tip: Page images with the "card" purpose work best when they have a 16:9 aspect ratio. Currently, the largest size that DocC displays a card image is 640×360 points. +/// +/// If you specify an "icon" page image without specifying a "card" page image, DocC will use the icon as a fallback in places where the card image is preferred. +/// To avoid upscaled pixelated icons in these places, either configure a "card" page image as well or use a scalable vector image asset for the "icon" page image. public final class PageImage: Semantic, AutomaticDirectiveConvertible { public static let introducedVersion = "5.8" public let originalMarkup: BlockDirective diff --git a/Sources/SwiftDocC/Semantics/ReferenceResolver.swift b/Sources/SwiftDocC/Semantics/ReferenceResolver.swift index 9703463ad0..399abe2033 100644 --- a/Sources/SwiftDocC/Semantics/ReferenceResolver.swift +++ b/Sources/SwiftDocC/Semantics/ReferenceResolver.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -95,9 +95,6 @@ struct ReferenceResolver: SemanticVisitor { /// The context to use to resolve references. var context: DocumentationContext - /// The bundle in which visited documents reside. - var bundle: DocumentationBundle - /// Problems found while trying to resolve references. var problems = [Problem]() @@ -106,10 +103,9 @@ struct ReferenceResolver: SemanticVisitor { /// If the documentation is inherited, the reference of the parent symbol. var inheritanceParentReference: ResolvedTopicReference? - init(context: DocumentationContext, bundle: DocumentationBundle, rootReference: ResolvedTopicReference? = nil, inheritanceParentReference: ResolvedTopicReference? = nil) { + init(context: DocumentationContext, rootReference: ResolvedTopicReference? = nil, inheritanceParentReference: ResolvedTopicReference? = nil) { self.context = context - self.bundle = bundle - self.rootReference = rootReference ?? bundle.rootReference + self.rootReference = rootReference ?? context.inputs.rootReference self.inheritanceParentReference = inheritanceParentReference } @@ -119,7 +115,7 @@ struct ReferenceResolver: SemanticVisitor { return .success(resolved) case let .failure(unresolved, error): - let uncuratedArticleMatch = context.uncuratedArticles[bundle.documentationRootReference.appendingPathOfReference(unresolved)]?.source + let uncuratedArticleMatch = context.uncuratedArticles[context.inputs.documentationRootReference.appendingPathOfReference(unresolved)]?.source problems.append(unresolvedReferenceProblem(source: range?.source, range: range, severity: severity, uncuratedArticleMatch: uncuratedArticleMatch, errorInfo: error, fromSymbolLink: false)) return .failure(unresolved, error) } @@ -172,9 +168,9 @@ struct ReferenceResolver: SemanticVisitor { // Change the context of the project file to `download` if let projectFiles = tutorial.projectFiles, - var resolvedDownload = context.resolveAsset(named: projectFiles.path, in: bundle.rootReference) { + var resolvedDownload = context.resolveAsset(named: projectFiles.path, in: rootReference) { resolvedDownload.context = .download - context.updateAsset(named: projectFiles.path, asset: resolvedDownload, in: bundle.rootReference) + context.updateAsset(named: projectFiles.path, asset: resolvedDownload, in: rootReference) } return Tutorial(originalMarkup: tutorial.originalMarkup, durationMinutes: tutorial.durationMinutes, projectFiles: tutorial.projectFiles, requirements: newRequirements, intro: newIntro, sections: newSections, assessments: newAssessments, callToActionImage: newCallToActionImage, redirects: tutorial.redirects) @@ -215,7 +211,7 @@ struct ReferenceResolver: SemanticVisitor { } mutating func visitMarkupContainer(_ markupContainer: MarkupContainer) -> Semantic { - var markupResolver = MarkupReferenceResolver(context: context, bundle: bundle, rootReference: rootReference) + var markupResolver = MarkupReferenceResolver(context: context, rootReference: rootReference) let parent = inheritanceParentReference let context = self.context @@ -252,11 +248,6 @@ struct ReferenceResolver: SemanticVisitor { return (visitMarkupContainer(MarkupContainer(markup)) as! MarkupContainer).elements.first! } - @available(*, deprecated) // This is a deprecated protocol requirement. Remove after 6.2 is released. - mutating func visitTechnology(_ technology: TutorialTableOfContents) -> Semantic { - visitTutorialTableOfContents(technology) - } - mutating func visitTutorialTableOfContents(_ tutorialTableOfContents: TutorialTableOfContents) -> Semantic { let newIntro = visit(tutorialTableOfContents.intro) as! Intro let newVolumes = tutorialTableOfContents.volumes.map { visit($0) } as! [Volume] @@ -320,7 +311,7 @@ struct ReferenceResolver: SemanticVisitor { // i.e. doc:/${SOME_TECHNOLOGY}/${PROJECT} or doc://${BUNDLE_ID}/${SOME_TECHNOLOGY}/${PROJECT} switch tutorialReference.topic { case .unresolved: - let maybeResolved = resolve(tutorialReference.topic, in: bundle.tutorialsContainerReference, + let maybeResolved = resolve(tutorialReference.topic, in: context.inputs.tutorialsContainerReference, range: tutorialReference.originalMarkup.range, severity: .warning) return TutorialReference(originalMarkup: tutorialReference.originalMarkup, tutorial: .resolved(maybeResolved)) @@ -374,10 +365,10 @@ struct ReferenceResolver: SemanticVisitor { visitMarkupContainer($0) as? MarkupContainer } // If there's a call to action with a local-file reference, change its context to `download` - if let downloadFile = article.metadata?.callToAction?.resolveFile(for: bundle, in: context, problems: &problems), - var resolvedDownload = context.resolveAsset(named: downloadFile.path, in: bundle.rootReference) { + if let downloadFile = article.metadata?.callToAction?.resolveFile(for: context.inputs, in: context, problems: &problems), + var resolvedDownload = context.resolveAsset(named: downloadFile.path, in: rootReference) { resolvedDownload.context = .download - context.updateAsset(named: downloadFile.path, asset: resolvedDownload, in: bundle.rootReference) + context.updateAsset(named: downloadFile.path, asset: resolvedDownload, in: rootReference) } return Article( diff --git a/Sources/SwiftDocC/Semantics/SemanticAnalysis.swift b/Sources/SwiftDocC/Semantics/SemanticAnalysis.swift deleted file mode 100644 index ac52cc8aae..0000000000 --- a/Sources/SwiftDocC/Semantics/SemanticAnalysis.swift +++ /dev/null @@ -1,76 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2025 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 -*/ - -public import Foundation -public import Markdown - -/** - A focused semantic analysis of a `BlockDirective`, recording problems and producing a result. - - A semantic analysis should check focus on the smallest meaningful aspect of the incoming `BlockDirective`. - This eases testing and helps prevent a tangle of dependencies and side effects. For every analysis, there - should be some number of tests for its robustness. - - For example, if an argument is required and is expected to be an integer, a semantic analysis - would check only that argument, attempt to convert it to an integer, and return it as the result. - - > Important: A ``SemanticAnalysis`` should not mutate outside state or directly depend on the results - of another analysis. This prevents runaway performance problems and strange bugs. - > It also makes it more amenable to parallelization should the need arise. - */ -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -public protocol SemanticAnalysis { - /** - The result of the analysis. - - > Note: This result may be `Void` as some analyses merely validate aspects of a `BlockDirective`. - */ - associatedtype Result - - /** - Perform the analysis on a directive, collect problems, and attempt to return a ``SemanticAnalysis/Result`` if required. - - - parameter directive: The `BlockDirective` that allegedly represents a ``Semantic`` object - - parameter children: The subset of `directive`'s children to analyze - - parameter source: A `URL` to the source file from which the `directive` came, if there was one. This is used for printing the location of a diagnostic. - - parameter bundle: The ``DocumentationBundle`` that owns the incoming `BlockDirective` - - parameter context: The ``DocumentationContext`` in which the bundle resides - - parameter problems: A container to append ``Problem``s encountered during the analysis - - returns: A result of the analysis if required, such as a validated parameter or subsection. - */ - func analyze(_ directive: BlockDirective, children: some Sequence, source: URL?, for bundle: DocumentationBundle, in context: DocumentationContext, problems: inout [Problem]) -> Result -} - -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.ExtractAll: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.ExtractAllMarkup: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasAtLeastOne: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasExactlyOne: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasExactlyOneOf: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasExactlyOneMedia: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasExactlyOneUnorderedList: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasExactlyOneImageOrVideoMedia: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasAtMostOne: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasContent: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasOnlyKnownArguments: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasOnlyKnownDirectives: SemanticAnalysis {} -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -extension Semantic.Analyses.HasOnlySequentialHeadings: SemanticAnalysis {} diff --git a/Sources/SwiftDocC/Semantics/Snippets/Snippet.swift b/Sources/SwiftDocC/Semantics/Snippets/Snippet.swift index a2188a3c82..a6f7f34dbe 100644 --- a/Sources/SwiftDocC/Semantics/Snippets/Snippet.swift +++ b/Sources/SwiftDocC/Semantics/Snippets/Snippet.swift @@ -12,16 +12,42 @@ import Foundation public import Markdown import SymbolKit +/// Embeds a code example from the project's code snippets. +/// +/// Use a `Snippet` directive to embed a code example from the project's "Snippets" directory on the page. +/// The `path` argument is the relative path from the package's top-level "Snippets" directory to your snippet file without the `.swift` extension. +/// +/// ```markdown +/// @Snippet(path: "example-snippet", slice: "setup") +/// ``` +/// +/// If you prefer, you can specify the relative path from the package's _root_ directory (by including a "Snippets/" prefix). +/// You can also include the package name---as defined in `Package.swift`---before the "Snippets/" prefix. +/// Neither of these leading path components are necessary because all your snippet code files are always located in your package's "Snippets" directory. +/// +/// > Earlier Versions: +/// > Before Swift-DocC 6.2.1, the `@Snippet` path needed to include both the package name component and the "Snippets" component: +/// > +/// > ```markdown +/// > @Snippet(path: "my-package/Snippets/example-snippet") +/// > ``` +/// +/// You can define named slices of your snippet by annotating the snippet file with `// snippet.` and `// snippet.end` lines. +/// A named slice automatically ends at the start of the next named slice, without an explicit `snippet.end` annotation. +/// +/// If the referenced snippet includes annotated slices, you can limit the embedded code example to a certain line range by specifying a `slice` name. +/// By default, the embedded code example includes the full snippet. For more information, see . public final class Snippet: Semantic, AutomaticDirectiveConvertible { - public static let introducedVersion = "5.6" + public static let introducedVersion = "5.7" public let originalMarkup: BlockDirective - /// The path components of a symbol link that would be used to resolve a reference to a snippet, - /// only occurring as a block directive argument. + /// The relative path from your package's top-level "Snippets" directory to the snippet file that you want to embed in the page, without the `.swift` file extension. @DirectiveArgumentWrapped public var path: String - /// An optional named range to limit the lines shown. + /// The name of a snippet slice to limit the embedded code example to a certain line range. + /// + /// By default, the embedded code example includes the full snippet. @DirectiveArgumentWrapped public var slice: String? = nil @@ -30,8 +56,6 @@ public final class Snippet: Semantic, AutomaticDirectiveConvertible { "slice" : \Snippet._slice, ] - static var hiddenFromDocumentation = true - @available(*, deprecated, message: "Do not call directly. Required for 'AutomaticDirectiveConvertible'.") init(originalMarkup: BlockDirective) { self.originalMarkup = originalMarkup @@ -50,30 +74,36 @@ public final class Snippet: Semantic, AutomaticDirectiveConvertible { extension Snippet: RenderableDirectiveConvertible { func render(with contentCompiler: inout RenderContentCompiler) -> [any RenderContent] { - guard let snippet = Snippet(from: originalMarkup, for: contentCompiler.bundle) else { + guard case .success(let resolvedSnippet) = contentCompiler.context.snippetResolver.resolveSnippet(path: path) else { + return [] + } + let mixin = resolvedSnippet.mixin + + if let slice { + guard let sliceRange = mixin.slices[slice] else { + // The warning says that unrecognized snippet slices will ignore the entire snippet. return [] } + // Render only this slice without the explanatory content. + let lines = mixin.lines + // Trim the lines + .dropFirst(sliceRange.startIndex).prefix(sliceRange.endIndex - sliceRange.startIndex) + // Trim the whitespace + .linesWithoutLeadingWhitespace() + // Make dedicated copies of each line because the RenderBlockContent.codeListing requires it. + .map { String($0) } + + let options = RenderBlockContent.CodeBlockOptions() - guard let snippetReference = contentCompiler.resolveSymbolReference(destination: snippet.path), - let snippetEntity = try? contentCompiler.context.entity(with: snippetReference), - let snippetSymbol = snippetEntity.symbol, - let snippetMixin = snippetSymbol.mixins[SymbolGraph.Symbol.Snippet.mixinKey] as? SymbolGraph.Symbol.Snippet else { - return [] - } + return [RenderBlockContent.codeListing(.init(syntax: mixin.language, code: lines, metadata: nil, options: options))] + } else { + // Render the full snippet and its explanatory content. + let options = RenderBlockContent.CodeBlockOptions() + let fullCode = RenderBlockContent.codeListing(.init(syntax: mixin.language, code: mixin.lines, metadata: nil, options: options)) - if let requestedSlice = snippet.slice, - let requestedLineRange = snippetMixin.slices[requestedSlice] { - // Render only the slice. - let lineRange = requestedLineRange.lowerBound.. { - get { .init(defaultVariantValue: dictionaryKeysSection) } - set { dictionaryKeysSection = newValue.firstValue } - } /// The symbol's possible values, if the symbol is a property list element with possible values. public var possibleValuesSection: PropertyListPossibleValuesSection? - @available(*, deprecated, renamed: "possibleValuesSection", message: "Use 'possibleValuesSection' instead. This deprecated API will be removed after 6.2 is released") - public var possibleValuesSectionVariants: DocumentationDataVariants { - get { .init(defaultVariantValue: possibleValuesSection) } - set { possibleValuesSection = newValue.firstValue } - } /// The HTTP endpoint of an HTTP request. public var httpEndpointSection: HTTPEndpointSection? - @available(*, deprecated, renamed: "httpEndpointSection", message: "Use 'httpEndpointSection' instead. This deprecated API will be removed after 6.2 is released") - public var httpEndpointSectionVariants: DocumentationDataVariants { - get { .init(defaultVariantValue: httpEndpointSection) } - set { httpEndpointSection = newValue.firstValue } - } - + /// The upload body of an HTTP request. public var httpBodySection: HTTPBodySection? - @available(*, deprecated, renamed: "httpBodySection", message: "Use 'httpBodySection' instead. This deprecated API will be removed after 6.2 is released") - public var httpBodySectionVariants: DocumentationDataVariants { - get { .init(defaultVariantValue: httpBodySection) } - set { httpBodySection = newValue.firstValue } - } - + /// The parameters of an HTTP request. public var httpParametersSection: HTTPParametersSection? - @available(*, deprecated, renamed: "httpParametersSection", message: "Use 'httpParametersSection' instead. This deprecated API will be removed after 6.2 is released") - public var httpParametersSectionVariants: DocumentationDataVariants { - get { .init(defaultVariantValue: httpParametersSection) } - set { httpParametersSection = newValue.firstValue } - } /// The responses of an HTTP request. public var httpResponsesSection: HTTPResponsesSection? - @available(*, deprecated, renamed: "httpResponsesSection", message: "Use 'httpResponsesSection' instead. This deprecated API will be removed after 6.2 is released") - public var httpResponsesSectionVariants: DocumentationDataVariants { - get { .init(defaultVariantValue: httpResponsesSection) } - set { httpResponsesSection = newValue.firstValue } - } /// Any redirect information of the symbol, if the symbol has been moved from another location. public var redirects: [Redirect]? - @available(*, deprecated, renamed: "redirects", message: "Use 'redirects' instead. This deprecated API will be removed after 6.2 is released") - public var redirectsVariants: DocumentationDataVariants<[Redirect]> { - get { .init(defaultVariantValue: redirects) } - set { redirects = newValue.firstValue } - } /// The symbol's abstract summary as a single paragraph, in each language variant the symbol is available in. public var abstractVariants: DocumentationDataVariants { @@ -496,14 +461,14 @@ extension Symbol { /// When building multi-platform documentation symbols might have more than one declaration /// depending on variances in their implementation across platforms (e.g. use `NSPoint` vs `CGPoint` parameter in a method). /// This method finds matching symbols between graphs and merges their declarations in case there are differences. - func mergeDeclaration(mergingDeclaration: SymbolGraph.Symbol.DeclarationFragments, identifier: String, symbolAvailability: SymbolGraph.Symbol.Availability?, alternateSymbols: SymbolGraph.Symbol.AlternateSymbols?, selector: UnifiedSymbolGraph.Selector) throws { + func mergeDeclaration(mergingDeclaration: SymbolGraph.Symbol.DeclarationFragments, identifier: String, symbolAvailability: SymbolGraph.Symbol.Availability?, alternateSymbols: SymbolGraph.Symbol.AlternateSymbols?, selector: UnifiedSymbolGraph.Selector) throws(DocumentationContext.ContextError) { let trait = DocumentationDataVariantsTrait(for: selector) let platformName = selector.platform func merge( _ mergingValue: Value, into variants: inout DocumentationDataVariants<[[PlatformName?] : Value]> - ) throws { + ) throws(DocumentationContext.ContextError) { guard let platformName else { variants[trait]?[[nil]] = mergingValue return diff --git a/Sources/SwiftDocC/Semantics/Symbol/UnifiedSymbol+Extensions.swift b/Sources/SwiftDocC/Semantics/Symbol/UnifiedSymbol+Extensions.swift index 594a4972e0..daf3b97558 100644 --- a/Sources/SwiftDocC/Semantics/Symbol/UnifiedSymbol+Extensions.swift +++ b/Sources/SwiftDocC/Semantics/Symbol/UnifiedSymbol+Extensions.swift @@ -93,11 +93,31 @@ extension UnifiedSymbolGraph.Symbol { /// Returns the primary symbol selector to use as documentation source. var documentedSymbolSelector: UnifiedSymbolGraph.Selector? { - // We'll prioritize the first documented 'swift' symbol, if we have - // one. - return docComment.keys.first { selector in - return selector.interfaceLanguage == "swift" - } ?? docComment.keys.first + // Prioritize the longest doc comment with a "swift" selector, + // if there is one. + return docComment.min(by: { lhs, rhs in + if (lhs.key.interfaceLanguage == "swift") != (rhs.key.interfaceLanguage == "swift") { + // sort swift selectors before non-swift ones + return lhs.key.interfaceLanguage == "swift" + } + + // if the comments are equal, bail early without iterating them again + guard lhs.value != rhs.value else { + return false + } + + let lhsLength = lhs.value.lines.totalCount + let rhsLength = rhs.value.lines.totalCount + + if lhsLength == rhsLength { + // if the comments are the same length, just sort them lexicographically + return lhs.value.lines.isLexicographicallyBefore(rhs.value.lines) + } else { + // otherwise, sort by the length of the doc comment, + // so that `min` returns the longest comment + return lhsLength > rhsLength + } + })?.key } func identifier(forLanguage interfaceLanguage: String) -> SymbolGraph.Symbol.Identifier { @@ -115,3 +135,15 @@ extension UnifiedSymbolGraph.Symbol { } } } + +extension [SymbolGraph.LineList.Line] { + fileprivate var totalCount: Int { + return reduce(into: 0) { result, line in + result += line.text.count + } + } + + fileprivate func isLexicographicallyBefore(_ other: Self) -> Bool { + self.lexicographicallyPrecedes(other) { $0.text < $1.text } + } +} diff --git a/Sources/SwiftDocC/Semantics/Technology/TutorialTableOfContents.swift b/Sources/SwiftDocC/Semantics/Technology/TutorialTableOfContents.swift index 1503e32f74..7e7098c0fb 100644 --- a/Sources/SwiftDocC/Semantics/Technology/TutorialTableOfContents.swift +++ b/Sources/SwiftDocC/Semantics/Technology/TutorialTableOfContents.swift @@ -120,6 +120,3 @@ public final class TutorialTableOfContents: Semantic, DirectiveConvertible, Abst return visitor.visitTutorialTableOfContents(self) } } - -@available(*, deprecated, renamed: "TutorialTableOfContents", message: "Use 'TutorialTableOfContents' instead. This deprecated API will be removed after 6.2 is released") -public typealias Technology = TutorialTableOfContents diff --git a/Sources/SwiftDocC/Semantics/TechnologyBound.swift b/Sources/SwiftDocC/Semantics/TechnologyBound.swift deleted file mode 100644 index c06ab6c3d9..0000000000 --- a/Sources/SwiftDocC/Semantics/TechnologyBound.swift +++ /dev/null @@ -1,16 +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 -*/ - -/// An entity directly referring to the technology it belongs to. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -public protocol TechnologyBound { - /// The `name` of the ``TutorialTableOfContents`` this section refers to. - var technology: TopicReference { get } -} diff --git a/Sources/SwiftDocC/Semantics/Visitor/SemanticVisitor.swift b/Sources/SwiftDocC/Semantics/Visitor/SemanticVisitor.swift index b301551338..3426102ccc 100644 --- a/Sources/SwiftDocC/Semantics/Visitor/SemanticVisitor.swift +++ b/Sources/SwiftDocC/Semantics/Visitor/SemanticVisitor.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -86,9 +86,6 @@ public protocol SemanticVisitor { Visit a ``TutorialTableOfContents`` and return the result. */ mutating func visitTutorialTableOfContents(_ tutorialTableOfContents: TutorialTableOfContents) -> Result - - @available(*, deprecated, renamed: "visitTutorialTableOfContents(_:)", message: "Use 'visitTutorialTableOfContents(_:)' instead. This deprecated API will be removed after 6.2 is released") - mutating func visitTechnology(_ technology: TutorialTableOfContents) -> Result /** Visit an ``ImageMedia`` and return the result. @@ -151,11 +148,3 @@ extension SemanticVisitor { return semantic.accept(&self) } } - -@available(*, deprecated) // Remove this default implementation after 6.2 is released. -extension SemanticVisitor { - // We need to provide a default implementation to avoid the breaking change of a new protocol requirement. - mutating func visitTutorialTableOfContents(_ tutorialTableOfContents: TutorialTableOfContents) -> Result { - self.visitTechnology(tutorialTableOfContents) - } -} diff --git a/Sources/SwiftDocC/Semantics/Walker/SemanticWalker.swift b/Sources/SwiftDocC/Semantics/Walker/SemanticWalker.swift index 242476129a..105353b6d6 100644 --- a/Sources/SwiftDocC/Semantics/Walker/SemanticWalker.swift +++ b/Sources/SwiftDocC/Semantics/Walker/SemanticWalker.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -50,8 +50,6 @@ extension SemanticWalker { mutating func visitTile(_ tile: Tile) { descendIntoChildren(of: tile) } /// Visits a comment node. mutating func visitComment(_ comment: Comment) { descendIntoChildren(of: comment) } - @available(*, deprecated) // This is a deprecated protocol requirement. Remove after 6.2 is released. - mutating func visitTechnology(_ technology: TutorialTableOfContents) { descendIntoChildren(of: technology) } /// Visits a tutorials table-of-contents page. mutating func visitTutorialTableOfContents(_ tutorialTableOfContents: TutorialTableOfContents) { descendIntoChildren(of: tutorialTableOfContents) } /// Visits an image node. diff --git a/Sources/SwiftDocC/Semantics/Walker/Walkers/SemanticTreeDumper.swift b/Sources/SwiftDocC/Semantics/Walker/Walkers/SemanticTreeDumper.swift index bc379233c4..c34d9a541d 100644 --- a/Sources/SwiftDocC/Semantics/Walker/Walkers/SemanticTreeDumper.swift +++ b/Sources/SwiftDocC/Semantics/Walker/Walkers/SemanticTreeDumper.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -148,11 +148,6 @@ struct SemanticTreeDumper: SemanticWalker { dump(markupContainer, customDescription: description) } - @available(*, deprecated) // This is a deprecated protocol requirement. Remove after 6.2 is released - mutating func visitTechnology(_ technology: TutorialTableOfContents) { - visitTutorialTableOfContents(technology) - } - mutating func visitTutorialTableOfContents(_ tutorialTableOfContents: TutorialTableOfContents) -> () { dump(tutorialTableOfContents, customDescription: "name: '\(tutorialTableOfContents.name)'") } diff --git a/Sources/SwiftDocC/SwiftDocC.docc/Resources/Diagnostics.json b/Sources/SwiftDocC/SwiftDocC.docc/Resources/Diagnostics.json index 0c268b7b7b..bd53d8e809 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/Diagnostics.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/Diagnostics.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "description": "Specification of the deprecated DocC diagnostics.json digest file. This deprecated file will be removed after 6.2 is released.", + "description": "Specification of the deprecated DocC diagnostics.json digest file. This deprecated file will be removed after 6.3 is released.", "version": "0.1.0", "title": "Diagnostics" }, diff --git a/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json b/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json index 6f156a7423..4b5e0a3b34 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json @@ -68,12 +68,21 @@ "usr": { "type": "string" }, + "plainTextDeclaration": { + "type": "string" + }, "fragments": { "type": "array", "items": { "$ref": "#/components/schemas/DeclarationToken" } }, + "navigatorFragments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarationToken" + } + }, "topicImages": { "type": "array", "items": { @@ -158,6 +167,10 @@ "type": "string", "nullable": true }, + "plainTextDeclaration": { + "type": "string", + "nullable": true + }, "fragments": { "type": "array", "items": { @@ -165,6 +178,13 @@ }, "nullable": true }, + "navigatorFragments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarationToken" + }, + "nullable": true + }, "taskGroups": { "type": "array", "items": { @@ -550,7 +570,7 @@ "type": "string", "enum": ["icon", "card"] }, - "reference": { + "identifier": { "type": "string", "format": "reference(ImageRenderReference)" } diff --git a/Sources/SwiftDocC/SwiftDocC.docc/Resources/Metadata.json b/Sources/SwiftDocC/SwiftDocC.docc/Resources/Metadata.json index 043a45977a..5fc325993a 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/Metadata.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/Metadata.json @@ -12,7 +12,7 @@ "type": "object", "required": [ "bundleDisplayName", - "bundleIdentifier", + "bundleID", "schemaVersion" ], "properties": { diff --git a/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderIndex.spec.json b/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderIndex.spec.json index c1c8beb5b3..d100305d97 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderIndex.spec.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderIndex.spec.json @@ -15,7 +15,7 @@ "interfaceLanguages" ], "properties": { - "identifier": { + "schemaVersion": { "$ref": "#/components/schemas/SchemaVersion" }, "interfaceLanguages": { diff --git a/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json b/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json index 4ced315007..6cb6924e51 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json @@ -360,8 +360,7 @@ "type": "object", "required": [ "kind", - "tiles", - "content" + "tiles" ], "properties": { "kind": { @@ -526,8 +525,7 @@ "kind", "content", "media", - "mediaPosition", - "layout" + "mediaPosition" ], "properties": { "kind": { @@ -781,6 +779,39 @@ } } }, + "LineAnnotation": { + "type": "object", + "properties": { + "style": { + "type": "string", + "enum": ["highlight", "strikeout"] + }, + "range": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Position" + } + } + }, + "required": [ + "style", + "range" + ] + }, + "Position": { + "type": "object", + "properties": { + "line": { + "type": "integer" + }, + "character": { + "type": "integer" + } + }, + "required": [ + "line" + ] + }, "CodeListing": { "type": "object", "required": [ @@ -805,6 +836,21 @@ }, "metadata": { "$ref": "#/components/schemas/RenderContentMetadata" + }, + "copyToClipboard": { + "type": "boolean" + }, + "showLineNumbers": { + "type": "boolean" + }, + "wrap": { + "type": "integer" + }, + "lineAnnotations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LineAnnotation" + } } } }, @@ -1802,7 +1848,7 @@ "type": "string", "enum": ["icon", "card"] }, - "reference": { + "identifier": { "type": "string", "format": "reference(ImageRenderReference)" } @@ -1963,7 +2009,7 @@ "type": "string", "format": "reference(RenderReference)" }, - "sections": { + "kind": { "type": "string", "enum": ["task", "assessment", "heading"] } @@ -2499,7 +2545,6 @@ }, "MentionsRenderSection": { "required": [ - "kind", "mentions" ], "type": "object", diff --git a/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/StaticAnalysis.md b/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/StaticAnalysis.md index 5155029943..a2330698d4 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/StaticAnalysis.md +++ b/Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/StaticAnalysis.md @@ -12,7 +12,6 @@ Run static analysis checks on markup files. ### Predefined Checks -- ``AbstractContainsFormattedTextOnly`` - ``DuplicateTopicsSections`` - ``InvalidAdditionalTitle`` - ``MissingAbstract`` @@ -20,4 +19,4 @@ Run static analysis checks on markup files. - ``NonOverviewHeadingChecker`` - ``SeeAlsoInTopicsHeadingChecker`` - + diff --git a/Sources/SwiftDocC/Utility/Collection+ConcurrentPerform.swift b/Sources/SwiftDocC/Utility/Collection+ConcurrentPerform.swift index 5fbcdea3be..37a0a5fe33 100644 --- a/Sources/SwiftDocC/Utility/Collection+ConcurrentPerform.swift +++ b/Sources/SwiftDocC/Utility/Collection+ConcurrentPerform.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -20,7 +20,7 @@ private let useConcurrentCollectionExtensions = true private let useConcurrentCollectionExtensions = false #endif -extension Collection where Index == Int { +extension Collection where Index == Int, Self: SendableMetatype { /// Concurrently transforms the elements of a collection. /// - Parameters: diff --git a/Sources/SwiftDocC/Utility/CollectionChanges.swift b/Sources/SwiftDocC/Utility/CollectionChanges.swift index 597fc28583..a64328cf04 100644 --- a/Sources/SwiftDocC/Utility/CollectionChanges.swift +++ b/Sources/SwiftDocC/Utility/CollectionChanges.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -65,7 +65,7 @@ struct CollectionChanges { /// /// - Important: /// Removals need to be applied in reverse order. All removals need to be applied before applying any insertions. Insertions need to be applied in order. -private struct ChangeSegmentBuilder { +private struct ChangeSegmentBuilder: ~Copyable { typealias Segment = CollectionChanges.Segment private(set) var segments: [Segment] diff --git a/Sources/SwiftDocC/Utility/CommonTypeExports.swift b/Sources/SwiftDocC/Utility/CommonTypeExports.swift new file mode 100644 index 0000000000..7194a5c125 --- /dev/null +++ b/Sources/SwiftDocC/Utility/CommonTypeExports.swift @@ -0,0 +1,13 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 +*/ + +public import DocCCommon + +public typealias SourceLanguage = DocCCommon.SourceLanguage diff --git a/Sources/SwiftDocC/Utility/FeatureFlags.swift b/Sources/SwiftDocC/Utility/FeatureFlags.swift index b2ec4dbc5d..538e55781f 100644 --- a/Sources/SwiftDocC/Utility/FeatureFlags.swift +++ b/Sources/SwiftDocC/Utility/FeatureFlags.swift @@ -13,7 +13,10 @@ public struct FeatureFlags: Codable { /// The current feature flags that Swift-DocC uses to conditionally enable /// (usually experimental) behavior in Swift-DocC. public static var current = FeatureFlags() - + + /// Whether or not experimental annotation of code blocks is enabled. + public var isExperimentalCodeBlockAnnotationsEnabled = false + /// Whether or not experimental support for device frames on images and video is enabled. public var isExperimentalDeviceFrameSupportEnabled = false @@ -26,30 +29,11 @@ public struct FeatureFlags: Codable { /// Whether support for automatically rendering links on symbol documentation to articles that mention that symbol is enabled. public var isMentionedInEnabled = true - @available(*, deprecated, renamed: "isMentionedInEnabled", message: "Use 'isMentionedInEnabled' instead. This deprecated API will be removed after 6.2 is released") - public var isExperimentalMentionedInEnabled: Bool { - get { isMentionedInEnabled } - set { isMentionedInEnabled = newValue } - } - /// Whether or not support for validating parameters and return value documentation is enabled. public var isParametersAndReturnsValidationEnabled = true /// Creates a set of feature flags with all default values. public init() {} - - /// Creates a set of feature flags with the given values. - /// - /// - Parameters: - /// - additionalFlags: Any additional flags to set. - /// - /// This field allows clients to set feature flags without adding new API. - @available(*, deprecated, renamed: "init()", message: "Use 'init()' instead. This deprecated API will be removed after 6.2 is released") - @_disfavoredOverload - public init( - additionalFlags: [String : Bool] = [:] - ) { - } /// Set feature flags that were loaded from a bundle's Info.plist. internal mutating func loadFlagsFromBundle(_ bundleFlags: DocumentationBundle.Info.BundleFeatureFlags) { diff --git a/Sources/SwiftDocC/Utility/FoundationExtensions/RangeReplaceableCollection+Group.swift b/Sources/SwiftDocC/Utility/FoundationExtensions/RangeReplaceableCollection+Group.swift index 8aaf30ec91..ba2384bf38 100644 --- a/Sources/SwiftDocC/Utility/FoundationExtensions/RangeReplaceableCollection+Group.swift +++ b/Sources/SwiftDocC/Utility/FoundationExtensions/RangeReplaceableCollection+Group.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -15,7 +15,7 @@ extension RangeReplaceableCollection { /// /// - Parameter belongsInGroupWithPrevious: A check whether the given element belongs in the same group as the previous element /// - Returns: An array of subsequences of elements that belong together. - func group(asLongAs belongsInGroupWithPrevious: (_ previous: Element, _ current: Element) throws -> Bool) rethrows -> [SubSequence] { + func group(asLongAs belongsInGroupWithPrevious: (_ previous: Element, _ current: Element) throws(Error) -> Bool) throws(Error) -> [SubSequence] { var result = [SubSequence]() let indexPairs = zip(indices, indices.dropFirst()) diff --git a/Sources/SwiftDocC/Utility/FoundationExtensions/SendableMetatypeShim.swift b/Sources/SwiftDocC/Utility/FoundationExtensions/SendableMetatypeShim.swift new file mode 100644 index 0000000000..f9471a06d2 --- /dev/null +++ b/Sources/SwiftDocC/Utility/FoundationExtensions/SendableMetatypeShim.swift @@ -0,0 +1,21 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 +*/ + +// In Swift 6.2, metatypes are no longer sendable by default (SE-0470). +// Instead a type needs to conform to `SendableMetatype` to indicate that its metatype is sendable. +// +// However, `SendableMetatype` doesn't exist before Swift 6.1 so we define an internal alias to `Any` here. +// This means that conformances to `SendableMetatype` has no effect before 6.2 indicates metatype sendability in 6.2 onwards. +// +// Note: Adding a protocol requirement to a _public_ API is a breaking change. + +#if compiler(<6.2) +typealias SendableMetatype = Any +#endif diff --git a/Sources/SwiftDocC/Utility/FoundationExtensions/Sequence+FirstMap.swift b/Sources/SwiftDocC/Utility/FoundationExtensions/Sequence+FirstMap.swift index c51d041d8b..e9502eec90 100644 --- a/Sources/SwiftDocC/Utility/FoundationExtensions/Sequence+FirstMap.swift +++ b/Sources/SwiftDocC/Utility/FoundationExtensions/Sequence+FirstMap.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -30,7 +30,7 @@ extension Sequence { /// - Parameter predicate: A mapping closure that accepts an element of this sequence as its parameter and returns a transformed value or `nil`. /// - Throws: Any error that's raised by the mapping closure. /// - Returns: The first mapped, non-nil value, or `nil` if the mapping closure returned `nil` for every value in the sequence. - func mapFirst(where predicate: (Element) throws -> T?) rethrows -> T? { + func mapFirst(where predicate: (Element) throws(Error) -> Result?) throws(Error) -> Result? { for element in self { if let result = try predicate(element) { return result diff --git a/Sources/SwiftDocC/Utility/Synchronization.swift b/Sources/SwiftDocC/Utility/Synchronization.swift index 7e243bbe9b..80395c176f 100644 --- a/Sources/SwiftDocC/Utility/Synchronization.swift +++ b/Sources/SwiftDocC/Utility/Synchronization.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -77,7 +77,7 @@ public class Synchronized { /// - Parameter block: A throwing block of work that optionally returns a value. /// - Returns: Returns the returned value of `block`, if any. @discardableResult - public func sync(_ block: (inout Value) throws -> Result) rethrows -> Result { + public func sync(_ block: (inout Value) throws(Error) -> Result) throws(Error) -> Result { #if os(macOS) || os(iOS) os_unfair_lock_lock(lock) defer { os_unfair_lock_unlock(lock) } @@ -116,7 +116,7 @@ extension Lock { } @discardableResult - package func sync(_ block: () throws -> Result) rethrows -> Result { + package func sync(_ block: () throws(Error) -> Result) throws(Error) -> Result { #if os(macOS) || os(iOS) os_unfair_lock_lock(lock) defer { os_unfair_lock_unlock(lock) } diff --git a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift index 248dcdc9db..406366c281 100644 --- a/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift +++ b/Sources/SwiftDocCTestUtilities/FilesAndFolders.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -94,11 +94,17 @@ public struct InfoPlist: File, DataRepresentable { /// The information that the Into.plist file contains. public let content: Content - public init(displayName: String? = nil, identifier: String? = nil, defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]? = nil) { + public init( + displayName: String? = nil, + identifier: String? = nil, + defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]? = nil, + defaultCodeListingLanguage: String? = nil + ) { self.content = Content( displayName: displayName, identifier: identifier, - defaultAvailability: defaultAvailability + defaultAvailability: defaultAvailability, + defaultCodeListingLanguage: defaultCodeListingLanguage ) } @@ -106,25 +112,29 @@ public struct InfoPlist: File, DataRepresentable { public let displayName: String? public let identifier: String? public let defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]? - - fileprivate init(displayName: String?, identifier: String?, defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]?) { + public let defaultCodeListingLanguage: String? + + fileprivate init(displayName: String?, identifier: String?, defaultAvailability: [String: [DefaultAvailability.ModuleAvailability]]?, defaultCodeListingLanguage: String?) { self.displayName = displayName self.identifier = identifier self.defaultAvailability = defaultAvailability + self.defaultCodeListingLanguage = defaultCodeListingLanguage } public init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: DocumentationBundle.Info.CodingKeys.self) + let container = try decoder.container(keyedBy: DocumentationContext.Inputs.Info.CodingKeys.self) displayName = try container.decodeIfPresent(String.self, forKey: .displayName) identifier = try container.decodeIfPresent(String.self, forKey: .id) defaultAvailability = try container.decodeIfPresent([String : [DefaultAvailability.ModuleAvailability]].self, forKey: .defaultAvailability) + defaultCodeListingLanguage = try container.decodeIfPresent(String.self, forKey: .defaultCodeListingLanguage) } public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: DocumentationBundle.Info.CodingKeys.self) + var container = encoder.container(keyedBy: DocumentationContext.Inputs.Info.CodingKeys.self) try container.encodeIfPresent(displayName, forKey: .displayName) try container.encodeIfPresent(identifier, forKey: .id) try container.encodeIfPresent(defaultAvailability, forKey: .defaultAvailability) + try container.encodeIfPresent(defaultCodeListingLanguage, forKey: .defaultCodeListingLanguage) } } diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift index ed10772efc..b6c98ecedd 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift @@ -178,12 +178,12 @@ public struct ConvertAction: AsyncAction { self.configuration = configuration - self.bundle = bundle + self.inputs = bundle self.dataProvider = dataProvider } let configuration: DocumentationContext.Configuration - private let bundle: DocumentationBundle + private let inputs: DocumentationBundle private let dataProvider: any DataProvider /// A block of extra work that tests perform to affect the time it takes to convert documentation @@ -286,11 +286,11 @@ public struct ConvertAction: AsyncAction { workingDirectory: temporaryFolder, fileManager: fileManager) - let indexer = try Indexer(outputURL: temporaryFolder, bundleID: bundle.id) + let indexer = try Indexer(outputURL: temporaryFolder, bundleID: inputs.id) - let context = try signposter.withIntervalSignpost("Register", id: signposter.makeSignpostID()) { - try DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) - } + let registerInterval = signposter.beginInterval("Register", id: signposter.makeSignpostID()) + let context = try await DocumentationContext(bundle: inputs, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) + signposter.endInterval("Register", registerInterval) let outputConsumer = ConvertFileWritingConsumer( targetFolder: temporaryFolder, @@ -300,7 +300,7 @@ public struct ConvertAction: AsyncAction { indexer: indexer, enableCustomTemplates: experimentalEnableCustomTemplates, transformForStaticHostingIndexHTML: transformForStaticHosting ? indexHTML : nil, - bundleID: bundle.id + bundleID: inputs.id ) if experimentalModifyCatalogWithGeneratedCuration, let catalogURL = rootURL { @@ -318,7 +318,6 @@ public struct ConvertAction: AsyncAction { do { conversionProblems = try signposter.withIntervalSignpost("Process") { try ConvertActionConverter.convert( - bundle: bundle, context: context, outputConsumer: outputConsumer, sourceRepository: sourceRepository, @@ -347,7 +346,7 @@ public struct ConvertAction: AsyncAction { let tableOfContentsFilename = CatalogTemplateKind.tutorialTopLevelFilename let source = rootURL?.appendingPathComponent(tableOfContentsFilename) var replacements = [Replacement]() - if let tableOfContentsTemplate = CatalogTemplateKind.tutorialTemplateFiles(bundle.displayName)[tableOfContentsFilename] { + if let tableOfContentsTemplate = CatalogTemplateKind.tutorialTemplateFiles(inputs.displayName)[tableOfContentsFilename] { replacements.append( Replacement( range: .init(line: 1, column: 1, source: source) ..< .init(line: 1, column: 1, source: source), @@ -438,7 +437,7 @@ public struct ConvertAction: AsyncAction { context: context, indexer: nil, transformForStaticHostingIndexHTML: nil, - bundleID: bundle.id + bundleID: inputs.id ) try outputConsumer.consume(benchmarks: Benchmark.main) diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertFileWritingConsumer.swift b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertFileWritingConsumer.swift index eb1e43f6dc..56e4925851 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertFileWritingConsumer.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertFileWritingConsumer.swift @@ -11,7 +11,7 @@ import Foundation import SwiftDocC -struct ConvertFileWritingConsumer: ConvertOutputConsumer { +struct ConvertFileWritingConsumer: ConvertOutputConsumer, ExternalNodeConsumer { var targetFolder: URL var bundleRootFolder: URL? var fileManager: any FileManagerProtocol @@ -50,8 +50,8 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer { self.assetPrefixComponent = bundleID?.rawValue.split(separator: "/").joined(separator: "-") } - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - func consume(problems: [Problem]) throws { + @available(*, deprecated, message: "This deprecated API will be removed after 6.3 is released") + func _deprecated_consume(problems: [Problem]) throws { let diagnostics = problems.map { problem in Digest.Diagnostic(diagnostic: problem.diagnostic, rootURL: bundleRootFolder) } @@ -68,6 +68,11 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer { indexer?.index(renderNode) } + func consume(externalRenderNode: ExternalRenderNode) throws { + // Index the external node, if indexing is enabled. + indexer?.index(externalRenderNode) + } + func consume(assetsInBundle bundle: DocumentationBundle) throws { func copyAsset(_ asset: DataAsset, to destinationFolder: URL) throws { for sourceURL in asset.variants.values where !sourceURL.isAbsoluteWebURL { @@ -79,7 +84,6 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer { } } - // TODO: Supporting a single bundle for the moment. let bundleID = bundle.id assert(bundleID.rawValue == self.assetPrefixComponent, "Unexpectedly encoding assets for a bundle other than the one this output consumer was created for.") @@ -246,7 +250,7 @@ enum Digest { let downloads: [DownloadReference] } - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") + @available(*, deprecated, message: "This deprecated API will be removed after 6.3 is released") struct Diagnostic: Codable { struct Location: Codable { let line: Int @@ -265,7 +269,7 @@ enum Digest { } } -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") +@available(*, deprecated, message: "This deprecated API will be removed after 6.3 is released") private extension Digest.Diagnostic { init(diagnostic: Diagnostic, rootURL: URL?) { self.start = (diagnostic.range?.lowerBound).map { Location(line: $0.line, column: $0.column) } diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift b/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift index 1097395d89..5a721a8e45 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift @@ -62,6 +62,22 @@ extension ConvertAction { }) } + /// Indexes the given external render node and collects any encountered problems. + /// - Parameter renderNode: A ``ExternalRenderNode`` value. + func index(_ renderNode: ExternalRenderNode) { + // Synchronously index the render node. + indexBuilder.sync({ + do { + try $0.index(renderNode: renderNode) + nodeCount += 1 + } catch { + self.problems.append(error.problem(source: renderNode.identifier.url, + severity: .warning, + summaryPrefix: "External render node indexing process failed")) + } + }) + } + /// Finalizes the index and writes it on disk. /// - Returns: Returns a list of problems if any were encountered during indexing. func finalize(emitJSON: Bool, emitLMDB: Bool) -> [Problem] { diff --git a/Sources/SwiftDocCUtilities/Action/Actions/EmitGeneratedCurationAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/EmitGeneratedCurationAction.swift index c23723c76f..5678ccc081 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/EmitGeneratedCurationAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/EmitGeneratedCurationAction.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -50,7 +50,7 @@ struct EmitGeneratedCurationAction: AsyncAction { additionalSymbolGraphFiles: symbolGraphFiles(in: additionalSymbolGraphDirectory) ) ) - let context = try DocumentationContext(bundle: bundle, dataProvider: dataProvider) + let context = try await DocumentationContext(bundle: bundle, dataProvider: dataProvider) let writer = GeneratedCurationWriter(context: context, catalogURL: catalogURL, outputURL: outputURL) let curation = try writer.generateDefaultCurationContents(fromSymbol: startingPointSymbolLink, depthLimit: depthLimit) diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction+SynthesizedLandingPage.swift b/Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction+SynthesizedLandingPage.swift index 0b76352f92..0c33da016c 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction+SynthesizedLandingPage.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction+SynthesizedLandingPage.swift @@ -36,7 +36,10 @@ extension MergeAction { func readRootNodeRenderReferencesIn(dataDirectory: URL) throws -> RootRenderReferences { func inner(url: URL) throws -> [RootRenderReferences.Information] { - try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) + // Path might not exist (e.g. tutorials for a reference-only archive) + guard let contents = try? fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) + else { return [] } + return try contents .compactMap { guard $0.pathExtension == "json" else { return nil diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction.swift index cc94bc1b80..d68fa32af6 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction.swift @@ -65,6 +65,8 @@ struct MergeAction: AsyncAction { let fromDirectory = archive.appendingPathComponent(directoryToCopy, isDirectory: true) let toDirectory = targetURL.appendingPathComponent(directoryToCopy, isDirectory: true) + guard fileManager.directoryExists(atPath: fromDirectory.path) else { continue } + // Ensure that the destination directory exist in case the first archive didn't have that kind of pages. // This is necessary when merging a reference-only archive with a tutorial-only archive. try? fileManager.createDirectory(at: toDirectory, withIntermediateDirectories: false, attributes: nil) diff --git a/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/ConvertAction+CommandInitialization.swift b/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/ConvertAction+CommandInitialization.swift index e8c8a31b45..4d7272d3a2 100644 --- a/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/ConvertAction+CommandInitialization.swift +++ b/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/ConvertAction+CommandInitialization.swift @@ -19,7 +19,7 @@ extension ConvertAction { public init(fromConvertCommand convert: Docc.Convert, withFallbackTemplate fallbackTemplateURL: URL? = nil) throws { var standardError = LogHandle.standardError let outOfProcessResolver: OutOfProcessReferenceResolver? - + FeatureFlags.current.isExperimentalCodeBlockAnnotationsEnabled = convert.enableExperimentalCodeBlockAnnotations FeatureFlags.current.isExperimentalDeviceFrameSupportEnabled = convert.enableExperimentalDeviceFrameSupport FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled = convert.enableExperimentalLinkHierarchySerialization FeatureFlags.current.isExperimentalOverloadedSymbolPresentationEnabled = convert.enableExperimentalOverloadedSymbolPresentation diff --git a/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Convert.swift b/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Convert.swift index 557b4f2a52..a33715a625 100644 --- a/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Convert.swift +++ b/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Convert.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -475,7 +475,13 @@ extension Docc { struct FeatureFlagOptions: ParsableArguments { @Flag(help: "Allows for custom templates, like `header.html`.") var experimentalEnableCustomTemplates = false - + + @Flag( + name: .customLong("enable-experimental-code-block-annotations"), + help: "Support annotations for code blocks." + ) + var enableExperimentalCodeBlockAnnotations = false + @Flag(help: .hidden) var enableExperimentalDeviceFrameSupport = false @@ -512,9 +518,10 @@ extension Docc { var enableMentionedIn = true // This flag only exist to allow developers to pass the previous '--enable-experimental-...' flag without errors. + // The last release to support this spelling was 6.2. @Flag(name: .customLong("enable-experimental-mentioned-in"), help: .hidden) - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - var enableExperimentalMentionedIn = false + @available(*, deprecated, message: "This flag is unused and only exist for backwards compatibility") + var _unusedExperimentalMentionedInFlagForBackwardsCompatibility = false @Flag( name: .customLong("parameters-and-returns-validation"), @@ -557,6 +564,14 @@ extension Docc { } + /// A user-provided value that is true if the user enables experimental support for code block annotation. + /// + /// Defaults to false. + public var enableExperimentalCodeBlockAnnotations: Bool { + get { featureFlags.enableExperimentalCodeBlockAnnotations } + set { featureFlags.enableExperimentalCodeBlockAnnotations = newValue} + } + /// A user-provided value that is true if the user enables experimental support for device frames. /// /// Defaults to false. @@ -609,11 +624,6 @@ extension Docc { get { featureFlags.enableMentionedIn } set { featureFlags.enableMentionedIn = newValue } } - @available(*, deprecated, renamed: "enableMentionedIn", message: "Use 'enableMentionedIn' instead. This deprecated API will be removed after 6.2 is released") - public var enableExperimentalMentionedIn: Bool { - get { enableMentionedIn } - set { enableMentionedIn = newValue } - } /// A user-provided value that is true if the user enables experimental validation for parameters and return value documentation. public var enableParametersAndReturnsValidation: Bool { diff --git a/Sources/SwiftDocCUtilities/CMakeLists.txt b/Sources/SwiftDocCUtilities/CMakeLists.txt new file mode 100644 index 0000000000..c419ba4807 --- /dev/null +++ b/Sources/SwiftDocCUtilities/CMakeLists.txt @@ -0,0 +1,74 @@ +#[[ +This source file is part of the Swift open source project + +Copyright © 2014 - 2025 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 +#]] + +add_library(SwiftDocCUtilities STATIC + Action/Action.swift + Action/ActionResult.swift + Action/Actions/Action+MoveOutput.swift + Action/Actions/Convert/ConvertAction.swift + Action/Actions/Convert/ConvertFileWritingConsumer.swift + Action/Actions/Convert/CoverageDataEntry+generateSummary.swift + Action/Actions/Convert/Indexer.swift + Action/Actions/Convert/JSONEncodingRenderNodeWriter.swift + Action/Actions/CoverageAction.swift + Action/Actions/EmitGeneratedCurationAction.swift + Action/Actions/IndexAction.swift + Action/Actions/Init/CatalogTemplate.swift + Action/Actions/Init/CatalogTemplateKind.swift + Action/Actions/Init/InitAction.swift + Action/Actions/Merge/MergeAction+SynthesizedLandingPage.swift + Action/Actions/Merge/MergeAction.swift + Action/Actions/PreviewAction.swift + Action/Actions/TransformForStaticHostingAction.swift + ArgumentParsing/ActionExtensions/Action+performAndHandleResult.swift + ArgumentParsing/ActionExtensions/ConvertAction+CommandInitialization.swift + ArgumentParsing/ActionExtensions/EmitGeneratedCurationAction+CommandInitialization.swift + ArgumentParsing/ActionExtensions/IndexAction+CommandInitialization.swift + ArgumentParsing/ActionExtensions/InitAction+CommandInitialization.swift + ArgumentParsing/ActionExtensions/PreviewAction+CommandInitialization.swift + ArgumentParsing/ActionExtensions/TransformForStaticHostingAction+CommandInitialization.swift + ArgumentParsing/ArgumentValidation/URLArgumentValidator.swift + ArgumentParsing/Options/DirectoryPathOption.swift + ArgumentParsing/Options/DocumentationArchiveOption.swift + ArgumentParsing/Options/DocumentationBundleOption.swift + ArgumentParsing/Options/DocumentationCoverageOptionsArgument.swift + ArgumentParsing/Options/InitOptions.swift + ArgumentParsing/Options/OutOfProcessLinkResolverOption.swift + ArgumentParsing/Options/PreviewOptions.swift + "ArgumentParsing/Options/Source Repository/SourceRepositoryArguments.swift" + ArgumentParsing/Options/TemplateOption.swift + ArgumentParsing/Subcommands/Convert.swift + ArgumentParsing/Subcommands/EmitGeneratedCuration.swift + ArgumentParsing/Subcommands/Index.swift + ArgumentParsing/Subcommands/Init.swift + ArgumentParsing/Subcommands/Merge.swift + ArgumentParsing/Subcommands/Preview.swift + ArgumentParsing/Subcommands/ProcessArchive.swift + ArgumentParsing/Subcommands/ProcessCatalog.swift + ArgumentParsing/Subcommands/TransformForStaticHosting.swift + Docc.swift + PreviewServer/PreviewHTTPHandler.swift + PreviewServer/PreviewServer.swift + PreviewServer/RequestHandler/DefaultRequestHandler.swift + PreviewServer/RequestHandler/ErrorRequestHandler.swift + PreviewServer/RequestHandler/FileRequestHandler.swift + PreviewServer/RequestHandler/HTTPResponseHead+FromRequest.swift + PreviewServer/RequestHandler/RequestHandlerFactory.swift + Transformers/StaticHostableTransformer.swift + Utility/DirectoryMonitor.swift + Utility/FoundationExtensions/Sequence+Unique.swift + Utility/FoundationExtensions/String+Path.swift + Utility/FoundationExtensions/URL+IsAbsoluteWebURL.swift + Utility/FoundationExtensions/URL+Relative.swift + Utility/PlatformArgumentParser.swift + Utility/Signal.swift + Utility/Throttle.swift) +target_link_libraries(SwiftDocCUtilities PUBLIC + ArgumentParser + SwiftDocC) diff --git a/Sources/docc/CMakeLists.txt b/Sources/docc/CMakeLists.txt new file mode 100644 index 0000000000..4289856b76 --- /dev/null +++ b/Sources/docc/CMakeLists.txt @@ -0,0 +1,16 @@ +#[[ +This source file is part of the Swift open source project + +Copyright © 2014 - 2025 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 +#]] + +add_executable(docc + main.swift) +target_link_libraries(docc PRIVATE + SwiftDocCUtilities) + +install(TARGETS docc + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/Sources/docc/DocCDocumentation.docc/DocC Documentation.md b/Sources/docc/DocCDocumentation.docc/DocC Documentation.md index acff62f201..4cda0406f4 100644 --- a/Sources/docc/DocCDocumentation.docc/DocC Documentation.md +++ b/Sources/docc/DocCDocumentation.docc/DocC Documentation.md @@ -25,6 +25,7 @@ DocC syntax — called _documentation markup_ — is a custom variant of Markdow - - - +- - ### Structure and Formatting diff --git a/Sources/docc/DocCDocumentation.docc/DocC.symbols.json b/Sources/docc/DocCDocumentation.docc/DocC.symbols.json index 1d87e25a69..ef6eaf073b 100644 --- a/Sources/docc/DocCDocumentation.docc/DocC.symbols.json +++ b/Sources/docc/DocCDocumentation.docc/DocC.symbols.json @@ -93,7 +93,7 @@ "text" : "such as the `@objc` and `@_objcImplementation` attributes in Swift," }, { - "text" : "or the `NS_SWIFT_NAME` macro in Objective C." + "text" : "or the `NS_SWIFT_NAME` macro in Objective-C." }, { "text" : "" @@ -4167,13 +4167,43 @@ "text" : "You can use this directive to set the image used when rendering a user-interface element representing the page." }, { - "text" : "For example, use the page image directive to customize the icon used to represent this page in the navigation sidebar," + "text" : "" + }, + { + "text" : "Use the \"icon\" purpose to customize the icon that DocC uses to represent this page in the navigation sidebar." + }, + { + "text" : "For article pages, DocC also uses this icon to represent the article in topics sections and in ``Links`` directives that use the `list` visual style." }, { - "text" : "or the card image used to represent this page when using the ``Links`` directive and the ``Links\/VisualStyle\/detailedGrid``" + "text" : "" }, { - "text" : "visual style." + "text" : "> Tip: Page images with the \"icon\" purpose work best when they're square and when they have good visual clarity at small sizes (less than 20×20 points)." + }, + { + "text" : "" + }, + { + "text" : "Use the \"card\" purpose to customize the image that DocC uses to represent this page inside ``Links`` directives that use the either the `detailedGrid` or the `compactGrid` visual style." + }, + { + "text" : "For article pages, DocC also incorporates a partially faded out version of the card image in the background of the page itself." + }, + { + "text" : "" + }, + { + "text" : "> Tip: Page images with the \"card\" purpose work best when they have a 16:9 aspect ratio. Currently, the largest size that DocC displays a card image is 640×360 points." + }, + { + "text" : "" + }, + { + "text" : "If you specify an \"icon\" page image without specifying a \"card\" page image, DocC will use the icon as a fallback in places where the card image is preferred." + }, + { + "text" : "To avoid upscaled pixelated icons in these places, either configure a \"card\" page image as well or use a scalable vector image asset for the \"icon\" page image." }, { "text" : "- Parameters:" @@ -5246,6 +5276,240 @@ "Small" ] }, + { + "accessLevel" : "public", + "availability" : [ + { + "domain" : "Swift-DocC", + "introduced" : { + "major" : 5, + "minor" : 7, + "patch" : 0 + } + } + ], + "declarationFragments" : [ + { + "kind" : "typeIdentifier", + "spelling" : "@" + }, + { + "kind" : "typeIdentifier", + "spelling" : "Snippet" + }, + { + "kind" : "text", + "spelling" : "(" + }, + { + "kind" : "identifier", + "spelling" : "path" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "spelling" : "String" + }, + { + "kind" : "text", + "spelling" : ", " + }, + { + "kind" : "identifier", + "spelling" : "slice" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "spelling" : "String" + }, + { + "kind" : "text", + "spelling" : "?" + }, + { + "kind" : "text", + "spelling" : ")" + } + ], + "docComment" : { + "lines" : [ + { + "text" : "Embeds a code example from the project's code snippets." + }, + { + "text" : "" + }, + { + "text" : "Use a `Snippet` directive to embed a code example from the project's \"Snippets\" directory on the page." + }, + { + "text" : "The `path` argument is the relative path from the package's top-level \"Snippets\" directory to your snippet file without the `.swift` extension." + }, + { + "text" : "" + }, + { + "text" : "```markdown" + }, + { + "text" : "@Snippet(path: \"example-snippet\", slice: \"setup\")" + }, + { + "text" : "```" + }, + { + "text" : "" + }, + { + "text" : "If you prefer, you can specify the relative path from the package's _root_ directory (by including a \"Snippets\/\" prefix)." + }, + { + "text" : "You can also include the package name---as defined in `Package.swift`---before the \"Snippets\/\" prefix." + }, + { + "text" : "Neither of these leading path components are necessary because all your snippet code files are always located in your package's \"Snippets\" directory." + }, + { + "text" : "" + }, + { + "text" : "> Earlier Versions:" + }, + { + "text" : "> Before Swift-DocC 6.2.1, the `@Snippet` path needed to include both the package name component and the \"Snippets\" component:" + }, + { + "text" : ">" + }, + { + "text" : "> ```markdown" + }, + { + "text" : "> @Snippet(path: \"my-package\/Snippets\/example-snippet\")" + }, + { + "text" : "> ```" + }, + { + "text" : "" + }, + { + "text" : "You can define named slices of your snippet by annotating the snippet file with `\/\/ snippet.` and `\/\/ snippet.end` lines." + }, + { + "text" : "A named slice automatically ends at the start of the next named slice, without an explicit `snippet.end` annotation." + }, + { + "text" : "" + }, + { + "text" : "If the referenced snippet includes annotated slices, you can limit the embedded code example to a certain line range by specifying a `slice` name." + }, + { + "text" : "By default, the embedded code example includes the full snippet. For more information, see ." + }, + { + "text" : "- Parameters:" + }, + { + "text" : " - path: The relative path from your package's top-level \"Snippets\" directory to the snippet file that you want to embed in the page, without the `.swift` file extension." + }, + { + "text" : " **(required)**" + }, + { + "text" : " - slice: The name of a snippet slice to limit the embedded code example to a certain line range." + }, + { + "text" : " **(optional)**" + }, + { + "text" : " " + }, + { + "text" : " By default, the embedded code example includes the full snippet." + } + ] + }, + "identifier" : { + "interfaceLanguage" : "swift", + "precise" : "__docc_universal_symbol_reference_$Snippet" + }, + "kind" : { + "displayName" : "Directive", + "identifier" : "class" + }, + "names" : { + "navigator" : [ + { + "kind" : "attribute", + "spelling" : "@" + }, + { + "kind" : "identifier", + "preciseIdentifier" : "__docc_universal_symbol_reference_$Snippet", + "spelling" : "Snippet" + } + ], + "subHeading" : [ + { + "kind" : "identifier", + "spelling" : "@" + }, + { + "kind" : "identifier", + "spelling" : "Snippet" + }, + { + "kind" : "text", + "spelling" : "(" + }, + { + "kind" : "identifier", + "spelling" : "path" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "spelling" : "String" + }, + { + "kind" : "text", + "spelling" : ", " + }, + { + "kind" : "identifier", + "spelling" : "slice" + }, + { + "kind" : "text", + "spelling" : ": " + }, + { + "kind" : "typeIdentifier", + "spelling" : "String" + }, + { + "kind" : "text", + "spelling" : ")" + } + ], + "title" : "Snippet" + }, + "pathComponents" : [ + "Snippet" + ] + }, { "accessLevel" : "public", "availability" : [ diff --git a/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/Top-Level Directives/Tutorial/Assessments/Assessments.md b/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/Top-Level Directives/Tutorial/Assessments/Assessments.md index 4fed90616c..e0e9a76699 100644 --- a/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/Top-Level Directives/Tutorial/Assessments/Assessments.md +++ b/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/Top-Level Directives/Tutorial/Assessments/Assessments.md @@ -8,7 +8,7 @@ Tests the reader's knowledge at the end of a tutorial page. ## Overview -Use the `Assessment` directive to display an assessments section that helps the reader check their knowledge of your Swift framework or package APIs at the end of a tutorial page. An assessment includes a set of multiple-choice questions that you create using the`MultipleChoice`` directive. If the reader gets a question wrong, you can provide a hint that points them toward the correct answer so they can try again. +Use the `Assessment` directive to display an assessments section that helps the reader check their knowledge of your Swift framework or package APIs at the end of a tutorial page. An assessment includes a set of multiple-choice questions that you create using the ``MultipleChoice`` directive. If the reader gets a question wrong, you can provide a hint that points them toward the correct answer so they can try again. ``` @Tutorial(time: 30) { diff --git a/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/Top-Level Directives/Tutorial/Section.md b/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/Top-Level Directives/Tutorial/Section.md index e2b913614c..ccb8f15c7a 100644 --- a/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/Top-Level Directives/Tutorial/Section.md +++ b/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/Top-Level Directives/Tutorial/Section.md @@ -11,7 +11,7 @@ Displays a grouping of text, images, and tasks on a tutorial page. ## Overview -Use a `Section` directive to show a unit of work that consists of text, media, for example images and videos, and tasks on a tutorial page. A tutorial page must includes one or more sections. +Use a `Section` directive to show a unit of work that consists of text, media (for example images and videos), and tasks on a tutorial page. A tutorial page must includes one or more sections. ![A screenshot showing a section on a tutorial page. The section includes text, an image, and coding steps.](1) @@ -79,4 +79,4 @@ The following pages can include sections: - ``Stack`` - + diff --git a/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/building-an-interactive-tutorial.md b/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/building-an-interactive-tutorial.md index 0dca7877a1..539461b6ce 100644 --- a/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/building-an-interactive-tutorial.md +++ b/Sources/docc/DocCDocumentation.docc/Reference Syntax/Tutorials Syntax/building-an-interactive-tutorial.md @@ -50,7 +50,7 @@ Use a text editor and the following listing to create a table of contents file n } ```` -The top level of the listing the includes a ``Tutorials`` directive. This directive and the directives it contains, define the structure of the page. +The top level of the listing the includes a ``Tutorials`` directive. This directive, and the directives it contains, define the structure of the page. Rename the table of contents file and replace the placeholder content with your custom content. Use the ``Intro`` directive to introduce the reader to your tutorial through engaging text and imagery. Next, use ``Chapter`` directives to reference the step-by-step pages. diff --git a/Sources/docc/DocCDocumentation.docc/adding-code-snippets-to-your-content.md b/Sources/docc/DocCDocumentation.docc/adding-code-snippets-to-your-content.md new file mode 100644 index 0000000000..dc336d20b4 --- /dev/null +++ b/Sources/docc/DocCDocumentation.docc/adding-code-snippets-to-your-content.md @@ -0,0 +1,208 @@ +# Adding Code Snippets to your Content + +@Metadata { + @Available("Swift", introduced: "5.7") + @TitleHeading("Article") + } + +Create and include code snippets to illustrate and provide examples of how to use your API. + +## Overview + + +DocC supports code listings in your code, as described in . +In addition to code listings written directly in the markup, Swift Package Manager and DocC supports compiler verified code examples called "snippets". + +Swift Package Manager looks for, and builds, any code included in the `Snippets` directory for your package. +DocC supports referencing all, or parts, of those files to present as code listings. +In addition to snippets presenting your code examples, you can run snippets directly on the command line. +This allows you to verify that code examples, referenced in your documentation, continue to compile as you evolve you app or library. + +### Add the Swift DocC plugin + +To generate or preview documentation with snippets, add [swift-docc-plugin](https://github.com/apple/swift-docc-plugin) as a dependency to your package. + +For example, use the command: + +```bash +swift package add-dependency https://github.com/apple/swift-docc-plugin --from 1.1.0 +``` + +Or edit your `Package.swift` to add the dependency: + +``` +let package = Package( + // name, platforms, products, etc. + dependencies: [ + // other dependencies + .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0"), + ], + targets: [ + // targets + ] +) +``` + +### Create a code snippet + +Swift Package Manager expects to find your code examples in the directory `Snippets` at the top of your project, parallel to the file `Package.swift` and the directory `Sources`. +At the root of your project, create the directory `Snippets`. +Within the `Snippets` directory, create a file with your code snippet. + +Your Swift package directory structure should resemble this: + +``` +YourProject + ├── Package.swift + ├── Snippets + │   └── example-snippet.swift + ├── Sources + │   └── YourProject + │   └── YourProject.swift +etc... +``` + +> Note: Snippets are a package-wide resource located in a "Snippets" directory next to the package's "Sources" and "Tests" directories. + +The following example illustrates a code example in the file `Snippets/example-snippet.swift`: + +```swift +import Foundation + +print("Hello") +``` + +Your snippets can import targets defined in your local package, as well as products from its direct dependencies. +Each snippet is its own unit and can't access code from other snippet files. + +Every time you build your project, the Swift Package Manager compiles any code snippets, and then fails if the build if they are unable to compile. + +### Run the snippet + +You and consumers of your library can run your snippets from the command line using `swift run snippet-name` where "snippet-name" corresponds to a file name in your Snippets directory without the ".swift" file extension. + +Run the earlier code example file named `example-snippet.swift` using the following command: + +```bash +swift run example-snippet +``` + +### Embed the snippet + +To embed your snippet in an article or within the symbol reference pages, use the `@Snippet` directive. +```markdown +@Snippet(path: "example-snippet") +``` + +The `path` argument is the relative path from the package's top-level "Snippets" directory to your snippet file without the `.swift` extension. + +If you prefer, you can specify the relative path from the package's _root_ directory (by including a "Snippets/" prefix). +You can also include the package name---as defined in `Package.swift`---before the "Snippets/" prefix. +Neither of these leading path components are necessary because all your snippet code files are always located in your package's "Snippets" directory. + +> Earlier Versions: +> Before Swift-DocC 6.2.1, the `@Snippet` path needed to include both the package name component and the "Snippets" component: +> +> ```markdown +> @Snippet(path: "my-package/Snippets/example-snippet") +> ``` + +A snippet reference displays as a block between other paragraphs. +In the example package above, the `YourProject.md` file might contain this markdown: + +```markdown +# ``YourProject`` + +Add a single sentence or sentence fragment, which DocC uses as the page’s abstract or summary. + +## Overview + +Add one or more paragraphs that introduce your content overview. + +@Snippet(path: "example-snippet") +``` + +If your snippet code requires setup — like imports or variable definitions — that distract from the snippet's main focus, you can add `// snippet.hide` and `// snippet.show` lines in the snippet code to exclude the lines in between from displaying in your documentation. +These comments act as a toggle to hide or show content from the snippet. + +```swift +print("Hello") + +// snippet.hide + +print("Hidden") + +// snippet.show + +print("Shown") +``` + +Hide segments of your snippet for content such as license footers, test code, or unique setup code. +Generally, it is useful for things that you wouldn't want the reader to use as a starting point. + +### Preview your content + +Use the [swift-docc-plugin](https://github.com/swiftlang/swift-docc-plugin) to preview content that includes snippets. +To run the preview, use the following command from a terminal. +Replace `YourTarget` with a target from your package to preview: + +```bash +swift package --disable-sandbox preview-documentation --target YourTarget +``` + +### Slice up your snippet to break it up in your content + +Long snippets dropped into documentation can result in a wall of text that is harder to parse and understand. +Instead, annotate non-overlapping slices in the snippet, which allows you to reference and embed the slice portion of the example code. + +Annotating slices in a snippet looks similar to annotating `snippet.show` and `snippet.hide`. +You define the slice's identity in the comment, and that slice continues until the next instance of `// snippet.end` appears on a new line. +When selecting your identifiers, use URL-compatible path characters. + +For example, to start a slice with an ID of `setup`, add the following comment on a new line. + +```swift +// snippet.setup +``` + +Then end the `setup` slice with: + +```swift +// snippet.end +``` + +Adding a new slice identifier automatically terminates an earlier slice. +For example, the follow code examples are effectively the same: + +```swift +// snippet.setup +var item = MyObject.init() +// snippet.end + +// snippet.configure +item.size = 3 +// snippet.end +``` + +```swift +// snippet.setup +var item = MyObject.init() + +// snippet.configure +item.size = 3 +``` + +Use the `@Snippet` directive with the `slice` parameter to embed that slice as sample code on your documentation. +Extending the earlier snippet example, the slice `setup` would be referenced with + +```markdown +@Snippet(path: "example-snippet", slice: "setup") +``` + +## Topics + +### Directives + +- ``Snippet`` + + diff --git a/Sources/docc/DocCDocumentation.docc/formatting-your-documentation-content.md b/Sources/docc/DocCDocumentation.docc/formatting-your-documentation-content.md index 640a6ca3e6..9793f97170 100644 --- a/Sources/docc/DocCDocumentation.docc/formatting-your-documentation-content.md +++ b/Sources/docc/DocCDocumentation.docc/formatting-your-documentation-content.md @@ -4,35 +4,35 @@ Enhance your content's presentation with special formatting and styling for text ## Overview -Use [Markdown](https://daringfireball.net/projects/markdown/syntax), a -lightweight markup language, to give structure and style to your documentation. -DocC includes a custom dialect of Markdown, documentation markup, which -extends Markdown's syntax to include features like symbol linking, improved +Use [Markdown](https://daringfireball.net/projects/markdown/syntax), a +lightweight markup language, to give structure and style to your documentation. +DocC includes a custom dialect of Markdown, documentation markup, which +extends Markdown's syntax to include features like symbol linking, improved image support, term lists, and asides. -To ensure consistent structure and styling, use DocC's documentation markup for +To ensure consistent structure and styling, use DocC's documentation markup for all of the documentation you write. ### Add a Page Title and Section Headers -To add a page title, precede the text you want to use with a hash (`#`) and a +To add a page title, precede the text you want to use with a hash (`#`) and a space. For the page title of an article or API collection, use plain text only. ```markdown # Getting Started with Sloths ``` -> Important: Page titles must be the first line of content in a documentation +> Important: Page titles must be the first line of content in a documentation file. One or more empty lines can precede the page title. -For the page title of a landing page, enter a symbol link by wrapping the framework's +For the page title of a landing page, enter a symbol link by wrapping the framework's module name within a set of double backticks (\`\`). ```markdown # ``SlothCreator`` ``` -For a documentation extension file, enter a symbol link by wrapping the path to the symbol +For a documentation extension file, enter a symbol link by wrapping the path to the symbol within double backticks (\`\`). The path may start with the framework's module name or with the name of a top-level symbol in the module. @@ -48,41 +48,41 @@ The following example shows a documentation extension link to the same symbol st # ``CareSchedule/Event`` ``` -Augment every page title with a short and concise single-sentence abstract or -summary that provides additional information about the content. Add the summary +Augment every page title with a short and concise single-sentence abstract or +summary that provides additional information about the content. Add the summary using a new paragraph directly below the page title. ```markdown # Getting Started with Sloths Create a sloth and assign personality traits and abilities. -``` +``` -To add a header for an Overview or a Discussion section, use a double hash +To add a header for an Overview or a Discussion section, use a double hash (`##`) and a space, and then include either term in plain text. ```markdown ## Overview ``` -For all other section headers, use a triple hash (`###`) and a space, and then +For all other section headers, use a triple hash (`###`) and a space, and then add the title of the header in plain text. ```markdown ### Create a Sloth ``` -Use this type of section header in framework landing pages, top-level pages, -articles, and occasionally in symbol reference pages where you need to +Use this type of section header in framework landing pages, top-level pages, +articles, and occasionally in symbol reference pages where you need to provide more detail. ### Format Text in Bold, Italics, and Code Voice -DocC provides three ways to format the text in your documentation. You can -apply bold or italic styling, or you can use code voice, which renders the +DocC provides three ways to format the text in your documentation. You can +apply bold or italic styling, or you can use code voice, which renders the specified text in a monospace font. -To add bold styling, wrap the text in a pair of double asterisks (`**`). +To add bold styling, wrap the text in a pair of double asterisks (`**`). Alternatively, use double underscores (`__`). The following example uses bold styling for the names of the sloths: @@ -92,42 +92,42 @@ The following example uses bold styling for the names of the sloths: __Silly Sloth__: Prefers twigs for breakfast. ``` -Use italicized text to introduce new or alternative terms to the reader. To add -italic styling, wrap the text in a set of single underscores (`_`) or single +Use italicized text to introduce new or alternative terms to the reader. To add +italic styling, wrap the text in a set of single underscores (`_`) or single asterisks (`*`). -The following example uses italics for the words _metabolism_ and _habitat_: +The following example uses italics for the words _metabolism_ and _habitat_: ```markdown A sloth's _metabolism_ is highly dependent on its *habitat*. ``` -Use code voice to refer to symbols inline, or to include short code fragments, -such as class names or method signatures. To add code voice, wrap the text in +Use code voice to refer to symbols inline, or to include short code fragments, +such as class names or method signatures. To add code voice, wrap the text in a set of backticks (\`). -In the following example, DocC renders the words _ice_, _fire_, _wind_, and +In the following example, DocC renders the words _ice_, _fire_, _wind_, and _lightning_ in a monospace font: ```markdown -If your sloth possesses one of the special powers: `ice`, `fire`, +If your sloth possesses one of the special powers: `ice`, `fire`, `wind`, or `lightning`. ``` -> Note: To include multiple lines of code, use a code listing instead. For more +> Note: To include multiple lines of code, use a code listing instead. For more information, see . ### Add Code Listings -DocC includes support for code listings, or fenced code blocks, which allow you -to go beyond the basic declaration sections you find in symbol reference pages, -and to provide more complete code examples for adopters of your framework. You can -include code listings in your in-source symbol documentation, in extension +DocC includes support for code listings, or fenced code blocks, which allow you +to go beyond the basic declaration sections you find in symbol reference pages, +and to provide more complete code examples for adopters of your framework. You can +include code listings in your in-source symbol documentation, in extension files, and in articles and tutorials. -To create a code listing, start a new paragraph and add three backticks -(\`\`\`). Then, directly following the backticks, add the name of the -programming language in lowercase text. Add one or more lines of code, and then +To create a code listing, start a new paragraph and add three backticks +(\`\`\`). Then, directly following the backticks, add the name of the +programming language in lowercase text. Add one or more lines of code, and then add a new line and terminate the code listing by adding another three backticks: ```swift @@ -139,11 +139,11 @@ add a new line and terminate the code listing by adding another three backticks: } ``` -> Important: When formatting your code listing, use spaces to indent lines -instead of tabs so that DocC preserves the indentation when compiling your +> Important: When formatting your code listing, use spaces to indent lines +instead of tabs so that DocC preserves the indentation when compiling your documentation. -DocC uses the programming language you specify to apply the correct syntax +DocC uses the programming language you specify to apply the correct syntax color formatting. For the example above, DocC generates the following: ```swift @@ -181,6 +181,7 @@ have one or more aliases. | shell | console, shellsession | | swift | | | xml | html, xhtml, rss, atom, xjb, xsd, xsl, plist, wsf, svg | +| yaml | yml | ### Add Bulleted, Numbered, and Term Lists @@ -190,12 +191,12 @@ DocC supports the following list types: | ------------- | ------------------------------------------------------ | | Bulleted list | Groups items that can appear in any order. | | Numbered list | Delineates a sequence of events in a particular order. | -| Term list | Defines a series of term-definition pairs. | +| Term list | Defines a series of term-definition pairs. | -> Important: Don't add images or code listings between list items. Bulleted and +> Important: Don't add images or code listings between list items. Bulleted and numbered lists must contain two or more items. -To create a bulleted list, precede each of the list's items with an asterisk (`*`) and a +To create a bulleted list, precede each of the list's items with an asterisk (`*`) and a space. Alternatively, use a dash (`-`) or a plus sign (`+`) instead of an asterisk (`*`); the list markers are interchangeable. ```markdown @@ -205,7 +206,7 @@ space. Alternatively, use a dash (`-`) or a plus sign (`+`) instead of an asteri + Lightning ``` -To create a numbered list, precede each of the list's items with the number of the step, then a period (`.`) and a space. +To create a numbered list, precede each of the list's items with the number of the step, then a period (`.`) and a space. ```markdown 1. Give the sloth some food. @@ -214,8 +215,8 @@ To create a numbered list, precede each of the list's items with the number of t 4. Put the sloth to bed. ``` -To create a term list, precede each term with a dash (`-`) and a -space, the `term` keyword, and another space. Then add a colon (`:`), a space, and the definition after the term. +To create a term list, precede each term with a dash (`-`) and a +space, the `term` keyword, and another space. Then add a colon (`:`), a space, and the definition after the term. ```markdown - term Ice: Ice sloths thrive below freezing temperatures. @@ -224,8 +225,8 @@ space, the `term` keyword, and another space. Then add a colon (`:`), a space, a - term Lightning: Lightning sloths thrive in stormy climates. ``` -A list item's text, including terms and their definitions, can use the same -style attributes as other text, and include links to other content, including +A list item's text, including terms and their definitions, can use the same +style attributes as other text, and include links to other content, including symbols. diff --git a/Sources/generate-symbol-graph/main.swift b/Sources/generate-symbol-graph/main.swift index 1f8b038702..b6717febd5 100644 --- a/Sources/generate-symbol-graph/main.swift +++ b/Sources/generate-symbol-graph/main.swift @@ -61,7 +61,7 @@ func directiveUSR(_ directiveName: String) -> String { // The `@retroactive` attribute is new in the Swift 6 compiler. The backwards compatible syntax for a retroactive conformance is fully-qualified types. // // This conformance it only relevant to the `generate-symbol-graph` script. -extension SymbolKit.SymbolGraph.Symbol.DeclarationFragments.Fragment: Swift.ExpressibleByStringInterpolation { +extension SymbolKit.SymbolGraph.Symbol.DeclarationFragments.Fragment: Swift.ExpressibleByStringInterpolation, Swift.ExpressibleByUnicodeScalarLiteral, Swift.ExpressibleByExtendedGraphemeClusterLiteral, Swift.ExpressibleByStringLiteral { public init(stringLiteral value: String) { self.init(kind: .text, spelling: value, preciseIdentifier: nil) } @@ -79,7 +79,7 @@ extension SymbolKit.SymbolGraph.Symbol.DeclarationFragments.Fragment: Swift.Expr // The `@retroactive` attribute is new in the Swift 6 compiler. The backwards compatible syntax for a retroactive conformance is fully-qualified types. // // This conformance it only relevant to the `generate-symbol-graph` script. -extension SymbolKit.SymbolGraph.LineList.Line: Swift.ExpressibleByStringInterpolation { +extension SymbolKit.SymbolGraph.LineList.Line: Swift.ExpressibleByStringInterpolation, Swift.ExpressibleByUnicodeScalarLiteral, Swift.ExpressibleByExtendedGraphemeClusterLiteral, Swift.ExpressibleByStringLiteral { public init(stringLiteral value: String) { self.init(text: value, range: nil) } diff --git a/Tests/DocCCommonTests/FixedSizeBitSetTests.swift b/Tests/DocCCommonTests/FixedSizeBitSetTests.swift new file mode 100644 index 0000000000..22a1d91de0 --- /dev/null +++ b/Tests/DocCCommonTests/FixedSizeBitSetTests.swift @@ -0,0 +1,263 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024-2025 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 DocCCommon +import Testing + +struct FixedSizeBitSetTests { + @Test + func testBehavesSameAsSet() { + var tiny = _FixedSizeBitSet() + var real = Set() + + #expect(tiny.contains(4) == real.contains(4)) + #expect(tiny.insert(4) == real.insert(4)) + #expect(tiny.contains(4) == real.contains(4)) + #expect(tiny.count == real.count) + + #expect(tiny.insert(4) == real.insert(4)) + #expect(tiny.contains(4) == real.contains(4)) + #expect(tiny.count == real.count) + + #expect(tiny.insert(7) == real.insert(7)) + #expect(tiny.contains(7) == real.contains(7)) + #expect(tiny.count == real.count) + + #expect(tiny.update(with: 2) == real.update(with: 2)) + #expect(tiny.contains(2) == real.contains(2)) + #expect(tiny.count == real.count) + + #expect(tiny.remove(9) == real.remove(9)) + #expect(tiny.contains(9) == real.contains(9)) + #expect(tiny.count == real.count) + + #expect(tiny.remove(4) == real.remove(4)) + #expect(tiny.contains(4) == real.contains(4)) + #expect(tiny.count == real.count) + + tiny.formUnion([19]) + real.formUnion([19]) + #expect(tiny.contains(19) == real.contains(19)) + #expect(tiny.count == real.count) + + tiny.formSymmetricDifference([9]) + real.formSymmetricDifference([9]) + #expect(tiny.contains(7) == real.contains(7)) + #expect(tiny.contains(9) == real.contains(9)) + #expect(tiny.count == real.count) + + tiny.formIntersection([5,6,7]) + real.formIntersection([5,6,7]) + #expect(tiny.contains(4) == real.contains(4)) + #expect(tiny.contains(5) == real.contains(5)) + #expect(tiny.contains(6) == real.contains(6)) + #expect(tiny.contains(7) == real.contains(7)) + #expect(tiny.contains(8) == real.contains(8)) + #expect(tiny.contains(9) == real.contains(9)) + #expect(tiny.count == real.count) + + tiny.formUnion([11,29]) + real.formUnion([11,29]) + #expect(tiny.contains(11) == real.contains(11)) + #expect(tiny.contains(29) == real.contains(29)) + #expect(tiny.count == real.count) + + #expect(tiny.isSuperset(of: tiny) == real.isSuperset(of: real)) + #expect(tiny.isSuperset(of: []) == real.isSuperset(of: [])) + #expect(tiny.isSuperset(of: .init(tiny.dropFirst())) == real.isSuperset(of: .init(real.dropFirst()))) + #expect(tiny.isSuperset(of: .init(tiny.dropLast())) == real.isSuperset(of: .init(real.dropLast()))) + } + + @Test(arguments: [ + [], + [ 2], + [0,1, 4, 6, 10], + [0, 3, 5,6, 10, 13, 15], + [ 7,8, 11, 14], + [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] + ]) + func testBehavesSameAsArray(_ real: [Int]) throws { + let tiny = _FixedSizeBitSet(real) + + #expect(tiny.elementsEqual(real)) + + // Sorting + #expect(tiny.min() == real.min()) + #expect(tiny.max() == real.max()) + #expect(tiny.sorted() == real.sorted()) + + // Indexes + #expect(tiny.distance(from: tiny.startIndex, to: tiny.endIndex) + == real.distance(from: real.startIndex, to: real.endIndex)) + + // Index distances + #expect(real.count == tiny.count) + for offset in 0 ..< tiny.count { + let tinyIndex = try #require(tiny.index(tiny.startIndex, offsetBy: offset, limitedBy: tiny.endIndex)) + let realIndex = try #require(real.index(real.startIndex, offsetBy: offset, limitedBy: real.endIndex)) + + #expect(tiny.distance(from: tiny.startIndex, to: tinyIndex) + == real.distance(from: real.startIndex, to: realIndex), "Distances from start to index @\(offset) is the same") + + #expect(tiny.distance(from: tinyIndex, to: tiny.endIndex) + == real.distance(from: realIndex, to: real.endIndex), "Distances from index @\(offset) to end is the same") + } + // Limited index offset by count is not-nil + #expect(tiny.index(tiny.startIndex, offsetBy: tiny.count, limitedBy: tiny.endIndex) != nil) + #expect(real.index(real.startIndex, offsetBy: real.count, limitedBy: real.endIndex) != nil) + + // Limited index offset beyond the count is nil + #expect(tiny.index(tiny.startIndex, offsetBy: tiny.count + 1, limitedBy: tiny.endIndex) == nil) + #expect(real.index(real.startIndex, offsetBy: real.count + 1, limitedBy: real.endIndex) == nil) + + // Index advancements + do { + var currentIndex = tiny.startIndex + + for _ in 0 ..< tiny.count { + let before = currentIndex + let after = tiny.index(after: currentIndex) + + #expect(before < after) + #expect(tiny.distance(from: before, to: after) == 1) + + #expect(currentIndex == before) + tiny.formIndex(after: ¤tIndex) + #expect(currentIndex == after) + } + } + + // Index subscripts + #expect(tiny.indices.count == real.indices.count) + for (tinyIndex, realIndex) in zip(tiny.indices, real.indices) { + #expect(tiny[tinyIndex] == real[realIndex]) + } + + // Subsequences + + // Dropping prefixes + for elementsToDrop in 0 ..< tiny.count { + #expect(real.dropFirst(elementsToDrop).elementsEqual(tiny.dropFirst(elementsToDrop)), "Dropping \(elementsToDrop) from the start should be the same") + #expect(real.dropLast(elementsToDrop).elementsEqual(tiny.dropLast(elementsToDrop)), "Dropping \(elementsToDrop) from the end should be the same") + } + + for elementsToKeep in 0 ..< tiny.count { + #expect(real.prefix(elementsToKeep).elementsEqual(tiny.prefix(elementsToKeep)), "A \(elementsToKeep) prefix should be the same") + #expect(real.suffix(elementsToKeep).elementsEqual(tiny.suffix(elementsToKeep)), "A \(elementsToKeep) suffix should be the same") + } + + // Iteration + for (tinyNumber, realNumber) in zip(tiny, real) { + #expect(tinyNumber == realNumber) + } + } + + @Test() + func testCombinations() { + do { + let tiny: _FixedSizeBitSet = [0,1,2] + #expect(tiny.allCombinationsOfValues().map { $0.sorted() } == [ + [0], [1], [2], + [0,1], [0,2], [1,2], + [0,1,2] + ]) + } + + do { + let tiny: _FixedSizeBitSet = [2,5,9] + #expect(tiny.allCombinationsOfValues().map { $0.sorted() } == [ + [2], [5], [9], + [2,5], [2,9], [5,9], + [2,5,9] + ]) + } + + do { + let tiny: _FixedSizeBitSet = [3,4,7,11,15,16] + + let expected: [[Int]] = [ + // 1 elements + [3], [4], [7], [11], [15], [16], + // 2 elements + [3,4], [3,7], [3,11], [3,15], [3,16], + [4,7], [4,11], [4,15], [4,16], + [7,11], [7,15], [7,16], + [11,15], [11,16], + [15,16], + // 3 elements + [3,4,7], [3,4,11], [3,4,15], [3,4,16], [3,7,11], [3,7,15], [3,7,16], [3,11,15], [3,11,16], [3,15,16], + [4,7,11], [4,7,15], [4,7,16], [4,11,15], [4,11,16], [4,15,16], + [7,11,15], [7,11,16], [7,15,16], + [11,15,16], + // 4 elements + [3,4,7,11], [3,4,7,15], [3,4,7,16], [3,4,11,15], [3,4,11,16], [3,4,15,16], [3,7,11,15], [3,7,11,16], [3,7,15,16], [3,11,15,16], + [4,7,11,15], [4,7,11,16], [4,7,15,16], [4,11,15,16], + [7,11,15,16], + // 5 elements + [3,4,7,11,15], [3,4,7,11,16], [3,4,7,15,16], [3,4,11,15,16], [3,7,11,15,16], + [4,7,11,15,16], + // 6 elements + [3,4,7,11,15,16], + ] + let actual = tiny.allCombinationsOfValues().map { Array($0) } + + #expect(expected.count == actual.count) + + // The order of combinations within a given size doesn't matter. + // It's only important that all combinations of a given size exist and that the sizes are in order. + let expectedBySize = [Int: [[Int]]](grouping: expected, by: \.count).sorted(by: { $0.key < $1.key }).map(\.value) + let actualBySize = [Int: [[Int]]](grouping: actual, by: \.count).sorted(by: { $0.key < $1.key }).map(\.value) + + for (expectedForSize, actualForSize) in zip(expectedBySize, actualBySize) { + #expect(expectedForSize.count == actualForSize.count) + + // Comparing [Int] descriptions to allow each same-size combination list to have different orders. + // For example, these two lists of combinations (with the last 2 elements swapped) are considered equivalent: + // [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4] + // [1, 2, 3], [1, 2, 4], [2, 3, 4], [1, 3, 4] + #expect(expectedForSize.map(\.description).sorted() + == actualForSize.map(\.description).sorted()) + } + } + } + + @Test + func testIsSameSizeAsWrappedStorageType() async { + // Size + #expect(MemoryLayout<_FixedSizeBitSet< Int8 >>.size == MemoryLayout< Int8 >.size) + #expect(MemoryLayout<_FixedSizeBitSet< UInt8 >>.size == MemoryLayout< UInt8 >.size) + #expect(MemoryLayout<_FixedSizeBitSet< Int16 >>.size == MemoryLayout< Int16 >.size) + #expect(MemoryLayout<_FixedSizeBitSet< UInt16 >>.size == MemoryLayout< UInt16 >.size) + #expect(MemoryLayout<_FixedSizeBitSet< Int32 >>.size == MemoryLayout< Int32 >.size) + #expect(MemoryLayout<_FixedSizeBitSet< UInt32 >>.size == MemoryLayout< UInt32 >.size) + #expect(MemoryLayout<_FixedSizeBitSet< Int64 >>.size == MemoryLayout< Int64 >.size) + #expect(MemoryLayout<_FixedSizeBitSet< UInt64 >>.size == MemoryLayout< UInt64 >.size) + + // Stride + #expect(MemoryLayout<_FixedSizeBitSet< Int8 >>.stride == MemoryLayout< Int8 >.stride) + #expect(MemoryLayout<_FixedSizeBitSet< UInt8 >>.stride == MemoryLayout< UInt8 >.stride) + #expect(MemoryLayout<_FixedSizeBitSet< Int16 >>.stride == MemoryLayout< Int16 >.stride) + #expect(MemoryLayout<_FixedSizeBitSet< UInt16 >>.stride == MemoryLayout< UInt16 >.stride) + #expect(MemoryLayout<_FixedSizeBitSet< Int32 >>.stride == MemoryLayout< Int32 >.stride) + #expect(MemoryLayout<_FixedSizeBitSet< UInt32 >>.stride == MemoryLayout< UInt32 >.stride) + #expect(MemoryLayout<_FixedSizeBitSet< Int64 >>.stride == MemoryLayout< Int64 >.stride) + #expect(MemoryLayout<_FixedSizeBitSet< UInt64 >>.stride == MemoryLayout< UInt64 >.stride) + + // Alignment + #expect(MemoryLayout<_FixedSizeBitSet< Int8 >>.alignment == MemoryLayout< Int8 >.alignment) + #expect(MemoryLayout<_FixedSizeBitSet< UInt8 >>.alignment == MemoryLayout< UInt8 >.alignment) + #expect(MemoryLayout<_FixedSizeBitSet< Int16 >>.alignment == MemoryLayout< Int16 >.alignment) + #expect(MemoryLayout<_FixedSizeBitSet< UInt16 >>.alignment == MemoryLayout< UInt16 >.alignment) + #expect(MemoryLayout<_FixedSizeBitSet< Int32 >>.alignment == MemoryLayout< Int32 >.alignment) + #expect(MemoryLayout<_FixedSizeBitSet< UInt32 >>.alignment == MemoryLayout< UInt32 >.alignment) + #expect(MemoryLayout<_FixedSizeBitSet< Int64 >>.alignment == MemoryLayout< Int64 >.alignment) + #expect(MemoryLayout<_FixedSizeBitSet< UInt64 >>.alignment == MemoryLayout< UInt64 >.alignment) + } +} diff --git a/Tests/DocCCommonTests/SmallSourceLanguageSetTests.swift b/Tests/DocCCommonTests/SmallSourceLanguageSetTests.swift new file mode 100644 index 0000000000..6270ce6225 --- /dev/null +++ b/Tests/DocCCommonTests/SmallSourceLanguageSetTests.swift @@ -0,0 +1,154 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 DocCCommon +import Testing +import Foundation + +struct SmallSourceLanguageSetTests { + @Test + func testBehavesSameAsSet() { + var tiny = SmallSourceLanguageSet() + var real = Set() + + #expect(tiny.isEmpty == real.isEmpty) + #expect(tiny.count == real.count) + #expect(tiny.min() == real.min()) + #expect(tiny.first == real.first) + for language in SourceLanguage.knownLanguages { + #expect(tiny.contains(language) == real.contains(language)) + } + + // Add known languages + #expect(tiny.insert(.swift) == real.insert(.swift)) + #expect(tiny.insert(.swift) == real.insert(.swift)) + #expect(tiny.insert(.objectiveC) == real.insert(.objectiveC)) + #expect(tiny.remove(.swift) == real.remove(.swift)) + #expect(tiny.remove(.swift) == real.remove(.swift)) + #expect(tiny.update(with: .swift) == real.update(with: .swift)) + + #expect(tiny.update(with: .swift) == real.update(with: .swift)) + #expect(tiny.update(with: .objectiveC) == real.update(with: .objectiveC)) + #expect(tiny.update(with: .data) == real.update(with: .data)) + + #expect(tiny.isEmpty == real.isEmpty) + #expect(tiny.count == real.count) + #expect(tiny.min() == real.min()) + for language in SourceLanguage.knownLanguages { + #expect(tiny.contains(language) == real.contains(language)) + } + + // Add unknown languages + for language in [ + SourceLanguage(name: "Custom"), + SourceLanguage(name: "AAA", id: "zzz" /* will sort last */), + SourceLanguage(name: "ZZZ", id: "aaa" /* will sort first (after Swift) */), + ] { + #expect(tiny.update(with: language) == real.update(with: language)) + #expect(tiny.contains(language) == real.contains(language)) + #expect(tiny.remove(language) == real.remove(language)) + #expect(tiny.remove(language) == real.remove(language)) + #expect(tiny.contains(language) == real.contains(language)) + #expect(tiny.insert(language) == real.insert(language)) + #expect(tiny.contains(language) == real.contains(language)) + #expect(tiny.update(with: language) == real.update(with: language)) + #expect(tiny.contains(language) == real.contains(language)) + } + + #expect(tiny.isEmpty == real.isEmpty) + #expect(tiny.count == real.count) + #expect(tiny.min() == real.min()) + for language in SourceLanguage.knownLanguages { + #expect(tiny.contains(language) == real.contains(language)) + } + + // Set operations + #expect(real.intersection([]) == []) + #expect(tiny.intersection([]) == []) + #expect(real.union([]) == real) + #expect(tiny.union([]) == tiny) + #expect(real.symmetricDifference([]) == real) + #expect(tiny.symmetricDifference([]) == tiny) + + #expect( real.intersection(Set( SourceLanguage.knownLanguages)) + == Set(tiny.intersection(SmallSourceLanguageSet(SourceLanguage.knownLanguages)) )) + #expect( real.union(Set( SourceLanguage.knownLanguages)) + == Set(tiny.union(SmallSourceLanguageSet(SourceLanguage.knownLanguages)) )) + #expect( real.symmetricDifference(Set( SourceLanguage.knownLanguages)) + == Set(tiny.symmetricDifference(SmallSourceLanguageSet(SourceLanguage.knownLanguages)) )) + } + + @Test + func testSortsSwiftFirstAndThenByID() { + var languages = SmallSourceLanguageSet(SourceLanguage.knownLanguages) + #expect(languages.min()?.name == "Swift") + #expect(languages.count == 5) + #expect(languages.sorted().map(\.name) == [ + "Swift", // swift (always first) + "Data", // data + "JavaScript", // javascript + "Metal", // metal + "Objective-C", // occ + ]) + + for language in SourceLanguage.knownLanguages { + #expect(languages.insert(language).inserted == false) + } + + // Add unknown languages + #expect(languages.insert(SourceLanguage(name: "Custom")).inserted == true) + #expect(languages.insert(SourceLanguage(name: "AAA", id: "zzz" /* will sort last */)).inserted == true) + #expect(languages.insert(SourceLanguage(name: "ZZZ", id: "aaa" /* will sort first (after Swift) */)).inserted == true) + + #expect(languages.min()?.name == "Swift") + #expect(languages.count == 8) + #expect(languages.sorted().map(\.name) == [ + "Swift", // swift (always first) + "ZZZ", // aaa (the AAA/zzz and ZZZ/aaa languages have their names and ids flipped to verify that sorting happens by id) + "Custom", // custom + "Data", // data + "JavaScript", // javascript + "Metal", // metal + "Objective-C", // occ + "AAA", // zzz (the AAA/zzz and ZZZ/aaa languages have their names and ids flipped to verify that sorting happens by id) + ]) + + for language in SourceLanguage.knownLanguages { + #expect(languages.remove(language) != nil) + #expect(languages.remove(language) == nil) + } + + #expect(languages.min()?.name == "ZZZ") + #expect(languages.count == 3) + #expect(languages.sorted().map(\.name) == [ + "ZZZ", // aaa (the AAA/zzz and ZZZ/aaa languages have their names and ids flipped to verify that sorting happens by id) + "Custom", // custom + "AAA", // zzz (the AAA/zzz and ZZZ/aaa languages have their names and ids flipped to verify that sorting happens by id) + ]) + + languages.insert(.swift) + + #expect(languages.min()?.name == "Swift") + #expect(languages.count == 4) + #expect(languages.sorted().map(\.name) == [ + "Swift", // swift (always first) + "ZZZ", // aaa (the AAA/zzz and ZZZ/aaa languages have their names and ids flipped to verify that sorting happens by id) + "Custom", // custom + "AAA", // zzz (the AAA/zzz and ZZZ/aaa languages have their names and ids flipped to verify that sorting happens by id) + ]) + } + + @Test + func testIsSameSizeAsUInt64() { + #expect(MemoryLayout.size == MemoryLayout.size) + #expect(MemoryLayout.stride == MemoryLayout.stride) + #expect(MemoryLayout.alignment == MemoryLayout.alignment) + } +} diff --git a/Tests/DocCCommonTests/SourceLanguageTests.swift b/Tests/DocCCommonTests/SourceLanguageTests.swift new file mode 100644 index 0000000000..111c79e6e5 --- /dev/null +++ b/Tests/DocCCommonTests/SourceLanguageTests.swift @@ -0,0 +1,155 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022-2025 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 DocCCommon +import Testing +import Foundation + +struct SourceLanguageTests { + @Test(arguments: SourceLanguage.knownLanguages) + func testUsesIDAliasesWhenQueryingFirstKnownLanguage(_ language: SourceLanguage) { + #expect(SourceLanguage(id: language.id) == language) + for alias in language.idAliases { + #expect(SourceLanguage(id: alias) == language, "Unexpectedly found different language for id alias '\(alias)'") + } + } + + // This test uses mutating SourceLanguage properties which is deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) + @Test + func testHasValueSemanticsForBothKnownAndUnknownLanguages() throws { + var original = SourceLanguage.swift + var copy = original + copy.name = "First" + #expect(copy.name == "First", "The copy has a modified value") + #expect(original.name == "Swift", "Modifying one value doesn't change the original") + + try assertRoundTripCoding(original) + try assertRoundTripCoding(copy) + + original = .init(name: "Custom", id: "custom") + copy = original + copy.name = "Second" + #expect(copy.name == "Second", "The copy has a modified value") + #expect(original.name == "Custom", "Modifying one value doesn't change the original") + + try assertRoundTripCoding(original) + try assertRoundTripCoding(copy) + } + + @Test + func testReusesExistingValuesWhenCreatingLanguages() throws { + // Creating more than 256 languages would fail if SourceLanguage initializer didn't reuse existing values + let numberOfIterations = 300 // anything more than `UInt8.max` + + for _ in 0...numberOfIterations { + let knownLanguageByID = SourceLanguage(id: "swift") + try assertRoundTripCoding(knownLanguageByID) + #expect(knownLanguageByID.id == "swift") + } + + for _ in 0...numberOfIterations { + let knownLanguageWithAllInfo = SourceLanguage(name: "Swift", id: "swift", idAliases: [], linkDisambiguationID: nil) + try assertRoundTripCoding(knownLanguageWithAllInfo) + #expect(knownLanguageWithAllInfo.id == "swift") + } + + for _ in 0...numberOfIterations { + let knownLanguageByName = SourceLanguage(name: "Swift") + try assertRoundTripCoding(knownLanguageByName) + #expect(knownLanguageByName.id == "swift") + } + + for _ in 0...numberOfIterations { + let unknownLanguage = SourceLanguage(name: "Custom") + try assertRoundTripCoding(unknownLanguage) + #expect(unknownLanguage.id == "custom") + } + + for _ in 0...numberOfIterations { + let unknownLanguageWithAllInfo = SourceLanguage(name: "Custom", id: "custom", idAliases: ["other", "preferred"], linkDisambiguationID: "preferred") + try assertRoundTripCoding(unknownLanguageWithAllInfo) + #expect(unknownLanguageWithAllInfo.name == "Custom") + #expect(unknownLanguageWithAllInfo.id == "custom") + #expect(unknownLanguageWithAllInfo.idAliases == ["other", "preferred"]) + #expect(unknownLanguageWithAllInfo.linkDisambiguationID == "preferred") + } + } + + // This test uses mutating SourceLanguage properties which is deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) + @Test + func testReusesExistingValuesModifyingProperties() { + // Creating more than 256 languages would fail if SourceLanguage initializer didn't reuse existing values + let numberOfIterations = 300 // anything more than `UInt8.max` + + var language = SourceLanguage.swift + for iteration in 0...numberOfIterations { + language.name = iteration.isMultiple(of: 2) ? "Even" : "Odd" + } + } + + @Test(arguments: [ + (SourceLanguage.swift, "Swift"), + (SourceLanguage.objectiveC, "Objective-C"), + (SourceLanguage.data, "Data"), + (SourceLanguage.javaScript, "JavaScript"), + (SourceLanguage.metal, "Metal"), + ]) + func testNameOfKnownLanguage(language: SourceLanguage, matches expectedName: String) { + // Known languages have their own dedicated implementation that requires two implementation detail values to be consistent. + #expect(language.name == expectedName) + } + + @Test + func testSortsSwiftFirstAndThenByID() throws { + var languages = SourceLanguage.knownLanguages + #expect(languages.min()?.name == "Swift") + #expect(languages.sorted().map(\.name) == [ + "Swift", // swift (always first) + "Data", // data + "JavaScript", // javascript + "Metal", // metal + "Objective-C", // occ + ]) + + languages.append(contentsOf: [ + SourceLanguage(name: "Custom"), + SourceLanguage(name: "AAA", id: "zzz"), // will sort last + SourceLanguage(name: "ZZZ", id: "aaa"), // will sort first (after Swift) + ]) + #expect(languages.min()?.name == "Swift") + #expect(languages.sorted().map(\.name) == [ + "Swift", // swift (always first) + "ZZZ", // aaa (the AAA/zzz and ZZZ/aaa languages have their names and ids flipped to verify that sorting happens by id) + "Custom", // custom + "Data", // data + "JavaScript", // javascript + "Metal", // metal + "Objective-C", // occ + "AAA", // zzz (the AAA/zzz and ZZZ/aaa languages have their names and ids flipped to verify that sorting happens by id) + ]) + } + + private func assertRoundTripCoding(_ original: SourceLanguage, sourceLocation: SourceLocation = #_sourceLocation) throws { + let encoded = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(SourceLanguage.self, from: encoded) + // Check that both values are equal + #expect(original == decoded, sourceLocation: sourceLocation) + + // Also check that all their properties are equal + #expect(original.id == decoded.id, sourceLocation: sourceLocation) + #expect(original.name == decoded.name, sourceLocation: sourceLocation) + #expect(original.idAliases == decoded.idAliases, sourceLocation: sourceLocation) + #expect(original.linkDisambiguationID == decoded.linkDisambiguationID, sourceLocation: sourceLocation) + } +} diff --git a/Tests/SwiftDocCTests/Benchmark/ExternalTopicsHashTests.swift b/Tests/SwiftDocCTests/Benchmark/ExternalTopicsHashTests.swift index 20c36ce3f7..ee391de385 100644 --- a/Tests/SwiftDocCTests/Benchmark/ExternalTopicsHashTests.swift +++ b/Tests/SwiftDocCTests/Benchmark/ExternalTopicsHashTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -22,24 +22,21 @@ class ExternalTopicsGraphHashTests: XCTestCase { func symbolReferenceAndEntity(withPreciseIdentifier preciseIdentifier: String) -> (ResolvedTopicReference, LinkResolver.ExternalEntity)? { let reference = ResolvedTopicReference(bundleID: "com.test.symbols", path: "/\(preciseIdentifier)", sourceLanguage: SourceLanguage.swift) let entity = LinkResolver.ExternalEntity( - topicRenderReference: TopicRenderReference( - identifier: .init(preciseIdentifier), - title: preciseIdentifier, - abstract: [], - url: "/" + preciseIdentifier, - kind: .symbol, - estimatedTime: nil - ), - renderReferenceDependencies: .init(), - sourceLanguages: [.swift] + kind: .class, + language: .swift, + relativePresentationURL: URL(string: "/\(preciseIdentifier)")!, + referenceURL: reference.url, + title: preciseIdentifier, + availableLanguages: [.swift], + variants: [] ) return (reference, entity) } } - func testNoMetricAddedIfNoExternalTopicsAreResolved() throws { + func testNoMetricAddedIfNoExternalTopicsAreResolved() async throws { // Load bundle without using external resolvers - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") XCTAssertTrue(context.externallyResolvedLinks.isEmpty) // Try adding external topics metrics @@ -50,12 +47,12 @@ class ExternalTopicsGraphHashTests: XCTestCase { XCTAssertNil(testBenchmark.metrics.first?.result, "Metric was added but there was no external links or symbols") } - func testExternalLinksSameHash() throws { + func testExternalLinksSameHash() async throws { let externalResolver = self.externalResolver // Add external links and verify the checksum is always the same - let hashes: [String] = try (0...10).map { _ -> MetricValue? in - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) { url in + func computeTopicHash(file: StaticString = #filePath, line: UInt = #line) async throws -> String { + let (_, _, context) = try await self.testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) { url in try """ # ``SideKit/SideClass`` @@ -74,26 +71,24 @@ class ExternalTopicsGraphHashTests: XCTestCase { let testBenchmark = Benchmark() benchmark(add: Benchmark.ExternalTopicsHash(context: context), benchmarkLog: testBenchmark) - // Verify that a metric was added - XCTAssertNotNil(testBenchmark.metrics[0].result) - return testBenchmark.metrics[0].result - } - .compactMap { value -> String? in - guard let value, - case MetricValue.checksum(let hash) = value else { return nil } - return hash + return try TopicAnchorHashTests.extractChecksumHash(from: testBenchmark) } + let expectedHash = try await computeTopicHash() + // Verify the produced topic graph hash is repeatedly the same - XCTAssertTrue(hashes.allSatisfy({ $0 == hashes.first })) + for _ in 0 ..< 10 { + let hash = try await computeTopicHash() + XCTAssertEqual(hash, expectedHash) + } } - func testLinksAndSymbolsSameHash() throws { + func testLinksAndSymbolsSameHash() async throws { let externalResolver = self.externalResolver // Add external links and verify the checksum is always the same - let hashes: [String] = try (0...10).map { _ -> MetricValue? in - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver], externalSymbolResolver: externalSymbolResolver) { url in + func computeTopicHash(file: StaticString = #filePath, line: UInt = #line) async throws -> String { + let (_, _, context) = try await self.testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver], externalSymbolResolver: self.externalSymbolResolver) { url in try """ # ``SideKit/SideClass`` @@ -113,25 +108,23 @@ class ExternalTopicsGraphHashTests: XCTestCase { let testBenchmark = Benchmark() benchmark(add: Benchmark.ExternalTopicsHash(context: context), benchmarkLog: testBenchmark) - // Verify that a metric was added - XCTAssertNotNil(testBenchmark.metrics[0].result) - return testBenchmark.metrics[0].result - } - .compactMap { value -> String? in - guard let value, - case MetricValue.checksum(let hash) = value else { return nil } - return hash + return try TopicAnchorHashTests.extractChecksumHash(from: testBenchmark) } + let expectedHash = try await computeTopicHash() + // Verify the produced topic graph hash is repeatedly the same - XCTAssertTrue(hashes.allSatisfy({ $0 == hashes.first })) + for _ in 0 ..< 10 { + let hash = try await computeTopicHash() + XCTAssertEqual(hash, expectedHash) + } } - func testExternalTopicsDetectsChanges() throws { + func testExternalTopicsDetectsChanges() async throws { let externalResolver = self.externalResolver // Load a bundle with external links - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) { url in try """ # ``SideKit/SideClass`` diff --git a/Tests/SwiftDocCTests/Benchmark/TopicAnchorHashTests.swift b/Tests/SwiftDocCTests/Benchmark/TopicAnchorHashTests.swift index 682dd1429a..72f714b752 100644 --- a/Tests/SwiftDocCTests/Benchmark/TopicAnchorHashTests.swift +++ b/Tests/SwiftDocCTests/Benchmark/TopicAnchorHashTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -12,26 +12,28 @@ import XCTest @testable import SwiftDocC class TopicAnchorHashTests: XCTestCase { - func testAnchorSectionsHash() throws { - let hashes: [String] = try (0...10).map { _ -> MetricValue? in - let (_, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + func testAnchorSectionsHash() async throws { + func computeTopicHash(file: StaticString = #filePath, line: UInt = #line) async throws -> String { + let (_, context) = try await self.testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") let testBenchmark = Benchmark() benchmark(add: Benchmark.TopicAnchorHash(context: context), benchmarkLog: testBenchmark) - return testBenchmark.metrics[0].result - } - .compactMap { value -> String? in - guard case MetricValue.checksum(let hash)? = value else { return nil } - return hash + + return try Self.extractChecksumHash(from: testBenchmark) } + + let expectedHash = try await computeTopicHash() // Verify the produced topic graph hash is repeatedly the same - XCTAssertTrue(hashes.allSatisfy({ $0 == hashes.first })) + for _ in 0 ..< 10 { + let hash = try await computeTopicHash() + XCTAssertEqual(hash, expectedHash) + } } - func testTopicAnchorsChangedHash() throws { + func testTopicAnchorsChangedHash() async throws { // Verify that the hash changes if we change the topic graph let initialHash: String - let (_, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + let (_, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") do { let testBenchmark = Benchmark() @@ -70,4 +72,17 @@ class TopicAnchorHashTests: XCTestCase { XCTAssertNotEqual(initialHash, modifiedHash) } + static func extractChecksumHash( + from benchmark: Benchmark, + file: StaticString = #filePath, + line: UInt = #line + ) throws -> String { + let hash: String? = switch benchmark.metrics[0].result { + case .checksum(let hash): + hash + default: + nil + } + return try XCTUnwrap(hash, file: file, line: line) + } } diff --git a/Tests/SwiftDocCTests/Benchmark/TopicGraphHashTests.swift b/Tests/SwiftDocCTests/Benchmark/TopicGraphHashTests.swift index d4b5ecbc50..4204122882 100644 --- a/Tests/SwiftDocCTests/Benchmark/TopicGraphHashTests.swift +++ b/Tests/SwiftDocCTests/Benchmark/TopicGraphHashTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -12,27 +12,28 @@ import XCTest @testable import SwiftDocC class TopicGraphHashTests: XCTestCase { - func testTopicGraphSameHash() throws { - let hashes: [String] = try (0...10).map { _ -> MetricValue? in - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testTopicGraphSameHash() async throws { + func computeTopicHash(file: StaticString = #filePath, line: UInt = #line) async throws -> String { + let (_, context) = try await self.testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let testBenchmark = Benchmark() benchmark(add: Benchmark.TopicGraphHash(context: context), benchmarkLog: testBenchmark) - return testBenchmark.metrics[0].result - } - .compactMap { value -> String? in - guard let value, - case MetricValue.checksum(let hash) = value else { return nil } - return hash + + return try TopicAnchorHashTests.extractChecksumHash(from: testBenchmark) } + + let expectedHash = try await computeTopicHash() // Verify the produced topic graph hash is repeatedly the same - XCTAssertTrue(hashes.allSatisfy({ $0 == hashes.first })) + for _ in 0 ..< 10 { + let hash = try await computeTopicHash() + XCTAssertEqual(hash, expectedHash) + } } - func testTopicGraphChangedHash() throws { + func testTopicGraphChangedHash() async throws { // Verify that the hash changes if we change the topic graph let initialHash: String - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") do { let testBenchmark = Benchmark() @@ -78,7 +79,7 @@ class TopicGraphHashTests: XCTestCase { /// Verify that we safely produce the topic graph hash when external symbols /// participate in the documentation hierarchy. rdar://76419740 - func testProducesTopicGraphHashWhenResolvedExternalReferencesInTaskGroups() throws { + func testProducesTopicGraphHashWhenResolvedExternalReferencesInTaskGroups() async throws { let resolver = TestMultiResultExternalReferenceResolver() resolver.entitiesToReturn = [ "/article": .success(.init(referencePath: "/externally/resolved/path/to/article")), @@ -88,7 +89,7 @@ class TopicGraphHashTests: XCTestCase { "/externally/resolved/path/to/article2": .success(.init(referencePath: "/externally/resolved/path/to/article2")), ] - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [ + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [ "com.external.testbundle" : resolver ]) { url in // Add external links to the MyKit Topics. diff --git a/Tests/SwiftDocCTests/Catalog Processing/GeneratedCurationWriterTests.swift b/Tests/SwiftDocCTests/Catalog Processing/GeneratedCurationWriterTests.swift index 472e25e912..a158e95440 100644 --- a/Tests/SwiftDocCTests/Catalog Processing/GeneratedCurationWriterTests.swift +++ b/Tests/SwiftDocCTests/Catalog Processing/GeneratedCurationWriterTests.swift @@ -14,8 +14,8 @@ import XCTest class GeneratedCurationWriterTests: XCTestCase { private let testOutputURL = URL(fileURLWithPath: "/unit-test/output-dir") // Nothing is written to this path in this test - func testWriteTopLevelSymbolCuration() throws { - let (url, _, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testWriteTopLevelSymbolCuration() async throws { + let (url, _, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let writer = try XCTUnwrap(GeneratedCurationWriter(context: context, catalogURL: url, outputURL: testOutputURL)) let contentsToWrite = try writer.generateDefaultCurationContents(depthLimit: 0) @@ -110,8 +110,8 @@ class GeneratedCurationWriterTests: XCTestCase { """) } - func testWriteSymbolCurationFromTopLevelSymbol() throws { - let (url, _, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testWriteSymbolCurationFromTopLevelSymbol() async throws { + let (url, _, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let writer = try XCTUnwrap(GeneratedCurationWriter(context: context, catalogURL: url, outputURL: testOutputURL)) @@ -141,8 +141,8 @@ class GeneratedCurationWriterTests: XCTestCase { """) } - func testWriteSymbolCurationWithLimitedDepth() throws { - let (url, _, context) = try testBundleAndContext(named: "BundleWithSameNameForSymbolAndContainer") + func testWriteSymbolCurationWithLimitedDepth() async throws { + let (url, _, context) = try await testBundleAndContext(named: "BundleWithSameNameForSymbolAndContainer") let writer = try XCTUnwrap(GeneratedCurationWriter(context: context, catalogURL: url, outputURL: testOutputURL)) let depthLevelsToTest = [nil, 0, 1, 2, 3, 4, 5] @@ -252,8 +252,8 @@ class GeneratedCurationWriterTests: XCTestCase { } } - func testSkipsManuallyCuratedPages() throws { - let (url, _, context) = try testBundleAndContext(named: "MixedManualAutomaticCuration") + func testSkipsManuallyCuratedPages() async throws { + let (url, _, context) = try await testBundleAndContext(named: "MixedManualAutomaticCuration") let writer = try XCTUnwrap(GeneratedCurationWriter(context: context, catalogURL: url, outputURL: testOutputURL)) let contentsToWrite = try writer.generateDefaultCurationContents() @@ -282,8 +282,8 @@ class GeneratedCurationWriterTests: XCTestCase { """) } - func testAddsCommentForDisambiguatedLinks() throws { - let (url, _, context) = try testBundleAndContext(named: "OverloadedSymbols") + func testAddsCommentForDisambiguatedLinks() async throws { + let (url, _, context) = try await testBundleAndContext(named: "OverloadedSymbols") let writer = try XCTUnwrap(GeneratedCurationWriter(context: context, catalogURL: url, outputURL: testOutputURL)) let contentsToWrite = try writer.generateDefaultCurationContents(fromSymbol: "OverloadedProtocol") @@ -308,8 +308,8 @@ class GeneratedCurationWriterTests: XCTestCase { """) } - func testLinksSupportNonPathCharacters() throws { - let (url, _, context) = try testBundleAndContext(named: "InheritedOperators") + func testLinksSupportNonPathCharacters() async throws { + let (url, _, context) = try await testBundleAndContext(named: "InheritedOperators") let writer = try XCTUnwrap(GeneratedCurationWriter(context: context, catalogURL: url, outputURL: testOutputURL)) let contentsToWrite = try writer.generateDefaultCurationContents(fromSymbol: "MyNumber") @@ -346,8 +346,8 @@ class GeneratedCurationWriterTests: XCTestCase { """) } - func testGeneratingLanguageSpecificCuration() throws { - let (url, _, context) = try testBundleAndContext(named: "GeometricalShapes") + func testGeneratingLanguageSpecificCuration() async throws { + let (url, _, context) = try await testBundleAndContext(named: "GeometricalShapes") let writer = try XCTUnwrap(GeneratedCurationWriter(context: context, catalogURL: url, outputURL: testOutputURL)) let contentsToWrite = try writer.generateDefaultCurationContents() @@ -439,8 +439,8 @@ class GeneratedCurationWriterTests: XCTestCase { } - func testCustomOutputLocation() throws { - let (url, _, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testCustomOutputLocation() async throws { + let (url, _, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let writer = try XCTUnwrap(GeneratedCurationWriter(context: context, catalogURL: url, outputURL: testOutputURL)) let contentsToWrite = try writer.generateDefaultCurationContents() diff --git a/Tests/SwiftDocCTests/Checker/Checkers/AbstractContainsFormattedTextOnlyTests.swift b/Tests/SwiftDocCTests/Checker/Checkers/AbstractContainsFormattedTextOnlyTests.swift index 6add5371de..958f31c3db 100644 --- a/Tests/SwiftDocCTests/Checker/Checkers/AbstractContainsFormattedTextOnlyTests.swift +++ b/Tests/SwiftDocCTests/Checker/Checkers/AbstractContainsFormattedTextOnlyTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -12,6 +12,9 @@ import XCTest @testable import SwiftDocC import Markdown +// This tests `AbstractContainsFormattedTextOnly` which are deprecated. +// Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. +@available(*, deprecated) class AbstractContainsFormattedTextOnlyTests: XCTestCase { var checker = AbstractContainsFormattedTextOnly(sourceFile: nil) diff --git a/Tests/SwiftDocCTests/Checker/Checkers/InvalidCodeBlockOptionTests.swift b/Tests/SwiftDocCTests/Checker/Checkers/InvalidCodeBlockOptionTests.swift new file mode 100644 index 0000000000..e67b5d8ff7 --- /dev/null +++ b/Tests/SwiftDocCTests/Checker/Checkers/InvalidCodeBlockOptionTests.swift @@ -0,0 +1,161 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 XCTest +@testable import SwiftDocC +import Markdown + +class InvalidCodeBlockOptionTests: XCTestCase { + + func testNoOptions() { + let markupSource = """ +``` +let a = 1 +``` +""" + let document = Document(parsing: markupSource, options: []) + var checker = InvalidCodeBlockOption(sourceFile: nil) + checker.visit(document) + XCTAssertTrue(checker.problems.isEmpty) + } + + func testOption() { + let markupSource = """ +```nocopy +let a = 1 +``` +""" + let document = Document(parsing: markupSource, options: []) + var checker = InvalidCodeBlockOption(sourceFile: nil) + checker.visit(document) + XCTAssertTrue(checker.problems.isEmpty) + } + + func testMultipleOptionTypos() { + let markupSource = """ +```nocoy +let b = 2 +``` + +```nocoy +let c = 3 +``` +""" + let document = Document(parsing: markupSource, options: []) + var checker = InvalidCodeBlockOption(sourceFile: URL(fileURLWithPath: #file)) + checker.visit(document) + XCTAssertEqual(2, checker.problems.count) + + for problem in checker.problems { + XCTAssertEqual("org.swift.docc.InvalidCodeBlockOption", problem.diagnostic.identifier) + XCTAssertEqual(problem.diagnostic.summary, "Unknown option 'nocoy' in code block.") + XCTAssertEqual(problem.possibleSolutions.map(\.summary), ["Replace 'nocoy' with 'nocopy'."]) + } + } + + func testOptionDifferentTypos() throws { + let markupSource = """ +```swift, nocpy +let d = 4 +``` + +```haskell, nocpoy +let e = 5 +``` + +```nocopy +let f = 6 +``` + +```ncopy +let g = 7 +``` +""" + let document = Document(parsing: markupSource, options: []) + var checker = InvalidCodeBlockOption(sourceFile: URL(fileURLWithPath: #file)) + checker.visit(document) + + XCTAssertEqual(3, checker.problems.count) + + let summaries = checker.problems.map { $0.diagnostic.summary } + XCTAssertEqual(summaries, [ + "Unknown option 'nocpy' in code block.", + "Unknown option 'nocpoy' in code block.", + "Unknown option 'ncopy' in code block.", + ]) + + for problem in checker.problems { + XCTAssertEqual( + "org.swift.docc.InvalidCodeBlockOption", + problem.diagnostic.identifier + ) + + XCTAssertEqual(problem.possibleSolutions.count, 1) + let solution = try XCTUnwrap(problem.possibleSolutions.first) + XCTAssert(solution.summary.hasSuffix("with 'nocopy'.")) + + } + } + + func testLanguageNotFirst() { + let markupSource = """ +```nocopy, swift, highlight=[1] +let b = 2 +``` +""" + let document = Document(parsing: markupSource, options: []) + var checker = InvalidCodeBlockOption(sourceFile: URL(fileURLWithPath: #file)) + checker.visit(document) + XCTAssertEqual(1, checker.problems.count) + + for problem in checker.problems { + XCTAssertEqual("org.swift.docc.InvalidCodeBlockOption", problem.diagnostic.identifier) + XCTAssertEqual(problem.diagnostic.summary, "Unknown option 'swift' in code block.") + XCTAssertEqual(problem.possibleSolutions.map(\.summary), ["If 'swift' is the language for this code block, then write 'swift' as the first option."]) + } + } + + func testInvalidHighlightIndex() throws { + let markupSource = """ +```swift, nocopy, highlight=[2] +let b = 2 +``` +""" + let document = Document(parsing: markupSource, options: []) + var checker = InvalidCodeBlockOption(sourceFile: URL(fileURLWithPath: #file)) + checker.visit(document) + XCTAssertEqual(1, checker.problems.count) + let problem = try XCTUnwrap(checker.problems.first) + + XCTAssertEqual("org.swift.docc.InvalidCodeBlockOption", problem.diagnostic.identifier) + XCTAssertEqual(problem.diagnostic.summary, "Invalid 'highlight' index in '[2]' for a code block with 1 line. Valid range is 1...1.") + XCTAssertEqual(problem.possibleSolutions.map(\.summary), ["If you intended the last line, change '2' to 1."]) + } + + func testInvalidHighlightandStrikeoutIndex() throws { + let markupSource = """ +```swift, nocopy, highlight=[0], strikeout=[-1, 4] +let a = 1 +let b = 2 +let c = 3 +``` +""" + let document = Document(parsing: markupSource, options: []) + var checker = InvalidCodeBlockOption(sourceFile: URL(fileURLWithPath: #file)) + checker.visit(document) + XCTAssertEqual(2, checker.problems.count) + + XCTAssertEqual("org.swift.docc.InvalidCodeBlockOption", checker.problems[0].diagnostic.identifier) + XCTAssertEqual(checker.problems[0].diagnostic.summary, "Invalid 'highlight' index in '[0]' for a code block with 3 lines. Valid range is 1...3.") + XCTAssertEqual(checker.problems[1].diagnostic.summary, "Invalid 'strikeout' indexes in '[-1, 4]' for a code block with 3 lines. Valid range is 1...3.") + XCTAssertEqual(checker.problems[1].possibleSolutions.map(\.summary), ["If you intended the last line, change '4' to 3."]) + } +} + diff --git a/Tests/SwiftDocCTests/Checker/Checkers/NonInclusiveLanguageCheckerTests.swift b/Tests/SwiftDocCTests/Checker/Checkers/NonInclusiveLanguageCheckerTests.swift index d57d9e7688..1073bc693e 100644 --- a/Tests/SwiftDocCTests/Checker/Checkers/NonInclusiveLanguageCheckerTests.swift +++ b/Tests/SwiftDocCTests/Checker/Checkers/NonInclusiveLanguageCheckerTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2022 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -177,17 +177,17 @@ func aBlackListedFunc() { - item three """ - func testDisabledByDefault() throws { + func testDisabledByDefault() async throws { // Create a test bundle with some non-inclusive content. let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "Root.md", utf8Content: nonInclusiveContent) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.count, 0) // Non-inclusive content is an info-level diagnostic, so it's filtered out. } - func testEnablingTheChecker() throws { + func testEnablingTheChecker() async throws { // The expectations of the checker being run, depending on the diagnostic level // set to to the documentation context for the compilation. let expectations: [(DiagnosticSeverity, Bool)] = [ @@ -203,7 +203,7 @@ func aBlackListedFunc() { ]) var configuration = DocumentationContext.Configuration() configuration.externalMetadata.diagnosticLevel = severity - let (_, context) = try loadBundle(catalog: catalog, diagnosticEngine: .init(filterLevel: severity), configuration: configuration) + let (_, context) = try await loadBundle(catalog: catalog, diagnosticFilterLevel: severity, configuration: configuration) // Verify that checker diagnostics were emitted or not, depending on the diagnostic level set. XCTAssertEqual(context.problems.contains(where: { $0.diagnostic.identifier == "org.swift.docc.NonInclusiveLanguage" }), enabled) diff --git a/Tests/SwiftDocCTests/Converter/DocumentationContextConverterTests.swift b/Tests/SwiftDocCTests/Converter/DocumentationContextConverterTests.swift index f5200edd8e..d9b63dfee5 100644 --- a/Tests/SwiftDocCTests/Converter/DocumentationContextConverterTests.swift +++ b/Tests/SwiftDocCTests/Converter/DocumentationContextConverterTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -13,15 +13,15 @@ import XCTest @testable import SwiftDocC class DocumentationContextConverterTests: XCTestCase { - func testRenderNodesAreIdentical() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testRenderNodesAreIdentical() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // We'll use this to convert nodes ad-hoc - let perNodeConverter = DocumentationNodeConverter(bundle: bundle, context: context) + let perNodeConverter = DocumentationNodeConverter(context: context) // We'll use these to convert nodes in bulk - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let bulkNodeConverter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + let renderContext = RenderContext(documentationContext: context) + let bulkNodeConverter = DocumentationContextConverter(context: context, renderContext: renderContext) let encoder = JSONEncoder() for identifier in context.knownPages { @@ -40,9 +40,9 @@ class DocumentationContextConverterTests: XCTestCase { } } - func testSymbolLocationsAreOnlyIncludedWhenRequested() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let renderContext = RenderContext(documentationContext: context, bundle: bundle) + func testSymbolLocationsAreOnlyIncludedWhenRequested() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let renderContext = RenderContext(documentationContext: context) let fillIntroducedSymbolNode = try XCTUnwrap( context.documentationCache["s:14FillIntroduced19macOSOnlyDeprecatedyyF"] @@ -50,7 +50,6 @@ class DocumentationContextConverterTests: XCTestCase { do { let documentationContextConverter = DocumentationContextConverter( - bundle: bundle, context: context, renderContext: renderContext, emitSymbolSourceFileURIs: true) @@ -61,7 +60,6 @@ class DocumentationContextConverterTests: XCTestCase { do { let documentationContextConverter = DocumentationContextConverter( - bundle: bundle, context: context, renderContext: renderContext) @@ -70,9 +68,9 @@ class DocumentationContextConverterTests: XCTestCase { } } - func testSymbolAccessLevelsAreOnlyIncludedWhenRequested() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let renderContext = RenderContext(documentationContext: context, bundle: bundle) + func testSymbolAccessLevelsAreOnlyIncludedWhenRequested() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let renderContext = RenderContext(documentationContext: context) let fillIntroducedSymbolNode = try XCTUnwrap( context.documentationCache["s:14FillIntroduced19macOSOnlyDeprecatedyyF"] @@ -80,7 +78,6 @@ class DocumentationContextConverterTests: XCTestCase { do { let documentationContextConverter = DocumentationContextConverter( - bundle: bundle, context: context, renderContext: renderContext, emitSymbolAccessLevels: true @@ -92,7 +89,6 @@ class DocumentationContextConverterTests: XCTestCase { do { let documentationContextConverter = DocumentationContextConverter( - bundle: bundle, context: context, renderContext: renderContext) diff --git a/Tests/SwiftDocCTests/Converter/DocumentationConverterTests.swift b/Tests/SwiftDocCTests/Converter/DocumentationConverterTests.swift deleted file mode 100644 index 6687305477..0000000000 --- a/Tests/SwiftDocCTests/Converter/DocumentationConverterTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2024 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 XCTest -@testable import SwiftDocC - -// This test verifies the behavior of `DocumentationConverter` which is a deprecated type. -// Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -class DocumentationConverterTests: XCTestCase { - /// An empty implementation of `ConvertOutputConsumer` that purposefully does nothing. - struct EmptyConvertOutputConsumer: ConvertOutputConsumer { - func consume(renderNode: RenderNode) throws { } - func consume(problems: [Problem]) throws { } - func consume(assetsInBundle bundle: DocumentationBundle) throws {} - func consume(linkableElementSummaries: [LinkDestinationSummary]) throws {} - func consume(indexingRecords: [IndexingRecord]) throws {} - func consume(assets: [RenderReferenceType: [any RenderReference]]) throws {} - func consume(benchmarks: Benchmark) throws {} - func consume(documentationCoverageInfo: [CoverageDataEntry]) throws {} - } - - func testThrowsErrorOnConvertingNoBundles() throws { - let rootURL = try createTemporaryDirectory() - - let dataProvider = try LocalFileSystemDataProvider(rootURL: rootURL) - let workspace = DocumentationWorkspace() - try workspace.registerProvider(dataProvider) - let context = try DocumentationContext(dataProvider: workspace) - var converter = DocumentationConverter(documentationBundleURL: rootURL, emitDigest: false, documentationCoverageOptions: .noCoverage, currentPlatforms: nil, workspace: workspace, context: context, dataProvider: dataProvider, bundleDiscoveryOptions: BundleDiscoveryOptions()) - XCTAssertThrowsError(try converter.convert(outputConsumer: EmptyConvertOutputConsumer())) { error in - let converterError = try? XCTUnwrap(error as? DocumentationConverter.Error) - XCTAssertEqual(converterError, DocumentationConverter.Error.doesNotContainBundle(url: rootURL)) - } - } -} diff --git a/Tests/SwiftDocCTests/Converter/RenderContextTests.swift b/Tests/SwiftDocCTests/Converter/RenderContextTests.swift index 786b321da4..3d9805df44 100644 --- a/Tests/SwiftDocCTests/Converter/RenderContextTests.swift +++ b/Tests/SwiftDocCTests/Converter/RenderContextTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -13,10 +13,9 @@ import XCTest @testable import SwiftDocC class RenderContextTests: XCTestCase { - func testCreatesRenderReferences() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - - let renderContext = RenderContext(documentationContext: context, bundle: bundle) + func testCreatesRenderReferences() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let renderContext = RenderContext(documentationContext: context) // Verify render references are created for all topics XCTAssertEqual(Array(renderContext.store.topics.keys.sorted(by: { $0.absoluteString < $1.absoluteString })), context.knownIdentifiers.sorted(by: { $0.absoluteString < $1.absoluteString }), "Didn't create render references for all context topics.") diff --git a/Tests/SwiftDocCTests/Converter/RenderNodeCodableTests.swift b/Tests/SwiftDocCTests/Converter/RenderNodeCodableTests.swift index 0081736b66..94c1290118 100644 --- a/Tests/SwiftDocCTests/Converter/RenderNodeCodableTests.swift +++ b/Tests/SwiftDocCTests/Converter/RenderNodeCodableTests.swift @@ -169,8 +169,8 @@ class RenderNodeCodableTests: XCTestCase { XCTAssertEqual(renderNode.topicSectionsStyle, .list) } - func testEncodeRenderNodeWithCustomTopicSectionStyle() throws { - let (bundle, context) = try testBundleAndContext() + func testEncodeRenderNodeWithCustomTopicSectionStyle() async throws { + let (_, context) = try await testBundleAndContext() var problems = [Problem]() let source = """ @@ -185,7 +185,7 @@ class RenderNodeCodableTests: XCTestCase { let document = Document(parsing: source, options: .parseBlockDirectives) let article = try XCTUnwrap( - Article(from: document.root, source: nil, for: bundle, problems: &problems) + Article(from: document.root, source: nil, for: context.inputs, problems: &problems) ) let reference = ResolvedTopicReference( @@ -203,7 +203,7 @@ class RenderNodeCodableTests: XCTestCase { ) context.topicGraph.addNode(topicGraphNode) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let node = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) XCTAssertEqual(node.topicSectionsStyle, .compactGrid) diff --git a/Tests/SwiftDocCTests/Converter/TopicRenderReferenceEncoderTests.swift b/Tests/SwiftDocCTests/Converter/TopicRenderReferenceEncoderTests.swift index efcda73806..0686fe3243 100644 --- a/Tests/SwiftDocCTests/Converter/TopicRenderReferenceEncoderTests.swift +++ b/Tests/SwiftDocCTests/Converter/TopicRenderReferenceEncoderTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -156,10 +156,10 @@ class TopicRenderReferenceEncoderTests: XCTestCase { } /// Verifies that when JSON encoder should sort keys, the custom render reference cache - /// respects that setting and prints the referencs in alphabetical order. - func testSortedReferences() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + /// respects that setting and prints the reference in alphabetical order. + func testSortedReferences() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let converter = DocumentationNodeConverter(context: context) // Create a JSON encoder let encoder = RenderJSONEncoder.makeEncoder() @@ -217,9 +217,9 @@ class TopicRenderReferenceEncoderTests: XCTestCase { } // Verifies that there is no extra comma at the end of the references list. - func testRemovesLastReferencesListDelimiter() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + func testRemovesLastReferencesListDelimiter() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let converter = DocumentationNodeConverter(context: context) // Create a JSON encoder let encoder = RenderJSONEncoder.makeEncoder() diff --git a/Tests/SwiftDocCTests/DeprecatedDiagnosticsDigestWarningTests.swift b/Tests/SwiftDocCTests/DeprecatedDiagnosticsDigestWarningTests.swift index 6a39627b41..8cfbad65db 100644 --- a/Tests/SwiftDocCTests/DeprecatedDiagnosticsDigestWarningTests.swift +++ b/Tests/SwiftDocCTests/DeprecatedDiagnosticsDigestWarningTests.swift @@ -13,8 +13,9 @@ import SwiftDocC import SwiftDocCTestUtilities import XCTest +// THIS SHOULD BE REMOVED, RIGHT?! class DeprecatedDiagnosticsDigestWarningTests: XCTestCase { - func testNoDeprecationWarningWhenThereAreNoOtherWarnings() throws { + func testNoDeprecationWarningWhenThereAreNoOtherWarnings() async throws { let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "Root.md", utf8Content: """ # Root @@ -22,12 +23,10 @@ class DeprecatedDiagnosticsDigestWarningTests: XCTestCase { An empty root page """) ]) - let (bundle, context) = try loadBundle(catalog: catalog) - + let (_, context) = try await loadBundle(catalog: catalog) let outputConsumer = TestOutputConsumer() _ = try ConvertActionConverter.convert( - bundle: bundle, context: context, outputConsumer: outputConsumer, sourceRepository: nil, @@ -38,7 +37,7 @@ class DeprecatedDiagnosticsDigestWarningTests: XCTestCase { XCTAssert(outputConsumer.problems.isEmpty, "Unexpected problems: \(outputConsumer.problems.map(\.diagnostic.summary).joined(separator: "\n"))") } - func testDeprecationWarningWhenThereAreOtherWarnings() throws { + func testDeprecationWarningWhenThereAreOtherWarnings() async throws { let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "Root.md", utf8Content: """ # Root @@ -48,12 +47,10 @@ class DeprecatedDiagnosticsDigestWarningTests: XCTestCase { This link will result in a warning: ``NotFound``. """) ]) - let (bundle, context) = try loadBundle(catalog: catalog) - + let (_, context) = try await loadBundle(catalog: catalog) let outputConsumer = TestOutputConsumer() _ = try ConvertActionConverter.convert( - bundle: bundle, context: context, outputConsumer: outputConsumer, sourceRepository: nil, @@ -66,14 +63,14 @@ class DeprecatedDiagnosticsDigestWarningTests: XCTestCase { let deprecationWarning = try XCTUnwrap(outputConsumer.problems.first?.diagnostic) XCTAssertEqual(deprecationWarning.identifier, "org.swift.docc.DeprecatedDiagnosticsDigets") - XCTAssertEqual(deprecationWarning.summary, "The 'diagnostics.json' digest file is deprecated and will be removed after 6.2 is released. Pass a `--diagnostics-file ` to specify a custom location where DocC will write a diagnostics JSON file with more information.") + XCTAssertEqual(deprecationWarning.summary, "The 'diagnostics.json' digest file is deprecated and will be removed after 6.3 is released. Pass a `--diagnostics-file ` to specify a custom location where DocC will write a diagnostics JSON file with more information.") } } -private class TestOutputConsumer: ConvertOutputConsumer { +private class TestOutputConsumer: ConvertOutputConsumer, ExternalNodeConsumer { var problems: [Problem] = [] - func consume(problems: [Problem]) throws { + func _deprecated_consume(problems: [Problem]) throws { self.problems.append(contentsOf: problems) } @@ -87,4 +84,5 @@ private class TestOutputConsumer: ConvertOutputConsumer { func consume(renderReferenceStore: RenderReferenceStore) throws { } func consume(buildMetadata: BuildMetadata) throws { } func consume(linkResolutionInformation: SerializableLinkResolutionInformation) throws { } + func consume(externalRenderNode: ExternalRenderNode) throws { } } diff --git a/Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift b/Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift index 148b110a96..f22e888977 100644 --- a/Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift +++ b/Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -78,8 +78,8 @@ class DiagnosticTests: XCTestCase { } /// Test offsetting diagnostic ranges - func testOffsetDiagnostics() throws { - let (bundle, context) = try loadBundle(catalog: Folder(name: "unit-test.docc", content: [ + func testOffsetDiagnostics() async throws { + let (_, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph(moduleName: "SomeModuleName")) ])) @@ -87,7 +87,7 @@ class DiagnosticTests: XCTestCase { let markup = Document(parsing: content, source: URL(string: "/tmp/foo.symbols.json"), options: .parseSymbolLinks) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) - var resolver = ReferenceResolver(context: context, bundle: bundle, rootReference: moduleReference) + var resolver = ReferenceResolver(context: context, rootReference: moduleReference) // Resolve references _ = resolver.visitMarkup(markup) diff --git a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift index ef50a6e018..a46afb677e 100644 --- a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift +++ b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -532,6 +532,9 @@ class ConvertServiceTests: XCTestCase { } } + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testConvertPageWithLinkResolvingAndKnownPathComponents() throws { let symbolGraphFile = Bundle.module.url( forResource: "mykit-one-symbol", @@ -827,7 +830,9 @@ class ConvertServiceTests: XCTestCase { ) } } - + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testConvertTutorialWithCode() throws { let tutorialContent = """ @Tutorial(time: 99) { @@ -998,6 +1003,9 @@ class ConvertServiceTests: XCTestCase { } } + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testConvertArticleWithImageReferencesAndDetailedGridLinks() throws { let articleData = try XCTUnwrap(""" # First article @@ -1487,14 +1495,14 @@ class ConvertServiceTests: XCTestCase { ) } - func testReturnsRenderReferenceStoreWhenRequestedForOnDiskBundleWithUncuratedArticles() throws { + func testReturnsRenderReferenceStoreWhenRequestedForOnDiskBundleWithUncuratedArticles() async throws { #if os(Linux) throw XCTSkip(""" Skipped on Linux due to an issue in Foundation.Codable where dictionaries are sometimes getting encoded as \ arrays. (github.com/apple/swift/issues/57363) """) #else - let (testBundleURL, _, _) = try testBundleAndContext( + let (testBundleURL, _, _) = try await testBundleAndContext( copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [ "sidekit.symbols.json", @@ -1616,14 +1624,14 @@ class ConvertServiceTests: XCTestCase { #endif } - func testNoRenderReferencesToNonLinkableNodes() throws { + func testNoRenderReferencesToNonLinkableNodes() async throws { #if os(Linux) throw XCTSkip(""" Skipped on Linux due to an issue in Foundation.Codable where dictionaries are sometimes getting encoded as \ arrays. (github.com/apple/swift/issues/57363) """) #else - let (testBundleURL, _, _) = try testBundleAndContext( + let (testBundleURL, _, _) = try await testBundleAndContext( copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [ "mykit-iOS.symbols.json", @@ -1658,14 +1666,14 @@ class ConvertServiceTests: XCTestCase { #endif } - func testReturnsRenderReferenceStoreWhenRequestedForOnDiskBundleWithCuratedArticles() throws { + func testReturnsRenderReferenceStoreWhenRequestedForOnDiskBundleWithCuratedArticles() async throws { #if os(Linux) throw XCTSkip(""" Skipped on Linux due to an issue in Foundation.Codable where dictionaries are sometimes getting encoded as \ arrays. (github.com/apple/swift/issues/57363) """) #else - let (testBundleURL, _, _) = try testBundleAndContext( + let (testBundleURL, _, _) = try await testBundleAndContext( // Use a bundle that contains only articles, one of which is declared as the TechnologyRoot and curates the // other articles. copying: "BundleWithTechnologyRoot" @@ -1718,6 +1726,9 @@ class ConvertServiceTests: XCTestCase { #endif } + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testConvertPageWithLinkResolving() throws { let symbolGraphFile = Bundle.module.url( forResource: "mykit-one-symbol", @@ -2007,6 +2018,9 @@ class ConvertServiceTests: XCTestCase { } } + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testConvertTopLevelSymbolWithLinkResolving() throws { let symbolGraphFile = Bundle.module.url( forResource: "one-symbol-top-level", @@ -2114,6 +2128,9 @@ class ConvertServiceTests: XCTestCase { } } + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testOrderOfLinkResolutionRequestsForDocLink() throws { let symbolGraphFile = try XCTUnwrap( Bundle.module.url( @@ -2152,6 +2169,9 @@ class ConvertServiceTests: XCTestCase { XCTAssertEqual(expectedLinkResolutionRequests, receivedLinkResolutionRequests) } + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testOrderOfLinkResolutionRequestsForDeeplyNestedSymbol() throws { let symbolGraphFile = try XCTUnwrap( Bundle.module.url( @@ -2191,6 +2211,9 @@ class ConvertServiceTests: XCTestCase { XCTAssertEqual(expectedLinkResolutionRequests, receivedLinkResolutionRequests) } + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testOrderOfLinkResolutionRequestsForSymbolLink() throws { let symbolGraphFile = try XCTUnwrap( Bundle.module.url( @@ -2226,7 +2249,10 @@ class ConvertServiceTests: XCTestCase { XCTAssertEqual(expectedLinkResolutionRequests, receivedLinkResolutionRequests) } - func linkResolutionRequestsForConvertRequest(_ request: ConvertRequest) throws -> [String] { + // This test helper uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) + private func linkResolutionRequestsForConvertRequest(_ request: ConvertRequest) throws -> [String] { var receivedLinkResolutionRequests = [String]() let mockLinkResolvingService = LinkResolvingService { message in do { @@ -2314,6 +2340,9 @@ class ConvertServiceTests: XCTestCase { } } + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testDoesNotResolveLinksUnlessBundleIDMatches() throws { let tempURL = try createTempFolder(content: [ Folder(name: "unit-test.docc", content: [ diff --git a/Tests/SwiftDocCTests/DocumentationService/DocumentationServer+DefaultTests.swift b/Tests/SwiftDocCTests/DocumentationService/DocumentationServer+DefaultTests.swift index 6f4ddc2c61..9d2fcf89b1 100644 --- a/Tests/SwiftDocCTests/DocumentationService/DocumentationServer+DefaultTests.swift +++ b/Tests/SwiftDocCTests/DocumentationService/DocumentationServer+DefaultTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -56,6 +56,9 @@ class DocumentationServer_DefaultTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } + // This test uses `OutOfProcessReferenceResolver/Request` and `OutOfProcessReferenceResolver.Response` which are deprecated. + // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. + @available(*, deprecated) func testQueriesLinkResolutionServer() throws { let symbolGraphFile = Bundle.module.url( forResource: "mykit-one-symbol", diff --git a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift new file mode 100644 index 0000000000..588c979ebd --- /dev/null +++ b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift @@ -0,0 +1,656 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 +@_spi(ExternalLinks) @testable import SwiftDocC +import SwiftDocCTestUtilities + +class ExternalRenderNodeTests: XCTestCase { + private func generateExternalResolver() -> TestMultiResultExternalReferenceResolver { + let externalResolver = TestMultiResultExternalReferenceResolver() + externalResolver.bundleID = "com.test.external" + externalResolver.entitiesToReturn["/path/to/external/swiftArticle"] = .success( + .init( + referencePath: "/path/to/external/swiftArticle", + title: "SwiftArticle", + kind: .article, + language: .swift, + platforms: [.init(name: "iOS", introduced: nil, isBeta: false)] + ) + ) + externalResolver.entitiesToReturn["/path/to/external/objCArticle"] = .success( + .init( + referencePath: "/path/to/external/objCArticle", + title: "ObjCArticle", + kind: .article, + language: .objectiveC, + platforms: [.init(name: "macOS", introduced: nil, isBeta: true)] + ) + ) + externalResolver.entitiesToReturn["/path/to/external/swiftSymbol"] = .success( + .init( + referencePath: "/path/to/external/swiftSymbol", + title: "SwiftSymbol", + kind: .class, + language: .swift, + declarationFragments: .init(declarationFragments: [ + .init(kind: .keyword, spelling: "class", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .identifier, spelling: "SwiftSymbol", preciseIdentifier: nil) + ]), + platforms: [.init(name: "iOS", introduced: nil, isBeta: true)] + ) + ) + externalResolver.entitiesToReturn["/path/to/external/objCSymbol"] = .success( + .init( + referencePath: "/path/to/external/objCSymbol", + title: "ObjCSymbol", + kind: .function, + language: .objectiveC, + declarationFragments: .init(declarationFragments: [ + .init(kind: .text, spelling: "- ", preciseIdentifier: nil), + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "void", preciseIdentifier: nil), + .init(kind: .text, spelling: ") ", preciseIdentifier: nil), + .init(kind: .identifier, spelling: "ObjCSymbol", preciseIdentifier: nil) + ]), + platforms: [.init(name: "macOS", introduced: nil, isBeta: false)] + ) + ) + externalResolver.entitiesToReturn["/path/to/external/navigatorTitleSwiftSymbol"] = .success( + .init( + referencePath: "/path/to/external/navigatorTitleSwiftSymbol", + title: "NavigatorTitleSwiftSymbol (title)", + kind: .class, + language: .swift, + declarationFragments: .init(declarationFragments: [ + .init(kind: .keyword, spelling: "class", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .identifier, spelling: "NavigatorTitleSwiftSymbol", preciseIdentifier: nil) + ]), + navigatorTitle: .init(declarationFragments: [ + .init(kind: .identifier, spelling: "NavigatorTitleSwiftSymbol (navigator title)", preciseIdentifier: nil) + ]), + platforms: [.init(name: "iOS", introduced: nil, isBeta: true)] + ) + ) + externalResolver.entitiesToReturn["/path/to/external/navigatorTitleObjCSymbol"] = .success( + .init( + referencePath: "/path/to/external/navigatorTitleObjCSymbol", + title: "NavigatorTitleObjCSymbol (title)", + kind: .function, + language: .objectiveC, + declarationFragments: .init(declarationFragments: [ + .init(kind: .text, spelling: "- ", preciseIdentifier: nil), + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "void", preciseIdentifier: nil), + .init(kind: .text, spelling: ") ", preciseIdentifier: nil), + .init(kind: .identifier, spelling: "ObjCSymbol", preciseIdentifier: nil) + ]), + navigatorTitle: .init(declarationFragments: [ + .init(kind: .identifier, spelling: "NavigatorTitleObjCSymbol (navigator title)", preciseIdentifier: nil) + ]), + platforms: [.init(name: "macOS", introduced: nil, isBeta: false)] + ) + ) + return externalResolver + } + + func testExternalRenderNode() async throws { + let externalResolver = generateExternalResolver() + let (_, bundle, context) = try await testBundleAndContext( + copying: "MixedLanguageFramework", + externalResolvers: [externalResolver.bundleID: externalResolver] + ) { url in + let mixedLanguageFrameworkExtension = """ + # ``MixedLanguageFramework`` + + This symbol has a Swift and Objective-C variant. + + ## Topics + + ### External Reference + + - + - + - + - + """ + try mixedLanguageFrameworkExtension.write(to: url.appendingPathComponent("/MixedLanguageFramework.md"), atomically: true, encoding: .utf8) + } + + let externalRenderNodes = context.externalCache.valuesByReference.values.map { + ExternalRenderNode(externalEntity: $0, bundleIdentifier: bundle.id) + }.sorted(by: \.titleVariants.defaultValue) + XCTAssertEqual(externalRenderNodes.count, 4) + + XCTAssertEqual(externalRenderNodes[0].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/path/to/external/objCArticle") + XCTAssertEqual(externalRenderNodes[0].kind, .article) + XCTAssertEqual(externalRenderNodes[0].symbolKind, nil) + XCTAssertEqual(externalRenderNodes[0].role, "article") + XCTAssertEqual(externalRenderNodes[0].externalIdentifier.identifier, "doc://com.test.external/path/to/external/objCArticle") + XCTAssertTrue(externalRenderNodes[0].isBeta) + + XCTAssertEqual(externalRenderNodes[1].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/path/to/external/objCSymbol") + XCTAssertEqual(externalRenderNodes[1].kind, .symbol) + XCTAssertEqual(externalRenderNodes[1].symbolKind, .func) + XCTAssertEqual(externalRenderNodes[1].role, "symbol") + XCTAssertEqual(externalRenderNodes[1].externalIdentifier.identifier, "doc://com.test.external/path/to/external/objCSymbol") + XCTAssertFalse(externalRenderNodes[1].isBeta) + + XCTAssertEqual(externalRenderNodes[2].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/path/to/external/swiftArticle") + XCTAssertEqual(externalRenderNodes[2].kind, .article) + XCTAssertEqual(externalRenderNodes[2].symbolKind, nil) + XCTAssertEqual(externalRenderNodes[2].role, "article") + XCTAssertEqual(externalRenderNodes[2].externalIdentifier.identifier, "doc://com.test.external/path/to/external/swiftArticle") + XCTAssertFalse(externalRenderNodes[2].isBeta) + + XCTAssertEqual(externalRenderNodes[3].identifier.absoluteString, "doc://org.swift.MixedLanguageFramework/path/to/external/swiftSymbol") + XCTAssertEqual(externalRenderNodes[3].kind, .symbol) + XCTAssertEqual(externalRenderNodes[3].symbolKind, .class) + XCTAssertEqual(externalRenderNodes[3].role, "symbol") + XCTAssertEqual(externalRenderNodes[3].externalIdentifier.identifier, "doc://com.test.external/path/to/external/swiftSymbol") + XCTAssertTrue(externalRenderNodes[3].isBeta) + } + + func testExternalRenderNodeVariantRepresentation() throws { + let reference = ResolvedTopicReference(bundleID: "com.test.external", path: "/path/to/external/symbol", sourceLanguages: [.swift, .objectiveC]) + + // Variants for the title + let swiftTitle = "Swift Symbol" + let objcTitle = "Objective-C Symbol" + + // Variants for the fragments + let swiftFragments: [DeclarationRenderSection.Token] = [.init(text: "func", kind: .keyword), .init(text: "symbol", kind: .identifier)] + let objcFragments: [DeclarationRenderSection.Token] = [.init(text: "func", kind: .keyword), .init(text: "occ_symbol", kind: .identifier)] + + let externalEntity = LinkResolver.ExternalEntity( + kind: .function, + language: .swift, + relativePresentationURL: URL(string: "/example/path/to/external/symbol")!, + referenceURL: reference.url, + title: swiftTitle, + availableLanguages: [.swift, .objectiveC], + usr: "some-unique-symbol-id", + subheadingDeclarationFragments: swiftFragments, + variants: [ + .init( + traits: [.interfaceLanguage(SourceLanguage.objectiveC.id)], + language: .objectiveC, + title: objcTitle, + subheadingDeclarationFragments: objcFragments + ) + ] + ) + let externalRenderNode = ExternalRenderNode( + externalEntity: externalEntity, + bundleIdentifier: "com.test.external" + ) + + let swiftNavigatorExternalRenderNode = try XCTUnwrap( + NavigatorExternalRenderNode(renderNode: externalRenderNode) + ) + XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.title, swiftTitle) + XCTAssertFalse(swiftNavigatorExternalRenderNode.metadata.isBeta) + XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.fragments, swiftFragments) + + let objcNavigatorExternalRenderNode = try XCTUnwrap( + NavigatorExternalRenderNode(renderNode: externalRenderNode, trait: .interfaceLanguage(SourceLanguage.objectiveC.id)) + ) + XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.title, objcTitle) + XCTAssertFalse(objcNavigatorExternalRenderNode.metadata.isBeta) + XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.fragments, objcFragments) + } + + func testNavigatorWithExternalNodes() async throws { + let catalog = Folder(name: "ModuleName.docc", content: [ + Folder(name: "swift", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ + makeSymbol(id: "some-symbol-id", language: .swift, kind: .class, pathComponents: ["SomeClass"]) + ])) + ]), + Folder(name: "clang", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ + makeSymbol(id: "some-symbol-id", language: .objectiveC, kind: .class, pathComponents: ["TLASomeClass"]) + ])) + ]), + + InfoPlist(identifier: "some.custom.identifier"), + + TextFile(name: "ModuleName.md", utf8Content: """ + # ``ModuleName`` + + Curate a few external language-specific symbols and articles + + ## Topics + + ### External Reference + + - + - + - + - + """), + ]) + + var configuration = DocumentationContext.Configuration() + let externalResolver = generateExternalResolver() + configuration.externalDocumentationConfiguration.sources[externalResolver.bundleID] = externalResolver + let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration) + XCTAssert(context.problems.isEmpty, "Encountered unexpected problems: \(context.problems.map(\.diagnostic.summary))") + + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) + let targetURL = try createTemporaryDirectory() + let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true) + builder.setup() + for externalLink in context.externalCache { + let externalRenderNode = ExternalRenderNode(externalEntity: externalLink.value, bundleIdentifier: context.inputs.id) + try builder.index(renderNode: externalRenderNode) + } + for identifier in context.knownPages { + let entity = try context.entity(with: identifier) + let renderNode = try XCTUnwrap(converter.renderNode(for: entity)) + try builder.index(renderNode: renderNode) + } + builder.finalize() + let renderIndex = try RenderIndex.fromURL(targetURL.appendingPathComponent("index.json")) + + // Verify that there are no uncurated external links at the top level + XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.swift.id]?.count(where: \.isExternal), 0) + XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.count(where: \.isExternal), 0) + + + func externalTopLevelNodes(for language: SourceLanguage) -> [RenderIndex.Node]? { + renderIndex.interfaceLanguages[language.id]?.first?.children?.filter(\.isExternal) + } + + // Verify that the curated external links are part of the index. + let swiftExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .swift)) + XCTAssertEqual(swiftExternalNodes.count, 3) + + let objcExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .objectiveC)) + XCTAssertEqual(objcExternalNodes.count, 3) + + let swiftArticleExternalNode = try XCTUnwrap(swiftExternalNodes.first(where: { $0.path == "/path/to/external/swiftarticle" })) + let swiftSymbolExternalNode = try XCTUnwrap(swiftExternalNodes.first(where: { $0.path == "/path/to/external/swiftsymbol" })) + let objcArticleExternalNode = try XCTUnwrap(objcExternalNodes.first(where: { $0.path == "/path/to/external/objcarticle" })) + let objcSymbolExternalNode = try XCTUnwrap(objcExternalNodes.first(where: { $0.path == "/path/to/external/objcsymbol" })) + + XCTAssertEqual(swiftArticleExternalNode.title, "SwiftArticle") + XCTAssertEqual(swiftArticleExternalNode.isBeta, false) + XCTAssertEqual(swiftArticleExternalNode.type, "article") + + XCTAssertEqual(swiftSymbolExternalNode.title, "SwiftSymbol") // Classes don't use declaration fragments in their navigator title + XCTAssertEqual(swiftSymbolExternalNode.isBeta, true) + XCTAssertEqual(swiftSymbolExternalNode.type, "class") + + XCTAssertEqual(objcArticleExternalNode.title, "ObjCArticle") + XCTAssertEqual(objcArticleExternalNode.isBeta, true) + XCTAssertEqual(objcArticleExternalNode.type, "article") + + XCTAssertEqual(objcSymbolExternalNode.title, "- (void) ObjCSymbol") + XCTAssertEqual(objcSymbolExternalNode.isBeta, false) + XCTAssertEqual(objcSymbolExternalNode.type, "func") + + // External articles curated in the Topics section appear in all language variants. This is a workaround for https://github.com/swiftlang/swift-docc/issues/240. + // FIXME: This should ideally be solved by making the article language-agnostic rather than accomodating the "Swift" language and special-casing for non-symbols. + let swiftArticleInObjcTree = try XCTUnwrap(objcExternalNodes.first(where: { $0.path == "/path/to/external/swiftarticle" })) + let objcArticleInSwiftTree = try XCTUnwrap(swiftExternalNodes.first(where: { $0.path == "/path/to/external/objcarticle" })) + + XCTAssertEqual(swiftArticleInObjcTree.title, "SwiftArticle") + XCTAssertEqual(swiftArticleInObjcTree.isBeta, false) + XCTAssertEqual(swiftArticleInObjcTree.type, "article") + + XCTAssertEqual(objcArticleInSwiftTree.title, "ObjCArticle") + XCTAssertEqual(objcArticleInSwiftTree.isBeta, true) + XCTAssertEqual(objcArticleInSwiftTree.type, "article") + } + + func testNavigatorWithExternalNodesWithNavigatorTitle() async throws { + let catalog = Folder(name: "ModuleName.docc", content: [ + Folder(name: "swift", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ + makeSymbol(id: "some-symbol-id", language: .swift, kind: .class, pathComponents: ["SomeClass"]) + ])) + ]), + Folder(name: "clang", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ + makeSymbol(id: "some-symbol-id", language: .objectiveC, kind: .class, pathComponents: ["TLASomeClass"]) + ])) + ]), + + InfoPlist(identifier: "some.custom.identifier"), + + TextFile(name: "ModuleName.md", utf8Content: """ + # ``ModuleName`` + + Curate a few external language-specific symbols and articles + + ## Topics + + ### External Reference + + - + - + """), + ]) + + var configuration = DocumentationContext.Configuration() + let externalResolver = generateExternalResolver() + configuration.externalDocumentationConfiguration.sources[externalResolver.bundleID] = externalResolver + let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration) + XCTAssert(context.problems.isEmpty, "Encountered unexpected problems: \(context.problems.map(\.diagnostic.summary))") + + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) + let targetURL = try createTemporaryDirectory() + let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true) + builder.setup() + for externalLink in context.externalCache { + let externalRenderNode = ExternalRenderNode(externalEntity: externalLink.value, bundleIdentifier: context.inputs.id) + try builder.index(renderNode: externalRenderNode) + } + for identifier in context.knownPages { + let entity = try context.entity(with: identifier) + let renderNode = try XCTUnwrap(converter.renderNode(for: entity)) + try builder.index(renderNode: renderNode) + } + builder.finalize() + let renderIndex = try RenderIndex.fromURL(targetURL.appendingPathComponent("index.json")) + + // Verify that there are no uncurated external links at the top level + XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.swift.id]?.count(where: \.isExternal), 0) + XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.count(where: \.isExternal), 0) + + func externalTopLevelNodes(for language: SourceLanguage) -> [RenderIndex.Node]? { + renderIndex.interfaceLanguages[language.id]?.first?.children?.filter(\.isExternal) + } + + // Verify that the curated external links are part of the index. + let swiftExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .swift)) + let objcExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .objectiveC)) + + XCTAssertEqual(swiftExternalNodes.count, 1) + XCTAssertEqual(objcExternalNodes.count, 1) + + let swiftSymbolExternalNode = try XCTUnwrap(swiftExternalNodes.first) + let objcSymbolExternalNode = try XCTUnwrap(objcExternalNodes.first) + + XCTAssertEqual(swiftSymbolExternalNode.title, "NavigatorTitleSwiftSymbol (title)") // Swift types prefer not using the navigator title where possible + XCTAssertEqual(objcSymbolExternalNode.title, "NavigatorTitleObjCSymbol (navigator title)") // Objective C types prefer using the navigator title where possible + } + + func testNavigatorWithExternalNodesOnlyAddsCuratedNodesToNavigator() async throws { + let catalog = Folder(name: "ModuleName.docc", content: [ + Folder(name: "swift", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ + makeSymbol(id: "some-symbol-id", language: .swift, kind: .class, pathComponents: ["SomeClass"]) + ])) + ]), + Folder(name: "clang", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ + makeSymbol(id: "some-symbol-id", language: .objectiveC, kind: .class, pathComponents: ["TLASomeClass"]) + ])) + ]), + + InfoPlist(identifier: "some.custom.identifier"), + + TextFile(name: "ModuleName.md", utf8Content: """ + # ``ModuleName`` + + Curate and link to a few external language-specific symbols and articles + + It also has an external reference which is not curated in the Topics section: + + + + ## Topics + + ### External Reference + + - + - + """), + ]) + + var configuration = DocumentationContext.Configuration() + let externalResolver = generateExternalResolver() + configuration.externalDocumentationConfiguration.sources[externalResolver.bundleID] = externalResolver + let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration) + XCTAssert(context.problems.isEmpty, "Encountered unexpected problems: \(context.problems.map(\.diagnostic.summary))") + + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) + let targetURL = try createTemporaryDirectory() + let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true) + builder.setup() + for externalLink in context.externalCache { + let externalRenderNode = ExternalRenderNode(externalEntity: externalLink.value, bundleIdentifier: context.inputs.id) + try builder.index(renderNode: externalRenderNode) + } + for identifier in context.knownPages { + let entity = try context.entity(with: identifier) + let renderNode = try XCTUnwrap(converter.renderNode(for: entity)) + try builder.index(renderNode: renderNode) + } + builder.finalize() + let renderIndex = try RenderIndex.fromURL(targetURL.appendingPathComponent("index.json")) + + // Verify that there are no uncurated external links at the top level + XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.swift.id]?.count(where: \.isExternal), 0) + XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.count(where: \.isExternal), 0) + + // Verify that the curated external links are part of the index. + let swiftExternalNodes = (renderIndex.interfaceLanguages[SourceLanguage.swift.id]?.first?.children?.filter(\.isExternal) ?? []).sorted(by: \.title) + let objcExternalNodes = (renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.first?.children?.filter(\.isExternal) ?? []).sorted(by: \.title) + XCTAssertEqual(swiftExternalNodes.count, 1) + XCTAssertEqual(objcExternalNodes.count, 2) + XCTAssertEqual(swiftExternalNodes.map(\.title), ["SwiftArticle"]) + XCTAssertEqual(objcExternalNodes.map(\.title), ["- (void) ObjCSymbol", "SwiftArticle"]) + XCTAssertEqual(swiftExternalNodes.map(\.type), ["article"]) + XCTAssertEqual(objcExternalNodes.map(\.type), ["func", "article"]) + } + + func testExternalRenderNodeVariantRepresentationWhenIsBeta() throws { + let reference = ResolvedTopicReference(bundleID: "com.test.external", path: "/path/to/external/symbol", sourceLanguages: [.swift, .objectiveC]) + + // Variants for the title + let swiftTitle = "Swift Symbol" + let objcTitle = "Objective-C Symbol" + + // Variants for the fragments + let swiftFragments: [DeclarationRenderSection.Token] = [.init(text: "func", kind: .keyword), .init(text: "symbol", kind: .identifier)] + let objcFragments: [DeclarationRenderSection.Token] = [.init(text: "func", kind: .keyword), .init(text: "occ_symbol", kind: .identifier)] + + let externalEntity = LinkResolver.ExternalEntity( + kind: .function, + language: .swift, + relativePresentationURL: URL(string: "/example/path/to/external/symbol")!, + referenceURL: reference.url, + title: swiftTitle, + availableLanguages: [.swift, .objectiveC], + platforms: [.init(name: "Platform name", introduced: "1.2.3", isBeta: true)], + usr: "some-unique-symbol-id", + subheadingDeclarationFragments: swiftFragments, + variants: [ + .init( + traits: [.interfaceLanguage(SourceLanguage.objectiveC.id)], + language: .objectiveC, + title: objcTitle, + subheadingDeclarationFragments: objcFragments + ) + ] + ) + let externalRenderNode = ExternalRenderNode( + externalEntity: externalEntity, + bundleIdentifier: "com.test.external" + ) + + let swiftNavigatorExternalRenderNode = try XCTUnwrap( + NavigatorExternalRenderNode(renderNode: externalRenderNode) + ) + XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.title, swiftTitle) + XCTAssertTrue(swiftNavigatorExternalRenderNode.metadata.isBeta) + + let objcNavigatorExternalRenderNode = try XCTUnwrap( + NavigatorExternalRenderNode(renderNode: externalRenderNode, trait: .interfaceLanguage(SourceLanguage.objectiveC.id)) + ) + XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.title, objcTitle) + XCTAssertTrue(objcNavigatorExternalRenderNode.metadata.isBeta) + } + + func testExternalLinksInContentDontAffectNavigatorIndex() async throws { + let externalResolver = ExternalReferenceResolverTests.TestExternalReferenceResolver() + externalResolver.expectedReferencePath = "/documentation/testbundle/sampleclass" + + let catalog = Folder(name: "unit-test.docc", content: [ + TextFile(name: "Article.md", utf8Content: """ + # Article + + This is an internal article with an external link which clashes with the curated local link. + + External links in content should not affect the navigator. + + ## Topics + + - ``SampleClass`` + """), + TextFile(name: "SampleClass.md", utf8Content: """ + # ``SampleClass`` + + This extends the documentation for this symbol. + + ## Topics + + - + - + """), + TextFile(name: "ChildArticleA.md", utf8Content: """ + # ChildArticleA + + A child article. + """), + TextFile(name: "ChildArticleB.md", utf8Content: """ + # ChildArticleB + + A child article. + """), + // Symbol graph with a class that matches an external link path + JSONFile(name: "TestBundle.symbols.json", content: makeSymbolGraph(moduleName: "TestBundle", symbols: [ + makeSymbol(id: "some-symbol-id", language: .swift, kind: .class, pathComponents: ["SampleClass"]) + ])), + ]) + + var configuration = DocumentationContext.Configuration() + configuration.externalDocumentationConfiguration.sources[externalResolver.bundleID] = externalResolver + let (bundle, context) = try await loadBundle(catalog: catalog, configuration: configuration) + XCTAssert(context.problems.isEmpty, "Unexpectedly found problems: \(context.problems.map(\.diagnostic.summary))") + + let renderIndexFolder = try createTemporaryDirectory() + let indexBuilder = NavigatorIndex.Builder(outputURL: renderIndexFolder, bundleIdentifier: bundle.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true) + indexBuilder.setup() + let outputConsumer = TestExternalRenderNodeOutputConsumer(indexBuilder: indexBuilder) + + let problems = try ConvertActionConverter.convert( + context: context, + outputConsumer: outputConsumer, + sourceRepository: nil, + emitDigest: false, + documentationCoverageOptions: .noCoverage + ) + XCTAssert(problems.isEmpty, "Unexpectedly found problems: \(DiagnosticConsoleWriter.formattedDescription(for: problems))") + indexBuilder.finalize(emitJSONRepresentation: true, emitLMDBRepresentation: false) + + XCTAssertEqual( + try RenderIndex.fromURL(renderIndexFolder.appendingPathComponent("index.json", isDirectory: false)), + try RenderIndex.fromString( + """ + { + "includedArchiveIdentifiers": [ + "unit-test" + ], + "interfaceLanguages": { + "swift": [ + { + "children": [ + { + "title": "Articles", + "type": "groupMarker" + }, + { + "children": [ + { + "children": [ + { + "path": "/documentation/unit-test/childarticlea", + "title": "ChildArticleA", + "type": "article" + }, + { + "path": "/documentation/unit-test/childarticleb", + "title": "ChildArticleB", + "type": "article" + } + ], + "path": "/documentation/testbundle/sampleclass", + "title": "SampleClass", + "type": "class" + } + ], + "path": "/documentation/unit-test/article", + "title": "Article", + "type": "symbol" + } + ], + "path": "/documentation/testbundle", + "title": "TestBundle", + "type": "module" + } + ] + }, + "schemaVersion": { + "major": 0, + "minor": 1, + "patch": 2 + } + } + """ + ) + ) + } +} + +private class TestExternalRenderNodeOutputConsumer: ConvertOutputConsumer, ExternalNodeConsumer { + let indexBuilder: Synchronized! + + init(indexBuilder: NavigatorIndex.Builder) { + self.indexBuilder = Synchronized(indexBuilder) + } + + func consume(externalRenderNode: ExternalRenderNode) throws { + try self.indexBuilder.sync { try $0.index(renderNode: externalRenderNode) } + } + + func consume(renderNode: RenderNode) throws { + try self.indexBuilder.sync { try $0.index(renderNode: renderNode) } + } + + func consume(assetsInBundle bundle: DocumentationBundle) throws { } + func consume(linkableElementSummaries: [LinkDestinationSummary]) throws { } + func consume(indexingRecords: [IndexingRecord]) throws { } + func consume(assets: [RenderReferenceType: [any RenderReference]]) throws { } + func consume(benchmarks: Benchmark) throws { } + func consume(documentationCoverageInfo: [CoverageDataEntry]) throws { } + func consume(renderReferenceStore: RenderReferenceStore) throws { } + func consume(buildMetadata: BuildMetadata) throws { } + func consume(linkResolutionInformation: SerializableLinkResolutionInformation) throws { } +} diff --git a/Tests/SwiftDocCTests/Indexing/IndexingTests.swift b/Tests/SwiftDocCTests/Indexing/IndexingTests.swift index a5381bd043..2b83a1e1d8 100644 --- a/Tests/SwiftDocCTests/Indexing/IndexingTests.swift +++ b/Tests/SwiftDocCTests/Indexing/IndexingTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -14,12 +14,12 @@ import XCTest class IndexingTests: XCTestCase { // MARK: - Tutorial - func testTutorial() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testTutorial() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let tutorialReference = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift) let node = try context.entity(with: tutorialReference) let tutorial = node.semantic as! Tutorial - var converter = RenderNodeTranslator(context: context, bundle: bundle, identifier: tutorialReference) + var converter = RenderNodeTranslator(context: context, identifier: tutorialReference) let renderNode = converter.visit(tutorial) as! RenderNode let indexingRecords = try renderNode.indexingRecords(onPage: tutorialReference) XCTAssertEqual(4, indexingRecords.count) @@ -88,12 +88,12 @@ class IndexingTests: XCTestCase { // MARK: - Article - func testArticle() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testArticle() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let articleReference = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/tutorials/Test-Bundle/TestTutorialArticle", sourceLanguage: .swift) let node = try context.entity(with: articleReference) let article = node.semantic as! TutorialArticle - var converter = RenderNodeTranslator(context: context, bundle: bundle, identifier: articleReference) + var converter = RenderNodeTranslator(context: context, identifier: articleReference) let renderNode = converter.visit(article) as! RenderNode let indexingRecords = try renderNode.indexingRecords(onPage: articleReference) @@ -186,12 +186,12 @@ class IndexingTests: XCTestCase { XCTAssertEqual("Hello, world!", aside.rawIndexableTextContent(references: [:])) } - func testRootPageIndexingRecord() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testRootPageIndexingRecord() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let articleReference = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit", sourceLanguage: .swift) let node = try context.entity(with: articleReference) let article = node.semantic as! Symbol - var converter = RenderNodeTranslator(context: context, bundle: bundle, identifier: articleReference) + var converter = RenderNodeTranslator(context: context, identifier: articleReference) let renderNode = converter.visit(article) as! RenderNode let indexingRecords = try renderNode.indexingRecords(onPage: articleReference) @@ -206,9 +206,9 @@ class IndexingTests: XCTestCase { indexingRecords[0]) } - func testSymbolIndexingRecord() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in - // Modify the documentaion to have default availability for MyKit so that there is platform availability + func testSymbolIndexingRecord() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + // Modify the documentation to have default availability for MyKit so that there is platform availability // information for MyProtocol (both in the render node and in the indexing record. let plistURL = url.appendingPathComponent("Info.plist") let plistData = try Data(contentsOf: plistURL) @@ -224,7 +224,7 @@ class IndexingTests: XCTestCase { let articleReference = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit/MyProtocol", sourceLanguage: .swift) let node = try context.entity(with: articleReference) let article = node.semantic as! Symbol - var converter = RenderNodeTranslator(context: context, bundle: bundle, identifier: articleReference) + var converter = RenderNodeTranslator(context: context, identifier: articleReference) let renderNode = converter.visit(article) as! RenderNode let indexingRecords = try renderNode.indexingRecords(onPage: articleReference) diff --git a/Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift b/Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift index c81a569971..16c63bede5 100644 --- a/Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift +++ b/Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -15,7 +15,7 @@ import SwiftDocCTestUtilities typealias Node = NavigatorTree.Node typealias PageType = NavigatorIndex.PageType -let testBundleIdentifier = "org.swift.docc.example" +private let testBundleIdentifier = "org.swift.docc.example" class NavigatorIndexingTests: XCTestCase { @@ -138,6 +138,66 @@ Root XCTAssertEqual(item, fromData) } + func testNavigatorEquality() { + // Test for equal + var item1 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024, isExternal: true, isBeta: true) + var item2 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024, isExternal: true, isBeta: true) + XCTAssertEqual(item1, item2) + + // Tests for not equal + item1 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024, isBeta: true) + item2 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024, isBeta: false) + XCTAssertNotEqual(item1, item2) + + item1 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024, isExternal: true) + item2 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024, isExternal: false) + XCTAssertNotEqual(item1, item2) + + item1 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024) + item2 = NavigatorItem(pageType: 2, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024) + XCTAssertNotEqual(item1, item2) + + item1 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024) + item2 = NavigatorItem(pageType: 1, languageID: 5, title: "My Title", platformMask: 256, availabilityID: 1024) + XCTAssertNotEqual(item1, item2) + + item1 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024) + item2 = NavigatorItem(pageType: 1, languageID: 4, title: "My Other Title", platformMask: 256, availabilityID: 1024) + XCTAssertNotEqual(item1, item2) + + item1 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024) + item2 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 257, availabilityID: 1024) + XCTAssertNotEqual(item1, item2) + + item1 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024) + item2 = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1025) + XCTAssertNotEqual(item1, item2) + } + + func testNavigatorItemRawDumpWithExtraProperties() { + let item = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024, isExternal: true, isBeta: true) + let data = item.rawValue + let fromData = NavigatorItem(rawValue: data) + XCTAssertEqual(item, fromData) + } + + func testNavigatorItemRawDumpBackwardCompatibility() { + let item = NavigatorItem(pageType: 1, languageID: 4, title: "My Title", platformMask: 256, availabilityID: 1024) + var data = Data() + data.append(packedDataFromValue(item.pageType)) + data.append(packedDataFromValue(item.languageID)) + data.append(packedDataFromValue(item.platformMask)) + data.append(packedDataFromValue(item.availabilityID)) + data.append(packedDataFromValue(UInt64(item.title.utf8.count))) + data.append(packedDataFromValue(UInt64(item.path.utf8.count))) + data.append(Data(item.title.utf8)) + data.append(Data(item.path.utf8)) + // Note: NOT adding isBeta and isExternal flags to simulate when they were not supported + + let fromData = NavigatorItem(rawValue: data) + XCTAssertEqual(item, fromData) + } + func testObjCLanguage() { let root = generateLargeTree() var objcFiltered: Node? @@ -396,10 +456,10 @@ Root XCTAssertNotNil(builder.navigatorIndex) } - func testNavigatorIndexGeneration() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func testNavigatorIndexGeneration() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) var results = Set() // Create an index 10 times to ensure we have not non-deterministic behavior across builds @@ -455,7 +515,7 @@ Root assertEqualDumps(results.first ?? "", try testTree(named: "testNavigatorIndexGeneration")) } - func testNavigatorIndexGenerationWithCyclicCuration() throws { + func testNavigatorIndexGenerationWithCyclicCuration() async throws { // This is a documentation hierarchy where every page exist in more than one place in the navigator, // through a mix of automatic and manual curation, with a cycle between the two "leaf" nodes: // @@ -586,10 +646,10 @@ Root """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) let targetURL = try createTemporaryDirectory() let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: testBundleIdentifier) @@ -633,13 +693,13 @@ Root """) } - func testNavigatorWithDifferentSwiftAndObjectiveCHierarchies() throws { - let (_, bundle, context) = try testBundleAndContext(named: "GeometricalShapes") - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func testNavigatorWithDifferentSwiftAndObjectiveCHierarchies() async throws { + let (_, _, context) = try await testBundleAndContext(named: "GeometricalShapes") + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) - let fromMemoryBuilder = NavigatorIndex.Builder(outputURL: try createTemporaryDirectory(), bundleIdentifier: bundle.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true) - let fromDecodedBuilder = NavigatorIndex.Builder(outputURL: try createTemporaryDirectory(), bundleIdentifier: bundle.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true) + let fromMemoryBuilder = NavigatorIndex.Builder(outputURL: try createTemporaryDirectory(), bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true) + let fromDecodedBuilder = NavigatorIndex.Builder(outputURL: try createTemporaryDirectory(), bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true) fromMemoryBuilder.setup() fromDecodedBuilder.setup() @@ -831,8 +891,8 @@ Root try FileManager.default.removeItem(at: targetURL) } - func testDoesNotCurateUncuratedPagesInLanguageThatAreCuratedInAnotherLanguage() throws { - let navigatorIndex = try generatedNavigatorIndex(for: "MixedLanguageFramework", bundleIdentifier: "org.swift.mixedlanguageframework") + func testDoesNotCurateUncuratedPagesInLanguageThatAreCuratedInAnotherLanguage() async throws { + let navigatorIndex = try await generatedNavigatorIndex(for: "MixedLanguageFramework", bundleIdentifier: "org.swift.mixedlanguageframework") XCTAssertEqual( navigatorIndex.navigatorTree.root.children @@ -864,8 +924,8 @@ Root ) } - func testMultiCuratesChildrenOfMultiCuratedPages() throws { - let navigatorIndex = try generatedNavigatorIndex(for: "MultiCuratedSubtree", bundleIdentifier: "org.swift.MultiCuratedSubtree") + func testMultiCuratesChildrenOfMultiCuratedPages() async throws { + let navigatorIndex = try await generatedNavigatorIndex(for: "MultiCuratedSubtree", bundleIdentifier: "org.swift.MultiCuratedSubtree") XCTAssertEqual( navigatorIndex.navigatorTree.root.dumpTree(), @@ -893,11 +953,82 @@ Root """ ) } + + // Bug: rdar://160284853 + // The supported languages of an article need to be stored in the resolved + // topic reference that eventually gets serialised into the render node, + // which the navigator uses. This must be done when creating the topic + // graph node, rather than updating the set of supported languages when + // registering the article. If a catalog contains more than one module, any + // articles present are not registered in the documentation cache, since it + // is not possible to determine what module it is belongs to. This test + // ensures that in such cases, the supported languages information is + // correctly included in the render node, and that the navigator is built + // correctly. + func testSupportedLanguageDirectiveForStandaloneArticles() async throws { + let catalog = Folder(name: "unit-test.docc", content: [ + InfoPlist(identifier: testBundleIdentifier), + TextFile(name: "UnitTest.md", utf8Content: """ + # UnitTest + + @Metadata { + @TechnologyRoot + @SupportedLanguage(data) + } + + ## Topics + + - + - ``Foo`` + """), + TextFile(name: "Article.md", utf8Content: """ + # Article + + Just a random article. + """), + // The correct way to configure a catalog is to have a single root module. If multiple modules, + // are present, it is not possible to determine which module an article is supposed to be + // registered with. We include multiple modules to prevent registering the articles in the + // documentation cache, to test if the supported languages are attached prior to registration. + JSONFile(name: "Foo.symbols.json", content: makeSymbolGraph(moduleName: "Foo", symbols: [ + makeSymbol(id: "some-symbol", language: SourceLanguage.data, kind: .class, pathComponents: ["SomeSymbol"]), + ])) + ]) + + let (_, context) = try await loadBundle(catalog: catalog) + + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) + + let targetURL = try createTemporaryDirectory() + let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: testBundleIdentifier) + builder.setup() + + for identifier in context.knownPages { + let entity = try context.entity(with: identifier) + let renderNode = try XCTUnwrap(converter.renderNode(for: entity)) + try builder.index(renderNode: renderNode) + } + + builder.finalize() + + let navigatorIndex = try XCTUnwrap(builder.navigatorIndex) + + let expectedNavigator = """ +[Root] +┗╸UnitTest + ┣╸Article + ┗╸Foo + ┣╸Classes + ┗╸SomeSymbol +""" + XCTAssertEqual(navigatorIndex.navigatorTree.root.dumpTree(), expectedNavigator) + } - func testNavigatorIndexUsingPageTitleGeneration() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func testNavigatorIndexUsingPageTitleGeneration() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) var results = Set() // Create an index 10 times to ensure we have not non-deterministic behavior across builds @@ -943,9 +1074,9 @@ Root assertEqualDumps(results.first ?? "", try testTree(named: "testNavigatorIndexPageTitleGeneration")) } - func testNavigatorIndexGenerationNoPaths() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + func testNavigatorIndexGenerationNoPaths() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let converter = DocumentationNodeConverter(context: context) var results = Set() // Create an index 10 times to ensure we have not non-deterministic behavior across builds @@ -1000,8 +1131,8 @@ Root assertEqualDumps(results.first ?? "", try testTree(named: "testNavigatorIndexGeneration")) } - func testNavigatorIndexGenerationWithLanguageGrouping() throws { - let navigatorIndex = try generatedNavigatorIndex(for: "LegacyBundle_DoNotUseInNewTests", bundleIdentifier: testBundleIdentifier) + func testNavigatorIndexGenerationWithLanguageGrouping() async throws { + let navigatorIndex = try await generatedNavigatorIndex(for: "LegacyBundle_DoNotUseInNewTests", bundleIdentifier: testBundleIdentifier) XCTAssertEqual(navigatorIndex.availabilityIndex.platforms, [.watchOS, .macCatalyst, .iOS, .tvOS, .macOS, .iPadOS]) XCTAssertEqual(navigatorIndex.availabilityIndex.versions(for: .iOS), Set([ @@ -1020,10 +1151,10 @@ Root } - func testNavigatorIndexGenerationWithCuratedFragment() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func testNavigatorIndexGenerationWithCuratedFragment() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) var results = Set() // Create an index 10 times to ensure we have no non-deterministic behavior across builds @@ -1083,10 +1214,10 @@ Root assertEqualDumps(results.first ?? "", try testTree(named: "testNavigatorIndexGeneration")) } - func testNavigatorIndexAvailabilityGeneration() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func testNavigatorIndexAvailabilityGeneration() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) let targetURL = try createTemporaryDirectory() let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: testBundleIdentifier, sortRootChildrenByName: true) @@ -1187,13 +1318,13 @@ Root XCTAssertNil(availabilityDB.get(type: String.self, forKey: "content")) } - func testCustomIconsInNavigator() throws { - let (bundle, context) = try testBundleAndContext(named: "BookLikeContent") // This content has a @PageImage with the "icon" purpose - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func testCustomIconsInNavigator() async throws { + let (_, context) = try await testBundleAndContext(named: "BookLikeContent") // This content has a @PageImage with the "icon" purpose + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) let targetURL = try createTemporaryDirectory() - let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: bundle.id.rawValue, sortRootChildrenByName: true) + let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true) builder.setup() for identifier in context.knownPages { @@ -1209,14 +1340,14 @@ Root let imageReference = try XCTUnwrap(renderIndex.references["plus.svg"]) XCTAssertEqual(imageReference.asset.variants.values.map(\.path).sorted(), [ - "/images/\(bundle.id)/plus.svg", + "/images/\(context.inputs.id)/plus.svg", ]) } - func testNavigatorIndexDifferentHasherGeneration() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func testNavigatorIndexDifferentHasherGeneration() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) let targetURL = try createTemporaryDirectory() let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: testBundleIdentifier, sortRootChildrenByName: true) @@ -1662,9 +1793,9 @@ Root #endif } - func testNavigatorIndexAsReadOnlyFile() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + func testNavigatorIndexAsReadOnlyFile() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let converter = DocumentationNodeConverter(context: context) let targetURL = try createTemporaryDirectory() let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: "org.swift.docc.test", sortRootChildrenByName: true) @@ -1902,8 +2033,8 @@ Root ) } - func testAnonymousTopicGroups() throws { - let navigatorIndex = try generatedNavigatorIndex( + func testAnonymousTopicGroups() async throws { + let navigatorIndex = try await generatedNavigatorIndex( for: "AnonymousTopicGroups", bundleIdentifier: "org.swift.docc.example" ) @@ -1923,10 +2054,10 @@ Root ) } - func testNavigatorDoesNotContainOverloads() throws { + func testNavigatorDoesNotContainOverloads() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let navigatorIndex = try generatedNavigatorIndex( + let navigatorIndex = try await generatedNavigatorIndex( for: "OverloadedSymbols", bundleIdentifier: "com.shapes.ShapeKit") @@ -1977,10 +2108,65 @@ Root ) } - func generatedNavigatorIndex(for testBundleName: String, bundleIdentifier: String) throws -> NavigatorIndex { - let (bundle, context) = try testBundleAndContext(named: testBundleName) - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func testNavigatorIndexCapturesBetaStatus() async throws { + // Set up configuration with beta platforms + let platformMetadata = [ + "macOS": PlatformVersion(VersionTriplet(1, 0, 0), beta: true), + "watchOS": PlatformVersion(VersionTriplet(2, 0, 0), beta: true), + "tvOS": PlatformVersion(VersionTriplet(3, 0, 0), beta: true), + "iOS": PlatformVersion(VersionTriplet(4, 0, 0), beta: true), + "Mac Catalyst": PlatformVersion(VersionTriplet(4, 0, 0), beta: true), + "iPadOS": PlatformVersion(VersionTriplet(4, 0, 0), beta: true), + ] + var configuration = DocumentationContext.Configuration() + configuration.externalMetadata.currentPlatforms = platformMetadata + + let (_, _, context) = try await testBundleAndContext(named: "AvailabilityBetaBundle", configuration: configuration) + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) + let targetURL = try createTemporaryDirectory() + let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true) + builder.setup() + for identifier in context.knownPages { + let entity = try context.entity(with: identifier) + let renderNode = try XCTUnwrap(converter.renderNode(for: entity)) + try builder.index(renderNode: renderNode) + } + builder.finalize() + let renderIndex = try RenderIndex.fromURL(targetURL.appendingPathComponent("index.json")) + + // Find nodes that should have beta status + let swiftNodes = renderIndex.interfaceLanguages["swift"] ?? [] + let betaNodes = findNodesWithBetaStatus(in: swiftNodes, isBeta: true) + let nonBetaNodes = findNodesWithBetaStatus(in: swiftNodes, isBeta: false) + + // Verify that beta status was captured in the render index + XCTAssertEqual(betaNodes.map(\.title), ["MyClass"]) + XCTAssert(betaNodes.allSatisfy(\.isBeta)) // Sanity check + XCTAssertEqual(nonBetaNodes.map(\.title).sorted(), ["Classes", "MyOtherClass", "MyThirdClass"]) + XCTAssert(nonBetaNodes.allSatisfy { $0.isBeta == false }) // Sanity check + } + + private func findNodesWithBetaStatus(in nodes: [RenderIndex.Node], isBeta: Bool) -> [RenderIndex.Node] { + var betaNodes: [RenderIndex.Node] = [] + + for node in nodes { + if node.isBeta == isBeta { + betaNodes.append(node) + } + + if let children = node.children { + betaNodes.append(contentsOf: findNodesWithBetaStatus(in: children, isBeta: isBeta)) + } + } + + return betaNodes + } + + func generatedNavigatorIndex(for testBundleName: String, bundleIdentifier: String) async throws -> NavigatorIndex { + let (_, context) = try await testBundleAndContext(named: testBundleName) + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) let targetURL = try createTemporaryDirectory() let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: bundleIdentifier, sortRootChildrenByName: true, groupByLanguage: true) @@ -2004,7 +2190,7 @@ Root expectation.fulfill() } } - wait(for: [expectation], timeout: 10.0) + await fulfillment(of: [expectation], timeout: 10.0) return navigatorIndex } diff --git a/Tests/SwiftDocCTests/Indexing/NavigatorIndexableRenderMetadataTests.swift b/Tests/SwiftDocCTests/Indexing/NavigatorIndexableRenderMetadataTests.swift new file mode 100644 index 0000000000..3a997e259a --- /dev/null +++ b/Tests/SwiftDocCTests/Indexing/NavigatorIndexableRenderMetadataTests.swift @@ -0,0 +1,134 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 +@testable import SwiftDocC + +class NavigatorIndexableRenderMetadataTests: XCTestCase { + + // MARK: - Test Helper Methods + + /// Creates a test platform with the specified beta status + private func createPlatform(name: String, isBeta: Bool) -> AvailabilityRenderItem { + return AvailabilityRenderItem(name: name, introduced: "1.0", isBeta: isBeta) + } + + /// Creates a RenderMetadata instance with the specified platforms + private func createRenderMetadata(platforms: [AvailabilityRenderItem]?) -> RenderMetadata { + var metadata = RenderMetadata() + metadata.platforms = platforms + return metadata + } + + /// Creates a RenderMetadataVariantView with the specified platforms + private func createRenderMetadataVariantView(platforms: [AvailabilityRenderItem]?) -> RenderMetadataVariantView { + let metadata = createRenderMetadata(platforms: platforms) + return RenderMetadataVariantView(wrapped: metadata, traits: []) + } + + // MARK: - RenderMetadataVariantView Tests + + func testRenderMetadataVariantViewIsBeta() { + var metadataView = createRenderMetadataVariantView(platforms: nil) + XCTAssertFalse(metadataView.isBeta, "isBeta should be false when no platforms are defined") + + metadataView = createRenderMetadataVariantView(platforms: []) + XCTAssertFalse(metadataView.isBeta, "isBeta should be false when platforms array is empty") + + metadataView = createRenderMetadataVariantView(platforms: [ + createPlatform(name: "iOS", isBeta: false) + ]) + XCTAssertFalse(metadataView.isBeta, "isBeta should be false when single platform is non-beta") + + metadataView = createRenderMetadataVariantView(platforms: [ + createPlatform(name: "iOS", isBeta: false), + createPlatform(name: "macOS", isBeta: false), + createPlatform(name: "watchOS", isBeta: false) + ]) + XCTAssertFalse(metadataView.isBeta, "isBeta should be false when multiple platforms are non-beta") + + var platform1 = AvailabilityRenderItem(name: "iOS", introduced: "1.0", isBeta: false) + platform1.isBeta = nil + var platform2 = AvailabilityRenderItem(name: "macOS", introduced: "1.0", isBeta: false) + platform2.isBeta = nil + + metadataView = createRenderMetadataVariantView(platforms: [platform1, platform2]) + XCTAssertFalse(metadataView.isBeta, "isBeta should be false when platforms have nil beta status") + + metadataView = createRenderMetadataVariantView(platforms: [ + createPlatform(name: "iOS", isBeta: true), + createPlatform(name: "macOS", isBeta: false), + createPlatform(name: "watchOS", isBeta: true) + ]) + XCTAssertFalse(metadataView.isBeta, "isBeta should be false when some platforms are beta and some are non-beta") + + metadataView = createRenderMetadataVariantView(platforms: [ + createPlatform(name: "iOS", isBeta: true) + ]) + XCTAssertTrue(metadataView.isBeta, "isBeta should be true when single platform is beta") + + metadataView = createRenderMetadataVariantView(platforms: [ + createPlatform(name: "iOS", isBeta: true), + createPlatform(name: "macOS", isBeta: true), + createPlatform(name: "watchOS", isBeta: true) + ]) + XCTAssertTrue(metadataView.isBeta, "isBeta should be true when multiple platforms are beta") + } + + // MARK: - RenderMetadata Tests + + func testRenderMetadataIsBeta() { + var metadata = createRenderMetadata(platforms: nil) + XCTAssertFalse(metadata.isBeta, "isBeta should be false when no platforms are defined") + + metadata = createRenderMetadata(platforms: []) + XCTAssertFalse(metadata.isBeta, "isBeta should be false when platforms array is empty") + + metadata = createRenderMetadata(platforms: [ + createPlatform(name: "macOS", isBeta: false) + ]) + XCTAssertFalse(metadata.isBeta, "isBeta should be false when single platform is non-beta") + + metadata = createRenderMetadata(platforms: [ + createPlatform(name: "iOS", isBeta: false), + createPlatform(name: "macOS", isBeta: false), + createPlatform(name: "tvOS", isBeta: false) + ]) + XCTAssertFalse(metadata.isBeta, "isBeta should be false when all platforms are non-beta") + + var platform1 = AvailabilityRenderItem(name: "iOS", introduced: "1.0", isBeta: false) + platform1.isBeta = nil + var platform2 = AvailabilityRenderItem(name: "macOS", introduced: "1.0", isBeta: false) + platform2.isBeta = nil + + metadata = createRenderMetadata(platforms: [platform1, platform2]) + XCTAssertFalse(metadata.isBeta, "isBeta should be false when platforms have nil beta status") + + metadata = createRenderMetadata(platforms: [ + createPlatform(name: "iOS", isBeta: false), + createPlatform(name: "macOS", isBeta: true), + createPlatform(name: "tvOS", isBeta: false) + ]) + XCTAssertFalse(metadata.isBeta, "isBeta should be false when some platforms are beta and some are non-beta") + + metadata = createRenderMetadata(platforms: [ + createPlatform(name: "macOS", isBeta: true) + ]) + XCTAssertTrue(metadata.isBeta, "isBeta should be true when single platform is beta") + + metadata = createRenderMetadata(platforms: [ + createPlatform(name: "iOS", isBeta: true), + createPlatform(name: "macOS", isBeta: true), + createPlatform(name: "tvOS", isBeta: true) + ]) + XCTAssertTrue(metadata.isBeta, "isBeta should be true when all platforms are beta") + } +} diff --git a/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift b/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift index 5cc0df3c62..d2682c360d 100644 --- a/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift +++ b/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022-2024 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -14,7 +14,7 @@ import SwiftDocCTestUtilities @testable import SwiftDocC final class RenderIndexTests: XCTestCase { - func testTestBundleRenderIndexGeneration() throws { + func testTestBundleRenderIndexGeneration() async throws { let expectedIndexURL = try XCTUnwrap( Bundle.module.url( forResource: "TestBundle-RenderIndex", @@ -22,16 +22,14 @@ final class RenderIndexTests: XCTestCase { subdirectory: "Test Resources" ) ) - - try XCTAssertEqual( - generatedRenderIndex(for: "LegacyBundle_DoNotUseInNewTests", with: "org.swift.docc.example"), - RenderIndex.fromURL(expectedIndexURL) - ) + let renderIndex = try await generatedRenderIndex(for: "LegacyBundle_DoNotUseInNewTests", with: "org.swift.docc.example") + try XCTAssertEqual(renderIndex, RenderIndex.fromURL(expectedIndexURL)) } - func testRenderIndexGenerationForBundleWithTechnologyRoot() throws { + func testRenderIndexGenerationForBundleWithTechnologyRoot() async throws { + let renderIndex = try await generatedRenderIndex(for: "BundleWithTechnologyRoot", with: "org.swift.docc.example") try XCTAssertEqual( - generatedRenderIndex(for: "BundleWithTechnologyRoot", with: "org.swift.docc.example"), + renderIndex, RenderIndex.fromString(#""" { "interfaceLanguages": { @@ -62,8 +60,8 @@ final class RenderIndexTests: XCTestCase { ) } - func testRenderIndexGenerationForMixedLanguageFramework() throws { - let renderIndex = try generatedRenderIndex(for: "MixedLanguageFramework", with: "org.swift.MixedLanguageFramework") + func testRenderIndexGenerationForMixedLanguageFramework() async throws { + let renderIndex = try await generatedRenderIndex(for: "MixedLanguageFramework", with: "org.swift.MixedLanguageFramework") XCTAssertEqual( renderIndex, @@ -91,7 +89,7 @@ final class RenderIndexTests: XCTestCase { }, { "path": "/documentation/mixedlanguageframework/bar", - "title": "Bar", + "title": "Bar (objective c)", "type": "class", "children": [ { @@ -629,7 +627,7 @@ final class RenderIndexTests: XCTestCase { try assertRoundTripCoding(renderIndexFromJSON) } - func testRenderIndexGenerationWithDeprecatedSymbol() throws { + func testRenderIndexGenerationWithDeprecatedSymbol() async throws { let swiftWithDeprecatedSymbolGraphFile = Bundle.module.url( forResource: "Deprecated", withExtension: "symbols.json", @@ -650,10 +648,10 @@ final class RenderIndexTests: XCTestCase { ) try bundle.write(to: bundleDirectory) - let (_, loadedBundle, context) = try loadBundle(from: bundleDirectory) + let (_, _, context) = try await loadBundle(from: bundleDirectory) XCTAssertEqual( - try generatedRenderIndex(for: loadedBundle, withIdentifier: "com.test.example", withContext: context), + try generatedRenderIndex(forIdentifier: "com.test.example", inContext: context), try RenderIndex.fromString(#""" { "interfaceLanguages": { @@ -682,9 +680,10 @@ final class RenderIndexTests: XCTestCase { """#)) } - func testRenderIndexGenerationWithCustomIcon() throws { + func testRenderIndexGenerationWithCustomIcon() async throws { + let renderIndex = try await generatedRenderIndex(for: "BookLikeContent", with: "org.swift.docc.Book") try XCTAssertEqual( - generatedRenderIndex(for: "BookLikeContent", with: "org.swift.docc.Book"), + renderIndex, RenderIndex.fromString(#""" { "interfaceLanguages" : { @@ -737,18 +736,18 @@ final class RenderIndexTests: XCTestCase { ) } - func generatedRenderIndex(for testBundleName: String, with bundleIdentifier: String) throws -> RenderIndex { - let (bundle, context) = try testBundleAndContext(named: testBundleName) - return try generatedRenderIndex(for: bundle, withIdentifier: bundleIdentifier, withContext: context) + func generatedRenderIndex(for testBundleName: String, with bundleIdentifier: String) async throws -> RenderIndex { + let (_, context) = try await testBundleAndContext(named: testBundleName) + return try generatedRenderIndex(forIdentifier: bundleIdentifier, inContext: context) } - func generatedRenderIndex(for bundle: DocumentationBundle, withIdentifier bundleIdentifier: String, withContext context: DocumentationContext) throws -> RenderIndex { - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func generatedRenderIndex(forIdentifier bundleIdentifier: String, inContext context: DocumentationContext) throws -> RenderIndex { + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) let indexDirectory = try createTemporaryDirectory() let builder = NavigatorIndex.Builder( outputURL: indexDirectory, - bundleIdentifier: bundleIdentifier, + bundleIdentifier: context.inputs.id.rawValue, sortRootChildrenByName: true ) diff --git a/Tests/SwiftDocCTests/Infrastructure/AnchorSectionTests.swift b/Tests/SwiftDocCTests/Infrastructure/AnchorSectionTests.swift index 95741340c4..ff67afb939 100644 --- a/Tests/SwiftDocCTests/Infrastructure/AnchorSectionTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/AnchorSectionTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -16,20 +16,20 @@ import Markdown class AnchorSectionTests: XCTestCase { - func testResolvingArticleSubsections() throws { - let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + func testResolvingArticleSubsections() async throws { + let (_, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") // Verify the sub-sections of the article have been collected in the context [ - ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/TechnologyX/Article", fragment: "Article-Sub-Section", sourceLanguage: .swift), - ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/TechnologyX/Article", fragment: "Article-Sub-Sub-Section", sourceLanguage: .swift), + ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/TechnologyX/Article", fragment: "Article-Sub-Section", sourceLanguage: .swift), + ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/TechnologyX/Article", fragment: "Article-Sub-Sub-Section", sourceLanguage: .swift), ] .forEach { sectionReference in XCTAssertTrue(context.nodeAnchorSections.keys.contains(sectionReference)) } // Load the module page - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/CoolFramework", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/CoolFramework", sourceLanguage: .swift) let entity = try context.entity(with: reference) // Extract the links from the discussion @@ -66,7 +66,7 @@ class AnchorSectionTests: XCTestCase { } // Verify collecting section render references - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(entity) let sectionReference = try XCTUnwrap(renderNode.references["doc://org.swift.docc.example/documentation/TechnologyX/Article#Article-Sub-Section"] as? TopicRenderReference) @@ -74,20 +74,20 @@ class AnchorSectionTests: XCTestCase { XCTAssertEqual(sectionReference.url, "/documentation/technologyx/article#Article-Sub-Section") } - func testResolvingSymbolSubsections() throws { - let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + func testResolvingSymbolSubsections() async throws { + let (_, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") // Verify the sub-sections of the article have been collected in the context [ - ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/CoolFramework/CoolClass", fragment: "Symbol-Sub-Section", sourceLanguage: .swift), - ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/CoolFramework/CoolClass", fragment: "Symbol-Sub-Sub-Section", sourceLanguage: .swift), + ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/CoolFramework/CoolClass", fragment: "Symbol-Sub-Section", sourceLanguage: .swift), + ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/CoolFramework/CoolClass", fragment: "Symbol-Sub-Sub-Section", sourceLanguage: .swift), ] .forEach { sectionReference in XCTAssertTrue(context.nodeAnchorSections.keys.contains(sectionReference)) } // Load the module page - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/CoolFramework", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/CoolFramework", sourceLanguage: .swift) let entity = try context.entity(with: reference) // Extract the links from the discussion @@ -124,7 +124,7 @@ class AnchorSectionTests: XCTestCase { } // Verify collecting section render references - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(entity) let sectionReference = try XCTUnwrap(renderNode.references["doc://org.swift.docc.example/documentation/CoolFramework/CoolClass#Symbol-Sub-Section"] as? TopicRenderReference) @@ -132,20 +132,20 @@ class AnchorSectionTests: XCTestCase { XCTAssertEqual(sectionReference.url, "/documentation/coolframework/coolclass#Symbol-Sub-Section") } - func testResolvingRootPageSubsections() throws { - let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + func testResolvingRootPageSubsections() async throws { + let (_, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") // Verify the sub-sections of the article have been collected in the context [ - ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/CoolFramework", fragment: "Module-Sub-Section", sourceLanguage: .swift), - ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/CoolFramework", fragment: "Module-Sub-Sub-Section", sourceLanguage: .swift), + ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/CoolFramework", fragment: "Module-Sub-Section", sourceLanguage: .swift), + ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/CoolFramework", fragment: "Module-Sub-Sub-Section", sourceLanguage: .swift), ] .forEach { sectionReference in XCTAssertTrue(context.nodeAnchorSections.keys.contains(sectionReference)) } // Load the article page - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/TechnologyX/Article", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/TechnologyX/Article", sourceLanguage: .swift) let entity = try context.entity(with: reference) // Extract the links from the discussion @@ -182,7 +182,7 @@ class AnchorSectionTests: XCTestCase { } // Verify collecting section render references - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(entity) let sectionReference = try XCTUnwrap(renderNode.references["doc://org.swift.docc.example/documentation/CoolFramework#Module-Sub-Section"] as? TopicRenderReference) @@ -190,8 +190,8 @@ class AnchorSectionTests: XCTestCase { XCTAssertEqual(sectionReference.url, "/documentation/coolframework#Module-Sub-Section") } - func testWarnsWhenCuratingSections() throws { - let (_, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + func testWarnsWhenCuratingSections() async throws { + let (_, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") // The module page has 3 section links in a Topics group, // the context should contain the three warnings about those links diff --git a/Tests/SwiftDocCTests/Infrastructure/AutoCapitalizationTests.swift b/Tests/SwiftDocCTests/Infrastructure/AutoCapitalizationTests.swift index 3eb8c06178..f45a2d7119 100644 --- a/Tests/SwiftDocCTests/Infrastructure/AutoCapitalizationTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/AutoCapitalizationTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -42,7 +42,7 @@ class AutoCapitalizationTests: XCTestCase { // MARK: End-to-end integration tests - func testParametersCapitalization() throws { + func testParametersCapitalization() async throws { let symbolGraph = makeSymbolGraph( docComment: """ Some symbol description. @@ -60,18 +60,18 @@ class AutoCapitalizationTests: XCTestCase { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.count, 0) - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift) let node = try context.entity(with: reference) let symbol = try XCTUnwrap(node.semantic as? Symbol) let parameterSections = symbol.parametersSectionVariants XCTAssertEqual(parameterSections[.swift]?.parameters.map(\.name), ["one", "two", "three", "four", "five"]) let parameterSectionTranslator = ParametersSectionTranslator() - var renderNodeTranslator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var renderNodeTranslator = RenderNodeTranslator(context: context, identifier: reference) var renderNode = renderNodeTranslator.visit(symbol) as! RenderNode let translatedParameters = parameterSectionTranslator.translateSection(for: symbol, renderNode: &renderNode, renderNodeTranslator: &renderNodeTranslator) let paramsRenderSection = translatedParameters?.defaultValue?.section as! ParametersRenderSection @@ -88,7 +88,7 @@ class AutoCapitalizationTests: XCTestCase { [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("a`nother invalid capitalization")]))]]) } - func testIndividualParametersCapitalization() throws { + func testIndividualParametersCapitalization() async throws { let symbolGraph = makeSymbolGraph( docComment: """ Some symbol description. @@ -105,18 +105,18 @@ class AutoCapitalizationTests: XCTestCase { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.count, 0) - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift) let node = try context.entity(with: reference) let symbol = try XCTUnwrap(node.semantic as? Symbol) let parameterSections = symbol.parametersSectionVariants XCTAssertEqual(parameterSections[.swift]?.parameters.map(\.name), ["one", "two", "three", "four", "five"]) let parameterSectionTranslator = ParametersSectionTranslator() - var renderNodeTranslator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var renderNodeTranslator = RenderNodeTranslator(context: context, identifier: reference) var renderNode = renderNodeTranslator.visit(symbol) as! RenderNode let translatedParameters = parameterSectionTranslator.translateSection(for: symbol, renderNode: &renderNode, renderNodeTranslator: &renderNodeTranslator) let paramsRenderSection = translatedParameters?.defaultValue?.section as! ParametersRenderSection @@ -133,7 +133,7 @@ class AutoCapitalizationTests: XCTestCase { [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("a`nother invalid capitalization")]))]]) } - func testReturnsCapitalization() throws { + func testReturnsCapitalization() async throws { let symbolGraph = makeSymbolGraph( docComment: """ Some symbol description. @@ -146,16 +146,16 @@ class AutoCapitalizationTests: XCTestCase { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.count, 0) - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift) let node = try context.entity(with: reference) let symbol = try XCTUnwrap(node.semantic as? Symbol) let returnsSectionTranslator = ReturnsSectionTranslator() - var renderNodeTranslator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var renderNodeTranslator = RenderNodeTranslator(context: context, identifier: reference) var renderNode = renderNodeTranslator.visit(symbol) as! RenderNode let translatedReturns = returnsSectionTranslator.translateSection(for: symbol, renderNode: &renderNode, renderNodeTranslator: &renderNodeTranslator) let returnsRenderSection = translatedReturns?.defaultValue?.section as! ContentRenderSection diff --git a/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift b/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift index 1d5761ee54..d214b77338 100644 --- a/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -19,7 +19,7 @@ class AutomaticCurationTests: XCTestCase { .filter { $0.symbolGeneratesPage() } .categorize(where: { $0.identifier.hasSuffix(".extension") }) - func testAutomaticTopicsGenerationForSameModuleTypes() throws { + func testAutomaticTopicsGenerationForSameModuleTypes() async throws { for kind in availableNonExtensionSymbolKinds { let containerID = "some-container-id" let memberID = "some-member-id" @@ -38,13 +38,13 @@ class AutomaticCurationTests: XCTestCase { )) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) - try assertRenderedPage(atPath: "/documentation/ModuleName/SomeClass", containsAutomaticTopicSectionFor: kind, context: context, bundle: bundle) + try assertRenderedPage(atPath: "/documentation/ModuleName/SomeClass", containsAutomaticTopicSectionFor: kind, context: context) } } - func testAutomaticTopicsGenerationForExtensionSymbols() throws { + func testAutomaticTopicsGenerationForExtensionSymbols() async throws { // The extended module behavior is already verified for each extended symbol kind in the module. for kind in availableExtensionSymbolKinds where kind != .extendedModule { let containerID = "some-container-id" @@ -83,10 +83,10 @@ class AutomaticCurationTests: XCTestCase { )), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) - try assertRenderedPage(atPath: "/documentation/ModuleName", containsAutomaticTopicSectionFor: .extendedModule, context: context, bundle: bundle) - try assertRenderedPage(atPath: "/documentation/ModuleName/ExtendedModule", containsAutomaticTopicSectionFor: kind, context: context, bundle: bundle) + try assertRenderedPage(atPath: "/documentation/ModuleName", containsAutomaticTopicSectionFor: .extendedModule, context: context) + try assertRenderedPage(atPath: "/documentation/ModuleName/ExtendedModule", containsAutomaticTopicSectionFor: kind, context: context) } } @@ -94,12 +94,11 @@ class AutomaticCurationTests: XCTestCase { atPath path: String, containsAutomaticTopicSectionFor kind: SymbolGraph.Symbol.KindIdentifier, context: DocumentationContext, - bundle: DocumentationBundle, file: StaticString = #filePath, line: UInt = #line ) throws { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: path, sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: path, sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(node.semantic) as? RenderNode, file: file, line: line) for section in renderNode.topicSections { @@ -116,8 +115,8 @@ class AutomaticCurationTests: XCTestCase { ) } - func testAutomaticTopicsSkippingCustomCuratedSymbols() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { url in + func testAutomaticTopicsSkippingCustomCuratedSymbols() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { url in // Curate some of members of SideClass in an API collection try """ # Some API collection @@ -136,11 +135,11 @@ class AutomaticCurationTests: XCTestCase { """.write(to: url.appendingPathComponent("sideclass.md"), atomically: true, encoding: .utf8) }) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) // Compile the render node to flex the automatic curator let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode // Verify that uncurated element `SideKit/SideClass/Element` is @@ -156,7 +155,7 @@ class AutomaticCurationTests: XCTestCase { }).isEmpty) } - func testMergingAutomaticTopics() throws { + func testMergingAutomaticTopics() async throws { let allExpectedChildren = [ "doc://org.swift.docc.example/documentation/SideKit/SideClass/Element", "doc://org.swift.docc.example/documentation/SideKit/SideClass/Value(_:)", @@ -172,7 +171,7 @@ class AutomaticCurationTests: XCTestCase { for curatedIndices in variationsOfChildrenToCurate { let manualCuration = curatedIndices.map { "- <\(allExpectedChildren[$0])>" }.joined(separator: "\n") - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in try """ # ``SideKit/SideClass`` @@ -186,10 +185,10 @@ class AutomaticCurationTests: XCTestCase { """.write(to: url.appendingPathComponent("documentation/sideclass.md"), atomically: true, encoding: .utf8) } - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) // Compile docs and verify the generated Topics section let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode // Verify that all the symbols are curated, either manually or automatically @@ -230,8 +229,8 @@ class AutomaticCurationTests: XCTestCase { } } - func testSeeAlsoSectionForAutomaticallyCuratedTopics() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testSeeAlsoSectionForAutomaticallyCuratedTopics() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in var graph = try JSONDecoder().decode(SymbolGraph.self, from: Data(contentsOf: url.appendingPathComponent("sidekit.symbols.json"))) // Copy `SideClass` a handful of times @@ -348,8 +347,8 @@ class AutomaticCurationTests: XCTestCase { // The first topic section do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // SideKit includes the "Manually curated" task group and additional automatically created groups. @@ -364,8 +363,8 @@ class AutomaticCurationTests: XCTestCase { // The second topic section do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClassFour", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClassFour", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // The other symbols in the same topic section appear in this See Also section @@ -377,8 +376,8 @@ class AutomaticCurationTests: XCTestCase { // The second topic section do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClassSix", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClassSix", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // The other symbols in the same topic section appear in this See Also section @@ -389,29 +388,29 @@ class AutomaticCurationTests: XCTestCase { // The automatically curated symbols shouldn't have a See Also section do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClassEight", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClassEight", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode XCTAssertNil(renderNode.seeAlsoSections.first, "This symbol was automatically curated and shouldn't have a See Also section") } do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClassNine", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClassNine", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode XCTAssertNil(renderNode.seeAlsoSections.first, "This symbol was automatically curated and shouldn't have a See Also section") } do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClassTen", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClassTen", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode XCTAssertNil(renderNode.seeAlsoSections.first, "This symbol was automatically curated and shouldn't have a See Also section") } } - func testTopLevelSymbolsAreNotAutomaticallyCuratedIfManuallyCuratedElsewhere() throws { + func testTopLevelSymbolsAreNotAutomaticallyCuratedIfManuallyCuratedElsewhere() async throws { // A symbol graph that defines symbol hierarchy of: // TestBed -> A // -> B -> C @@ -421,17 +420,14 @@ class AutomaticCurationTests: XCTestCase { forResource: "TopLevelCuration.symbols", withExtension: "json", subdirectory: "Test Resources")! // Create a test bundle copy with the symbol graph from above - let (bundleURL, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { url in try? FileManager.default.copyItem(at: topLevelCurationSGFURL, to: url.appendingPathComponent("TopLevelCuration.symbols.json")) } - defer { - try? FileManager.default.removeItem(at: bundleURL) - } do { // Get the framework render node - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/TestBed", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/TestBed", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify that `B` isn't automatically curated under the framework node @@ -443,8 +439,8 @@ class AutomaticCurationTests: XCTestCase { do { // Get the `A` render node - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/TestBed/A", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/TestBed/A", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify that `B` was in fact curated under `A` @@ -455,8 +451,8 @@ class AutomaticCurationTests: XCTestCase { } } - func testNoAutoCuratedMixedLanguageDuplicates() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "MixedLanguageFramework") { url in + func testNoAutoCuratedMixedLanguageDuplicates() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "MixedLanguageFramework") { url in // Load the existing Obj-C symbol graph from this fixture. let path = "symbol-graphs/clang/MixedLanguageFramework.symbols.json" @@ -504,8 +500,8 @@ class AutomaticCurationTests: XCTestCase { ) } - func testRelevantLanguagesAreAutoCuratedInMixedLanguageFramework() throws { - let (bundle, context) = try testBundleAndContext(named: "MixedLanguageFramework") + func testRelevantLanguagesAreAutoCuratedInMixedLanguageFramework() async throws { + let (bundle, context) = try await testBundleAndContext(named: "MixedLanguageFramework") let frameworkDocumentationNode = try context.entity( with: ResolvedTopicReference( @@ -574,11 +570,11 @@ class AutomaticCurationTests: XCTestCase { ) } - func testIvarsAndMacrosAreCuratedProperly() throws { + func testIvarsAndMacrosAreCuratedProperly() async throws { let whatsitSymbols = Bundle.module.url( forResource: "Whatsit-Objective-C.symbols", withExtension: "json", subdirectory: "Test Resources")! - let (bundleURL, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + let (bundleURL, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in try? FileManager.default.copyItem(at: whatsitSymbols, to: url.appendingPathComponent("Whatsit-Objective-C.symbols.json")) } defer { @@ -635,11 +631,11 @@ class AutomaticCurationTests: XCTestCase { ) } - func testTypeSubscriptsAreCuratedProperly() throws { + func testTypeSubscriptsAreCuratedProperly() async throws { let symbolURL = Bundle.module.url( forResource: "TypeSubscript.symbols", withExtension: "json", subdirectory: "Test Resources")! - let (bundleURL, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + let (bundleURL, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in try? FileManager.default.copyItem(at: symbolURL, to: url.appendingPathComponent("TypeSubscript.symbols.json")) } defer { @@ -670,8 +666,8 @@ class AutomaticCurationTests: XCTestCase { ) } - func testCPlusPlusSymbolsAreCuratedProperly() throws { - let (bundle, context) = try testBundleAndContext(named: "CxxSymbols") + func testCPlusPlusSymbolsAreCuratedProperly() async throws { + let (bundle, context) = try await testBundleAndContext(named: "CxxSymbols") let rootDocumentationNode = try context.entity( with: .init( @@ -702,8 +698,8 @@ class AutomaticCurationTests: XCTestCase { // Ensures that manually curated sample code articles are not also // automatically curated. - func testSampleCodeArticlesRespectManualCuration() throws { - let renderNode = try renderNode(atPath: "/documentation/SomeSample", fromTestBundleNamed: "SampleBundle") + func testSampleCodeArticlesRespectManualCuration() async throws { + let renderNode = try await renderNode(atPath: "/documentation/SomeSample", fromTestBundleNamed: "SampleBundle") guard renderNode.topicSections.count == 2 else { XCTFail("Expected to find '2' topic sections. Found: \(renderNode.topicSections.count.description.singleQuoted).") @@ -731,10 +727,10 @@ class AutomaticCurationTests: XCTestCase { ) } - func testOverloadedSymbolsAreCuratedUnderGroup() throws { + func testOverloadedSymbolsAreCuratedUnderGroup() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let protocolRenderNode = try renderNode( + let protocolRenderNode = try await renderNode( atPath: "/documentation/ShapeKit/OverloadedProtocol", fromTestBundleNamed: "OverloadedSymbols") @@ -748,7 +744,7 @@ class AutomaticCurationTests: XCTestCase { "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)" ]) - let overloadGroupRenderNode = try renderNode( + let overloadGroupRenderNode = try await renderNode( atPath: "/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)", fromTestBundleNamed: "OverloadedSymbols") @@ -758,10 +754,10 @@ class AutomaticCurationTests: XCTestCase { ) } - func testAutomaticCurationHandlesOverloadsWithLanguageFilters() throws { + func testAutomaticCurationHandlesOverloadsWithLanguageFilters() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let (bundle, context) = try testBundleAndContext(named: "OverloadedSymbols") + let (bundle, context) = try await testBundleAndContext(named: "OverloadedSymbols") let protocolDocumentationNode = try context.entity( with: .init( @@ -799,10 +795,10 @@ class AutomaticCurationTests: XCTestCase { try assertAutomaticCuration(variants: [.swift]) } - func testAutomaticCurationDropsOverloadGroupWhenOverloadsAreCurated() throws { + func testAutomaticCurationDropsOverloadGroupWhenOverloadsAreCurated() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let (_, bundle, context) = try testBundleAndContext(copying: "OverloadedSymbols") { url in + let (_, bundle, context) = try await testBundleAndContext(copying: "OverloadedSymbols") { url in try """ # ``OverloadedProtocol`` @@ -825,7 +821,7 @@ class AutomaticCurationTests: XCTestCase { // Compile the render node to flex the automatic curator let symbol = protocolDocumentationNode.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: protocolDocumentationNode.reference) + var translator = RenderNodeTranslator(context: context, identifier: protocolDocumentationNode.reference) let renderNode = translator.visit(symbol) as! RenderNode XCTAssertEqual(renderNode.topicSections.count, 2) @@ -848,7 +844,7 @@ class AutomaticCurationTests: XCTestCase { ]) } - func testCuratingTopLevelSymbolUnderModuleStopsAutomaticCuration() throws { + func testCuratingTopLevelSymbolUnderModuleStopsAutomaticCuration() async throws { let catalog = Folder(name: "Something.docc", content: [ JSONFile(name: "Something.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: [ makeSymbol(id: "first-symbol-id", kind: .class, pathComponents: ["FirstClass"]), @@ -862,7 +858,7 @@ class AutomaticCurationTests: XCTestCase { - ``SecondClass`` """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -873,7 +869,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssertFalse(secondNode.shouldAutoCurateInCanonicalLocation, "This symbol is manually curated under its module") } - func testCuratingTopLevelSymbolUnderAPICollectionInModuleStopsAutomaticCuration() throws { + func testCuratingTopLevelSymbolUnderAPICollectionInModuleStopsAutomaticCuration() async throws { let catalog = Folder(name: "Something.docc", content: [ JSONFile(name: "Something.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: [ makeSymbol(id: "first-symbol-id", kind: .class, pathComponents: ["FirstClass"]), @@ -894,7 +890,7 @@ class AutomaticCurationTests: XCTestCase { - """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -908,7 +904,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssertFalse(apiCollectionNode.shouldAutoCurateInCanonicalLocation, "Any curation of non-symbols stops automatic curation") } - func testCuratingTopLevelSymbolUnderOtherTopLevelSymbolStopsAutomaticCuration() throws { + func testCuratingTopLevelSymbolUnderOtherTopLevelSymbolStopsAutomaticCuration() async throws { let catalog = Folder(name: "Something.docc", content: [ JSONFile(name: "Something.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: [ makeSymbol(id: "first-symbol-id", kind: .class, pathComponents: ["FirstClass"]), @@ -922,7 +918,7 @@ class AutomaticCurationTests: XCTestCase { - ``SecondClass`` """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -933,7 +929,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssertFalse(secondNode.shouldAutoCurateInCanonicalLocation, "Curating a top-level symbol under another top-level symbol stops automatic curation") } - func testCuratingTopLevelSymbolUnderOtherTopLevelSymbolAPICollectionStopsAutomaticCuration() throws { + func testCuratingTopLevelSymbolUnderOtherTopLevelSymbolAPICollectionStopsAutomaticCuration() async throws { let catalog = Folder(name: "Something.docc", content: [ JSONFile(name: "Something.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: [ makeSymbol(id: "first-symbol-id", kind: .class, pathComponents: ["FirstClass"]), @@ -954,7 +950,7 @@ class AutomaticCurationTests: XCTestCase { - """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -965,7 +961,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssertFalse(secondNode.shouldAutoCurateInCanonicalLocation, "Curating a top-level symbol under another top-level symbol's API collection stops automatic curation") } - func testCuratingTopLevelSymbolUnderDeeperThanTopLevelDoesNotStopAutomaticCuration() throws { + func testCuratingTopLevelSymbolUnderDeeperThanTopLevelDoesNotStopAutomaticCuration() async throws { let catalog = Folder(name: "Something.docc", content: [ JSONFile(name: "Something.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: [ makeSymbol(id: "first-symbol-id", kind: .class, pathComponents: ["FirstClass"]), @@ -982,7 +978,7 @@ class AutomaticCurationTests: XCTestCase { - ``SecondClass`` """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -995,7 +991,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssert(secondNode.shouldAutoCurateInCanonicalLocation, "Curating a top-level symbol deeper than top-level doesn't stops automatic curation") } - func testCuratingMemberOutsideCanonicalContainerDoesNotStopAutomaticCuration() throws { + func testCuratingMemberOutsideCanonicalContainerDoesNotStopAutomaticCuration() async throws { let catalog = Folder(name: "Something.docc", content: [ JSONFile(name: "Something.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: [ makeSymbol(id: "first-symbol-id", kind: .class, pathComponents: ["FirstClass"]), @@ -1011,7 +1007,7 @@ class AutomaticCurationTests: XCTestCase { - ``FirstClass/firstMember`` """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -1022,7 +1018,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssert(memberNode.shouldAutoCurateInCanonicalLocation, "Curation of member outside its canonical container's hierarchy doesn't stop automatic curation") } - func testCuratingMemberUnderAPICollectionOutsideCanonicalContainerDoesNotStopAutomaticCuration() throws { + func testCuratingMemberUnderAPICollectionOutsideCanonicalContainerDoesNotStopAutomaticCuration() async throws { let catalog = Folder(name: "Something.docc", content: [ JSONFile(name: "Something.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: [ makeSymbol(id: "first-symbol-id", kind: .class, pathComponents: ["FirstClass"]), @@ -1045,7 +1041,7 @@ class AutomaticCurationTests: XCTestCase { - """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -1056,7 +1052,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssert(memberNode.shouldAutoCurateInCanonicalLocation, "Curation of member outside its canonical container's hierarchy doesn't stop automatic curation") } - func testCuratingMemberInCanonicalContainerStopsAutomaticCuration() throws { + func testCuratingMemberInCanonicalContainerStopsAutomaticCuration() async throws { let outerContainerID = "outer-container-symbol-id" let innerContainerID = "inner-container-symbol-id" let memberID = "some-member-symbol-id" @@ -1077,7 +1073,7 @@ class AutomaticCurationTests: XCTestCase { - ``FirstClass/firstMember`` """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -1088,7 +1084,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssertFalse(memberNode.shouldAutoCurateInCanonicalLocation) } - func testCuratingMemberInLevelsOfAPICollectionsStopsAutomaticCuration() throws { + func testCuratingMemberInLevelsOfAPICollectionsStopsAutomaticCuration() async throws { let outerContainerID = "outer-container-symbol-id" let innerContainerID = "inner-container-symbol-id" let memberID = "some-member-symbol-id" @@ -1123,7 +1119,7 @@ class AutomaticCurationTests: XCTestCase { - """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -1134,7 +1130,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssertFalse(memberNode.shouldAutoCurateInCanonicalLocation) } - func testCuratingMemberUnderOtherMemberDoesNotStopAutomaticCuration() throws { + func testCuratingMemberUnderOtherMemberDoesNotStopAutomaticCuration() async throws { let outerContainerID = "outer-container-symbol-id" let innerContainerID = "inner-container-symbol-id" let memberID = "some-member-symbol-id" @@ -1156,7 +1152,7 @@ class AutomaticCurationTests: XCTestCase { - ``OuterClass/someMember`` """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -1170,7 +1166,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssert(memberNode.shouldAutoCurateInCanonicalLocation, "Curating a member under another member doesn't stop automatic curation") } - func testCuratingArticleAnywhereStopAutomaticCuration() throws { + func testCuratingArticleAnywhereStopAutomaticCuration() async throws { let catalog = Folder(name: "Something.docc", content: [ JSONFile(name: "Something.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: [ makeSymbol(id: "first-symbol-id", kind: .class, pathComponents: ["FirstClass"]), @@ -1195,7 +1191,7 @@ class AutomaticCurationTests: XCTestCase { # First article """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -1210,7 +1206,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssertFalse(secondArticleNode.shouldAutoCurateInCanonicalLocation) } - func testAutomaticallyCuratedSymbolTopicsAreMergedWithManuallyCuratedTopics() throws { + func testAutomaticallyCuratedSymbolTopicsAreMergedWithManuallyCuratedTopics() async throws { for kind in availableNonExtensionSymbolKinds { let containerID = "some-container-id" let memberID = "some-member-id" @@ -1242,12 +1238,12 @@ class AutomaticCurationTests: XCTestCase { """), ]) let catalogURL = try exampleDocumentation.write(inside: createTemporaryDirectory()) - let (_, bundle, context) = try loadBundle(from: catalogURL) + let (_, _, context) = try await loadBundle(from: catalogURL) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleName/SomeClass", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleName/SomeClass", sourceLanguage: .swift)) // Compile docs and verify the generated Topics section - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(node.semantic) as? RenderNode) // Verify that there are no duplicate sections in `SomeClass`'s "Topics" section @@ -1266,7 +1262,7 @@ class AutomaticCurationTests: XCTestCase { } } - func testAutomaticallyCuratedArticlesAreSortedByTitle() throws { + func testAutomaticallyCuratedArticlesAreSortedByTitle() async throws { // Test bundle with articles where file names and titles are in different orders let catalog = Folder(name: "TestBundle.docc", content: [ JSONFile(name: "TestModule.symbols.json", content: makeSymbolGraph(moduleName: "TestModule")), @@ -1285,7 +1281,7 @@ class AutomaticCurationTests: XCTestCase { ]) // Load the bundle - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") // Get the module and its automatic curation groups @@ -1309,7 +1305,7 @@ class AutomaticCurationTests: XCTestCase { // autoCuratedArticles are sorted by title in a case-insensitive manner // this test verifies that the sorting is correct even when the file names have different cases - func testAutomaticallyCuratedArticlesAreSortedByTitleDifferentCases() throws { + func testAutomaticallyCuratedArticlesAreSortedByTitleDifferentCases() async throws { // In the catalog, the articles are named with the same letter, different cases, // and other articles are added as well @@ -1351,7 +1347,7 @@ class AutomaticCurationTests: XCTestCase { ]) // Load the bundle - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") // Get the module and its automatic curation groups diff --git a/Tests/SwiftDocCTests/Infrastructure/BundleDiscoveryTests.swift b/Tests/SwiftDocCTests/Infrastructure/BundleDiscoveryTests.swift index bd543071c2..323fe5a2c5 100644 --- a/Tests/SwiftDocCTests/Infrastructure/BundleDiscoveryTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/BundleDiscoveryTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -26,97 +26,6 @@ class BundleDiscoveryTests: XCTestCase { return files } - // This tests registration of multiple catalogs which is deprecated - // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - func testFirstBundle() throws { - let url = try createTemporaryDirectory() - // Create 3 minimal doc bundles - for i in 1 ... 3 { - let nestedBundle = Folder(name: "TestBundle\(i).docc", content: [ - InfoPlist(displayName: "Test Bundle \(i)", identifier: "com.example.bundle\(i)"), - TextFile(name: "Root.md", utf8Content: """ - # Test Bundle \(i) - @Metadata { - @TechnologyRoot - } - Abstract. - - Content. - """), - ]) - _ = try nestedBundle.write(inside: url) - } - - let workspace = DocumentationWorkspace() - let context = try DocumentationContext(dataProvider: workspace) - let dataProvider = try LocalFileSystemDataProvider(rootURL: url) - try workspace.registerProvider(dataProvider) - - // Verify all bundles are loaded - XCTAssertEqual(context.registeredBundles.map { $0.identifier }.sorted(), - ["com.example.bundle1", "com.example.bundle2", "com.example.bundle3"] - ) - - // Verify the first one is bundle1 - let converter = DocumentationConverter(documentationBundleURL: url, emitDigest: false, documentationCoverageOptions: .noCoverage, currentPlatforms: nil, workspace: workspace, context: context, dataProvider: dataProvider, bundleDiscoveryOptions: .init()) - XCTAssertEqual(converter.firstAvailableBundle()?.identifier, "com.example.bundle1") - } - - // This test registration more than once data provider which is deprecated. - // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - func testLoadComplexWorkspace() throws { - let allFiles = try flatListOfFiles() - let workspace = Folder(name: "TestWorkspace", content: [ - CopyOfFolder(original: testBundleLocation), - Folder(name: "nested", content: [ - Folder(name: "irrelevant", content: [ - TextFile(name: "irrelevant.txt", utf8Content: "distraction"), - ]), - TextFile(name: "irrelevant.txt", utf8Content: "distraction"), - Folder(name: "TestBundle2.docc", content: [ - InfoPlist(displayName: "Test Bundle", identifier: "com.example.bundle2"), - Folder(name: "Subfolder", content: // All files flattened into one folder - allFiles.map { CopyOfFile(original: $0) } - ), - ]), - ]), - ]) - - let tempURL = try createTemporaryDirectory() - - let workspaceURL = try workspace.write(inside: tempURL) - - let dataProvider = try LocalFileSystemDataProvider(rootURL: workspaceURL) - - let bundles = (try dataProvider.bundles()).sorted { (bundle1, bundle2) -> Bool in - return bundle1.identifier < bundle2.identifier - } - - XCTAssertEqual(bundles.count, 2) - - guard bundles.count == 2 else { return } - - XCTAssertEqual(bundles[0].identifier, "com.example.bundle2") - XCTAssertEqual(bundles[1].identifier, "org.swift.docc.example") - - func checkBundle(_ bundle: DocumentationBundle) { - XCTAssertEqual(bundle.displayName, "Test Bundle") - XCTAssertEqual(bundle.symbolGraphURLs.count, 4) - XCTAssertTrue(bundle.symbolGraphURLs.map { $0.lastPathComponent }.contains("mykit-iOS.symbols.json")) - XCTAssertTrue(bundle.symbolGraphURLs.map { $0.lastPathComponent }.contains("MyKit@SideKit.symbols.json")) - XCTAssertTrue(bundle.symbolGraphURLs.map { $0.lastPathComponent }.contains("sidekit.symbols.json")) - XCTAssertTrue(bundle.symbolGraphURLs.map { $0.lastPathComponent }.contains("FillIntroduced.symbols.json")) - XCTAssertFalse(bundle.markupURLs.isEmpty) - XCTAssertTrue(bundle.miscResourceURLs.map { $0.lastPathComponent }.sorted().contains("intro.png")) - } - - for bundle in bundles { - checkBundle(bundle) - } - } - func testBundleFormat() throws { let allFiles = try flatListOfFiles() diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationBundleTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationBundleTests.swift index 8d03e4f305..4671760ea2 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationBundleTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationBundleTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -9,7 +9,6 @@ */ import XCTest -@testable import SwiftDocC class DocumentationBundleTests: XCTestCase { // Test whether the bundle correctly loads a documentation source folder. diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+MixedLanguageLinkResolutionTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+MixedLanguageLinkResolutionTests.swift index 06d2eb6ac7..24fc0d736f 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+MixedLanguageLinkResolutionTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+MixedLanguageLinkResolutionTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022-2024 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -13,8 +13,8 @@ import XCTest class DocumentationContext_MixedLanguageLinkResolutionTests: XCTestCase { - func testResolvingLinksWhenSymbolHasSameNameInBothLanguages() throws { - let (_, _, context) = try testBundleAndContext(copying: "MixedLanguageFrameworkComplexLinks") { url in + func testResolvingLinksWhenSymbolHasSameNameInBothLanguages() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "MixedLanguageFrameworkComplexLinks") { url in let swiftSymbolGraph = url.appendingPathComponent("symbol-graph/swift/ObjCLinks.symbols.json") try String(contentsOf: swiftSymbolGraph) .replacingOccurrences(of: "FooSwift", with: "FooObjC") diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+MixedLanguageSourceLanguagesTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+MixedLanguageSourceLanguagesTests.swift index 17d1606726..2206ad29ea 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+MixedLanguageSourceLanguagesTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+MixedLanguageSourceLanguagesTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -12,22 +12,22 @@ import XCTest @testable import SwiftDocC class DocumentationContext_MixedLanguageSourceLanguagesTests: XCTestCase { - func testArticleAvailableSourceLanguagesIsSwiftInSwiftModule() throws { - try assertArticleAvailableSourceLanguages( + func testArticleAvailableSourceLanguagesIsSwiftInSwiftModule() async throws { + try await assertArticleAvailableSourceLanguages( moduleAvailableLanguages: [.swift], expectedArticleDefaultLanguage: .swift ) } - func testArticleAvailableSourceLanguagesIsMixedLanguageInMixedLanguageModule() throws { - try assertArticleAvailableSourceLanguages( + func testArticleAvailableSourceLanguagesIsMixedLanguageInMixedLanguageModule() async throws { + try await assertArticleAvailableSourceLanguages( moduleAvailableLanguages: [.swift, .objectiveC], expectedArticleDefaultLanguage: .swift ) } - func testArticleAvailableSourceLanguagesIsObjectiveCInObjectiveCModule() throws { - try assertArticleAvailableSourceLanguages( + func testArticleAvailableSourceLanguagesIsObjectiveCInObjectiveCModule() async throws { + try await assertArticleAvailableSourceLanguages( moduleAvailableLanguages: [.objectiveC], expectedArticleDefaultLanguage: .objectiveC ) @@ -38,13 +38,13 @@ class DocumentationContext_MixedLanguageSourceLanguagesTests: XCTestCase { expectedArticleDefaultLanguage: SourceLanguage, file: StaticString = #filePath, line: UInt = #line - ) throws { + ) async throws { precondition( moduleAvailableLanguages.allSatisfy { [.swift, .objectiveC].contains($0) }, "moduleAvailableLanguages can only contain Swift and Objective-C as languages." ) - let (_, _, context) = try testBundleAndContext(copying: "MixedLanguageFramework") { url in + let (_, _, context) = try await testBundleAndContext(copying: "MixedLanguageFramework") { url in try """ # MyArticle diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift index aab9b5b857..9523dc0966 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -14,8 +14,8 @@ import SymbolKit import SwiftDocCTestUtilities class DocumentationContext_RootPageTests: XCTestCase { - func testArticleOnlyCatalogWithExplicitTechnologyRoot() throws { - let (_, context) = try loadBundle(catalog: + func testArticleOnlyCatalogWithExplicitTechnologyRoot() async throws { + let (_, context) = try await loadBundle(catalog: Folder(name: "no-sgf-test.docc", content: [ // Root page for the collection TextFile(name: "ReleaseNotes.md", utf8Content: """ @@ -50,8 +50,8 @@ class DocumentationContext_RootPageTests: XCTestCase { ["/documentation/TestBundle/ReleaseNotes-1.2"]) } - func testWarnsAboutExtensionFileTechnologyRoot() throws { - let (_, context) = try loadBundle(catalog: + func testWarnsAboutExtensionFileTechnologyRoot() async throws { + let (_, context) = try await loadBundle(catalog: Folder(name: "no-sgf-test.docc", content: [ // Root page for the collection TextFile(name: "ReleaseNotes.md", utf8Content: """ @@ -84,8 +84,8 @@ class DocumentationContext_RootPageTests: XCTestCase { XCTAssertEqual(solution.replacements.first?.range.upperBound.line, 3) } - func testSingleArticleWithoutTechnologyRootDirective() throws { - let (_, context) = try loadBundle(catalog: + func testSingleArticleWithoutTechnologyRootDirective() async throws { + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ TextFile(name: "Article.md", utf8Content: """ # My article @@ -101,8 +101,8 @@ class DocumentationContext_RootPageTests: XCTestCase { XCTAssertEqual(context.problems.count, 0) } - func testMultipleArticlesWithoutTechnologyRootDirective() throws { - let (_, context) = try loadBundle(catalog: + func testMultipleArticlesWithoutTechnologyRootDirective() async throws { + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ TextFile(name: "First.md", utf8Content: """ # My first article @@ -135,8 +135,8 @@ class DocumentationContext_RootPageTests: XCTestCase { XCTAssertEqual(context.problems.count, 0) } - func testMultipleArticlesWithoutTechnologyRootDirectiveWithOneMatchingTheCatalogName() throws { - let (_, context) = try loadBundle(catalog: + func testMultipleArticlesWithoutTechnologyRootDirectiveWithOneMatchingTheCatalogName() async throws { + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ TextFile(name: "Something.md", utf8Content: """ # Some article diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift index b9e67fcb4c..7b48aa7794 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -36,54 +36,8 @@ extension CollectionDifference { } class DocumentationContextTests: XCTestCase { - // This test checks unregistration of workspace data providers which is deprecated - // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - func testResolve() throws { - let workspace = DocumentationWorkspace() - let context = try DocumentationContext(dataProvider: workspace) - let bundle = try testBundle(named: "LegacyBundle_DoNotUseInNewTests") - let dataProvider = PrebuiltLocalFileSystemDataProvider(bundles: [bundle]) - try workspace.registerProvider(dataProvider) - - // Test resolving - let unresolved = UnresolvedTopicReference(topicURL: ValidatedURL(parsingExact: "doc:/TestTutorial")!) - let parent = ResolvedTopicReference(bundleIdentifier: bundle.id.rawValue, path: "", sourceLanguage: .swift) - - guard case let .success(resolved) = context.resolve(.unresolved(unresolved), in: parent) else { - XCTFail("Couldn't resolve \(unresolved)") - return - } - - XCTAssertEqual(parent.bundleIdentifier, resolved.bundleIdentifier) - XCTAssertEqual("/tutorials/Test-Bundle/TestTutorial", resolved.path) - - // Test lowercasing of path - let unresolvedUppercase = UnresolvedTopicReference(topicURL: ValidatedURL(parsingExact: "doc:/TESTTUTORIAL")!) - guard case .failure = context.resolve(.unresolved(unresolvedUppercase), in: parent) else { - XCTFail("Did incorrectly resolve \(unresolvedUppercase)") - return - } - - // Test expected URLs - let expectedURL = URL(string: "doc://org.swift.docc.example/tutorials/Test-Bundle/TestTutorial") - XCTAssertEqual(expectedURL, resolved.url) - - guard context.documentURL(for: resolved) != nil else { - XCTFail("Couldn't resolve file URL for \(resolved)") - return - } - - try workspace.unregisterProvider(dataProvider) - - guard case .failure = context.resolve(.unresolved(unresolved), in: parent) else { - XCTFail("Unexpectedly resolved \(unresolved.topicURL) despite removing a data provider for it") - return - } - } - - func testLoadEntity() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testLoadEntity() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let identifier = ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift) @@ -416,13 +370,13 @@ class DocumentationContextTests: XCTestCase { XCTAssertEqual(expectedDump, node.markup.debugDescription(), diffDescription(lhs: expectedDump, rhs: node.markup.debugDescription())) } - func testThrowsErrorForMissingResource() throws { - let (_, context) = try testBundleAndContext() + func testThrowsErrorForMissingResource() async throws { + let (_, context) = try await testBundleAndContext() XCTAssertThrowsError(try context.resource(with: ResourceReference(bundleID: "com.example.missing", path: "/missing.swift")), "Expected requesting an unknown file to result in an error.") } - func testThrowsErrorForQualifiedImagePaths() throws { - let (bundle, context) = try loadBundle(catalog: Folder(name: "unit-test.docc", content: [ + func testThrowsErrorForQualifiedImagePaths() async throws { + let (bundle, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ DataFile(name: "figure1.jpg", data: Data()) ])) let id = bundle.id @@ -434,8 +388,8 @@ class DocumentationContextTests: XCTestCase { XCTAssertThrowsError(try context.resource(with: imageFigure), "Images should be registered (and referred to) by their name, not by their path.") } - func testResourceExists() throws { - let (bundle, context) = try loadBundle(catalog: Folder(name: "unit-test.docc", content: [ + func testResourceExists() async throws { + let (bundle, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ DataFile(name: "figure1.jpg", data: Data()), DataFile(name: "introposter.jpg", data: Data()), ])) @@ -475,7 +429,7 @@ class DocumentationContextTests: XCTestCase { ) } - func testURLs() throws { + func testURLs() async throws { let exampleDocumentation = Folder(name: "unit-test.docc", content: [ Folder(name: "Symbols", content: []), Folder(name: "Resources", content: [ @@ -504,7 +458,7 @@ class DocumentationContextTests: XCTestCase { ]) // Parse this test content - let (_, context) = try loadBundle(catalog: exampleDocumentation) + let (_, context) = try await loadBundle(catalog: exampleDocumentation) // Verify all the reference identifiers for this content XCTAssertEqual(context.knownIdentifiers.count, 3) @@ -518,8 +472,8 @@ class DocumentationContextTests: XCTestCase { ]) } - func testRegisteredImages() throws { - let (bundle, context) = try loadBundle(catalog: Folder(name: "unit-test.docc", content: [ + func testRegisteredImages() async throws { + let (bundle, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ DataFile(name: "figure1.jpg", data: Data()), DataFile(name: "figure1.png", data: Data()), DataFile(name: "figure1~dark.png", data: Data()), @@ -558,8 +512,8 @@ class DocumentationContextTests: XCTestCase { ) } - func testExternalAssets() throws { - let (bundle, context) = try testBundleAndContext() + func testExternalAssets() async throws { + let (bundle, context) = try await testBundleAndContext() let image = context.resolveAsset(named: "https://example.com/figure.png", in: bundle.rootReference) XCTAssertNotNil(image) @@ -576,8 +530,8 @@ class DocumentationContextTests: XCTestCase { XCTAssertEqual(video.variants, [DataTraitCollection(userInterfaceStyle: .light, displayScale: .standard): URL(string: "https://example.com/introvideo.mp4")!]) } - func testDownloadAssets() throws { - let (bundle, context) = try loadBundle(catalog: Folder(name: "unit-test.docc", content: [ + func testDownloadAssets() async throws { + let (bundle, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ DataFile(name: "intro.png", data: Data()), DataFile(name: "project.zip", data: Data()), @@ -651,37 +605,8 @@ class DocumentationContextTests: XCTestCase { XCTAssertEqual(downloadsAfter.first?.variants.values.first?.lastPathComponent, "intro.png") } - // This test registration more than once data provider which is deprecated. - // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - func testCreatesCorrectIdentifiers() throws { - let testBundleLocation = Bundle.module.url( - forResource: "LegacyBundle_DoNotUseInNewTests", withExtension: "docc", subdirectory: "Test Bundles")! - let workspaceContent = Folder(name: "TestWorkspace", content: [ - CopyOfFolder(original: testBundleLocation), - - Folder(name: "TestBundle2.docc", content: [ - InfoPlist(displayName: "Test Bundle", identifier: "com.example.bundle2"), - CopyOfFolder(original: testBundleLocation, newName: "Subfolder", filter: { $0.lastPathComponent != "Info.plist" }), - ]) - ]) - - let tempURL = try createTemporaryDirectory() - - let workspaceURL = try workspaceContent.write(inside: tempURL) - let dataProvider = try LocalFileSystemDataProvider(rootURL: workspaceURL) - - let workspace = DocumentationWorkspace() - try workspace.registerProvider(dataProvider) - - let context = try DocumentationContext(dataProvider: workspace) - let identifiers = context.knownIdentifiers - let identifierSet = Set(identifiers) - XCTAssertEqual(identifiers.count, identifierSet.count, "Found duplicate identifiers.") - } - - func testDetectsReferenceCollision() throws { - let (_, context) = try testBundleAndContext(named: "TestBundleWithDupe") + func testDetectsReferenceCollision() async throws { + let (_, context) = try await testBundleAndContext(named: "TestBundleWithDupe") let problemWithDuplicate = context.problems.filter { $0.diagnostic.identifier == "org.swift.docc.DuplicateReference" } @@ -692,8 +617,8 @@ class DocumentationContextTests: XCTestCase { } - func testDetectsMultipleMarkdownFilesWithSameName() throws { - let (_, context) = try testBundleAndContext(named: "TestBundleWithDupMD") + func testDetectsMultipleMarkdownFilesWithSameName() async throws { + let (_, context) = try await testBundleAndContext(named: "TestBundleWithDupMD") let problemWithDuplicateReference = context.problems.filter { $0.diagnostic.identifier == "org.swift.docc.DuplicateReference" } @@ -706,7 +631,7 @@ class DocumentationContextTests: XCTestCase { XCTAssertEqual(localizedSummarySecond, "Redeclaration of \'overview.md\'; this file will be skipped") } - func testUsesMultipleDocExtensionFilesWithSameName() throws { + func testUsesMultipleDocExtensionFilesWithSameName() async throws { // Generate 2 different symbols with the same name. let someSymbol = makeSymbol(id: "someEnumSymbol-id", kind: .init(rawValue: "enum"), pathComponents: ["SomeDirectory", "MyEnum"]) @@ -751,7 +676,7 @@ class DocumentationContextTests: XCTestCase { ) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) // Since documentation extensions' filenames have no impact on the URL of pages, we should not see warnings enforcing unique filenames for them. let problemWithDuplicateReference = context.problems.filter { $0.diagnostic.identifier == "org.swift.docc.DuplicateReference" } @@ -767,7 +692,7 @@ class DocumentationContextTests: XCTestCase { XCTAssertEqual(anotherEnumSymbol.abstract?.plainText, "A documentation extension for an unrelated enum.", "The abstract should be from the symbol's documentation extension.") } - func testGraphChecks() throws { + func testGraphChecks() async throws { var configuration = DocumentationContext.Configuration() configuration.topicAnalysisConfiguration.additionalChecks.append( { (context, reference) -> [Problem] in @@ -780,7 +705,7 @@ class DocumentationContextTests: XCTestCase { # Some root page """) ]) - let (_, context) = try loadBundle(catalog: catalog, configuration: configuration) + let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration) /// Checks if the custom check added problems to the context. let testProblems = context.problems.filter({ (problem) -> Bool in @@ -804,7 +729,7 @@ class DocumentationContextTests: XCTestCase { } } - func testIgnoresUnknownMarkupFiles() throws { + func testIgnoresUnknownMarkupFiles() async throws { let testCatalog = Folder(name: "TestIgnoresUnknownMarkupFiles.docc", content: [ InfoPlist(displayName: "TestIgnoresUnknownMarkupFiles", identifier: "com.example.documentation"), Folder(name: "Resources", content: [ @@ -813,13 +738,13 @@ class DocumentationContextTests: XCTestCase { ]) ]) - let (_, context) = try loadBundle(catalog: testCatalog) + let (_, context) = try await loadBundle(catalog: testCatalog) XCTAssertEqual(context.knownPages.map { $0.path }, ["/tutorials/TestIgnoresUnknownMarkupFiles/Article1"]) XCTAssertTrue(context.problems.map { $0.diagnostic.identifier }.contains("org.swift.docc.Article.Title.NotFound")) } - func testLoadsSymbolData() throws { + func testLoadsSymbolData() async throws { let testCatalog = Folder(name: "TestIgnoresUnknownMarkupFiles.docc", content: [ InfoPlist(displayName: "TestIgnoresUnknownMarkupFiles", identifier: "com.example.documentation"), Folder(name: "Resources", content: [ @@ -835,7 +760,7 @@ class DocumentationContextTests: XCTestCase { ]) ]) - let (_, context) = try loadBundle(catalog: testCatalog) + let (_, context) = try await loadBundle(catalog: testCatalog) // Symbols are loaded XCTAssertFalse(context.documentationCache.isEmpty) @@ -1056,8 +981,128 @@ class DocumentationContextTests: XCTestCase { └─ Text "Return value" """) } - - func testMergesMultipleSymbolDeclarations() throws { + + func testLoadsConflictingDocComments() async throws { + let macOSSymbolGraph = makeSymbolGraph( + moduleName: "TestProject", + platform: .init(operatingSystem: .init(name: "macOS")), + symbols: [ + makeSymbol( + id: "TestSymbol", + kind: .func, + pathComponents: ["TestSymbol"], + docComment: "This is a comment.", + otherMixins: [ + SymbolGraph.Symbol.DeclarationFragments(declarationFragments: [ + .init( + kind: .text, + spelling: "TestSymbol Mac", + preciseIdentifier: nil) + ]) + ]) + ]) + let iOSSymbolGraph = makeSymbolGraph( + moduleName: "TestProject", + platform: .init(operatingSystem: .init(name: "iOS")), + symbols: [ + makeSymbol( + id: "TestSymbol", + kind: .func, + pathComponents: ["TestSymbol"], + docComment: "This is a longer comment that should be shown instead.", + otherMixins: [ + SymbolGraph.Symbol.DeclarationFragments(declarationFragments: [ + .init( + kind: .text, + spelling: "TestSymbol iOS", + preciseIdentifier: nil) + ]) + ]) + ]) + + for forwards in [true, false] { + let catalog = Folder(name: "unit-test.docc", content: [ + InfoPlist(displayName: "TestProject", identifier: "com.test.example"), + JSONFile(name: "symbols\(forwards ? "1" : "2").symbols.json", content:macOSSymbolGraph), + JSONFile(name: "symbols\(forwards ? "2" : "1").symbols.json", content: iOSSymbolGraph), + ]) + + let (bundle, context) = try await loadBundle(catalog: catalog) + + let reference = ResolvedTopicReference( + bundleID: bundle.id, + path: "/documentation/TestProject/TestSymbol", + sourceLanguage: .swift + ) + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + let abstract = try XCTUnwrap(symbol.abstractSection) + XCTAssertEqual( + abstract.paragraph.plainText, + "This is a longer comment that should be shown instead.") + } + } + + func testLoadsConflictingDocCommentsOfSameLength() async throws { + let macOSSymbolGraph = makeSymbolGraph( + moduleName: "TestProject", + platform: .init(operatingSystem: .init(name: "macOS")), + symbols: [ + makeSymbol( + id: "TestSymbol", + kind: .func, + pathComponents: ["TestSymbol"], + docComment: "Comment A.", + otherMixins: [ + SymbolGraph.Symbol.DeclarationFragments(declarationFragments: [ + .init( + kind: .text, + spelling: "TestSymbol Mac", + preciseIdentifier: nil) + ]) + ]) + ]) + let iOSSymbolGraph = makeSymbolGraph( + moduleName: "TestProject", + platform: .init(operatingSystem: .init(name: "iOS")), + symbols: [ + makeSymbol( + id: "TestSymbol", + kind: .func, + pathComponents: ["TestSymbol"], + docComment: "Comment B.", + otherMixins: [ + SymbolGraph.Symbol.DeclarationFragments(declarationFragments: [ + .init( + kind: .text, + spelling: "TestSymbol iOS", + preciseIdentifier: nil) + ]) + ]) + ]) + + for forwards in [true, false] { + let catalog = Folder(name: "unit-test.docc", content: [ + InfoPlist(displayName: "TestProject", identifier: "com.test.example"), + JSONFile(name: "symbols\(forwards ? "1" : "2").symbols.json", content:macOSSymbolGraph), + JSONFile(name: "symbols\(forwards ? "2" : "1").symbols.json", content: iOSSymbolGraph), + ]) + + let (bundle, context) = try await loadBundle(catalog: catalog) + + let reference = ResolvedTopicReference( + bundleID: bundle.id, + path: "/documentation/TestProject/TestSymbol", + sourceLanguage: .swift + ) + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + let abstract = try XCTUnwrap(symbol.abstractSection) + XCTAssertEqual( + abstract.paragraph.plainText, + "Comment A.") + } + } + + func testMergesMultipleSymbolDeclarations() async throws { let graphContentiOS = try String(contentsOf: Bundle.module.url( forResource: "LegacyBundle_DoNotUseInNewTests", withExtension: "docc", subdirectory: "Test Bundles")! .appendingPathComponent("mykit-iOS.symbols.json")) @@ -1078,7 +1123,7 @@ class DocumentationContextTests: XCTestCase { ]), ]) - let (_, context) = try loadBundle(catalog: testCatalog) + let (_, context) = try await loadBundle(catalog: testCatalog) // MyClass is loaded guard let myClass = context.documentationCache["s:5MyKit0A5ClassC"], @@ -1094,7 +1139,7 @@ class DocumentationContextTests: XCTestCase { XCTAssertNotNil(myClassSymbol.declaration[[PlatformName(operatingSystemName: "ios"), PlatformName(operatingSystemName: "macos")]] ?? myClassSymbol.declaration[[PlatformName(operatingSystemName: "macos"), PlatformName(operatingSystemName: "ios")]]) } - func testMergedMultipleSymbolDeclarationsIncludesPlatformSpecificSymbols() throws { + func testMergedMultipleSymbolDeclarationsIncludesPlatformSpecificSymbols() async throws { let iOSGraphURL = Bundle.module.url( forResource: "LegacyBundle_DoNotUseInNewTests", withExtension: "docc", subdirectory: "Test Bundles")! .appendingPathComponent("mykit-iOS.symbols.json") @@ -1135,7 +1180,7 @@ class DocumentationContextTests: XCTestCase { ]) ]) - let (_, context) = try loadBundle(catalog: testCatalog) + let (_, context) = try await loadBundle(catalog: testCatalog) // MyFunction is loaded XCTAssertNotNil(context.documentationCache[myFunctionSymbolPreciseIdentifier], "myFunction which only exist on iOS should be found in the graph") @@ -1148,7 +1193,7 @@ class DocumentationContextTests: XCTestCase { ) } - func testResolvesSymbolsBetweenSymbolGraphs() throws { + func testResolvesSymbolsBetweenSymbolGraphs() async throws { let testCatalog = Folder(name: "CrossGraphResolving.docc", content: [ InfoPlist(displayName: "CrossGraphResolving", identifier: "com.example.documentation"), Folder(name: "Resources", content: [ @@ -1163,7 +1208,7 @@ class DocumentationContextTests: XCTestCase { ]) ]) - let (_, context) = try loadBundle(catalog: testCatalog) + let (_, context) = try await loadBundle(catalog: testCatalog) // SideClass is loaded guard let sideClass = context.documentationCache["s:7SideKit0A5ClassC"], @@ -1178,7 +1223,7 @@ class DocumentationContextTests: XCTestCase { }) } - func testLoadsDeclarationWithNoOS() throws { + func testLoadsDeclarationWithNoOS() async throws { var graphContentiOS = try String(contentsOf: Bundle.module.url( forResource: "LegacyBundle_DoNotUseInNewTests", withExtension: "docc", subdirectory: "Test Bundles")! .appendingPathComponent("mykit-iOS.symbols.json")) @@ -1194,7 +1239,7 @@ class DocumentationContextTests: XCTestCase { ]) ]) - let (_, context) = try loadBundle(catalog: testCatalog) + let (_, context) = try await loadBundle(catalog: testCatalog) // MyClass is loaded guard let myClass = context.documentationCache["s:5MyKit0A5ClassC"], @@ -1207,7 +1252,7 @@ class DocumentationContextTests: XCTestCase { XCTAssertNotNil(myClassSymbol.declaration[[nil]]) } - func testDetectsDuplicateSymbolArticles() throws { + func testDetectsDuplicateSymbolArticles() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ makeSymbol(id: "some-symbol-id", kind: .class, pathComponents: ["SomeClass"]) @@ -1226,7 +1271,7 @@ class DocumentationContextTests: XCTestCase { """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) let duplicateExtensionProblems = context.problems.filter { $0.diagnostic.identifier == "org.swift.docc.DuplicateMarkdownTitleSymbolReferences" } let diagnostic = try XCTUnwrap(duplicateExtensionProblems.first).diagnostic @@ -1240,7 +1285,7 @@ class DocumentationContextTests: XCTestCase { XCTAssert(missingMarkupURLs.isEmpty, "\(missingMarkupURLs.map(\.lastPathComponent).sorted()) isn't mentioned in the diagnostic.") } - func testCanResolveArticleFromTutorial() throws { + func testCanResolveArticleFromTutorial() async throws { struct TestData { let symbolGraphNames: [String] @@ -1283,13 +1328,13 @@ class DocumentationContextTests: XCTestCase { """), ] + testData.symbolGraphFiles) - let (bundle, context) = try loadBundle(catalog: testCatalog) - let renderContext = RenderContext(documentationContext: context, bundle: bundle) + let (_, context) = try await loadBundle(catalog: testCatalog) + let renderContext = RenderContext(documentationContext: context) - let identifier = ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/TestOverview", sourceLanguage: .swift) + let identifier = ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/TestOverview", sourceLanguage: .swift) let node = try context.entity(with: identifier) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) let renderNode = try XCTUnwrap(converter.renderNode(for: node)) XCTAssertEqual( @@ -1305,8 +1350,8 @@ class DocumentationContextTests: XCTestCase { } } - func testCuratesSymbolsAndArticlesCorrectly() throws { - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testCuratesSymbolsAndArticlesCorrectly() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // Sort the edges for each node to get consistent results, no matter the order that the symbols were processed. for (source, targets) in context.topicGraph.edges { @@ -1397,11 +1442,11 @@ let expected = """ return (node, tgNode) } - func testSortingBreadcrumbsOfEqualDistanceToRoot() throws { + func testSortingBreadcrumbsOfEqualDistanceToRoot() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph(moduleName: "SomeModuleName")) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) /// @@ -1433,11 +1478,11 @@ let expected = """ XCTAssertEqual(["/documentation/SomeModuleName", "/documentation/SomeModuleName/DDD"], canonicalPathFFF.map({ $0.path })) } - func testSortingBreadcrumbsOfDifferentDistancesToRoot() throws { + func testSortingBreadcrumbsOfDifferentDistancesToRoot() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph(moduleName: "SomeModuleName")) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) let moduleTopicNode = try XCTUnwrap(context.topicGraph.nodeWithReference(moduleReference)) @@ -1475,13 +1520,13 @@ let expected = """ } // Verify that a symbol that has no parents in the symbol graph is automatically curated under the module node. - func testRootSymbolsAreCuratedInModule() throws { + func testRootSymbolsAreCuratedInModule() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph(moduleName: "SomeModuleName", symbols: [ makeSymbol(id: "some-class-id", kind: .class, pathComponents: ["SomeClass"]), ])), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) // Verify the node is a child of the module node when the graph is loaded. let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -1491,9 +1536,9 @@ let expected = """ } /// Tests whether tutorial curated multiple times gets the correct breadcrumbs and hierarchy. - func testCurateTutorialMultipleTimes() throws { + func testCurateTutorialMultipleTimes() async throws { // Curate "TestTutorial" under MyKit as well as TechnologyX. - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in let myKitURL = root.appendingPathComponent("documentation/mykit.md") let text = try String(contentsOf: myKitURL).replacingOccurrences(of: "## Topics", with: """ ## Topics @@ -1518,9 +1563,9 @@ let expected = """ XCTAssertEqual(paths, [["/documentation/MyKit"], ["/documentation/MyKit", "/documentation/Test-Bundle/article"], ["/tutorials/TestOverview", "/tutorials/TestOverview/$volume", "/tutorials/TestOverview/Chapter-1"]]) } - func testNonOverloadPaths() throws { + func testNonOverloadPaths() async throws { // Add some symbol collisions to graph - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in let sideKitURL = root.appendingPathComponent("sidekit.symbols.json") let text = try String(contentsOf: sideKitURL).replacingOccurrences(of: "\"symbols\" : [", with: """ "symbols" : [ @@ -1571,11 +1616,11 @@ let expected = """ XCTAssertNoThrow(try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass/test-swift.var", sourceLanguage: .swift))) } - func testModuleLanguageFallsBackToSwiftIfItHasNoSymbols() throws { + func testModuleLanguageFallsBackToSwiftIfItHasNoSymbols() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph(moduleName: "SomeModuleName")), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual( context.soleRootModuleReference.map { context.sourceLanguages(for: $0) }, @@ -1584,9 +1629,9 @@ let expected = """ ) } - func testOverloadPlusNonOverloadCollisionPaths() throws { + func testOverloadPlusNonOverloadCollisionPaths() async throws { // Add some symbol collisions to graph - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in let sideKitURL = root.appendingPathComponent("sidekit.symbols.json") let text = try String(contentsOf: sideKitURL).replacingOccurrences(of: "\"symbols\" : [", with: """ "symbols" : [ @@ -1656,14 +1701,14 @@ let expected = """ XCTAssertNoThrow(try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass/test-959hd", sourceLanguage: .swift))) } - func testUnknownSymbolKind() throws { + func testUnknownSymbolKind() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph(moduleName: "SomeModuleName", symbols: [ makeSymbol(id: "some-symbol-id", kind: .init(identifier: "blip-blop"), pathComponents: ["SomeUnknownSymbol"]), ])), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) // Get the node, verify its kind is unknown @@ -1671,8 +1716,8 @@ let expected = """ XCTAssertEqual(node.kind, .unknown) } - func testCuratingSymbolsWithSpecialCharacters() throws { - let (_, _, context) = try testBundleAndContext(copying: "InheritedOperators") { root in + func testCuratingSymbolsWithSpecialCharacters() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "InheritedOperators") { root in try """ # ``Operators/MyNumber`` @@ -1710,8 +1755,8 @@ let expected = """ XCTAssertEqual(unresolvedTopicProblems.map(\.diagnostic.summary), [], "All links should resolve without warnings") } - func testOperatorReferences() throws { - let (_, context) = try testBundleAndContext(named: "InheritedOperators") + func testOperatorReferences() async throws { + let (_, context) = try await testBundleAndContext(named: "InheritedOperators") let pageIdentifiersAndNames = Dictionary(uniqueKeysWithValues: try context.knownPages.map { reference in (key: reference.path, value: try context.entity(with: reference).name.description) @@ -1745,7 +1790,7 @@ let expected = """ XCTAssertEqual("/=(_:_:)", pageIdentifiersAndNames["/documentation/Operators/MyNumber/_=(_:_:)"]) } - func testFileNamesWithDifferentPunctuation() throws { + func testFileNamesWithDifferentPunctuation() async throws { let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "Hello-world.md", utf8Content: """ @@ -1779,7 +1824,7 @@ let expected = """ """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.map(\.diagnostic.summary), ["Redeclaration of 'Hello world.md'; this file will be skipped"]) @@ -1792,7 +1837,7 @@ let expected = """ ]) } - func testSpecialCharactersInLinks() throws { + func testSpecialCharactersInLinks() async throws { let catalog = Folder(name: "special-characters.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph( moduleName: "SomeModuleName", @@ -1853,7 +1898,7 @@ let expected = """ """), ]) let bundleURL = try catalog.write(inside: createTemporaryDirectory()) - let (_, bundle, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) let problems = context.problems XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") @@ -1879,7 +1924,7 @@ let expected = """ ) } - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: moduleReference) + var translator = RenderNodeTranslator(context: context, identifier: moduleReference) let renderNode = translator.visit(moduleSymbol) as! RenderNode // Verify that the resolved links rendered as links @@ -1982,9 +2027,9 @@ let expected = """ ) } - func testNonOverloadCollisionFromExtension() throws { + func testNonOverloadCollisionFromExtension() async throws { // Add some symbol collisions to graph - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: ["mykit-iOS.symbols.json"]) { root in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: ["mykit-iOS.symbols.json"]) { root in let sideKitURL = root.appendingPathComponent("something@SideKit.symbols.json") let text = """ { @@ -2046,7 +2091,7 @@ let expected = """ XCTAssertNoThrow(try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/sideClass-swift.var", sourceLanguage: .swift))) } - func testUnresolvedSidecarDiagnostics() throws { + func testUnresolvedSidecarDiagnostics() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( moduleName: "ModuleName", @@ -2068,7 +2113,7 @@ let expected = """ """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) let unmatchedSidecarProblem = try XCTUnwrap(context.problems.first(where: { $0.diagnostic.identifier == "org.swift.docc.SymbolUnmatched" })) XCTAssertNotNil(unmatchedSidecarProblem) @@ -2084,7 +2129,7 @@ let expected = """ XCTAssertEqual(unmatchedSidecarDiagnostic.severity, .warning) } - func testExtendingSymbolWithSpaceInName() throws { + func testExtendingSymbolWithSpaceInName() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( moduleName: "ModuleName", @@ -2106,7 +2151,7 @@ let expected = """ """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary).joined(separator: "\n"))") @@ -2116,7 +2161,7 @@ let expected = """ XCTAssertEqual((node.semantic as? Symbol)?.abstract?.plainText, "Extend a symbol with a space in its name.") } - func testDeprecationSummaryWithLocalLink() throws { + func testDeprecationSummaryWithLocalLink() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( moduleName: "ModuleName", @@ -2146,7 +2191,7 @@ let expected = """ """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems:\n\(context.problems.map(\.diagnostic.summary).joined(separator: "\n"))") @@ -2169,7 +2214,7 @@ let expected = """ } } - func testUncuratedArticleDiagnostics() throws { + func testUncuratedArticleDiagnostics() async throws { let catalog = Folder(name: "unit-test.docc", content: [ // This setup only happens if the developer manually mixes symbol inputs from different builds JSONFile(name: "FirstModuleName.symbols.json", content: makeSymbolGraph(moduleName: "FirstModuleName")), @@ -2184,7 +2229,7 @@ let expected = """ """), ]) - let (bundle, context) = try loadBundle(catalog: catalog, diagnosticEngine: .init(filterLevel: .information)) + let (bundle, context) = try await loadBundle(catalog: catalog, diagnosticFilterLevel: .information) XCTAssertNil(context.soleRootModuleReference) let curationDiagnostics = context.problems.filter({ $0.diagnostic.identifier == "org.swift.docc.ArticleUncurated" }).map(\.diagnostic) @@ -2194,9 +2239,9 @@ let expected = """ XCTAssertEqual(sidecarDiagnostic.severity, .information) } - func testUpdatesReferencesForChildrenOfCollisions() throws { + func testUpdatesReferencesForChildrenOfCollisions() async throws { // Add some symbol collisions to graph - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in let sideKitURL = root.appendingPathComponent("sidekit.symbols.json") var text = try String(contentsOf: sideKitURL) @@ -2340,11 +2385,11 @@ let expected = """ XCTAssertEqual(context.documentationCache.reference(symbolID: "s:5MyKit0A5MyProtocol0Afunc()DefaultImp")?.path, "/documentation/SideKit/SideProtocol/func()-2dxqn") } - func testResolvingArticleLinkBeforeCuratingIt() throws { + func testResolvingArticleLinkBeforeCuratingIt() async throws { var newArticle1URL: URL! // Add an article without curating it anywhere - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Curate MyKit -> new-article1 let myKitURL = root.appendingPathComponent("documentation").appendingPathComponent("mykit.md") try """ @@ -2379,8 +2424,8 @@ let expected = """ XCTAssertEqual(context.problems.filter { $0.diagnostic.source?.path.hasSuffix(newArticle1URL.lastPathComponent) == true }.count, 0) } - func testPrefersNonSymbolsInDocLink() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "SymbolsWithSameNameAsModule") { url in + func testPrefersNonSymbolsInDocLink() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "SymbolsWithSameNameAsModule") { url in // This bundle has a top-level struct named "Wrapper". Adding an article named "Wrapper.md" introduces a possibility for a link collision try """ # An article @@ -2406,14 +2451,17 @@ let expected = """ let moduleReference = try XCTUnwrap(context.rootModules.first) let moduleNode = try context.entity(with: moduleReference) - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) let renderNode = try XCTUnwrap(converter.renderNode(for: moduleNode)) let curatedTopic = try XCTUnwrap(renderNode.topicSections.first?.identifiers.first) let topicReference = try XCTUnwrap(renderNode.references[curatedTopic] as? TopicRenderReference) - XCTAssertEqual(topicReference.title, "An article") + + // FIXME: Verify that article matches are preferred for general (non-symbol) links once rdar://79745455 https://github.com/swiftlang/swift-docc/issues/593 is fixed + XCTAssertEqual(topicReference.title, "Wrapper") +// XCTAssertEqual(topicReference.title, "An article") // This test also reproduce https://github.com/swiftlang/swift-docc/issues/593 // When that's fixed this test should also use a symbol link to curate the top-level symbol and verify that @@ -2421,9 +2469,9 @@ let expected = """ } // Modules that are being extended should not have their own symbol in the current bundle's graph. - func testNoSymbolForTertiarySymbolGraphModules() throws { + func testNoSymbolForTertiarySymbolGraphModules() async throws { // Add an article without curating it anywhere - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Create an extension only symbol graph. let tertiaryURL = root.appendingPathComponent("Tertiary@MyKit.symbols.json") try """ @@ -2456,8 +2504,8 @@ let expected = """ XCTAssertNil(try? context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/Tertiary", sourceLanguage: .swift))) } - func testDeclarationTokenKinds() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testDeclarationTokenKinds() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let myFunc = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) @@ -2471,7 +2519,7 @@ let expected = """ // Render declaration and compare token kinds with symbol graph let symbol = myFunc.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: myFunc.reference) + var translator = RenderNodeTranslator(context: context, identifier: myFunc.reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode let declarationTokens = renderNode.primaryContentSections.mapFirst { section -> [String]? in @@ -2487,7 +2535,7 @@ let expected = """ } // Test reference resolving in symbol graph docs - func testReferenceResolvingDiagnosticsInSourceDocs() throws { + func testReferenceResolvingDiagnosticsInSourceDocs() async throws { for (source, expectedDiagnosticSource) in [ ("file:///path/to/file.swift", "file:///path/to/file.swift"), // Test the scenario where the symbol graph file contains invalid URLs (rdar://77335208). @@ -2584,7 +2632,7 @@ let expected = """ try text.write(to: referencesURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, _, context) = try loadBundle(from: targetURL) + let (_, _, context) = try await loadBundle(from: targetURL) guard context.problems.count == 5 else { XCTFail("Expected 5 problems during reference resolving; got \(context.problems.count)") @@ -2604,14 +2652,14 @@ let expected = """ } } - func testNavigatorTitle() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testNavigatorTitle() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") func renderNodeForPath(path: String) throws -> (DocumentationNode, RenderNode) { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: path, sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: path, sourceLanguage: .swift) let node = try context.entity(with: reference) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode return (node, renderNode) @@ -2643,7 +2691,7 @@ let expected = """ } } - func testCrossSymbolGraphPathCollisions() throws { + func testCrossSymbolGraphPathCollisions() async throws { // Create temp folder let tempURL = try createTemporaryDirectory() @@ -2659,7 +2707,7 @@ let expected = """ ]).write(inside: tempURL) // Load test bundle - let (_, _, context) = try loadBundle(from: catalogURL) + let (_, _, context) = try await loadBundle(from: catalogURL) let referenceForPath: (String) -> ResolvedTopicReference = { path in return ResolvedTopicReference(bundleID: "com.test.collisions", path: "/documentation" + path, sourceLanguage: .swift) @@ -2678,7 +2726,7 @@ let expected = """ XCTAssertNotNil(try context.entity(with: referenceForPath("/Collisions/SharedStruct/iOSVar"))) } - func testLinkToSymbolWithoutPage() throws { + func testLinkToSymbolWithoutPage() async throws { let inheritedDefaultImplementationsSGF = Bundle.module.url( forResource: "InheritedDefaultImplementations.symbols", withExtension: "json", @@ -2705,18 +2753,18 @@ let expected = """ ] ).write(inside: createTemporaryDirectory()) - let (_, _, context) = try loadBundle(from: testBundle) + let (_, _, context) = try await loadBundle(from: testBundle) let problem = try XCTUnwrap(context.problems.first(where: { $0.diagnostic.identifier == "org.swift.docc.unresolvedTopicReference" })) XCTAssertEqual(problem.diagnostic.summary, "'FirstTarget/Comparable/localDefaultImplementation()' has no page and isn't available for linking.") } - func testContextCachesReferences() throws { + func testContextCachesReferences() async throws { let bundleID: DocumentationBundle.Identifier = #function // Verify there is no pool bucket for the bundle we're about to test XCTAssertNil(ResolvedTopicReference._numberOfCachedReferences(bundleID: bundleID)) - let (_, _, _) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { rootURL in + let (_, _, _) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { rootURL in let infoPlistURL = rootURL.appendingPathComponent("Info.plist", isDirectory: false) try! String(contentsOf: infoPlistURL) .replacingOccurrences(of: "org.swift.docc.example", with: bundleID.rawValue) @@ -2743,8 +2791,8 @@ let expected = """ ResolvedTopicReference.purgePool(for: bundleID) } - func testAbstractAfterMetadataDirective() throws { - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testAbstractAfterMetadataDirective() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // Get the SideKit/SideClass/init() node and verify it has an abstract and no discussion. // We're verifying that the metadata directive between the title and the abstract didn't cause @@ -2759,7 +2807,7 @@ let expected = """ } /// rdar://69242313 - func testLinkResolutionDoesNotSkipSymbolGraph() throws { + func testLinkResolutionDoesNotSkipSymbolGraph() async throws { let tempURL = try createTemporaryDirectory() let bundleURL = try Folder(name: "Missing.docc", content: [ @@ -2769,7 +2817,7 @@ let expected = """ subdirectory: "Test Resources")!), ]).write(inside: tempURL) - let (_, _, context) = try XCTUnwrap(loadBundle(from: bundleURL)) + let (_, _, context) = try await loadBundle(from: bundleURL) // MissingDocs contains a struct that has a link to a non-existent type. // If there are no problems, that indicates that symbol graph link @@ -2794,8 +2842,8 @@ let expected = """ XCTAssertThrowsError(try DocumentationNode(reference: reference, article: semanticArticle)) } - func testTaskGroupsPersistInitialRangesFromMarkup() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testTaskGroupsPersistInitialRangesFromMarkup() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // Verify task group ranges are persisted for symbol docs let symbolReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit", sourceLanguage: .swift) @@ -2831,8 +2879,8 @@ let expected = """ /// Tests that diagnostics raised during link resolution for symbols have the correct source URLs /// - Bug: rdar://63288817 - func testDiagnosticsForSymbolsHaveCorrectSource() throws { - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testDiagnosticsForSymbolsHaveCorrectSource() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in let extensionFile = """ # ``SideKit/SideClass/myFunction()`` @@ -2874,7 +2922,7 @@ let expected = """ XCTAssertEqual(extensionFileChunks.count, 1) } - func testLinkResolutionDiagnosticsEmittedForTechnologyPages() throws { + func testLinkResolutionDiagnosticsEmittedForTechnologyPages() async throws { let tempURL = try createTemporaryDirectory() let bundleURL = try Folder(name: "module-links.docc", content: [ @@ -2902,14 +2950,14 @@ let expected = """ """), ]).write(inside: tempURL) - let (_, _, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) let problems = context.diagnosticEngine.problems let linkResolutionProblems = problems.filter { $0.diagnostic.source?.relativePath.hasSuffix("sidekit.md") == true } XCTAssertEqual(linkResolutionProblems.count, 1) XCTAssertEqual(linkResolutionProblems.first?.diagnostic.identifier, "org.swift.docc.unresolvedTopicReference") } - func testLinkDiagnosticsInSynthesizedTechnologyRoots() throws { + func testLinkDiagnosticsInSynthesizedTechnologyRoots() async throws { // Verify that when synthesizing a technology root, links are resolved in the roots content. // Also, if an article is promoted to a root, verify that any existing metadata is preserved. @@ -2942,7 +2990,7 @@ let expected = """ - """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.map(\.diagnostic.summary), [ "'NotFoundSymbol' doesn't exist at '/Root'", @@ -2987,7 +3035,7 @@ let expected = """ """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.map(\.diagnostic.summary), [ "'NotFoundSymbol' doesn't exist at '/CatalogName'", @@ -3032,7 +3080,7 @@ let expected = """ """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.map(\.diagnostic.summary).sorted(), [ "'NotFoundArticle' doesn't exist at '/CatalogName/Second'", @@ -3045,7 +3093,7 @@ let expected = """ XCTAssertNotNil(rootPage.metadata?.technologyRoot) } - func testResolvingLinksToHeaders() throws { + func testResolvingLinksToHeaders() async throws { let tempURL = try createTemporaryDirectory() let bundleURL = try Folder(name: "module-links.docc", content: [ @@ -3110,7 +3158,7 @@ let expected = """ """), ]).write(inside: tempURL) - let (_, _, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) let articleReference = try XCTUnwrap(context.knownPages.first) let node = try context.entity(with: articleReference) @@ -3135,8 +3183,8 @@ let expected = """ XCTAssertEqual(node.anchorSections.dropLast().last?.reference.absoluteString, "doc://com.test.docc/documentation/article#Emoji-%F0%9F%92%BB") } - func testResolvingLinksToTopicSections() throws { - let (_, context) = try loadBundle(catalog: + func testResolvingLinksToTopicSections() async throws { + let (_, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName")), @@ -3241,8 +3289,7 @@ let expected = """ ]) // Verify that the links are resolved in the render model. - let bundle = try XCTUnwrap(context.bundle) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(entity) XCTAssertEqual(renderNode.topicSections.map(\.anchor), [ @@ -3277,10 +3324,10 @@ let expected = """ ]) } - func testExtensionCanUseLanguageSpecificRelativeLinks() throws { + func testExtensionCanUseLanguageSpecificRelativeLinks() async throws { // This test uses a symbol with different names in Swift and Objective-C, each with a member that's only available in that language. let symbolID = "some-symbol-id" - let (_, context) = try loadBundle(catalog: + let (_, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ Folder(name: "swift", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( @@ -3393,7 +3440,7 @@ let expected = """ ]) } - func testWarnOnMultipleMarkdownExtensions() throws { + func testWarnOnMultipleMarkdownExtensions() async throws { let fileContent = """ # ``MyKit/MyClass/myFunction()`` @@ -3426,7 +3473,7 @@ let expected = """ let bundleURL = try exampleDocumentation.write(inside: tempURL) // Parse this test content - let (_, _, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) let identifier = "org.swift.docc.DuplicateMarkdownTitleSymbolReferences" let duplicateMarkdownProblems = context.problems.filter({ $0.diagnostic.identifier == identifier }) @@ -3439,10 +3486,10 @@ let expected = """ /// This test verifies that collision nodes and children of collision nodes are correctly /// matched with their documentation extension files. Besides verifying the correct content /// it verifies also that the curation in these doc extensions is reflected in the topic graph. - func testMatchesCorrectlyDocExtensionToChildOfCollisionTopic() throws { + func testMatchesCorrectlyDocExtensionToChildOfCollisionTopic() async throws { let fifthTestMemberPath = "ShapeKit/OverloadedParentStruct-1jr3p/fifthTestMember" - let (_, bundle, context) = try testBundleAndContext(copying: "OverloadedSymbols") { url in + let (_, bundle, context) = try await testBundleAndContext(copying: "OverloadedSymbols") { url in // Add an article to be curated from collided nodes' doc extensions. try """ # New Article @@ -3509,8 +3556,8 @@ let expected = """ XCTAssertTrue(tgNode2.contains(articleReference)) } - func testMatchesDocumentationExtensionsAsSymbolLinks() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in + func testMatchesDocumentationExtensionsAsSymbolLinks() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in // Two colliding symbols that differ by capitalization. try """ # ``MixedFramework/CollisionsWithDifferentCapitalization/someThing`` @@ -3620,8 +3667,8 @@ let expected = """ } } - func testMatchesDocumentationExtensionsWithSourceLanguageSpecificLinks() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in + func testMatchesDocumentationExtensionsWithSourceLanguageSpecificLinks() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in // typedef NS_OPTIONS(NSInteger, MyObjectiveCOption) { // MyObjectiveCOptionNone = 0, // MyObjectiveCOptionFirst = 1 << 0, @@ -3722,8 +3769,8 @@ let expected = """ } } - func testMatchesDocumentationExtensionsRelativeToModule() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in + func testMatchesDocumentationExtensionsRelativeToModule() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in // Top level symbols, omitting the module name try """ # ``MyStruct/myStructProperty`` @@ -3765,8 +3812,8 @@ let expected = """ } } - func testCurationOfSymbolsWithSameNameAsModule() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "SymbolsWithSameNameAsModule") { url in + func testCurationOfSymbolsWithSameNameAsModule() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "SymbolsWithSameNameAsModule") { url in // Top level symbols, omitting the module name try """ # ``Something`` @@ -3795,8 +3842,8 @@ let expected = """ } } - func testMultipleDocumentationExtensionMatchDiagnostic() throws { - let (_, _, context) = try testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in + func testMultipleDocumentationExtensionMatchDiagnostic() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in // typedef NS_OPTIONS(NSInteger, MyObjectiveCOption) { // MyObjectiveCOptionNone = 0, // MyObjectiveCOptionFirst = 1 << 0, @@ -3852,7 +3899,7 @@ let expected = """ XCTAssertNotEqual(methodMultipleMatchProblem.diagnostic.source, methodMultipleMatchProblem.diagnostic.notes.first?.source, "The warning and the note should refer to different documentation extension files") } - func testAutomaticallyCuratesArticles() throws { + func testAutomaticallyCuratesArticles() async throws { let articleOne = TextFile(name: "Article1.md", utf8Content: """ # Article 1 @@ -3886,7 +3933,7 @@ let expected = """ articleOne, articleTwo, ]).write(inside: tempURL) - let (_, bundle, context) = try loadBundle(from: bundleURL) + let (_, bundle, context) = try await loadBundle(from: bundleURL) let identifiers = context.problems.map(\.diagnostic.identifier) XCTAssertFalse(identifiers.contains(where: { $0 == "org.swift.docc.ArticleUncurated" })) @@ -3926,7 +3973,7 @@ let expected = """ articleOne, articleTwo, ]).write(inside: tempURL) - let (_, bundle, context) = try loadBundle(from: bundleURL) + let (_, bundle, context) = try await loadBundle(from: bundleURL) let rootReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Module", sourceLanguage: .swift) let docNode = try context.entity(with: rootReference) @@ -3936,7 +3983,7 @@ let expected = """ } } - func testAutomaticTaskGroupsPlacedAfterManualCuration() throws { + func testAutomaticTaskGroupsPlacedAfterManualCuration() async throws { let tempURL = try createTemporaryDirectory() let bundleURL = try Folder(name: "Module.docc", content: [ @@ -3969,7 +4016,7 @@ let expected = """ - """), ]).write(inside: tempURL) - let (_, bundle, context) = try loadBundle(from: bundleURL) + let (_, bundle, context) = try await loadBundle(from: bundleURL) let rootReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Module", sourceLanguage: .swift) let docNode = try context.entity(with: rootReference) @@ -3991,8 +4038,8 @@ let expected = """ } // Verifies if the context resolves linkable nodes. - func testLinkableNodes() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testLinkableNodes() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in try "# Article1".write(to: url.appendingPathComponent("resolvable-article.md"), atomically: true, encoding: .utf8) let myKitURL = url.appendingPathComponent("documentation").appendingPathComponent("mykit.md") try String(contentsOf: myKitURL) @@ -4012,9 +4059,9 @@ let expected = """ } // Verifies if the context fails to resolve non-resolvable nodes. - func testNonLinkableNodes() throws { + func testNonLinkableNodes() async throws { // Create a bundle with variety absolute and relative links and symbol links to a non linkable node. - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in try """ # ``SideKit/SideClass`` Abstract. @@ -4135,7 +4182,7 @@ let expected = """ /// Verify we resolve a relative link to the article if we have /// an article, a tutorial, and a symbol with the *same* names. - func testResolvePrecedenceArticleOverTutorialOverSymbol() throws { + func testResolvePrecedenceArticleOverTutorialOverSymbol() async throws { // Verify resolves correctly between a bundle with an article and a tutorial. do { let infoPlistURL = try XCTUnwrap(Bundle.module.url(forResource: "Info+Availability", withExtension: "plist", subdirectory: "Test Resources")) @@ -4150,7 +4197,7 @@ let expected = """ try testBundle.write(to: tempFolderURL) // Load the bundle - let (_, bundle, context) = try loadBundle(from: tempFolderURL) + let (_, bundle, context) = try await loadBundle(from: tempFolderURL) // Verify the context contains the conflicting topic names // Article XCTAssertNotNil(context.documentationCache[ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Test-Bundle/Test", sourceLanguage: .swift)]) @@ -4187,7 +4234,7 @@ let expected = """ try testBundle.write(to: tempFolderURL) // Load the bundle - let (_, bundle, context) = try loadBundle(from: tempFolderURL) + let (_, bundle, context) = try await loadBundle(from: tempFolderURL) // Verify the context contains the conflicting topic names // Article XCTAssertNotNil(context.documentationCache[ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Test-Bundle/Test", sourceLanguage: .swift)]) @@ -4215,7 +4262,7 @@ let expected = """ } } - func testResolvePrecedenceSymbolInBackticks() throws { + func testResolvePrecedenceSymbolInBackticks() async throws { // Verify resolves correctly a double-backtick link. do { let infoPlistURL = try XCTUnwrap(Bundle.module.url(forResource: "Info+Availability", withExtension: "plist", subdirectory: "Test Resources")) @@ -4248,7 +4295,7 @@ let expected = """ try testBundle.write(to: tempFolderURL) // Load the bundle - let (_, bundle, context) = try loadBundle(from: tempFolderURL) + let (_, bundle, context) = try await loadBundle(from: tempFolderURL) let symbolReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Minimal_docs/Test", sourceLanguage: .swift) let moduleReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Minimal_docs", sourceLanguage: .swift) @@ -4301,7 +4348,7 @@ let expected = """ } } - func testSymbolMatchingModuleName() throws { + func testSymbolMatchingModuleName() async throws { // Verify as top-level symbol with name matching the module name // does not trip the context when building the topic graph do { @@ -4318,7 +4365,7 @@ let expected = """ try testBundle.write(to: tempFolderURL) // Load the bundle - let (_, bundle, context) = try loadBundle(from: tempFolderURL) + let (_, bundle, context) = try await loadBundle(from: tempFolderURL) // Verify the module and symbol node kinds. let symbolReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Minimal_docs/Minimal_docs", sourceLanguage: .swift) @@ -4350,7 +4397,7 @@ let expected = """ /// public func method(_ param: String) { } /// } /// ``` - func testWarningForUnresolvableLinksInInheritedDocs() throws { + func testWarningForUnresolvableLinksInInheritedDocs() async throws { // Create temp folder let tempURL = try createTemporaryDirectory() @@ -4363,7 +4410,7 @@ let expected = """ ]).write(inside: tempURL) // Load the test bundle - let (_, _, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) // Get the emitted diagnostic and verify it contains a solution and replacement fix-it. let problem = try XCTUnwrap(context.problems.first(where: { p in @@ -4392,8 +4439,8 @@ let expected = """ XCTAssertEqual(problem.possibleSolutions[0].replacements[0].replacement, "") } - func testCustomModuleKind() throws { - let (bundle, context) = try testBundleAndContext(named: "BundleWithExecutableModuleKind") + func testCustomModuleKind() async throws { + let (bundle, context) = try await testBundleAndContext(named: "BundleWithExecutableModuleKind") XCTAssertEqual(bundle.info.defaultModuleKind, "Executable") let moduleSymbol = try XCTUnwrap(context.documentationCache["ExampleDocumentedExecutable"]?.symbol) @@ -4403,7 +4450,7 @@ let expected = """ /// Verifies that the number of symbols registered in the documentation context is consistent with /// the number of symbols in the symbol graph files. - func testSymbolsCountIsConsistentWithSymbolGraphData() throws { + func testSymbolsCountIsConsistentWithSymbolGraphData() async throws { let exampleDocumentation = Folder(name: "unit-test.docc", content: [ Folder(name: "Symbols", content: [ JSONFile( @@ -4434,7 +4481,7 @@ let expected = """ InfoPlist(displayName: "TestBundle", identifier: "com.test.example") ]) - let (_, context) = try loadBundle(catalog: exampleDocumentation) + let (_, context) = try await loadBundle(catalog: exampleDocumentation) XCTAssertEqual( context.documentationCache.count, @@ -4443,7 +4490,7 @@ let expected = """ ) } - func testDocumentationExtensionURLForReferenceReturnsURLForSymbolReference() throws { + func testDocumentationExtensionURLForReferenceReturnsURLForSymbolReference() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph(moduleName: "SomeModuleName", symbols: [ makeSymbol(id: "some-symbol-id", kind: .class, pathComponents: ["SomeClass"]) @@ -4454,7 +4501,7 @@ let expected = """ """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) XCTAssertEqual( @@ -4463,8 +4510,8 @@ let expected = """ ) } - func testDocumentationExtensionURLForReferenceReturnsNilForTutorialReference() throws { - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") + func testDocumentationExtensionURLForReferenceReturnsNilForTutorialReference() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") XCTAssertNil( context.documentationExtensionURL( @@ -4479,14 +4526,14 @@ let expected = """ ) } - func testAddingProtocolExtensionMemberConstraint() throws { + func testAddingProtocolExtensionMemberConstraint() async throws { // This fixture contains a protocol extension: // extension Swift.Collection { // public func fixture() -> String { // return "collection" // } // } - let (_, _, context) = try testBundleAndContext(copying: "ModuleWithProtocolExtensions") + let (_, _, context) = try await testBundleAndContext(copying: "ModuleWithProtocolExtensions") // The member function of the protocol extension // should have a constraint: Self is Collection @@ -4524,14 +4571,14 @@ let expected = """ XCTAssertEqual(constraint.rightTypeName, "Hashable") } - func testDiagnosticLocations() throws { + func testDiagnosticLocations() async throws { // The ObjCFrameworkWithInvalidLink.docc test bundle contains symbol // graphs for both Obj-C and Swift, built after setting: // "Build Multi-Language Documentation for Objective-C Only Targets" = true. // One doc comment in the Obj-C header file contains an invalid doc // link on line 24, columns 56-63: // "Log a hello world message. This line contains an ``invalid`` link." - let (_, context) = try testBundleAndContext(named: "ObjCFrameworkWithInvalidLink") + let (_, context) = try await testBundleAndContext(named: "ObjCFrameworkWithInvalidLink") let problems = context.problems if FeatureFlags.current.isParametersAndReturnsValidationEnabled { XCTAssertEqual(4, problems.count) @@ -4547,7 +4594,7 @@ let expected = """ XCTAssertEqual(start.. """.utf8)) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) for kindID in overloadableKindIDs { @@ -4887,10 +4969,14 @@ let expected = """ } // The overload behavior doesn't apply to symbol kinds that don't support overloading - func testContextDoesNotRecognizeNonOverloadableSymbolKinds() throws { + func testContextDoesNotRecognizeNonOverloadableSymbolKinds() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let nonOverloadableKindIDs = SymbolGraph.Symbol.KindIdentifier.allCases.filter { !$0.isOverloadableKind } + let nonOverloadableKindIDs = SymbolGraph.Symbol.KindIdentifier.allCases.filter { + !$0.isOverloadableKind && + !$0.isSnippetKind && // avoid mixing snippets with "real" symbols + $0 != .module // avoid creating multiple modules + } // Generate a 4 symbols with the same name for every non overloadable symbol kind let symbols: [SymbolGraph.Symbol] = nonOverloadableKindIDs.flatMap { [ makeSymbol(id: "first-\($0.identifier)-id", kind: $0, pathComponents: ["SymbolName"]), @@ -4907,7 +4993,7 @@ let expected = """ )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) for kindID in nonOverloadableKindIDs { // Find the 4 symbols of this specific kind @@ -4923,7 +5009,7 @@ let expected = """ } } - func testWarnsOnUnknownPlistFeatureFlag() throws { + func testWarnsOnUnknownPlistFeatureFlag() async throws { let catalog = Folder(name: "unit-test.docc", content: [ DataFile(name: "Info.plist", data: Data(""" @@ -4938,7 +5024,7 @@ let expected = """ """.utf8)) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let unknownFeatureFlagProblems = context.problems.filter({ $0.diagnostic.identifier == "org.swift.docc.UnknownBundleFeatureFlag" }) XCTAssertEqual(unknownFeatureFlagProblems.count, 1) @@ -4948,7 +5034,7 @@ let expected = """ XCTAssertEqual(problem.diagnostic.summary, "Unknown feature flag in Info.plist: 'NonExistentFeature'") } - func testUnknownFeatureFlagSuggestsOtherFlags() throws { + func testUnknownFeatureFlagSuggestsOtherFlags() async throws { let catalog = Folder(name: "unit-test.docc", content: [ DataFile(name: "Info.plist", data: Data(""" @@ -4963,7 +5049,7 @@ let expected = """ """.utf8)) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let unknownFeatureFlagProblems = context.problems.filter({ $0.diagnostic.identifier == "org.swift.docc.UnknownBundleFeatureFlag" }) XCTAssertEqual(unknownFeatureFlagProblems.count, 1) @@ -4975,7 +5061,7 @@ let expected = """ "Unknown feature flag in Info.plist: 'ExperimenalOverloadedSymbolPresentation'. Possible suggestions: 'ExperimentalOverloadedSymbolPresentation'") } - func testContextGeneratesUnifiedOverloadGroupsAcrossPlatforms() throws { + func testContextGeneratesUnifiedOverloadGroupsAcrossPlatforms() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) let symbolKind = try XCTUnwrap(SymbolGraph.Symbol.KindIdentifier.allCases.filter({ $0.isOverloadableKind }).first) @@ -4998,7 +5084,7 @@ let expected = """ ])), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) let overloadGroupNode: DocumentationNode @@ -5048,7 +5134,7 @@ let expected = """ } } - func testContextGeneratesOverloadGroupsWhenOnePlatformHasNoOverloads() throws { + func testContextGeneratesOverloadGroupsWhenOnePlatformHasNoOverloads() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) let symbolKind = try XCTUnwrap(SymbolGraph.Symbol.KindIdentifier.allCases.filter({ $0.isOverloadableKind }).first) @@ -5075,7 +5161,7 @@ let expected = """ ])), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) let overloadGroupNode: DocumentationNode @@ -5128,7 +5214,7 @@ let expected = """ /// Ensure that overload groups are correctly loaded into the path hierarchy and create nodes, /// even when they came from an extension symbol graph. - func testContextGeneratesOverloadGroupsForExtensionGraphOverloads() throws { + func testContextGeneratesOverloadGroupsForExtensionGraphOverloads() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) let symbolKind = try XCTUnwrap(SymbolGraph.Symbol.KindIdentifier.allCases.filter({ $0.isOverloadableKind }).first) @@ -5150,7 +5236,7 @@ let expected = """ ])), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) let overloadGroupNode: DocumentationNode @@ -5198,7 +5284,7 @@ let expected = """ } } - func testContextGeneratesOverloadGroupsForDisjointOverloads() throws { + func testContextGeneratesOverloadGroupsForDisjointOverloads() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) let symbolKind = try XCTUnwrap(SymbolGraph.Symbol.KindIdentifier.allCases.filter({ $0.isOverloadableKind }).first) @@ -5219,7 +5305,7 @@ let expected = """ makeSymbol(id: "symbol-2", kind: symbolKind, pathComponents: ["SymbolName"]), ])), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) let overloadGroupNode: DocumentationNode @@ -5267,10 +5353,10 @@ let expected = """ } } - func testContextDiagnosesInsufficientDisambiguationWithCorrectRange() throws { + func testContextDiagnosesInsufficientDisambiguationWithCorrectRange() async throws { // This test deliberately does not turn on the overloads feature // to ensure the symbol link below does not accidentally resolve correctly. - for symbolKindID in SymbolGraph.Symbol.KindIdentifier.allCases where !symbolKindID.isOverloadableKind { + for symbolKindID in SymbolGraph.Symbol.KindIdentifier.allCases where !symbolKindID.isOverloadableKind && !symbolKindID.isSnippetKind { // Generate a 4 symbols with the same name for every non overloadable symbol kind let symbols: [SymbolGraph.Symbol] = [ makeSymbol(id: "first-\(symbolKindID.identifier)-id", kind: symbolKindID, pathComponents: ["SymbolName"]), @@ -5297,7 +5383,7 @@ let expected = """ """) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let problems = context.problems.sorted(by: \.diagnostic.summary) XCTAssertEqual(problems.count, 1) @@ -5321,10 +5407,10 @@ let expected = """ } } - func testContextDiagnosesIncorrectDisambiguationWithCorrectRange() throws { + func testContextDiagnosesIncorrectDisambiguationWithCorrectRange() async throws { // This test deliberately does not turn on the overloads feature // to ensure the symbol link below does not accidentally resolve correctly. - for symbolKindID in SymbolGraph.Symbol.KindIdentifier.allCases where !symbolKindID.isOverloadableKind { + for symbolKindID in SymbolGraph.Symbol.KindIdentifier.allCases where !symbolKindID.isOverloadableKind && !symbolKindID.isSnippetKind { // Generate a 4 symbols with the same name for every non overloadable symbol kind let symbols: [SymbolGraph.Symbol] = [ makeSymbol(id: "first-\(symbolKindID.identifier)-id", kind: symbolKindID, pathComponents: ["SymbolName"]), @@ -5351,7 +5437,7 @@ let expected = """ """) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let problems = context.problems.sorted(by: \.diagnostic.summary) XCTAssertEqual(problems.count, 1) @@ -5373,10 +5459,10 @@ let expected = """ } } - func testContextDiagnosesIncorrectSymbolNameWithCorrectRange() throws { + func testContextDiagnosesIncorrectSymbolNameWithCorrectRange() async throws { // This test deliberately does not turn on the overloads feature // to ensure the symbol link below does not accidentally resolve correctly. - for symbolKindID in SymbolGraph.Symbol.KindIdentifier.allCases where !symbolKindID.isOverloadableKind { + for symbolKindID in SymbolGraph.Symbol.KindIdentifier.allCases where !symbolKindID.isOverloadableKind && !symbolKindID.isSnippetKind { // Generate a 4 symbols with the same name for every non overloadable symbol kind let symbols: [SymbolGraph.Symbol] = [ makeSymbol(id: "first-\(symbolKindID.identifier)-id", kind: symbolKindID, pathComponents: ["SymbolName"]), @@ -5403,7 +5489,7 @@ let expected = """ """) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let problems = context.problems.sorted(by: \.diagnostic.summary) XCTAssertEqual(problems.count, 1) @@ -5425,13 +5511,13 @@ let expected = """ } } - func testResolveExternalLinkFromTechnologyRoot() throws { + func testResolveExternalLinkFromTechnologyRoot() async throws { enableFeatureFlag(\.isExperimentalLinkHierarchySerializationEnabled) let externalModuleName = "ExternalModuleName" - func makeExternalDependencyFiles() throws -> (SerializableLinkResolutionInformation, [LinkDestinationSummary]) { - let (bundle, context) = try loadBundle( + func makeExternalDependencyFiles() async throws -> (SerializableLinkResolutionInformation, [LinkDestinationSummary]) { + let (_, context) = try await loadBundle( catalog: Folder(name: "Dependency.docc", content: [ JSONFile(name: "\(externalModuleName).symbols.json", content: makeSymbolGraph(moduleName: externalModuleName)), TextFile(name: "Extension.md", utf8Content: """ @@ -5443,14 +5529,14 @@ let expected = """ ) // Retrieve the link information from the dependency, as if '--enable-experimental-external-link-support' was passed to DocC - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let linkSummaries: [LinkDestinationSummary] = try context.knownPages.flatMap { reference in let entity = try context.entity(with: reference) let renderNode = try XCTUnwrap(converter.convert(entity)) return entity.externallyLinkableElementSummaries(context: context, renderNode: renderNode, includeTaskGroups: false) } - let linkResolutionInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: bundle.id) + let linkResolutionInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: context.inputs.id) return (linkResolutionInformation, linkSummaries) } @@ -5465,14 +5551,14 @@ let expected = """ """), ]) - let (linkResolutionInformation, linkSummaries) = try makeExternalDependencyFiles() + let (linkResolutionInformation, linkSummaries) = try await makeExternalDependencyFiles() var configuration = DocumentationContext.Configuration() configuration.externalDocumentationConfiguration.dependencyArchives = [ URL(fileURLWithPath: "/path/to/SomeDependency.doccarchive") ] - let (bundle, context) = try loadBundle( + let (_, context) = try await loadBundle( catalog: catalog, otherFileSystemDirectories: [ Folder(name: "path", content: [ @@ -5491,7 +5577,7 @@ let expected = """ let reference = try XCTUnwrap(context.soleRootModuleReference) let node = try context.entity(with: reference) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(node) let externalReference = "doc://Dependency/documentation/ExternalModuleName" @@ -5515,8 +5601,8 @@ let expected = """ XCTAssertEqual(externalRenderReference.abstract, [.text("Some description of this module.")]) } - func testResolvesAlternateDeclarations() throws { - let (bundle, context) = try loadBundle(catalog: Folder( + func testResolvesAlternateDeclarations() async throws { + let (bundle, context) = try await loadBundle(catalog: Folder( name: "unit-test.docc", content: [ TextFile(name: "Symbol.md", utf8Content: """ @@ -5593,8 +5679,8 @@ let expected = """ XCTAssertEqual(problem.diagnostic.summary, "Can't resolve 'MissingSymbol'") } - func testDiagnosesSymbolAlternateDeclarations() throws { - let (_, context) = try loadBundle(catalog: Folder( + func testDiagnosesSymbolAlternateDeclarations() async throws { + let (_, context) = try await loadBundle(catalog: Folder( name: "unit-test.docc", content: [ TextFile(name: "Symbol.md", utf8Content: """ @@ -5664,8 +5750,8 @@ let expected = """ XCTAssertEqual(solution.replacements.first?.replacement, "") } - func testDiagnosesArticleAlternateDeclarations() throws { - let (_, context) = try loadBundle(catalog: Folder( + func testDiagnosesArticleAlternateDeclarations() async throws { + let (_, context) = try await loadBundle(catalog: Folder( name: "unit-test.docc", content: [ TextFile(name: "Symbol.md", utf8Content: """ @@ -5724,6 +5810,48 @@ let expected = """ XCTAssertEqual(solution.replacements.first?.replacement, "") } + func testSupportedLanguageDirectiveForStandaloneArticles() async throws { + let catalog = Folder(name: "unit-test.docc", content: [ + TextFile(name: "Root.md", utf8Content: """ + # Root + + @Metadata { + @TechnologyRoot + @SupportedLanguage(objc) + @SupportedLanguage(data) + } + + ## Topics + + - + """), + TextFile(name: "Article.md", utf8Content: """ + # Article + + @Metadata { + @SupportedLanguage(objc) + @SupportedLanguage(data) + } + """), + // The correct way to configure a catalog is to have a single root module. If multiple modules, + // are present, it is not possible to determine which module an article is supposed to be + // registered with. We include multiple modules to prevent registering the articles in the + // documentation cache, to test if the supported languages are attached prior to registration. + JSONFile(name: "Foo.symbols.json", content: makeSymbolGraph(moduleName: "Foo")), + ]) + + let (bundle, context) = try await loadBundle(catalog: catalog) + + XCTAssert(context.problems.isEmpty, "Unexpected problems:\n\(context.problems.map(\.diagnostic.summary).joined(separator: "\n"))") + + do { + let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/unit-test/Article", sourceLanguage: .data) + // Find the topic graph node for the article + let node = context.topicGraph.nodes.first { $0.key == reference }?.value + // Ensure that the reference within the topic graph node contains the supported languages + XCTAssertEqual(node?.reference.sourceLanguages, [.objectiveC, .data]) + } + } } func assertEqualDumps(_ lhs: String, _ rhs: String, file: StaticString = #filePath, line: UInt = #line) { diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationCuratorTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationCuratorTests.swift index f40e9ef03d..cab832ec10 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationCuratorTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationCuratorTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -27,10 +27,10 @@ class DocumentationCuratorTests: XCTestCase { } } - func testCrawl() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testCrawl() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var crawler = DocumentationCurator.init(in: context, bundle: bundle) + var crawler = DocumentationCurator(in: context) let mykit = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit", sourceLanguage: .swift)) var symbolsWithCustomCuration = [ResolvedTopicReference]() @@ -74,8 +74,8 @@ class DocumentationCuratorTests: XCTestCase { ) } - func testCrawlDiagnostics() throws { - let (tempCatalogURL, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testCrawlDiagnostics() async throws { + let (tempCatalogURL, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in let extensionFile = url.appendingPathComponent("documentation/myfunction.md") try """ @@ -97,7 +97,7 @@ class DocumentationCuratorTests: XCTestCase { } let extensionFile = tempCatalogURL.appendingPathComponent("documentation/myfunction.md") - var crawler = DocumentationCurator(in: context, bundle: bundle) + var crawler = DocumentationCurator(in: context) let mykit = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit", sourceLanguage: .swift)) XCTAssertNoThrow(try crawler.crawlChildren(of: mykit.reference, prepareForCuration: { _ in }, relateNodes: { _, _ in })) @@ -136,8 +136,8 @@ class DocumentationCuratorTests: XCTestCase { """) } - func testCyclicCurationDiagnostic() throws { - let (_, context) = try loadBundle(catalog: + func testCyclicCurationDiagnostic() async throws { + let (_, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ // A number of articles with this cyclic curation: // @@ -201,7 +201,7 @@ class DocumentationCuratorTests: XCTestCase { XCTAssertEqual(curationProblem.possibleSolutions.map(\.summary), ["Remove '- '"]) } - func testCurationInUncuratedAPICollection() throws { + func testCurationInUncuratedAPICollection() async throws { // Everything should behave the same when an API Collection is automatically curated as when it is explicitly curated for shouldCurateAPICollection in [true, false] { let assertionMessageDescription = "when the API collection is \(shouldCurateAPICollection ? "explicitly curated" : "auto-curated as an article under the module")." @@ -228,7 +228,7 @@ class DocumentationCuratorTests: XCTestCase { - ``NotFound`` """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual( context.problems.map(\.diagnostic.summary), [ @@ -266,7 +266,7 @@ class DocumentationCuratorTests: XCTestCase { ) // Verify that the rendered top-level page doesn't have an automatic "Classes" topic section anymore. - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) let rootRenderNode = converter.convert(try context.entity(with: moduleReference)) @@ -285,8 +285,8 @@ class DocumentationCuratorTests: XCTestCase { } } - func testModuleUnderTechnologyRoot() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "SourceLocations") { url in + func testModuleUnderTechnologyRoot() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "SourceLocations") { url in try """ # Root curating a module @@ -303,7 +303,7 @@ class DocumentationCuratorTests: XCTestCase { """.write(to: url.appendingPathComponent("Root.md"), atomically: true, encoding: .utf8) } - let crawler = DocumentationCurator.init(in: context, bundle: bundle) + let crawler = DocumentationCurator(in: context) XCTAssert(context.problems.isEmpty, "Expected no problems. Found: \(context.problems.map(\.diagnostic.summary))") guard let moduleNode = context.documentationCache["SourceLocations"], @@ -316,11 +316,109 @@ class DocumentationCuratorTests: XCTestCase { XCTAssertEqual(root.path, "/documentation/Root") XCTAssertEqual(crawler.problems.count, 0) - + } + + func testCuratorDoesNotRelateNodesWhenArticleLinksContainExtraPathComponents() async throws { + let (_, context) = try await loadBundle(catalog: + Folder(name: "CatalogName.docc", content: [ + TextFile(name: "Root.md", utf8Content: """ + # Root + + @Metadata { + @TechnologyRoot + } + + Add an API Collection of indirection to more easily detect the failed curation. + + ## Topics + - + """), + + TextFile(name: "API-Collection.md", utf8Content: """ + # Some API Collection + + Fail to curate all 4 articles because of extra incorrect path components. + + ## Topics + + ### No links will resolve in this section + + - + - + - + - + """), + + TextFile(name: "First.md", utf8Content: "# First"), + TextFile(name: "Second.md", utf8Content: "# Second"), + TextFile(name: "Third.md", utf8Content: "# Third"), + TextFile(name: "Forth.md", utf8Content: "# Forth"), + ]) + ) + let (linkResolutionProblems, otherProblems) = context.problems.categorize(where: { $0.diagnostic.identifier == "org.swift.docc.unresolvedTopicReference" }) + XCTAssert(otherProblems.isEmpty, "Unexpected problems: \(otherProblems.map(\.diagnostic.summary).sorted())") + + XCTAssertEqual( + linkResolutionProblems.map(\.diagnostic.source?.lastPathComponent), + ["API-Collection.md", "API-Collection.md", "API-Collection.md", "API-Collection.md"], + "Every unresolved link is in the API collection" + ) + XCTAssertEqual( + linkResolutionProblems.map({ $0.diagnostic.range?.lowerBound.line }), [9, 10, 11, 12], + "There should be one warning about an unresolved reference for each link in the API collection's top" + ) + + let rootReference = try XCTUnwrap(context.soleRootModuleReference) + + for articleName in ["First", "Second", "Third", "Forth"] { + let reference = try XCTUnwrap(context.documentationCache.allReferences.first(where: { $0.lastPathComponent == articleName })) + XCTAssertEqual( + context.topicGraph.nodeWithReference(reference)?.shouldAutoCurateInCanonicalLocation, true, + "Article '\(articleName)' isn't (successfully) manually curated and should therefore automatically curate." + ) + XCTAssertEqual( + context.topicGraph.reverseEdges[reference]?.map(\.path), [rootReference.path], + "Article '\(articleName)' should only have a reverse edge to the root page where it will be automatically curated." + ) + } + + let apiCollectionReference = try XCTUnwrap(context.documentationCache.allReferences.first(where: { $0.lastPathComponent == "API-Collection" })) + let apiCollectionSemantic = try XCTUnwrap(try context.entity(with: apiCollectionReference).semantic as? Article) + XCTAssertEqual(apiCollectionSemantic.topics?.taskGroups.count, 1, "The API Collection has one topic section") + let topicSection = try XCTUnwrap(apiCollectionSemantic.topics?.taskGroups.first) + XCTAssertEqual(topicSection.links.map(\.destination), [ + // All these links are the same as they were authored which means that they didn't resolve. + "doc:WrongModuleName/First", + "doc:documentation/WrongModuleName/Second", + "doc:documentation/CatalogName/ExtraPathComponent/Third", + "doc:CatalogName/ExtraPathComponent/Forth", + ]) + + let rootPage = try context.entity(with: rootReference) + let renderer = DocumentationNodeConverter(context: context) + let renderNode = renderer.convert(rootPage) + + XCTAssertEqual(renderNode.topicSections.map(\.title), [ + nil, // An unnamed topic section + "Articles", // The automatic topic section + ]) + XCTAssertEqual(renderNode.topicSections.map { $0.identifiers.sorted() }, [ + // The unnamed topic section curates the API collection + [ + "doc://CatalogName/documentation/CatalogName/API-Collection" + ], + // The automatic "Articles" section curates all 4 articles + [ + "doc://CatalogName/documentation/CatalogName/First", + "doc://CatalogName/documentation/CatalogName/Forth", + "doc://CatalogName/documentation/CatalogName/Second", + "doc://CatalogName/documentation/CatalogName/Third", + ], + ]) } - func testModuleUnderAncestorOfTechnologyRoot() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "SourceLocations") { url in + func testModuleUnderAncestorOfTechnologyRoot() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "SourceLocations") { url in try """ # Root with ancestor curating a module @@ -347,7 +445,6 @@ class DocumentationCuratorTests: XCTestCase { """.write(to: url.appendingPathComponent("Ancestor.md"), atomically: true, encoding: .utf8) } - let _ = DocumentationCurator.init(in: context, bundle: bundle) XCTAssert(context.problems.isEmpty, "Expected no problems. Found: \(context.problems.map(\.diagnostic.summary))") guard let moduleNode = context.documentationCache["SourceLocations"], @@ -361,10 +458,10 @@ class DocumentationCuratorTests: XCTestCase { XCTAssertEqual(root.path, "/documentation/Root") } - func testSymbolLinkResolving() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testSymbolLinkResolving() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let crawler = DocumentationCurator.init(in: context, bundle: bundle) + let crawler = DocumentationCurator(in: context) // Resolve top-level symbol in module parent do { @@ -414,10 +511,10 @@ class DocumentationCuratorTests: XCTestCase { } } - func testLinkResolving() throws { - let (sourceRoot, bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testLinkResolving() async throws { + let (sourceRoot, _, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var crawler = DocumentationCurator.init(in: context, bundle: bundle) + var crawler = DocumentationCurator(in: context) // Resolve and curate an article in module root (absolute link) do { @@ -469,8 +566,8 @@ class DocumentationCuratorTests: XCTestCase { } } - func testGroupLinkValidation() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { root in + func testGroupLinkValidation() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { root in // Create a sidecar with invalid group links try! """ # ``SideKit`` @@ -510,7 +607,7 @@ class DocumentationCuratorTests: XCTestCase { """.write(to: root.appendingPathComponent("documentation").appendingPathComponent("api-collection.md"), atomically: true, encoding: .utf8) } - var crawler = DocumentationCurator.init(in: context, bundle: bundle) + var crawler = DocumentationCurator(in: context) let reference = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit", sourceLanguage: .swift) try crawler.crawlChildren(of: reference, prepareForCuration: {_ in }) { (_, _) in } @@ -563,17 +660,17 @@ class DocumentationCuratorTests: XCTestCase { /// +-- SecondLevelNesting (Manually curated) /// +-- MyArticle ( <--- This should be crawled even if we've mixed manual and automatic curation) /// ``` - func testMixedManualAndAutomaticCuration() throws { - let (bundle, context) = try testBundleAndContext(named: "MixedManualAutomaticCuration") + func testMixedManualAndAutomaticCuration() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedManualAutomaticCuration") - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/TestBed/TopClass/NestedEnum/SecondLevelNesting", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/TestBed/TopClass/NestedEnum/SecondLevelNesting", sourceLanguage: .swift) let entity = try context.entity(with: reference) let symbol = try XCTUnwrap(entity.semantic as? Symbol) // Verify the link was resolved and it's found in the node's topics task group. XCTAssertEqual("doc://com.test.TestBed/documentation/TestBed/MyArticle", symbol.topics?.taskGroups.first?.links.first?.destination) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(entity) // Verify the article identifier is included in the task group for the render node. @@ -581,7 +678,7 @@ class DocumentationCuratorTests: XCTestCase { // Verify that the ONLY curation for `TopClass/name` is the manual curation under `MyArticle` // and the automatic curation under `TopClass` is not present. - let nameReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/TestBed/TopClass/name", sourceLanguage: .swift) + let nameReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/TestBed/TopClass/name", sourceLanguage: .swift) XCTAssertEqual(context.finitePaths(to: nameReference).map({ $0.map(\.path) }), [ ["/documentation/TestBed", "/documentation/TestBed/TopClass", "/documentation/TestBed/TopClass-API-Collection"], ["/documentation/TestBed", "/documentation/TestBed/TopClass", "/documentation/TestBed/TopClass/NestedEnum", "/documentation/TestBed/TopClass/NestedEnum/SecondLevelNesting", "/documentation/TestBed/MyArticle"], @@ -589,7 +686,7 @@ class DocumentationCuratorTests: XCTestCase { // Verify that the BOTH manual curations for `TopClass/age` are preserved // even if one of the manual curations overlaps with the inheritance edge from the symbol graph. - let ageReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/TestBed/TopClass/age", sourceLanguage: .swift) + let ageReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/TestBed/TopClass/age", sourceLanguage: .swift) XCTAssertEqual(context.finitePaths(to: ageReference).map({ $0.map(\.path) }), [ ["/documentation/TestBed", "/documentation/TestBed/TopClass"], ["/documentation/TestBed", "/documentation/TestBed/TopClass", "/documentation/TestBed/TopClass-API-Collection"], @@ -599,8 +696,8 @@ class DocumentationCuratorTests: XCTestCase { /// In case a symbol has automatically curated children and is manually curated multiple times, /// the hierarchy should be created as it's authored. rdar://75453839 - func testMultipleManualCurationIsPreserved() throws { - let (bundle, context) = try testBundleAndContext(named: "MixedManualAutomaticCuration") + func testMultipleManualCurationIsPreserved() async throws { + let (bundle, context) = try await testBundleAndContext(named: "MixedManualAutomaticCuration") let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/TestBed/DoublyManuallyCuratedClass/type()", sourceLanguage: .swift) diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationWorkspaceTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationWorkspaceTests.swift deleted file mode 100644 index 81ea209019..0000000000 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationWorkspaceTests.swift +++ /dev/null @@ -1,206 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2024 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 XCTest -@testable import SwiftDocC - -// This test verifies the behavior of `DocumentationWorkspace` which is a deprecated type. -// Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -class DocumentationWorkspaceTests: XCTestCase { - func testEmptyWorkspace() { - let workspace = DocumentationWorkspace() - let workspaceDelegate = SimpleWorkspaceDelegate() - workspace.delegate = workspaceDelegate - - XCTAssertEqual(workspace.bundles.count, 0) - - XCTAssertEqual(workspaceDelegate.record, []) - - checkTestWorkspaceContents(workspace: workspace, bundles: [SimpleDataProvider.bundle1, SimpleDataProvider.bundle2], filled: false) - } - - func testRegisterProvider() throws { - let provider = SimpleDataProvider(bundles: [SimpleDataProvider.bundle1, SimpleDataProvider.bundle2]) - let workspace = DocumentationWorkspace() - let workspaceDelegate = SimpleWorkspaceDelegate() - workspace.delegate = workspaceDelegate - - try workspace.registerProvider(provider) - - let events: [SimpleWorkspaceDelegate.Event] = provider._bundles.map { .add($0.identifier) } - - XCTAssertEqual(workspace.bundles.count, 2) - for bundlePair in workspace.bundles { - XCTAssertEqual(bundlePair.key, bundlePair.value.identifier) - } - - XCTAssertEqual(Set(workspace.bundles.map { $0.value.identifier }), Set(provider._bundles.map { $0.identifier })) - XCTAssertEqual(workspaceDelegate.record, events) - - checkTestWorkspaceContents(workspace: workspace, bundles: provider._bundles, filled: true) - } - - func testUnregisterProvider() throws { - let provider = SimpleDataProvider(bundles: [SimpleDataProvider.bundle1, SimpleDataProvider.bundle2]) - let workspace = DocumentationWorkspace() - let workspaceDelegate = SimpleWorkspaceDelegate() - workspace.delegate = workspaceDelegate - - try workspace.registerProvider(provider) - - var events: [SimpleWorkspaceDelegate.Event] = provider._bundles.map { .add($0.identifier) } - - XCTAssertEqual(workspace.bundles.count, 2) - for bundlePair in workspace.bundles { - XCTAssertEqual(bundlePair.key, bundlePair.value.identifier) - } - - XCTAssertEqual(Set(workspace.bundles.map { $0.value.identifier }), Set(provider._bundles.map { $0.identifier })) - XCTAssertEqual(workspaceDelegate.record, events) - - checkTestWorkspaceContents(workspace: workspace, bundles: provider._bundles, filled: true) - - try workspace.unregisterProvider(provider) - - events.append(contentsOf: provider._bundles.map { .remove($0.identifier) }) - - XCTAssertEqual(workspace.bundles.count, 0) - XCTAssertEqual(workspaceDelegate.record, events) - - checkTestWorkspaceContents(workspace: workspace, bundles: provider._bundles, filled: false) - } - - func testMultipleProviders() throws { - let provider1 = SimpleDataProvider(bundles: [SimpleDataProvider.bundle1, SimpleDataProvider.bundle2]) - let workspace = DocumentationWorkspace() - let workspaceDelegate = SimpleWorkspaceDelegate() - workspace.delegate = workspaceDelegate - - try workspace.registerProvider(provider1) - - var events: [SimpleWorkspaceDelegate.Event] = provider1._bundles.map { .add($0.identifier) } - - XCTAssertEqual(workspace.bundles.count, 2) - for bundlePair in workspace.bundles { - XCTAssertEqual(bundlePair.key, bundlePair.value.identifier) - } - - XCTAssertEqual(Set(workspace.bundles.map { $0.value.identifier }), Set(provider1._bundles.map { $0.identifier })) - XCTAssertEqual(workspaceDelegate.record, events) - - checkTestWorkspaceContents(workspace: workspace, bundles: provider1._bundles, filled: true) - - let provider2 = SimpleDataProvider(bundles: [SimpleDataProvider.bundle3, SimpleDataProvider.bundle4]) - try workspace.registerProvider(provider2) - - events.append(contentsOf: provider2._bundles.map { .add($0.identifier) }) - - XCTAssertEqual(workspace.bundles.count, 4) - for bundlePair in workspace.bundles { - XCTAssertEqual(bundlePair.key, bundlePair.value.identifier) - } - - XCTAssertEqual(Set(workspace.bundles.map { $0.value.identifier }), Set(provider1._bundles.map { $0.identifier } + provider2._bundles.map { $0.identifier })) - XCTAssertEqual(workspaceDelegate.record, events) - - checkTestWorkspaceContents(workspace: workspace, bundles: provider1._bundles + provider2._bundles, filled: true) - } - - func checkTestWorkspaceContents(workspace: DocumentationWorkspace, bundles: [DocumentationBundle], filled: Bool, line: UInt = #line) { - func check(file: URL, bundle: DocumentationBundle, line: UInt) { - if filled { - XCTAssertEqual(try workspace.contentsOfURL(file, in: bundle), SimpleDataProvider.files[file]!, line: line) - } else { - XCTAssertThrowsError(try workspace.contentsOfURL(file, in: bundle), line: line) - } - } - - for bundle in bundles { - check(file: SimpleDataProvider.testMarkupFile, bundle: bundle, line: line) - check(file: SimpleDataProvider.testResourceFile, bundle: bundle, line: line) - check(file: SimpleDataProvider.testSymbolGraphFile, bundle: bundle, line: line) - } - } - - struct SimpleDataProvider: DocumentationWorkspaceDataProvider { - let identifier: String = UUID().uuidString - - static let testMarkupFile = URL(fileURLWithPath: "/test.documentation/markup.md") - static let testResourceFile = URL(fileURLWithPath: "/test.documentation/resource.png") - static let testSymbolGraphFile = URL(fileURLWithPath: "/test.documentation/graph.json") - - static var files: [URL: Data] = [ - testMarkupFile: staticDataFromString("markup"), - testResourceFile: staticDataFromString("image"), - testSymbolGraphFile: staticDataFromString("symbols"), - ] - - private static func staticDataFromString(_ string: String) -> Data { - return string.data(using: .utf8)! - } - - static func bundle(_ suffix: String) -> DocumentationBundle { - return DocumentationBundle( - info: DocumentationBundle.Info( - displayName: "Test" + suffix, - id: DocumentationBundle.Identifier(rawValue: "com.example.test" + suffix) - ), - symbolGraphURLs: [testSymbolGraphFile], - markupURLs: [testMarkupFile], - miscResourceURLs: [testResourceFile] - ) - } - - static let bundle1 = bundle("1") - static let bundle2 = bundle("2") - static let bundle3 = bundle("3") - static let bundle4 = bundle("4") - - enum ProviderError: Error { - case missing - } - - func contentsOfURL(_ url: URL) throws -> Data { - guard let data = SimpleDataProvider.files[url] else { - throw ProviderError.missing - } - - return data - } - - var _bundles: [DocumentationBundle] = [] - - func bundles(options: BundleDiscoveryOptions) throws -> [DocumentationBundle] { - // Ignore the bundle discovery options. These test bundles are already built. - return _bundles - } - - init(bundles: [DocumentationBundle]) { - self._bundles = bundles - } - } - - class SimpleWorkspaceDelegate: DocumentationContextDataProviderDelegate { - enum Event: Equatable { - case add(String) - case remove(String) - } - var record: [Event] = [] - - func dataProvider(_ dataProvider: any DocumentationContextDataProvider, didAddBundle bundle: DocumentationBundle) throws { - record.append(.add(bundle.identifier)) - } - - func dataProvider(_ dataProvider: any DocumentationContextDataProvider, didRemoveBundle bundle: DocumentationBundle) throws { - record.append(.remove(bundle.identifier)) - } - } -} diff --git a/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift index ebf394ab15..0d0bab01b5 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -23,8 +23,8 @@ class ExternalPathHierarchyResolverTests: XCTestCase { // These tests resolve absolute symbol links in both a local and external context to verify that external links work the same local links. - func testUnambiguousAbsolutePaths() throws { - let linkResolvers = try makeLinkResolversForTestBundle(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testUnambiguousAbsolutePaths() async throws { + let linkResolvers = try await makeLinkResolversForTestBundle(named: "MixedLanguageFrameworkWithLanguageRefinements") try linkResolvers.assertSuccessfullyResolves(authoredLink: "/MixedFramework") @@ -408,8 +408,8 @@ class ExternalPathHierarchyResolverTests: XCTestCase { ) } - func testAmbiguousPaths() throws { - let linkResolvers = try makeLinkResolversForTestBundle(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testAmbiguousPaths() async throws { + let linkResolvers = try await makeLinkResolversForTestBundle(named: "MixedLanguageFrameworkWithLanguageRefinements") // public enum CollisionsWithDifferentKinds { // case something @@ -577,8 +577,8 @@ class ExternalPathHierarchyResolverTests: XCTestCase { ) } - func testRedundantDisambiguations() throws { - let linkResolvers = try makeLinkResolversForTestBundle(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testRedundantDisambiguations() async throws { + let linkResolvers = try await makeLinkResolversForTestBundle(named: "MixedLanguageFrameworkWithLanguageRefinements") try linkResolvers.assertSuccessfullyResolves(authoredLink: "/MixedFramework") @@ -685,7 +685,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { ) } - func testSymbolLinksInDeclarationsAndRelationships() throws { + func testSymbolLinksInDeclarationsAndRelationships() async throws { // Build documentation for the dependency first let symbols = [("First", .class), ("Second", .protocol), ("Third", .struct), ("Fourth", .enum)].map { (name: String, kind: SymbolGraph.Symbol.KindIdentifier) in return SymbolGraph.Symbol( @@ -699,7 +699,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { ) } - let (dependencyBundle, dependencyContext) = try loadBundle( + let (_ , dependencyContext) = try await loadBundle( catalog: Folder(name: "Dependency.docc", content: [ InfoPlist(identifier: "com.example.dependency"), // This isn't necessary but makes it easier to distinguish the identifier from the module name in the external references. JSONFile(name: "Dependency.symbols.json", content: makeSymbolGraph(moduleName: "Dependency", symbols: symbols)) @@ -707,7 +707,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { ) // Retrieve the link information from the dependency, as if '--enable-experimental-external-link-support' was passed to DocC - let dependencyConverter = DocumentationContextConverter(bundle: dependencyBundle, context: dependencyContext, renderContext: .init(documentationContext: dependencyContext, bundle: dependencyBundle)) + let dependencyConverter = DocumentationContextConverter(context: dependencyContext, renderContext: .init(documentationContext: dependencyContext)) let linkSummaries: [LinkDestinationSummary] = try dependencyContext.knownPages.flatMap { reference in let entity = try dependencyContext.entity(with: reference) @@ -715,7 +715,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { return entity.externallyLinkableElementSummaries(context: dependencyContext, renderNode: renderNode, includeTaskGroups: false) } - let linkResolutionInformation = try dependencyContext.linkResolver.localResolver.prepareForSerialization(bundleID: dependencyBundle.id) + let linkResolutionInformation = try dependencyContext.linkResolver.localResolver.prepareForSerialization(bundleID: dependencyContext.inputs.id) XCTAssertEqual(linkResolutionInformation.pathHierarchy.nodes.count - linkResolutionInformation.nonSymbolPaths.count, 5 /* 4 symbols & 1 module */) XCTAssertEqual(linkSummaries.count, 5 /* 4 symbols & 1 module */) @@ -724,7 +724,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { configuration.externalDocumentationConfiguration.dependencyArchives = [URL(fileURLWithPath: "/Dependency.doccarchive")] // After building the dependency, - let (mainBundle, mainContext) = try loadBundle( + let (_, mainContext) = try await loadBundle( catalog: Folder(name: "Main.docc", content: [ JSONFile(name: "Main.symbols.json", content: makeSymbolGraph( moduleName: "Main", @@ -794,11 +794,11 @@ class ExternalPathHierarchyResolverTests: XCTestCase { XCTAssertEqual(mainContext.knownPages.count, 3 /* 2 symbols & 1 module*/) - let mainConverter = DocumentationContextConverter(bundle: mainBundle, context: mainContext, renderContext: .init(documentationContext: mainContext, bundle: mainBundle)) + let mainConverter = DocumentationContextConverter(context: mainContext, renderContext: .init(documentationContext: mainContext)) // Check the relationships of 'SomeClass' do { - let reference = ResolvedTopicReference(bundleID: mainBundle.id, path: "/documentation/Main/SomeClass", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: mainContext.inputs.id, path: "/documentation/Main/SomeClass", sourceLanguage: .swift) let entity = try mainContext.entity(with: reference) let renderNode = try XCTUnwrap(mainConverter.renderNode(for: entity)) @@ -822,7 +822,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { // Check the declaration of 'someFunction' do { - let reference = ResolvedTopicReference(bundleID: mainBundle.id, path: "/documentation/Main/SomeClass/someFunction(parameter:)", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: mainContext.inputs.id, path: "/documentation/Main/SomeClass/someFunction(parameter:)", sourceLanguage: .swift) let entity = try mainContext.entity(with: reference) let renderNode = try XCTUnwrap(mainConverter.renderNode(for: entity)) @@ -851,10 +851,10 @@ class ExternalPathHierarchyResolverTests: XCTestCase { } } - func testOverloadGroupSymbolsResolveWithoutHash() throws { + func testOverloadGroupSymbolsResolveWithoutHash() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let linkResolvers = try makeLinkResolversForTestBundle(named: "OverloadedSymbols") + let linkResolvers = try await makeLinkResolversForTestBundle(named: "OverloadedSymbols") // The enum case should continue to resolve by kind, since it has no hash collision try linkResolvers.assertSuccessfullyResolves(authoredLink: "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-swift.enum.case") @@ -872,7 +872,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { ) } - func testBetaInformationPreserved() throws { + func testBetaInformationPreserved() async throws { let platformMetadata = [ "macOS": PlatformVersion(VersionTriplet(1, 0, 0), beta: true), "watchOS": PlatformVersion(VersionTriplet(2, 0, 0), beta: true), @@ -884,7 +884,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { var configuration = DocumentationContext.Configuration() configuration.externalMetadata.currentPlatforms = platformMetadata - let linkResolvers = try makeLinkResolversForTestBundle(named: "AvailabilityBetaBundle", configuration: configuration) + let linkResolvers = try await makeLinkResolversForTestBundle(named: "AvailabilityBetaBundle", configuration: configuration) // MyClass is only available on beta platforms (macos=1.0.0, watchos=2.0.0, tvos=3.0.0, ios=4.0.0) try linkResolvers.assertBetaStatus(authoredLink: "/MyKit/MyClass", isBeta: true) @@ -950,7 +950,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { switch result { case .success(let resolved): let entity = externalResolver.entity(resolved) - XCTAssertEqual(entity.topicRenderReference.isBeta, isBeta, file: file, line: line) + XCTAssertEqual(entity.makeTopicRenderReference().isBeta, isBeta, file: file, line: line) case .failure(_, let errorInfo): XCTFail("Unexpectedly failed to resolve \(label) link: \(errorInfo.message) \(errorInfo.solutions.map(\.summary).joined(separator: ", "))", file: file, line: line) } @@ -989,18 +989,18 @@ class ExternalPathHierarchyResolverTests: XCTestCase { } } - private func makeLinkResolversForTestBundle(named testBundleName: String, configuration: DocumentationContext.Configuration = .init()) throws -> LinkResolvers { + private func makeLinkResolversForTestBundle(named testBundleName: String, configuration: DocumentationContext.Configuration = .init()) async throws -> LinkResolvers { let bundleURL = try XCTUnwrap(Bundle.module.url(forResource: testBundleName, withExtension: "docc", subdirectory: "Test Bundles")) - let (_, bundle, context) = try loadBundle(from: bundleURL, configuration: configuration) + let (_, _, context) = try await loadBundle(from: bundleURL, configuration: configuration) let localResolver = try XCTUnwrap(context.linkResolver.localResolver) - let resolverInfo = try localResolver.prepareForSerialization(bundleID: bundle.id) + let resolverInfo = try localResolver.prepareForSerialization(bundleID: context.inputs.id) let resolverData = try JSONEncoder().encode(resolverInfo) let roundtripResolverInfo = try JSONDecoder().decode(SerializableLinkResolutionInformation.self, from: resolverData) var entitySummaries = [LinkDestinationSummary]() - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) for reference in context.knownPages { let node = try context.entity(with: reference) let renderNode = converter.convert(node) diff --git a/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift index e5ed51ddb4..d4a10d31d5 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -38,27 +38,21 @@ class ExternalReferenceResolverTests: XCTestCase { fatalError("It is a programming mistake to retrieve an entity for a reference that the external resolver didn't resolve.") } - let (kind, role) = DocumentationContentRenderer.renderKindAndRole(resolvedEntityKind, semantic: nil) return LinkResolver.ExternalEntity( - topicRenderReference: TopicRenderReference( - identifier: .init(reference.absoluteString), - title: resolvedEntityTitle, - abstract: [.text("Externally Resolved Markup Content")], - url: "/example" + reference.path + (reference.fragment.map { "#\($0)" } ?? ""), - kind: kind, - role: role, - fragments: resolvedEntityDeclarationFragments?.declarationFragments.map { fragment in - return DeclarationRenderSection.Token(fragment: fragment, identifier: nil) - } - ), - renderReferenceDependencies: RenderReferenceDependencies(), - sourceLanguages: [resolvedEntityLanguage] + kind: resolvedEntityKind, + language: resolvedEntityLanguage, + relativePresentationURL: URL(string: "/example" + reference.path + (reference.fragment.map { "#\($0)" } ?? ""))!, + referenceURL: reference.url, + title: resolvedEntityTitle, + availableLanguages: [resolvedEntityLanguage], + subheadingDeclarationFragments: resolvedEntityDeclarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, + variants: [] ) } } - func testResolveExternalReference() throws { - let (_, bundle, context) = try testBundleAndContext( + func testResolveExternalReference() async throws { + let (_, bundle, context) = try await testBundleAndContext( copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: ["com.external.testbundle" : TestExternalReferenceResolver()] ) { url in @@ -86,7 +80,7 @@ class ExternalReferenceResolverTests: XCTestCase { // Asserts that an external reference from a source language not locally included // in the current DocC catalog is still included in any rendered topic groups that // manually curate it. (94406023) - func testExternalReferenceInOtherLanguageIsIncludedInTopicGroup() throws { + func testExternalReferenceInOtherLanguageIsIncludedInTopicGroup() async throws { let externalResolver = TestExternalReferenceResolver() externalResolver.bundleID = "com.test.external" externalResolver.expectedReferencePath = "/path/to/external/api" @@ -96,7 +90,7 @@ class ExternalReferenceResolverTests: XCTestCase { // Set the language of the externally resolved entity to 'data'. externalResolver.resolvedEntityLanguage = .data - let (_, bundle, context) = try testBundleAndContext( + let (_, _, context) = try await testBundleAndContext( copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver] ) { url in @@ -118,9 +112,9 @@ class ExternalReferenceResolverTests: XCTestCase { try sideClassExtension.write(to: sideClassExtensionURL, atomically: true, encoding: .utf8) } - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let sideClassReference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift ) @@ -153,81 +147,15 @@ class ExternalReferenceResolverTests: XCTestCase { ) } - // This test verifies the behavior of a deprecated functionality (changing external documentation sources after registering the documentation) - // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - func testResolvesReferencesExternallyOnlyWhenFallbackResolversAreSet() throws { - let workspace = DocumentationWorkspace() - let bundle = try testBundle(named: "LegacyBundle_DoNotUseInNewTests") - let dataProvider = PrebuiltLocalFileSystemDataProvider(bundles: [bundle]) - try workspace.registerProvider(dataProvider) - let context = try DocumentationContext(dataProvider: workspace) - let bundleIdentifier = bundle.identifier - - let unresolved = UnresolvedTopicReference(topicURL: ValidatedURL(parsingExact: "doc://\(bundleIdentifier)/ArticleThatDoesNotExistInLocally")!) - let parent = ResolvedTopicReference(bundleIdentifier: bundle.id.rawValue, path: "", sourceLanguage: .swift) - - do { - context.configuration.externalDocumentationConfiguration.sources = [:] - context.configuration.convertServiceConfiguration.fallbackResolver = nil - - if case .success = context.resolve(.unresolved(unresolved), in: parent) { - XCTFail("The reference was unexpectedly resolved.") - } - } - - do { - class TestFallbackResolver: ConvertServiceFallbackResolver { - init(bundleID: DocumentationBundle.Identifier) { - resolver.bundleID = bundleID - } - var bundleID: DocumentationBundle.Identifier { - resolver.bundleID - } - private var resolver = TestExternalReferenceResolver() - func resolve(_ reference: SwiftDocC.TopicReference) -> TopicReferenceResolutionResult { - TestExternalReferenceResolver().resolve(reference) - } - func entityIfPreviouslyResolved(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity? { - nil - } - func resolve(assetNamed assetName: String) -> DataAsset? { - nil - } - } - - context.configuration.externalDocumentationConfiguration.sources = [:] - context.configuration.convertServiceConfiguration.fallbackResolver = TestFallbackResolver(bundleID: "org.swift.docc.example") - - guard case let .success(resolved) = context.resolve(.unresolved(unresolved), in: parent) else { - XCTFail("The reference was unexpectedly unresolved.") - return - } - - XCTAssertEqual("com.external.testbundle", resolved.bundleIdentifier) - XCTAssertEqual("/externally/resolved/path", resolved.path) - - let expectedURL = URL(string: "doc://com.external.testbundle/externally/resolved/path") - XCTAssertEqual(expectedURL, resolved.url) - - try workspace.unregisterProvider(dataProvider) - context.configuration.externalDocumentationConfiguration.sources = [:] - guard case .failure = context.resolve(.unresolved(unresolved), in: parent) else { - XCTFail("Unexpectedly resolved \(unresolved.topicURL) despite removing a data provider for it") - return - } - } - } - - func testLoadEntityForExternalReference() throws { - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests", externalResolvers: ["com.external.testbundle" : TestExternalReferenceResolver()]) + func testLoadEntityForExternalReference() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests", externalResolvers: ["com.external.testbundle" : TestExternalReferenceResolver()]) let identifier = ResolvedTopicReference(bundleID: "com.external.testbundle", path: "/externally/resolved/path", sourceLanguage: .swift) XCTAssertThrowsError(try context.entity(with: ResolvedTopicReference(bundleID: "some.other.bundle", path: identifier.path, sourceLanguage: .swift))) XCTAssertThrowsError(try context.entity(with: identifier)) } - func testRenderReferenceHasSymbolKind() throws { + func testRenderReferenceHasSymbolKind() async throws { let fixtures: [(DocumentationNode.Kind, RenderNode.Kind)] = [ (.class, .symbol), (.structure, .symbol), @@ -251,10 +179,10 @@ class ExternalReferenceResolverTests: XCTestCase { externalResolver.resolvedEntityTitle = "ClassName" externalResolver.resolvedEntityKind = resolvedEntityKind - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) + let converter = DocumentationNodeConverter(context: context) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) guard let fileURL = context.documentURL(for: node.reference) else { XCTFail("Unable to find the file for \(node.reference.path)") @@ -281,7 +209,7 @@ class ExternalReferenceResolverTests: XCTestCase { } } - func testReferenceFromRenderedPageHasFragments() throws { + func testReferenceFromRenderedPageHasFragments() async throws { let externalResolver = TestExternalReferenceResolver() externalResolver.bundleID = "com.test.external" externalResolver.expectedReferencePath = "/path/to/external/symbol" @@ -293,7 +221,7 @@ class ExternalReferenceResolverTests: XCTestCase { .init(kind: .identifier, spelling: "ClassName", preciseIdentifier: nil), ]) - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) { url in try """ # ``SideKit/SideClass`` @@ -307,8 +235,8 @@ class ExternalReferenceResolverTests: XCTestCase { """.write(to: url.appendingPathComponent("documentation/sideclass.md"), atomically: true, encoding: .utf8) } - let converter = DocumentationNodeConverter(bundle: bundle, context: context) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) + let converter = DocumentationNodeConverter(context: context) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) let renderNode = converter.convert(node) @@ -328,7 +256,7 @@ class ExternalReferenceResolverTests: XCTestCase { ]) } - func testExternalReferenceWithDifferentResolvedPath() throws { + func testExternalReferenceWithDifferentResolvedPath() async throws { let externalResolver = TestExternalReferenceResolver() externalResolver.bundleID = "com.test.external" // Return a different path for this resolved reference @@ -350,10 +278,10 @@ class ExternalReferenceResolverTests: XCTestCase { var configuration = DocumentationContext.Configuration() configuration.externalDocumentationConfiguration.sources = [externalResolver.bundleID: externalResolver] - let (bundle, context) = try loadBundle(catalog: tempFolder, configuration: configuration) + let (_, context) = try await loadBundle(catalog: tempFolder, configuration: configuration) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/article", sourceLanguage: .swift)) + let converter = DocumentationNodeConverter(context: context) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/article", sourceLanguage: .swift)) let renderNode = converter.convert(node) @@ -378,14 +306,14 @@ class ExternalReferenceResolverTests: XCTestCase { } } - func testSampleCodeReferenceHasSampleCodeRole() throws { + func testSampleCodeReferenceHasSampleCodeRole() async throws { let externalResolver = TestExternalReferenceResolver() externalResolver.bundleID = "com.test.external" externalResolver.expectedReferencePath = "/path/to/external/sample" externalResolver.resolvedEntityTitle = "Name of Sample" externalResolver.resolvedEntityKind = .sampleCode - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [externalResolver.bundleID: externalResolver]) { url in try """ # ``SideKit/SideClass`` @@ -399,8 +327,8 @@ class ExternalReferenceResolverTests: XCTestCase { """.write(to: url.appendingPathComponent("documentation/sideclass.md"), atomically: true, encoding: .utf8) } - let converter = DocumentationNodeConverter(bundle: bundle, context: context) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) + let converter = DocumentationNodeConverter(context: context) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) let renderNode = converter.convert(node) @@ -417,7 +345,7 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(sampleRenderReference.role, RenderMetadata.Role.sampleCode.rawValue) } - func testExternalTopicWithTopicImage() throws { + func testExternalTopicWithTopicImage() async throws { let externalResolver = TestMultiResultExternalReferenceResolver() externalResolver.bundleID = "com.test.external" @@ -473,7 +401,7 @@ class ExternalReferenceResolverTests: XCTestCase { ), ] - let (_, bundle, context) = try testBundleAndContext(copying: "SampleBundle", excludingPaths: ["MySample.md", "MyLocalSample.md"], externalResolvers: [externalResolver.bundleID: externalResolver]) { url in + let (_, _, context) = try await testBundleAndContext(copying: "SampleBundle", excludingPaths: ["MySample.md", "MyLocalSample.md"], externalResolvers: [externalResolver.bundleID: externalResolver]) { url in try """ # SomeSample @@ -498,8 +426,8 @@ class ExternalReferenceResolverTests: XCTestCase { """.write(to: url.appendingPathComponent("SomeSample.md"), atomically: true, encoding: .utf8) } - let converter = DocumentationNodeConverter(bundle: bundle, context: context) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SomeSample", sourceLanguage: .swift)) + let converter = DocumentationNodeConverter(context: context) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SomeSample", sourceLanguage: .swift)) let renderNode = converter.convert(node) @@ -510,7 +438,7 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(firstExternalRenderReference.identifier.identifier, "doc://com.test.external/path/to/external-page-with-topic-image-1") XCTAssertEqual(firstExternalRenderReference.title, "First external page with topic image") - XCTAssertEqual(firstExternalRenderReference.url, "/example/path/to/external-page-with-topic-image-1") + XCTAssertEqual(firstExternalRenderReference.url, "/path/to/external-page-with-topic-image-1") XCTAssertEqual(firstExternalRenderReference.kind, .article) XCTAssertEqual(firstExternalRenderReference.images, [ @@ -522,7 +450,7 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(secondExternalRenderReference.identifier.identifier, "doc://com.test.external/path/to/external-page-with-topic-image-2") XCTAssertEqual(secondExternalRenderReference.title, "Second external page with topic image") - XCTAssertEqual(secondExternalRenderReference.url, "/example/path/to/external-page-with-topic-image-2") + XCTAssertEqual(secondExternalRenderReference.url, "/path/to/external-page-with-topic-image-2") XCTAssertEqual(secondExternalRenderReference.kind, .article) XCTAssertEqual(secondExternalRenderReference.images, [ @@ -585,7 +513,7 @@ class ExternalReferenceResolverTests: XCTestCase { } // Tests that external references are included in task groups, rdar://72119391 - func testResolveExternalReferenceInTaskGroups() throws { + func testResolveExternalReferenceInTaskGroups() async throws { let resolver = TestMultiResultExternalReferenceResolver() resolver.entitiesToReturn = [ "/article": .success(.init(referencePath: "/externally/resolved/path/to/article")), @@ -595,7 +523,7 @@ class ExternalReferenceResolverTests: XCTestCase { "/externally/resolved/path/to/article2": .success(.init(referencePath: "/externally/resolved/path/to/article2")), ] - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [ + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [ "com.external.testbundle" : resolver ]) { url in // Add external links to the MyKit Topics. @@ -625,9 +553,9 @@ class ExternalReferenceResolverTests: XCTestCase { } // Tests that external references are resolved in tutorial content - func testResolveExternalReferenceInTutorials() throws { + func testResolveExternalReferenceInTutorials() async throws { let resolver = TestExternalReferenceResolver() - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: ["com.external.bundle": resolver, "com.external.testbundle": resolver], configureBundle: { (bundleURL) in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: ["com.external.bundle": resolver, "com.external.testbundle": resolver], configureBundle: { (bundleURL) in // Replace TestTutorial.tutorial with a copy that includes a bunch of external links try FileManager.default.removeItem(at: bundleURL.appendingPathComponent("TestTutorial.tutorial")) try FileManager.default.copyItem( @@ -671,7 +599,7 @@ class ExternalReferenceResolverTests: XCTestCase { } // Tests that external references are included in task groups, rdar://72119391 - func testExternalResolverIsNotPassedReferencesItDidNotResolve() throws { + func testExternalResolverIsNotPassedReferencesItDidNotResolve() async throws { final class CallCountingReferenceResolver: ExternalDocumentationSource { var referencesAskedToResolve: Set = [] @@ -696,18 +624,15 @@ class ExternalReferenceResolverTests: XCTestCase { func entity(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity { referencesCreatingEntityFor.insert(reference) - // Return an empty node + // Return an "empty" node return .init( - topicRenderReference: TopicRenderReference( - identifier: .init(reference.absoluteString), - title: "Resolved", - abstract: [], - url: reference.absoluteString, - kind: .symbol, - estimatedTime: nil - ), - renderReferenceDependencies: RenderReferenceDependencies(), - sourceLanguages: [.swift] + kind: .instanceProperty, + language: .swift, + relativePresentationURL: reference.url.withoutHostAndPortAndScheme(), + referenceURL: reference.url, + title: "Resolved", + availableLanguages: [.swift], + variants: [] ) } } @@ -716,7 +641,7 @@ class ExternalReferenceResolverTests: XCTestCase { // Copy the test bundle and add external links to the MyKit See Also. // We're using a See Also group, because external links aren't rendered in Topics groups. - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: ["com.external.testbundle" : resolver]) { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: ["com.external.testbundle" : resolver]) { url in try """ # ``MyKit`` MyKit module root symbol @@ -768,8 +693,8 @@ class ExternalReferenceResolverTests: XCTestCase { "The external reference resolver error message is included in that problem's error summary.") // Get MyKit symbol - let entity = try context.entity(with: .init(bundleID: bundle.id, path: "/documentation/MyKit", sourceLanguage: .swift)) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let entity = try context.entity(with: .init(bundleID: context.inputs.id, path: "/documentation/MyKit", sourceLanguage: .swift)) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(entity) let taskGroupLinks = try XCTUnwrap(renderNode.seeAlsoSections.first?.identifiers) @@ -793,7 +718,7 @@ class ExternalReferenceResolverTests: XCTestCase { } /// Tests that the external resolving handles correctly fragments in URLs. - func testExternalReferenceWithFragment() throws { + func testExternalReferenceWithFragment() async throws { // Configure an external resolver let resolver = TestExternalReferenceResolver() @@ -802,7 +727,7 @@ class ExternalReferenceResolverTests: XCTestCase { resolver.expectedFragment = "67890" // Prepare a test bundle - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: ["com.external.testbundle" : resolver], externalSymbolResolver: nil, configureBundle: { url in + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: ["com.external.testbundle" : resolver], externalSymbolResolver: nil, configureBundle: { url in // Add external link with fragment let myClassMDURL = url.appendingPathComponent("documentation").appendingPathComponent("myclass.md") try String(contentsOf: myClassMDURL) @@ -829,7 +754,7 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(linkReference.absoluteString, "doc://com.external.testbundle/externally/resolved/path#67890") } - func testExternalArticlesAreIncludedInAllVariantsTopicsSection() throws { + func testExternalArticlesAreIncludedInAllVariantsTopicsSection() async throws { let externalResolver = TestMultiResultExternalReferenceResolver() externalResolver.bundleID = "com.test.external" @@ -869,7 +794,7 @@ class ExternalReferenceResolverTests: XCTestCase { ) ) - let (_, bundle, context) = try testBundleAndContext( + let (_, _, context) = try await testBundleAndContext( copying: "MixedLanguageFramework", externalResolvers: [externalResolver.bundleID: externalResolver] ) { url in @@ -889,9 +814,9 @@ class ExternalReferenceResolverTests: XCTestCase { """ try mixedLanguageFrameworkExtension.write(to: url.appendingPathComponent("/MixedLanguageFramework.md"), atomically: true, encoding: .utf8) } - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let mixedLanguageFrameworkReference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/MixedLanguageFramework", sourceLanguage: .swift ) @@ -921,7 +846,7 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertFalse(objCTopicIDs.contains("doc://com.test.external/path/to/external/swiftSymbol")) } - func testDeprecationSummaryWithExternalLink() throws { + func testDeprecationSummaryWithExternalLink() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( moduleName: "ModuleName", @@ -963,7 +888,7 @@ class ExternalReferenceResolverTests: XCTestCase { var configuration = DocumentationContext.Configuration() configuration.externalDocumentationConfiguration.sources = [resolver.bundleID: resolver] - let (bundle, context) = try loadBundle(catalog: catalog, configuration: configuration) + let (bundle, context) = try await loadBundle(catalog: catalog, configuration: configuration) XCTAssert(context.problems.isEmpty, "Unexpected problems:\n\(context.problems.map(\.diagnostic.summary).joined(separator: "\n"))") @@ -986,7 +911,7 @@ class ExternalReferenceResolverTests: XCTestCase { } } - func testExternalLinkInGeneratedSeeAlso() throws { + func testExternalLinkInGeneratedSeeAlso() async throws { let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "Root.md", utf8Content: """ # Root @@ -1020,7 +945,7 @@ class ExternalReferenceResolverTests: XCTestCase { var configuration = DocumentationContext.Configuration() configuration.externalDocumentationConfiguration.sources = [resolver.bundleID: resolver] - let (bundle, context) = try loadBundle(catalog: catalog, configuration: configuration) + let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") @@ -1036,10 +961,10 @@ class ExternalReferenceResolverTests: XCTestCase { ]) // Check the rendered SeeAlso sections for the two curated articles. - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) do { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/unit-test/First", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/unit-test/First", sourceLanguage: .swift) let node = try context.entity(with: reference) let rendered = converter.convert(node) @@ -1053,7 +978,7 @@ class ExternalReferenceResolverTests: XCTestCase { } do { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/unit-test/Second", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/unit-test/Second", sourceLanguage: .swift) let node = try context.entity(with: reference) let rendered = converter.convert(node) @@ -1067,7 +992,7 @@ class ExternalReferenceResolverTests: XCTestCase { } } - func testExternalLinkInAuthoredSeeAlso() throws { + func testExternalLinkInAuthoredSeeAlso() async throws { let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "Root.md", utf8Content: """ # Root @@ -1088,7 +1013,7 @@ class ExternalReferenceResolverTests: XCTestCase { var configuration = DocumentationContext.Configuration() configuration.externalDocumentationConfiguration.sources = [resolver.bundleID: resolver] - let (bundle, context) = try loadBundle(catalog: catalog, configuration: configuration) + let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") @@ -1096,7 +1021,7 @@ class ExternalReferenceResolverTests: XCTestCase { // Check the curation on the root page let reference = try XCTUnwrap(context.soleRootModuleReference) let node = try context.entity(with: reference) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let rendered = converter.convert(node) XCTAssertEqual(rendered.seeAlsoSections.count, 1, "The page should only have the authored See Also section.") @@ -1107,7 +1032,7 @@ class ExternalReferenceResolverTests: XCTestCase { ]) } - func testParametersWithExternalLink() throws { + func testParametersWithExternalLink() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.swift.symbols.json", content: makeSymbolGraph( @@ -1174,7 +1099,7 @@ class ExternalReferenceResolverTests: XCTestCase { var configuration = DocumentationContext.Configuration() configuration.externalDocumentationConfiguration.sources = [resolver.bundleID: resolver] - let (bundle, context) = try loadBundle(catalog: catalog, configuration: configuration) + let (bundle, context) = try await loadBundle(catalog: catalog, configuration: configuration) XCTAssert(context.problems.isEmpty, "Unexpected problems:\n\(context.problems.map(\.diagnostic.summary).joined(separator: "\n"))") @@ -1203,9 +1128,9 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(externalLinks.count, 4, "Did not resolve the 4 expected external links.") } - func exampleDocumentation(copying bundleName: String, documentationExtension: TextFile, path: String, file: StaticString = #filePath, line: UInt = #line) throws -> Symbol { + func exampleDocumentation(copying bundleName: String, documentationExtension: TextFile, path: String, file: StaticString = #filePath, line: UInt = #line) async throws -> Symbol { let externalResolver = TestExternalReferenceResolver() - let (_, bundle, context) = try testBundleAndContext( + let (_, bundle, context) = try await testBundleAndContext( copying: bundleName, externalResolvers: [externalResolver.bundleID: externalResolver] ) { url in @@ -1228,7 +1153,7 @@ class ExternalReferenceResolverTests: XCTestCase { return symbol } - func testDictionaryKeysWithExternalLink() throws { + func testDictionaryKeysWithExternalLink() async throws { // Create some example documentation using the symbol graph file located under // Tests/SwiftDocCTests/Test Bundles/DictionaryData.docc, and the following @@ -1248,7 +1173,7 @@ class ExternalReferenceResolverTests: XCTestCase { - monthOfBirth: 1 - genre: Classic Rock """) - let symbol = try exampleDocumentation( + let symbol = try await exampleDocumentation( copying: "DictionaryData", documentationExtension: documentationExtension, path: "/documentation/DictionaryData/Artist" @@ -1280,7 +1205,7 @@ class ExternalReferenceResolverTests: XCTestCase { // Create some example documentation using the symbol graph file located under // Tests/SwiftDocCTests/Test Bundles/HTTPRequests.docc, and the following // documentation extension markup. - func exampleRESTDocumentation(file: StaticString = #filePath, line: UInt = #line) throws -> Symbol { + func exampleRESTDocumentation(file: StaticString = #filePath, line: UInt = #line) async throws -> Symbol { let documentationExtension = TextFile( name: "GetArtist.md", utf8Content: """ @@ -1307,7 +1232,7 @@ class ExternalReferenceResolverTests: XCTestCase { - 204: Another response with a link: . - 887: Bad value. """) - return try exampleDocumentation( + return try await exampleDocumentation( copying: "HTTPRequests", documentationExtension: documentationExtension, path: "/documentation/HTTPRequests/Get_Artist", @@ -1317,11 +1242,11 @@ class ExternalReferenceResolverTests: XCTestCase { } - func testHTTPParametersWithExternalLink() throws { + func testHTTPParametersWithExternalLink() async throws { // Get the variant of the example symbol that has no interface language, meaning it was // generated by the markup above. - let symbol = try exampleRESTDocumentation() + let symbol = try await exampleRESTDocumentation() let section = try XCTUnwrap(symbol.httpParametersSection) XCTAssertEqual(section.parameters.count, 3) @@ -1343,11 +1268,11 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(externalLinkCount, 2, "Did not resolve the 2 expected external links.") } - func testHTTPBodyWithExternalLink() throws { + func testHTTPBodyWithExternalLink() async throws { // Get the variant of the example symbol that has no interface language, meaning it was // generated by the markup above. - let symbol = try exampleRESTDocumentation() + let symbol = try await exampleRESTDocumentation() let section = try XCTUnwrap(symbol.httpBodySection) // Check that the two keys with external links in the markup above were found @@ -1356,11 +1281,11 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(value, "Simple body with a link: .") } - func testHTTPBodyParametersWithExternalLink() throws { + func testHTTPBodyParametersWithExternalLink() async throws { // Get the variant of the example symbol that has no interface language, meaning it was // generated by the markup above. - let symbol = try exampleRESTDocumentation() + let symbol = try await exampleRESTDocumentation() let section = try XCTUnwrap(symbol.httpBodySection) XCTAssertEqual(section.body.parameters.count, 3) @@ -1383,11 +1308,11 @@ class ExternalReferenceResolverTests: XCTestCase { } - func testHTTPResponsesWithExternalLink() throws { + func testHTTPResponsesWithExternalLink() async throws { // Get the variant of the example symbol that has no interface language, meaning it was // generated by the markup above. - let symbol = try exampleRESTDocumentation() + let symbol = try await exampleRESTDocumentation() let section = try XCTUnwrap(symbol.httpResponsesSection) XCTAssertEqual(section.responses.count, 3) @@ -1409,7 +1334,7 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(externalLinkCount, 2, "Did not resolve the 2 expected external links.") } - func testPossibleValuesWithExternalLink() throws { + func testPossibleValuesWithExternalLink() async throws { // Create some example documentation using the symbol graph file located under // Tests/SwiftDocCTests/Test Bundles/DictionaryData.docc, and the following @@ -1427,7 +1352,7 @@ class ExternalReferenceResolverTests: XCTestCase { - Classic Rock: Something about classic rock with a link: . - Folk: Something about folk music with a link: . """) - let symbol = try exampleDocumentation( + let symbol = try await exampleDocumentation( copying: "DictionaryData", documentationExtension: documentationExtension, path: "/documentation/DictionaryData/Genre" @@ -1454,4 +1379,65 @@ class ExternalReferenceResolverTests: XCTestCase { XCTAssertEqual(externalLinkCount, 2, "Did not resolve the 2 expected external links.") } + func testExternalReferenceWithAbsolutePresentationURL() async throws { + class Resolver: ExternalDocumentationSource { + let bundleID: DocumentationBundle.Identifier = "com.example.test" + + func resolve(_ reference: TopicReference) -> TopicReferenceResolutionResult { + .success(ResolvedTopicReference(bundleID: bundleID, path: "/path/to/something", sourceLanguage: .swift)) + } + + var entityToReturn: LinkDestinationSummary + init(entityToReturn: LinkDestinationSummary) { + self.entityToReturn = entityToReturn + } + + func entity(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity { + entityToReturn + } + } + + let catalog = Folder(name: "unit-test.docc", content: [ + TextFile(name: "Root.md", utf8Content: """ + # Root + + Link to an external page: + """), + ]) + + // Only decoded link summaries support absolute presentation URLs. + let externalEntity = try JSONDecoder().decode(LinkDestinationSummary.self, from: Data(""" + { + "path": "https://com.example/path/to/something", + "title": "Something", + "kind": "org.swift.docc.kind.article", + "referenceURL": "doc://com.example.test/path/to/something", + "language": "swift", + "availableLanguages": [ + "swift" + ] + } + """.utf8)) + XCTAssertEqual(externalEntity.relativePresentationURL.absoluteString, "/path/to/something") + XCTAssertEqual(externalEntity.absolutePresentationURL?.absoluteString, "https://com.example/path/to/something") + + // Test that encoding the link summary preserves the absolute URL + try assertRoundTripCoding(externalEntity) + + let resolver = Resolver(entityToReturn: externalEntity) + + var configuration = DocumentationContext.Configuration() + configuration.externalDocumentationConfiguration.sources = [resolver.bundleID: resolver] + let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration) + + XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") + + // Check the curation on the root page + let rootNode = try context.entity(with: XCTUnwrap(context.soleRootModuleReference)) + let converter = DocumentationNodeConverter(context: context) + + let renderNode = converter.convert(rootNode) + let externalTopicReference = try XCTUnwrap(renderNode.references.values.first as? TopicRenderReference) + XCTAssertEqual(externalTopicReference.url, "https://com.example/path/to/something") + } } diff --git a/Tests/SwiftDocCTests/Infrastructure/GeneratedDataProvider.swift b/Tests/SwiftDocCTests/Infrastructure/GeneratedDataProvider.swift deleted file mode 100644 index 56360a8793..0000000000 --- a/Tests/SwiftDocCTests/Infrastructure/GeneratedDataProvider.swift +++ /dev/null @@ -1,154 +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 XCTest -@testable import SwiftDocC -import SymbolKit - -// This test verifies the behavior of `GeneratedDataProvider` which is a deprecated type. -// Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. -@available(*, deprecated, message: "Use 'DocumentationContext.InputProvider' instead. This deprecated API will be removed after 6.2 is released") -class GeneratedDataProviderTests: XCTestCase { - - func testGeneratingBundles() throws { - let firstSymbolGraph = SymbolGraph( - metadata: .init( - formatVersion: .init(major: 0, minor: 0, patch: 1), - generator: "unit-test" - ), - module: .init( - name: "FirstModuleName", - platform: .init() - ), - symbols: [], - relationships: [] - ) - var secondSymbolGraph = firstSymbolGraph - secondSymbolGraph.module.name = "SecondModuleName" - - let thirdSymbolGraph = firstSymbolGraph // Another symbol graph with the same module name - - let dataProvider = GeneratedDataProvider( - symbolGraphDataLoader: { - switch $0.lastPathComponent { - case "first.symbols.json": - return try? JSONEncoder().encode(firstSymbolGraph) - case "second.symbols.json": - return try? JSONEncoder().encode(secondSymbolGraph) - case "third.symbols.json": - return try? JSONEncoder().encode(thirdSymbolGraph) - default: - return nil - } - } - ) - - let options = BundleDiscoveryOptions( - infoPlistFallbacks: [ - "CFBundleDisplayName": "Custom Display Name", - "CFBundleIdentifier": "com.test.example", - ], - additionalSymbolGraphFiles: [ - URL(fileURLWithPath: "first.symbols.json"), - URL(fileURLWithPath: "second.symbols.json"), - URL(fileURLWithPath: "third.symbols.json"), - ] - ) - let bundles = try dataProvider.bundles(options: options) - XCTAssertEqual(bundles.count, 1) - let bundle = try XCTUnwrap(bundles.first) - - XCTAssertEqual(bundle.displayName, "Custom Display Name") - XCTAssertEqual(bundle.symbolGraphURLs.map { $0.lastPathComponent }.sorted(), [ - "first.symbols.json", - "second.symbols.json", - "third.symbols.json", - - ]) - XCTAssertEqual(bundle.markupURLs.map { $0.path }.sorted(), [ - "FirstModuleName.md", - "SecondModuleName.md", - // No third file since that symbol graph has the same module name as the first - ]) - - XCTAssertEqual( - try String(data: dataProvider.contentsOfURL(URL(fileURLWithPath: "FirstModuleName.md")), encoding: .utf8), - "# ``FirstModuleName``" - ) - XCTAssertEqual( - try String(data: dataProvider.contentsOfURL(URL(fileURLWithPath: "SecondModuleName.md")), encoding: .utf8), - "# ``SecondModuleName``" - ) - } - - func testGeneratingSingleModuleBundle() throws { - let firstSymbolGraph = SymbolGraph( - metadata: .init( - formatVersion: .init(major: 0, minor: 0, patch: 1), - generator: "unit-test" - ), - module: .init( - name: "FirstModuleName", - platform: .init() - ), - symbols: [], - relationships: [] - ) - - let secondSymbolGraph = firstSymbolGraph // Another symbol graph with the same module name - - let dataProvider = GeneratedDataProvider( - symbolGraphDataLoader: { - switch $0.lastPathComponent { - case "first.symbols.json": - return try? JSONEncoder().encode(firstSymbolGraph) - case "second.symbols.json": - return try? JSONEncoder().encode(secondSymbolGraph) - default: - return nil - } - } - ) - - let options = BundleDiscoveryOptions( - infoPlistFallbacks: [ - "CFBundleDisplayName": "Custom Display Name", - "CFBundleIdentifier": "com.test.example", - ], - additionalSymbolGraphFiles: [ - URL(fileURLWithPath: "first.symbols.json"), - URL(fileURLWithPath: "second.symbols.json"), - ] - ) - let bundles = try dataProvider.bundles(options: options) - XCTAssertEqual(bundles.count, 1) - let bundle = try XCTUnwrap(bundles.first) - - XCTAssertEqual(bundle.displayName, "Custom Display Name") - XCTAssertEqual(bundle.symbolGraphURLs.map { $0.lastPathComponent }.sorted(), [ - "first.symbols.json", - "second.symbols.json", - ]) - XCTAssertEqual(bundle.markupURLs.map { $0.path }.sorted(), [ - "FirstModuleName.md", - // No second file since that symbol graph has the same module name as the first - ]) - - XCTAssertEqual( - try String(data: dataProvider.contentsOfURL(URL(fileURLWithPath: "FirstModuleName.md")), encoding: .utf8), """ - # ``FirstModuleName`` - - @Metadata { - @DisplayName("Custom Display Name") - } - """ - ) - } -} diff --git a/Tests/SwiftDocCTests/Infrastructure/InheritIntroducedAvailabilityTests.swift b/Tests/SwiftDocCTests/Infrastructure/InheritIntroducedAvailabilityTests.swift index fdf3147144..1602d4dee3 100644 --- a/Tests/SwiftDocCTests/Infrastructure/InheritIntroducedAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/InheritIntroducedAvailabilityTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -34,16 +34,14 @@ class InheritIntroducedAvailabilityTests: XCTestCase { typealias Domain = SymbolGraph.Symbol.Availability.Domain typealias Version = SymbolGraph.SemanticVersion - var testBundle: DocumentationBundle! var context: DocumentationContext! - override func setUpWithError() throws { - try super.setUpWithError() - (testBundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + override func setUp() async throws { + try await super.setUp() + (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") } override func tearDown() { - testBundle = nil context = nil super.tearDown() } diff --git a/Tests/SwiftDocCTests/Infrastructure/Input Discovery/DocumentationInputsProviderTests.swift b/Tests/SwiftDocCTests/Infrastructure/Input Discovery/DocumentationInputsProviderTests.swift index 5778863dda..2d6edd7e57 100644 --- a/Tests/SwiftDocCTests/Infrastructure/Input Discovery/DocumentationInputsProviderTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/Input Discovery/DocumentationInputsProviderTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -14,9 +14,6 @@ import SwiftDocCTestUtilities class DocumentationInputsProviderTests: XCTestCase { - // After 6.2 we can update this test to verify that the input provider discovers the same inputs regardless of FileManagerProtocol - // Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. - @available(*, deprecated, message: "This test uses `LocalFileSystemDataProvider` as a `DocumentationWorkspaceDataProvider` which is deprecated and will be removed after 6.2 is released") func testDiscoversSameFilesAsPreviousImplementation() throws { let folderHierarchy = Folder(name: "one", content: [ Folder(name: "two", content: [ @@ -68,38 +65,26 @@ class DocumentationInputsProviderTests: XCTestCase { Folder(name: "OutsideSearchScope.docc", content: []), ]) + // Prepare the real on-disk file system let tempDirectory = try createTempFolder(content: [folderHierarchy]) - let realProvider = DocumentationContext.InputsProvider(fileManager: FileManager.default) - let testFileSystem = try TestFileSystem(folders: [folderHierarchy]) - let testProvider = DocumentationContext.InputsProvider(fileManager: testFileSystem) - - let options = BundleDiscoveryOptions(fallbackIdentifier: "com.example.test", additionalSymbolGraphFiles: [ - tempDirectory.appendingPathComponent("/path/to/SomethingAdditional.symbols.json") - ]) + // Prepare the test file system + let testFileSystem = try TestFileSystem(folders: []) + try testFileSystem.addFolder(folderHierarchy, basePath: tempDirectory) - let foundPrevImplBundle = try XCTUnwrap(LocalFileSystemDataProvider(rootURL: tempDirectory.appendingPathComponent("/one/two")).bundles(options: options).first) - let (foundRealBundle, _) = try XCTUnwrap(realProvider.inputsAndDataProvider(startingPoint: tempDirectory.appendingPathComponent("/one/two"), options: options)) - - let (foundTestBundle, _) = try XCTUnwrap(testProvider.inputsAndDataProvider(startingPoint: URL(fileURLWithPath: "/one/two"), options: .init( - infoPlistFallbacks: options.infoPlistFallbacks, - // The test file system has a default base URL and needs different URLs for the symbol graph files - additionalSymbolGraphFiles: [ - URL(fileURLWithPath: "/path/to/SomethingAdditional.symbols.json") + for fileManager in [FileManager.default as FileManagerProtocol, testFileSystem as FileManagerProtocol] { + let inputsProvider = DocumentationContext.InputsProvider(fileManager: fileManager) + let options = BundleDiscoveryOptions(fallbackIdentifier: "com.example.test", additionalSymbolGraphFiles: [ + tempDirectory.appendingPathComponent("/path/to/SomethingAdditional.symbols.json") ]) - )) - - for (bundle, relativeBase) in [ - (foundPrevImplBundle, tempDirectory.appendingPathComponent("/one/two/three")), - (foundRealBundle, tempDirectory.appendingPathComponent("/one/two/three")), - (foundTestBundle, URL(fileURLWithPath: "/one/two/three")), - ] { + let (bundle, _) = try XCTUnwrap(inputsProvider.inputsAndDataProvider(startingPoint: tempDirectory.appendingPathComponent("/one/two"), options: options)) + func relativePathString(_ url: URL) -> String { - url.relative(to: relativeBase)!.path + url.relative(to: tempDirectory.appendingPathComponent("/one/two/three"))!.path } XCTAssertEqual(bundle.displayName, "CustomDisplayName") - XCTAssertEqual(bundle.identifier, "com.example.test") + XCTAssertEqual(bundle.id, "com.example.test") XCTAssertEqual(bundle.markupURLs.map(relativePathString).sorted(), [ "Found.docc/CCC.md", "Found.docc/Inner/DDD.md", diff --git a/Tests/SwiftDocCTests/Infrastructure/NodeTagsTests.swift b/Tests/SwiftDocCTests/Infrastructure/NodeTagsTests.swift index a3598257b7..4a34e693ca 100644 --- a/Tests/SwiftDocCTests/Infrastructure/NodeTagsTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/NodeTagsTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -13,7 +13,7 @@ import XCTest import SwiftDocCTestUtilities class NodeTagsTests: XCTestCase { - func testSPIMetadata() throws { + func testSPIMetadata() async throws { let spiSGURL = Bundle.module.url( forResource: "SPI.symbols", withExtension: "json", subdirectory: "Test Resources")! @@ -24,27 +24,27 @@ class NodeTagsTests: XCTestCase { let tempURL = try createTemporaryDirectory().appendingPathComponent("unit-tests.docc") try bundleFolder.write(to: tempURL) - let (_, bundle, context) = try loadBundle(from: tempURL) + let (_, _, context) = try await loadBundle(from: tempURL) // Verify that `Test` is marked as SPI. - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Minimal_docs/Test", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/Minimal_docs/Test", sourceLanguage: .swift) let node = try XCTUnwrap(context.entity(with: reference)) let symbol = try XCTUnwrap(node.semantic as? Symbol) XCTAssertTrue(symbol.isSPI) // Verify the render node contains the SPI tag. - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) XCTAssertEqual(renderNode.metadata.tags, [.spi]) // Verify that the link to the node contains the SPI tag. - let moduleReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Minimal_docs", sourceLanguage: .swift) + let moduleReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/Minimal_docs", sourceLanguage: .swift) let moduleNode = try XCTUnwrap(context.entity(with: moduleReference)) let moduleSymbol = try XCTUnwrap(moduleNode.semantic as? Symbol) // Verify the render node contains the SPI tag. - var moduleTranslator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var moduleTranslator = RenderNodeTranslator(context: context, identifier: node.reference) let moduleRenderNode = try XCTUnwrap(moduleTranslator.visit(moduleSymbol) as? RenderNode) let linkReference = try XCTUnwrap(moduleRenderNode.references["doc://com.tests.spi/documentation/Minimal_docs/Test"] as? TopicRenderReference) diff --git a/Tests/SwiftDocCTests/Infrastructure/NodeURLGeneratorTests.swift b/Tests/SwiftDocCTests/Infrastructure/NodeURLGeneratorTests.swift index 422f2e2859..55da5f9f3b 100644 --- a/Tests/SwiftDocCTests/Infrastructure/NodeURLGeneratorTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/NodeURLGeneratorTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -14,8 +14,6 @@ import XCTest @testable import SwiftDocC class NodeURLGeneratorTests: XCTestCase { - let generator = NodeURLGenerator() - let unchangedURLs = [ URL(string: "doc://com.bundle/folder-prefix/type/symbol")!, URL(string: "doc://com.bundle/fol.der-pref.ix./type-swift.class/symbol.name.")!, diff --git a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyBasedLinkResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyBasedLinkResolverTests.swift index b546fe6160..4c82a540a7 100644 --- a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyBasedLinkResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyBasedLinkResolverTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -13,10 +13,10 @@ import XCTest class PathHierarchyBasedLinkResolverTests: XCTestCase { - func testOverloadedSymbolsWithOverloadGroups() throws { + func testOverloadedSymbolsWithOverloadGroups() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let (_, context) = try testBundleAndContext(named: "OverloadedSymbols") + let (_, context) = try await testBundleAndContext(named: "OverloadedSymbols") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) // Returns nil for all non-overload groups diff --git a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift index 47c61b9702..661198700a 100644 --- a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift @@ -16,8 +16,8 @@ import Markdown class PathHierarchyTests: XCTestCase { - func testFindingUnambiguousAbsolutePaths() throws { - let (_, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testFindingUnambiguousAbsolutePaths() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/MixedFramework", in: tree, asSymbolID: "MixedFramework") @@ -286,10 +286,10 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("/MixedFramework/MyTypedObjectiveCExtensibleEnumSecond", in: tree, asSymbolID: "c:@MyTypedObjectiveCExtensibleEnumSecond") } - func testAmbiguousPaths() throws { + func testAmbiguousPaths() async throws { enableFeatureFlag(\.isExperimentalLinkHierarchySerializationEnabled) - let (_, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let tree = context.linkResolver.localResolver.pathHierarchy // Symbol name not found. Suggestions only include module names (search is not relative to a known page) @@ -574,8 +574,8 @@ class PathHierarchyTests: XCTestCase { try assertPathNotFound("MixedFramework/MyTypedObjectiveCExtensibleEnum-typealias/second", in: tree) } - func testRedundantKindDisambiguation() throws { - let (_, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testRedundantKindDisambiguation() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/MixedFramework-module", in: tree, asSymbolID: "MixedFramework") @@ -626,8 +626,8 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("/MixedFramework/myTopLevelVariable-var", in: tree, asSymbolID: "s:14MixedFramework18myTopLevelVariableSbvp") } - func testBothRedundantDisambiguations() throws { - let (_, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testBothRedundantDisambiguations() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/MixedFramework-module-9r7pl", in: tree, asSymbolID: "MixedFramework") @@ -678,7 +678,7 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("/MixedFramework-module-9r7pl/myTopLevelVariable-var-520ez", in: tree, asSymbolID: "s:14MixedFramework18myTopLevelVariableSbvp") } - func testDefaultImplementationWithCollidingTargetSymbol() throws { + func testDefaultImplementationWithCollidingTargetSymbol() async throws { // ---- Inner // public protocol Something { @@ -691,7 +691,7 @@ class PathHierarchyTests: XCTestCase { // ---- Outer // @_exported import Inner // public typealias Something = Inner.Something - let (_, context) = try testBundleAndContext(named: "DefaultImplementationsWithExportedImport") + let (_, context) = try await testBundleAndContext(named: "DefaultImplementationsWithExportedImport") let tree = context.linkResolver.localResolver.pathHierarchy // The @_export imported protocol can be found @@ -712,8 +712,8 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("DefaultImplementationsWithExportedImport/Something-protocol/doSomething()-scj9", in: tree, asSymbolID: "s:5Inner9SomethingPAAE02doB0yyF") } - func testDisambiguatedPaths() throws { - let (_, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testDisambiguatedPaths() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -849,8 +849,8 @@ class PathHierarchyTests: XCTestCase { "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-757cj") } - func testDisambiguatedOperatorPaths() throws { - let (_, context) = try testBundleAndContext(named: "InheritedOperators") + func testDisambiguatedOperatorPaths() async throws { + let (_, context) = try await testBundleAndContext(named: "InheritedOperators") let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -919,8 +919,8 @@ class PathHierarchyTests: XCTestCase { } - func testFindingRelativePaths() throws { - let (_, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testFindingRelativePaths() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let tree = context.linkResolver.localResolver.pathHierarchy let moduleID = try tree.find(path: "/MixedFramework", onlyFindSymbols: true) @@ -1091,8 +1091,8 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(try tree.findSymbol(path: "second", parent: myTypedExtensibleEnumID).identifier.precise, "c:@MyTypedObjectiveCExtensibleEnumSecond") } - func testPathWithDocumentationPrefix() throws { - let (_, context) = try testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") + func testPathWithDocumentationPrefix() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFrameworkWithLanguageRefinements") let tree = context.linkResolver.localResolver.pathHierarchy let moduleID = try tree.find(path: "/MixedFramework", onlyFindSymbols: true) @@ -1106,8 +1106,8 @@ class PathHierarchyTests: XCTestCase { assertParsedPathComponents("/documentation/MixedFramework/MyEnum", [("documentation", nil), ("MixedFramework", nil), ("MyEnum", nil)]) } - func testUnrealisticMixedTestCatalog() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testUnrealisticMixedTestCatalog() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let linkResolver = try XCTUnwrap(context.linkResolver.localResolver) let tree = try XCTUnwrap(linkResolver.pathHierarchy) @@ -1172,8 +1172,8 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(tree.lookup[symbolPageTaskGroupID]!.name, "Task-Group-Exercising-Symbol-Links") } - func testMixedLanguageFramework() throws { - let (_, context) = try testBundleAndContext(named: "MixedLanguageFramework") + func testMixedLanguageFramework() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFramework") let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("MixedLanguageFramework/Bar/myStringFunction(_:)", in: tree, asSymbolID: "c:objc(cs)Bar(cm)myStringFunction:error:") @@ -1226,8 +1226,8 @@ class PathHierarchyTests: XCTestCase { "/MixedLanguageFramework/SwiftOnlyStruct/tada()") } - func testArticleAndSymbolCollisions() throws { - let (_, _, context) = try testBundleAndContext(copying: "MixedLanguageFramework") { url in + func testArticleAndSymbolCollisions() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "MixedLanguageFramework") { url in try """ # An article @@ -1239,12 +1239,15 @@ class PathHierarchyTests: XCTestCase { // The added article above has the same path as an existing symbol in the this module. let symbolNode = try tree.findNode(path: "/MixedLanguageFramework/Bar", onlyFindSymbols: true) XCTAssertNotNil(symbolNode.symbol, "Symbol link finds the symbol") + let articleNode = try tree.findNode(path: "/MixedLanguageFramework/Bar", onlyFindSymbols: false) - XCTAssertNil(articleNode.symbol, "General documentation link find the article") + XCTAssertNotNil(articleNode.symbol, "This should be an article but can't be because of rdar://79745455") + // FIXME: Verify that article matches are preferred for general (non-symbol) links once https://github.com/swiftlang/swift-docc/issues/593 is fixed +// XCTAssertNil(articleNode.symbol, "General documentation link find the article") } - func testArticleSelfAnchorLinks() throws { - let (_, _, context) = try testBundleAndContext(copying: "MixedLanguageFramework") { url in + func testArticleSelfAnchorLinks() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "MixedLanguageFramework") { url in try """ # ArticleWithHeading @@ -1266,8 +1269,8 @@ class PathHierarchyTests: XCTestCase { XCTAssertNotNil(anchorLinkNode) } - func testOverloadedSymbols() throws { - let (_, context) = try testBundleAndContext(named: "OverloadedSymbols") + func testOverloadedSymbols() async throws { + let (_, context) = try await testBundleAndContext(named: "OverloadedSymbols") let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -1328,10 +1331,10 @@ class PathHierarchyTests: XCTestCase { ]) } - func testOverloadedSymbolsWithOverloadGroups() throws { + func testOverloadedSymbolsWithOverloadGroups() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let (_, context) = try testBundleAndContext(named: "OverloadedSymbols") + let (_, context) = try await testBundleAndContext(named: "OverloadedSymbols") let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -1931,7 +1934,7 @@ class PathHierarchyTests: XCTestCase { } } - func testParameterDisambiguationWithAnyType() throws { + func testParameterDisambiguationWithAnyType() async throws { // Create two overloads with different parameter types let parameterTypes: [SymbolGraph.Symbol.DeclarationFragments.Fragment] = [ // Any (swift) @@ -1956,7 +1959,7 @@ class PathHierarchyTests: XCTestCase { )) })), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy XCTAssert(context.problems.isEmpty, "Unexpected problems \(context.problems.map(\.diagnostic.summary))") @@ -1988,7 +1991,7 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("doSomething(with:)-9kd0v", in: tree, asSymbolID: "some-function-id-AnyObject") } - func testReturnDisambiguationWithAnyType() throws { + func testReturnDisambiguationWithAnyType() async throws { // Create two overloads with different return types let returnTypes: [SymbolGraph.Symbol.DeclarationFragments.Fragment] = [ // Any (swift) @@ -2005,7 +2008,7 @@ class PathHierarchyTests: XCTestCase { )) })), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy XCTAssert(context.problems.isEmpty, "Unexpected problems \(context.problems.map(\.diagnostic.summary))") @@ -2037,10 +2040,75 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("doSomething()-9kd0v", in: tree, asSymbolID: "some-function-id-AnyObject") } - func testOverloadGroupSymbolsResolveLinksWithoutHash() throws { + func testParameterDisambiguationWithKeyPathType() async throws { + // Create two overloads with different key path parameter types + let parameterTypes: [SymbolGraph.Symbol.DeclarationFragments.Fragment] = [ + // Swift.Int + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + // Swift.Bool + .init(kind: .typeIdentifier, spelling: "Bool", preciseIdentifier: "s:Sb"), + ] + + let catalog = Folder(name: "CatalogName.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: parameterTypes.map { parameterTypeFragment in + makeSymbol(id: "some-function-id-\(parameterTypeFragment.spelling)-KeyPath", kind: .func, pathComponents: ["doSomething(keyPath:)"], signature: .init( + parameters: [ + // "keyPath: KeyPath" or "keyPath: KeyPath" + .init(name: "keyPath", externalName: nil, declarationFragments: [ + .init(kind: .identifier, spelling: "keyPath", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "KeyPath", preciseIdentifier: "s:s7KeyPathC"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"), + .init(kind: .text, spelling: ", ", preciseIdentifier: nil), + parameterTypeFragment, + .init(kind: .text, spelling: ">", preciseIdentifier: nil) + ], children: []) + ], + returns: [ + .init(kind: .text, spelling: "()", preciseIdentifier: nil) // 'Void' in text representation + ] + )) + })), + ]) + let (_, context) = try await loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + XCTAssert(context.problems.isEmpty, "Unexpected problems \(context.problems.map(\.diagnostic.summary))") + + let paths = tree.caseInsensitiveDisambiguatedPaths() + + XCTAssertEqual(paths["some-function-id-Int-KeyPath"], "/ModuleName/doSomething(keyPath:)-(KeyPath)") + XCTAssertEqual(paths["some-function-id-Bool-KeyPath"], "/ModuleName/doSomething(keyPath:)-(KeyPath)") + + try assertPathCollision("doSomething(keyPath:)", in: tree, collisions: [ + ("some-function-id-Int-KeyPath", "-(KeyPath)"), + ("some-function-id-Bool-KeyPath", "-(KeyPath)"), + ]) + + try assertPathRaisesErrorMessage("doSomething(keyPath:)", in: tree, context: context, expectedErrorMessage: "'doSomething(keyPath:)' is ambiguous at '/ModuleName'") { error in + XCTAssertEqual(error.solutions.count, 2) + + // These test symbols don't have full declarations. A real solution would display enough information to distinguish these. + XCTAssertEqual(error.solutions.dropFirst(0).first, .init(summary: "Insert '-(KeyPath)' for \n'doSomething(keyPath:)'" , replacements: [("-(KeyPath)", 21, 21)])) + XCTAssertEqual(error.solutions.dropFirst(1).first, .init(summary: "Insert '-(KeyPath)' for \n'doSomething(keyPath:)'" /* the test symbols don't have full declarations */, replacements: [("-(KeyPath)", 21, 21)])) + } + + assertParsedPathComponents("doSomething(keyPath:)-(KeyPath)", [("doSomething(keyPath:)", .typeSignature(parameterTypes: ["KeyPath"], returnTypes: nil))]) + try assertFindsPath("doSomething(keyPath:)-(KeyPath)", in: tree, asSymbolID: "some-function-id-Int-KeyPath") + try assertFindsPath("doSomething(keyPath:)-(KeyPath)->()", in: tree, asSymbolID: "some-function-id-Int-KeyPath") + try assertFindsPath("doSomething(keyPath:)-2zg7h", in: tree, asSymbolID: "some-function-id-Int-KeyPath") + + assertParsedPathComponents("doSomething(keyPath:)-(KeyPath)", [("doSomething(keyPath:)", .typeSignature(parameterTypes: ["KeyPath"], returnTypes: nil))]) + try assertFindsPath("doSomething(keyPath:)-(KeyPath)", in: tree, asSymbolID: "some-function-id-Bool-KeyPath") + try assertFindsPath("doSomething(keyPath:)-(KeyPath)->()", in: tree, asSymbolID: "some-function-id-Bool-KeyPath") + try assertFindsPath("doSomething(keyPath:)-2frrn", in: tree, asSymbolID: "some-function-id-Bool-KeyPath") + } + + func testOverloadGroupSymbolsResolveLinksWithoutHash() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let (_, context) = try testBundleAndContext(named: "OverloadedSymbols") + let (_, context) = try await testBundleAndContext(named: "OverloadedSymbols") let tree = context.linkResolver.localResolver.pathHierarchy // The enum case should continue to resolve by kind, since it has no hash collision @@ -2056,9 +2124,9 @@ class PathHierarchyTests: XCTestCase { } - func testAmbiguousPathsForOverloadedGroupSymbols() throws { + func testAmbiguousPathsForOverloadedGroupSymbols() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let (_, context) = try testBundleAndContext(named: "OverloadedSymbols") + let (_, context) = try await testBundleAndContext(named: "OverloadedSymbols") let tree = context.linkResolver.localResolver.pathHierarchy try assertPathRaisesErrorMessage("/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-abc123", in: tree, context: context, expectedErrorMessage: """ 'abc123' isn't a disambiguation for 'fourthTestMemberName(test:)' at '/ShapeKit/OverloadedProtocol' @@ -2075,7 +2143,7 @@ class PathHierarchyTests: XCTestCase { } } - func testDoesNotSuggestBundleNameForSymbolLink() throws { + func testDoesNotSuggestBundleNameForSymbolLink() async throws { let exampleDocumentation = Folder(name: "Something.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName")), @@ -2089,7 +2157,7 @@ class PathHierarchyTests: XCTestCase { """), ]) let catalogURL = try exampleDocumentation.write(inside: createTemporaryDirectory()) - let (_, _, context) = try loadBundle(from: catalogURL) + let (_, _, context) = try await loadBundle(from: catalogURL) let tree = context.linkResolver.localResolver.pathHierarchy // This link is intentionally misspelled @@ -2101,8 +2169,8 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(linkProblem.possibleSolutions.map(\.summary), ["Replace 'ModuleNaem' with 'ModuleName'"]) } - func testSymbolsWithSameNameAsModule() throws { - let (_, context) = try testBundleAndContext(named: "SymbolsWithSameNameAsModule") + func testSymbolsWithSameNameAsModule() async throws { + let (_, context) = try await testBundleAndContext(named: "SymbolsWithSameNameAsModule") let tree = context.linkResolver.localResolver.pathHierarchy // /* in a module named "Something "*/ @@ -2148,7 +2216,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(try tree.findSymbol(path: "Something/second", parent: topLevelSymbolID).identifier.precise, "s:9SomethingAAV6secondSivp") } - func testSymbolsWithSameNameAsExtendedModule() throws { + func testSymbolsWithSameNameAsExtendedModule() async throws { // ---- Inner // public struct InnerStruct {} // public class InnerClass {} @@ -2163,7 +2231,7 @@ class PathHierarchyTests: XCTestCase { // public extension InnerClass { // func something() {} // } - let (_, context) = try testBundleAndContext(named: "ShadowExtendedModuleWithLocalSymbol") + let (_, context) = try await testBundleAndContext(named: "ShadowExtendedModuleWithLocalSymbol") let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("Outer/Inner", in: tree, collisions: [ @@ -2191,7 +2259,7 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("Inner/InnerClass/something()", in: tree, asSymbolID: "s:5Inner0A5ClassC5OuterE9somethingyyF") } - func testExtensionSymbolsWithSameNameAsExtendedModule() throws { + func testExtensionSymbolsWithSameNameAsExtendedModule() async throws { // ---- ExtendedModule // public struct SomeStruct { // public struct SomeNestedStruct {} @@ -2238,7 +2306,7 @@ class PathHierarchyTests: XCTestCase { ), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -2263,7 +2331,7 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("ExtendedModule/SomeStruct/SomeNestedStruct/doSomething()", in: tree, asSymbolID: extendedMethodSymbolID) } - func testContinuesSearchingIfNonSymbolMatchesSymbolLink() throws { + func testContinuesSearchingIfNonSymbolMatchesSymbolLink() async throws { let exampleDocumentation = Folder(name: "CatalogName.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ makeSymbol(id: "some-class-id", kind: .class, pathComponents: ["SomeClass"]) @@ -2282,7 +2350,7 @@ class PathHierarchyTests: XCTestCase { """), ]) let catalogURL = try exampleDocumentation.write(inside: createTemporaryDirectory()) - let (_, _, context) = try loadBundle(from: catalogURL) + let (_, _, context) = try await loadBundle(from: catalogURL) let tree = context.linkResolver.localResolver.pathHierarchy XCTAssert(context.problems.isEmpty, "Unexpected problems \(context.problems.map(\.diagnostic.summary))") @@ -2306,7 +2374,7 @@ class PathHierarchyTests: XCTestCase { } } - func testDiagnosticDoesNotSuggestReplacingPartOfSymbolName() throws { + func testDiagnosticDoesNotSuggestReplacingPartOfSymbolName() async throws { let exampleDocumentation = Folder(name: "CatalogName.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ makeSymbol(id: "some-class-id-1", kind: .class, pathComponents: ["SomeClass-(Something)"]), @@ -2314,7 +2382,7 @@ class PathHierarchyTests: XCTestCase { ])), ]) let catalogURL = try exampleDocumentation.write(inside: createTemporaryDirectory()) - let (_, _, context) = try loadBundle(from: catalogURL) + let (_, _, context) = try await loadBundle(from: catalogURL) let tree = context.linkResolver.localResolver.pathHierarchy XCTAssert(context.problems.isEmpty, "Unexpected problems \(context.problems.map(\.diagnostic.summary))") @@ -2337,33 +2405,8 @@ class PathHierarchyTests: XCTestCase { } } - func testSnippets() throws { - let (_, context) = try testBundleAndContext(named: "Snippets") - let tree = context.linkResolver.localResolver.pathHierarchy - - try assertFindsPath("/Snippets/Snippets/MySnippet", in: tree, asSymbolID: "$snippet__Test.Snippets.MySnippet") - - let paths = tree.caseInsensitiveDisambiguatedPaths() - XCTAssertEqual(paths["$snippet__Test.Snippets.MySnippet"], - "/Snippets/Snippets/MySnippet") - - // Test relative links from the article that overlap with the snippet's path - let snippetsArticleID = try tree.find(path: "/Snippets/Snippets", onlyFindSymbols: false) - XCTAssertEqual(try tree.findSymbol(path: "MySnippet", parent: snippetsArticleID).identifier.precise, "$snippet__Test.Snippets.MySnippet") - XCTAssertEqual(try tree.findSymbol(path: "Snippets/MySnippet", parent: snippetsArticleID).identifier.precise, "$snippet__Test.Snippets.MySnippet") - XCTAssertEqual(try tree.findSymbol(path: "Snippets/Snippets/MySnippet", parent: snippetsArticleID).identifier.precise, "$snippet__Test.Snippets.MySnippet") - XCTAssertEqual(try tree.findSymbol(path: "/Snippets/Snippets/MySnippet", parent: snippetsArticleID).identifier.precise, "$snippet__Test.Snippets.MySnippet") - - // Test relative links from another article (which doesn't overlap with the snippet's path) - let sliceArticleID = try tree.find(path: "/Snippets/SliceIndentation", onlyFindSymbols: false) - XCTAssertThrowsError(try tree.findSymbol(path: "MySnippet", parent: sliceArticleID)) - XCTAssertEqual(try tree.findSymbol(path: "Snippets/MySnippet", parent: sliceArticleID).identifier.precise, "$snippet__Test.Snippets.MySnippet") - XCTAssertEqual(try tree.findSymbol(path: "Snippets/Snippets/MySnippet", parent: sliceArticleID).identifier.precise, "$snippet__Test.Snippets.MySnippet") - XCTAssertEqual(try tree.findSymbol(path: "/Snippets/Snippets/MySnippet", parent: sliceArticleID).identifier.precise, "$snippet__Test.Snippets.MySnippet") - } - - func testInheritedOperators() throws { - let (_, context) = try testBundleAndContext(named: "InheritedOperators") + func testInheritedOperators() async throws { + let (_, context) = try await testBundleAndContext(named: "InheritedOperators") let tree = context.linkResolver.localResolver.pathHierarchy // public struct MyNumber: SignedNumeric, Comparable, Equatable, Hashable { @@ -2464,8 +2507,8 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(repeatedHumanReadablePaths.keys.sorted(), [], "Every path should be unique") } - func testSameNameForSymbolAndContainer() throws { - let (_, context) = try testBundleAndContext(named: "BundleWithSameNameForSymbolAndContainer") + func testSameNameForSymbolAndContainer() async throws { + let (_, context) = try await testBundleAndContext(named: "BundleWithSameNameForSymbolAndContainer") let tree = context.linkResolver.localResolver.pathHierarchy // public struct Something { @@ -2498,8 +2541,8 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(try tree.findSymbol(path: "Something/SomethingElse", parent: moduleID).absolutePath, "Something/SomethingElse") } - func testPrefersNonSymbolsWhenOnlyFindSymbolIsFalse() throws { - let (_, _, context) = try testBundleAndContext(copying: "SymbolsWithSameNameAsModule") { url in + func testPrefersNonSymbolsWhenOnlyFindSymbolIsFalse() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "SymbolsWithSameNameAsModule") { url in // This bundle has a top-level struct named "Wrapper". Adding an article named "Wrapper.md" introduces a possibility for a link collision try """ # An article @@ -2516,13 +2559,17 @@ class PathHierarchyTests: XCTestCase { // Links to non-symbols can use only the file name, without specifying the module or catalog name. let articleID = try tree.find(path: "Wrapper", onlyFindSymbols: false) let articleMatch = try XCTUnwrap(tree.lookup[articleID]) - XCTAssertNil(articleMatch.symbol, "Should have found the article") + XCTAssertNotNil(articleMatch.symbol, "This should be an article but can't be because of rdar://79745455") + // FIXME: Verify that article matches are preferred for general (non-symbol) links once rdar://79745455 https://github.com/swiftlang/swift-docc/issues/593 is fixed +// XCTAssertNil(articleMatch.symbol, "Should have found the article") } do { // Links to non-symbols can also use module-relative links. let articleID = try tree.find(path: "/Something/Wrapper", onlyFindSymbols: false) let articleMatch = try XCTUnwrap(tree.lookup[articleID]) - XCTAssertNil(articleMatch.symbol, "Should have found the article") + XCTAssertNotNil(articleMatch.symbol, "This should be an article but can't be because of rdar://79745455") + // FIXME: Verify that article matches are preferred for general (non-symbol) links once rdar://79745455 https://github.com/swiftlang/swift-docc/issues/593 is fixed +// XCTAssertNil(articleMatch.symbol, "Should have found the article") } // Symbols can only use absolute links or be found relative to another page. let symbolID = try tree.find(path: "/Something/Wrapper", onlyFindSymbols: true) @@ -2530,7 +2577,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertNotNil(symbolMatch.symbol, "Should have found the struct") } - func testOneSymbolPathsWithKnownDisambiguation() throws { + func testOneSymbolPathsWithKnownDisambiguation() async throws { let exampleDocumentation = Folder(name: "MyKit.docc", content: [ CopyOfFile(original: Bundle.module.url(forResource: "mykit-one-symbol.symbols", withExtension: "json", subdirectory: "Test Resources")!), InfoPlist(displayName: "MyKit", identifier: "com.test.MyKit"), @@ -2539,7 +2586,7 @@ class PathHierarchyTests: XCTestCase { let bundleURL = try exampleDocumentation.write(inside: tempURL) do { - let (_, _, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/MyKit/MyClass/myFunction()", in: tree, asSymbolID: "s:5MyKit0A5ClassC10myFunctionyyF") @@ -2558,7 +2605,7 @@ class PathHierarchyTests: XCTestCase { configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents = [ "s:5MyKit0A5ClassC10myFunctionyyF": ["MyClass-swift.class", "myFunction()"] ] - let (_, _, context) = try loadBundle(from: bundleURL, configuration: configuration) + let (_, _, context) = try await loadBundle(from: bundleURL, configuration: configuration) let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/MyKit/MyClass-swift.class/myFunction()", in: tree, asSymbolID: "s:5MyKit0A5ClassC10myFunctionyyF") @@ -2577,7 +2624,7 @@ class PathHierarchyTests: XCTestCase { configuration.convertServiceConfiguration.knownDisambiguatedSymbolPathComponents = [ "s:5MyKit0A5ClassC10myFunctionyyF": ["MyClass-swift.class-hash", "myFunction()"] ] - let (_, _, context) = try loadBundle(from: bundleURL, configuration: configuration) + let (_, _, context) = try await loadBundle(from: bundleURL, configuration: configuration) let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/MyKit/MyClass-swift.class-hash/myFunction()", in: tree, asSymbolID: "s:5MyKit0A5ClassC10myFunctionyyF") @@ -2593,7 +2640,7 @@ class PathHierarchyTests: XCTestCase { } } - func testArticleWithDisambiguationLookingName() throws { + func testArticleWithDisambiguationLookingName() async throws { let exampleDocumentation = Folder(name: "MyKit.docc", content: [ CopyOfFile(original: Bundle.module.url(forResource: "BaseKit.symbols", withExtension: "json", subdirectory: "Test Resources")!), InfoPlist(displayName: "BaseKit", identifier: "com.test.BaseKit"), @@ -2617,7 +2664,7 @@ class PathHierarchyTests: XCTestCase { let bundleURL = try exampleDocumentation.write(inside: tempURL) do { - let (_, _, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map { DiagnosticConsoleWriter.formattedDescription(for: $0) })") let tree = context.linkResolver.localResolver.pathHierarchy @@ -2633,8 +2680,8 @@ class PathHierarchyTests: XCTestCase { } } - func testGeometricalShapes() throws { - let (_, context) = try testBundleAndContext(named: "GeometricalShapes") + func testGeometricalShapes() async throws { + let (_, context) = try await testBundleAndContext(named: "GeometricalShapes") let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths().values.sorted() @@ -2657,7 +2704,7 @@ class PathHierarchyTests: XCTestCase { ]) } - func testPartialSymbolGraphPaths() throws { + func testPartialSymbolGraphPaths() async throws { let symbolPaths = [ ["A", "B", "C"], ["A", "B", "C2"], @@ -2675,7 +2722,7 @@ class PathHierarchyTests: XCTestCase { let tempURL = try createTemporaryDirectory() let bundleURL = try exampleDocumentation.write(inside: tempURL) - let (_, _, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathNotFound("/Module/A", in: tree) @@ -2697,7 +2744,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(paths["X.Y2.Z.W"], "/Module/X/Y2/Z/W") } - func testMixedLanguageSymbolWithSameKindAndAddedMemberFromExtendingModule() throws { + func testMixedLanguageSymbolWithSameKindAndAddedMemberFromExtendingModule() async throws { let containerID = "some-container-symbol-id" let memberID = "some-member-symbol-id" @@ -2731,7 +2778,7 @@ class PathHierarchyTests: XCTestCase { ]) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -2739,7 +2786,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName/MemberName") } - func testMixedLanguageSymbolWithDifferentKindsAndAddedMemberFromExtendingModule() throws { + func testMixedLanguageSymbolWithDifferentKindsAndAddedMemberFromExtendingModule() async throws { let containerID = "some-container-symbol-id" let memberID = "some-member-symbol-id" @@ -2773,7 +2820,7 @@ class PathHierarchyTests: XCTestCase { ]) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -2781,7 +2828,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName/MemberName") } - func testLanguageRepresentationsWithDifferentCapitalization() throws { + func testLanguageRepresentationsWithDifferentCapitalization() async throws { let containerID = "some-container-symbol-id" let memberID = "some-member-symbol-id" @@ -2813,7 +2860,7 @@ class PathHierarchyTests: XCTestCase { ]) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -2821,7 +2868,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName/memberName") // The Swift spelling is preferred } - func testLanguageRepresentationsWithDifferentParentKinds() throws { + func testLanguageRepresentationsWithDifferentParentKinds() async throws { enableFeatureFlag(\.isExperimentalLinkHierarchySerializationEnabled) let containerID = "some-container-symbol-id" @@ -2861,7 +2908,7 @@ class PathHierarchyTests: XCTestCase { }) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let resolvedSwiftContainerID = try tree.find(path: "/ModuleName/ContainerName-struct", onlyFindSymbols: true) @@ -2905,7 +2952,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName/MemberName") } - func testMixedLanguageSymbolAndItsExtendingModuleWithDifferentContainerNames() throws { + func testMixedLanguageSymbolAndItsExtendingModuleWithDifferentContainerNames() async throws { let containerID = "some-container-symbol-id" let memberID = "some-member-symbol-id" @@ -2939,7 +2986,7 @@ class PathHierarchyTests: XCTestCase { ]) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -2947,7 +2994,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(paths[memberID], "/ModuleName/SwiftContainerName/MemberName") } - func testOptionalMemberUnderCorrectContainer() throws { + func testOptionalMemberUnderCorrectContainer() async throws { let containerID = "some-container-symbol-id" let otherID = "some-other-symbol-id" let memberID = "some-member-symbol-id" @@ -2966,7 +3013,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths(includeDisambiguationForUnambiguousChildren: true) @@ -2975,7 +3022,7 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(paths[memberID], "/ModuleName/ContainerName-qwwf/MemberName1") } - func testLinkToTopicSection() throws { + func testLinkToTopicSection() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( moduleName: "ModuleName", @@ -3020,7 +3067,7 @@ class PathHierarchyTests: XCTestCase { """) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let moduleID = try tree.find(path: "/ModuleName", onlyFindSymbols: true) @@ -3071,7 +3118,7 @@ class PathHierarchyTests: XCTestCase { ], "The hierarchy only computes paths for symbols, not for headings or topic sections") } - func testModuleAndCollidingTechnologyRootHasPathsForItsSymbols() throws { + func testModuleAndCollidingTechnologyRootHasPathsForItsSymbols() async throws { let symbolID = "some-symbol-id" let catalog = Folder(name: "unit-test.docc", content: [ @@ -3094,14 +3141,14 @@ class PathHierarchyTests: XCTestCase { """) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths(includeDisambiguationForUnambiguousChildren: true) XCTAssertEqual(paths[symbolID], "/ModuleName/SymbolName") } - func testSameDefaultImplementationOnMultiplePlatforms() throws { + func testSameDefaultImplementationOnMultiplePlatforms() async throws { let protocolID = "some-protocol-symbol-id" let protocolRequirementID = "some-protocol-requirement-symbol-id" let defaultImplementationID = "some-default-implementation-symbol-id" @@ -3127,7 +3174,7 @@ class PathHierarchyTests: XCTestCase { makeSymbolGraphFile(platformName: "PlatformTwo"), ]) - let (_, context) = try loadBundle(catalog: multiPlatformCatalog) + let (_, context) = try await loadBundle(catalog: multiPlatformCatalog) let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -3138,21 +3185,105 @@ class PathHierarchyTests: XCTestCase { let singlePlatformCatalog = Folder(name: "unit-test.docc", content: [ makeSymbolGraphFile(platformName: "PlatformOne"), ]) - let (_, singlePlatformContext) = try loadBundle(catalog: singlePlatformCatalog) + let (_, singlePlatformContext) = try await loadBundle(catalog: singlePlatformCatalog) let singlePlatformPaths = singlePlatformContext.linkResolver.localResolver.pathHierarchy.caseInsensitiveDisambiguatedPaths() XCTAssertEqual(paths[protocolRequirementID], singlePlatformPaths[protocolRequirementID]) XCTAssertEqual(paths[defaultImplementationID], singlePlatformPaths[defaultImplementationID]) } - func testMultiPlatformModuleWithExtension() throws { - let (_, context) = try testBundleAndContext(named: "MultiPlatformModuleWithExtension") + func testMultiPlatformModuleWithExtension() async throws { + let (_, context) = try await testBundleAndContext(named: "MultiPlatformModuleWithExtension") let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/MainModule/TopLevelProtocol/extensionMember(_:)", in: tree, asSymbolID: "extensionMember1") try assertFindsPath("/MainModule/TopLevelProtocol/InnerStruct/extensionMember(_:)", in: tree, asSymbolID: "extensionMember2") } - - func testMissingRequiredMemberOfSymbolGraphRelationshipInOneLanguageAcrossManyPlatforms() throws { + + func testAbsoluteLinksToOtherModuleWithExtensions() async throws { + enableFeatureFlag(\.isExperimentalLinkHierarchySerializationEnabled) + + let extendedTypeID = "extended-type-id" + let extensionID = "extension-id" + let extensionMethodID = "extension-method-id" + + let extensionMixin = SymbolGraph.Symbol.Swift.Extension( + extendedModule: "ExtendedModule", + typeKind: .struct, + constraints: [] + ) + + let catalog = Folder(name: "TestCatalog.docc", content: [ + JSONFile(name: "MainModule.symbols.json", content: makeSymbolGraph(moduleName: "MainModule", symbols: [])), + JSONFile(name: "MainModule@ExtendedModule.symbols.json", content: makeSymbolGraph( + moduleName: "MainModule", + symbols: [ + makeSymbol( + id: extensionID, + kind: .extension, + pathComponents: ["ExtendedType"], + otherMixins: [extensionMixin] + ), + makeSymbol( + id: extensionMethodID, + kind: .method, + pathComponents: ["ExtendedType", "extensionMethod()"], + otherMixins: [extensionMixin] + ) + ], + relationships: [ + .init( + source: extensionMethodID, + target: extensionID, + kind: .memberOf, + targetFallback: "ExtendedModule.ExtendedType" + ), + .init( + source: extensionID, + target: extendedTypeID, + kind: .extensionTo, + targetFallback: "ExtendedModule.ExtendedType" + ) + ] + )) + ]) + + let (_, context) = try await loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertFindsPath( + "/MainModule/ExtendedModule/ExtendedType/extensionMethod()", + in: tree, + asSymbolID: extensionMethodID + ) + + try assertFindsPath( + "ExtendedModule/ExtendedType", + in: tree, + asSymbolID: extensionID + ) + try assertFindsPath( + "ExtendedModule/ExtendedType/extensionMethod()", + in: tree, + asSymbolID: extensionMethodID + ) + + // Verify that a link that resolves relative to the module + // fails to resolve as an absolute link, with a moduleNotFound error. + try assertPathRaisesErrorMessage( + "/ExtendedModule/ExtendedType", + in: tree, + context: context, + expectedErrorMessage: "No module named 'ExtendedModule'" + ) + try assertPathRaisesErrorMessage( + "/ExtendedModule/ExtendedType/extensionMethod()", + in: tree, + context: context, + expectedErrorMessage: "No module named 'ExtendedModule'" + ) + } + + func testMissingRequiredMemberOfSymbolGraphRelationshipInOneLanguageAcrossManyPlatforms() async throws { // We make a best-effort attempt to create a valid path hierarchy, even if the symbol graph inputs are not valid. // If the symbol graph files define container and member symbols without the required memberOf relationships we still try to match them up. @@ -3179,7 +3310,7 @@ class PathHierarchyTests: XCTestCase { }) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let container = try tree.findNode(path: "/ModuleName/ContainerName-struct", onlyFindSymbols: true) @@ -3198,7 +3329,7 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("/ModuleName/ContainerName", in: tree, asSymbolID: containerID) } - func testInvalidSymbolGraphWithNoMemberOfRelationshipsDesptiteDeepHierarchyAcrossManyPlatforms() throws { + func testInvalidSymbolGraphWithNoMemberOfRelationshipsDesptiteDeepHierarchyAcrossManyPlatforms() async throws { // We make a best-effort attempt to create a valid path hierarchy, even if the symbol graph inputs are not valid. // If the symbol graph files define a deep hierarchy, with the same symbol names but different symbol kinds across different, we try to match them up by language. @@ -3237,7 +3368,7 @@ class PathHierarchyTests: XCTestCase { }) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let swiftSpecificNode = try tree.findNode(path: "/ModuleName/OuterContainerName-struct/MiddleContainerName-struct/InnerContainerName-struct/swiftSpecificMember()", onlyFindSymbols: true, parent: nil) @@ -3286,7 +3417,7 @@ class PathHierarchyTests: XCTestCase { } } - func testMissingReferencedContainerSymbolOnSomePlatforms() throws { + func testMissingReferencedContainerSymbolOnSomePlatforms() async throws { // We make a best-effort attempt to create a valid path hierarchy, even if the symbol graph inputs are not valid. // If some platforms are missing the local container symbol from a `memberOf` relationship, but other platforms with the same relationship define that symbol, @@ -3321,14 +3452,81 @@ class PathHierarchyTests: XCTestCase { )) }) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertFindsPath("/ModuleName/ContainerName/memberName", in: tree, asSymbolID: memberID) try assertFindsPath("/ModuleName/ContainerName", in: tree, asSymbolID: containerID) } - func testMissingMemberOfAnonymousStructInsideUnion() throws { + func testMinimalTypeDisambiguationForClosureParameterWithVoidReturnType() async throws { + // Create a `doSomething(with:and:)` function with a `String` parameter (same in every overload) and a `(TYPE)->()` closure parameter. + func makeSymbolOverload(closureParameterType: SymbolGraph.Symbol.DeclarationFragments.Fragment) -> SymbolGraph.Symbol { + makeSymbol( + id: "some-function-overload-\(closureParameterType.spelling.lowercased())", + kind: .method, + pathComponents: ["doSomething(with:and:)"], + signature: .init( + parameters: [ + .init(name: "first", externalName: "with", declarationFragments: [ + .init(kind: .externalParameter, spelling: "with", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "first", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS") + ], children: []), + + .init(name: "second", externalName: "and", declarationFragments: [ + .init(kind: .externalParameter, spelling: "and", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "second", preciseIdentifier: nil), + .init(kind: .text, spelling: " (", preciseIdentifier: nil), + closureParameterType, + .init(kind: .text, spelling: ") -> ()", preciseIdentifier: nil), + ], children: []) + ], + returns: [.init(kind: .typeIdentifier, spelling: "Void", preciseIdentifier: "s:s4Voida")] + ) + ) + } + + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: [ + makeSymbolOverload(closureParameterType: .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si")), // (String, (Int)->()) -> Void + makeSymbolOverload(closureParameterType: .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd")), // (String, (Double)->()) -> Void + makeSymbolOverload(closureParameterType: .init(kind: .typeIdentifier, spelling: "Float", preciseIdentifier: "s:Sf")), // (String, (Float)->()) -> Void + ], + relationships: [] + )) + ]) + + let (_, context) = try await loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + let link = "/ModuleName/doSomething(with:and:)" + try assertPathRaisesErrorMessage(link, in: tree, context: context, expectedErrorMessage: "'doSomething(with:and:)' is ambiguous at '/ModuleName'") { errorInfo in + XCTAssertEqual(errorInfo.solutions.count, 3, "There should be one suggestion per overload") + for solution in errorInfo.solutions { + // Apply the suggested replacements for each solution and verify that _that_ link resolves to a single symbol. + var linkWithSuggestion = link + XCTAssertFalse(solution.replacements.isEmpty, "Diagnostics about ambiguous links should have some replacements for each solution.") + for (replacementText, start, end) in solution.replacements { + let range = linkWithSuggestion.index(linkWithSuggestion.startIndex, offsetBy: start) ..< linkWithSuggestion.index(linkWithSuggestion.startIndex, offsetBy: end) + linkWithSuggestion.replaceSubrange(range, with: replacementText) + } + + XCTAssertNotNil(try? tree.findSymbol(path: linkWithSuggestion), """ + Failed to resolve \(linkWithSuggestion) after applying replacements \(solution.replacements.map { "'\($0.0)'@\($0.start)-\($0.end)" }.joined(separator: ",")) to '\(link)'. + + The replacement that DocC suggests in its warnings should unambiguously refer to a single symbol match. + """) + } + } + } + + func testMissingMemberOfAnonymousStructInsideUnion() async throws { let outerContainerID = "some-outer-container-symbol-id" let innerContainerID = "some-inner-container-symbol-id" let memberID = "some-member-symbol-id" @@ -3381,7 +3579,7 @@ class PathHierarchyTests: XCTestCase { }) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() @@ -3398,8 +3596,8 @@ class PathHierarchyTests: XCTestCase { try assertPathNotFound("/ModuleName/Outer-struct/inner/member", in: tree) } - func testLinksToCxxOperators() throws { - let (_, context) = try testBundleAndContext(named: "CxxOperators") + func testLinksToCxxOperators() async throws { + let (_, context) = try await testBundleAndContext(named: "CxxOperators") let tree = context.linkResolver.localResolver.pathHierarchy // MyClass operator+() const; // unary plus @@ -3631,7 +3829,7 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("/CxxOperators/MyClass/operator,", in: tree, asSymbolID: "c:@S@MyClass@F@operator,#&$@S@MyClass#") } - func testMinimalTypeDisambiguation() throws { + func testMinimalTypeDisambiguation() async throws { enum DeclToken: ExpressibleByStringLiteral { case text(String) case internalParameter(String) @@ -3665,7 +3863,7 @@ class PathHierarchyTests: XCTestCase { let voidType = DeclToken.typeIdentifier("Void", precise: "s:s4Voida") func makeParameter(_ name: String, decl: [DeclToken]) -> SymbolGraph.Symbol.FunctionSignature.FunctionParameter { - .init(name: name, externalName: nil, declarationFragments: makeFragments([.internalParameter(name), .text("")] + decl), children: []) + .init(name: name, externalName: nil, declarationFragments: makeFragments([.internalParameter(name), .text(" ")] + decl), children: []) } func makeSignature(first: DeclToken..., second: DeclToken..., third: DeclToken...) -> SymbolGraph.Symbol.FunctionSignature { @@ -3712,7 +3910,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("ModuleName/doSomething(first:second:third:)", in: tree, collisions: [ @@ -3762,7 +3960,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("ModuleName/doSomething(first:second:third:)", in: tree, collisions: [ @@ -3772,6 +3970,92 @@ class PathHierarchyTests: XCTestCase { ]) } + // Each overload has a unique closure parameter with a "()" literal closure return type + do { + func makeSignature(first: DeclToken..., second: DeclToken...) -> SymbolGraph.Symbol.FunctionSignature { + .init( + parameters: [ + .init(name: "first", externalName: nil, declarationFragments: makeFragments(first), children: []), + .init(name: "second", externalName: nil, declarationFragments: makeFragments(second), children: []) + ], + returns: makeFragments([voidType]) + ) + } + + // String (Int)->() + // String (Double)->() + // String (Float)->() + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: [ + // String (Int)->Void + makeSymbol(id: "function-overload-1", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: makeSignature( + first: stringType, // String + second: "(", intType, ") -> ()" // (Int)->() + )), + + // String (Double)->Void + makeSymbol(id: "function-overload-2", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: makeSignature( + first: stringType, // String + second: "(", doubleType, ") -> ()" // (Double)->() + )), + + // String (Float)->Void + makeSymbol(id: "function-overload-3", kind: .func, pathComponents: ["doSomething(first:second:)"], signature: makeSignature( + first: stringType, // String + second: "(", floatType, ") -> ()" // (Double)->() + )), + ] + )) + ]) + + let (_, context) = try await loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertPathCollision("ModuleName/doSomething(first:second:)", in: tree, collisions: [ + (symbolID: "function-overload-1", disambiguation: "-(_,(Int)->())"), // _ (Int)->() + (symbolID: "function-overload-2", disambiguation: "-(_,(Double)->())"), // _ (Double)->() + (symbolID: "function-overload-3", disambiguation: "-(_,(Float)->())"), // _ (Float)->() + ]) + } + + // The second overload refers to the metatype of the parameter + do { + func makeSignature(first: DeclToken...) -> SymbolGraph.Symbol.FunctionSignature { + .init( + parameters: [.init(name: "first", externalName: "with", declarationFragments: makeFragments(first), children: []),], + returns: makeFragments([voidType]) + ) + } + + let someGenericTypeID = "some-generic-type-id" + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: [ + makeSymbol(id: "function-overload-1", kind: .func, pathComponents: ["doSomething(with:)"], signature: makeSignature( + // GenericName + first: .typeIdentifier("GenericName", precise: someGenericTypeID) + )), + + makeSymbol(id: "function-overload-2", kind: .func, pathComponents: ["doSomething(with:)"], signature: makeSignature( + // GenericName.Type + first: .typeIdentifier("GenericName", precise: someGenericTypeID), ".Type" + )), + ] + )) + ]) + + let (_, context) = try await loadBundle(catalog: catalog) + let tree = context.linkResolver.localResolver.pathHierarchy + + try assertPathCollision("ModuleName/doSomething(with:)", in: tree, collisions: [ + (symbolID: "function-overload-1", disambiguation: "-(GenericName)"), // GenericName + (symbolID: "function-overload-2", disambiguation: "-(GenericName.Type)"), // GenericName.Type + ]) + } + // Second overload requires combination of two non-unique types to disambiguate do { // String Set (Double)->Void @@ -3805,7 +4089,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("ModuleName/doSomething(first:second:third:)", in: tree, collisions: [ @@ -3904,7 +4188,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("ModuleName/doSomething(first:second:third:fourth:fifth:sixth:)", in: tree, collisions: [ @@ -3970,7 +4254,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("ModuleName/doSomething(first:second:)", in: tree, collisions: [ @@ -4020,7 +4304,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("ModuleName/doSomething(first:)", in: tree, collisions: [ @@ -4065,7 +4349,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("ModuleName/doSomething(...)", in: tree, collisions: [ @@ -4092,7 +4376,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("ModuleName/doSomething(...)", in: tree, collisions: [ @@ -4119,7 +4403,7 @@ class PathHierarchyTests: XCTestCase { )) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("ModuleName/doSomething(...)", in: tree, collisions: [ @@ -4276,9 +4560,29 @@ class PathHierarchyTests: XCTestCase { } assertParsedPathComponents("operator[]-(std::string&)->std::string&", [("operator[]", .typeSignature(parameterTypes: ["std::string&"], returnTypes: ["std::string&"]))]) + + // Nested generic types + assertParsedPathComponents("functionName-(KeyPath)", [("functionName", .typeSignature(parameterTypes: ["KeyPath"], returnTypes: nil))]) + assertParsedPathComponents("functionName->KeyPath", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath"]))]) + + assertParsedPathComponents("functionName-(KeyPath,Dictionary)", [("functionName", .typeSignature(parameterTypes: ["KeyPath", "Dictionary"], returnTypes: nil))]) + assertParsedPathComponents("functionName->(KeyPath,Dictionary)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath", "Dictionary"]))]) + + assertParsedPathComponents("functionName-(KeyPath>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath>"], returnTypes: nil))]) + assertParsedPathComponents("functionName->KeyPath>", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath>"]))]) + + assertParsedPathComponents("functionName-(KeyPath,Dictionary>)", [("functionName", .typeSignature(parameterTypes: ["KeyPath,Dictionary>"], returnTypes: nil))]) + assertParsedPathComponents("functionName->KeyPath,Dictionary>", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["KeyPath,Dictionary>"]))]) + + // Nested generics and tuple types + assertParsedPathComponents( "functionName-(A,(D,H<(I,J),(K,L)>),M,R),S>)", [("functionName", .typeSignature(parameterTypes: ["A", "(D,H<(I,J),(K,L)>)", "M,R),S>"], returnTypes: nil))]) + assertParsedPathComponents("functionName->(A,(D,H<(I,J),(K,L)>),M,R),S>)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["A", "(D,H<(I,J),(K,L)>)", "M,R),S>"]))]) + // With special characters + assertParsedPathComponents( "functionName-(Å<𝔹,©>,(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>),𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>)", [("functionName", .typeSignature(parameterTypes: ["Å<𝔹,©>", "(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>)", "𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>"], returnTypes: nil))]) + assertParsedPathComponents("functionName->(Å<𝔹,©>,(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>),𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["Å<𝔹,©>", "(Δ<∃,⨍,𝄞>,ℌ<(𝓲,ⅉ),(🄺,ƛ)>)", "𝔐<𝚗,(Ω<π,Ⓠ>,℟),𝔖>"]))]) } - func testResolveExternalLinkFromTechnologyRoot() throws { + func testResolveExternalLinkFromTechnologyRoot() async throws { enableFeatureFlag(\.isExperimentalLinkHierarchySerializationEnabled) let catalog = Folder(name: "unit-test.docc", content: [ @@ -4289,7 +4593,7 @@ class PathHierarchyTests: XCTestCase { """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let tree = context.linkResolver.localResolver.pathHierarchy let rootIdentifier = try XCTUnwrap(tree.modules.first?.identifier) @@ -4314,8 +4618,8 @@ class PathHierarchyTests: XCTestCase { XCTFail("Symbol for \(path.singleQuoted) not found in tree", file: file, line: line) } catch PathHierarchy.Error.unknownName { XCTFail("Symbol for \(path.singleQuoted) not found in tree. Only part of path is found.", file: file, line: line) - } catch PathHierarchy.Error.unknownDisambiguation { - XCTFail("Symbol for \(path.singleQuoted) not found in tree. Unknown disambiguation.", file: file, line: line) + } catch PathHierarchy.Error.unknownDisambiguation(_, _, let candidates) { + XCTFail("Symbol for \(path.singleQuoted) not found in tree. Unknown disambiguation. Suggested disambiguations: \(candidates.map(\.disambiguation.singleQuoted).sorted().joined(separator: ", "))", file: file, line: line) } catch PathHierarchy.Error.lookupCollision(_, _, let collisions) { let symbols = collisions.map { $0.node.symbol! } XCTFail("Unexpected collision for \(path.singleQuoted); \(symbols.map { return "\($0.names.title) - \($0.kind.identifier.identifier) - \($0.identifier.precise.stableHashString)"})", file: file, line: line) diff --git a/Tests/SwiftDocCTests/Infrastructure/PresentationURLGeneratorTests.swift b/Tests/SwiftDocCTests/Infrastructure/PresentationURLGeneratorTests.swift index 7f36a61e9a..96adc54726 100644 --- a/Tests/SwiftDocCTests/Infrastructure/PresentationURLGeneratorTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/PresentationURLGeneratorTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -13,8 +13,8 @@ import Foundation @testable import SwiftDocC class PresentationURLGeneratorTests: XCTestCase { - func testInternalURLs() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testInternalURLs() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let generator = PresentationURLGenerator(context: context, baseURL: URL(string: "https://host:1024/webPrefix")!) // Test resolved tutorial reference diff --git a/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift index a2a06b12d3..4a37493359 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ReferenceResolverTests.swift @@ -14,7 +14,7 @@ import Markdown import SymbolKit class ReferenceResolverTests: XCTestCase { - func testResolvesMediaForIntro() throws { + func testResolvesMediaForIntro() async throws { let source = """ @Intro( title: x) { @@ -24,16 +24,16 @@ class ReferenceResolverTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, context) = try testBundleAndContext() + let (_, context) = try await testBundleAndContext() var problems = [Problem]() - let intro = Intro(from: directive, source: nil, for: bundle, problems: &problems)! + let intro = Intro(from: directive, source: nil, for: context.inputs, problems: &problems)! - var resolver = ReferenceResolver(context: context, bundle: bundle) + var resolver = ReferenceResolver(context: context) _ = resolver.visitIntro(intro) XCTAssertEqual(resolver.problems.count, 1) } - func testResolvesMediaForContentAndMedia() throws { + func testResolvesMediaForContentAndMedia() async throws { let source = """ @ContentAndMedia { Blah blah. @@ -43,16 +43,16 @@ class ReferenceResolverTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, context) = try testBundleAndContext() + let (_, context) = try await testBundleAndContext() var problems = [Problem]() - let contentAndMedia = ContentAndMedia(from: directive, source: nil, for: bundle, problems: &problems)! + let contentAndMedia = ContentAndMedia(from: directive, source: nil, for: context.inputs, problems: &problems)! - var resolver = ReferenceResolver(context: context, bundle: bundle) + var resolver = ReferenceResolver(context: context) _ = resolver.visit(contentAndMedia) XCTAssertEqual(resolver.problems.count, 1) } - func testResolvesExternalLinks() throws { + func testResolvesExternalLinks() async throws { let source = """ @Intro(title: "Technology X") { Info at: . @@ -60,11 +60,11 @@ class ReferenceResolverTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, context) = try testBundleAndContext() + let (_, context) = try await testBundleAndContext() var problems = [Problem]() - let intro = Intro(from: directive, source: nil, for: bundle, problems: &problems)! + let intro = Intro(from: directive, source: nil, for: context.inputs, problems: &problems)! - var resolver = ReferenceResolver(context: context, bundle: bundle) + var resolver = ReferenceResolver(context: context) guard let container = resolver.visit(intro).children.first as? MarkupContainer, let firstElement = container.elements.first, @@ -77,8 +77,8 @@ class ReferenceResolverTests: XCTestCase { } // Tests all reference syntax formats to a child symbol - func testReferencesToChildFromFramework() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testReferencesToChildFromFramework() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Article that curates `SideClass` try """ # ``SideKit`` @@ -101,7 +101,7 @@ class ReferenceResolverTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify resolved links @@ -110,8 +110,8 @@ class ReferenceResolverTests: XCTestCase { } // Test relative paths to non-child symbol - func testReferencesToGrandChildFromFramework() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testReferencesToGrandChildFromFramework() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Article that curates `SideClass` try """ # ``SideKit`` @@ -127,7 +127,7 @@ class ReferenceResolverTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify resolved links @@ -136,8 +136,8 @@ class ReferenceResolverTests: XCTestCase { } // Test references to a sibling symbol - func testReferencesToSiblingFromFramework() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testReferencesToSiblingFromFramework() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Article that curates `SideClass` try """ # ``SideKit/SideClass/myFunction()`` @@ -153,7 +153,7 @@ class ReferenceResolverTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass/myFunction()", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify resolved links @@ -162,8 +162,8 @@ class ReferenceResolverTests: XCTestCase { } // Test references to symbols in root paths - func testReferencesToTutorial() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testReferencesToTutorial() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Article that curates `SideClass` try """ # ``SideKit/SideClass/myFunction()`` @@ -179,7 +179,7 @@ class ReferenceResolverTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass/myFunction()", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify resolved links @@ -188,8 +188,8 @@ class ReferenceResolverTests: XCTestCase { } // Test references to technology pages - func testReferencesToTechnologyPages() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testReferencesToTechnologyPages() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Article that curates `SideClass` try """ # ``SideKit/SideClass/myFunction()`` @@ -204,7 +204,7 @@ class ReferenceResolverTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass/myFunction()", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify resolved links @@ -213,8 +213,8 @@ class ReferenceResolverTests: XCTestCase { } // Test external references - func testExternalReferencesConsiderBundleIdentifier() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testExternalReferencesConsiderBundleIdentifier() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Article that curates `SideClass` try """ # ``SideKit/SideClass/myFunction()`` @@ -230,7 +230,7 @@ class ReferenceResolverTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass/myFunction()", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify resolved links @@ -306,7 +306,7 @@ class ReferenceResolverTests: XCTestCase { } } - func testRegisteredButUncuratedArticles() throws { + func testRegisteredButUncuratedArticles() async throws { var referencingArticleURL: URL! var uncuratedArticleFile: URL! @@ -321,7 +321,7 @@ class ReferenceResolverTests: XCTestCase { """ // TestBundle has more than one module, so automatic registration and curation won't happen - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in referencingArticleURL = root.appendingPathComponent("article.md") try source.write(to: referencingArticleURL, atomically: true, encoding: .utf8) @@ -345,8 +345,8 @@ class ReferenceResolverTests: XCTestCase { XCTAssertEqual(referencingFileDiagnostics.filter({ $0.identifier == "org.swift.docc.unresolvedTopicReference" }).count, 1) } - func testRelativeReferencesToExtensionSymbols() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "BundleWithRelativePathAmbiguity") { root in + func testRelativeReferencesToExtensionSymbols() async throws { + let (_, _, context) = try await 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")) @@ -378,7 +378,7 @@ class ReferenceResolverTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/BundleWithRelativePathAmbiguity/Dependency", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode let content = try XCTUnwrap(renderNode.primaryContentSections.first as? ContentRenderSection).content @@ -406,11 +406,11 @@ class ReferenceResolverTests: XCTestCase { } } - func testCuratedExtensionRemovesEmptyPage() throws { - let (bundle, context) = try testBundleAndContext(named: "ModuleWithSingleExtension") + func testCuratedExtensionRemovesEmptyPage() async throws { + let (_, context) = try await testBundleAndContext(named: "ModuleWithSingleExtension") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleWithSingleExtension", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleWithSingleExtension", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // The only children of the root topic should be the `MyNamespace` enum - i.e. the Swift @@ -421,11 +421,11 @@ class ReferenceResolverTests: XCTestCase { // Make sure that the symbol added in the extension is still present in the topic graph, // even though its synthetic "extended symbol" parents are not - XCTAssertNoThrow(try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleWithSingleExtension/Swift/Array/asdf", sourceLanguage: .swift))) + XCTAssertNoThrow(try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleWithSingleExtension/Swift/Array/asdf", sourceLanguage: .swift))) } - func testCuratedExtensionWithDanglingReference() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "ModuleWithSingleExtension") { root in + func testCuratedExtensionWithDanglingReference() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "ModuleWithSingleExtension") { root in let topLevelArticle = root.appendingPathComponent("ModuleWithSingleExtension.md") try FileManager.default.removeItem(at: topLevelArticle) @@ -445,16 +445,16 @@ class ReferenceResolverTests: XCTestCase { XCTAssertEqual(replacement.replacement, "`Swift/Array`") // Also make sure that the extension pages are still gone - let extendedModule = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleWithSingleExtension/Swift", sourceLanguage: .swift) + let extendedModule = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleWithSingleExtension/Swift", sourceLanguage: .swift) XCTAssertFalse(context.knownPages.contains(where: { $0 == extendedModule })) - let extendedStructure = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleWithSingleExtension/Swift/Array", sourceLanguage: .swift) + let extendedStructure = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleWithSingleExtension/Swift/Array", sourceLanguage: .swift) XCTAssertFalse(context.knownPages.contains(where: { $0 == extendedStructure })) // Load the RenderNode for the root article and make sure that the `Swift/Array` symbol link // is not rendered as a link - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleWithSingleExtension", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleWithSingleExtension", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode XCTAssertEqual(renderNode.abstract, [ @@ -464,8 +464,8 @@ class ReferenceResolverTests: XCTestCase { ]) } - func testCuratedExtensionWithDanglingReferenceToFragment() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "ModuleWithSingleExtension") { root in + func testCuratedExtensionWithDanglingReferenceToFragment() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "ModuleWithSingleExtension") { root in let topLevelArticle = root.appendingPathComponent("ModuleWithSingleExtension.md") try FileManager.default.removeItem(at: topLevelArticle) @@ -492,8 +492,8 @@ class ReferenceResolverTests: XCTestCase { XCTAssertFalse(context.knownPages.contains(where: { $0 == extendedStructure })) } - func testCuratedExtensionWithDocumentationExtension() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "ModuleWithSingleExtension") { root in + func testCuratedExtensionWithDocumentationExtension() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "ModuleWithSingleExtension") { root in let topLevelArticle = root.appendingPathComponent("ModuleWithSingleExtension.md") try FileManager.default.removeItem(at: topLevelArticle) @@ -521,11 +521,11 @@ class ReferenceResolverTests: XCTestCase { XCTAssert(context.knownPages.contains(where: { $0 == extendedStructure })) } - func testCuratedExtensionWithAdditionalConformance() throws { - let (bundle, context) = try testBundleAndContext(named: "ModuleWithConformanceAndExtension") + func testCuratedExtensionWithAdditionalConformance() async throws { + let (_, context) = try await testBundleAndContext(named: "ModuleWithConformanceAndExtension") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleWithConformanceAndExtension/MyProtocol", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleWithConformanceAndExtension/MyProtocol", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode let conformanceSection = try XCTUnwrap(renderNode.relationshipSections.first(where: { $0.type == RelationshipsGroup.Kind.conformingTypes.rawValue })) @@ -537,11 +537,11 @@ class ReferenceResolverTests: XCTestCase { XCTAssert(renderReference is UnresolvedRenderReference) } - func testExtensionWithEmptyDeclarationFragments() throws { - let (bundle, context) = try testBundleAndContext(named: "ModuleWithEmptyDeclarationFragments") + func testExtensionWithEmptyDeclarationFragments() async throws { + let (_, context) = try await testBundleAndContext(named: "ModuleWithEmptyDeclarationFragments") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleWithEmptyDeclarationFragments", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleWithEmptyDeclarationFragments", sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Despite having an extension to Float, there are no symbols added by that extension, so @@ -549,7 +549,7 @@ class ReferenceResolverTests: XCTestCase { XCTAssertEqual(renderNode.topicSections.count, 0) } - func testUnresolvedTutorialReferenceIsWarning() throws { + func testUnresolvedTutorialReferenceIsWarning() async throws { let source = """ @Chapter(name: "SwiftUI Essentials") { @@ -560,18 +560,18 @@ class ReferenceResolverTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, context) = try testBundleAndContext() + let (_, context) = try await testBundleAndContext() var problems = [Problem]() - let chapter = try XCTUnwrap(Chapter(from: directive, source: nil, for: bundle, problems: &problems)) - var resolver = ReferenceResolver(context: context, bundle: bundle) + let chapter = try XCTUnwrap(Chapter(from: directive, source: nil, for: context.inputs, problems: &problems)) + var resolver = ReferenceResolver(context: context) _ = resolver.visitChapter(chapter) XCTAssertFalse(resolver.problems.containsErrors) XCTAssertEqual(resolver.problems.count, 1) XCTAssertEqual(resolver.problems.filter({ $0.diagnostic.severity == .warning }).count, 1) } - func testResolvesArticleContent() throws { + func testResolvesArticleContent() async throws { let source = """ # An Article @@ -580,11 +580,11 @@ class ReferenceResolverTests: XCTestCase { Discussion link to ``SideKit``. """ - let (bundle, context) = try testBundleAndContext() + let (_, context) = try await testBundleAndContext() let document = Document(parsing: source, options: [.parseBlockDirectives, .parseSymbolLinks]) let article = try XCTUnwrap(Article(markup: document, metadata: nil, redirects: nil, options: [:])) - var resolver = ReferenceResolver(context: context, bundle: bundle) + var resolver = ReferenceResolver(context: context) let resolvedArticle = try XCTUnwrap(resolver.visitArticle(article) as? Article) let abstractSection = try XCTUnwrap(resolvedArticle.abstractSection) @@ -611,10 +611,10 @@ class ReferenceResolverTests: XCTestCase { XCTAssertTrue(foundSymbolDiscussionLink) } - func testForwardsSymbolPropertiesThatAreUnmodifiedDuringLinkResolution() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testForwardsSymbolPropertiesThatAreUnmodifiedDuringLinkResolution() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var resolver = ReferenceResolver(context: context, bundle: bundle) + var resolver = ReferenceResolver(context: context) let symbol = try XCTUnwrap(context.documentationCache["s:5MyKit0A5ClassC"]?.semantic as? Symbol) @@ -736,7 +736,7 @@ class ReferenceResolverTests: XCTestCase { } } - func testEmitsDiagnosticsForEachDocumentationChunk() throws { + func testEmitsDiagnosticsForEachDocumentationChunk() async throws { let moduleReference = ResolvedTopicReference(bundleID: "com.example.test", path: "/documentation/ModuleName", sourceLanguage: .swift) let reference = ResolvedTopicReference(bundleID: "com.example.test", path: "/documentation/ModuleName/Something", sourceLanguage: .swift) @@ -768,7 +768,7 @@ class ReferenceResolverTests: XCTestCase { mixins: [:] ) - let (bundle, context) = try testBundleAndContext() + let (_, context) = try await testBundleAndContext() let documentationExtensionContent = """ # ``Something`` @@ -785,7 +785,7 @@ class ReferenceResolverTests: XCTestCase { let article = Article( from: Document(parsing: documentationExtensionContent, source: documentationExtensionURL, options: [.parseSymbolLinks, .parseBlockDirectives]), source: documentationExtensionURL, - for: bundle, + for: context.inputs, problems: &ignoredProblems ) XCTAssert(ignoredProblems.isEmpty, "Unexpected problems creating article") @@ -801,7 +801,7 @@ class ReferenceResolverTests: XCTestCase { XCTAssertEqual(node.docChunks.count, 2, "This node has content from both the in-source comment and the documentation extension file.") - var resolver = ReferenceResolver(context: context, bundle: bundle) + var resolver = ReferenceResolver(context: context) _ = resolver.visitSymbol(node.semantic as! Symbol) let problems = resolver.problems.sorted(by: \.diagnostic.summary) diff --git a/Tests/SwiftDocCTests/Infrastructure/SnippetResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/SnippetResolverTests.swift new file mode 100644 index 0000000000..84e39551d5 --- /dev/null +++ b/Tests/SwiftDocCTests/Infrastructure/SnippetResolverTests.swift @@ -0,0 +1,272 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 XCTest +@testable import SwiftDocC +import SymbolKit +import SwiftDocCTestUtilities + +class SnippetResolverTests: XCTestCase { + + let optionalPathPrefixes = [ + // The module name as the first component + "/ModuleName/Snippets/", + "ModuleName/Snippets/", + + // The catalog name as the first component + "/Something/Snippets/", + "Something/Snippets/", + + // Snippets repeated as the first component + "/Snippets/Snippets/", + "Snippets/Snippets/", + + // Only the "Snippets" prefix + "/Snippets/", + "Snippets/", + + // No prefix + "/", + "", + ] + + func testRenderingSnippetsWithOptionalPathPrefixes() async throws { + for pathPrefix in optionalPathPrefixes { + let (problems, _, snippetRenderBlocks) = try await makeSnippetContext( + snippets: [ + makeSnippet( + pathComponents: ["Snippets", "First"], + explanation: """ + Some _formatted_ **content** that provides context to the snippet. + """, + code: """ + // Some code comment + print("Hello, world!") + """, + slices: ["comment": 0..<1] + ), + makeSnippet( + pathComponents: ["Snippets", "Path", "To", "Second"], + explanation: nil, + code: """ + print("1 + 2 = \\(1+2)") + """ + ) + ], + rootContent: """ + @Snippet(path: \(pathPrefix)First) + + @Snippet(path: \(pathPrefix)Path/To/Second) + + @Snippet(path: \(pathPrefix)First, slice: comment) + """ + ) + + // These links should all resolve, regardless of optional prefix + XCTAssertTrue(problems.isEmpty, "Unexpected problems for path prefix '\(pathPrefix)': \(problems.map(\.diagnostic.summary))") + + // Because the snippet links resolved, their content should render on the page. + + // The explanation for the first snippet + if case .paragraph(let paragraph) = snippetRenderBlocks.first { + XCTAssertEqual(paragraph.inlineContent, [ + .text("Some "), + .emphasis(inlineContent: [.text("formatted")]), + .text(" "), + .strong(inlineContent: [.text("content")]), + .text(" that provides context to the snippet."), + ]) + } else { + XCTFail("Missing expected rendered explanation.") + } + + // The first snippet code + if case .codeListing(let codeListing) = snippetRenderBlocks.dropFirst().first { + XCTAssertEqual(codeListing.syntax, "swift") + XCTAssertEqual(codeListing.code, [ + #"// Some code comment"#, + #"print("Hello, world!")"#, + ]) + } else { + XCTFail("Missing expected rendered code block.") + } + + // The second snippet (without an explanation) + if case .codeListing(let codeListing) = snippetRenderBlocks.dropFirst(2).first { + XCTAssertEqual(codeListing.syntax, "swift") + XCTAssertEqual(codeListing.code, [ + #"print("1 + 2 = \(1+2)")"# + ]) + } else { + XCTFail("Missing expected rendered code block.") + } + + // The third snippet is a slice, so it doesn't display its explanation + if case .codeListing(let codeListing) = snippetRenderBlocks.dropFirst(3).first { + XCTAssertEqual(codeListing.syntax, "swift") + XCTAssertEqual(codeListing.code, [ + #"// Some code comment"#, + ]) + } else { + XCTFail("Missing expected rendered code block.") + } + + XCTAssertNil(snippetRenderBlocks.dropFirst(4).first, "There's no more content after the snippets") + } + } + + func testWarningsAboutMisspelledSnippetPathsAndMisspelledSlice() async throws { + for pathPrefix in optionalPathPrefixes.prefix(1) { + let (problems, logOutput, snippetRenderBlocks) = try await makeSnippetContext( + snippets: [ + makeSnippet( + pathComponents: ["Snippets", "First"], + explanation: """ + Some _formatted_ **content** that provides context to the snippet. + """, + code: """ + // Some code comment + print("Hello, world!") + """, + slices: [ + "comment": 0..<1, + "print": 1..<2, + ] + ), + ], + rootContent: """ + @Snippet(path: \(pathPrefix)Frst) + + @Snippet(path: \(pathPrefix)First, slice: commt) + """ + ) + + // The first snippet has a misspelled path and the second has a misspelled slice + XCTAssertEqual(problems.map(\.diagnostic.summary), [ + "Snippet named 'Frst' couldn't be found", + "Slice named 'commt' doesn't exist in snippet 'First'", + ]) + + // Verify that the suggested solutions correct the issues. + let rootMarkupContent = """ + # Heading + + Abstract + + ## Subheading + + @Snippet(path: \(pathPrefix)Frst) + + @Snippet(path: \(pathPrefix)First, slice: commt) + """ + do { + let snippetPathProblem = try XCTUnwrap(problems.first) + let solution = try XCTUnwrap(snippetPathProblem.possibleSolutions.first) + let modifiedLines = try solution.applyTo(rootMarkupContent).components(separatedBy: "\n") + XCTAssertEqual(modifiedLines[6], "@Snippet(path: \(pathPrefix)First)") + } + do { + let snippetSliceProblem = try XCTUnwrap(problems.last) + let solution = try XCTUnwrap(snippetSliceProblem.possibleSolutions.first) + let modifiedLines = try solution.applyTo(rootMarkupContent).components(separatedBy: "\n") + XCTAssertEqual(modifiedLines[8], "@Snippet(path: \(pathPrefix)First, slice: comment)") + } + + let prefixLength = pathPrefix.count + XCTAssertEqual(logOutput, """ + \u{001B}[1;33mwarning: Snippet named 'Frst' couldn't be found\u{001B}[0;0m + --> ModuleName.md:7:\(16 + prefixLength)-7:\(20 + prefixLength) + 5 | ## Overview + 6 | + 7 + @Snippet(path: \(pathPrefix)\u{001B}[1;32mFrst\u{001B}[0;0m) + | \(String(repeating: " ", count: prefixLength)) ╰─\u{001B}[1;39msuggestion: Replace 'Frst' with 'First'\u{001B}[0;0m + 8 | + 9 | @Snippet(path: \(pathPrefix)First, slice: commt) + + \u{001B}[1;33mwarning: Slice named 'commt' doesn't exist in snippet 'First'\u{001B}[0;0m + --> ModuleName.md:9:\(30 + prefixLength)-9:\(35 + prefixLength) + 7 | @Snippet(path: \(pathPrefix)Frst) + 8 | + 9 + @Snippet(path: \(pathPrefix)First, slice: \u{001B}[1;32mcommt\u{001B}[0;0m) + | \(String(repeating: " ", count: prefixLength)) ╰─\u{001B}[1;39msuggestion: Replace 'commt' with 'comment'\u{001B}[0;0m + + """) + + // Because the snippet links failed to resolve, their content shouldn't render on the page. + XCTAssertTrue(snippetRenderBlocks.isEmpty, "There's no more content after the snippets") + } + } + + private func makeSnippetContext( + snippets: [SymbolGraph.Symbol], + rootContent: String, + file: StaticString = #filePath, + line: UInt = #line + ) async throws -> ([Problem], logOutput: String, some Collection) { + let catalog = Folder(name: "Something.docc", content: [ + JSONFile(name: "something-snippets.symbols.json", content: makeSymbolGraph(moduleName: "Snippets", symbols: snippets)), + // Include a "real" module that's separate from the snippet symbol graph. + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName")), + + TextFile(name: "ModuleName.md", utf8Content: """ + # ``ModuleName`` + + Always include an abstract here before the custom markup + + ## Overview + + \(rootContent) + """) + ]) + // We make the "Overview" heading explicit above so that the rendered page will always have a `primaryContentSections`. + // This makes it easier for the test to then + + let logStore = LogHandle.LogStorage() + let (_, context) = try await loadBundle(catalog: catalog, logOutput: LogHandle.memory(logStore)) + + XCTAssertEqual(context.knownIdentifiers.count, 1, "The snippets don't have their own identifiers", file: file, line: line) + + let reference = try XCTUnwrap(context.soleRootModuleReference, file: file, line: line) + let moduleNode = try context.entity(with: reference) + let renderNode = DocumentationNodeConverter(context: context).convert(moduleNode) + + let renderBlocks = try XCTUnwrap(renderNode.primaryContentSections.first as? ContentRenderSection, file: file, line: line).content + + if case .heading(let heading) = renderBlocks.first { + XCTAssertEqual(heading.level, 2, file: file, line: line) + XCTAssertEqual(heading.text, "Overview", file: file, line: line) + } else { + XCTFail("The rendered page is missing the 'Overview' heading. Something unexpected is happening with the page content.", file: file, line: line) + } + + return (context.problems.sorted(by: \.diagnostic.range!.lowerBound.line), logStore.text, renderBlocks.dropFirst()) + } + + private func makeSnippet( + pathComponents: [String], + explanation: String?, + code: String, + slices: [String: Range] = [:] + ) -> SymbolGraph.Symbol { + makeSymbol( + id: "$snippet__module-name.\(pathComponents.map { $0.lowercased() }.joined(separator: "."))", + kind: .snippet, + pathComponents: pathComponents, + docComment: explanation, + otherMixins: [ + SymbolGraph.Symbol.Snippet( + language: SourceLanguage.swift.id, + lines: code.components(separatedBy: "\n"), + slices: slices + ) + ] + ) + } +} diff --git a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift deleted file mode 100644 index 009caf03fd..0000000000 --- a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift +++ /dev/null @@ -1,870 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2024 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 -@testable import SwiftDocC - -// This test uses ``AbsoluteSymbolLink`` which is deprecated. -// Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -class AbsoluteSymbolLinkTests: XCTestCase { - func testCreationOfValidLinks() throws { - let validLinks = [ - "doc://org.swift.ShapeKit/documentation/ShapeKit", - "doc://org.swift.ShapeKit/documentation/ShapeKit/ParentType/Test-swift.class/", - "doc://org.swift.ShapeKit/documentation/ShapeKit/ParentType/Test-swift.class/testFunc()-k2k9d", - ] - - let expectedLinkDescriptions = [ - """ - { - bundleID: 'org.swift.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'ShapeKit', suffix: (none)), - representsModule: true, - basePathComponents: [] - } - """, - """ - { - bundleID: 'org.swift.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'ParentType', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'Test', suffix: (kind: 'swift.class'))] - } - """, - """ - { - bundleID: 'org.swift.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'ParentType', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'Test', suffix: (kind: 'swift.class')), (name: 'testFunc()', suffix: (idHash: 'k2k9d'))] - } - """, - ] - - let absoluteSymbolLinks = validLinks.compactMap(AbsoluteSymbolLink.init(string:)) - - XCTAssertEqual(absoluteSymbolLinks.count, expectedLinkDescriptions.count) - - for (absoluteSymbolLink, expectedDescription) in zip(absoluteSymbolLinks, expectedLinkDescriptions) { - XCTAssertEqual(absoluteSymbolLink.description, expectedDescription) - } - } - - func testCreationOfInvalidLinkWithBadScheme() { - XCTAssertNil( - AbsoluteSymbolLink(string: "dc://org.swift.ShapeKit/documentation/ShapeKit") - ) - - XCTAssertNil( - AbsoluteSymbolLink(string: "http://org.swift.ShapeKit/documentation/ShapeKit") - ) - - XCTAssertNil( - AbsoluteSymbolLink(string: "https://org.swift.ShapeKit/documentation/ShapeKit") - ) - } - - func testCreationOfInvalidLinkWithoutDocumentationPath() { - XCTAssertNil( - AbsoluteSymbolLink(string: "doc://org.swift.ShapeKit/tutorials/ShapeKit") - ) - - XCTAssertNil( - AbsoluteSymbolLink(string: "doc://org.swift.ShapeKit/ShapeKit") - ) - } - - func testCreationOfInvalidLinkWithNoBundleID() { - XCTAssertNil( - AbsoluteSymbolLink(string: "doc:///documentation/ShapeKit") - ) - XCTAssertNil( - AbsoluteSymbolLink(string: "doc:/documentation/ShapeKit") - ) - } - - func testCreationOfInvalidLinkWithBadSuffix() { - XCTAssertNil( - // Empty suffix - AbsoluteSymbolLink(string: "doc://org.swift.ShapeKit/ShapeKit/ParentType/Test-swift.class/testFunc()-") - ) - - XCTAssertNil( - // Empty suffix - AbsoluteSymbolLink(string: "doc://org.swift.ShapeKit/ShapeKit/ParentType/Test-/testFunc()") - ) - - XCTAssertNil( - // Empty suffix - AbsoluteSymbolLink(string: "doc://org.swift.ShapeKit/ShapeKit/ParentType-/Test/testFunc()") - ) - - XCTAssertNil( - // Empty suffix - AbsoluteSymbolLink(string: "doc://org.swift.ShapeKit/ShapeKit-/ParentType/Test/testFunc()") - ) - - XCTAssertNil( - // Invalid type - AbsoluteSymbolLink(string: "doc://org.swift.ShapeKit/ShapeKit/ParentType/Test-swift.class/testFunc()-swift.funny-1s4Rt") - ) - - XCTAssertNil( - // Invalid type - AbsoluteSymbolLink(string: "doc://org.swift.ShapeKit/ShapeKit/ParentType/Test-swift.clss-5f7h9/testFunc()") - ) - } - - func testCreationOfValidLinksFromRenderNode() throws { - let symbolJSON = try String(contentsOf: Bundle.module.url( - forResource: "symbol-with-automatic-see-also-section", withExtension: "json", - subdirectory: "Converter Fixtures")!) - - let renderNode = try RenderNodeTransformer(renderNodeData: symbolJSON.data(using: .utf8)!) - - let references = Array(renderNode.renderNode.references.keys).sorted() - - let absoluteSymbolLinks = references.map(AbsoluteSymbolLink.init(string:)) - let absoluteSymbolLinkDescriptions = absoluteSymbolLinks.map(\.?.description) - - let expectedDescriptions: [String?] = [ - // doc://org.swift.docc.example/documentation/MyKit - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyKit', suffix: (none)), - representsModule: true, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/MyKit-Basics: (This is an article link) - nil, - // doc://org.swift.docc.example/documentation/MyKit/MyClass: - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyClass', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/MyClass/init(_:)-3743d: - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'init(_:)', suffix: (idHash: '3743d'))] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/MyClass/init(_:)-98u07: - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'init(_:)', suffix: (idHash: '98u07'))] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/MyClass/myFunction(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'myFunction()', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/YourClass: - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'YourClass', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/Reference-From-Automatic-SeeAlso-Section-Only: - nil, - // doc://org.swift.docc.example/documentation/Reference-In-Automatic-SeeAlso-And-Fragments: - nil, - // doc://org.swift.docc.example/tutorials/TechnologyX/Tutorial: (Tutorials link): - nil, - // doc://org.swift.docc.example/tutorials/TechnologyX/Tutorial2: - nil, - // doc://org.swift.docc.example/tutorials/TechnologyX/Tutorial4: - nil, - ] - - for (index, expectedDescription) in expectedDescriptions.enumerated() { - XCTAssertEqual( - absoluteSymbolLinkDescriptions[index], - expectedDescription, - """ - Failed to correctly construct link from '\(references[index])' - """ - ) - } - } - - func testCompileSymbolGraphAndValidateLinks() throws { - let (_, _, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let expectedDescriptions = [ - // doc://org.swift.docc.example/documentation/FillIntroduced: - """ - { - bundleID: 'org.swift.docc.example', - module: 'FillIntroduced', - topLevelSymbol: (name: 'FillIntroduced', suffix: (none)), - representsModule: true, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/FillIntroduced/iOSMacOSOnly(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'FillIntroduced', - topLevelSymbol: (name: 'iOSMacOSOnly()', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/FillIntroduced/iOSOnlyDeprecated(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'FillIntroduced', - topLevelSymbol: (name: 'iOSOnlyDeprecated()', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/FillIntroduced/iOSOnlyIntroduced(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'FillIntroduced', - topLevelSymbol: (name: 'iOSOnlyIntroduced()', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/FillIntroduced/macCatalystOnlyDeprecated(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'FillIntroduced', - topLevelSymbol: (name: 'macCatalystOnlyDeprecated()', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/FillIntroduced/macCatalystOnlyIntroduced(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'FillIntroduced', - topLevelSymbol: (name: 'macCatalystOnlyIntroduced()', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/FillIntroduced/macOSOnlyDeprecated(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'FillIntroduced', - topLevelSymbol: (name: 'macOSOnlyDeprecated()', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/FillIntroduced/macOSOnlyIntroduced(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'FillIntroduced', - topLevelSymbol: (name: 'macOSOnlyIntroduced()', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/MyKit: - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyKit', suffix: (none)), - representsModule: true, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/MyClass: - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyClass', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/MyClass/init()-33vaw: - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'init()', suffix: (idHash: '33vaw'))] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/MyClass/init()-3743d: - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'init()', suffix: (idHash: '3743d'))] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/MyClass/myFunction(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'myFunction()', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/MyProtocol: - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'MyProtocol', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/MyKit/globalFunction(_:considering:): - """ - { - bundleID: 'org.swift.docc.example', - module: 'MyKit', - topLevelSymbol: (name: 'globalFunction(_:considering:)', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/SideKit: - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideKit', suffix: (none)), - representsModule: true, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideClass: - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideClass', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideClass/Element: - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'Element', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideClass/Element/inherited(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'Element', suffix: (none)), (name: 'inherited()', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideClass/Value(_:): - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'Value(_:)', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideClass/init(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'init()', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideClass/myFunction(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'myFunction()', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideClass/path: - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'path', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideClass/url: - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'url', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideProtocol: - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideProtocol', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func(): - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideProtocol', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'func()', suffix: (none))] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func()-2dxqn: - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'SideProtocol', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'func()', suffix: (idHash: '2dxqn'))] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/UncuratedClass: - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'UncuratedClass', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://org.swift.docc.example/documentation/SideKit/UncuratedClass/angle: - """ - { - bundleID: 'org.swift.docc.example', - module: 'SideKit', - topLevelSymbol: (name: 'UncuratedClass', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'angle', suffix: (none))] - } - """, - ] - XCTAssertEqual(expectedDescriptions.count, context.documentationCache.symbolReferences.count) - - let validatedSymbolLinkDescriptions = context.documentationCache.symbolReferences - .map(\.url.absoluteString) - .sorted() - .compactMap(AbsoluteSymbolLink.init(string:)) - .map(\.description) - - XCTAssertEqual(validatedSymbolLinkDescriptions.count, context.documentationCache.symbolReferences.count) - for (symbolLinkDescription, expectedDescription) in zip(validatedSymbolLinkDescriptions, expectedDescriptions) { - XCTAssertEqual(symbolLinkDescription, expectedDescription) - } - } - - func testCompileOverloadedSymbolGraphAndValidateLinks() throws { - let (_, _, context) = try testBundleAndContext(named: "OverloadedSymbols") - - let expectedDescriptions = [ - // doc://com.shapes.ShapeKit/documentation/ShapeKit: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'ShapeKit', suffix: (none)), - representsModule: true, - basePathComponents: [] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedByCaseStruct: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedByCaseStruct', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedByCaseStruct/ThirdTestMemberName-5vyx9: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedByCaseStruct', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'ThirdTestMemberName', suffix: (idHash: '5vyx9'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedByCaseStruct/thirdTestMemberNamE-4irjn: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedByCaseStruct', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'thirdTestMemberNamE', suffix: (idHash: '4irjn'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedByCaseStruct/thirdTestMemberName-8x5kx: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedByCaseStruct', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'thirdTestMemberName', suffix: (idHash: '8x5kx'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedByCaseStruct/thirdtestMemberName-u0gl: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedByCaseStruct', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'thirdtestMemberName', suffix: (idHash: 'u0gl'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedEnum', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14g8s: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedEnum', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'firstTestMemberName(_:)', suffix: (idHash: '14g8s'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14ife: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedEnum', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'firstTestMemberName(_:)', suffix: (idHash: '14ife'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14ob0: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedEnum', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'firstTestMemberName(_:)', suffix: (idHash: '14ob0'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-4ja8m: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedEnum', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'firstTestMemberName(_:)', suffix: (idHash: '4ja8m'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-88rbf: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedEnum', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'firstTestMemberName(_:)', suffix: (idHash: '88rbf'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-swift.enum.case: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedEnum', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'firstTestMemberName(_:)', suffix: (kind: 'swift.enum.case'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedParentStruct-1jr3p: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedParentStruct', suffix: (idHash: '1jr3p')), - representsModule: false, - basePathComponents: [] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedParentStruct-1jr3p/fifthTestMember: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedParentStruct', suffix: (idHash: '1jr3p')), - representsModule: false, - basePathComponents: [(name: 'fifthTestMember', suffix: (none))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedProtocol', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-1h173: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedProtocol', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'fourthTestMemberName(test:)', suffix: (idHash: '1h173'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-8iuz7: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedProtocol', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'fourthTestMemberName(test:)', suffix: (idHash: '8iuz7'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-91hxs: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedProtocol', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'fourthTestMemberName(test:)', suffix: (idHash: '91hxs'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-961zx: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedProtocol', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'fourthTestMemberName(test:)', suffix: (idHash: '961zx'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedStruct: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedStruct', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedStruct/secondTestMemberName-swift.property: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedStruct', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'secondTestMemberName', suffix: (kind: 'swift.property'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedStruct/secondTestMemberName-swift.type.property: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'OverloadedStruct', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'secondTestMemberName', suffix: (kind: 'swift.type.property'))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/RegularParent: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'RegularParent', suffix: (none)), - representsModule: false, - basePathComponents: [] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/RegularParent/FourthMember: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'RegularParent', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'FourthMember', suffix: (none))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/RegularParent/firstMember: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'RegularParent', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'firstMember', suffix: (none))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/RegularParent/secondMember(first:second:): - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'RegularParent', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'secondMember(first:second:)', suffix: (none))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/RegularParent/thirdMember: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'RegularParent', suffix: (none)), - representsModule: false, - basePathComponents: [(name: 'thirdMember', suffix: (none))] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/overloadedparentstruct-6a7lx: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'overloadedparentstruct', suffix: (idHash: '6a7lx')), - representsModule: false, - basePathComponents: [] - } - """, - // doc://com.shapes.ShapeKit/documentation/ShapeKit/overloadedparentstruct-6a7lx/fifthTestMember: - """ - { - bundleID: 'com.shapes.ShapeKit', - module: 'ShapeKit', - topLevelSymbol: (name: 'overloadedparentstruct', suffix: (idHash: '6a7lx')), - representsModule: false, - basePathComponents: [(name: 'fifthTestMember', suffix: (none))] - } - """, - ] - - XCTAssertEqual(expectedDescriptions.count, context.documentationCache.count) - - let validatedSymbolLinkDescriptions = context.documentationCache.allReferences - .map(\.url.absoluteString) - .sorted() - .compactMap(AbsoluteSymbolLink.init(string:)) - .map(\.description) - - XCTAssertEqual(validatedSymbolLinkDescriptions.count, context.documentationCache.count) - for (symbolLinkDescription, expectedDescription) in zip(validatedSymbolLinkDescriptions, expectedDescriptions) { - XCTAssertEqual(symbolLinkDescription, expectedDescription) - } - } - - func testLinkComponentStringConversion() throws { - let (_, _, context) = try testBundleAndContext(named: "OverloadedSymbols") - - let bundlePathComponents = context.documentationCache.allReferences - .flatMap(\.pathComponents) - - - bundlePathComponents.forEach { component in - let symbolLinkComponent = AbsoluteSymbolLink.LinkComponent(string: component) - // Assert that round-trip conversion doesn't change the string representation - // of the component - XCTAssertEqual(symbolLinkComponent?.asLinkComponentString, component) - } - } -} diff --git a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/DocCSymbolRepresentableTests.swift b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/DocCSymbolRepresentableTests.swift deleted file mode 100644 index d18bf2da17..0000000000 --- a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/DocCSymbolRepresentableTests.swift +++ /dev/null @@ -1,205 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021-2024 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 Foundation -import XCTest -import SymbolKit -@testable import SwiftDocC - -// This test uses ``DocCSymbolRepresentable`` and ``AbsoluteSymbolLink`` which are deprecated. -// Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") -class DocCSymbolRepresentableTests: XCTestCase { - func testDisambiguatedByType() throws { - try performOverloadSymbolDisambiguationTest( - correctLink: """ - doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedStruct/secondTestMemberName-swift.property - """, - incorrectLinks: [ - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedStruct/secondTestMemberName-swift.method", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedStruct/secondTestMemberName-swift.enum.case", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedStruct/secondTestMemberName", - ], - symbolTitle: "secondTestMemberName", - expectedNumberOfAmbiguousSymbols: 2 - ) - } - - func testOverloadedByCaseInsensitivity() throws { - try performOverloadSymbolDisambiguationTest( - correctLink: """ - doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedByCaseStruct/ThirdTestMemberName-5vyx9 - """, - incorrectLinks: [ - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedByCaseStruct/ThirdTestMemberName", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedByCaseStruct/ThirdTestMemberName-swift.enum.case", - ], - symbolTitle: "thirdtestmembername", - expectedNumberOfAmbiguousSymbols: 4 - ) - } - - func testProtocolMemberWithUSRHash() throws { - try performOverloadSymbolDisambiguationTest( - correctLink: """ - doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-961zx - """, - incorrectLinks: [ - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthtestmembername(test:)-961zx", - ], - symbolTitle: "fourthTestMemberName(test:)", - expectedNumberOfAmbiguousSymbols: 4 - ) - } - - func testFunctionWithKindIdentifierAndUSRHash() throws { - try performOverloadSymbolDisambiguationTest( - correctLink: """ - doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14g8s - """, - incorrectLinks: [ - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-swift.method-14g8s", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-swift.method", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedEnum/firstTestMemberName(_:)", - ], - symbolTitle: "firstTestMemberName(_:)", - expectedNumberOfAmbiguousSymbols: 6 - ) - } - - func testSymbolWithNoDisambiguation() throws { - try performOverloadSymbolDisambiguationTest( - correctLink: """ - doc://com.shapes.ShapeKit/documentation/ShapeKit/RegularParent/firstMember - """, - incorrectLinks: [ - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/firstMember-961zx", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/firstMember-swift.property", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/firstMember-swift.property-961zx", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/RegularParent/firstmember", - ], - symbolTitle: "firstMember", - expectedNumberOfAmbiguousSymbols: 1 - ) - } - - func testAmbiguousProtocolMember() throws { - try performOverloadSymbolDisambiguationTest( - correctLink: """ - doc://com.shapes.ShapeKit/documentation/ShapeKit/RegularParent/firstMember - """, - incorrectLinks: [ - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/firstMember-961zx", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/firstMember-swift.property", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/firstMember-swift.property-961zx", - "doc://com.shapes.ShapeKit/documentation/ShapeKit/RegularParent/firstmember", - ], - symbolTitle: "firstMember", - expectedNumberOfAmbiguousSymbols: 1 - ) - } - - func performOverloadSymbolDisambiguationTest( - correctLink: String, - incorrectLinks: [String], - symbolTitle: String, - expectedNumberOfAmbiguousSymbols: Int - ) throws { - // Build a bundle with an unusual number of overloaded symbols - let (_, _, context) = try testBundleAndContext(named: "OverloadedSymbols") - - // Collect the overloaded symbols nodes from the built bundle - let ambiguousSymbols = context.documentationCache - .compactMap { $0.value.symbol } - .filter { $0.names.title.lowercased() == symbolTitle.lowercased() } - XCTAssertEqual(ambiguousSymbols.count, expectedNumberOfAmbiguousSymbols) - - // Find the documentation node based on what we expect the correct link to be - let correctReferenceToSelect = try XCTUnwrap( - context.documentationCache.allReferences.first(where: { $0.absoluteString == correctLink }) - ) - let correctSymbolToSelect = try XCTUnwrap( - context.documentationCache[correctReferenceToSelect]?.symbol - ) - - // First confirm the first link does resolve as expected - do { - // Build an absolute symbol link - let absoluteSymbolLinkLastPathComponent = try XCTUnwrap( - AbsoluteSymbolLink(string: correctLink)?.basePathComponents.last - ) - - // Pass it all of the ambiguous symbols to disambiguate between - let selectedSymbols = absoluteSymbolLinkLastPathComponent.disambiguateBetweenOverloadedSymbols( - ambiguousSymbols - ) - - // Assert that it selects a single symbol - XCTAssertEqual(selectedSymbols.count, 1) - - // Assert that the correct symbol is selected - let selectedSymbol = try XCTUnwrap(selectedSymbols.first) - XCTAssertEqual(correctSymbolToSelect, selectedSymbol) - } - - // Now we'll try a couple of imprecise links and verify they don't resolve - try incorrectLinks.forEach { incorrectLink in - let absoluteSymbolLinkLastPathComponent = try XCTUnwrap( - AbsoluteSymbolLink(string: incorrectLink)?.basePathComponents.last - ) - - // Pass it all of the ambiguous symbols to disambiguate between - let selectedSymbols = absoluteSymbolLinkLastPathComponent.disambiguateBetweenOverloadedSymbols( - ambiguousSymbols - ) - - // We expect it to return an empty array since the given - // absolute symbol link isn't correct - XCTAssertTrue(selectedSymbols.isEmpty) - } - } - - func testLinkComponentInitialization() throws { - let (_, _, context) = try testBundleAndContext(named: "OverloadedSymbols") - - var count = 0 - for (reference, documentationNode) in context.documentationCache { - guard let symbolLink = AbsoluteSymbolLink(string: reference.absoluteString) else { - continue - } - - // The `asLinkComponent` property of DocCSymbolRepresentable doesn't have the context - // to know what type disambiguation information it should use, so it always includes - // all the available disambiguation information. Because of this, - // we want to restrict to symbols that require both. - guard case .kindAndPreciseIdentifier = symbolLink.basePathComponents.last?.disambiguationSuffix else { - continue - } - - // Create a link component from the symbol information - let linkComponent = try XCTUnwrap(documentationNode.symbol?.asLinkComponent) - - // Confirm that link component we created is the same on the compiler - // created in a full documentation build. - XCTAssertEqual( - linkComponent.asLinkComponentString, - documentationNode.reference.lastPathComponent - ) - - count += 1 - } - - // It's not necessary to disambiguate with both kind and usr. - XCTAssertEqual(count, 0) - } -} diff --git a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/LinkCompletionToolsTests.swift b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/LinkCompletionToolsTests.swift index 6cc34779d6..d267afc81f 100644 --- a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/LinkCompletionToolsTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/LinkCompletionToolsTests.swift @@ -10,7 +10,7 @@ import Foundation import XCTest -@_spi(LinkCompletion) @testable import SwiftDocC +@testable import SwiftDocC class LinkCompletionToolsTests: XCTestCase { func testParsingLinkStrings() { @@ -237,4 +237,25 @@ class LinkCompletionToolsTests: XCTestCase { "->_", // The only overload that returns something ]) } + + func testRemovesWhitespaceFromTypeSignatureDisambiguation() { + let overloads = [ + // The caller included whitespace in these closure type spellings but the DocC disambiguation won't include this whitespace. + (parameters: ["(Int) -> Int"], returns: []), // ((Int) -> Int) -> Void + (parameters: ["(Bool) -> ()"], returns: []), // ((Bool) -> () ) -> Void + ].map { + LinkCompletionTools.SymbolInformation( + kind: "func", + symbolIDHash: "\($0)".stableHashString, + parameterTypes: $0.parameters, + returnTypes: $0.returns + ) + } + + XCTAssertEqual(LinkCompletionTools.suggestedDisambiguation(forCollidingSymbols: overloads), [ + // Both parameters require the only parameter type as disambiguation. The suggested disambiguation shouldn't contain extra whitespace. + "-((Int)->Int)", + "-((Bool)->())", + ]) + } } diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolBreadcrumbTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolBreadcrumbTests.swift index 0bfbd29004..0389cea89f 100644 --- a/Tests/SwiftDocCTests/Infrastructure/SymbolBreadcrumbTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolBreadcrumbTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -12,8 +12,8 @@ import XCTest @testable import SwiftDocC class SymbolBreadcrumbTests: XCTestCase { - func testLanguageSpecificBreadcrumbs() throws { - let (bundle, context) = try testBundleAndContext(named: "GeometricalShapes") + func testLanguageSpecificBreadcrumbs() async throws { + let (bundle, context) = try await testBundleAndContext(named: "GeometricalShapes") let resolver = try XCTUnwrap(context.linkResolver.localResolver) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -96,8 +96,8 @@ class SymbolBreadcrumbTests: XCTestCase { } } - func testMixedLanguageSpecificBreadcrumbs() throws { - let (bundle, context) = try testBundleAndContext(named: "MixedLanguageFramework") + func testMixedLanguageSpecificBreadcrumbs() async throws { + let (bundle, context) = try await testBundleAndContext(named: "MixedLanguageFramework") let resolver = try XCTUnwrap(context.linkResolver.localResolver) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -140,7 +140,7 @@ class SymbolBreadcrumbTests: XCTestCase { file: StaticString = #filePath, line: UInt = #line ) { - var hierarchyTranslator = RenderHierarchyTranslator(context: context, bundle: bundle) + var hierarchyTranslator = RenderHierarchyTranslator(context: context) let hierarchyVariants = hierarchyTranslator.visitSymbol(reference) XCTAssertNotNil(hierarchyVariants.defaultValue, "Should always have default breadcrumbs", file: file, line: line) @@ -154,7 +154,7 @@ class SymbolBreadcrumbTests: XCTestCase { file: StaticString = #filePath, line: UInt = #line ) { - var hierarchyTranslator = RenderHierarchyTranslator(context: context, bundle: bundle) + var hierarchyTranslator = RenderHierarchyTranslator(context: context) let hierarchyVariants = hierarchyTranslator.visitSymbol(reference) XCTAssertNotNil(hierarchyVariants.defaultValue, "Should always have default breadcrumbs", file: file, line: line) diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolDisambiguationTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolDisambiguationTests.swift index b838e90664..160722cda1 100644 --- a/Tests/SwiftDocCTests/Infrastructure/SymbolDisambiguationTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolDisambiguationTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -14,8 +14,8 @@ import SymbolKit class SymbolDisambiguationTests: XCTestCase { - func testPathCollisionWithDifferentTypesInSameLanguage() throws { - let references = try disambiguatedReferencesForSymbols( + func testPathCollisionWithDifferentTypesInSameLanguage() async throws { + let references = try await disambiguatedReferencesForSymbols( swift: [ TestSymbolData(preciseID: "first", pathComponents: ["Something", "first"], kind: .property), TestSymbolData(preciseID: "second", pathComponents: ["Something", "First"], kind: .struct), @@ -36,8 +36,8 @@ class SymbolDisambiguationTests: XCTestCase { ) } - func testPathCollisionWithDifferentArgumentTypesInSameLanguage() throws { - let references = try disambiguatedReferencesForSymbols( + func testPathCollisionWithDifferentArgumentTypesInSameLanguage() async throws { + let references = try await disambiguatedReferencesForSymbols( swift: [ // The argument type isn't represented in the symbol name in the path components TestSymbolData(preciseID: "first", pathComponents: ["Something", "first(_:)"], kind: .method), @@ -59,8 +59,8 @@ class SymbolDisambiguationTests: XCTestCase { ) } - func testSameSymbolWithDifferentKindsInDifferentLanguages() throws { - let references = try disambiguatedReferencesForSymbols( + func testSameSymbolWithDifferentKindsInDifferentLanguages() async throws { + let references = try await disambiguatedReferencesForSymbols( swift: [ TestSymbolData(preciseID: "first", pathComponents: ["Something", "First"], kind: .enum), ], @@ -77,8 +77,8 @@ class SymbolDisambiguationTests: XCTestCase { ) } - func testDifferentSymbolsWithDifferentKindsInDifferentLanguages() throws { - let references = try disambiguatedReferencesForSymbols( + func testDifferentSymbolsWithDifferentKindsInDifferentLanguages() async throws { + let references = try await disambiguatedReferencesForSymbols( swift: [ TestSymbolData(preciseID: "first", pathComponents: ["Something", "First"], kind: .struct), ], @@ -98,8 +98,8 @@ class SymbolDisambiguationTests: XCTestCase { ) } - func testSameSymbolWithDifferentNamesInDifferentLanguages() throws { - let references = try disambiguatedReferencesForSymbols( + func testSameSymbolWithDifferentNamesInDifferentLanguages() async throws { + let references = try await disambiguatedReferencesForSymbols( swift: [ TestSymbolData(preciseID: "first", pathComponents: ["Something", "first(one:two:)"], kind: .method), ], @@ -116,8 +116,8 @@ class SymbolDisambiguationTests: XCTestCase { ) } - func testOneVariantOfMultiLanguageSymbolCollidesWithDifferentTypeSymbol() throws { - let references = try disambiguatedReferencesForSymbols( + func testOneVariantOfMultiLanguageSymbolCollidesWithDifferentTypeSymbol() async throws { + let references = try await disambiguatedReferencesForSymbols( swift: [ TestSymbolData(preciseID: "instance-method", pathComponents: ["Something", "first(one:two:)"], kind: .method), TestSymbolData(preciseID: "type-method", pathComponents: ["Something", "first(one:two:)"], kind: .typeMethod), @@ -139,8 +139,8 @@ class SymbolDisambiguationTests: XCTestCase { ) } - func testStructAndEnumAndTypeAliasCollisionOfSameSymbol() throws { - let references = try disambiguatedReferencesForSymbols( + func testStructAndEnumAndTypeAliasCollisionOfSameSymbol() async throws { + let references = try await disambiguatedReferencesForSymbols( swift: [ TestSymbolData(preciseID: "first", pathComponents: ["Something", "First"], kind: .struct), ], @@ -161,8 +161,8 @@ class SymbolDisambiguationTests: XCTestCase { ) } - func testTripleCollisionWithBothSameTypeAndDifferentType() throws { - let references = try disambiguatedReferencesForSymbols( + func testTripleCollisionWithBothSameTypeAndDifferentType() async throws { + let references = try await disambiguatedReferencesForSymbols( swift: [ TestSymbolData(preciseID: "first", pathComponents: ["Something", "first(_:_:)"], kind: .method), TestSymbolData(preciseID: "second", pathComponents: ["Something", "first(_:_:)"], kind: .typeMethod), @@ -188,13 +188,13 @@ class SymbolDisambiguationTests: XCTestCase { ) } - func testMixedLanguageFramework() throws { - let (bundle, context) = try testBundleAndContext(named: "MixedLanguageFramework") + func testMixedLanguageFramework() async throws { + let (inputs, context) = try await testBundleAndContext(named: "MixedLanguageFramework") - var loader = SymbolGraphLoader(bundle: bundle, dataLoader: { try context.contentsOfURL($0, in: $1) }) + var loader = SymbolGraphLoader(bundle: inputs, dataProvider: context.dataProvider) try loader.loadAll() - let references = context.linkResolver.localResolver.referencesForSymbols(in: loader.unifiedGraphs, bundle: bundle, context: context).mapValues(\.path) + let references = context.linkResolver.localResolver.referencesForSymbols(in: loader.unifiedGraphs, context: context).mapValues(\.path) XCTAssertEqual(Set(references.keys), [ SymbolGraph.Symbol.Identifier(precise: "c:@CM@TestFramework@objc(cs)MixedLanguageClassConformingToProtocol(im)mixedLanguageMethod", interfaceLanguage: "swift"), .init(precise: "c:@E@Foo", interfaceLanguage: "swift"), @@ -265,7 +265,7 @@ class SymbolDisambiguationTests: XCTestCase { let kind: SymbolGraph.Symbol.KindIdentifier } - private func disambiguatedReferencesForSymbols(swift swiftSymbols: [TestSymbolData], objectiveC objectiveCSymbols: [TestSymbolData]) throws -> [SymbolGraph.Symbol.Identifier : ResolvedTopicReference] { + private func disambiguatedReferencesForSymbols(swift swiftSymbols: [TestSymbolData], objectiveC objectiveCSymbols: [TestSymbolData]) async throws -> [SymbolGraph.Symbol.Identifier : ResolvedTopicReference] { let graph = SymbolGraph( metadata: SymbolGraph.Metadata( formatVersion: SymbolGraph.SemanticVersion(major: 1, minor: 1, patch: 1), @@ -321,7 +321,7 @@ class SymbolDisambiguationTests: XCTestCase { let uniqueSymbolCount = Set(swiftSymbols.map(\.preciseID) + objectiveCSymbols.map(\.preciseID)).count XCTAssertEqual(unified.symbols.count, uniqueSymbolCount) - let bundle = DocumentationBundle( + let inputs = DocumentationBundle( info: DocumentationBundle.Info( displayName: "SymbolDisambiguationTests", id: "com.test.SymbolDisambiguationTests"), @@ -335,8 +335,8 @@ class SymbolDisambiguationTests: XCTestCase { objcSymbolGraphURL: try JSONEncoder().encode(graph2), ], fallback: nil) - let context = try DocumentationContext(bundle: bundle, dataProvider: provider) + let context = try await DocumentationContext(bundle: inputs, dataProvider: provider) - return context.linkResolver.localResolver.referencesForSymbols(in: ["SymbolDisambiguationTests": unified], bundle: bundle, context: context) + return context.linkResolver.localResolver.referencesForSymbols(in: ["SymbolDisambiguationTests": unified], context: context) } } diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift index 4a97349758..39ad7e8482 100644 --- a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphLoaderTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -146,9 +146,9 @@ class SymbolGraphLoaderTests: XCTestCase { } /// Tests if we detect correctly a Mac Catalyst graph - func testLoadingiOSAndCatalystGraphs() throws { - func testBundleCopy(iOSSymbolGraphName: String, catalystSymbolGraphName: String) throws -> (URL, DocumentationBundle, DocumentationContext) { - return try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", configureBundle: { bundleURL in + func testLoadingiOSAndCatalystGraphs() async throws { + func testBundleCopy(iOSSymbolGraphName: String, catalystSymbolGraphName: String) async throws -> (URL, DocumentationBundle, DocumentationContext) { + return try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", configureBundle: { bundleURL in // Create an iOS symbol graph file let iOSGraphURL = bundleURL.appendingPathComponent("mykit-iOS.symbols.json") let renamediOSGraphURL = bundleURL.appendingPathComponent(iOSSymbolGraphName) @@ -177,7 +177,7 @@ class SymbolGraphLoaderTests: XCTestCase { do { // We rename the iOS graph file to contain a "@" which makes it being loaded after main symbol graphs // to simulate the loading order we want to test. - let (_, _, context) = try testBundleCopy(iOSSymbolGraphName: "faux@MyKit.symbols.json", catalystSymbolGraphName: "MyKit.symbols.json") + let (_, _, context) = try await testBundleCopy(iOSSymbolGraphName: "faux@MyKit.symbols.json", catalystSymbolGraphName: "MyKit.symbols.json") guard let availability = (context.documentationCache["s:5MyKit0A5ClassC"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 's:5MyKit0A5ClassC'") @@ -197,7 +197,7 @@ class SymbolGraphLoaderTests: XCTestCase { do { // We rename the Mac Catalyst graph file to contain a "@" which makes it being loaded after main symbol graphs // to simulate the loading order we want to test. - let (_, _, context) = try testBundleCopy(iOSSymbolGraphName: "MyKit.symbols.json", catalystSymbolGraphName: "faux@MyKit.symbols.json") + let (_, _, context) = try await testBundleCopy(iOSSymbolGraphName: "MyKit.symbols.json", catalystSymbolGraphName: "faux@MyKit.symbols.json") guard let availability = (context.documentationCache["s:5MyKit0A5ClassC"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 's:5MyKit0A5ClassC'") @@ -213,8 +213,8 @@ class SymbolGraphLoaderTests: XCTestCase { } // Tests if main and bystanders graphs are loaded - func testLoadingModuleBystanderExtensions() throws { - let (_, bundle, _) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [:]) { url in + func testLoadingModuleBystanderExtensions() async throws { + let (_, bundle, _) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", externalResolvers: [:]) { url in let bystanderSymbolGraphURL = Bundle.module.url( forResource: "MyKit@Foundation@_MyKit_Foundation.symbols", withExtension: "json", subdirectory: "Test Resources")! try FileManager.default.copyItem(at: bystanderSymbolGraphURL, to: url.appendingPathComponent("MyKit@Foundation@_MyKit_Foundation.symbols.json")) @@ -381,7 +381,7 @@ class SymbolGraphLoaderTests: XCTestCase { ) } - func testDefaulAvailabilityWhenMissingSGFs() throws { + func testDefaulAvailabilityWhenMissingSGFs() async throws { // Symbol from SGF let symbol = """ { @@ -453,7 +453,7 @@ class SymbolGraphLoaderTests: XCTestCase { let infoPlistURL = targetURL.appendingPathComponent("Info.plist") try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - var (_, _, context) = try loadBundle(from: targetURL) + var (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -478,7 +478,7 @@ class SymbolGraphLoaderTests: XCTestCase { """ try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - (_, _, context) = try loadBundle(from: targetURL) + (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -519,7 +519,7 @@ class SymbolGraphLoaderTests: XCTestCase { """ try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - (_, _, context) = try loadBundle(from: targetURL) + (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -531,7 +531,7 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertTrue(availability.first(where: { $0.domain?.rawValue == "iPadOS" })?.introducedVersion == SymbolGraph.SemanticVersion(major: 8, minor: 0, patch: 0)) } - func testFallbackAvailabilityVersion() throws { + func testFallbackAvailabilityVersion() async throws { // Symbol from SG let symbol = """ { @@ -582,7 +582,7 @@ class SymbolGraphLoaderTests: XCTestCase { let symbolGraphURL = targetURL.appendingPathComponent("MyModule.symbols.json") try symbolGraphString.write(to: symbolGraphURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, _, context) = try loadBundle(from: targetURL) + let (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -595,7 +595,7 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iPadOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 12, minor: 0, patch: 0)) } - func testFallbackPlatformsDontOverrideSourceAvailability() throws { + func testFallbackPlatformsDontOverrideSourceAvailability() async throws { // Symbol from SG let symbolGraphStringiOS = makeSymbolGraphString( moduleName: "MyModule", @@ -687,7 +687,7 @@ class SymbolGraphLoaderTests: XCTestCase { try symbolGraphStringiOS.write(to: targetURL.appendingPathComponent("MyModule-ios.symbols.json"), atomically: true, encoding: .utf8) try symbolGraphStringCatalyst.write(to: targetURL.appendingPathComponent("MyModule-catalyst.symbols.json"), atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, _, context) = try loadBundle(from: targetURL) + let (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -702,7 +702,7 @@ class SymbolGraphLoaderTests: XCTestCase { } - func testDefaultAvailabilityDontOverrideSourceAvailability() throws { + func testDefaultAvailabilityDontOverrideSourceAvailability() async throws { // Symbol from SGF let iosSymbolGraphString = makeSymbolGraphString( moduleName: "MyModule", @@ -824,7 +824,7 @@ class SymbolGraphLoaderTests: XCTestCase { let infoPlistURL = targetURL.appendingPathComponent("Info.plist") try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, _, context) = try loadBundle(from: targetURL) + let (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -838,7 +838,7 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "iPadOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 12, minor: 0, patch: 0)) } - func testDefaultAvailabilityFillSourceAvailability() throws { + func testDefaultAvailabilityFillSourceAvailability() async throws { // Symbol from SGF let symbol = """ { @@ -921,7 +921,7 @@ class SymbolGraphLoaderTests: XCTestCase { let infoPlistURL = targetURL.appendingPathComponent("Info.plist") try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, _, context) = try loadBundle(from: targetURL) + let (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -933,7 +933,7 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "macCatalyst" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 7, minor: 0, patch: 0)) } - func testUnconditionallyunavailablePlatforms() throws { + func testUnconditionallyunavailablePlatforms() async throws { // Create an empty bundle let targetURL = try createTemporaryDirectory(named: "test.docc") // Symbol from SGF @@ -1050,7 +1050,7 @@ class SymbolGraphLoaderTests: XCTestCase { let infoPlistURL = targetURL.appendingPathComponent("Info.plist") try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - var (_, _, context) = try loadBundle(from: targetURL) + var (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -1070,7 +1070,7 @@ class SymbolGraphLoaderTests: XCTestCase { platform: """ """ ).write(to: targetURL.appendingPathComponent("MyModule-catalyst.symbols.json"), atomically: true, encoding: .utf8) - (_, _, context) = try loadBundle(from: targetURL) + (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -1084,7 +1084,7 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertNil(availability.first(where: { $0.domain?.rawValue == "iPadOS" })) } - func testSymbolUnavailablePerPlatform() throws { + func testSymbolUnavailablePerPlatform() async throws { // Create an empty bundle let targetURL = try createTemporaryDirectory(named: "test.docc") // Symbol from SGF @@ -1192,7 +1192,7 @@ class SymbolGraphLoaderTests: XCTestCase { """ ).write(to: targetURL.appendingPathComponent("MyModule-catalyst.symbols.json"), atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - var (_, _, context) = try loadBundle(from: targetURL) + var (_, _, context) = try await loadBundle(from: targetURL) guard let availabilityFoo = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -1245,7 +1245,7 @@ class SymbolGraphLoaderTests: XCTestCase { // Create info list let infoPlistURL = targetURL.appendingPathComponent("Info.plist") try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) - (_, _, context) = try loadBundle(from: targetURL) + (_, _, context) = try await loadBundle(from: targetURL) guard let availabilityFoo = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -1262,7 +1262,7 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertNotNil(availabilityBar.first(where: { $0.domain?.rawValue == "macCatalyst" })) } - func testDefaultModuleAvailability() throws { + func testDefaultModuleAvailability() async throws { // Create an empty bundle let targetURL = try createTemporaryDirectory(named: "test.docc") // Symbol from SGF @@ -1319,7 +1319,7 @@ class SymbolGraphLoaderTests: XCTestCase { """ let infoPlistURL = targetURL.appendingPathComponent("Info.plist") try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) - let (_, _, context) = try loadBundle(from: targetURL) + let (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -1328,7 +1328,7 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "macOS" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 10, minor: 0, patch: 0)) } - func testCanonicalPlatformNameUniformity() throws { + func testCanonicalPlatformNameUniformity() async throws { let testBundle = Folder(name: "TestBundle.docc", content: [ TextFile(name: "Info.plist", utf8Content: """ @@ -1444,7 +1444,7 @@ class SymbolGraphLoaderTests: XCTestCase { ]) let tempURL = try createTemporaryDirectory() let bundleURL = try testBundle.write(inside: tempURL) - let (_, _, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -1456,7 +1456,7 @@ class SymbolGraphLoaderTests: XCTestCase { XCTAssertTrue(availability.filter({ $0.domain?.rawValue == "maccatalyst" }).count == 0) } - func testFallbackOverrideDefaultAvailability() throws { + func testFallbackOverrideDefaultAvailability() async throws { // Symbol from SG let symbolGraphStringiOS = makeSymbolGraphString( moduleName: "MyModule", @@ -1568,14 +1568,14 @@ class SymbolGraphLoaderTests: XCTestCase { try symbolGraphStringCatalyst.write(to: targetURL.appendingPathComponent("MyModule-catalyst.symbols.json"), atomically: true, encoding: .utf8) try infoPlist.write(to: targetURL.appendingPathComponent("Info.plist"), atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, _, context) = try loadBundle(from: targetURL) + let (_, _, context) = try await loadBundle(from: targetURL) let availability = try XCTUnwrap((context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability) // Verify we fallback to iOS even if there's default availability for the Catalyst platform. XCTAssertNotNil(availability.first(where: { $0.domain?.rawValue == "iOS" })) XCTAssertEqual(availability.first(where: { $0.domain?.rawValue == "macCatalyst" })?.introducedVersion, SymbolGraph.SemanticVersion(major: 12, minor: 0, patch: 0)) } - func testDefaultAvailabilityWhenMissingFallbackPlatform() throws { + func testDefaultAvailabilityWhenMissingFallbackPlatform() async throws { // Symbol from SG let symbolGraphStringCatalyst = makeSymbolGraphString( moduleName: "MyModule", @@ -1641,7 +1641,7 @@ class SymbolGraphLoaderTests: XCTestCase { try symbolGraphStringCatalyst.write(to: targetURL.appendingPathComponent("MyModule-catalyst.symbols.json"), atomically: true, encoding: .utf8) try infoPlist.write(to: targetURL.appendingPathComponent("Info.plist"), atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, _, context) = try loadBundle(from: targetURL) + let (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@A"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@A'") return @@ -1653,7 +1653,7 @@ class SymbolGraphLoaderTests: XCTestCase { } - func testDefaultAvailabilityWhenSymbolIsNotAvailableForThatPlatform() throws { + func testDefaultAvailabilityWhenSymbolIsNotAvailableForThatPlatform() async throws { // Symbol from SGF let symbolTVOS = """ { @@ -1769,7 +1769,7 @@ class SymbolGraphLoaderTests: XCTestCase { let infoPlistURL = targetURL.appendingPathComponent("Info.plist") try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, _, context) = try loadBundle(from: targetURL) + let (_, _, context) = try await loadBundle(from: targetURL) guard let availability = (context.documentationCache["c:@F@Bar"]?.semantic as? Symbol)?.availability?.availability else { XCTFail("Did not find availability for symbol 'c:@F@Bar'") return @@ -1801,9 +1801,7 @@ class SymbolGraphLoaderTests: XCTestCase { return SymbolGraphLoader( bundle: bundle, - dataLoader: { url, _ in - try FileManager.default.contents(of: url) - }, + dataProvider: FileManager.default, symbolGraphTransformer: configureSymbolGraph ) } diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphRelationshipsBuilderTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphRelationshipsBuilderTests.swift index 784c6d3199..329a53f3b9 100644 --- a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphRelationshipsBuilderTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/SymbolGraphRelationshipsBuilderTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -50,8 +50,8 @@ class SymbolGraphRelationshipsBuilderTests: XCTestCase { private let swiftSelector = UnifiedSymbolGraph.Selector(interfaceLanguage: "swift", platform: nil) - func testImplementsRelationship() throws { - let (bundle, context) = try testBundleAndContext() + func testImplementsRelationship() async throws { + let (bundle, context) = try await testBundleAndContext() var documentationCache = DocumentationContext.ContentCache() let engine = DiagnosticEngine() @@ -64,8 +64,52 @@ class SymbolGraphRelationshipsBuilderTests: XCTestCase { XCTAssertFalse((documentationCache["B"]!.semantic as! Symbol).defaultImplementations.implementations.isEmpty) } - func testConformsRelationship() throws { - let (bundle, _) = try testBundleAndContext() + func testMultipleImplementsRelationships() async throws { + let (bundle, context) = try await testBundleAndContext() + var documentationCache = DocumentationContext.ContentCache() + let engine = DiagnosticEngine() + + let identifierA = SymbolGraph.Symbol.Identifier(precise: "A", interfaceLanguage: SourceLanguage.swift.id) + let identifierB = SymbolGraph.Symbol.Identifier(precise: "B", interfaceLanguage: SourceLanguage.swift.id) + let identifierC = SymbolGraph.Symbol.Identifier(precise: "C", interfaceLanguage: SourceLanguage.swift.id) + + let symbolRefA = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SomeModuleName/A", sourceLanguage: .swift) + let symbolRefB = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SomeModuleName/B", sourceLanguage: .swift) + let symbolRefC = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SomeModuleName/C", sourceLanguage: .swift) + let moduleRef = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SomeModuleName", sourceLanguage: .swift) + + let symbolA = SymbolGraph.Symbol(identifier: identifierA, names: SymbolGraph.Symbol.Names(title: "A", navigator: nil, subHeading: nil, prose: nil), pathComponents: ["SomeModuleName", "A"], docComment: nil, accessLevel: .init(rawValue: "public"), kind: SymbolGraph.Symbol.Kind(parsedIdentifier: .func, displayName: "Function"), mixins: [:]) + let symbolB = SymbolGraph.Symbol(identifier: identifierB, names: SymbolGraph.Symbol.Names(title: "B", navigator: nil, subHeading: nil, prose: nil), pathComponents: ["SomeModuleName", "B"], docComment: nil, accessLevel: .init(rawValue: "public"), kind: SymbolGraph.Symbol.Kind(parsedIdentifier: .func, displayName: "Function"), mixins: [:]) + let symbolC = SymbolGraph.Symbol(identifier: identifierC, names: SymbolGraph.Symbol.Names(title: "C", navigator: nil, subHeading: nil, prose: nil), pathComponents: ["SomeModuleName", "C"], docComment: nil, accessLevel: .init(rawValue: "public"), kind: SymbolGraph.Symbol.Kind(parsedIdentifier: .func, displayName: "Function"), mixins: [:]) + + documentationCache.add( + DocumentationNode(reference: symbolRefA, symbol: symbolA, platformName: "macOS", moduleReference: moduleRef, article: nil, engine: engine), + reference: symbolRefA, + symbolID: "A" + ) + documentationCache.add( + DocumentationNode(reference: symbolRefB, symbol: symbolB, platformName: "macOS", moduleReference: moduleRef, article: nil, engine: engine), + reference: symbolRefB, + symbolID: "B" + ) + documentationCache.add( + DocumentationNode(reference: symbolRefC, symbol: symbolC, platformName: "macOS", moduleReference: moduleRef, article: nil, engine: engine), + reference: symbolRefC, + symbolID: "C" + ) + XCTAssert(engine.problems.isEmpty) + + let edge1 = SymbolGraph.Relationship(source: identifierB.precise, target: identifierA.precise, kind: .defaultImplementationOf, targetFallback: nil) + let edge2 = SymbolGraph.Relationship(source: identifierC.precise, target: identifierA.precise, kind: .defaultImplementationOf, targetFallback: nil) + + SymbolGraphRelationshipsBuilder.addImplementationRelationship(edge: edge1, selector: swiftSelector, in: bundle, context: context, localCache: documentationCache, engine: engine) + SymbolGraphRelationshipsBuilder.addImplementationRelationship(edge: edge2, selector: swiftSelector, in: bundle, context: context, localCache: documentationCache, engine: engine) + + XCTAssertEqual((documentationCache["A"]!.semantic as! Symbol).defaultImplementations.groups.first?.references.map(\.url?.lastPathComponent), ["B", "C"]) + } + + func testConformsRelationship() async throws { + let (bundle, _) = try await testBundleAndContext() var documentationCache = DocumentationContext.ContentCache() let engine = DiagnosticEngine() @@ -93,8 +137,8 @@ class SymbolGraphRelationshipsBuilderTests: XCTestCase { XCTAssertEqual(conforming.destinations.first?.url?.absoluteString, "doc://com.example.test/documentation/SomeModuleName/A") } - func testInheritanceRelationship() throws { - let (bundle, _) = try testBundleAndContext() + func testInheritanceRelationship() async throws { + let (bundle, _) = try await testBundleAndContext() var documentationCache = DocumentationContext.ContentCache() let engine = DiagnosticEngine() @@ -122,8 +166,8 @@ class SymbolGraphRelationshipsBuilderTests: XCTestCase { XCTAssertEqual(inherited.destinations.first?.url?.absoluteString, "doc://com.example.test/documentation/SomeModuleName/A") } - func testInheritanceRelationshipFromOtherFramework() throws { - let (bundle, _) = try testBundleAndContext() + func testInheritanceRelationshipFromOtherFramework() async throws { + let (bundle, _) = try await testBundleAndContext() var documentationCache = DocumentationContext.ContentCache() let engine = DiagnosticEngine() @@ -159,8 +203,8 @@ class SymbolGraphRelationshipsBuilderTests: XCTestCase { }), "Could not fallback for parent in inherits from relationship") } - func testRequirementRelationship() throws { - let (bundle, _) = try testBundleAndContext() + func testRequirementRelationship() async throws { + let (bundle, _) = try await testBundleAndContext() var documentationCache = DocumentationContext.ContentCache() let engine = DiagnosticEngine() @@ -173,8 +217,8 @@ class SymbolGraphRelationshipsBuilderTests: XCTestCase { XCTAssertTrue((documentationCache["A"]!.semantic as! Symbol).isRequired) } - func testOptionalRequirementRelationship() throws { - let (bundle, _) = try testBundleAndContext() + func testOptionalRequirementRelationship() async throws { + let (bundle, _) = try await testBundleAndContext() var documentationCache = DocumentationContext.ContentCache() let engine = DiagnosticEngine() @@ -186,4 +230,36 @@ class SymbolGraphRelationshipsBuilderTests: XCTestCase { // Test default implementation was added XCTAssertFalse((documentationCache["A"]!.semantic as! Symbol).isRequired) } + + func testRequiredAndOptionalRequirementRelationships() async throws { + do { + let (bundle, _) = try await testBundleAndContext() + var documentationCache = DocumentationContext.ContentCache() + let engine = DiagnosticEngine() + + let edge = createSymbols(documentationCache: &documentationCache, bundle: bundle, sourceType: .init(parsedIdentifier: .method, displayName: "Method"), targetType: .init(parsedIdentifier: .protocol, displayName: "Protocol")) + + // Adding the "required" relationship before the "optional" one + SymbolGraphRelationshipsBuilder.addRequirementRelationship(edge: edge, localCache: documentationCache, engine: engine) + SymbolGraphRelationshipsBuilder.addOptionalRequirementRelationship(edge: edge, localCache: documentationCache, engine: engine) + + // Make sure that the "optional" relationship wins + XCTAssertFalse((documentationCache["A"]!.semantic as! Symbol).isRequired) + } + + do { + let (bundle, _) = try await testBundleAndContext() + var documentationCache = DocumentationContext.ContentCache() + let engine = DiagnosticEngine() + + let edge = createSymbols(documentationCache: &documentationCache, bundle: bundle, sourceType: .init(parsedIdentifier: .method, displayName: "Method"), targetType: .init(parsedIdentifier: .protocol, displayName: "Protocol")) + + // Adding the "optional" relationship before the "required" one + SymbolGraphRelationshipsBuilder.addOptionalRequirementRelationship(edge: edge, localCache: documentationCache, engine: engine) + SymbolGraphRelationshipsBuilder.addRequirementRelationship(edge: edge, localCache: documentationCache, engine: engine) + + // Make sure that the "optional" relationship still wins + XCTAssertFalse((documentationCache["A"]!.semantic as! Symbol).isRequired) + } + } } diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolReferenceTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolReferenceTests.swift index f91081fb56..49d4985b93 100644 --- a/Tests/SwiftDocCTests/Infrastructure/SymbolReferenceTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolReferenceTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -105,7 +105,7 @@ class SymbolReferenceTests: XCTestCase { } } - func testCreatesUniquePathsForOverloadSymbols() throws { + func testCreatesUniquePathsForOverloadSymbols() async throws { let testCatalog = Folder(name: "TestCreatesUniquePathsForOverloadSymbols.docc", content: [ InfoPlist(displayName: "TestCreatesUniquePathsForOverloadSymbols", identifier: "com.example.documentation"), Folder(name: "Resources", content: [ @@ -198,7 +198,7 @@ class SymbolReferenceTests: XCTestCase { ]), ]) - let (_, context) = try loadBundle(catalog: testCatalog) + let (_, context) = try await loadBundle(catalog: testCatalog) // The overloads are sorted and all dupes get a hash suffix. XCTAssertEqual( diff --git a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift index 129d866338..d02463b820 100644 --- a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift +++ b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -28,7 +28,9 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { var kind = DocumentationNode.Kind.article var language = SourceLanguage.swift var declarationFragments: SymbolGraph.Symbol.DeclarationFragments? = nil + var navigatorTitle: SymbolGraph.Symbol.DeclarationFragments? = nil var topicImages: [(TopicImage, alt: String)]? = nil + var platforms: [AvailabilityRenderItem]? = nil } // When more tests use this we may find that there's a better way to describe this (for example by separating @@ -52,7 +54,7 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { let entity = entityInfo(path: path) return .success( - ResolvedTopicReference(bundleID: bundleID, path: entity.referencePath,fragment: entity.fragment,sourceLanguage: entity.language) + ResolvedTopicReference(bundleID: bundleID, path: entity.referencePath, fragment: entity.fragment, sourceLanguage: entity.language) ) } } @@ -83,32 +85,21 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { } private func makeNode(for entityInfo: EntityInfo, reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity { - let (kind, role) = DocumentationContentRenderer.renderKindAndRole(entityInfo.kind, semantic: nil) - - let dependencies: RenderReferenceDependencies - if let topicImages = entityInfo.topicImages { - dependencies = .init(imageReferences: topicImages.map { topicImage, altText in - return ImageReference(identifier: topicImage.identifier, altText: altText, imageAsset: assetsToReturn[topicImage.identifier.identifier] ?? .init()) - }) - } else { - dependencies = .init() - } - - return LinkResolver.ExternalEntity( - topicRenderReference: TopicRenderReference( - identifier: .init(reference.absoluteString), - title: entityInfo.title, - abstract: [.text(entityInfo.abstract.format())], - url: "/example" + reference.path, - kind: kind, - role: role, - fragments: entityInfo.declarationFragments?.declarationFragments.map { fragment in - return DeclarationRenderSection.Token(fragment: fragment, identifier: nil) - }, - images: entityInfo.topicImages?.map(\.0) ?? [] - ), - renderReferenceDependencies: dependencies, - sourceLanguages: [entityInfo.language] + LinkResolver.ExternalEntity( + kind: entityInfo.kind, + language: entityInfo.language, + relativePresentationURL: reference.url.withoutHostAndPortAndScheme(), + referenceURL: reference.url, + title: entityInfo.title, + availableLanguages: [entityInfo.language], + platforms: entityInfo.platforms, + subheadingDeclarationFragments: entityInfo.declarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, + navigatorDeclarationFragments: entityInfo.navigatorTitle?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, + topicImages: entityInfo.topicImages?.map(\.0), + references: entityInfo.topicImages?.map { topicImage, altText in + ImageReference(identifier: topicImage.identifier, altText: altText, imageAsset: assetsToReturn[topicImage.identifier.identifier] ?? .init()) + }, + variants: [] ) } } diff --git a/Tests/SwiftDocCTests/Infrastructure/TinySmallValueIntSetTests.swift b/Tests/SwiftDocCTests/Infrastructure/TinySmallValueIntSetTests.swift deleted file mode 100644 index 8f72b8b467..0000000000 --- a/Tests/SwiftDocCTests/Infrastructure/TinySmallValueIntSetTests.swift +++ /dev/null @@ -1,149 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2024 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 XCTest -@testable import SwiftDocC - -class TinySmallValueIntSetTests: XCTestCase { - func testBehavesSameAsSet() { - var tiny = _TinySmallValueIntSet() - var real = Set() - - func AssertEqual(_ lhs: (inserted: Bool, memberAfterInsert: Int), _ rhs: (inserted: Bool, memberAfterInsert: Int), file: StaticString = #filePath, line: UInt = #line) { - XCTAssertEqual(lhs.inserted, rhs.inserted, file: file, line: line) - XCTAssertEqual(lhs.memberAfterInsert, rhs.memberAfterInsert, file: file, line: line) - } - - XCTAssertEqual(tiny.contains(4), real.contains(4)) - AssertEqual(tiny.insert(4), real.insert(4)) - XCTAssertEqual(tiny.contains(4), real.contains(4)) - XCTAssertEqual(tiny.count, real.count) - - AssertEqual(tiny.insert(4), real.insert(4)) - XCTAssertEqual(tiny.contains(4), real.contains(4)) - XCTAssertEqual(tiny.count, real.count) - - AssertEqual(tiny.insert(7), real.insert(7)) - XCTAssertEqual(tiny.contains(7), real.contains(7)) - XCTAssertEqual(tiny.count, real.count) - - XCTAssertEqual(tiny.update(with: 2), real.update(with: 2)) - XCTAssertEqual(tiny.contains(2), real.contains(2)) - XCTAssertEqual(tiny.count, real.count) - - XCTAssertEqual(tiny.remove(9), real.remove(9)) - XCTAssertEqual(tiny.contains(9), real.contains(9)) - XCTAssertEqual(tiny.count, real.count) - - XCTAssertEqual(tiny.remove(4), real.remove(4)) - XCTAssertEqual(tiny.contains(4), real.contains(4)) - XCTAssertEqual(tiny.count, real.count) - - tiny.formUnion([19]) - real.formUnion([19]) - XCTAssertEqual(tiny.contains(19), real.contains(19)) - XCTAssertEqual(tiny.count, real.count) - - tiny.formSymmetricDifference([9]) - real.formSymmetricDifference([9]) - XCTAssertEqual(tiny.contains(7), real.contains(7)) - XCTAssertEqual(tiny.contains(9), real.contains(9)) - XCTAssertEqual(tiny.count, real.count) - - tiny.formIntersection([5,6,7]) - real.formIntersection([5,6,7]) - XCTAssertEqual(tiny.contains(4), real.contains(4)) - XCTAssertEqual(tiny.contains(5), real.contains(5)) - XCTAssertEqual(tiny.contains(6), real.contains(6)) - XCTAssertEqual(tiny.contains(7), real.contains(7)) - XCTAssertEqual(tiny.contains(8), real.contains(8)) - XCTAssertEqual(tiny.contains(9), real.contains(9)) - XCTAssertEqual(tiny.count, real.count) - - tiny.formUnion([11,29]) - real.formUnion([11,29]) - XCTAssertEqual(tiny.contains(11), real.contains(11)) - XCTAssertEqual(tiny.contains(29), real.contains(29)) - XCTAssertEqual(tiny.count, real.count) - - XCTAssertEqual(tiny.isSuperset(of: tiny), real.isSuperset(of: real)) - XCTAssertEqual(tiny.isSuperset(of: []), real.isSuperset(of: [])) - XCTAssertEqual(tiny.isSuperset(of: .init(tiny.dropFirst())), real.isSuperset(of: .init(real.dropFirst()))) - XCTAssertEqual(tiny.isSuperset(of: .init(tiny.dropLast())), real.isSuperset(of: .init(real.dropLast()))) - } - - func testCombinations() { - do { - let tiny: _TinySmallValueIntSet = [0,1,2] - XCTAssertEqual(tiny.combinationsToCheck().map { $0.sorted() }, [ - [0], [1], [2], - [0,1], [0,2], [1,2], - [0,1,2] - ]) - } - - do { - let tiny: _TinySmallValueIntSet = [2,5,9] - XCTAssertEqual(tiny.combinationsToCheck().map { $0.sorted() }, [ - [2], [5], [9], - [2,5], [2,9], [5,9], - [2,5,9] - ]) - } - - do { - let tiny: _TinySmallValueIntSet = [3,4,7,11,15,16] - - let expected: [[Int]] = [ - // 1 elements - [3], [4], [7], [11], [15], [16], - // 2 elements - [3,4], [3,7], [3,11], [3,15], [3,16], - [4,7], [4,11], [4,15], [4,16], - [7,11], [7,15], [7,16], - [11,15], [11,16], - [15,16], - // 3 elements - [3,4,7], [3,4,11], [3,4,15], [3,4,16], [3,7,11], [3,7,15], [3,7,16], [3,11,15], [3,11,16], [3,15,16], - [4,7,11], [4,7,15], [4,7,16], [4,11,15], [4,11,16], [4,15,16], - [7,11,15], [7,11,16], [7,15,16], - [11,15,16], - // 4 elements - [3,4,7,11], [3,4,7,15], [3,4,7,16], [3,4,11,15], [3,4,11,16], [3,4,15,16], [3,7,11,15], [3,7,11,16], [3,7,15,16], [3,11,15,16], - [4,7,11,15], [4,7,11,16], [4,7,15,16], [4,11,15,16], - [7,11,15,16], - // 5 elements - [3,4,7,11,15], [3,4,7,11,16], [3,4,7,15,16], [3,4,11,15,16], [3,7,11,15,16], - [4,7,11,15,16], - // 6 elements - [3,4,7,11,15,16], - ] - let actual = tiny.combinationsToCheck().map { Array($0) } - - XCTAssertEqual(expected.count, actual.count) - - // The order of combinations within a given size doesn't matter. - // It's only important that all combinations of a given size exist and that the sizes are in order. - let expectedBySize = [Int: [[Int]]](grouping: expected, by: \.count).sorted(by: \.key).map(\.value) - let actualBySize = [Int: [[Int]]](grouping: actual, by: \.count).sorted(by: \.key).map(\.value) - - for (expectedForSize, actualForSize) in zip(expectedBySize, actualBySize) { - XCTAssertEqual(expectedForSize.count, actualForSize.count) - - // Comparing [Int] descriptions to allow each same-size combination list to have different orders. - // For example, these two lists of combinations (with the last 2 elements swapped) are considered equivalent: - // [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4] - // [1, 2, 3], [1, 2, 4], [2, 3, 4], [1, 3, 4] - XCTAssertEqual(expectedForSize.map(\.description).sorted(), - actualForSize .map(\.description).sorted()) - } - } - } -} diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index 572f2ea2a3..43bb6f0739 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -13,12 +13,10 @@ import SymbolKit @testable import SwiftDocC import SwiftDocCTestUtilities -class ExternalLinkableTests: XCTestCase { +class LinkDestinationSummaryTests: XCTestCase { - // Write example documentation bundle with a minimal Tutorials page - let catalogHierarchy = Folder(name: "unit-test.docc", content: [ - Folder(name: "Symbols", content: []), - Folder(name: "Resources", content: [ + func testSummaryOfTutorialPage() async throws { + let catalogHierarchy = Folder(name: "unit-test.docc", content: [ TextFile(name: "TechnologyX.tutorial", utf8Content: """ @Tutorials(name: "TechnologyX") { @Intro(title: "Technology X") { @@ -89,16 +87,14 @@ class ExternalLinkableTests: XCTestCase { } } """), - ]), - InfoPlist(displayName: "TestBundle", identifier: "com.test.example"), - ]) - - func testSummaryOfTutorialPage() throws { - let (bundle, context) = try loadBundle(catalog: catalogHierarchy) + InfoPlist(displayName: "TestBundle", identifier: "com.test.example") + ]) + + let (_, context) = try await loadBundle(catalog: catalogHierarchy) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/TestBundle/Tutorial", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/TestBundle/Tutorial", sourceLanguage: .swift)) let renderNode = converter.convert(node) let summaries = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode) @@ -117,7 +113,9 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(pageSummary.platforms, renderNode.metadata.platforms) XCTAssertEqual(pageSummary.redirects, nil) XCTAssertNil(pageSummary.usr, "Only symbols have USRs") - XCTAssertNil(pageSummary.declarationFragments, "Only symbols have declaration fragments") + XCTAssertNil(pageSummary.plainTextDeclaration, "Only symbols have a plain text declaration") + XCTAssertNil(pageSummary.subheadingDeclarationFragments, "Only symbols have subheading declaration fragments") + XCTAssertNil(pageSummary.navigatorDeclarationFragments, "Only symbols have navigator titles") XCTAssertNil(pageSummary.abstract, "There is no text to use as an abstract for the tutorial page") XCTAssertNil(pageSummary.topicImages, "The tutorial page doesn't have any topic images") XCTAssertNil(pageSummary.references, "Since the tutorial page doesn't have any topic images it also doesn't have any references") @@ -135,7 +133,9 @@ class ExternalLinkableTests: XCTestCase { URL(string: "old/path/to/this/landmark")!, ]) XCTAssertNil(sectionSummary.usr, "Only symbols have USRs") - XCTAssertNil(sectionSummary.declarationFragments, "Only symbols have declaration fragments") + XCTAssertNil(sectionSummary.plainTextDeclaration, "Only symbols have a plain text declaration") + XCTAssertNil(sectionSummary.subheadingDeclarationFragments, "Only symbols have subheading declaration fragments") + XCTAssertNil(sectionSummary.navigatorDeclarationFragments, "Only symbols have navigator titles") XCTAssertEqual(sectionSummary.abstract, [ .text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt"), .text(" "), @@ -150,9 +150,9 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(summaries, decoded) } - func testSymbolSummaries() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + func testSymbolSummaries() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let converter = DocumentationNodeConverter(context: context) do { let symbolReference = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit/MyClass", sourceLanguage: .swift) let node = try context.entity(with: symbolReference) @@ -184,11 +184,15 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "class MyClass") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "MyClass", kind: .identifier, identifier: nil), ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .init(text: "MyClassNavigator", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -223,13 +227,17 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ProtocolP") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "protocol MyProtocol : Hashable") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "protocol", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "MyProtocol", kind: .identifier, identifier: nil), .init(text: " : ", kind: .text, identifier: nil), .init(text: "Hashable", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "p:hPP"), ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .init(text: "MyProtocol", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -254,7 +262,8 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "func myFunction(for name...)") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "myFunction", kind: .identifier, identifier: nil), @@ -265,6 +274,7 @@ class ExternalLinkableTests: XCTestCase { .init(text: "...", kind: .text, identifier: nil), .init(text: ")", kind: .text, identifier: nil) ]) + XCTAssertNil(summary.navigatorDeclarationFragments, "This symbol doesn't have a navigator title") XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -289,13 +299,24 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit14globalFunction_11consideringy10Foundation4DataV_SitF") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "func globalFunction(_: Data, considering: Int)") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "globalFunction", kind: .identifier, identifier: nil), .init(text: "(", kind: .text, identifier: nil), - .init(text: "_", kind: .identifier, identifier: nil), + .init(text: "Data", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:10Foundation4DataV"), + .init(text: ", ", kind: .text, identifier: nil), + .init(text: "considering", kind: .identifier, identifier: nil), .init(text: ": ", kind: .text, identifier: nil), + .init(text: "Int", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:Si"), + .init(text: ")", kind: .text, identifier: nil) + ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .init(text: "func", kind: .keyword, identifier: nil), + .init(text: " ", kind: .text, identifier: nil), + .init(text: "globalFunction", kind: .identifier, identifier: nil), + .init(text: "(", kind: .text, identifier: nil), .init(text: "Data", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:10Foundation4DataV"), .init(text: ", ", kind: .text, identifier: nil), .init(text: "considering", kind: .identifier, identifier: nil), @@ -312,8 +333,8 @@ class ExternalLinkableTests: XCTestCase { } } - func testTopicImageReferences() throws { - let (url, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testTopicImageReferences() async throws { + let (url, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in let extensionFile = """ # ``MyKit/MyClass/myFunction()`` @@ -328,7 +349,7 @@ class ExternalLinkableTests: XCTestCase { let fileURL = url.appendingPathComponent("documentation").appendingPathComponent("myFunction.md") try extensionFile.write(to: fileURL, atomically: true, encoding: .utf8) } - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) do { let symbolReference = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift) @@ -346,7 +367,8 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "func myFunction(for name...)") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "myFunction", kind: .identifier, identifier: nil), @@ -357,7 +379,8 @@ class ExternalLinkableTests: XCTestCase { .init(text: "...", kind: .text, identifier: nil), .init(text: ")", kind: .text, identifier: nil) ]) - + XCTAssertNil(summary.navigatorDeclarationFragments, "This symbol doesn't have a navigator title") + XCTAssertEqual(summary.topicImages, [ TopicImage( type: .card, @@ -415,24 +438,24 @@ class ExternalLinkableTests: XCTestCase { summary.references = summary.references?.compactMap { (original: RenderReference) -> (any RenderReference)? in guard var imageRef = original as? ImageReference else { return nil } imageRef.asset.variants = imageRef.asset.variants.mapValues { variant in - return imageRef.destinationURL(for: variant.lastPathComponent, prefixComponent: bundle.id.rawValue) + return imageRef.destinationURL(for: variant.lastPathComponent, prefixComponent: context.inputs.id.rawValue) } imageRef.asset.metadata = .init(uniqueKeysWithValues: imageRef.asset.metadata.map { key, value in - return (imageRef.destinationURL(for: key.lastPathComponent, prefixComponent: bundle.id.rawValue), value) + return (imageRef.destinationURL(for: key.lastPathComponent, prefixComponent: context.inputs.id.rawValue), value) }) return imageRef as (any RenderReference) } - let encoded = try RenderJSONEncoder.makeEncoder(assetPrefixComponent: bundle.id.rawValue).encode(summary) + let encoded = try RenderJSONEncoder.makeEncoder(assetPrefixComponent: context.inputs.id.rawValue).encode(summary) let decoded = try JSONDecoder().decode(LinkDestinationSummary.self, from: encoded) XCTAssertEqual(decoded, summary) } } - func testVariantSummaries() throws { - let (bundle, context) = try testBundleAndContext(named: "MixedLanguageFramework") - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + func testVariantSummaries() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFramework") + let converter = DocumentationNodeConverter(context: context) // Check a symbol that's represented as a class in both Swift and Objective-C do { @@ -458,12 +481,15 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(summary.availableLanguages.sorted(), [.swift, .objectiveC]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "c:objc(cs)Bar") - - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "class Bar") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "Bar", kind: .identifier, identifier: nil) ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .init(text: "Bar", kind: .identifier, identifier: nil) + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -472,21 +498,24 @@ class ExternalLinkableTests: XCTestCase { // Check variant content that is different XCTAssertEqual(variant.language, .objectiveC) - XCTAssertEqual(variant.declarationFragments, [ + XCTAssertEqual(variant.plainTextDeclaration, "@interface Bar : NSObject") + XCTAssertEqual(variant.subheadingDeclarationFragments, [ .init(text: "@interface", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "Bar", kind: .identifier, identifier: nil), .init(text: " : ", kind: .text, identifier: nil), .init(text: "NSObject", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSObject"), ]) - + XCTAssertEqual(variant.navigatorDeclarationFragments, [ + .init(text: "Bar (objective c)", kind: .identifier, identifier: nil), + ]) + // Check variant content that is the same as the summarized element XCTAssertEqual(variant.title, nil) XCTAssertEqual(variant.abstract, nil) XCTAssertEqual(variant.usr, nil) XCTAssertEqual(variant.kind, nil) XCTAssertEqual(variant.taskGroups, nil) - XCTAssertEqual(variant.topicImages, nil) let encoded = try JSONEncoder().encode(summary) let decoded = try JSONDecoder().decode(LinkDestinationSummary.self, from: encoded) @@ -519,23 +548,23 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(summary.availableLanguages.sorted(), [.swift, .objectiveC]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "c:objc(cs)Bar(cm)myStringFunction:error:") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "class func myStringFunction(_ string: String) throws -> String") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "myStringFunction", kind: .identifier, identifier: nil), .init(text: "(", kind: .text, identifier: nil), - .init(text: "_", kind: .externalParam, identifier: nil), - .init(text: " ", kind: .text, identifier: nil), - .init(text: "string", kind: .internalParam, identifier: nil), - .init(text: ": ", kind: .text, identifier: nil), .init(text: "String", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:SS"), .init(text: ") ", kind: .text, identifier: nil), .init(text: "throws", kind: .keyword, identifier: nil), .init(text: " -> ", kind: .text, identifier: nil), .init(text: "String", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:SS") ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .init(text: "myStringFunction:error: (navigator title)", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -545,20 +574,13 @@ class ExternalLinkableTests: XCTestCase { // Check variant content that is different XCTAssertEqual(variant.language, .objectiveC) XCTAssertEqual(variant.title, "myStringFunction:error:") - XCTAssertEqual(variant.declarationFragments, [ - .init(text: "+ (", kind: .text, identifier: nil), - .init(text: "NSString", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSString"), - .init(text: " *) ", kind: .text, identifier: nil), - .init(text: "myStringFunction", kind: .identifier, identifier: nil), - .init(text: ": (", kind: .text, identifier: nil), - .init(text: "NSString", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSString"), - .init(text: " *)string", kind: .text, identifier: nil), - .init(text: "error", kind: .identifier, identifier: nil), - .init(text: ": (", kind: .text, identifier: nil), - .init(text: "NSError", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSError"), - .init(text: " **)error;", kind: .text, identifier: nil) + XCTAssertEqual(variant.plainTextDeclaration, "+ (NSString *) myStringFunction: (NSString *)string error: (NSError **)error;") + XCTAssertEqual(variant.subheadingDeclarationFragments, [ + .init(text: "+ ", kind: .text, identifier: nil), + .init(text: "myStringFunction:error:", kind: .identifier, identifier: nil) ]) - + XCTAssertEqual(variant.navigatorDeclarationFragments, .none, "Navigator title is the same across variants") + // Check variant content that is the same as the summarized element XCTAssertEqual(variant.abstract, nil) XCTAssertEqual(variant.usr, nil) @@ -577,7 +599,6 @@ class ExternalLinkableTests: XCTestCase { ) ] ) - XCTAssertEqual(variant.topicImages, nil) let encoded = try JSONEncoder().encode(summary) let decoded = try JSONDecoder().decode(LinkDestinationSummary.self, from: encoded) @@ -585,6 +606,63 @@ class ExternalLinkableTests: XCTestCase { } } + func testDecodingUnknownKindAndLanguage() throws { + let json = """ + { + "kind" : { + "id" : "kind-id", + "name" : "Kind name", + "isSymbol" : false + }, + "language" : { + "id" : "language-id", + "name" : "Language name", + "idAliases" : [ + "language-alias-id" + ], + "linkDisambiguationID" : "language-id" + }, + "availableLanguages" : [ + "swift", + "data", + { + "id" : "language-id", + "idAliases" : [ + "language-alias-id" + ], + "linkDisambiguationID" : "language-id", + "name" : "Language name" + }, + { + "id" : "language-id-2", + "linkDisambiguationID" : "language-id-2", + "name" : "Other language name" + }, + "occ" + ], + "title" : "Something", + "path" : "/documentation/something", + "referenceURL" : "/documentation/something" + } + """ + + let decoded = try JSONDecoder().decode(LinkDestinationSummary.self, from: Data(json.utf8)) + try assertRoundTripCoding(decoded) + + XCTAssertEqual(decoded.kind, DocumentationNode.Kind(name: "Kind name", id: "kind-id", isSymbol: false)) + XCTAssertEqual(decoded.language, SourceLanguage(name: "Language name", id: "language-id", idAliases: ["language-alias-id"])) + XCTAssertEqual(decoded.availableLanguages, [ + // Known languages + .swift, + .objectiveC, + .data, + + // Custom languages + SourceLanguage(name: "Language name", id: "language-id", idAliases: ["language-alias-id"]), + SourceLanguage(name: "Other language name", id: "language-id-2"), + ]) + } + func testDecodingLegacyData() throws { let legacyData = """ { @@ -635,7 +713,7 @@ class ExternalLinkableTests: XCTestCase { XCTAssertEqual(decoded.title, "ClassName") XCTAssertEqual(decoded.abstract?.plainText, "A brief explanation of my class.") XCTAssertEqual(decoded.relativePresentationURL.absoluteString, "documentation/MyKit/ClassName") - XCTAssertEqual(decoded.declarationFragments, [ + XCTAssertEqual(decoded.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "ClassName", kind: .identifier, identifier: nil), @@ -647,7 +725,7 @@ class ExternalLinkableTests: XCTestCase { } /// Ensure that the task group link summary for overload group pages doesn't overwrite any manual curation. - func testOverloadSymbolsWithManualCuration() throws { + func testOverloadSymbolsWithManualCuration() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) let symbolGraph = SymbolGraph.init( @@ -723,11 +801,11 @@ class ExternalLinkableTests: XCTestCase { JSONFile(name: "MyModule.symbols.json", content: symbolGraph), InfoPlist(displayName: "MyModule", identifier: "com.example.mymodule") ]) - let (bundle, context) = try loadBundle(catalog: catalogHierarchy) + let (_, context) = try await loadBundle(catalog: catalogHierarchy) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyModule/MyClass/myFunc()", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyModule/MyClass/myFunc()", sourceLanguage: .swift)) let renderNode = converter.convert(node) let summaries = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode) @@ -751,4 +829,94 @@ class ExternalLinkableTests: XCTestCase { "doc://com.example.mymodule/documentation/MyModule/MyClass/myFunc()-9a7po", ]) } + + /// Tests that API Collections (articles with Topics sections) are correctly identified as `.collectionGroup` + /// kind in linkable entities, ensuring cross-framework references display the correct icon. + func testAPICollectionKindForLinkDestinationSummary() async throws { + let symbolGraph = makeSymbolGraph( + moduleName: "TestModule", + symbols: [makeSymbol(id: "test-class", kind: .class, pathComponents: ["TestClass"])] + ) + + let catalogHierarchy = Folder(name: "unit-test.docc", content: [ + TextFile(name: "APICollection.md", utf8Content: """ + # API Collection + + This is an API Collection that curates symbols. + + ## Topics + + - ``TestModule/TestClass`` + """), + JSONFile(name: "TestModule.symbols.json", content: symbolGraph) + ]) + + let (_, context) = try await loadBundle(catalog: catalogHierarchy) + let converter = DocumentationNodeConverter(context: context) + + let apiCollectionReference = ResolvedTopicReference( + bundleID: context.inputs.id, + path: "/documentation/unit-test/APICollection", + sourceLanguage: .swift + ) + let node = try context.entity(with: apiCollectionReference) + let renderNode = converter.convert(node) + + let summaries = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode) + let pageSummary = try XCTUnwrap(summaries.first) + + XCTAssertEqual(pageSummary.kind, .collectionGroup) + XCTAssertEqual(pageSummary.title, "API Collection") + XCTAssertEqual(pageSummary.abstract, [.text("This is an API Collection that curates symbols.")]) + + // Verify round-trip encoding preserves the correct kind + try assertRoundTripCoding(pageSummary) + } + + /// Tests that explicit `@PageKind(article)` metadata overrides API Collection detection, + /// ensuring that explicit page kind directives take precedence over automatic detection. + func testExplicitPageKindOverridesAPICollectionDetection() async throws { + let symbolGraph = makeSymbolGraph( + moduleName: "TestModule", + symbols: [makeSymbol(id: "test-class", kind: .class, pathComponents: ["TestClass"])] + ) + + let catalogHierarchy = Folder(name: "unit-test.docc", content: [ + TextFile(name: "ExplicitArticle.md", utf8Content: """ + # Explicit Article + + This looks like an API Collection but is explicitly marked as an article. + + @Metadata { + @PageKind(article) + } + + ## Topics + + - ``TestModule/TestClass`` + """), + JSONFile(name: "TestModule.symbols.json", content: symbolGraph) + ]) + + let (_, context) = try await loadBundle(catalog: catalogHierarchy) + let converter = DocumentationNodeConverter(context: context) + + let explicitArticleReference = ResolvedTopicReference( + bundleID: context.inputs.id, + path: "/documentation/unit-test/ExplicitArticle", + sourceLanguage: .swift + ) + let node = try context.entity(with: explicitArticleReference) + let renderNode = converter.convert(node) + + let summaries = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode) + let pageSummary = try XCTUnwrap(summaries.first) + + // Should be .article because of explicit @PageKind(article), not .collectionGroup + XCTAssertEqual(pageSummary.kind, .article) + XCTAssertEqual(pageSummary.title, "Explicit Article") + + // Verify round-trip encoding preserves the correct kind + try assertRoundTripCoding(pageSummary) + } } diff --git a/Tests/SwiftDocCTests/Model/DocumentationNodeTests.swift b/Tests/SwiftDocCTests/Model/DocumentationNodeTests.swift index 9b371d23ed..669c3c3146 100644 --- a/Tests/SwiftDocCTests/Model/DocumentationNodeTests.swift +++ b/Tests/SwiftDocCTests/Model/DocumentationNodeTests.swift @@ -11,6 +11,7 @@ import Foundation import Markdown @testable import SwiftDocC +import SymbolKit import XCTest class DocumentationNodeTests: XCTestCase { @@ -41,4 +42,61 @@ class DocumentationNodeTests: XCTestCase { XCTAssertEqual(anchorSection.reference, node.reference.withFragment(expectedTitle)) } } + + func testDocumentationKindToSymbolKindMapping() throws { + // Testing all symbol kinds map to a documentation kind + for symbolKind in SymbolGraph.Symbol.KindIdentifier.allCases { + let documentationKind = DocumentationNode.kind(forKind: symbolKind) + guard documentationKind != .unknown else { + continue + } + + let roundtrippedSymbolKind = DocumentationNode.symbolKind(for: documentationKind) + XCTAssertEqual(symbolKind, roundtrippedSymbolKind) + } + + // Testing that documentation kinds correctly map to a symbol kind + // Sometimes there are multiple mappings from DocumentationKind -> SymbolKind, exclude those here and test them separately + let documentationKinds = DocumentationNode.Kind.allKnownValues + .filter({ ![.localVariable, .typeDef, .typeConstant, .`keyword`, .tag, .object].contains($0) }) + for documentationKind in documentationKinds { + let symbolKind = DocumentationNode.symbolKind(for: documentationKind) + if documentationKind.isSymbol { + let symbolKind = try XCTUnwrap(DocumentationNode.symbolKind(for: documentationKind), "Expected a symbol kind equivalent for \(documentationKind)") + let rountrippedDocumentationKind = DocumentationNode.kind(forKind: symbolKind) + XCTAssertEqual(documentationKind, rountrippedDocumentationKind) + } else { + XCTAssertNil(symbolKind) + } + } + + // Test the exception documentation kinds + XCTAssertEqual(DocumentationNode.symbolKind(for: .localVariable), .var) + XCTAssertEqual(DocumentationNode.symbolKind(for: .typeDef), .typealias) + XCTAssertEqual(DocumentationNode.symbolKind(for: .typeConstant), .typeProperty) + XCTAssertEqual(DocumentationNode.symbolKind(for: .object), .dictionary) + } + + func testWithMultipleSourceLanguages() throws { + let sourceLanguages: Set = [.swift, .objectiveC] + // Test if articles contain all available source languages + let article = Article(markup: Document(parsing: "# Title", options: []), metadata: nil, redirects: nil, options: [:]) + let articleNode = try DocumentationNode( + reference: ResolvedTopicReference(bundleID: "org.swift.docc", path: "/blah", sourceLanguages: sourceLanguages), + article: article + ) + XCTAssertEqual(articleNode.availableSourceLanguages, sourceLanguages) + + // Test if symbols contain all available source languages + let symbol = makeSymbol(id: "blah", kind: .class, pathComponents: ["blah"]) + let symbolNode = DocumentationNode( + reference: ResolvedTopicReference(bundleID: "org.swift.docc", path: "/blah", sourceLanguages: sourceLanguages), + symbol: symbol, + platformName: nil, + moduleReference: ResolvedTopicReference(bundleID: "org.swift.docc", path: "/blah", sourceLanguages: sourceLanguages), + article: nil, + engine: DiagnosticEngine() + ) + XCTAssertEqual(symbolNode.availableSourceLanguages, sourceLanguages) + } } diff --git a/Tests/SwiftDocCTests/Model/LineHighlighterTests.swift b/Tests/SwiftDocCTests/Model/LineHighlighterTests.swift index 85112fa852..c63d951a60 100644 --- a/Tests/SwiftDocCTests/Model/LineHighlighterTests.swift +++ b/Tests/SwiftDocCTests/Model/LineHighlighterTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -47,9 +47,9 @@ class LineHighlighterTests: XCTestCase { ]) } - func highlights(tutorialFile: TextFile, codeFiles: [TextFile]) throws -> [LineHighlighter.Result] { + func highlights(tutorialFile: TextFile, codeFiles: [TextFile]) async throws -> [LineHighlighter.Result] { let catalog = Self.makeCatalog(tutorial: tutorialFile, codeFiles: codeFiles) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) let tutorialReference = ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Line-Highlighter-Tests/Tutorial", fragment: nil, sourceLanguage: .swift) let tutorial = try context.entity(with: tutorialReference).semantic as! Tutorial @@ -57,7 +57,7 @@ class LineHighlighterTests: XCTestCase { return LineHighlighter(context: context, tutorialSection: section, tutorialReference: tutorialReference).highlights } - func testNoSteps() throws { + func testNoSteps() async throws { let tutorialFile = TextFile(name: "Tutorial.tutorial", utf8Content: """ @Tutorial(time: 20, projectFiles: nothing.zip) { @XcodeRequirement(title: "Xcode X.Y Beta Z", destination: "https://www.example.com/download") @@ -70,10 +70,11 @@ class LineHighlighterTests: XCTestCase { @Assessments } """) - XCTAssertTrue(try highlights(tutorialFile: tutorialFile, codeFiles: []).isEmpty) + let highlights = try await highlights(tutorialFile: tutorialFile, codeFiles: []) + XCTAssertTrue(highlights.isEmpty) } - func testOneStep() throws { + func testOneStep() async throws { let tutorialFile = TextFile(name: "Tutorial.tutorial", utf8Content: """ @Tutorial(title: "No Steps", time: 20, projectFiles: nothing.zip) { @XcodeRequirement(title: "Xcode X.Y Beta Z", destination: "https://www.example.com/download") @@ -92,7 +93,7 @@ class LineHighlighterTests: XCTestCase { @Assessments """) let code1 = TextFile(name: "code1.swift", utf8Content: "func foo() {}") - let results = try highlights(tutorialFile: tutorialFile, codeFiles: [code1]) + let results = try await highlights(tutorialFile: tutorialFile, codeFiles: [code1]) XCTAssertEqual(1, results.count) results.first.map { result in XCTAssertEqual(ResourceReference(bundleID: LineHighlighterTests.bundleID, path: code1.name), result.file) @@ -100,7 +101,7 @@ class LineHighlighterTests: XCTestCase { } } - func testOneStepWithPrevious() throws { + func testOneStepWithPrevious() async throws { let tutorialFile = TextFile(name: "Tutorial.tutorial", utf8Content: """ @Tutorial(title: "No Steps", time: 20, projectFiles: nothing.zip) { @XcodeRequirement(title: "Xcode X.Y Beta Z", destination: "https://www.example.com/download") @@ -121,7 +122,7 @@ class LineHighlighterTests: XCTestCase { """) let code0 = TextFile(name: "code0.swift", utf8Content: "func foo() {}") let code1 = TextFile(name: "code1.swift", utf8Content: "func foo() {}\nfunc bar() {}") - let results = try highlights(tutorialFile: tutorialFile, codeFiles: [code0, code1]) + let results = try await highlights(tutorialFile: tutorialFile, codeFiles: [code0, code1]) XCTAssertEqual(1, results.count) results.first.map { result in XCTAssertEqual(ResourceReference(bundleID: LineHighlighterTests.bundleID, path: code1.name), result.file) @@ -134,7 +135,7 @@ class LineHighlighterTests: XCTestCase { } } - func testNameMismatch() throws { + func testNameMismatch() async throws { let tutorialFile = TextFile(name: "Tutorial.tutorial", utf8Content: """ @Tutorial(title: "No Steps", time: 20, projectFiles: nothing.zip) { @XcodeRequirement(title: "Xcode X.Y Beta Z", destination: "https://www.example.com/download") @@ -160,7 +161,7 @@ class LineHighlighterTests: XCTestCase { let code1 = TextFile(name: "code1.swift", utf8Content: "func foo() {}") let code2 = TextFile(name: "code2.swift", utf8Content: "func foo() {}\nfunc bar() {}") - let results = try highlights(tutorialFile: tutorialFile, codeFiles: [code1, code2]) + let results = try await highlights(tutorialFile: tutorialFile, codeFiles: [code1, code2]) XCTAssertEqual(2, results.count) XCTAssertEqual(ResourceReference(bundleID: LineHighlighterTests.bundleID, path: code1.name), results[0].file) @@ -170,7 +171,7 @@ class LineHighlighterTests: XCTestCase { XCTAssertTrue(results[1].highlights.isEmpty) } - func testResetDiffAtStart() throws { + func testResetDiffAtStart() async throws { let tutorialFile = TextFile(name: "Tutorial.tutorial", utf8Content: """ @Tutorial(title: "No Steps", time: 20, projectFiles: nothing.zip) { @XcodeRequirement(title: "Xcode X.Y Beta Z", destination: "https://www.example.com/download") @@ -190,7 +191,7 @@ class LineHighlighterTests: XCTestCase { """) let code0 = TextFile(name: "code0.swift", utf8Content: "func foo() {}") let code1 = TextFile(name: "code1.swift", utf8Content: "func foo() {}\nfunc bar() {}") - let results = try highlights(tutorialFile: tutorialFile, codeFiles: [code0, code1]) + let results = try await highlights(tutorialFile: tutorialFile, codeFiles: [code0, code1]) XCTAssertEqual(1, results.count) results.first.map { result in XCTAssertEqual(ResourceReference(bundleID: LineHighlighterTests.bundleID, path: code1.name), result.file) @@ -198,7 +199,7 @@ class LineHighlighterTests: XCTestCase { } } - func testResetDiff() throws { + func testResetDiff() async throws { let tutorialFile = TextFile(name: "Tutorial.tutorial", utf8Content: """ @Tutorial(title: "No Steps", time: 20, projectFiles: nothing.zip) { @XcodeRequirement(title: "Xcode X.Y Beta Z", destination: "https://www.example.com/download") @@ -222,7 +223,7 @@ class LineHighlighterTests: XCTestCase { """) let code1 = TextFile(name: "code1.swift", utf8Content: "func foo() {}") let code2 = TextFile(name: "code2.swift", utf8Content: "func foo() {}\nfunc bar() {}") - let results = try highlights(tutorialFile: tutorialFile, codeFiles: [code1, code2]) + let results = try await highlights(tutorialFile: tutorialFile, codeFiles: [code1, code2]) XCTAssertEqual(2, results.count) @@ -233,7 +234,7 @@ class LineHighlighterTests: XCTestCase { XCTAssertTrue(results[1].highlights.isEmpty) } - func testPreviousOverride() throws { + func testPreviousOverride() async throws { let tutorialFile = TextFile(name: "Tutorial.tutorial", utf8Content: """ @Tutorial(title: "No Steps", time: 20, projectFiles: nothing.zip) { @XcodeRequirement(title: "Xcode X.Y Beta Z", destination: "https://www.example.com/download") @@ -261,7 +262,7 @@ class LineHighlighterTests: XCTestCase { let code0 = TextFile(name: "code0.swift", utf8Content: "") let code1 = TextFile(name: "code1.swift", utf8Content: "func foo() {}") let code2 = TextFile(name: "code2.swift", utf8Content: "func foo() {}\nfunc bar() {}") - let results = try highlights(tutorialFile: tutorialFile, codeFiles: [code0, code1, code2]) + let results = try await highlights(tutorialFile: tutorialFile, codeFiles: [code0, code1, code2]) XCTAssertEqual(2, results.count) diff --git a/Tests/SwiftDocCTests/Model/ParametersAndReturnValidatorTests.swift b/Tests/SwiftDocCTests/Model/ParametersAndReturnValidatorTests.swift index 5f93f56ead..df4061c5d4 100644 --- a/Tests/SwiftDocCTests/Model/ParametersAndReturnValidatorTests.swift +++ b/Tests/SwiftDocCTests/Model/ParametersAndReturnValidatorTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -17,8 +17,8 @@ import SwiftDocCTestUtilities class ParametersAndReturnValidatorTests: XCTestCase { - func testFiltersParameters() throws { - let (bundle, context) = try testBundleAndContext(named: "ErrorParameters") + func testFiltersParameters() async throws { + let (bundle, context) = try await testBundleAndContext(named: "ErrorParameters") // /// - Parameters: // /// - someValue: Some value. @@ -111,7 +111,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { } } - func testExtendsReturnValueDocumentation() throws { + func testExtendsReturnValueDocumentation() async throws { for (returnValueDescription, expectsExtendedDocumentation) in [ // Expects to extend the documentation ("Returns some value.", true), @@ -153,7 +153,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { ]) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") @@ -177,8 +177,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { } } - func testParametersWithAlternateSignatures() throws { - let (_, _, context) = try testBundleAndContext(copying: "AlternateDeclarations") { url in + func testParametersWithAlternateSignatures() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "AlternateDeclarations") { url in try """ # ``MyClass/present(completion:)`` @@ -207,8 +207,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { XCTAssertEqual(swiftReturnsContent, "Description of the return value that’s available for some other alternatives.") } - func testParameterDiagnosticsInDocumentationExtension() throws { - let (url, _, context) = try testBundleAndContext(copying: "ErrorParameters") { url in + func testParameterDiagnosticsInDocumentationExtension() async throws { + let (url, _, context) = try await testBundleAndContext(copying: "ErrorParameters") { url in try """ # ``MyClassInObjectiveC/doSomethingWith:error:`` @@ -292,8 +292,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { } } - func testFunctionsThatCorrespondToPropertiesInAnotherLanguage() throws { - let (_, _, context) = try testBundleAndContext(named: "GeometricalShapes") + func testFunctionsThatCorrespondToPropertiesInAnotherLanguage() async throws { + let (_, _, context) = try await testBundleAndContext(named: "GeometricalShapes") XCTAssertEqual(context.problems.map(\.diagnostic.summary), []) let reference = try XCTUnwrap(context.knownPages.first(where: { $0.lastPathComponent == "isEmpty" })) @@ -302,7 +302,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { let symbolSemantic = try XCTUnwrap(node.semantic as? Symbol) let swiftParameterNames = symbolSemantic.parametersSectionVariants.firstValue?.parameters let objcParameterNames = symbolSemantic.parametersSectionVariants.allValues.mapFirst(where: { (trait, variant) -> [Parameter]? in - guard trait.interfaceLanguage == SourceLanguage.objectiveC.id else { return nil } + guard trait.sourceLanguage == .objectiveC else { return nil } return variant.parameters }) @@ -312,7 +312,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { let swiftReturnsContent = symbolSemantic.returnsSection.map { _format($0.content) } let objcReturnsContent = symbolSemantic.returnsSectionVariants.allValues.mapFirst(where: { (trait, variant) -> String? in - guard trait.interfaceLanguage == SourceLanguage.objectiveC.id else { return nil } + guard trait.sourceLanguage == .objectiveC else { return nil } return variant.content.map { $0.format() }.joined() }) @@ -320,8 +320,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { XCTAssertEqual(objcReturnsContent, "`YES` if the specified circle is empty; otherwise, `NO`.") } - func testCanDocumentInitializerReturnValue() throws { - let (_, _, context) = try testBundleAndContext(copying: "GeometricalShapes") { url in + func testCanDocumentInitializerReturnValue() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "GeometricalShapes") { url in try """ # ``Circle/init(center:radius:)`` @@ -344,14 +344,14 @@ class ParametersAndReturnValidatorTests: XCTestCase { let symbolSemantic = try XCTUnwrap(node.semantic as? Symbol) let swiftReturnsSection = try XCTUnwrap( - symbolSemantic.returnsSectionVariants.allValues.first(where: { trait, _ in trait.interfaceLanguage == "swift" }) + symbolSemantic.returnsSectionVariants.allValues.first(where: { trait, _ in trait.sourceLanguage == .swift }) ).variant XCTAssertEqual(swiftReturnsSection.content.map { $0.format() }, [ "Return value documentation for an initializer." ]) } - func testNoParameterDiagnosticWithoutFunctionSignature() throws { + func testNoParameterDiagnosticWithoutFunctionSignature() async throws { var symbolGraph = makeSymbolGraph(docComment: """ Some function description @@ -365,12 +365,12 @@ class ParametersAndReturnValidatorTests: XCTestCase { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.count, 0) } - func testNoParameterDiagnosticWithoutDocumentationComment() throws { + func testNoParameterDiagnosticWithoutDocumentationComment() async throws { let symbolGraph = makeSymbolGraph(docComment: """ Some function description @@ -380,12 +380,12 @@ class ParametersAndReturnValidatorTests: XCTestCase { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.count, 0) } - func testMissingParametersInDocCommentDiagnostics() throws { + func testMissingParametersInDocCommentDiagnostics() async throws { let symbolGraph = makeSymbolGraph(docComment: """ Some function description @@ -396,7 +396,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.count, 2) let endOfParameterSectionLocation = SourceLocation(line: start.line + 5, column: start.character + 40, source: symbolURL) @@ -425,7 +425,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { XCTAssertEqual(otherMissingParameterProblem.possibleSolutions.first?.replacements.first?.replacement, "\n/// - fourthParameter: <#parameter description#>") } - func testMissingSeparateParametersInDocCommentDiagnostics() throws { + func testMissingSeparateParametersInDocCommentDiagnostics() async throws { let symbolGraph = makeSymbolGraph(docComment: """ Some function description @@ -435,7 +435,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.count, 2) let endOfParameterSectionLocation = SourceLocation(line: start.line + 4, column: start.character + 48, source: symbolURL) @@ -464,7 +464,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { XCTAssertEqual(otherMissingParameterProblem.possibleSolutions.first?.replacements.first?.replacement, "\n///- Parameter fourthParameter: <#parameter description#>") } - func testFunctionWithOnlyErrorParameter() throws { + func testFunctionWithOnlyErrorParameter() async throws { let catalog = Folder(name: "unit-test.docc", content: [ Folder(name: "swift", content: [ @@ -490,7 +490,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { )) ]) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") @@ -508,7 +508,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { XCTAssertEqual(returnsSections[.objectiveC]?.content.map({ $0.format() }).joined(), "Some return value description.") } - func testFunctionWithDifferentSignaturesOnDifferentPlatforms() throws { + func testFunctionWithDifferentSignaturesOnDifferentPlatforms() async throws { let catalog = Folder(name: "unit-test.docc", content: [ // One parameter, void return @@ -550,7 +550,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { """) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") @@ -567,7 +567,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { XCTAssertEqual(returnSections[.objectiveC]?.content.map({ $0.format() }).joined(), "Some description of the return value that is only available on platform 3.") } - func testFunctionWithErrorParameterButVoidType() throws { + func testFunctionWithErrorParameterButVoidType() async throws { let catalog = Folder(name: "unit-test.docc", content: [ Folder(name: "swift", content: [ @@ -594,7 +594,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { ]) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") @@ -613,8 +613,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { XCTAssertNil(returnsSections[.objectiveC]) } - func testWarningForDocumentingExternalParameterNames() throws { - let warningOutput = try warningOutputRaisedFrom( + func testWarningForDocumentingExternalParameterNames() async throws { + let warningOutput = try await warningOutputRaisedFrom( docComment: """ Some function description @@ -633,8 +633,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { """) } - func testWarningForDocumentingVoidReturn() throws { - let warningOutput = try warningOutputRaisedFrom( + func testWarningForDocumentingVoidReturn() async throws { + let warningOutput = try await warningOutputRaisedFrom( docComment: """ Some function description @@ -654,8 +654,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { """) } - func testWarningForParameterDocumentedTwice() throws { - let warningOutput = try warningOutputRaisedFrom( + func testWarningForParameterDocumentedTwice() async throws { + let warningOutput = try await warningOutputRaisedFrom( docComment: """ Some function description @@ -676,8 +676,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { """) } - func testWarningForExtraDocumentedParameter() throws { - let warningOutput = try warningOutputRaisedFrom( + func testWarningForExtraDocumentedParameter() async throws { + let warningOutput = try await warningOutputRaisedFrom( docComment: """ Some function description @@ -697,8 +697,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { """) } - func testWarningForUndocumentedParameter() throws { - let missingFirstWarningOutput = try warningOutputRaisedFrom( + func testWarningForUndocumentedParameter() async throws { + let missingFirstWarningOutput = try await warningOutputRaisedFrom( docComment: """ Some function description @@ -717,7 +717,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { """) - let missingSecondWarningOutput = try warningOutputRaisedFrom( + let missingSecondWarningOutput = try await warningOutputRaisedFrom( docComment: """ Some function description @@ -736,8 +736,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { """) } - func testDoesNotWarnAboutInheritedDocumentation() throws { - let warningOutput = try warningOutputRaisedFrom( + func testDoesNotWarnAboutInheritedDocumentation() async throws { + let warningOutput = try await warningOutputRaisedFrom( docComment: """ Some function description @@ -751,7 +751,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { XCTAssertEqual(warningOutput, "") } - func testDocumentingTwoUnnamedParameters() throws { + func testDocumentingTwoUnnamedParameters() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( docComment: """ @@ -768,7 +768,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { )) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") @@ -787,7 +787,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { XCTAssertNil(returnsSections[.objectiveC]) } - func testDocumentingMixedNamedAndUnnamedParameters() throws { + func testDocumentingMixedNamedAndUnnamedParameters() async throws { // This test verifies the behavior of documenting two named parameters and one unnamed parameter. // // It checks different combinations of which parameter is unnamed: @@ -828,7 +828,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { returnValue: .init(kind: .typeIdentifier, spelling: "Void", preciseIdentifier: "s:s4Voida") )) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") @@ -852,10 +852,10 @@ class ParametersAndReturnValidatorTests: XCTestCase { } } - func testWarningsForMissingOrExtraUnnamedParameters() throws { + func testWarningsForMissingOrExtraUnnamedParameters() async throws { let returnValue = SymbolKit.SymbolGraph.Symbol.DeclarationFragments.Fragment(kind: .typeIdentifier, spelling: "void", preciseIdentifier: "c:v") - let tooFewParametersOutput = try warningOutputRaisedFrom( + let tooFewParametersOutput = try await warningOutputRaisedFrom( docComment: """ Some function description @@ -882,7 +882,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { | ╰─suggestion: Document unnamed parameter #3 """) - let tooManyParametersOutput = try warningOutputRaisedFrom( + let tooManyParametersOutput = try await warningOutputRaisedFrom( docComment: """ Some function description @@ -910,10 +910,8 @@ class ParametersAndReturnValidatorTests: XCTestCase { docComment: String, docCommentModuleName: String? = "ModuleName", parameters: [(name: String, externalName: String?)], - returnValue: SymbolGraph.Symbol.DeclarationFragments.Fragment, - file: StaticString = #filePath, - line: UInt = #line - ) throws -> String { + returnValue: SymbolGraph.Symbol.DeclarationFragments.Fragment + ) async throws -> String { let fileSystem = try TestFileSystem(folders: [ Folder(name: "path", content: [ Folder(name: "to", content: [ @@ -941,7 +939,7 @@ class ParametersAndReturnValidatorTests: XCTestCase { let (bundle, dataProvider) = try DocumentationContext.InputsProvider(fileManager: fileSystem) .inputsAndDataProvider(startingPoint: URL(fileURLWithPath: "/unit-test.docc"), options: .init()) - _ = try DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine) + _ = try await DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine) diagnosticEngine.flush() return logStorage.text.trimmingCharacters(in: .newlines) diff --git a/Tests/SwiftDocCTests/Model/PropertyListPossibleValuesSectionTests.swift b/Tests/SwiftDocCTests/Model/PropertyListPossibleValuesSectionTests.swift index f551be022d..2a5ddab8bf 100644 --- a/Tests/SwiftDocCTests/Model/PropertyListPossibleValuesSectionTests.swift +++ b/Tests/SwiftDocCTests/Model/PropertyListPossibleValuesSectionTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -17,9 +17,9 @@ import SwiftDocCTestUtilities class PropertyListPossibleValuesSectionTests: XCTestCase { - func testPossibleValuesDiagnostics() throws { + func testPossibleValuesDiagnostics() async throws { // Check that a problem is emitted when extra possible values are documented. - var (url, _, context) = try testBundleAndContext(copying: "DictionaryData") { url in + var (url, _, context) = try await testBundleAndContext(copying: "DictionaryData") { url in try """ # ``Month`` @@ -44,7 +44,7 @@ class PropertyListPossibleValuesSectionTests: XCTestCase { } // Check that no problems are emitted if no extra possible values are documented. - (url, _, context) = try testBundleAndContext(copying: "DictionaryData") { url in + (url, _, context) = try await testBundleAndContext(copying: "DictionaryData") { url in try """ # ``Month`` @@ -61,7 +61,7 @@ class PropertyListPossibleValuesSectionTests: XCTestCase { } // Check that a problem is emitted with possible solutions. - (url, _, context) = try testBundleAndContext(copying: "DictionaryData") { url in + (url, _, context) = try await testBundleAndContext(copying: "DictionaryData") { url in try """ # ``Month`` @@ -81,19 +81,19 @@ class PropertyListPossibleValuesSectionTests: XCTestCase { } } - func testAbsenceOfPossibleValues() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "DictionaryData") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/DictionaryData/Artist", sourceLanguage: .swift)) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + func testAbsenceOfPossibleValues() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "DictionaryData") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/DictionaryData/Artist", sourceLanguage: .swift)) + let converter = DocumentationNodeConverter(context: context) // Check that the `Possible Values` section is not rendered if the symbol don't define any possible value. XCTAssertNil(converter.convert(node).primaryContentSections.first(where: { $0.kind == .possibleValues}) as? PossibleValuesRenderSection) } - func testUndocumentedPossibleValues() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "DictionaryData") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/DictionaryData/Month", sourceLanguage: .swift)) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + func testUndocumentedPossibleValues() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "DictionaryData") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/DictionaryData/Month", sourceLanguage: .swift)) + let converter = DocumentationNodeConverter(context: context) let possibleValuesSection = try XCTUnwrap(converter.convert(node).primaryContentSections.first(where: { $0.kind == .possibleValues}) as? PossibleValuesRenderSection) let possibleValues: [PossibleValuesRenderSection.NamedValue] = possibleValuesSection.values @@ -101,8 +101,8 @@ class PropertyListPossibleValuesSectionTests: XCTestCase { XCTAssertEqual(possibleValues.map { $0.name }, ["January", "February", "March"]) } - func testDocumentedPossibleValuesMatchSymbolGraphPossibleValues() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "DictionaryData") { url in + func testDocumentedPossibleValuesMatchSymbolGraphPossibleValues() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "DictionaryData") { url in try """ # ``Month`` @@ -125,8 +125,8 @@ class PropertyListPossibleValuesSectionTests: XCTestCase { XCTAssertEqual(possibleValues.map { $0.value }, ["January", "February", "March"]) } - func testDocumentedPossibleValues() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "DictionaryData") { url in + func testDocumentedPossibleValues() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "DictionaryData") { url in try """ # ``Month`` @@ -149,8 +149,8 @@ class PropertyListPossibleValuesSectionTests: XCTestCase { XCTAssertEqual(documentedPossibleValue.contents.count , 1) } - func testUnresolvedLinkWarnings() throws { - let (_, _, context) = try testBundleAndContext(copying: "DictionaryData") { url in + func testUnresolvedLinkWarnings() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "DictionaryData") { url in try """ # ``Month`` @@ -171,8 +171,8 @@ class PropertyListPossibleValuesSectionTests: XCTestCase { XCTAssertTrue(problemDiagnosticsSummary.contains("\'NotFoundSymbol\' doesn\'t exist at \'/DictionaryData/Month\'")) } - func testResolvedLins() throws { - let (_, _, context) = try testBundleAndContext(copying: "DictionaryData") { url in + func testResolvedLins() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "DictionaryData") { url in try """ # ``Month`` diff --git a/Tests/SwiftDocCTests/Model/RenderContentMetadataTests.swift b/Tests/SwiftDocCTests/Model/RenderContentMetadataTests.swift index e4d2ec5a9b..291f377a72 100644 --- a/Tests/SwiftDocCTests/Model/RenderContentMetadataTests.swift +++ b/Tests/SwiftDocCTests/Model/RenderContentMetadataTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -54,7 +54,7 @@ class RenderContentMetadataTests: XCTestCase { RenderInlineContent.text("Content"), ]) - let code = RenderBlockContent.codeListing(.init(syntax: nil, code: [], metadata: metadata)) + let code = RenderBlockContent.codeListing(.init(syntax: nil, code: [], metadata: metadata, options: nil)) let data = try JSONEncoder().encode(code) let roundtrip = try JSONDecoder().decode(RenderBlockContent.self, from: data) @@ -66,9 +66,9 @@ class RenderContentMetadataTests: XCTestCase { XCTAssertEqual(metadata, roundtripListing.metadata) } - func testRenderingTables() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testRenderingTables() async throws { + let (_, context) = try await testBundleAndContext() + var renderContentCompiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: context.inputs.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = """ | Column 1 | Column 2 | @@ -107,9 +107,9 @@ class RenderContentMetadataTests: XCTestCase { } } - func testRenderingTableSpans() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testRenderingTableSpans() async throws { + let (_, context) = try await testBundleAndContext() + var renderContentCompiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: context.inputs.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = """ | one | two | three | @@ -160,9 +160,9 @@ class RenderContentMetadataTests: XCTestCase { try assertRoundTripCoding(renderedTable) } - func testRenderingTableColumnAlignments() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testRenderingTableColumnAlignments() async throws { + let (_, context) = try await testBundleAndContext() + var renderContentCompiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: context.inputs.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = """ | one | two | three | four | @@ -202,9 +202,9 @@ class RenderContentMetadataTests: XCTestCase { } /// Verifies that a table with `nil` alignments and a table with all-unset alignments still compare as equal. - func testRenderedTableEquality() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testRenderedTableEquality() async throws { + let (_, context) = try await testBundleAndContext() + var renderContentCompiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: context.inputs.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = """ | Column 1 | Column 2 | @@ -228,9 +228,9 @@ class RenderContentMetadataTests: XCTestCase { } /// Verifies that two tables with otherwise-identical contents but different column alignments compare as unequal. - func testRenderedTableInequality() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testRenderedTableInequality() async throws { + let (_, context) = try await testBundleAndContext() + var renderContentCompiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: context.inputs.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let decodedTableWithUnsetColumns: RenderBlockContent.Table do { @@ -275,9 +275,9 @@ class RenderContentMetadataTests: XCTestCase { XCTAssertNotEqual(decodedTableWithUnsetColumns, decodedTableWithLeftColumns) } - func testStrikethrough() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testStrikethrough() async throws { + let (_, context) = try await testBundleAndContext() + var renderContentCompiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: context.inputs.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = """ ~~Striken~~ text. @@ -298,9 +298,9 @@ class RenderContentMetadataTests: XCTestCase { } } - func testHeadingAnchorShouldBeEncoded() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testHeadingAnchorShouldBeEncoded() async throws { + let (_, context) = try await testBundleAndContext() + var renderContentCompiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: context.inputs.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = """ ## テスト diff --git a/Tests/SwiftDocCTests/Model/RenderHierarchyTranslatorTests.swift b/Tests/SwiftDocCTests/Model/RenderHierarchyTranslatorTests.swift index 7621008ed4..00b8424964 100644 --- a/Tests/SwiftDocCTests/Model/RenderHierarchyTranslatorTests.swift +++ b/Tests/SwiftDocCTests/Model/RenderHierarchyTranslatorTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -12,11 +12,11 @@ import XCTest class RenderHierarchyTranslatorTests: XCTestCase { - func test() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func test() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let technologyReference = ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/TestOverview", sourceLanguage: .swift) - var translator = RenderHierarchyTranslator(context: context, bundle: bundle) + var translator = RenderHierarchyTranslator(context: context) let renderHierarchyVariants = translator.visitTutorialTableOfContentsNode(technologyReference)?.hierarchyVariants XCTAssertEqual(renderHierarchyVariants?.variants, [], "Unexpected variant hierarchies for tutorial table of content page") let renderHierarchy = renderHierarchyVariants?.defaultValue @@ -87,9 +87,9 @@ class RenderHierarchyTranslatorTests: XCTestCase { XCTAssertEqual(assessments.reference.identifier, "doc://org.swift.docc.example/tutorials/Test-Bundle/TestTutorial#Check-Your-Understanding") } - func testMultiplePaths() throws { + func testMultiplePaths() async throws { // Curate "TestTutorial" under MyKit as well as TechnologyX. - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in let myKitURL = root.appendingPathComponent("documentation/mykit.md") let text = try String(contentsOf: myKitURL).replacingOccurrences(of: "## Topics", with: """ ## Topics @@ -104,7 +104,7 @@ class RenderHierarchyTranslatorTests: XCTestCase { // Get a translated render node let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift) let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: identifier) + var translator = RenderNodeTranslator(context: context, identifier: identifier) let renderNode = translator.visit(node.semantic) as! RenderNode guard case .tutorials(let hierarchy) = renderNode.hierarchyVariants.defaultValue else { @@ -128,8 +128,8 @@ class RenderHierarchyTranslatorTests: XCTestCase { ]) } - func testLanguageSpecificHierarchies() throws { - let (bundle, context) = try testBundleAndContext(named: "GeometricalShapes") + func testLanguageSpecificHierarchies() async throws { + let (_, context) = try await testBundleAndContext(named: "GeometricalShapes") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) // An inner function to assert the rendered hierarchy values for a given reference @@ -141,7 +141,7 @@ class RenderHierarchyTranslatorTests: XCTestCase { line: UInt = #line ) throws { let documentationNode = try context.entity(with: reference) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visit(documentationNode.semantic) as? RenderNode, file: file, line: line) if let expectedSwiftPaths { diff --git a/Tests/SwiftDocCTests/Model/RenderNodeDiffingBundleTests.swift b/Tests/SwiftDocCTests/Model/RenderNodeDiffingBundleTests.swift index 452763cc21..e910d461bf 100644 --- a/Tests/SwiftDocCTests/Model/RenderNodeDiffingBundleTests.swift +++ b/Tests/SwiftDocCTests/Model/RenderNodeDiffingBundleTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -15,7 +15,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { let testBundleName = "LegacyBundle_DoNotUseInNewTests" let testBundleID: DocumentationBundle.Identifier = "org.swift.docc.example" - func testDiffSymbolFromBundleWithDiscussionSectionRemoved() throws { + func testDiffSymbolFromBundleWithDiscussionSectionRemoved() async throws { let pathToSymbol = "/documentation/MyKit" let modification = { (url: URL) in @@ -28,7 +28,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { try text.write(to: symbolURL, atomically: true, encoding: .utf8) } - let differences = try getDiffsFromModifiedDocument(bundleName: testBundleName, + let differences = try await getDiffsFromModifiedDocument(bundleName: testBundleName, bundleID: testBundleID, topicReferencePath: pathToSymbol, modification: modification) @@ -41,7 +41,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { valueType: RenderInlineContent.self) } - func testDiffArticleFromBundleWithTopicSectionAdded() throws { + func testDiffArticleFromBundleWithTopicSectionAdded() async throws { let pathToArticle = "/documentation/Test-Bundle/article" let modification = { (url: URL) in @@ -56,7 +56,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { try text.write(to: articleURL, atomically: true, encoding: .utf8) } - let differences = try getDiffsFromModifiedDocument(bundleName: testBundleName, + let differences = try await getDiffsFromModifiedDocument(bundleName: testBundleName, bundleID: testBundleID, topicReferencePath: pathToArticle, modification: modification) @@ -76,7 +76,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { valueType: TaskGroupRenderSection.self) } - func testDiffArticleFromBundleWithSeeAlsoSectionRemoved() throws { + func testDiffArticleFromBundleWithSeeAlsoSectionRemoved() async throws { let pathToArticle = "/documentation/Test-Bundle/article" let modification = { (url: URL) in @@ -89,7 +89,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { try text.write(to: articleURL, atomically: true, encoding: .utf8) } - let differences = try getDiffsFromModifiedDocument(bundleName: testBundleName, + let differences = try await getDiffsFromModifiedDocument(bundleName: testBundleName, bundleID: testBundleID, topicReferencePath: pathToArticle, modification: modification) @@ -108,7 +108,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { valueType: RenderInlineContent.self) } - func testDiffSymbolFromBundleWithTopicSectionRemoved() throws { + func testDiffSymbolFromBundleWithTopicSectionRemoved() async throws { let pathToSymbol = "/documentation/MyKit" let modification = { (url: URL) in @@ -121,7 +121,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { try text.write(to: symbolURL, atomically: true, encoding: .utf8) } - let differences = try getDiffsFromModifiedDocument(bundleName: testBundleName, + let differences = try await getDiffsFromModifiedDocument(bundleName: testBundleName, bundleID: testBundleID, topicReferencePath: pathToSymbol, modification: modification) @@ -140,7 +140,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { valueType: RenderInlineContent.self) } - func testDiffSymbolFromBundleWithAbstractUpdated() throws { + func testDiffSymbolFromBundleWithAbstractUpdated() async throws { let pathToSymbol = "/documentation/MyKit/MyClass" let newAbstractValue = "MyClass new abstract." @@ -150,7 +150,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { try text.write(to: symbolURL, atomically: true, encoding: .utf8) } - let differences = try getDiffsFromModifiedDocument(bundleName: testBundleName, + let differences = try await getDiffsFromModifiedDocument(bundleName: testBundleName, bundleID: testBundleID, topicReferencePath: pathToSymbol, modification: modification) @@ -172,7 +172,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { valueType: AnyRenderReference.self) } - func testDiffSymbolFromBundleWithDeprecationAdded() throws { + func testDiffSymbolFromBundleWithDeprecationAdded() async throws { let pathToSymbol = "/documentation/MyKit/MyProtocol" let newDeprecationValue = "This protocol has been deprecated." @@ -188,7 +188,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { try text.write(to: symbolURL, atomically: true, encoding: .utf8) } - let differences = try getDiffsFromModifiedDocument(bundleName: testBundleName, + let differences = try await getDiffsFromModifiedDocument(bundleName: testBundleName, bundleID: testBundleID, topicReferencePath: pathToSymbol, modification: modification) @@ -211,7 +211,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { valueType: Bool.self) } - func testDiffSymbolFromBundleWithDisplayNameDirectiveAdded() throws { + func testDiffSymbolFromBundleWithDisplayNameDirectiveAdded() async throws { let pathToSymbol = "/documentation/MyKit" let newTitleValue = "My Kit" @@ -227,7 +227,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { try text.write(to: symbolURL, atomically: true, encoding: .utf8) } - let differences = try getDiffsFromModifiedDocument(bundleName: testBundleName, + let differences = try await getDiffsFromModifiedDocument(bundleName: testBundleName, bundleID: testBundleID, topicReferencePath: pathToSymbol, modification: modification) @@ -247,7 +247,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { valueType: RenderMetadata.Module.self) } - func testDiffArticleFromBundleWithDownloadDirectiveAdded() throws { + func testDiffArticleFromBundleWithDownloadDirectiveAdded() async throws { let pathToArticle = "/documentation/Test-Bundle/article" let modification = { (url: URL) in @@ -263,7 +263,7 @@ class RenderNodeDiffingBundleTests: XCTestCase { try text.write(to: articleURL, atomically: true, encoding: .utf8) } - let differences = try getDiffsFromModifiedDocument(bundleName: testBundleName, + let differences = try await getDiffsFromModifiedDocument(bundleName: testBundleName, bundleID: testBundleID, topicReferencePath: pathToArticle, modification: modification) @@ -283,10 +283,10 @@ class RenderNodeDiffingBundleTests: XCTestCase { valueType: String.self) } - func testNoDiffsWhenReconvertingSameBundle() throws { - let (bundle, context) = try testBundleAndContext(named: testBundleName) - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + func testNoDiffsWhenReconvertingSameBundle() async throws { + let (_, context) = try await testBundleAndContext(named: testBundleName) + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) for identifier in context.knownPages { let entity = try context.entity(with: identifier) @@ -302,25 +302,25 @@ class RenderNodeDiffingBundleTests: XCTestCase { bundleID: DocumentationBundle.Identifier, topicReferencePath: String, modification: @escaping (URL) throws -> () - ) throws -> JSONPatchDifferences { - let (bundleOriginal, contextOriginal) = try testBundleAndContext(named: bundleName) + ) async throws -> JSONPatchDifferences { + let (_, contextOriginal) = try await testBundleAndContext(named: bundleName) let nodeOriginal = try contextOriginal.entity(with: ResolvedTopicReference(bundleID: bundleID, path: topicReferencePath, sourceLanguage: .swift)) - var renderContext = RenderContext(documentationContext: contextOriginal, bundle: bundleOriginal) - var converter = DocumentationContextConverter(bundle: bundleOriginal, context: contextOriginal, renderContext: renderContext) + var renderContext = RenderContext(documentationContext: contextOriginal) + var converter = DocumentationContextConverter(context: contextOriginal, renderContext: renderContext) let renderNodeOriginal = try XCTUnwrap(converter.renderNode(for: nodeOriginal)) // Make copy of the bundle on disk, modify the document, and write it - let (_, bundleModified, contextModified) = try testBundleAndContext(copying: bundleName) { url in + let (_, _, contextModified) = try await testBundleAndContext(copying: bundleName) { url in try modification(url) } let nodeModified = try contextModified.entity(with: ResolvedTopicReference(bundleID: bundleID, path: topicReferencePath, sourceLanguage: .swift)) - renderContext = RenderContext(documentationContext: contextModified, bundle: bundleModified) - converter = DocumentationContextConverter(bundle: bundleModified, context: contextModified, renderContext: renderContext) + renderContext = RenderContext(documentationContext: contextModified) + converter = DocumentationContextConverter(context: contextModified, renderContext: renderContext) let renderNodeModified = try XCTUnwrap(converter.renderNode(for: nodeModified)) diff --git a/Tests/SwiftDocCTests/Model/RenderNodeSerializationTests.swift b/Tests/SwiftDocCTests/Model/RenderNodeSerializationTests.swift index a333c82977..cf1fb17698 100644 --- a/Tests/SwiftDocCTests/Model/RenderNodeSerializationTests.swift +++ b/Tests/SwiftDocCTests/Model/RenderNodeSerializationTests.swift @@ -44,7 +44,7 @@ class RenderNodeSerializationTests: XCTestCase { .strong(inlineContent: [.text("Project > Run")]), .text(" menu item, or the following code:"), ])), - .codeListing(.init(syntax: "swift", code: ["xcrun xcodebuild -h", "xcrun xcodebuild build -configuration Debug"], metadata: nil)), + .codeListing(.init(syntax: "swift", code: ["xcrun xcodebuild -h", "xcrun xcodebuild build -configuration Debug"], metadata: nil, options: nil)), ])) ] @@ -71,16 +71,16 @@ class RenderNodeSerializationTests: XCTestCase { let assessment1 = TutorialAssessmentsRenderSection.Assessment(title: [.paragraph(.init(inlineContent: [.text("Lorem ipsum dolor sit amet?")]))], content: nil, choices: [ - .init(content: [.codeListing(.init(syntax: "swift", code: ["override func viewDidLoad() {", "super.viewDidLoad()", "}"], metadata: nil))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "That's right!"), - .init(content: [.codeListing(.init(syntax: "swift", code: ["sceneView.delegate = self"], metadata: nil))], isCorrect: false, justification: [.paragraph(.init(inlineContent: [.text("It's incorrect because...")]))], reaction: "Not quite."), + .init(content: [.codeListing(.init(syntax: "swift", code: ["override func viewDidLoad() {", "super.viewDidLoad()", "}"], metadata: nil, options: nil))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "That's right!"), + .init(content: [.codeListing(.init(syntax: "swift", code: ["sceneView.delegate = self"], metadata: nil, options: nil))], isCorrect: false, justification: [.paragraph(.init(inlineContent: [.text("It's incorrect because...")]))], reaction: "Not quite."), .init(content: [.paragraph(.init(inlineContent: [.text("None of the above.")]))], isCorrect: false, justification: [.paragraph(.init(inlineContent: [.text("It's incorrect because...")]))], reaction: nil), ]) let assessment2 = TutorialAssessmentsRenderSection.Assessment(title: [.paragraph(.init(inlineContent: [.text("Duis aute irure dolor in reprehenderit?")]))], content: [.paragraph(.init(inlineContent: [.text("What is the airspeed velocity of an unladen swallow?")]))], choices: [ - .init(content: [.codeListing(.init(syntax: "swift", code: ["super.viewWillAppear()"], metadata: nil))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "Correct."), - .init(content: [.codeListing(.init(syntax: "swift", code: ["sceneView.delegate = self"], metadata: nil))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "Yep."), + .init(content: [.codeListing(.init(syntax: "swift", code: ["super.viewWillAppear()"], metadata: nil, options: nil))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "Correct."), + .init(content: [.codeListing(.init(syntax: "swift", code: ["sceneView.delegate = self"], metadata: nil, options: nil))], isCorrect: true, justification: [.paragraph(.init(inlineContent: [.text("It's correct because...")]))], reaction: "Yep."), .init(content: [.paragraph(.init(inlineContent: [.text("None of the above.")]))], isCorrect: false, justification: [.paragraph(.init(inlineContent: [.text("It's incorrect because...")]))], reaction: "Close!"), ]) @@ -91,9 +91,9 @@ class RenderNodeSerializationTests: XCTestCase { checkRoundTrip(inputNode) } - func testBundleRoundTrip() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) + func testBundleRoundTrip() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) guard let tutorialDirective = node.markup as? BlockDirective else { XCTFail("Unexpected document structure, tutorial not found as first child.") @@ -101,22 +101,22 @@ class RenderNodeSerializationTests: XCTestCase { } var problems = [Problem]() - guard let tutorial = Tutorial(from: tutorialDirective, source: nil, for: bundle, problems: &problems) else { + guard let tutorial = Tutorial(from: tutorialDirective, source: nil, for: context.inputs, problems: &problems) else { XCTFail("Couldn't create tutorial from markup: \(problems)") return } XCTAssertEqual(problems.count, 1, "Found problems \(problems.map { DiagnosticConsoleWriter.formattedDescription(for: $0.diagnostic) }) analyzing tutorial markup") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(tutorial) as! RenderNode checkRoundTrip(renderNode) } - func testTutorialArticleRoundTrip() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorialArticle", sourceLanguage: .swift)) + func testTutorialArticleRoundTrip() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/Test-Bundle/TestTutorialArticle", sourceLanguage: .swift)) guard let articleDirective = node.markup as? BlockDirective else { XCTFail("Unexpected document structure, article not found as first child.") @@ -124,24 +124,24 @@ class RenderNodeSerializationTests: XCTestCase { } var problems = [Problem]() - guard let article = TutorialArticle(from: articleDirective, source: nil, for: bundle, problems: &problems) else { + guard let article = TutorialArticle(from: articleDirective, source: nil, for: context.inputs, problems: &problems) else { XCTFail("Couldn't create article from markup: \(problems)") return } XCTAssertEqual(problems.count, 0, "Found problems \(problems.map { DiagnosticConsoleWriter.formattedDescription(for: $0.diagnostic) }) analyzing article markup") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(article) as! RenderNode checkRoundTrip(renderNode) } - func testAssetReferenceDictionary() throws { + func testAssetReferenceDictionary() async throws { typealias JSONDictionary = [String: Any] - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) guard let tutorialDirective = node.markup as? BlockDirective else { XCTFail("Unexpected document structure, tutorial not found as first child.") @@ -149,14 +149,14 @@ class RenderNodeSerializationTests: XCTestCase { } var problems = [Problem]() - guard let tutorial = Tutorial(from: tutorialDirective, source: nil, for: bundle, problems: &problems) else { + guard let tutorial = Tutorial(from: tutorialDirective, source: nil, for: context.inputs, problems: &problems) else { XCTFail("Couldn't create tutorial from markup: \(problems)") return } XCTAssertEqual(problems.count, 1, "Found problems \(problems.map { DiagnosticConsoleWriter.formattedDescription(for: $0.diagnostic) }) analyzing tutorial markup") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(tutorial) as! RenderNode let data = try encode(renderNode: renderNode) @@ -191,9 +191,9 @@ class RenderNodeSerializationTests: XCTestCase { } } - func testDiffAvailability() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorialArticle", sourceLanguage: .swift)) + func testDiffAvailability() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/Test-Bundle/TestTutorialArticle", sourceLanguage: .swift)) guard let articleDirective = node.markup as? BlockDirective else { XCTFail("Unexpected document structure, article not found as first child.") @@ -201,12 +201,12 @@ class RenderNodeSerializationTests: XCTestCase { } var problems = [Problem]() - guard let article = TutorialArticle(from: articleDirective, source: nil, for: bundle, problems: &problems) else { + guard let article = TutorialArticle(from: articleDirective, source: nil, for: context.inputs, problems: &problems) else { XCTFail("Couldn't create article from markup: \(problems)") return } - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) var renderNode = translator.visit(article) as! RenderNode diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeArticleOnlyCatalogTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeArticleOnlyCatalogTests.swift index 0dcdb8052e..4a994fe222 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeArticleOnlyCatalogTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeArticleOnlyCatalogTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -12,8 +12,8 @@ import XCTest @testable import SwiftDocC class SemaToRenderNodeArticleOnlyCatalogTests: XCTestCase { - func testDoesNotEmitVariantsForPagesInArticleOnlyCatalog() throws { - for renderNode in try renderNodeConsumer(for: "BundleWithTechnologyRoot").allRenderNodes() { + func testDoesNotEmitVariantsForPagesInArticleOnlyCatalog() async throws { + for renderNode in try await renderNodeConsumer(for: "BundleWithTechnologyRoot").allRenderNodes() { XCTAssertNil(renderNode.variants) } } diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeDictionaryDataTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeDictionaryDataTests.swift index b17d400fc3..b0335f9745 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeDictionaryDataTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeDictionaryDataTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -14,8 +14,8 @@ import SymbolKit import XCTest class SemaToRenderNodeDictionaryDataTests: XCTestCase { - func testBaseRenderNodeFromDictionaryData() throws { - let (_, context) = try testBundleAndContext(named: "DictionaryData") + func testBaseRenderNodeFromDictionaryData() async throws { + let (_, context) = try await testBundleAndContext(named: "DictionaryData") let expectedPageUSRsAndLangs: [String : Set] = [ // Artist dictionary - ``Artist``: @@ -75,8 +75,8 @@ class SemaToRenderNodeDictionaryDataTests: XCTestCase { } } - func testFrameworkRenderNodeHasExpectedContent() throws { - let outputConsumer = try renderNodeConsumer(for: "DictionaryData") + func testFrameworkRenderNodeHasExpectedContent() async throws { + let outputConsumer = try await renderNodeConsumer(for: "DictionaryData") let frameworkRenderNode = try outputConsumer.renderNode( withIdentifier: "DictionaryData" ) @@ -152,8 +152,8 @@ class SemaToRenderNodeDictionaryDataTests: XCTestCase { ) } - func testDictionaryRenderNodeHasExpectedContent() throws { - let outputConsumer = try renderNodeConsumer(for: "DictionaryData") + func testDictionaryRenderNodeHasExpectedContent() async throws { + let outputConsumer = try await renderNodeConsumer(for: "DictionaryData") let artistRenderNode = try outputConsumer.renderNode(withIdentifier: "data:test:Artist") assertExpectedContent( @@ -245,8 +245,8 @@ class SemaToRenderNodeDictionaryDataTests: XCTestCase { XCTAssert((nameProperty.attributes ?? []).isEmpty) } - func testTypeRenderNodeHasExpectedContent() throws { - let outputConsumer = try renderNodeConsumer(for: "DictionaryData") + func testTypeRenderNodeHasExpectedContent() async throws { + let outputConsumer = try await renderNodeConsumer(for: "DictionaryData") let genreRenderNode = try outputConsumer.renderNode(withIdentifier: "data:test:Genre") let type1 = DeclarationRenderSection.Token(fragment: SymbolGraph.Symbol.DeclarationFragments.Fragment(kind: .text, spelling: "string", preciseIdentifier: nil), identifier: nil) diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeHTTPRequestTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeHTTPRequestTests.swift index 7e014be213..6ebf753a32 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeHTTPRequestTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeHTTPRequestTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -14,8 +14,8 @@ import SymbolKit import XCTest class SemaToRenderNodeHTTPRequestTests: XCTestCase { - func testBaseRenderNodeFromHTTPRequest() throws { - let (_, context) = try testBundleAndContext(named: "HTTPRequests") + func testBaseRenderNodeFromHTTPRequest() async throws { + let (_, context) = try await testBundleAndContext(named: "HTTPRequests") let expectedPageUSRsAndLanguages: [String : Set] = [ // Get Artist endpoint - ``Get_Artist``: @@ -77,8 +77,8 @@ class SemaToRenderNodeHTTPRequestTests: XCTestCase { } } - func testFrameworkRenderNodeHasExpectedContent() throws { - let outputConsumer = try renderNodeConsumer(for: "HTTPRequests") + func testFrameworkRenderNodeHasExpectedContent() async throws { + let outputConsumer = try await renderNodeConsumer(for: "HTTPRequests") let frameworkRenderNode = try outputConsumer.renderNode( withIdentifier: "HTTPRequests" ) @@ -144,8 +144,8 @@ class SemaToRenderNodeHTTPRequestTests: XCTestCase { ) } - func testRestGetRequestRenderNodeHasExpectedContent() throws { - let outputConsumer = try renderNodeConsumer(for: "HTTPRequests") + func testRestGetRequestRenderNodeHasExpectedContent() async throws { + let outputConsumer = try await renderNodeConsumer(for: "HTTPRequests") let getArtistRenderNode = try outputConsumer.renderNode(withIdentifier: "rest:test:get:v1/artists/{}") assertExpectedContent( @@ -229,8 +229,8 @@ class SemaToRenderNodeHTTPRequestTests: XCTestCase { } } - func testRestPostRequestRenderNodeHasExpectedContent() throws { - let outputConsumer = try renderNodeConsumer(for: "HTTPRequests") + func testRestPostRequestRenderNodeHasExpectedContent() async throws { + let outputConsumer = try await renderNodeConsumer(for: "HTTPRequests") let getArtistRenderNode = try outputConsumer.renderNode(withIdentifier: "rest:test:post:v1/artists") assertExpectedContent( diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift index ed0809eaf8..d3a3fc2446 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -15,8 +15,8 @@ import SwiftDocCTestUtilities import XCTest class SemaToRenderNodeMixedLanguageTests: XCTestCase { - func testBaseRenderNodeFromMixedLanguageFramework() throws { - let (_, context) = try testBundleAndContext(named: "MixedLanguageFramework") + func testBaseRenderNodeFromMixedLanguageFramework() async throws { + let (_, context) = try await testBundleAndContext(named: "MixedLanguageFramework") for (_, documentationNode) in context.documentationCache where documentationNode.kind.isSymbol { let symbolUSR = try XCTUnwrap((documentationNode.semantic as? Symbol)?.externalID) @@ -78,8 +78,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { } } - func assertOutputsMultiLanguageRenderNodes(variantInterfaceLanguage: String) throws { - let outputConsumer = try renderNodeConsumer( + func assertOutputsMultiLanguageRenderNodes(variantInterfaceLanguage: String) async throws { + let outputConsumer = try await renderNodeConsumer( for: "MixedLanguageFramework", configureBundle: { bundleURL in // Update the clang symbol graph with the Objective-C identifier given in variantInterfaceLanguage. @@ -198,20 +198,20 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testOutputsMultiLanguageRenderNodesWithOccIdentifier() throws { - try assertOutputsMultiLanguageRenderNodes(variantInterfaceLanguage: "occ") + func testOutputsMultiLanguageRenderNodesWithOccIdentifier() async throws { + try await assertOutputsMultiLanguageRenderNodes(variantInterfaceLanguage: "occ") } - func testOutputsMultiLanguageRenderNodesWithObjectiveCIdentifier() throws { - try assertOutputsMultiLanguageRenderNodes(variantInterfaceLanguage: "objective-c") + func testOutputsMultiLanguageRenderNodesWithObjectiveCIdentifier() async throws { + try await assertOutputsMultiLanguageRenderNodes(variantInterfaceLanguage: "objective-c") } - func testOutputsMultiLanguageRenderNodesWithCIdentifier() throws { - try assertOutputsMultiLanguageRenderNodes(variantInterfaceLanguage: "c") + func testOutputsMultiLanguageRenderNodesWithCIdentifier() async throws { + try await assertOutputsMultiLanguageRenderNodes(variantInterfaceLanguage: "c") } - func testFrameworkRenderNodeHasExpectedContentAcrossLanguages() throws { - let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") + func testFrameworkRenderNodeHasExpectedContentAcrossLanguages() async throws { + let outputConsumer = try await renderNodeConsumer(for: "MixedLanguageFramework") let mixedLanguageFrameworkRenderNode = try outputConsumer.renderNode( withIdentifier: "MixedLanguageFramework" ) @@ -347,8 +347,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testObjectiveCAuthoredRenderNodeHasExpectedContentAcrossLanguages() throws { - let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") + func testObjectiveCAuthoredRenderNodeHasExpectedContentAcrossLanguages() async throws { + let outputConsumer = try await renderNodeConsumer(for: "MixedLanguageFramework") let fooRenderNode = try outputConsumer.renderNode(withIdentifier: "c:@E@Foo") assertExpectedContent( @@ -448,8 +448,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testSymbolLinkWorkInMultipleLanguages() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "MixedLanguageFramework") { url in + func testSymbolLinkWorkInMultipleLanguages() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "MixedLanguageFramework") { url in try """ # ``MixedLanguageFramework/Bar`` @@ -466,12 +466,12 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { """.write(to: url.appendingPathComponent("bar.md"), atomically: true, encoding: .utf8) } - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MixedLanguageFramework/Bar", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MixedLanguageFramework/Bar", sourceLanguage: .swift)) let symbol = try XCTUnwrap(node.semantic as? Symbol) XCTAssert(context.problems.isEmpty, "Encountered unexpected problems: \(context.problems)") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) XCTAssert(context.problems.isEmpty, "Encountered unexpected problems: \(context.problems)") @@ -501,8 +501,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ], "Both spellings of the symbol link should resolve to the canonical reference.") } - func testArticleInMixedLanguageFramework() throws { - let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") { url in + func testArticleInMixedLanguageFramework() async throws { + let outputConsumer = try await renderNodeConsumer(for: "MixedLanguageFramework") { url in try """ # MyArticle @@ -556,7 +556,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { "myStringFunction:error:", ], referenceFragments: [ - "typedef enum Foo : NSString {\n ...\n} Foo;", + "+ myStringFunction:error:", ], failureMessage: { fieldName in "Objective-C variant of 'MyArticle' article has unexpected content for '\(fieldName)'." @@ -564,8 +564,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testAPICollectionInMixedLanguageFramework() throws { - let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") + func testAPICollectionInMixedLanguageFramework() async throws { + let outputConsumer = try await renderNodeConsumer(for: "MixedLanguageFramework") let articleRenderNode = try outputConsumer.renderNode(withTitle: "APICollection") @@ -629,8 +629,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testGeneratedImplementationsCollectionIsCuratedInAllAvailableLanguages() throws { - let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") + func testGeneratedImplementationsCollectionIsCuratedInAllAvailableLanguages() async throws { + let outputConsumer = try await renderNodeConsumer(for: "MixedLanguageFramework") let protocolRenderNode = try outputConsumer.renderNode(withTitle: "MixedLanguageClassConformingToProtocol") @@ -653,8 +653,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testGeneratedImplementationsCollectionDoesNotCurateInAllUnavailableLanguages() throws { - let outputConsumer = try renderNodeConsumer( + func testGeneratedImplementationsCollectionDoesNotCurateInAllUnavailableLanguages() async throws { + let outputConsumer = try await renderNodeConsumer( for: "MixedLanguageFramework", configureBundle: { bundleURL in // Update the clang symbol graph to remove the protocol method requirement, so that it's effectively @@ -696,8 +696,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testAutomaticSeeAlsoOnlyShowsAPIsAvailableInParentsLanguageForSymbol() throws { - let outputConsumer = try renderNodeConsumer(for: "MixedLanguageFramework") + func testAutomaticSeeAlsoOnlyShowsAPIsAvailableInParentsLanguageForSymbol() async throws { + let outputConsumer = try await renderNodeConsumer(for: "MixedLanguageFramework") // Swift-only symbol. XCTAssertEqual( @@ -766,8 +766,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testMultiLanguageChildOfSingleParentSymbolIsCuratedInMultiLanguage() throws { - let outputConsumer = try renderNodeConsumer( + func testMultiLanguageChildOfSingleParentSymbolIsCuratedInMultiLanguage() async throws { + let outputConsumer = try await renderNodeConsumer( for: "MixedLanguageFrameworkSingleLanguageParent" ) @@ -793,8 +793,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testMultiLanguageSymbolWithLanguageSpecificRelationships() throws { - let outputConsumer = try renderNodeConsumer( + func testMultiLanguageSymbolWithLanguageSpecificRelationships() async throws { + let outputConsumer = try await renderNodeConsumer( for: "MixedLanguageFrameworkWithLanguageSpecificRelationships" ) @@ -821,8 +821,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { ) } - func testMultiLanguageSymbolWithLanguageSpecificProtocolRequirements() throws { - let outputConsumer = try renderNodeConsumer( + func testMultiLanguageSymbolWithLanguageSpecificProtocolRequirements() async throws { + let outputConsumer = try await renderNodeConsumer( for: "MixedLanguageFrameworkWithLanguageSpecificRelationships" ) @@ -841,8 +841,8 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { XCTAssert(objectiveCSymbol.relationshipSections.isEmpty) } - func testArticlesWithSupportedLanguagesDirective() throws { - let outputConsumer = try renderNodeConsumer( + func testArticlesWithSupportedLanguagesDirective() async throws { + let outputConsumer = try await renderNodeConsumer( for: "MixedLanguageFrameworkWithArticlesUsingSupportedLanguages" ) @@ -878,91 +878,9 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { defaultLanguage: .swift ) } - - func testArticlesAreIncludedInAllVariantsTopicsSection() throws { - let outputConsumer = try renderNodeConsumer( - for: "MixedLanguageFramework", - configureBundle: { bundleURL in - try """ - # ObjCArticle - - @Metadata { - @SupportedLanguage(objc) - } - - This article has Objective-C as the source language. - - ## Topics - """.write(to: bundleURL.appendingPathComponent("ObjCArticle.md"), atomically: true, encoding: .utf8) - try """ - # SwiftArticle - - @Metadata { - @SupportedLanguage(swift) - } - - This article has Swift as the source language. - """.write(to: bundleURL.appendingPathComponent("SwiftArticle.md"), atomically: true, encoding: .utf8) - try """ - # ``MixedLanguageFramework`` - - This symbol has a Swift and Objective-C variant. - - ## Topics - - - - - - - ``_MixedLanguageFrameworkVersionNumber`` - - ``SwiftOnlyStruct`` - - """.write(to: bundleURL.appendingPathComponent("MixedLanguageFramework.md"), atomically: true, encoding: .utf8) - } - ) - assertIsAvailableInLanguages( - try outputConsumer.renderNode( - withTitle: "ObjCArticle" - ), - languages: ["occ"], - defaultLanguage: .objectiveC - ) - assertIsAvailableInLanguages( - try outputConsumer.renderNode( - withTitle: "_MixedLanguageFrameworkVersionNumber" - ), - languages: ["occ"], - defaultLanguage: .objectiveC - ) - - let renderNode = try outputConsumer.renderNode(withIdentifier: "MixedLanguageFramework") - - // Topic identifiers in the Swift variant of the `MixedLanguageFramework` symbol - let swiftTopicIDs = renderNode.topicSections.flatMap(\.identifiers) - - let data = try renderNode.encodeToJSON() - let variantRenderNode = try RenderNodeVariantOverridesApplier() - .applyVariantOverrides(in: data, for: [.interfaceLanguage("occ")]) - let objCRenderNode = try RenderJSONDecoder.makeDecoder().decode(RenderNode.self, from: variantRenderNode) - // Topic identifiers in the ObjC variant of the `MixedLanguageFramework` symbol - let objCTopicIDs = objCRenderNode.topicSections.flatMap(\.identifiers) - - - // Verify that articles are included in the Topics section of both symbol - // variants regardless of their perceived language. - XCTAssertTrue(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/ObjCArticle")) - XCTAssertTrue(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftArticle")) - XCTAssertTrue(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftArticle")) - XCTAssertTrue(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/ObjCArticle")) - - // Verify that language specific symbols are dropped from the Topics section in the - // variants for languages where the symbol isn't available. - XCTAssertTrue(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct")) - XCTAssertFalse(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber")) - XCTAssertTrue(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber")) - XCTAssertFalse(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct")) - } - func testAutomaticSeeAlsoSectionElementLimit() throws { - let (bundle, context) = try loadBundle(catalog: + func testAutomaticSeeAlsoSectionElementLimit() async throws { + let (_, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: (1...50).map { makeSymbol(id: "symbol-id-\($0)", kind: .class, pathComponents: ["SymbolName\($0)"]) @@ -984,7 +902,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))") - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) let moduleNode = try converter.convert(context.entity(with: moduleReference)) diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeSourceRepositoryTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeSourceRepositoryTests.swift index 0725e946b8..a0aa7c3c8d 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeSourceRepositoryTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeSourceRepositoryTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022-2024 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -14,8 +14,8 @@ import SymbolKit import XCTest class SemaToRenderNodeSourceRepositoryTests: XCTestCase { - func testDoesNotEmitsSourceRepositoryInformationWhenNoSourceIsGiven() throws { - let outputConsumer = try renderNodeConsumer( + func testDoesNotEmitsSourceRepositoryInformationWhenNoSourceIsGiven() async throws { + let outputConsumer = try await renderNodeConsumer( for: "SourceLocations", sourceRepository: nil ) @@ -23,8 +23,8 @@ class SemaToRenderNodeSourceRepositoryTests: XCTestCase { XCTAssertNil(try outputConsumer.renderNode(withTitle: "MyStruct").metadata.remoteSource) } - func testEmitsSourceRepositoryInformationForSymbolsWhenPresent() throws { - let outputConsumer = try renderNodeConsumer( + func testEmitsSourceRepositoryInformationForSymbolsWhenPresent() async throws { + let outputConsumer = try await renderNodeConsumer( for: "SourceLocations", sourceRepository: SourceRepository.github( checkoutPath: "/path/to/checkout", diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift index cc1d497b58..73b4a508ed 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift @@ -15,8 +15,8 @@ import SymbolKit import SwiftDocCTestUtilities class SemaToRenderNodeTests: XCTestCase { - func testCompileTutorial() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testCompileTutorial() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) guard let tutorialDirective = node.markup as? BlockDirective else { @@ -32,7 +32,7 @@ class SemaToRenderNodeTests: XCTestCase { XCTAssertEqual(problems.count, 1, "Found problems \(DiagnosticConsoleWriter.formattedDescription(for: problems)) analyzing tutorial markup") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(tutorial) as! RenderNode @@ -400,8 +400,8 @@ class SemaToRenderNodeTests: XCTestCase { ) } - func testTutorialBackgroundComesFromImageOrVideoPoster() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testTutorialBackgroundComesFromImageOrVideoPoster() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") func assertTutorialWithPath(_ tutorialPath: String, hasBackground backgroundIdentifier: String) throws { let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: tutorialPath, sourceLanguage: .swift)) @@ -417,7 +417,7 @@ class SemaToRenderNodeTests: XCTestCase { return } - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(tutorial) as! RenderNode let intro = renderNode.sections.compactMap { $0 as? IntroRenderSection }.first! XCTAssertEqual(RenderReferenceIdentifier(backgroundIdentifier), intro.backgroundImage) @@ -430,13 +430,13 @@ class SemaToRenderNodeTests: XCTestCase { try assertTutorialWithPath("/tutorials/Test-Bundle/TestTutorial2", hasBackground: "introposter2.png") } - func testCompileTutorialArticle() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testCompileTutorialArticle() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorialArticle", sourceLanguage: .swift)) let article = node.semantic as! TutorialArticle - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(article) as! RenderNode @@ -491,13 +491,13 @@ class SemaToRenderNodeTests: XCTestCase { ]) } - func testCompileOverviewWithNoVolumes() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testCompileOverviewWithNoVolumes() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") try assertCompileOverviewWithNoVolumes(bundle: bundle, context: context) } - func testCompileOverviewWithEmptyChapter() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testCompileOverviewWithEmptyChapter() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in try """ @Tutorials(name: "Technology X") { @Intro(title: "Technology X") { @@ -586,7 +586,7 @@ class SemaToRenderNodeTests: XCTestCase { // Verify we emit a diagnostic for the chapter with no tutorial references. XCTAssertEqual(problems.count, expectedProblemsCount, "Found problems \(DiagnosticConsoleWriter.formattedDescription(for: problems)) analyzing tutorial markup") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(tutorialTableOfContents) as! RenderNode @@ -717,8 +717,8 @@ class SemaToRenderNodeTests: XCTestCase { ) } - func testCompileOverviewWithVolumes() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testCompileOverviewWithVolumes() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in let overviewURL = root.appendingPathComponent("TestOverview.tutorial") let text = """ @Tutorials(name: "Technology X") { @@ -822,7 +822,7 @@ class SemaToRenderNodeTests: XCTestCase { XCTAssertEqual(problems.count, 0, "Found problems \(DiagnosticConsoleWriter.formattedDescription(for: problems)) analyzing tutorial markup") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(tutorialTableOfContents) as! RenderNode @@ -926,8 +926,8 @@ class SemaToRenderNodeTests: XCTestCase { ) } - func testCompileSymbol() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testCompileSymbol() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in // Remove the SideClass sub heading to match the expectations of this test let graphURL = url.appendingPathComponent("sidekit.symbols.json") var graph = try JSONDecoder().decode(SymbolGraph.self, from: try Data(contentsOf: graphURL)) @@ -958,7 +958,7 @@ class SemaToRenderNodeTests: XCTestCase { let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode guard renderNode.primaryContentSections.count == 4 else { @@ -998,7 +998,7 @@ class SemaToRenderNodeTests: XCTestCase { return } - XCTAssertEqual(declarations.declarations[0].platforms, [PlatformName(operatingSystemName: "ios")]) + XCTAssertEqual(Set(declarations.declarations[0].platforms), Set([PlatformName(operatingSystemName: "ios"), PlatformName.iPadOS, PlatformName.catalyst])) XCTAssertEqual(declarations.declarations[0].tokens.count, 5) XCTAssertEqual(declarations.declarations[0].tokens.map { $0.text }.joined(), "protocol MyProtocol : Hashable") XCTAssertEqual(declarations.declarations[0].languages?.first, "swift") @@ -1172,23 +1172,19 @@ class SemaToRenderNodeTests: XCTestCase { ) } - func testCompileSymbolWithExternalReferences() throws { + func testCompileSymbolWithExternalReferences() async throws { class TestSymbolResolver: GlobalExternalSymbolResolver { func symbolReferenceAndEntity(withPreciseIdentifier preciseIdentifier: String) -> (ResolvedTopicReference, LinkResolver.ExternalEntity)? { let reference = ResolvedTopicReference(bundleID: "com.test.external.symbols", path: "/\(preciseIdentifier)", sourceLanguage: .objectiveC) let entity = LinkResolver.ExternalEntity( - topicRenderReference: TopicRenderReference( - identifier: .init(reference.absoluteString), - title: "SymbolName ( \(preciseIdentifier) )", - abstract: [], - url: "/documentation/FrameworkName/path/to/symbol/\(preciseIdentifier)", - kind: .symbol, - role: "ExternalResolvedSymbolRoleHeading", - estimatedTime: nil - ), - renderReferenceDependencies: .init(), - sourceLanguages: [.objectiveC] + kind: .class, + language: .objectiveC, + relativePresentationURL: URL(string: "/documentation/FrameworkName/path/to/symbol/\(preciseIdentifier)")!, + referenceURL: reference.url, + title: "SymbolName ( \(preciseIdentifier) )", + availableLanguages: [.objectiveC], + variants: [] ) return (reference, entity) } @@ -1206,26 +1202,22 @@ class SemaToRenderNodeTests: XCTestCase { } func entity(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity { - let (kind, role) = DocumentationContentRenderer.renderKindAndRole(.collection, semantic: nil) - return LinkResolver.ExternalEntity( - topicRenderReference: TopicRenderReference( - identifier: .init(reference.absoluteString), - title: "Title for \(reference.url.path)", - abstract: [.text("Abstract for \(reference.url.path)")], - url: reference.url.path, - kind: kind, - role: role, - estimatedTime: nil - ), - renderReferenceDependencies: .init(), - sourceLanguages: [.swift] + LinkResolver.ExternalEntity( + kind: .collection, + language: .swift, + relativePresentationURL: reference.url.withoutHostAndPortAndScheme(), + referenceURL: reference.url, + title: "Title for \(reference.url.path)", + abstract: [.text("Abstract for \(reference.url.path)")], + availableLanguages: [.swift], + variants: [] ) } } let testBundleURL = Bundle.module.url( forResource: "LegacyBundle_DoNotUseInNewTests", withExtension: "docc", subdirectory: "Test Bundles")! - let (_, bundle, context) = try loadBundle( + let (_, _, context) = try await loadBundle( from: testBundleURL, externalResolvers: ["com.test.external": TestReferenceResolver()], externalSymbolResolver: TestSymbolResolver() @@ -1245,7 +1237,7 @@ class SemaToRenderNodeTests: XCTestCase { XCTAssertNotNil(context.externalCache["s:10Foundation4DataV"]) XCTAssertNotNil(context.externalCache["s:5Foundation0A5NSCodableP"]) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: myProtocol.reference) + var translator = RenderNodeTranslator(context: context, identifier: myProtocol.reference) let renderNode = translator.visit(myProtocolSymbol) as! RenderNode guard renderNode.primaryContentSections.count == 4 else { @@ -1265,7 +1257,7 @@ class SemaToRenderNodeTests: XCTestCase { return } - XCTAssertEqual(declarations.declarations[0].platforms, [PlatformName(operatingSystemName: "ios")]) + XCTAssertEqual(Set(declarations.declarations[0].platforms), Set([PlatformName(operatingSystemName: "ios"), PlatformName.iPadOS, PlatformName.catalyst])) XCTAssertEqual(declarations.declarations[0].tokens.count, 5) XCTAssertEqual(declarations.declarations[0].tokens.map { $0.text }.joined(), "protocol MyProtocol : Hashable") XCTAssertEqual(declarations.declarations[0].languages?.first, "swift") @@ -1321,15 +1313,15 @@ class SemaToRenderNodeTests: XCTestCase { ) } - func testRenderConstraints() throws { + func testRenderConstraints() async throws { // Check for constraints in render node - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode guard let conf = renderNode.metadata.conformance else { @@ -1349,7 +1341,7 @@ class SemaToRenderNodeTests: XCTestCase { let parent = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) let parentSymbol = parent.semantic as! Symbol - var parentTranslator = RenderNodeTranslator(context: context, bundle: bundle, identifier: parent.reference) + var parentTranslator = RenderNodeTranslator(context: context, identifier: parent.reference) let parentRenderNode = parentTranslator.visit(parentSymbol) as! RenderNode guard let functionReference = parentRenderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass/myFunction()"] as? TopicRenderReference else { @@ -1377,11 +1369,11 @@ class SemaToRenderNodeTests: XCTestCase { XCTAssertEqual(constraintsString, "Label is Text, Observer inherits NSObject, and S conforms to StringProtocol.") } - func testRenderConditionalConstraintsOnConformingType() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testRenderConditionalConstraintsOnConformingType() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyProtocol", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode // Test conditional conformance for the conforming type @@ -1399,11 +1391,11 @@ class SemaToRenderNodeTests: XCTestCase { }.joined(), "Element conforms to Equatable.") } - func testRenderConditionalConstraintsOnProtocol() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testRenderConditionalConstraintsOnProtocol() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode // Test conditional conformance for the conforming type @@ -1421,13 +1413,13 @@ class SemaToRenderNodeTests: XCTestCase { }.joined(), "Element conforms to Equatable.") } - func testRenderReferenceResolving() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testRenderReferenceResolving() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) // Compile docs and verify contents let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode @@ -1486,13 +1478,13 @@ class SemaToRenderNodeTests: XCTestCase { ]) } - func testAvailabilityMetadata() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testAvailabilityMetadata() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) // Compile docs and verify contents let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode @@ -1528,14 +1520,14 @@ class SemaToRenderNodeTests: XCTestCase { XCTAssertEqual(platforms[5].introduced, "6.0") } - func testAvailabilityFromCurrentPlatformOverridesExistingValue() throws { + func testAvailabilityFromCurrentPlatformOverridesExistingValue() async throws { // The `MyClass` symbol has availability information for all platforms. Copy the symbol graph for each platform and override only the // availability for that platform to verify that the end result preferred the information for each platform. let allPlatformsNames: [(platformName: String, operatingSystemName: String)] = [("iOS", "ios"), ("macOS", "macosx"), ("watchOS", "watchos"), ("tvOS", "tvos")] // Override with both a low and a high value for version in [SymbolGraph.SemanticVersion(major: 1, minor: 1, patch: 1), SymbolGraph.SemanticVersion(major: 99, minor: 99, patch: 99)] { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { url in + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { url in // Duplicate the symbol graph let myKitURL = url.appendingPathComponent("mykit-iOS.symbols.json") let myClassUSR = "s:5MyKit0A5ClassC" @@ -1565,7 +1557,7 @@ class SemaToRenderNodeTests: XCTestCase { // Compile docs and verify contents let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode @@ -1586,8 +1578,8 @@ class SemaToRenderNodeTests: XCTestCase { } } - func testMediaReferencesWithSpaces() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testMediaReferencesWithSpaces() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TutorialMediaWithSpaces", sourceLanguage: .swift)) guard let tutorialDirective = node.markup as? BlockDirective else { @@ -1603,7 +1595,7 @@ class SemaToRenderNodeTests: XCTestCase { XCTAssertTrue(problems.isEmpty) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(tutorial) as! RenderNode @@ -1611,7 +1603,7 @@ class SemaToRenderNodeTests: XCTestCase { renderNode.references.keys.filter({ !$0.hasPrefix("doc://") }).sorted()) } - func testUnexpectedDirectivesAreDropped() throws { + func testUnexpectedDirectivesAreDropped() async throws { let source = """ This is some text. @@ -1648,8 +1640,8 @@ Document @1:1-11:19 """, markup.debugDescription(options: .printSourceLocations)) - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestTutorial", sourceLanguage: .swift)) + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + var contentTranslator = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestTutorial", sourceLanguage: .swift)) let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) let expectedContent: [RenderBlockContent] = [ .paragraph(.init(inlineContent: [ @@ -1667,7 +1659,7 @@ Document @1:1-11:19 XCTAssertEqual(expectedContent, renderContent) } - func testTaskLists() throws { + func testTaskLists() async throws { let source = """ This is some text. @@ -1690,8 +1682,8 @@ Document """, markup.debugDescription()) - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestTutorial", sourceLanguage: .swift)) + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + var contentTranslator = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestTutorial", sourceLanguage: .swift)) let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) let expectedContent: [RenderBlockContent] = [ .paragraph(.init(inlineContent: [ @@ -1705,7 +1697,7 @@ Document XCTAssertEqual(expectedContent, renderContent) } - func testInlineHTMLDoesNotCrashTranslator() throws { + func testInlineHTMLDoesNotCrashTranslator() async throws { let markupSource = """ # Test @@ -1715,19 +1707,19 @@ Document let document = Document(parsing: markupSource, options: []) let node = DocumentationNode(reference: ResolvedTopicReference(bundleID: "org.swift.docc", path: "/blah", sourceLanguage: .swift), kind: .article, sourceLanguage: .swift, name: .conceptual(title: "Title"), markup: document, semantic: Semantic()) - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + var translator = RenderNodeTranslator(context: context, identifier: node.reference) XCTAssertNotNil(translator.visit(MarkupContainer(document.children))) } - func testCompileSymbolMetadata() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyProtocol", sourceLanguage: .swift)) + func testCompileSymbolMetadata() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyProtocol", sourceLanguage: .swift)) // Compile docs and verify contents let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode @@ -1770,8 +1762,8 @@ Document ]) } - func testArticleRoleHeadings() throws { - try assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: "Article", content: """ + func testArticleRoleHeadings() async throws { + try await assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: "Article", content: """ # Article 2 This is article 2. @@ -1779,8 +1771,8 @@ Document ) } - func testArticleRoleHeadingsWithAutomaticTitleHeadingDisabled() throws { - try assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: nil, content: """ + func testArticleRoleHeadingsWithAutomaticTitleHeadingDisabled() async throws { + try await assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: nil, content: """ # Article 2 @Options { @@ -1792,8 +1784,8 @@ Document ) } - func testArticleRoleHeadingsWithAutomaticTitleHeadingForPageKind() throws { - try assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: "Article", content: """ + func testArticleRoleHeadingsWithAutomaticTitleHeadingForPageKind() async throws { + try await assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: "Article", content: """ # Article 2 @Options { @@ -1805,8 +1797,8 @@ Document ) } - func testAPICollectionRoleHeading() throws { - try assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: nil, content: """ + func testAPICollectionRoleHeading() async throws { + try await assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: nil, content: """ # Article 2 This is article 2. @@ -1819,7 +1811,7 @@ Document ) } - private func renderNodeForArticleInTestBundle(content: String) throws -> RenderNode { + private func renderNodeForArticleInTestBundle(content: String) async throws -> RenderNode { // Overwrite the article so we can test the article eyebrow for articles without task groups let sourceURL = Bundle.module.url( forResource: "LegacyBundle_DoNotUseInNewTests", withExtension: "docc", subdirectory: "Test Bundles")! @@ -1829,10 +1821,10 @@ Document try content.write(to: targetURL.appendingPathComponent("article2.md"), atomically: true, encoding: .utf8) - let (_, bundle, context) = try loadBundle(from: targetURL) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Test-Bundle/article2", sourceLanguage: .swift)) + let (_, _, context) = try await loadBundle(from: targetURL) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/Test-Bundle/article2", sourceLanguage: .swift)) let article = node.semantic as! Article - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) return translator.visit(article) as! RenderNode } @@ -1840,16 +1832,16 @@ Document Asserts if `expectedRoleHeading` does not match the parsed render node's `roleHeading` after it's parsed. Uses 'TestBundle's documentation as a base for compiling, overwriting 'article2' with `content`. */ - private func assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: String?, content: String, file: StaticString = #filePath, line: UInt = #line) throws { - let renderNode = try renderNodeForArticleInTestBundle(content: content) + private func assertRoleHeadingForArticleInTestBundle(expectedRoleHeading: String?, content: String, file: StaticString = #filePath, line: UInt = #line) async throws { + let renderNode = try await renderNodeForArticleInTestBundle(content: content) XCTAssertEqual(expectedRoleHeading, renderNode.metadata.roleHeading, file: (file), line: line) } - func testDisablingAutomaticArticleSubheadingGeneration() throws { + func testDisablingAutomaticArticleSubheadingGeneration() async throws { // Assert that by default, articles include an "Overview" heading even if it's not authored. do { - let articleRenderNode = try renderNodeForArticleInTestBundle( + let articleRenderNode = try await renderNodeForArticleInTestBundle( content: """ # Article 2 @@ -1873,7 +1865,7 @@ Document // Assert that disabling the automatic behavior with the option directive works as expected. do { - let articleRenderNode = try renderNodeForArticleInTestBundle( + let articleRenderNode = try await renderNodeForArticleInTestBundle( content: """ # Article 2 @@ -1900,7 +1892,7 @@ Document } /// Verifies we emit the correct warning for external links in topic task groups. - func testWarnForExternalLinksInTopicTaskGroups() throws { + func testWarnForExternalLinksInTopicTaskGroups() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph(moduleName: "SomeModuleName", symbols: [ ])), @@ -1916,7 +1908,7 @@ Document """), ]) - let (_, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.problems.filter({ $0.diagnostic.identifier == "org.swift.docc.InvalidDocumentationLink" }).count, 1) XCTAssertNotNil(context.problems.first(where: { problem -> Bool in @@ -1925,8 +1917,8 @@ Document })) } - func testRendersBetaViolators() throws { - func makeTestBundle(currentPlatforms: [String : PlatformVersion]?, file: StaticString = #filePath, line: UInt = #line, referencePath: String) throws -> (DocumentationBundle, DocumentationContext, ResolvedTopicReference) { + func testRendersBetaViolators() async throws { + func makeTestBundle(currentPlatforms: [String : PlatformVersion]?, file: StaticString = #filePath, line: UInt = #line, referencePath: String) async throws -> (DocumentationContext, ResolvedTopicReference) { var configuration = DocumentationContext.Configuration() // Add missing platforms if their fallback platform is present. var currentPlatforms = currentPlatforms ?? [:] @@ -1935,33 +1927,33 @@ Document } configuration.externalMetadata.currentPlatforms = currentPlatforms - let (_, bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests", configuration: configuration) + let (_, _, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests", configuration: configuration) - let reference = ResolvedTopicReference(bundleID: bundle.id, path: referencePath, sourceLanguage: .swift) - return (bundle, context, reference) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: referencePath, sourceLanguage: .swift) + return (context, reference) } // Not a beta platform do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: nil, referencePath: "/documentation/MyKit/globalFunction(_:considering:)") + let (context, reference) = try await makeTestBundle(currentPlatforms: nil, referencePath: "/documentation/MyKit/globalFunction(_:considering:)") let node = try context.entity(with: reference) - let renderNode = DocumentationNodeConverter(bundle: bundle, context: context).convert(node) + let renderNode = DocumentationNodeConverter(context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, false) } - // Symbol with an empty set of availbility items. + // Symbol with an empty set of availability items. do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "Custom Name": PlatformVersion(VersionTriplet(100, 0, 0), beta: true) ], referencePath: "/documentation/MyKit/globalFunction(_:considering:)") let node = try context.entity(with: reference) (node.semantic as? Symbol)?.availability = SymbolGraph.Symbol.Availability(availability: []) - let documentationContentRendered = DocumentationContentRenderer(documentationContext: context, bundle: bundle) + let documentationContentRendered = DocumentationContentRenderer(context: context) let isBeta = documentationContentRendered.isBeta(node) // Verify that the symbol is not beta since it does not contains availability info. XCTAssertFalse(isBeta) @@ -1969,12 +1961,12 @@ Document // Different platform is beta do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "tvOS": PlatformVersion(VersionTriplet(100, 0, 0), beta: true) ], referencePath: "/documentation/MyKit/globalFunction(_:considering:)") let node = try context.entity(with: reference) - let renderNode = DocumentationNodeConverter(bundle: bundle, context: context).convert(node) + let renderNode = DocumentationNodeConverter(context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, false) @@ -1983,12 +1975,12 @@ Document // Beta platform but *not* matching the introduced version do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(100, 0, 0), beta: true) ], referencePath: "/documentation/MyKit/globalFunction(_:considering:)") let node = try context.entity(with: reference) - let renderNode = DocumentationNodeConverter(bundle: bundle, context: context).convert(node) + let renderNode = DocumentationNodeConverter(context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first?.isBeta, false) @@ -1997,12 +1989,12 @@ Document // Beta platform matching the introduced version do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true) ], referencePath: "/documentation/MyKit/globalFunction(_:considering:)") let node = try context.entity(with: reference) - let renderNode = DocumentationNodeConverter(bundle: bundle, context: context).convert(node) + let renderNode = DocumentationNodeConverter(context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first(where: { $0.name == "macOS"})?.isBeta, true) @@ -2011,12 +2003,12 @@ Document // Beta platform earlier than the introduced version do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 14, 0), beta: true) ], referencePath: "/documentation/MyKit/globalFunction(_:considering:)") let node = try context.entity(with: reference) - let renderNode = DocumentationNodeConverter(bundle: bundle, context: context).convert(node) + let renderNode = DocumentationNodeConverter(context: context).convert(node) // Verify platform beta was plumbed all the way to the render JSON XCTAssertEqual(renderNode.metadata.platforms?.first(where: { $0.name == "macOS" })?.isBeta, true) @@ -2025,14 +2017,14 @@ Document // Set only some platforms to beta & the exact version globalFunction is being introduced at do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true), "watchOS": PlatformVersion(VersionTriplet(9, 0, 0), beta: true), "tvOS": PlatformVersion(VersionTriplet(1, 0, 0), beta: true), ], referencePath: "/documentation/MyKit/globalFunction(_:considering:)") let node = try context.entity(with: reference) - let renderNode = DocumentationNodeConverter(bundle: bundle, context: context).convert(node) + let renderNode = DocumentationNodeConverter(context: context).convert(node) // Verify task group link is not in beta betas "iOS" is not being marked as beta XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/globalFunction(_:considering:)"] as? TopicRenderReference)?.isBeta, false) @@ -2040,7 +2032,7 @@ Document // Set all platforms to beta & the exact version globalFunction is being introduced at to test beta SDK documentation do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true), "watchOS": PlatformVersion(VersionTriplet(6, 0, 0), beta: true), "tvOS": PlatformVersion(VersionTriplet(13, 0, 0), beta: true), @@ -2048,7 +2040,7 @@ Document ], referencePath: "/documentation/MyKit/globalFunction(_:considering:)") let node = try context.entity(with: reference) - let renderNode = try XCTUnwrap(DocumentationNodeConverter(bundle: bundle, context: context).convert(node)) + let renderNode = try XCTUnwrap(DocumentationNodeConverter(context: context).convert(node)) // Verify task group link is beta XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/globalFunction(_:considering:)"] as? TopicRenderReference)?.isBeta, true) @@ -2056,7 +2048,7 @@ Document // Set all platforms to beta where the symbol is available, // some platforms not beta but the symbol is not available there. - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true), "watchOS": PlatformVersion(VersionTriplet(6, 0, 0), beta: true), "tvOS": PlatformVersion(VersionTriplet(13, 0, 0), beta: true), @@ -2066,7 +2058,7 @@ Document ], referencePath: "/documentation/MyKit/globalFunction(_:considering:)") let node = try context.entity(with: reference) - let renderNode = try XCTUnwrap(DocumentationNodeConverter(bundle: bundle, context: context).convert(node)) + let renderNode = try XCTUnwrap(DocumentationNodeConverter(context: context).convert(node)) // Verify task group link is beta XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/globalFunction(_:considering:)"] as? TopicRenderReference)?.isBeta, true) @@ -2079,16 +2071,16 @@ Document renderReferenceSymbol.availability?.availability.append(SymbolGraph.Symbol.Availability.AvailabilityItem(domain: SymbolGraph.Symbol.Availability.Domain(rawValue: "ImaginaryOS"), introducedVersion: nil, deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: true, willEventuallyBeDeprecated: false)) // Verify the rendered reference - let renderNode = try XCTUnwrap(DocumentationNodeConverter(bundle: bundle, context: context).convert(node)) + let renderNode = try XCTUnwrap(DocumentationNodeConverter(context: context).convert(node)) // Verify task group link is beta XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/globalFunction(_:considering:)"] as? TopicRenderReference)?.isBeta, true) } // Set all platforms to beta & the exact version MyClass is being introduced. - // Expect the symbol to no be in beta sinceit does not have an introduced version for iOS + // Expect the symbol to no be in beta since it does not have an introduced version for iOS do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 15, 0), beta: true), "watchOS": PlatformVersion(VersionTriplet(6, 0, 0), beta: true), "tvOS": PlatformVersion(VersionTriplet(13, 0, 0), beta: true), @@ -2096,7 +2088,7 @@ Document ], referencePath: "/documentation/MyKit") let node = try context.entity(with: reference) - let renderNode = try XCTUnwrap(DocumentationNodeConverter(bundle: bundle, context: context).convert(node)) + let renderNode = try XCTUnwrap(DocumentationNodeConverter(context: context).convert(node)) // Verify task group link is not in beta because `iOS` does not have an introduced version XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass"] as? TopicRenderReference)?.isBeta, false) @@ -2104,47 +2096,47 @@ Document // Set all platforms as unconditionally unavailable and test that the symbol is not marked as beta. do { - let (bundle, context, reference) = try makeTestBundle(currentPlatforms: [ + let (context, reference) = try await makeTestBundle(currentPlatforms: [ "iOS": PlatformVersion(VersionTriplet(100, 0, 0), beta: true) ], referencePath: "/documentation/MyKit/MyClass") let node = try context.entity(with: reference) (node.semantic as? Symbol)?.availability = SymbolGraph.Symbol.Availability(availability: [.init(domain: SymbolGraph.Symbol.Availability.Domain(rawValue: "iOS"), introducedVersion: nil, deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: true, willEventuallyBeDeprecated: false)]) - let documentationContentRendered = DocumentationContentRenderer(documentationContext: context, bundle: bundle) + let documentationContentRendered = DocumentationContentRenderer(context: context) let isBeta = documentationContentRendered.isBeta(node) // Verify that the symbol is not beta since it's unavailable in all the platforms. XCTAssertFalse(isBeta) } } - func testRendersDeprecatedViolator() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testRendersDeprecatedViolator() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // Make the referenced symbol deprecated do { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift) let node = try context.entity(with: reference) (node.semantic as? Symbol)?.availability = SymbolGraph.Symbol.Availability(availability: [ SymbolGraph.Symbol.Availability.AvailabilityItem(domain: .init(rawValue: "iOS"), introducedVersion: nil, deprecatedVersion: .init(major: 13, minor: 0, patch: 0), obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false), ]) } - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift) let node = try context.entity(with: reference) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode // The reference is deprecated on all platforms XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass/myFunction()"] as? TopicRenderReference)?.isDeprecated, true) } - func testDoesNotRenderDeprecatedViolator() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testDoesNotRenderDeprecatedViolator() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // Make the referenced symbol deprecated do { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift) let node = try context.entity(with: reference) (node.semantic as? Symbol)?.availability = SymbolGraph.Symbol.Availability(availability: [ SymbolGraph.Symbol.Availability.AvailabilityItem(domain: .init(rawValue: "iOS"), introducedVersion: .init(major: 13, minor: 0, patch: 0), deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false), @@ -2152,23 +2144,23 @@ Document ]) } - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift) let node = try context.entity(with: reference) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode // The reference is not deprecated on all platforms XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass/myFunction()"] as? TopicRenderReference)?.isDeprecated, false) } - func testRendersDeprecatedViolatorForUnconditionallyDeprecatedReference() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testRendersDeprecatedViolatorForUnconditionallyDeprecatedReference() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // Make the referenced symbol deprecated do { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift) let node = try context.entity(with: reference) (node.semantic as? Symbol)?.availability = SymbolGraph.Symbol.Availability(availability: [ SymbolGraph.Symbol.Availability.AvailabilityItem(domain: .init(rawValue: "iOS"), introducedVersion: .init(major: 13, minor: 0, patch: 0), deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: true, isUnconditionallyUnavailable: false, willEventuallyBeDeprecated: false), @@ -2176,25 +2168,24 @@ Document ]) } - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift) let node = try context.entity(with: reference) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode // Verify that the reference is deprecated on all platforms XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyClass/myFunction()"] as? TopicRenderReference)?.isDeprecated, true) } - func testRenderMetadataFragments() throws { - + func testRenderMetadataFragments() async throws { // Check for fragments in metadata in render node - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode guard let fragments = renderNode.metadata.fragments else { @@ -2209,25 +2200,25 @@ Document ]) } - func testRenderMetadataExtendedModule() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) + func testRenderMetadataExtendedModule() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) XCTAssertEqual(renderNode.metadata.extendedModule, "MyKit") } - func testDefaultImplementations() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testDefaultImplementations() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // Verify that the render reference to a required symbol includes the 'required' key and the number of default implementations provided. do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideProtocol", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideProtocol", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode let requiredFuncReference = try XCTUnwrap(renderNode.references["doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func()"]) @@ -2238,9 +2229,9 @@ Document // Verify that a required symbol includes a required metadata and default implementations do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideProtocol/func()", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideProtocol/func()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode // Verify that the render reference to a required symbol includes the 'required' key and the number of default implementations provided. @@ -2253,14 +2244,14 @@ Document } } - func testDefaultImplementationsNotListedInTopics() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testDefaultImplementationsNotListedInTopics() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // Verify that a required symbol does not include default implementations in Topics groups do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideProtocol/func()", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideProtocol/func()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode // Test that default implementations are listed ONLY under Default Implementations and not Topics @@ -2269,14 +2260,13 @@ Document } } - func testNoStringMetadata() throws { - + func testNoStringMetadata() async throws { // Check for fragments in metadata in render node - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode let encoded = try JSONEncoder().encode(renderNode) @@ -2298,14 +2288,13 @@ Document XCTAssertEqual(extra, roundtripMetadata as? [String]) } - func testRenderDeclarations() throws { - + func testRenderDeclarations() async throws { // Check for fragments in metadata in render node - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode @@ -2318,13 +2307,13 @@ Document XCTAssertEqual(section.declarations.first?.languages, ["swift"]) } - func testDocumentationRenderReferenceRoles() throws { + func testDocumentationRenderReferenceRoles() async throws { // Check for fragments in metadata in render node - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit", sourceLanguage: .swift)) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode @@ -2338,13 +2327,13 @@ Document XCTAssertEqual(roleFor("doc://org.swift.docc.example/documentation/Test-Bundle/Default-Code-Listing-Syntax"), "article") } - func testTutorialsRenderReferenceRoles() throws { + func testTutorialsRenderReferenceRoles() async throws { // Check for fragments in metadata in render node - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/TestOverview", sourceLanguage: .swift)) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/TestOverview", sourceLanguage: .swift)) let symbol = node.semantic as! TutorialTableOfContents - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode @@ -2357,11 +2346,11 @@ Document XCTAssertEqual(roleFor("doc://org.swift.docc.example/tutorials/TestOverview"), "overview") } - func testRemovingTrailingNewLinesInDeclaration() throws { + func testRemovingTrailingNewLinesInDeclaration() async throws { // Check for fragments in metadata in render node - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/globalFunction(_:considering:)", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/globalFunction(_:considering:)", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol // Subheading with trailing "\n" @@ -2370,7 +2359,7 @@ Document // Navigator title with trailing "\n" XCTAssertEqual(symbol.navigator?.count, 11) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode // Verify trailing newline removed from subheading @@ -2380,13 +2369,13 @@ Document XCTAssertEqual(renderNode.metadata.navigatorTitle?.count, 10) } - func testRenderManualSeeAlsoInArticles() throws { + func testRenderManualSeeAlsoInArticles() async throws { // Check for fragments in metadata in render node - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Test-Bundle/article", sourceLanguage: .swift)) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/Test-Bundle/article", sourceLanguage: .swift)) let article = node.semantic as! Article - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(article) as! RenderNode @@ -2403,12 +2392,12 @@ Document XCTAssertEqual(link.titleInlineContent, [.text("Website")]) } - func testSafeSectionAnchorNames() throws { + func testSafeSectionAnchorNames() async throws { // Check that heading's anchor was safe-ified - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode @@ -2424,14 +2413,14 @@ Document }) } - func testDuplicateNavigatorTitleIsRemoved() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testDuplicateNavigatorTitleIsRemoved() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let myFuncReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/globalFunction(_:considering:)", sourceLanguage: .swift) + let myFuncReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/globalFunction(_:considering:)", sourceLanguage: .swift) let node = try context.entity(with: myFuncReference) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) translator.collectedTopicReferences.append(myFuncReference) let renderNode = translator.visit(symbol) as! RenderNode @@ -2440,14 +2429,14 @@ Document XCTAssertNil(renderReference.navigatorTitle) } - func testNonDuplicateNavigatorTitleIsRendered() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testNonDuplicateNavigatorTitleIsRendered() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let myFuncReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyProtocol", sourceLanguage: .swift) + let myFuncReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyProtocol", sourceLanguage: .swift) let node = try context.entity(with: myFuncReference) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode let renderReference = try XCTUnwrap(renderNode.references[myFuncReference.absoluteString] as? TopicRenderReference) @@ -2483,8 +2472,8 @@ Document .aside(.init(style: .init(rawValue: "Throws"), content: [.paragraph(.init(inlineContent: [.text("A serious error.")]))])), ] - func testBareTechnology() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testBareTechnology() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in try """ @Tutorials(name: "<#text#>") { @Intro(title: "<#text#>") { @@ -2500,7 +2489,7 @@ Document """.write(to: url.appendingPathComponent("TestOverview.tutorial"), atomically: true, encoding: .utf8) } - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/TestOverview", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/TestOverview", sourceLanguage: .swift)) guard let tutorialTableOfContentsDirective = node.markup as? BlockDirective else { XCTFail("Unexpected document structure, tutorial table-of-contents not found as first child.") @@ -2508,38 +2497,38 @@ Document } var problems = [Problem]() - guard let tutorialTableOfContents = TutorialTableOfContents(from: tutorialTableOfContentsDirective, source: nil, for: bundle, problems: &problems) else { + guard let tutorialTableOfContents = TutorialTableOfContents(from: tutorialTableOfContentsDirective, source: nil, for: context.inputs, problems: &problems) else { XCTFail("Couldn't create tutorial from markup: \(problems)") return } XCTAssert(problems.filter { $0.diagnostic.severity == .error }.isEmpty, "Found errors when analyzing Tutorials overview.") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) // Verify we don't crash. _ = translator.visit(tutorialTableOfContents) do { - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) guard let technologyDirective = node.markup as? BlockDirective else { XCTFail("Unexpected document structure, tutorial not found as first child.") return } - guard let tutorial = Tutorial(from: technologyDirective, source: nil, for: bundle, problems: &problems) else { + guard let tutorial = Tutorial(from: technologyDirective, source: nil, for: context.inputs, problems: &problems) else { XCTFail("Couldn't create tutorial from markup: \(problems)") return } - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) XCTAssertNil(translator.visit(tutorial), "Render node for uncurated tutorial should not have been produced") } } - func testBareTutorial() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testBareTutorial() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in try """ @Tutorial(time: <#number#>, projectFiles: <#.zip#>) { @Intro(title: "<#text#>") { @@ -2592,7 +2581,7 @@ Document """.write(to: url.appendingPathComponent("TestTutorial.tutorial"), atomically: true, encoding: .utf8) } - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/Test-Bundle/TestTutorial", sourceLanguage: .swift)) guard let technologyDirective = node.markup as? BlockDirective else { XCTFail("Unexpected document structure, tutorial not found as first child.") @@ -2600,52 +2589,49 @@ Document } var problems = [Problem]() - guard let tutorial = Tutorial(from: technologyDirective, source: nil, for: bundle, problems: &problems) else { + guard let tutorial = Tutorial(from: technologyDirective, source: nil, for: context.inputs, problems: &problems) else { XCTFail("Couldn't create tutorial from markup: \(problems)") return } XCTAssert(problems.filter { $0.diagnostic.severity == .error }.isEmpty, "Found errors when analyzing tutorial.") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) // Verify we don't crash. _ = translator.visit(tutorial) } /// Ensures we render our supported asides from symbol-graph content correctly, whether as a blockquote or as a list item. - func testRenderAsides() throws { + func testRenderAsides() async throws { let asidesSGFURL = Bundle.module.url( forResource: "Asides.symbols", withExtension: "json", subdirectory: "Test Resources")! - let (bundleURL, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { url in try? FileManager.default.copyItem(at: asidesSGFURL, to: url.appendingPathComponent("Asides.symbols.json")) } - defer { - try? FileManager.default.removeItem(at: bundleURL) - } // Both of these symbols have the same content; one just has its asides as list items and the other has blockquotes. let testReference: (ResolvedTopicReference) throws -> () = { myFuncReference in let node = try context.entity(with: myFuncReference) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode let asides = try XCTUnwrap(renderNode.primaryContentSections.first(where: { $0.kind == .content }) as? ContentRenderSection) XCTAssertEqual(Array(asides.content.dropFirst()), self.asidesStressTest) } - let dashReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Asides/dashAsides()", sourceLanguage: .swift) - let quoteReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Asides/quoteAsides()", sourceLanguage: .swift) + let dashReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/Asides/dashAsides()", sourceLanguage: .swift) + let quoteReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/Asides/quoteAsides()", sourceLanguage: .swift) try testReference(dashReference) try testReference(quoteReference) } /// Tests parsing origin data from symbol graph. - func testOriginMetadata() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testOriginMetadata() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let myFuncReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) let node = try context.entity(with: myFuncReference) @@ -2658,24 +2644,24 @@ Document } /// Tests that we inherit docs by default from within the same module. - func testDocInheritanceInsideModule() throws { + func testDocInheritanceInsideModule() async throws { let sgURL = Bundle.module.url( forResource: "LegacyBundle_DoNotUseInNewTests.docc/sidekit.symbols", withExtension: "json", subdirectory: "Test Bundles")! - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in // Replace the out-of-bundle origin with a symbol from the same bundle. try String(contentsOf: sgURL) .replacingOccurrences(of: #"identifier" : "s:OriginalUSR"#, with: #"identifier" : "s:5MyKit0A5MyProtocol0Afunc()"#) .write(to: url.appendingPathComponent("sidekit.symbols.json"), atomically: true, encoding: .utf8) }) - let myFuncReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) + let myFuncReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) let node = try context.entity(with: myFuncReference) let symbol = try XCTUnwrap(node.semantic as? Symbol) // Verify that by default we inherit docs. do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) // Verify the expected inherited abstract text. @@ -2684,11 +2670,11 @@ Document } /// Tests that we don't inherit docs by default from within the same bundle but not module. - func testDocInheritanceInsideBundleButNotModule() throws { + func testDocInheritanceInsideBundleButNotModule() async throws { let sgURL = Bundle.module.url( forResource: "LegacyBundle_DoNotUseInNewTests.docc/sidekit.symbols", withExtension: "json", subdirectory: "Test Bundles")! - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in // Replace the out-of-bundle origin with a symbol from the same bundle but // from the MyKit module. try String(contentsOf: sgURL) @@ -2696,13 +2682,13 @@ Document .write(to: url.appendingPathComponent("sidekit.symbols.json"), atomically: true, encoding: .utf8) }) - let myFuncReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) + let myFuncReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) let node = try context.entity(with: myFuncReference) let symbol = try XCTUnwrap(node.semantic as? Symbol) // Verify that by default we inherit docs. do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) // Verify the expected default abstract text. @@ -2710,8 +2696,8 @@ Document } } /// Tests that we generated an automatic abstract and remove source docs. - func testDisabledDocInheritance() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testDisabledDocInheritance() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") // Verify that the inherited docs which should be ignored are not reference resolved. // Verify inherited docs are reference resolved and their problems are recorded. @@ -2722,13 +2708,13 @@ Document return p.diagnostic.summary == "Resource 'my-inherited-image.png' couldn't be found" })) - let myFuncReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) + let myFuncReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) let node = try context.entity(with: myFuncReference) let symbol = try XCTUnwrap(node.semantic as? Symbol) // Verify that by default we don't inherit docs and we generate default abstract. do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) // Verify the expected default abstract text. @@ -2741,8 +2727,8 @@ Document } /// Tests doc extensions are matched to inherited symbols - func testInheritedSymbolDocExtension() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + func testInheritedSymbolDocExtension() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in try? """ # ``SideKit/SideClass/Element/inherited()`` Doc extension abstract. @@ -2751,13 +2737,13 @@ Document """.write(to: url.appendingPathComponent("inherited.md"), atomically: true, encoding: .utf8) }) - let myFuncReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) + let myFuncReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) let node = try context.entity(with: myFuncReference) let symbol = try XCTUnwrap(node.semantic as? Symbol) // Verify the doc extension was matched to the inherited symbol. do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) // Verify the expected default abstract text. @@ -2783,7 +2769,7 @@ Document } /// Tests that authored documentation for inherited symbols isn't removed. - func testInheritedSymbolWithAuthoredDocComment() throws { + func testInheritedSymbolWithAuthoredDocComment() async throws { struct TestData { let docCommentJSON: String let expectedRenderedAbstract: [RenderInlineContent] @@ -2885,7 +2871,7 @@ Document for testData in testData { let sgURL = Bundle.module.url(forResource: "LegacyBundle_DoNotUseInNewTests.docc/sidekit.symbols", withExtension: "json", subdirectory: "Test Bundles")! - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in // Replace the out-of-bundle origin with a symbol from the same bundle but // from the MyKit module. var graph = try JSONDecoder().decode(SymbolGraph.self, from: Data(contentsOf: sgURL)) @@ -2896,13 +2882,13 @@ Document .write(to: url.appendingPathComponent("sidekit.symbols.json")) }) - let myFuncReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) + let myFuncReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) let node = try context.entity(with: myFuncReference) let symbol = try XCTUnwrap(node.semantic as? Symbol) // Verify the doc extension was matched to the inherited symbol. do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) // Verify the expected default abstract text. @@ -2912,27 +2898,27 @@ Document } /// Tests that we inherit docs when the feature is enabled. - func testEnabledDocInheritance() throws { + func testEnabledDocInheritance() async throws { let bundleURL = Bundle.module.url( forResource: "LegacyBundle_DoNotUseInNewTests", withExtension: "docc", subdirectory: "Test Bundles")! var configuration = DocumentationContext.Configuration() configuration.externalMetadata.inheritDocs = true - let (_, bundle, context) = try loadBundle(from: bundleURL, configuration: configuration) + let (_, _, context) = try await loadBundle(from: bundleURL, configuration: configuration) // Verify that we don't reference resolve inherited docs. XCTAssertFalse(context.diagnosticEngine.problems.contains(where: { problem in problem.diagnostic.summary.contains("my-inherited-image.png") })) - let myFuncReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) + let myFuncReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass/Element/inherited()", sourceLanguage: .swift) let node = try context.entity(with: myFuncReference) let symbol = try XCTUnwrap(node.semantic as? Symbol) // Verify that by default we don't inherit docs and we generate default abstract. do { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) // Verify the expected default abstract text. @@ -2958,14 +2944,14 @@ Document } // Verifies that undocumented symbol gets a nil abstract. - func testNonDocumentedSymbolNilAbstract() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testNonDocumentedSymbolNilAbstract() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/globalFunction(_:considering:)", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/globalFunction(_:considering:)", sourceLanguage: .swift) let node = try context.entity(with: reference) let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) // Verify that an undocumented symbol gets a nil abstract. @@ -3066,8 +3052,8 @@ Document } /// Tests links to symbols that have deprecation summary in markdown appear deprecated. - func testLinkToDeprecatedSymbolViaDirectiveIsDeprecated() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + func testLinkToDeprecatedSymbolViaDirectiveIsDeprecated() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in try """ # ``MyKit/MyProtocol`` @DeprecationSummary { @@ -3076,18 +3062,18 @@ Document """.write(to: url.appendingPathComponent("documentation").appendingPathComponent("myprotocol.md"), atomically: true, encoding: .utf8) }) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit", sourceLanguage: .swift)) let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode) let reference = try XCTUnwrap(renderNode.references["doc://org.swift.docc.example/documentation/MyKit/MyProtocol"] as? TopicRenderReference) XCTAssertTrue(reference.isDeprecated) } - func testCustomSymbolDisplayNames() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + func testCustomSymbolDisplayNames() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in try """ # ``MyKit`` @@ -3117,9 +3103,9 @@ Document """.write(to: url.appendingPathComponent("documentation").appendingPathComponent("myprotocol.md"), atomically: true, encoding: .utf8) }) - let moduleReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit", sourceLanguage: .swift) - let protocolReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyProtocol", sourceLanguage: .swift) - let functionReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift) + let moduleReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit", sourceLanguage: .swift) + let protocolReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyProtocol", sourceLanguage: .swift) + let functionReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift) // Verify the MyKit module @@ -3132,7 +3118,7 @@ Document XCTAssertEqual(titleVariant.variant, "My custom conceptual name") } - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: moduleNode.reference) + var translator = RenderNodeTranslator(context: context, identifier: moduleNode.reference) let moduleRenderNode = try XCTUnwrap(translator.visit(moduleSymbol) as? RenderNode) XCTAssertEqual(moduleRenderNode.metadata.title, "My custom conceptual name") @@ -3178,7 +3164,7 @@ Document let functionNode = try context.entity(with: functionReference) let functionSymbol = try XCTUnwrap(functionNode.semantic as? Symbol) - translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: functionNode.reference) + translator = RenderNodeTranslator(context: context, identifier: functionNode.reference) let functionRenderNode = try XCTUnwrap(translator.visit(functionSymbol) as? RenderNode) XCTAssertTrue(functionRenderNode.metadata.modulesVariants.variants.isEmpty) // Test that the symbol name `MyKit` is not added as a related module. @@ -3187,7 +3173,7 @@ Document } /// Tests that we correctly resolve links in automatic inherited API Collections. - func testInheritedAPIGroupsInCollidedParents() throws { + func testInheritedAPIGroupsInCollidedParents() async throws { // Loads a symbol graph which has a property `b` and a struct `B` that // collide path-wise and `B` has inherited children: @@ -3196,7 +3182,7 @@ Document // │ ╰ doc://com.test.TestBed/documentation/Minimal_docs/A/B-swift.struct/Equatable-Implementations // │ ╰ doc://com.test.TestBed/documentation/Minimal_docs/A/B-swift.struct/!=(_:_:) // ╰ doc://com.test.TestBed/documentation/Minimal_docs/A/b-swift.property - let (bundle, context) = try testBundleAndContext(named: "InheritedUnderCollision") + let (bundle, context) = try await testBundleAndContext(named: "InheritedUnderCollision") // Verify that the inherited symbol got a path that accounts for the collision between // the struct `B` and the property `b`. @@ -3216,8 +3202,8 @@ Document XCTAssertEqual(inheritedSymbolReference.absoluteString, groupReference.absoluteString) } - func testVisitTutorialMediaWithoutExtension() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testVisitTutorialMediaWithoutExtension() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in try """ @Tutorials(name: "Technology X") { @Intro(title: "Technology X") { @@ -3239,17 +3225,17 @@ Document } """.write(to: url.appendingPathComponent("TestOverview.tutorial"), atomically: true, encoding: .utf8) } - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/TestOverview", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/tutorials/TestOverview", sourceLanguage: .swift)) guard let technologyDirective = node.markup as? BlockDirective else { XCTFail("Unexpected document structure, tutorial not found as first child.") return } var problems = [Problem]() - guard let tutorialTableOfContents = TutorialTableOfContents(from: technologyDirective, source: nil, for: bundle, problems: &problems) else { + guard let tutorialTableOfContents = TutorialTableOfContents(from: technologyDirective, source: nil, for: context.inputs, problems: &problems) else { XCTFail("Couldn't create technology from markup: \(problems)") return } - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(tutorialTableOfContents) as? RenderNode) XCTAssertEqual(renderNode.references.count, 5) XCTAssertNotNil(renderNode.references["doc://org.swift.docc.example/tutorials/Test-Bundle/TestTutorial"] as? TopicRenderReference) @@ -3262,8 +3248,8 @@ Document XCTAssertNil(renderNode.references["introposter"] as? ImageReference) } - func testTopicsSectionWithAnonymousTopicGroup() throws { - let (_, bundle, context) = try testBundleAndContext( + func testTopicsSectionWithAnonymousTopicGroup() async throws { + let (_, _, context) = try await testBundleAndContext( copying: "LegacyBundle_DoNotUseInNewTests", configureBundle: { url in try """ @@ -3284,14 +3270,14 @@ Document ) let moduleReference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/Test-Bundle/article", sourceLanguage: .swift ) let moduleNode = try context.entity(with: moduleReference) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: moduleNode.reference) + var translator = RenderNodeTranslator(context: context, identifier: moduleNode.reference) let moduleRenderNode = try XCTUnwrap(translator.visit(moduleNode.semantic) as? RenderNode) XCTAssertEqual( @@ -3308,7 +3294,7 @@ Document ) } - func testTopicsSectionWithSingleAnonymousTopicGroup() throws { + func testTopicsSectionWithSingleAnonymousTopicGroup() async throws { let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "SomeModuleName.symbols.json", content: makeSymbolGraph(moduleName: "SomeModuleName", symbols: [ makeSymbol(id: "some-class-id", kind: .class, pathComponents: ["SomeClass"]), @@ -3327,18 +3313,17 @@ Document """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) - let articleReference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/unit-test/Article", sourceLanguage: .swift ) let articleNode = try context.entity(with: articleReference) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: articleNode.reference) + var translator = RenderNodeTranslator(context: context, identifier: articleNode.reference) let articleRenderNode = try XCTUnwrap(translator.visit(articleNode.semantic) as? RenderNode) XCTAssertEqual( @@ -3353,8 +3338,8 @@ Document ) } - func testLanguageSpecificTopicSections() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in + func testLanguageSpecificTopicSections() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "MixedLanguageFrameworkWithLanguageRefinements") { url in try """ # ``MixedFramework/MyObjectiveCClassObjectiveCName`` @@ -3391,7 +3376,7 @@ Document let documentationNode = try context.entity(with: reference) XCTAssertEqual(documentationNode.availableVariantTraits.count, 2, "This page has Swift and Objective-C variants") - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(documentationNode) let topicSectionsVariants = renderNode.topicSectionsVariants @@ -3413,7 +3398,7 @@ Document ]) } - func testLanguageSpecificTopicSectionDoesNotAppearInAutomaticSeeAlso() throws { + func testLanguageSpecificTopicSectionDoesNotAppearInAutomaticSeeAlso() async throws { let catalog = Folder(name: "Something.docc", content: [ JSONFile(name: "Something-swift.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: (1...4).map { makeSymbol(id: "symbol-id-\($0)", language: .swift, kind: .class, pathComponents: ["SomeClass\($0)"]) @@ -3445,7 +3430,7 @@ Document - ``SomeClass4`` """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssert(context.problems.isEmpty, "\(context.problems.map(\.diagnostic.summary))") let moduleReference = try XCTUnwrap(context.soleRootModuleReference) @@ -3477,18 +3462,17 @@ Document ], file: file, line: line) } - let nodeConverter = DocumentationNodeConverter(bundle: bundle, context: context) + let nodeConverter = DocumentationNodeConverter(context: context) assertExpectedTopicSections(nodeConverter.convert(documentationNode)) let contextConverter = DocumentationContextConverter( - bundle: bundle, context: context, - renderContext: RenderContext(documentationContext: context, bundle: bundle) + renderContext: RenderContext(documentationContext: context) ) try assertExpectedTopicSections(XCTUnwrap(contextConverter.renderNode(for: documentationNode))) } - func testTopicSectionWithUnsupportedDirectives() throws { + func testTopicSectionWithUnsupportedDirectives() async throws { let exampleDocumentation = Folder(name: "unit-test.docc", content: [ TextFile(name: "root.md", utf8Content: """ # Main article @@ -3519,14 +3503,14 @@ Document let tempURL = try createTemporaryDirectory() let bundleURL = try exampleDocumentation.write(inside: tempURL) - let (_, bundle, context) = try loadBundle(from: bundleURL, diagnosticEngine: .init() /* no diagnostic consumers */) + let (_, _, context) = try await loadBundle(from: bundleURL, diagnosticEngine: .init() /* no diagnostic consumers */) let reference = try XCTUnwrap(context.soleRootModuleReference) let documentationNode = try context.entity(with: reference) XCTAssertEqual(documentationNode.availableVariantTraits.count, 1) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(documentationNode) let topicSection = renderNode.topicSectionsVariants.defaultValue @@ -3538,14 +3522,14 @@ Document ]) } - func testAutomaticCurationForRefinedSymbols() throws { - let (_, bundle, context) = try testBundleAndContext(named: "GeometricalShapes") + func testAutomaticCurationForRefinedSymbols() async throws { + let (_, _, context) = try await testBundleAndContext(named: "GeometricalShapes") do { let root = try XCTUnwrap(context.soleRootModuleReference) let node = try context.entity(with: root) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(node) let swiftTopicSections = renderNode.topicSectionsVariants.defaultValue @@ -3575,10 +3559,10 @@ Document } do { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/GeometricalShapes/Circle", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/GeometricalShapes/Circle", sourceLanguage: .swift) let node = try context.entity(with: reference) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(node) let swiftTopicSections = renderNode.topicSectionsVariants.defaultValue @@ -3613,7 +3597,7 @@ Document } } - func testThematicBreak() throws { + func testThematicBreak() async throws { let source = """ --- @@ -3624,9 +3608,9 @@ Document XCTAssertEqual(markup.childCount, 1) - let (bundle, context) = try testBundleAndContext() + let (bundle, context) = try await testBundleAndContext() - var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestThematicBreak", sourceLanguage: .swift)) + var contentTranslator = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestThematicBreak", sourceLanguage: .swift)) let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) let expectedContent: [RenderBlockContent] = [ @@ -3636,7 +3620,7 @@ Document XCTAssertEqual(expectedContent, renderContent) } - func testSymbolWithEmptyName() throws { + func testSymbolWithEmptyName() async throws { // Symbols _should_ have names, but due to bugs there's cases when anonymous C structs don't. let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( @@ -3657,7 +3641,7 @@ Document )) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) XCTAssertEqual(context.knownPages.map(\.path).sorted(), [ "/documentation/ModuleName", @@ -3670,7 +3654,7 @@ Document let unnamedStructReference = try XCTUnwrap(context.soleRootModuleReference).appendingPath("SomeContainer/struct_(unnamed)") let node = try context.entity(with: unnamedStructReference) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(node) XCTAssertEqual(renderNode.metadata.title, "struct (unnamed)") diff --git a/Tests/SwiftDocCTests/Model/SourceLanguageTests.swift b/Tests/SwiftDocCTests/Model/SourceLanguageTests.swift deleted file mode 100644 index 734c5c7a1d..0000000000 --- a/Tests/SwiftDocCTests/Model/SourceLanguageTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2022-2023 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 -*/ - -@testable import SwiftDocC -import XCTest - -class SourceLanguageTests: XCTestCase { - func testUsesIDAliasesWhenQueryingFirstKnownLanguage() { - XCTAssertEqual(SourceLanguage(knownLanguageIdentifier: "objective-c"), .objectiveC) - XCTAssertEqual(SourceLanguage(knownLanguageIdentifier: "objc"), .objectiveC) - XCTAssertEqual(SourceLanguage(knownLanguageIdentifier: "c"), .objectiveC) - } -} diff --git a/Tests/SwiftDocCTests/Model/TaskGroupTests.swift b/Tests/SwiftDocCTests/Model/TaskGroupTests.swift index 46207219fc..c8e66f8453 100644 --- a/Tests/SwiftDocCTests/Model/TaskGroupTests.swift +++ b/Tests/SwiftDocCTests/Model/TaskGroupTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -68,10 +68,6 @@ class SectionExtractionTests: XCTestCase { } } - private func testNode(with document: Document) -> DocumentationNode { - return DocumentationNode(reference: ResolvedTopicReference(bundleID: "org.swift.docc", path: "/blah", sourceLanguage: .swift), kind: .article, sourceLanguage: .swift, name: .conceptual(title: "Title"), markup: document, semantic: Semantic()) - } - func testSection() { // Empty -> nil do { diff --git a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift b/Tests/SwiftDocCTests/OutOfProcessReferenceResolverV1Tests.swift similarity index 90% rename from Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift rename to Tests/SwiftDocCTests/OutOfProcessReferenceResolverV1Tests.swift index d96a0216f9..a4640f8b2f 100644 --- a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/OutOfProcessReferenceResolverV1Tests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -12,10 +12,12 @@ import XCTest import Foundation import SymbolKit @_spi(ExternalLinks) @testable import SwiftDocC -@testable import SwiftDocCUtilities import SwiftDocCTestUtilities -class OutOfProcessReferenceResolverTests: XCTestCase { +// This tests the deprecated V1 implementation of `OutOfProcessReferenceResolver`. +// Deprecating the test silences the deprecation warning when running the tests. It doesn't skip the test. +@available(*, deprecated) +class OutOfProcessReferenceResolverV1Tests: XCTestCase { func testInitializationProcess() throws { #if os(macOS) @@ -51,9 +53,9 @@ class OutOfProcessReferenceResolverTests: XCTestCase { #endif } - func assertResolvesTopicLink(makeResolver: (OutOfProcessReferenceResolver.ResolvedInformation) throws -> OutOfProcessReferenceResolver) throws { + private func assertResolvesTopicLink(makeResolver: (OutOfProcessReferenceResolver.ResolvedInformation) throws -> OutOfProcessReferenceResolver) throws { let testMetadata = OutOfProcessReferenceResolver.ResolvedInformation( - kind: .init(name: "Kind Name", id: "com.test.kind.id", isSymbol: true), + kind: .function, url: URL(string: "doc://com.test.bundle/something")!, title: "Resolved Title", abstract: "Resolved abstract for this topic.", @@ -100,39 +102,42 @@ class OutOfProcessReferenceResolverTests: XCTestCase { // Resolve the symbol let entity = resolver.entity(with: resolvedReference) + let topicRenderReference = entity.makeTopicRenderReference() - XCTAssertEqual(entity.topicRenderReference.url, testMetadata.url.withoutHostAndPortAndScheme().absoluteString) + XCTAssertEqual(topicRenderReference.url, testMetadata.url.withoutHostAndPortAndScheme().absoluteString) - XCTAssertEqual(entity.topicRenderReference.kind.rawValue, "symbol") - XCTAssertEqual(entity.topicRenderReference.role, "symbol") + XCTAssertEqual(topicRenderReference.kind.rawValue, "symbol") + XCTAssertEqual(topicRenderReference.role, "symbol") - XCTAssertEqual(entity.topicRenderReference.title, "Resolved Title") - XCTAssertEqual(entity.topicRenderReference.abstract, [.text("Resolved abstract for this topic.")]) + XCTAssertEqual(topicRenderReference.title, "Resolved Title") + XCTAssertEqual(topicRenderReference.abstract, [.text("Resolved abstract for this topic.")]) - XCTAssertFalse(entity.topicRenderReference.isBeta) + XCTAssertFalse(topicRenderReference.isBeta) - XCTAssertEqual(entity.sourceLanguages.count, 3) + XCTAssertEqual(entity.availableLanguages.count, 3) - let availableSourceLanguages = entity.sourceLanguages.sorted() + let availableSourceLanguages = entity.availableLanguages.sorted() let expectedLanguages = testMetadata.availableLanguages.sorted() XCTAssertEqual(availableSourceLanguages[0], expectedLanguages[0]) XCTAssertEqual(availableSourceLanguages[1], expectedLanguages[1]) XCTAssertEqual(availableSourceLanguages[2], expectedLanguages[2]) - XCTAssertEqual(entity.topicRenderReference.fragments, [.init(text: "declaration fragment", kind: .text, preciseIdentifier: nil)]) + XCTAssertEqual(topicRenderReference.fragments, [.init(text: "declaration fragment", kind: .text, preciseIdentifier: nil)]) let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage("com.test.another-language.id")] - XCTAssertEqual(entity.topicRenderReference.titleVariants.value(for: variantTraits), "Resolved Variant Title") - XCTAssertEqual(entity.topicRenderReference.abstractVariants.value(for: variantTraits), [.text("Resolved variant abstract for this topic.")]) + XCTAssertEqual(topicRenderReference.titleVariants.value(for: variantTraits), "Resolved Variant Title") + XCTAssertEqual(topicRenderReference.abstractVariants.value(for: variantTraits), [.text("Resolved variant abstract for this topic.")]) - let fragmentVariant = try XCTUnwrap(entity.topicRenderReference.fragmentsVariants.variants.first(where: { $0.traits == variantTraits })) + let fragmentVariant = try XCTUnwrap(topicRenderReference.fragmentsVariants.variants.first(where: { $0.traits == variantTraits })) XCTAssertEqual(fragmentVariant.patch.map(\.operation), [.replace]) if case .replace(let variantFragment) = fragmentVariant.patch.first { XCTAssertEqual(variantFragment, [.init(text: "variant declaration fragment", kind: .text, preciseIdentifier: nil)]) } else { XCTFail("Unexpected fragments variant patch") } + + XCTAssertEqual(entity.kind, .function) } func testResolvingTopicLinkProcess() throws { @@ -271,30 +276,31 @@ class OutOfProcessReferenceResolverTests: XCTestCase { // Resolve the symbol let (_, entity) = try XCTUnwrap(resolver.symbolReferenceAndEntity(withPreciseIdentifier: "abc123"), "Unexpectedly failed to resolve symbol") + let topicRenderReference = entity.makeTopicRenderReference() - XCTAssertEqual(entity.topicRenderReference.url, testMetadata.url.absoluteString) + XCTAssertEqual(topicRenderReference.url, testMetadata.url.absoluteString) - XCTAssertEqual(entity.topicRenderReference.kind.rawValue, "symbol") - XCTAssertEqual(entity.topicRenderReference.role, "symbol") + XCTAssertEqual(topicRenderReference.kind.rawValue, "symbol") + XCTAssertEqual(topicRenderReference.role, "symbol") - XCTAssertEqual(entity.topicRenderReference.title, "Resolved Title") + XCTAssertEqual(topicRenderReference.title, "Resolved Title") - XCTAssertEqual(entity.sourceLanguages.count, 3) + XCTAssertEqual(entity.availableLanguages.count, 3) - let availableSourceLanguages = entity.sourceLanguages.sorted() + let availableSourceLanguages = entity.availableLanguages.sorted() let expectedLanguages = testMetadata.availableLanguages.sorted() XCTAssertEqual(availableSourceLanguages[0], expectedLanguages[0]) XCTAssertEqual(availableSourceLanguages[1], expectedLanguages[1]) XCTAssertEqual(availableSourceLanguages[2], expectedLanguages[2]) - XCTAssertEqual(entity.topicRenderReference.fragments, [.init(text: "declaration fragment", kind: .text, preciseIdentifier: nil)]) + XCTAssertEqual(topicRenderReference.fragments, [.init(text: "declaration fragment", kind: .text, preciseIdentifier: nil)]) let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage("com.test.another-language.id")] - XCTAssertEqual(entity.topicRenderReference.titleVariants.value(for: variantTraits), "Resolved Variant Title") - XCTAssertEqual(entity.topicRenderReference.abstractVariants.value(for: variantTraits), [.text("Resolved variant abstract for this topic.")]) + XCTAssertEqual(topicRenderReference.titleVariants.value(for: variantTraits), "Resolved Variant Title") + XCTAssertEqual(topicRenderReference.abstractVariants.value(for: variantTraits), [.text("Resolved variant abstract for this topic.")]) - let fragmentVariant = try XCTUnwrap(entity.topicRenderReference.fragmentsVariants.variants.first(where: { $0.traits == variantTraits })) + let fragmentVariant = try XCTUnwrap(topicRenderReference.fragmentsVariants.variants.first(where: { $0.traits == variantTraits })) XCTAssertEqual(fragmentVariant.patch.map(\.operation), [.replace]) if case .replace(let variantFragment) = fragmentVariant.patch.first { XCTAssertEqual(variantFragment, [.init(text: "variant declaration fragment", kind: .text, preciseIdentifier: nil)]) @@ -302,19 +308,19 @@ class OutOfProcessReferenceResolverTests: XCTestCase { XCTFail("Unexpected fragments variant patch") } - XCTAssertNil(entity.topicRenderReference.conformance) - XCTAssertNil(entity.topicRenderReference.estimatedTime) - XCTAssertNil(entity.topicRenderReference.defaultImplementationCount) - XCTAssertFalse(entity.topicRenderReference.isBeta) - XCTAssertFalse(entity.topicRenderReference.isDeprecated) - XCTAssertNil(entity.topicRenderReference.propertyListKeyNames) - XCTAssertNil(entity.topicRenderReference.tags) - - XCTAssertEqual(entity.topicRenderReference.images.count, 1) - let topicImage = try XCTUnwrap(entity.topicRenderReference.images.first) + XCTAssertNil(topicRenderReference.conformance) + XCTAssertNil(topicRenderReference.estimatedTime) + XCTAssertNil(topicRenderReference.defaultImplementationCount) + XCTAssertFalse(topicRenderReference.isBeta) + XCTAssertFalse(topicRenderReference.isDeprecated) + XCTAssertNil(topicRenderReference.propertyListKeyNames) + XCTAssertNil(topicRenderReference.tags) + + XCTAssertEqual(topicRenderReference.images.count, 1) + let topicImage = try XCTUnwrap(topicRenderReference.images.first) XCTAssertEqual(topicImage.type, .card) - let image = try XCTUnwrap(entity.renderReferenceDependencies.imageReferences.first(where: { $0.identifier == topicImage.identifier })) + let image = try XCTUnwrap(entity.makeRenderDependencies().imageReferences.first(where: { $0.identifier == topicImage.identifier })) XCTAssertEqual(image.identifier, RenderReferenceIdentifier("external-card")) XCTAssertEqual(image.altText, "External card alt text") @@ -676,11 +682,10 @@ class OutOfProcessReferenceResolverTests: XCTestCase { let resolver = try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in }) XCTAssertEqual(resolver.bundleID, "com.test.bundle") - XCTAssertThrowsError(try resolver.resolveInformationForTopicURL(URL(string: "doc://com.test.bundle/something")!)) { - guard case OutOfProcessReferenceResolver.Error.executableSentBundleIdentifierAgain = $0 else { - XCTFail("Encountered an unexpected type of error.") - return - } + if case .failure(_, let errorInfo) = resolver.resolve(.unresolved(UnresolvedTopicReference(topicURL: ValidatedURL(parsingAuthoredLink: "doc://com.test.bundle/something")!))) { + XCTAssertEqual(errorInfo.message, "Executable sent bundle identifier message again, after it was already received.") + } else { + XCTFail("Unexpectedly resolved the link from an identifier and capabilities response") } #endif } @@ -732,13 +737,13 @@ class OutOfProcessReferenceResolverTests: XCTestCase { // Resolve the symbol let topicLinkEntity = resolver.entity(with: resolvedReference) - - XCTAssertEqual(topicLinkEntity.topicRenderReference.isBeta, isBeta, file: file, line: line) + + XCTAssertEqual(topicLinkEntity.makeTopicRenderReference().isBeta, isBeta, file: file, line: line) // Resolve the symbol let (_, symbolEntity) = try XCTUnwrap(resolver.symbolReferenceAndEntity(withPreciseIdentifier: "abc123"), "Unexpectedly failed to resolve symbol") - XCTAssertEqual(symbolEntity.topicRenderReference.isBeta, isBeta, file: file, line: line) + XCTAssertEqual(symbolEntity.makeTopicRenderReference().isBeta, isBeta, file: file, line: line) } diff --git a/Tests/SwiftDocCTests/OutOfProcessReferenceResolverV2Tests.swift b/Tests/SwiftDocCTests/OutOfProcessReferenceResolverV2Tests.swift new file mode 100644 index 0000000000..e4e42b500a --- /dev/null +++ b/Tests/SwiftDocCTests/OutOfProcessReferenceResolverV2Tests.swift @@ -0,0 +1,739 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 XCTest +import Foundation +import SymbolKit +@_spi(ExternalLinks) @testable import SwiftDocC +import SwiftDocCTestUtilities + +#if os(macOS) +class OutOfProcessReferenceResolverV2Tests: XCTestCase { + + func testInitializationProcess() throws { + let temporaryFolder = try createTemporaryDirectory() + + let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable") + // When the executable file doesn't exist + XCTAssertFalse(FileManager.default.fileExists(atPath: executableLocation.path)) + XCTAssertThrowsError(try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in }), + "There should be a validation error if the executable file doesn't exist") + + // When the file isn't executable + try "".write(to: executableLocation, atomically: true, encoding: .utf8) + XCTAssertFalse(FileManager.default.isExecutableFile(atPath: executableLocation.path)) + XCTAssertThrowsError(try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in }), + "There should be a validation error if the file isn't executable") + + // When the file isn't executable + try """ + #!/bin/bash + echo '{"identifier":"com.test.bundle","capabilities": 0}' # Write this resolver's identifier & capabilities + read # Wait for docc to send a request + """.write(to: executableLocation, atomically: true, encoding: .utf8) + + // `0o0700` is `-rwx------` (read, write, & execute only for owner) + try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path) + XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path)) + + let resolver = try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { errorMessage in + XCTFail("No error output is expected for this test executable. Got:\n\(errorMessage)") + }) + XCTAssertEqual(resolver.bundleID, "com.test.bundle") + } + + private func makeTestSummary() -> (summary: LinkDestinationSummary, imageReference: RenderReferenceIdentifier, imageURLs: (light: URL, dark: URL)) { + let linkedReference = RenderReferenceIdentifier("doc://com.test.bundle/something-else") + let linkedImage = RenderReferenceIdentifier("some-image-identifier") + let linkedVariantReference = RenderReferenceIdentifier("doc://com.test.bundle/something-else-2") + + func cardImages(name: String) -> (light: URL, dark: URL) { + ( URL(string: "https://example.com/path/to/\(name)@2x.png")!, + URL(string: "https://example.com/path/to/\(name)~dark@2x.png")! ) + } + + let imageURLs = cardImages(name: "some-image") + + let summary = LinkDestinationSummary( + kind: .structure, + language: .swift, // This is Swift to account for what is considered a symbol's "first" variant value (rdar://86580516), + relativePresentationURL: URL(string: "/path/so/something")!, + referenceURL: URL(string: "doc://com.test.bundle/something")!, + title: "Resolved Title", + abstract: [ + .text("Resolved abstract with "), + .emphasis(inlineContent: [.text("formatted")]), + .text(" "), + .strong(inlineContent: [.text("formatted")]), + .text(" and a link: "), + .reference(identifier: linkedReference, isActive: true, overridingTitle: nil, overridingTitleInlineContent: nil) + ], + availableLanguages: [ + .swift, + .init(name: "Language Name 2", id: "com.test.another-language.id"), + .objectiveC, + ], + platforms: [ + .init(name: "firstOS", introduced: "1.2.3", isBeta: false), + .init(name: "secondOS", introduced: "4.5.6", isBeta: false), + ], + usr: "some-unique-symbol-id", + subheadingDeclarationFragments: .init([ + .init(text: "struct", kind: .keyword, preciseIdentifier: nil), + .init(text: " ", kind: .text, preciseIdentifier: nil), + .init(text: "declaration fragment", kind: .identifier, preciseIdentifier: nil), + ]), + topicImages: [ + .init(pageImagePurpose: .card, identifier: linkedImage) + ], + references: [ + TopicRenderReference(identifier: linkedReference, title: "Something Else", abstract: [.text("Some other page")], url: "/path/to/something-else", kind: .symbol), + TopicRenderReference(identifier: linkedVariantReference, title: "Another Page", abstract: [.text("Yet another page")], url: "/path/to/something-else-2", kind: .article), + + ImageReference( + identifier: linkedImage, + altText: "External card alt text", + imageAsset: DataAsset( + variants: [ + DataTraitCollection(userInterfaceStyle: .light, displayScale: .double): imageURLs.light, + DataTraitCollection(userInterfaceStyle: .dark, displayScale: .double): imageURLs.dark, + ], + metadata: [ + imageURLs.light : DataAsset.Metadata(svgID: nil), + imageURLs.dark : DataAsset.Metadata(svgID: nil), + ], + context: .display + ) + ), + ], + variants: [ + .init( + traits: [.interfaceLanguage("com.test.another-language.id")], + kind: .init(name: "Variant Kind Name", id: "com.test.kind2.id", isSymbol: true), + language: .init(name: "Language Name 2", id: "com.test.another-language.id"), + title: "Resolved Variant Title", + abstract: [ + .text("Resolved variant abstract with "), + .emphasis(inlineContent: [.text("formatted")]), + .text(" "), + .strong(inlineContent: [.text("formatted")]), + .text(" and a link: "), + .reference(identifier: linkedVariantReference, isActive: true, overridingTitle: nil, overridingTitleInlineContent: nil) + ], + subheadingDeclarationFragments: .init([ + .init(text: "variant declaration fragment", kind: .text, preciseIdentifier: nil) + ]) + ) + ] + ) + + return (summary, linkedImage, imageURLs) + } + + func testResolvingLinkAndSymbol() throws { + enum RequestKind { + case link, symbol + + func perform(resolver: OutOfProcessReferenceResolver, file: StaticString = #filePath, line: UInt = #line) throws -> LinkResolver.ExternalEntity? { + switch self { + case .link: + let unresolved = TopicReference.unresolved(UnresolvedTopicReference(topicURL: ValidatedURL(parsingExact: "doc://com.test.bundle/something")!)) + let reference: ResolvedTopicReference + switch resolver.resolve(unresolved) { + case .success(let resolved): + reference = resolved + case .failure(_, let errorInfo): + XCTFail("Unexpectedly failed to resolve reference with error: \(errorInfo.message)", file: file, line: line) + return nil + } + + // Resolve the symbol + return resolver.entity(with: reference) + + case .symbol: + return try XCTUnwrap(resolver.symbolReferenceAndEntity(withPreciseIdentifier: "")?.1, file: file, line: line) + } + } + } + + for requestKind in [RequestKind.link, .symbol] { + let (testSummary, linkedImage, imageURLs) = makeTestSummary() + + let resolver: OutOfProcessReferenceResolver + do { + let temporaryFolder = try createTemporaryDirectory() + let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable") + + let encodedLinkSummary = try String(data: JSONEncoder().encode(testSummary), encoding: .utf8)! + + try """ + #!/bin/bash + echo '{"identifier":"com.test.bundle","capabilities": 0}' # Write this resolver's identifier & capabilities + read # Wait for docc to send a request + echo '{"resolved":\(encodedLinkSummary)}' # Respond with the test link summary (above) + """.write(to: executableLocation, atomically: true, encoding: .utf8) + + // `0o0700` is `-rwx------` (read, write, & execute only for owner) + try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path) + XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path)) + + resolver = try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in }) + XCTAssertEqual(resolver.bundleID, "com.test.bundle") + } + + let entity = try XCTUnwrap(requestKind.perform(resolver: resolver)) + let topicRenderReference = entity.makeTopicRenderReference() + + XCTAssertEqual(topicRenderReference.url, testSummary.relativePresentationURL.absoluteString) + + XCTAssertEqual(topicRenderReference.kind.rawValue, "symbol") + XCTAssertEqual(topicRenderReference.role, "symbol") + + XCTAssertEqual(topicRenderReference.title, "Resolved Title") + XCTAssertEqual(topicRenderReference.abstract, [ + .text("Resolved abstract with "), + .emphasis(inlineContent: [.text("formatted")]), + .text(" "), + .strong(inlineContent: [.text("formatted")]), + .text(" and a link: "), + .reference(identifier: .init("doc://com.test.bundle/something-else"), isActive: true, overridingTitle: nil, overridingTitleInlineContent: nil) + ]) + + XCTAssertFalse(topicRenderReference.isBeta) + + XCTAssertEqual(entity.availableLanguages.count, 3) + + let availableSourceLanguages = entity.availableLanguages.sorted() + let expectedLanguages = testSummary.availableLanguages.sorted() + + XCTAssertEqual(availableSourceLanguages[0], expectedLanguages[0]) + XCTAssertEqual(availableSourceLanguages[1], expectedLanguages[1]) + XCTAssertEqual(availableSourceLanguages[2], expectedLanguages[2]) + + XCTAssertEqual(topicRenderReference.fragments, [ + .init(text: "struct", kind: .keyword, preciseIdentifier: nil), + .init(text: " ", kind: .text, preciseIdentifier: nil), + .init(text: "declaration fragment", kind: .identifier, preciseIdentifier: nil), + ]) + + let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage("com.test.another-language.id")] + XCTAssertEqual(topicRenderReference.titleVariants.value(for: variantTraits), "Resolved Variant Title") + XCTAssertEqual(topicRenderReference.abstractVariants.value(for: variantTraits), [ + .text("Resolved variant abstract with "), + .emphasis(inlineContent: [.text("formatted")]), + .text(" "), + .strong(inlineContent: [.text("formatted")]), + .text(" and a link: "), + .reference(identifier: .init("doc://com.test.bundle/something-else-2"), isActive: true, overridingTitle: nil, overridingTitleInlineContent: nil) + ]) + + let fragmentVariant = try XCTUnwrap(topicRenderReference.fragmentsVariants.variants.first(where: { $0.traits == variantTraits })) + XCTAssertEqual(fragmentVariant.patch.map(\.operation), [.replace]) + if case .replace(let variantFragment) = fragmentVariant.patch.first { + XCTAssertEqual(variantFragment, [.init(text: "variant declaration fragment", kind: .text, preciseIdentifier: nil)]) + } else { + XCTFail("Unexpected fragments variant patch") + } + + XCTAssertNil(topicRenderReference.conformance) + XCTAssertNil(topicRenderReference.estimatedTime) + XCTAssertNil(topicRenderReference.defaultImplementationCount) + XCTAssertFalse(topicRenderReference.isBeta) + XCTAssertFalse(topicRenderReference.isDeprecated) + XCTAssertNil(topicRenderReference.propertyListKeyNames) + XCTAssertNil(topicRenderReference.tags) + + XCTAssertEqual(topicRenderReference.images.count, 1) + let topicImage = try XCTUnwrap(topicRenderReference.images.first) + XCTAssertEqual(topicImage.type, .card) + + let image = try XCTUnwrap(entity.makeRenderDependencies().imageReferences.first(where: { $0.identifier == topicImage.identifier })) + + XCTAssertEqual(image.identifier, linkedImage) + XCTAssertEqual(image.altText, "External card alt text") + + XCTAssertEqual(image.asset, DataAsset( + variants: [ + DataTraitCollection(userInterfaceStyle: .light, displayScale: .double): imageURLs.light, + DataTraitCollection(userInterfaceStyle: .dark, displayScale: .double): imageURLs.dark, + ], + metadata: [ + imageURLs.light: DataAsset.Metadata(svgID: nil), + imageURLs.dark: DataAsset.Metadata(svgID: nil), + ], + context: .display + )) + } + } + + func testForwardsErrorOutputProcess() throws { + let temporaryFolder = try createTemporaryDirectory() + + let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable") + try """ + #!/bin/bash + echo '{"identifier":"com.test.bundle","capabilities": 0}' # Write this resolver's identifier & capabilities + echo "Some error output" 1>&2 # Write to stderr + read # Wait for docc to send a request + """.write(to: executableLocation, atomically: true, encoding: .utf8) + + // `0o0700` is `-rwx------` (read, write, & execute only for owner) + try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path) + XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path)) + + let didReadErrorOutputExpectation = expectation(description: "Did read forwarded error output.") + + let resolver = try? OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { + errorMessage in + XCTAssertEqual(errorMessage, "Some error output\n") + didReadErrorOutputExpectation.fulfill() + }) + XCTAssertEqual(resolver?.bundleID, "com.test.bundle") + + wait(for: [didReadErrorOutputExpectation], timeout: 20.0) + } + + func testLinksAndImagesInExternalAbstractAreIncludedInTheRenderedPageReferenecs() async throws { + let externalBundleID: DocumentationBundle.Identifier = "com.example.test" + + let imageRef = RenderReferenceIdentifier("some-external-card-image-identifier") + let linkRef = RenderReferenceIdentifier("doc://\(externalBundleID)/path/to/other-page") + + let imageURL = URL(string: "https://example.com/path/to/some-image.png")! + + let originalLinkedImage = ImageReference( + identifier: imageRef, + imageAsset: DataAsset( + variants: [.init(displayScale: .standard): imageURL], + metadata: [imageURL: .init()], + context: .display + ) + ) + + let originalLinkedTopic = TopicRenderReference( + identifier: linkRef, + title: "Resolved title of link inside abstract", + abstract: [ + .text("This transient content is not displayed anywhere"), + ], + url: "/path/to/other-page", + kind: .article + ) + + let externalSummary = LinkDestinationSummary( + kind: .article, + language: .swift, + relativePresentationURL: URL(string: "/path/to/something")!, + referenceURL: URL(string: "doc://\(externalBundleID)/path/to/something")!, + title: "Resolved title", + abstract: [ + .text("External abstract with an image "), + .image(identifier: imageRef, metadata: nil), + .text(" and link "), + .reference(identifier: linkRef, isActive: true, overridingTitle: nil, overridingTitleInlineContent: nil), + .text("."), + ], + availableLanguages: [.swift], + references: [originalLinkedImage, originalLinkedTopic], + variants: [] + ) + + let resolver: OutOfProcessReferenceResolver + do { + let temporaryFolder = try createTemporaryDirectory() + let encodedResponse = try String(decoding: JSONEncoder().encode(OutOfProcessReferenceResolver.ResponseV2.resolved(externalSummary)), as: UTF8.self) + + let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable") + try """ + #!/bin/bash + echo '{"identifier":"\(externalBundleID)","capabilities": 0}' # Write this resolver's identifier & capabilities + read # Wait for docc to send a request + echo '\(encodedResponse)' # Respond with the resolved link summary + read # Wait for docc to send another request + """.write(to: executableLocation, atomically: true, encoding: .utf8) + + // `0o0700` is `-rwx------` (read, write, & execute only for owner) + try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path) + XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path)) + + resolver = try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in }) + } + + let catalog = Folder(name: "unit-test.docc", content: [ + TextFile(name: "Something.md", utf8Content: """ + # My root page + + This page curates an an external page (so that its abstract and transient references are displayed on the page) + + ## Topics + + ### An external link + + - + """) + ]) + let inputDirectory = Folder(name: "path", content: [Folder(name: "to", content: [catalog])]) + + var configuration = DocumentationContext.Configuration() + configuration.externalDocumentationConfiguration.sources = [ + externalBundleID: resolver + ] + let (_, context) = try await loadBundle(catalog: inputDirectory, configuration: configuration) + XCTAssertEqual(context.problems.map(\.diagnostic.summary), [], "Encountered unexpected problems") + + let reference = try XCTUnwrap(context.soleRootModuleReference, "This example catalog only has a root page") + + let converter = DocumentationContextConverter( + context: context, + renderContext: RenderContext(documentationContext: context) + ) + let renderNode = try XCTUnwrap(converter.renderNode(for: context.entity(with: reference))) + + // Verify that the topic section exist and has the external link + XCTAssertEqual(renderNode.topicSections.flatMap { [$0.title ?? ""] + $0.identifiers }, [ + "An external link", + "doc://\(externalBundleID)/path/to/something", // Resolved links use their canonical references + ]) + + // Verify that the externally resolved page's references are included on the page + XCTAssertEqual(Set(renderNode.references.keys), [ + "doc://com.example.test/path/to/something", // The external page that the root links to + + "some-external-card-image-identifier", // The image in that page's abstract + "doc://com.example.test/path/to/other-page", // The link in that page's abstract + ], "The external page and its two references should be included on this page") + + XCTAssertEqual(renderNode.references[imageRef.identifier] as? ImageReference, originalLinkedImage) + XCTAssertEqual(renderNode.references[linkRef.identifier] as? TopicRenderReference, originalLinkedTopic) + } + + func testExternalLinkFailureResultInDiagnosticWithSolutions() async throws { + let externalBundleID: DocumentationBundle.Identifier = "com.example.test" + + let resolver: OutOfProcessReferenceResolver + do { + let temporaryFolder = try createTemporaryDirectory() + + let diagnosticInfo = OutOfProcessReferenceResolver.ResponseV2.DiagnosticInformation( + summary: "Some external link issue summary", + solutions: [ + .init(summary: "Some external solution", replacement: "/some-replacement") + ] + ) + let encodedDiagnostic = try String(decoding: JSONEncoder().encode(diagnosticInfo), as: UTF8.self) + + let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable") + try """ + #!/bin/bash + echo '{"identifier":"\(externalBundleID)","capabilities": 0}' # Write this resolver's identifier & capabilities + read # Wait for docc to send a request + echo '{"failure":\(encodedDiagnostic)}' # Respond with an error message + """.write(to: executableLocation, atomically: true, encoding: .utf8) + + // `0o0700` is `-rwx------` (read, write, & execute only for owner) + try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path) + XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path)) + + resolver = try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in }) + } + + let catalog = Folder(name: "unit-test.docc", content: [ + TextFile(name: "Something.md", utf8Content: """ + # My root page + + This page contains an external link that will fail to resolve: + """) + ]) + let inputDirectory = Folder(name: "path", content: [Folder(name: "to", content: [catalog])]) + + var configuration = DocumentationContext.Configuration() + configuration.externalDocumentationConfiguration.sources = [ + externalBundleID: resolver + ] + let (_, context) = try await loadBundle(catalog: inputDirectory, configuration: configuration) + + XCTAssertEqual(context.problems.map(\.diagnostic.summary), [ + "Some external link issue summary", + ]) + + let problem = try XCTUnwrap(context.problems.sorted(by: \.diagnostic.identifier).first) + + XCTAssertEqual(problem.diagnostic.summary, "Some external link issue summary") + XCTAssertEqual(problem.diagnostic.range?.lowerBound, .init(line: 3, column: 69, source: URL(fileURLWithPath: "/path/to/unit-test.docc/Something.md"))) + XCTAssertEqual(problem.diagnostic.range?.upperBound, .init(line: 3, column: 97, source: URL(fileURLWithPath: "/path/to/unit-test.docc/Something.md"))) + + XCTAssertEqual(problem.possibleSolutions.count, 1) + let solution = try XCTUnwrap(problem.possibleSolutions.first) + XCTAssertEqual(solution.summary, "Some external solution") + XCTAssertEqual(solution.replacements.count, 1) + XCTAssertEqual(solution.replacements.first?.range.lowerBound, .init(line: 3, column: 87, source: nil)) + XCTAssertEqual(solution.replacements.first?.range.upperBound, .init(line: 3, column: 97, source: nil)) + + // Verify the warning presentation + let diagnosticOutput = LogHandle.LogStorage() + let fileSystem = try TestFileSystem(folders: [inputDirectory]) + let diagnosticFormatter = DiagnosticConsoleWriter(LogHandle.memory(diagnosticOutput), formattingOptions: [], highlight: true, dataProvider: fileSystem) + diagnosticFormatter.receive(context.diagnosticEngine.problems) + try diagnosticFormatter.flush() + + let warning = "\u{001B}[1;33m" + let highlight = "\u{001B}[1;32m" + let suggestion = "\u{001B}[1;39m" + let clear = "\u{001B}[0;0m" + XCTAssertEqual(diagnosticOutput.text, """ + \(warning)warning: Some external link issue summary\(clear) + --> /path/to/unit-test.docc/Something.md:3:69-3:97 + 1 | # My root page + 2 | + 3 + This page contains an external link that will fail to resolve: + | ╰─\(suggestion)suggestion: Some external solution\(clear) + + """) + + // Verify the suggestion replacement + let source = try XCTUnwrap(problem.diagnostic.source) + let original = String(decoding: try fileSystem.contents(of: source), as: UTF8.self) + + XCTAssertEqual(try solution.applyTo(original), """ + # My root page + + This page contains an external link that will fail to resolve: + """) + } + + func testOnlySendsPathAndFragmentInLinkRequest() async throws { + let externalBundleID: DocumentationBundle.Identifier = "com.example.test" + + let resolver: OutOfProcessReferenceResolver + let savedRequestsFile: URL + do { + let temporaryFolder = try createTemporaryDirectory() + savedRequestsFile = temporaryFolder.appendingPathComponent("saved-requests.txt") + + let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable") + try """ + #!/bin/bash + echo '{"identifier":"\(externalBundleID)","capabilities": 0}' # Write this resolver's identifier & capabilities + read # Wait for docc to send a request + echo $REPLY >> \(savedRequestsFile.path) # Save the raw request string + echo '{"failure":"ignored error message"}' # Respond with an error message + # Repeat the same read-save-respond steps 2 more times + read # Wait for 2nd request + echo $REPLY >> \(savedRequestsFile.path) # Save the raw request + echo '{"failure":"ignored error message"}' # Respond + read # Wait for 3rd request + echo $REPLY >> \(savedRequestsFile.path) # Save the raw request + echo '{"failure":"ignored error message"}' # Respond + read # Wait for 4th request + echo $REPLY >> \(savedRequestsFile.path) # Save the raw request + echo '{"failure":"ignored error message"}' # Respond + """.write(to: executableLocation, atomically: true, encoding: .utf8) + + // `0o0700` is `-rwx------` (read, write, & execute only for owner) + try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path) + XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path)) + + resolver = try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in }) + } + + let catalog = Folder(name: "unit-test.docc", content: [ + TextFile(name: "Something.md", utf8Content: """ + # My root page + + This page contains an 4 external links that will fail to resolve: + - + - + - + - + """) + ]) + let inputDirectory = Folder(name: "path", content: [Folder(name: "to", content: [catalog])]) + + var configuration = DocumentationContext.Configuration() + configuration.externalDocumentationConfiguration.sources = [ + externalBundleID: resolver + ] + // Create the context, just to process all the documentation and make the 3 external link requests + _ = try await loadBundle(catalog: inputDirectory, configuration: configuration) + + // The requests can come in any order so we sort the output lines for easier comparison + let readRequests = try String(contentsOf: savedRequestsFile, encoding: .utf8) + .components(separatedBy: .newlines) + .filter { !$0.isEmpty } + .sorted(by: \.count) + .joined(separator: "\n") + XCTAssertEqual(readRequests, """ + {"link":"/some-link"} + {"link":"/path/to/some-link"} + {"link":"//not-parsable-as-url"} + {"link":"/path/to/some-link#some-fragment"} + """) + } + + func testEncodingAndDecodingRequests() throws { + do { + let request = OutOfProcessReferenceResolver.RequestV2.link("/path/to/some-page#some-fragment") + + let data = try JSONEncoder().encode(request) + if case .link(let link) = try JSONDecoder().decode(OutOfProcessReferenceResolver.RequestV2.self, from: data) { + XCTAssertEqual(link, "/path/to/some-page#some-fragment") + } else { + XCTFail("Decoded the wrong type of request") + } + } + + do { + let request = OutOfProcessReferenceResolver.RequestV2.symbol("some-unique-symbol-id") + + let data = try JSONEncoder().encode(request) + if case .symbol(let usr) = try JSONDecoder().decode(OutOfProcessReferenceResolver.RequestV2.self, from: data) { + XCTAssertEqual(usr, "some-unique-symbol-id") + } else { + XCTFail("Decoded the wrong type of request") + } + } + } + + func testEncodingAndDecodingResponses() throws { + // Identifier and capabilities + do { + let request = OutOfProcessReferenceResolver.ResponseV2.identifierAndCapabilities("com.example.test", []) + + let data = try JSONEncoder().encode(request) + if case .identifierAndCapabilities(let identifier, let capabilities) = try JSONDecoder().decode(OutOfProcessReferenceResolver.ResponseV2.self, from: data) { + XCTAssertEqual(identifier.rawValue, "com.example.test") + XCTAssertEqual(capabilities.rawValue, 0) + } else { + XCTFail("Decoded the wrong type of message") + } + } + + // Failures + do { + let originalInfo = OutOfProcessReferenceResolver.ResponseV2.DiagnosticInformation( + summary: "Some summary", + solutions: [ + .init(summary: "Some solution", replacement: "some-replacement") + ] + ) + + let request = OutOfProcessReferenceResolver.ResponseV2.failure(originalInfo) + let data = try JSONEncoder().encode(request) + if case .failure(let info) = try JSONDecoder().decode(OutOfProcessReferenceResolver.ResponseV2.self, from: data) { + XCTAssertEqual(info.summary, originalInfo.summary) + XCTAssertEqual(info.solutions?.count, originalInfo.solutions?.count) + for (solution, originalSolution) in zip(info.solutions ?? [], originalInfo.solutions ?? []) { + XCTAssertEqual(solution.summary, originalSolution.summary) + XCTAssertEqual(solution.replacement, originalSolution.replacement) + } + } else { + XCTFail("Decoded the wrong type of message") + } + } + + // Resolved link information + do { + let originalSummary = makeTestSummary().summary + let message = OutOfProcessReferenceResolver.ResponseV2.resolved(originalSummary) + + let data = try JSONEncoder().encode(message) + if case .resolved(let summary) = try JSONDecoder().decode(OutOfProcessReferenceResolver.ResponseV2.self, from: data) { + XCTAssertEqual(summary, originalSummary) + } else { + XCTFail("Decoded the wrong type of message") + return + } + } + } + + func testErrorWhenReceivingBundleIdentifierTwice() throws { + let temporaryFolder = try createTemporaryDirectory() + + let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable") + try """ + #!/bin/bash + echo '{"identifier":"com.test.bundle","capabilities": 0}' # Write this resolver's identifier & capabilities + read # Wait for docc to send a request + echo '{"identifier":"com.test.bundle","capabilities": 0}' # Write this identifier & capabilities again + """.write(to: executableLocation, atomically: true, encoding: .utf8) + + // `0o0700` is `-rwx------` (read, write, & execute only for owner) + try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path) + XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path)) + + let resolver = try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in }) + XCTAssertEqual(resolver.bundleID, "com.test.bundle") + + if case .failure(_, let errorInfo) = resolver.resolve(.unresolved(UnresolvedTopicReference(topicURL: ValidatedURL(parsingAuthoredLink: "doc://com.test.bundle/something")!))) { + XCTAssertEqual(errorInfo.message, "Executable sent bundle identifier message again, after it was already received.") + } else { + XCTFail("Unexpectedly resolved the link from an identifier and capabilities response") + } + } + + func testResolvingSymbolBetaStatusProcess() throws { + func betaStatus(forSymbolWithPlatforms platforms: [LinkDestinationSummary.PlatformAvailability], file: StaticString = #filePath, line: UInt = #line) throws -> Bool { + let summary = LinkDestinationSummary( + kind: .class, + language: .swift, + relativePresentationURL: URL(string: "/documentation/ModuleName/Something")!, + referenceURL: URL(string: "/documentation/ModuleName/Something")!, + title: "Something", + availableLanguages: [.swift, .objectiveC], + platforms: platforms, + variants: [] + ) + + let resolver: OutOfProcessReferenceResolver + do { + let temporaryFolder = try createTemporaryDirectory() + let executableLocation = temporaryFolder.appendingPathComponent("link-resolver-executable") + + let encodedLinkSummary = try String(data: JSONEncoder().encode(summary), encoding: .utf8)! + + try """ + #!/bin/bash + #!/bin/bash + echo '{"identifier":"com.test.bundle","capabilities": 0}' # Write this resolver's identifier & capabilities + read # Wait for docc to send a request + echo '{"resolved":\(encodedLinkSummary)}' # Respond with the test link summary (above) + """.write(to: executableLocation, atomically: true, encoding: .utf8) + + // `0o0700` is `-rwx------` (read, write, & execute only for owner) + try FileManager.default.setAttributes([.posixPermissions: 0o0700], ofItemAtPath: executableLocation.path) + XCTAssert(FileManager.default.isExecutableFile(atPath: executableLocation.path)) + + resolver = try OutOfProcessReferenceResolver(processLocation: executableLocation, errorOutputHandler: { _ in }) + XCTAssertEqual(resolver.bundleID, "com.test.bundle", file: file, line: line) + } + + let (_, symbolEntity) = try XCTUnwrap(resolver.symbolReferenceAndEntity(withPreciseIdentifier: "abc123"), "Unexpectedly failed to resolve symbol") + return symbolEntity.makeTopicRenderReference().isBeta + } + + // All platforms are in beta + XCTAssertEqual(true, try betaStatus(forSymbolWithPlatforms: [ + .init(name: "fooOS", introduced: "1.2.3", isBeta: true), + .init(name: "barOS", introduced: "1.2.3", isBeta: true), + .init(name: "bazOS", introduced: "1.2.3", isBeta: true), + ])) + + // One platform is stable, the other two are in beta + XCTAssertEqual(false, try betaStatus(forSymbolWithPlatforms: [ + .init(name: "fooOS", introduced: "1.2.3", isBeta: false), + .init(name: "barOS", introduced: "1.2.3", isBeta: true), + .init(name: "bazOS", introduced: "1.2.3", isBeta: true), + ])) + + // No platforms explicitly supported + XCTAssertEqual(false, try betaStatus(forSymbolWithPlatforms: [])) + } +} +#endif diff --git a/Tests/SwiftDocCTests/Rendering/AutomaticSeeAlsoTests.swift b/Tests/SwiftDocCTests/Rendering/AutomaticSeeAlsoTests.swift index 2ce82d8703..4c264ec3e7 100644 --- a/Tests/SwiftDocCTests/Rendering/AutomaticSeeAlsoTests.swift +++ b/Tests/SwiftDocCTests/Rendering/AutomaticSeeAlsoTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -17,8 +17,8 @@ class AutomaticSeeAlsoTests: XCTestCase { /// Test that a symbol with no authored See Also and with no curated siblings /// does not have a See Also section. - func testNoSeeAlso() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testNoSeeAlso() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Extension that curates `SideClass` try """ # ``SideKit`` @@ -31,7 +31,7 @@ class AutomaticSeeAlsoTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify there is no See Also @@ -40,8 +40,8 @@ class AutomaticSeeAlsoTests: XCTestCase { /// Test that a symbol with authored See Also and with no curated siblings /// does include an authored See Also section - func testAuthoredSeeAlso() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testAuthoredSeeAlso() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Extension that curates `SideClass` try """ # ``SideKit`` @@ -62,7 +62,7 @@ class AutomaticSeeAlsoTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify there is an authored See Also from markdown @@ -76,8 +76,8 @@ class AutomaticSeeAlsoTests: XCTestCase { /// Test that a symbol with authored See Also and with curated siblings /// does include both in See Also with authored section first - func testAuthoredAndAutomaticSeeAlso() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testAuthoredAndAutomaticSeeAlso() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Extension that curates `SideClass` try """ # ``SideKit`` @@ -105,7 +105,7 @@ class AutomaticSeeAlsoTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify there is an authored See Also & automatically created See Also @@ -122,7 +122,7 @@ class AutomaticSeeAlsoTests: XCTestCase { // Verify that articles get same automatic See Also sections as symbols do { let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/Test-Bundle/sidearticle", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Article) as! RenderNode // Verify there is an automacially created See Also @@ -137,8 +137,8 @@ class AutomaticSeeAlsoTests: XCTestCase { // Duplicate of the `testAuthoredAndAutomaticSeeAlso()` test above // but with automatic see also creation disabled - func testAuthoredSeeAlsoWithDisabledAutomaticSeeAlso() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testAuthoredSeeAlsoWithDisabledAutomaticSeeAlso() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Article that curates `SideClass` try """ # ``SideKit`` @@ -172,7 +172,7 @@ class AutomaticSeeAlsoTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify there is an authored See Also but no automatically created See Also @@ -185,7 +185,7 @@ class AutomaticSeeAlsoTests: XCTestCase { // Verify that article without options directive still gets automatic See Also sections do { let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/Test-Bundle/sidearticle", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Article) as! RenderNode // Verify there is an automacially created See Also @@ -200,8 +200,8 @@ class AutomaticSeeAlsoTests: XCTestCase { // Duplicate of the `testAuthoredAndAutomaticSeeAlso()` test above // but with automatic see also creation globally disabled - func testAuthoredSeeAlsoWithGloballyDisabledAutomaticSeeAlso() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in + func testAuthoredSeeAlsoWithGloballyDisabledAutomaticSeeAlso() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { root in /// Article that curates `SideClass` try """ # ``SideKit`` @@ -236,7 +236,7 @@ class AutomaticSeeAlsoTests: XCTestCase { // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify there is an authored See Also but no automatically created See Also @@ -249,7 +249,7 @@ class AutomaticSeeAlsoTests: XCTestCase { // Verify that article without options directive still gets automatic See Also sections do { let node = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/Test-Bundle/sidearticle", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Article) as! RenderNode // Verify there is an automacially created See Also @@ -257,7 +257,7 @@ class AutomaticSeeAlsoTests: XCTestCase { } } - func testSeeAlsoWithSymbolAndTutorial() throws { + func testSeeAlsoWithSymbolAndTutorial() async throws { let exampleDocumentation = Folder(name: "MyKit.docc", content: [ CopyOfFile(original: Bundle.module.url(forResource: "mykit-one-symbol.symbols", withExtension: "json", subdirectory: "Test Resources")!), @@ -283,11 +283,11 @@ class AutomaticSeeAlsoTests: XCTestCase { let tempURL = try createTemporaryDirectory() let bundleURL = try exampleDocumentation.write(inside: tempURL) - let (_, bundle, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) // Get a translated render node let node = try context.entity(with: ResolvedTopicReference(bundleID: "MyKit", path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify there is a See Also with the resolved tutorial reference diff --git a/Tests/SwiftDocCTests/Rendering/AvailabilityRenderOrderTests.swift b/Tests/SwiftDocCTests/Rendering/AvailabilityRenderOrderTests.swift index 0e91aa65e3..f0a76bd050 100644 --- a/Tests/SwiftDocCTests/Rendering/AvailabilityRenderOrderTests.swift +++ b/Tests/SwiftDocCTests/Rendering/AvailabilityRenderOrderTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -9,6 +9,7 @@ */ import Foundation +import SymbolKit import XCTest @testable import SwiftDocC @@ -16,22 +17,60 @@ class AvailabilityRenderOrderTests: XCTestCase { let availabilitySGFURL = Bundle.module.url( forResource: "Availability.symbols", withExtension: "json", subdirectory: "Test Resources")! - func testSortingAtRenderTime() throws { - let (bundleURL, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { url in - try? FileManager.default.copyItem(at: self.availabilitySGFURL, to: url.appendingPathComponent("Availability.symbols.json")) - } - defer { - try? FileManager.default.removeItem(at: bundleURL) + func testSortingAtRenderTime() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { url in + let availabilitySymbolGraphURL = url.appendingPathComponent("Availability.symbols.json") + try? FileManager.default.copyItem(at: self.availabilitySGFURL, to: availabilitySymbolGraphURL) + + // Load the symbol graph fixture + var availabilitySymbolGraph = try JSONDecoder().decode(SymbolGraph.self, from: try Data(contentsOf: availabilitySymbolGraphURL)) + + // There should be at least one symbol in this graph + XCTAssertEqual(1, availabilitySymbolGraph.symbols.count) + if let tuple = availabilitySymbolGraph.symbols.first { + + let key = tuple.key + var symbol = tuple.value + + // The symbol should have availability info specified + XCTAssertNotNil(symbol.availability) + if var alternateSymbols = symbol.mixins[Availability.mixinKey] as? Availability { + + // Create a new availability item which is missing a domain (platform name). + let missingDomain = SymbolGraph.Symbol.Availability.AvailabilityItem( + domain: nil, + introducedVersion: nil, + deprecatedVersion: nil, + obsoletedVersion: nil, + message: "Don't use this function; call some other function instead.", + renamed: nil, + isUnconditionallyDeprecated: true, + isUnconditionallyUnavailable: false, + willEventuallyBeDeprecated: false + ) + + // Append the invalid item and update the symbol + alternateSymbols.availability.insert(missingDomain, at: 4) + symbol.mixins[Availability.mixinKey] = alternateSymbols + } + availabilitySymbolGraph.symbols[key] = symbol + } + + // Update the temporary copy of the fixture + let jsonEncoder = JSONEncoder() + let data = try jsonEncoder.encode(availabilitySymbolGraph) + try data.write(to: availabilitySymbolGraphURL) } - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Availability/MyStruct", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/Availability/MyStruct", sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode // Verify that all the symbol's availabilities were sorted into the order // they need to appear for rendering (they are not in the symbol graph fixture). // Additionally verify all the platforms have their correctly spelled name including spaces. + // Finally, the invalid item added above should be filtered out. XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")" }), [ "iOS 12.0", "iOS App Extension 12.0", "iPadOS 12.0", diff --git a/Tests/SwiftDocCTests/Rendering/ConstraintsRenderSectionTests.swift b/Tests/SwiftDocCTests/Rendering/ConstraintsRenderSectionTests.swift index f517383200..19bd1673ac 100644 --- a/Tests/SwiftDocCTests/Rendering/ConstraintsRenderSectionTests.swift +++ b/Tests/SwiftDocCTests/Rendering/ConstraintsRenderSectionTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,6 +11,7 @@ import Foundation import XCTest @testable import SwiftDocC +import SwiftDocCTestUtilities import SymbolKit fileprivate let jsonDecoder = JSONDecoder() @@ -18,8 +19,8 @@ fileprivate let jsonEncoder = JSONEncoder() class ConstraintsRenderSectionTests: XCTestCase { - func testSingleConstraint() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in + func testSingleConstraint() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in // Add constraints to `MyClass` let graphURL = bundleURL.appendingPathComponent("mykit-iOS.symbols.json") var graph = try jsonDecoder.decode(SymbolGraph.self, from: try Data(contentsOf: graphURL)) @@ -40,16 +41,16 @@ class ConstraintsRenderSectionTests: XCTestCase { } // Compile docs and verify contents - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode XCTAssertEqual(renderNode.metadata.conformance?.constraints.map(flattenInlineElements).joined(), "Label is Text.") } - func testSingleRedundantConstraint() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in + func testSingleRedundantConstraint() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in // Add constraints to `MyClass` let graphURL = bundleURL.appendingPathComponent("mykit-iOS.symbols.json") var graph = try jsonDecoder.decode(SymbolGraph.self, from: try Data(contentsOf: graphURL)) @@ -70,15 +71,15 @@ class ConstraintsRenderSectionTests: XCTestCase { } // Compile docs and verify contents - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode XCTAssertNil(renderNode.metadata.conformance) } - func testSingleRedundantConstraintForLeaves() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in + func testSingleRedundantConstraintForLeaves() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in // Add constraints to `MyClass` let graphURL = bundleURL.appendingPathComponent("mykit-iOS.symbols.json") var graph = try jsonDecoder.decode(SymbolGraph.self, from: try Data(contentsOf: graphURL)) @@ -99,15 +100,15 @@ class ConstraintsRenderSectionTests: XCTestCase { } // Compile docs and verify contents - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode XCTAssertNil(renderNode.metadata.conformance) } - func testPreservesNonRedundantConstraints() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in + func testPreservesNonRedundantConstraints() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in // Add constraints to `MyClass` let graphURL = bundleURL.appendingPathComponent("mykit-iOS.symbols.json") var graph = try jsonDecoder.decode(SymbolGraph.self, from: try Data(contentsOf: graphURL)) @@ -129,15 +130,15 @@ class ConstraintsRenderSectionTests: XCTestCase { } // Compile docs and verify contents - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode XCTAssertEqual(renderNode.metadata.conformance?.constraints.map(flattenInlineElements).joined(), "Element is MyClass.") } - func testGroups2Constraints() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in + func testGroups2Constraints() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in // Add constraints to `MyClass` let graphURL = bundleURL.appendingPathComponent("mykit-iOS.symbols.json") var graph = try jsonDecoder.decode(SymbolGraph.self, from: try Data(contentsOf: graphURL)) @@ -159,15 +160,15 @@ class ConstraintsRenderSectionTests: XCTestCase { } // Compile docs and verify contents - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode XCTAssertEqual(renderNode.metadata.conformance?.constraints.map(flattenInlineElements).joined(), "Element conforms to MyProtocol and Equatable.") } - func testGroups3Constraints() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in + func testGroups3Constraints() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in // Add constraints to `MyClass` let graphURL = bundleURL.appendingPathComponent("mykit-iOS.symbols.json") var graph = try jsonDecoder.decode(SymbolGraph.self, from: try Data(contentsOf: graphURL)) @@ -190,15 +191,15 @@ class ConstraintsRenderSectionTests: XCTestCase { } // Compile docs and verify contents - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass/myFunction()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode XCTAssertEqual(renderNode.metadata.conformance?.constraints.map(flattenInlineElements).joined(), "Element conforms to MyProtocol, Equatable, and Hashable.") } - func testRenderReferences() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in + func testRenderReferences() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in // Add constraints to `MyClass` let graphURL = bundleURL.appendingPathComponent("mykit-iOS.symbols.json") var graph = try jsonDecoder.decode(SymbolGraph.self, from: try Data(contentsOf: graphURL)) @@ -220,9 +221,9 @@ class ConstraintsRenderSectionTests: XCTestCase { } // Compile docs and verify contents - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode guard let renderReference = renderNode.references.first(where: { (key, value) -> Bool in @@ -235,8 +236,8 @@ class ConstraintsRenderSectionTests: XCTestCase { XCTAssertEqual(renderReference.conformance?.constraints.map(flattenInlineElements).joined(), "Element conforms to MyProtocol and Equatable.") } - func testRenderReferencesWithNestedTypeInSelf() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in + func testRenderReferencesWithNestedTypeInSelf() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { bundleURL in // Add constraints to `MyClass` let graphURL = bundleURL.appendingPathComponent("mykit-iOS.symbols.json") var graph = try jsonDecoder.decode(SymbolGraph.self, from: try Data(contentsOf: graphURL)) @@ -258,9 +259,9 @@ class ConstraintsRenderSectionTests: XCTestCase { } // Compile docs and verify contents - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visitSymbol(symbol) as! RenderNode guard let renderReference = renderNode.references.first(where: { (key, value) -> Bool in @@ -273,6 +274,39 @@ class ConstraintsRenderSectionTests: XCTestCase { // Verify we've removed the "Self." prefix in the type names XCTAssertEqual(renderReference.conformance?.constraints.map(flattenInlineElements).joined(), "Element conforms to MyProtocol and Index conforms to Equatable.") } + + func testRenderSameShape() async throws { + let symbolGraphFile = Bundle.module.url( + forResource: "SameShapeConstraint", + withExtension: "symbols.json", + subdirectory: "Test Resources" + )! + + let catalog = Folder(name: "unit-test.docc", content: [ + InfoPlist(displayName: "SameShapeConstraint", identifier: "com.test.example"), + CopyOfFile(original: symbolGraphFile), + ]) + + let (_, context) = try await loadBundle(catalog: catalog) + + // Compile docs and verify contents + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SameShapeConstraint/function(_:)", sourceLanguage: .swift)) + let symbol = node.semantic as! Symbol + var translator = RenderNodeTranslator(context: context, identifier: node.reference) + let renderNode = translator.visitSymbol(symbol) as! RenderNode + + guard let renderReference = renderNode.references.first(where: { (key, value) -> Bool in + return key.hasSuffix("function(_:)") + })?.value as? TopicRenderReference else { + XCTFail("Did not find render reference to function(_:)") + return + } + + // The symbol graph only defines constraints on the `swiftGenerics` mixin, + // which docc doesn't load or render. + // However, this test should still run without crashing on decoding the symbol graph. + XCTAssertEqual(renderReference.conformance?.constraints.map(flattenInlineElements).joined(), nil) + } } fileprivate func flattenInlineElements(el: RenderInlineContent) -> String { diff --git a/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift b/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift index b7a887235c..24e21475fa 100644 --- a/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -12,6 +12,7 @@ import Foundation import XCTest @testable import SwiftDocC import SwiftDocCTestUtilities +import SymbolKit class DeclarationsRenderSectionTests: XCTestCase { func testDecodingTokens() throws { @@ -132,10 +133,10 @@ class DeclarationsRenderSectionTests: XCTestCase { try assertRoundTripCoding(value) } - func testAlternateDeclarations() throws { - let (bundle, context) = try testBundleAndContext(named: "AlternateDeclarations") + func testAlternateDeclarations() async throws { + let (_, context) = try await testBundleAndContext(named: "AlternateDeclarations") let reference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/AlternateDeclarations/MyClass/present(completion:)", sourceLanguage: .swift ) @@ -151,15 +152,90 @@ class DeclarationsRenderSectionTests: XCTestCase { })) // Verify that the rendered symbol displays both signatures - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first) XCTAssertEqual(declarationsSection.declarations.count, 2) - XCTAssert(declarationsSection.declarations.allSatisfy({ $0.platforms == [.iOS, .macOS] })) + XCTAssert(declarationsSection.declarations.allSatisfy({ Set($0.platforms) == Set([.iOS, .iPadOS, .macOS, .catalyst]) })) } - func testHighlightDiff() throws { + func testPlatformSpecificDeclarations() async throws { + // init(_ content: MyClass) throws + let declaration1: SymbolGraph.Symbol.DeclarationFragments = .init(declarationFragments: [ + .init(kind: .keyword, spelling: "init", preciseIdentifier: nil), + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .externalParameter, spelling: "_", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "content", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "MyClass", preciseIdentifier: "s:MyClass"), + .init(kind: .text, spelling: ") ", preciseIdentifier: nil), + .init(kind: .keyword, spelling: "throws", preciseIdentifier: nil), + ]) + + // init(_ content: OtherClass) throws + let declaration2: SymbolGraph.Symbol.DeclarationFragments = .init(declarationFragments: [ + .init(kind: .keyword, spelling: "init", preciseIdentifier: nil), + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .externalParameter, spelling: "_", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "content", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "OtherClass", preciseIdentifier: "s:OtherClass"), + .init(kind: .text, spelling: ") ", preciseIdentifier: nil), + .init(kind: .keyword, spelling: "throws", preciseIdentifier: nil), + ]) + let symbol1 = makeSymbol( + id: "myInit", + kind: .func, + pathComponents: ["myInit"], + otherMixins: [declaration1]) + let symbol2 = makeSymbol( + id: "myInit", + kind: .func, + pathComponents: ["myInit"], + otherMixins: [declaration2]) + let symbolGraph1 = makeSymbolGraph(moduleName: "PlatformSpecificDeclarations", platform: .init(operatingSystem: .init(name: "macos")), symbols: [symbol1]) + let symbolGraph2 = makeSymbolGraph(moduleName: "PlatformSpecificDeclarations", platform: .init(operatingSystem: .init(name: "ios")), symbols: [symbol2]) + + func runAssertions(forwards: Bool) async throws { + // Toggling the order of platforms here doesn't necessarily _enforce_ a + // nondeterminism failure in a unit-test environment, but it does make it + // much more likely. Make sure that the order of the platform-specific + // declarations is consistent between runs. + let catalog = Folder(name: "unit-test.docc", content: [ + InfoPlist(displayName: "PlatformSpecificDeclarations", identifier: "com.test.example"), + JSONFile(name: "symbols\(forwards ? "1" : "2").symbols.json", content: symbolGraph1), + JSONFile(name: "symbols\(forwards ? "2" : "1").symbols.json", content: symbolGraph2), + ]) + + let (bundle, context) = try await loadBundle(catalog: catalog) + + let reference = ResolvedTopicReference( + bundleID: bundle.id, + path: "/documentation/PlatformSpecificDeclarations/myInit", + sourceLanguage: .swift + ) + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + var translator = RenderNodeTranslator(context: context, identifier: reference) + let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) + let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first) + XCTAssertEqual(declarationsSection.declarations.count, 2) + + XCTAssertEqual(Set(declarationsSection.declarations[0].platforms), Set([.iOS, .iPadOS, .catalyst])) + XCTAssertEqual(declarationsSection.declarations[0].tokens.map(\.text).joined(), + "init(_ content: OtherClass) throws") + XCTAssertEqual(declarationsSection.declarations[1].platforms, [.macOS]) + XCTAssertEqual(declarationsSection.declarations[1].tokens.map(\.text).joined(), + "init(_ content: MyClass) throws") + } + + try await runAssertions(forwards: true) + try await runAssertions(forwards: false) + } + + func testHighlightDiff() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) let symbolGraphFile = Bundle.module.url( @@ -173,7 +249,7 @@ class DeclarationsRenderSectionTests: XCTestCase { CopyOfFile(original: symbolGraphFile), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) // Make sure that type decorators like arrays, dictionaries, and optionals are correctly highlighted. do { @@ -189,7 +265,7 @@ class DeclarationsRenderSectionTests: XCTestCase { sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first) XCTAssertEqual(declarationsSection.declarations.count, 1) @@ -240,7 +316,7 @@ class DeclarationsRenderSectionTests: XCTestCase { sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first) XCTAssertEqual(declarationsSection.declarations.count, 1) @@ -295,7 +371,7 @@ class DeclarationsRenderSectionTests: XCTestCase { sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first) XCTAssertEqual(declarationsSection.declarations.count, 1) @@ -322,7 +398,92 @@ class DeclarationsRenderSectionTests: XCTestCase { } } - func testDontHighlightWhenOverloadsAreDisabled() throws { + func testInconsistentHighlightDiff() async throws { + enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) + + // Generate a symbol graph with many overload groups that share declarations. + // The overloaded declarations have two legitimate solutions for their longest common subsequence: + // one that ends in a close-parenthesis, and one that ends in a space. + // By alternating the order in which these declarations appear, + // the computed difference highlighting can differ + // unless the declarations are sorted prior to the calculation. + // Ensure that the overload difference highlighting is consistent for these declarations. + + // init(_ content: MyClass) throws + let declaration1: SymbolGraph.Symbol.DeclarationFragments = .init(declarationFragments: [ + .init(kind: .keyword, spelling: "init", preciseIdentifier: nil), + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .externalParameter, spelling: "_", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "content", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "MyClass", preciseIdentifier: "s:MyClass"), + .init(kind: .text, spelling: ") ", preciseIdentifier: nil), + .init(kind: .keyword, spelling: "throws", preciseIdentifier: nil), + ]) + + // init(_ content: some ConvertibleToMyClass) + let declaration2: SymbolGraph.Symbol.DeclarationFragments = .init(declarationFragments: [ + .init(kind: .keyword, spelling: "init", preciseIdentifier: nil), + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .externalParameter, spelling: "_", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "content", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ", preciseIdentifier: nil), + .init(kind: .keyword, spelling: "some", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "ConvertibleToMyClass", preciseIdentifier: "s:ConvertibleToMyClass"), + .init(kind: .text, spelling: ")", preciseIdentifier: nil), + ]) + let overloadsCount = 10 + let symbols = (0...overloadsCount).flatMap({ index in + let reverseDeclarations = index % 2 != 0 + return [ + makeSymbol( + id: "overload-\(index)-1", + kind: .func, + pathComponents: ["overload-\(index)"], + otherMixins: [reverseDeclarations ? declaration2 : declaration1]), + makeSymbol( + id: "overload-\(index)-2", + kind: .func, + pathComponents: ["overload-\(index)"], + otherMixins: [reverseDeclarations ? declaration1 : declaration2]), + ] + }) + let symbolGraph = makeSymbolGraph(moduleName: "FancierOverloads", symbols: symbols) + + let catalog = Folder(name: "unit-test.docc", content: [ + InfoPlist(displayName: "FancierOverloads", identifier: "com.test.example"), + JSONFile(name: "FancierOverloads.symbols.json", content: symbolGraph), + ]) + + let (_, context) = try await loadBundle(catalog: catalog) + + func assertDeclarations(for USR: String, file: StaticString = #filePath, line: UInt = #line) throws { + let reference = try XCTUnwrap(context.documentationCache.reference(symbolID: USR), file: file, line: line) + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol, file: file, line: line) + var translator = RenderNodeTranslator(context: context, identifier: reference) + let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode, file: file, line: line) + let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first, file: file, line: line) + XCTAssertEqual(declarationsSection.declarations.count, 1, file: file, line: line) + let declarations = try XCTUnwrap(declarationsSection.declarations.first, file: file, line: line) + + XCTAssertEqual(declarationsAndHighlights(for: declarations), [ + "init(_ content: MyClass) throws", + " ~~~~~~~~ ~~~~~~", + "init(_ content: some ConvertibleToMyClass)", + " ~~~~ ~~~~~~~~~~~~~~~~~~~~~", + ], file: file, line: line) + } + + for i in 0...overloadsCount { + try assertDeclarations(for: "overload-\(i)-1") + try assertDeclarations(for: "overload-\(i)-2") + } + } + + func testDontHighlightWhenOverloadsAreDisabled() async throws { let symbolGraphFile = Bundle.module.url( forResource: "FancyOverloads", withExtension: "symbols.json", @@ -334,7 +495,7 @@ class DeclarationsRenderSectionTests: XCTestCase { CopyOfFile(original: symbolGraphFile), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) for hash in ["7eht8", "8p1lo", "858ja"] { let reference = ResolvedTopicReference( @@ -343,7 +504,7 @@ class DeclarationsRenderSectionTests: XCTestCase { sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first) XCTAssertEqual(declarationsSection.declarations.count, 1) @@ -353,7 +514,7 @@ class DeclarationsRenderSectionTests: XCTestCase { } } - func testOverloadConformanceDataIsSavedWithDeclarations() throws { + func testOverloadConformanceDataIsSavedWithDeclarations() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) let symbolGraphFile = Bundle.module.url( @@ -367,7 +528,7 @@ class DeclarationsRenderSectionTests: XCTestCase { CopyOfFile(original: symbolGraphFile), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) // MyClass // - myFunc() where T: Equatable @@ -378,7 +539,7 @@ class DeclarationsRenderSectionTests: XCTestCase { sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first) XCTAssertEqual(declarationsSection.declarations.count, 1) @@ -403,3 +564,12 @@ func declarationAndHighlights(for tokens: [DeclarationRenderSection.Token]) -> [ tokens.map({ String(repeating: $0.highlight == .changed ? "~" : " ", count: $0.text.count) }).joined() ] } + +private func declarationsAndHighlights(for section: DeclarationRenderSection) -> [String] { + guard let otherDeclarations = section.otherDeclarations else { + return [] + } + var declarations = otherDeclarations.declarations.map(\.tokens) + declarations.insert(section.tokens, at: otherDeclarations.displayIndex) + return declarations.flatMap(declarationAndHighlights(for:)) +} diff --git a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift index 0900cd8db3..200fd3073b 100644 --- a/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DefaultAvailabilityTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -17,8 +17,8 @@ import SwiftDocCTestUtilities class DefaultAvailabilityTests: XCTestCase { // Test whether missing default availability key correctly produces nil availability - func testBundleWithoutDefaultAvailability() throws { - let bundle = try testBundle(named: "BundleWithoutAvailability") + func testBundleWithoutDefaultAvailability() async throws { + let bundle = try await testBundle(named: "BundleWithoutAvailability") XCTAssertNil(bundle.info.defaultAvailability) } @@ -32,9 +32,9 @@ class DefaultAvailabilityTests: XCTestCase { ] // Test whether the default availability is loaded from Info.plist and applied during render time - func testBundleWithDefaultAvailability() throws { + func testBundleWithDefaultAvailability() async throws { // Copy an Info.plist with default availability - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { (url) in + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { (url) in try? FileManager.default.removeItem(at: url.appendingPathComponent("Info.plist")) try? FileManager.default.copyItem(at: self.infoPlistAvailabilityURL, to: url.appendingPathComponent("Info.plist")) @@ -73,7 +73,7 @@ class DefaultAvailabilityTests: XCTestCase { do { let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit", fragment: nil, sourceLanguage: .swift) let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")" }).sorted(), expectedDefaultAvailability) @@ -83,7 +83,7 @@ class DefaultAvailabilityTests: XCTestCase { do { let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit/MyClass/init()-3743d", fragment: nil, sourceLanguage: .swift) let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")" }).sorted(), ["Mac Catalyst ", "iOS ", "iPadOS ", "macOS 10.15.1"]) @@ -93,7 +93,7 @@ class DefaultAvailabilityTests: XCTestCase { do { let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit/MyClass", fragment: nil, sourceLanguage: .swift) let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertNotEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")" }), expectedDefaultAvailability) @@ -101,9 +101,9 @@ class DefaultAvailabilityTests: XCTestCase { } // Test whether the default availability is merged with beta status from the command line - func testBundleWithDefaultAvailabilityInBetaDocs() throws { + func testBundleWithDefaultAvailabilityInBetaDocs() async throws { // Beta status for the docs (which would normally be set via command line argument) - try assertRenderedPlatformsFor(currentPlatforms: [ + try await assertRenderedPlatformsFor(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 15, 1), beta: true), "Mac Catalyst": PlatformVersion(VersionTriplet(13, 5, 0), beta: true), ], equal: [ @@ -112,7 +112,7 @@ class DefaultAvailabilityTests: XCTestCase { ]) // Repeat the assertions, but use an earlier platform version this time - try assertRenderedPlatformsFor(currentPlatforms: [ + try await assertRenderedPlatformsFor(currentPlatforms: [ "macOS": PlatformVersion(VersionTriplet(10, 14, 1), beta: true), "Mac Catalyst": PlatformVersion(VersionTriplet(13, 5, 0), beta: true), ], equal: [ @@ -121,7 +121,7 @@ class DefaultAvailabilityTests: XCTestCase { ]) } - private func assertRenderedPlatformsFor(currentPlatforms: [String : PlatformVersion], equal expected: [String], file: StaticString = #filePath, line: UInt = #line) throws { + private func assertRenderedPlatformsFor(currentPlatforms: [String : PlatformVersion], equal expected: [String], file: StaticString = #filePath, line: UInt = #line) async throws { var configuration = DocumentationContext.Configuration() configuration.externalMetadata.currentPlatforms = currentPlatforms @@ -131,14 +131,14 @@ class DefaultAvailabilityTests: XCTestCase { JSONFile(name: "MyKit.symbols.json", content: makeSymbolGraph(moduleName: "MyKit")), ]) - let (bundle, context) = try loadBundle(catalog: catalog, configuration: configuration) + let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration) let reference = try XCTUnwrap(context.soleRootModuleReference, file: file, line: line) // Test whether we: // 1) Fallback on iOS when Mac Catalyst availability is missing // 2) Render [Beta] or not for Mac Catalyst's inherited iOS availability let node = try context.entity(with: reference) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")\($0.isBeta == true ? "(beta)" : "")" }).sorted(), expected, file: (file), line: line) @@ -146,9 +146,9 @@ class DefaultAvailabilityTests: XCTestCase { // Test whether when Mac Catalyst availability is missing we fall back on // Mac Catalyst info.plist availability and not on iOS availability. - func testBundleWithMissingCatalystAvailability() throws { + func testBundleWithMissingCatalystAvailability() async throws { // Beta status for both iOS and Mac Catalyst - try assertRenderedPlatformsFor(currentPlatforms: [ + try await assertRenderedPlatformsFor(currentPlatforms: [ "iOS": PlatformVersion(VersionTriplet(13, 5, 0), beta: true), "Mac Catalyst": PlatformVersion(VersionTriplet(13, 5, 0), beta: true), ], equal: [ @@ -157,7 +157,7 @@ class DefaultAvailabilityTests: XCTestCase { ]) // Public status for Mac Catalyst - try assertRenderedPlatformsFor(currentPlatforms: [ + try await assertRenderedPlatformsFor(currentPlatforms: [ "Mac Catalyst": PlatformVersion(VersionTriplet(13, 5, 0), beta: false), ], equal: [ "Mac Catalyst 13.5", @@ -165,19 +165,19 @@ class DefaultAvailabilityTests: XCTestCase { ]) // Verify that a bug rendering availability as beta when no platforms are provided is fixed. - try assertRenderedPlatformsFor(currentPlatforms: [:], equal: [ + try await assertRenderedPlatformsFor(currentPlatforms: [:], equal: [ "Mac Catalyst 13.5", "macOS 10.15.1", ]) } // Test whether the default availability is not beta when not matching current target platform - func testBundleWithDefaultAvailabilityNotInBetaDocs() throws { + func testBundleWithDefaultAvailabilityNotInBetaDocs() async throws { var configuration = DocumentationContext.Configuration() // Set a beta status for the docs (which would normally be set via command line argument) configuration.externalMetadata.currentPlatforms = ["macOS": PlatformVersion(VersionTriplet(10, 16, 0), beta: true)] - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", configuration: configuration) { (url) in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", configuration: configuration) { (url) in // Copy an Info.plist with default availability of macOS 10.15.1 try? FileManager.default.removeItem(at: url.appendingPathComponent("Info.plist")) try? FileManager.default.copyItem(at: self.infoPlistAvailabilityURL, to: url.appendingPathComponent("Info.plist")) @@ -187,7 +187,7 @@ class DefaultAvailabilityTests: XCTestCase { do { let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit", fragment: nil, sourceLanguage: .swift) let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertEqual(renderNode.metadata.platforms?.map({ "\($0.name ?? "") \($0.introduced ?? "")\($0.isBeta == true ? "(beta)" : "")" }).sorted(), [ @@ -198,11 +198,11 @@ class DefaultAvailabilityTests: XCTestCase { } // Test that a symbol is unavailable and default availability does not precede the "unavailable" attribute. - func testUnavailableAvailability() throws { + func testUnavailableAvailability() async throws { var configuration = DocumentationContext.Configuration() // Set a beta status for the docs (which would normally be set via command line argument) configuration.externalMetadata.currentPlatforms = ["iOS": PlatformVersion(VersionTriplet(14, 0, 0), beta: true)] - let (_, bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests", configuration: configuration) + let (_, _, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests", configuration: configuration) do { let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit/MyClass/myFunction()", fragment: nil, sourceLanguage: .swift) @@ -218,7 +218,7 @@ class DefaultAvailabilityTests: XCTestCase { SymbolGraph.Symbol.Availability.AvailabilityItem(domain: .init(rawValue: "macOS"), introducedVersion: nil, deprecatedVersion: nil, obsoletedVersion: nil, message: nil, renamed: nil, isUnconditionallyDeprecated: false, isUnconditionallyUnavailable: true, willEventuallyBeDeprecated: false), ]) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode // Verify that the 'watchOS' & 'tvOS' platforms are filtered out because the symbol is unavailable @@ -328,8 +328,8 @@ class DefaultAvailabilityTests: XCTestCase { // Test that setting default availability doesn't prevent symbols with "universal" deprecation // (i.e. a platform of '*' and unconditional deprecation) from showing up as deprecated. - func testUniversalDeprecationWithDefaultAvailability() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "BundleWithLonelyDeprecationDirective", excludingPaths: []) { (url) in + func testUniversalDeprecationWithDefaultAvailability() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "BundleWithLonelyDeprecationDirective", excludingPaths: []) { (url) in try? FileManager.default.removeItem(at: url.appendingPathComponent("Info.plist")) try? FileManager.default.copyItem(at: self.infoPlistAvailabilityURL, to: url.appendingPathComponent("Info.plist")) } @@ -344,7 +344,7 @@ class DefaultAvailabilityTests: XCTestCase { // Compile docs and verify contents let symbol = node.semantic as! Symbol - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) guard let renderNode = translator.visit(symbol) as? RenderNode else { XCTFail("Could not compile the node") @@ -545,7 +545,7 @@ class DefaultAvailabilityTests: XCTestCase { ) } - func testInheritDefaultAvailabilityOptions() throws { + func testInheritDefaultAvailabilityOptions() async throws { func makeInfoPlist( defaultAvailability: String ) -> String { @@ -565,7 +565,7 @@ class DefaultAvailabilityTests: XCTestCase { } func setupContext( defaultAvailability: String - ) throws -> (DocumentationBundle, DocumentationContext) { + ) async throws -> (DocumentationBundle, DocumentationContext) { // Create an empty bundle let targetURL = try createTemporaryDirectory(named: "test.docc") // Create symbol graph @@ -576,7 +576,7 @@ class DefaultAvailabilityTests: XCTestCase { let infoPlist = makeInfoPlist(defaultAvailability: defaultAvailability) try infoPlist.write(to: infoPlistURL, atomically: true, encoding: .utf8) // Load the bundle & reference resolve symbol graph docs - let (_, bundle, context) = try loadBundle(from: targetURL) + let (_, bundle, context) = try await loadBundle(from: targetURL) return (bundle, context) } @@ -641,7 +641,7 @@ class DefaultAvailabilityTests: XCTestCase { // Don't use default availability version. - var (bundle, context) = try setupContext( + var (_, context) = try await setupContext( defaultAvailability: """ name @@ -667,14 +667,14 @@ class DefaultAvailabilityTests: XCTestCase { // Verify we remove the version from the module availability information. var identifier = ResolvedTopicReference(bundleID: "test", path: "/documentation/MyModule", fragment: nil, sourceLanguage: .swift) var node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: identifier) + var translator = RenderNodeTranslator(context: context, identifier: identifier) var renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertEqual(renderNode.metadata.platforms?.count, 1) XCTAssertEqual(renderNode.metadata.platforms?.first?.name, "iOS") XCTAssertEqual(renderNode.metadata.platforms?.first?.introduced, nil) // Add an extra default availability to test behaviour when mixin in source with default behaviour. - (bundle, context) = try setupContext(defaultAvailability: """ + (_, context) = try await setupContext(defaultAvailability: """ name iOS @@ -710,7 +710,7 @@ class DefaultAvailabilityTests: XCTestCase { // Verify the module availability shows as expected. identifier = ResolvedTopicReference(bundleID: "test", path: "/documentation/MyModule", fragment: nil, sourceLanguage: .swift) node = try context.entity(with: identifier) - translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: identifier) + translator = RenderNodeTranslator(context: context, identifier: identifier) renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertEqual(renderNode.metadata.platforms?.count, 4) var moduleAvailability = try XCTUnwrap(renderNode.metadata.platforms?.first(where: {$0.name == "iOS"})) diff --git a/Tests/SwiftDocCTests/Rendering/DefaultCodeListingSyntaxTests.swift b/Tests/SwiftDocCTests/Rendering/DefaultCodeListingSyntaxTests.swift index e7ad1f13bc..002f93d25a 100644 --- a/Tests/SwiftDocCTests/Rendering/DefaultCodeListingSyntaxTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DefaultCodeListingSyntaxTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,103 +11,62 @@ import Foundation import XCTest @testable import SwiftDocC +import SwiftDocCTestUtilities class DefaultCodeBlockSyntaxTests: XCTestCase { - enum Errors: Error { - case noCodeBlockFound + func testCodeBlockWithoutAnyLanguageOrDefault() async throws { + let codeListing = try await makeCodeBlock(fenceLanguage: nil, infoPlistLanguage: nil) + XCTAssertEqual(codeListing.language, nil) } - var renderSectionWithLanguageDefault: ContentRenderSection! - var renderSectionWithoutLanguageDefault: ContentRenderSection! - - var testBundleWithLanguageDefault: DocumentationBundle! - var testBundleWithoutLanguageDefault: DocumentationBundle! - - override func setUpWithError() throws { - func renderSection(for bundle: DocumentationBundle, in context: DocumentationContext) throws -> ContentRenderSection { - let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/Test-Bundle/Default-Code-Listing-Syntax", fragment: nil, sourceLanguage: .swift) - - let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) - let renderNode = translator.visit(node.semantic) as! RenderNode - - return renderNode.primaryContentSections.first! as! ContentRenderSection - } - - let (_, bundleWithLanguageDefault, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") - - testBundleWithLanguageDefault = bundleWithLanguageDefault - - // Copy the bundle but explicitly set `defaultCodeListingLanguage` to `nil` to mimic having no default language set. - testBundleWithoutLanguageDefault = DocumentationBundle( - info: DocumentationBundle.Info( - displayName: testBundleWithLanguageDefault.displayName, - id: testBundleWithLanguageDefault.id, - defaultCodeListingLanguage: nil - ), - baseURL: testBundleWithLanguageDefault.baseURL, - symbolGraphURLs: testBundleWithLanguageDefault.symbolGraphURLs, - markupURLs: testBundleWithLanguageDefault.markupURLs, - miscResourceURLs: testBundleWithLanguageDefault.miscResourceURLs - ) - - renderSectionWithLanguageDefault = try renderSection(for: testBundleWithLanguageDefault, in: context) - renderSectionWithoutLanguageDefault = try renderSection(for: testBundleWithoutLanguageDefault, in: context) + func testExplicitFencedCodeBlockLanguage() async throws { + let codeListing = try await makeCodeBlock(fenceLanguage: "swift", infoPlistLanguage: nil) + XCTAssertEqual(codeListing.language, "swift") } - struct CodeListing { - var language: String? - var lines: [String] - } - - private func codeListing(at index: Int, in renderSection: ContentRenderSection, file: StaticString = #filePath, line: UInt = #line) throws -> CodeListing { - if case let .codeListing(l) = renderSection.content[index] { - return CodeListing(language: l.syntax, lines: l.code) - } - - XCTFail("Expected code listing at index \(index)", file: (file), line: line) - throw Errors.noCodeBlockFound + func testDefaultCodeBlockLanguage() async throws { + let codeListing = try await makeCodeBlock(fenceLanguage: nil, infoPlistLanguage: "swift") + XCTAssertEqual(codeListing.language, "swift") } - func testDefaultCodeBlockSyntaxForFencedCodeListingWithoutExplicitLanguage() throws { - let fencedCodeListing = try codeListing(at: 1, in: renderSectionWithLanguageDefault) - - XCTAssertEqual("swift", fencedCodeListing.language, "Default a language of 'CDDefaultCodeListingLanguage' if it is set in the 'Info.plist'") - - XCTAssertEqual(fencedCodeListing.lines, [ - "// With no language set, this should highlight to 'swift' because the 'CDDefaultCodeListingLanguage' key is set to 'swift'.", - "func foo()", - ]) + func testExplicitlySetLanguageOverridesDefaultLanguage() async throws { + let codeListing = try await makeCodeBlock(fenceLanguage: "objective-c", infoPlistLanguage: "swift") + XCTAssertEqual(codeListing.language, "objective-c", "The explicit language of the code listing should override the bundle's default language") } - func testDefaultCodeBlockSyntaxForNonFencedCodeListing() throws { - let indentedCodeListing = try codeListing(at: 2, in: renderSectionWithLanguageDefault) - - XCTAssertEqual("swift", indentedCodeListing.language, "Default a language of 'CDDefaultCodeListingLanguage' if it is set in the 'Info.plist'") - XCTAssertEqual(indentedCodeListing.lines, [ - "/// This is a non fenced code listing and should also default to the 'CDDefaultCodeListingLanguage' language.", - "func foo()", - ]) + private struct CodeListing { + var language: String? + var lines: [String] } - - func testExplicitlySetLanguageOverridesBundleDefault() throws { - let explicitlySetLanguageCodeListing = try codeListing(at: 3, in: renderSectionWithLanguageDefault) - - XCTAssertEqual("objective-c", explicitlySetLanguageCodeListing.language, "The explicit language of the code listing should override the bundle's default language") - - XCTAssertEqual(explicitlySetLanguageCodeListing.lines, [ - "/// This is a fenced code block with an explicit language set, and it should override the default language for the bundle.", - "- (void)foo;", + + private func makeCodeBlock(fenceLanguage: String?, infoPlistLanguage: String?) async throws -> CodeListing { + let catalog = Folder(name: "Something.docc", content: [ + InfoPlist(defaultCodeListingLanguage: infoPlistLanguage), + + TextFile(name: "Root.md", utf8Content: """ + # Root + + This article contains a code block + + ```\(fenceLanguage ?? "") + Some code goes + ``` + """) ]) - } - - func testHasNoLanguageWhenNoPlistKeySetAndNoExplicitLanguageProvided() throws { - let fencedCodeListing = try codeListing(at: 1, in: renderSectionWithoutLanguageDefault) - let indentedCodeListing = try codeListing(at: 2, in: renderSectionWithoutLanguageDefault) - let explicitlySetLanguageCodeListing = try codeListing(at: 3, in: renderSectionWithoutLanguageDefault) - - XCTAssertEqual(fencedCodeListing.language, nil) - XCTAssertEqual(indentedCodeListing.language, nil) - XCTAssertEqual(explicitlySetLanguageCodeListing.language, "objective-c") + + let (_, context) = try await loadBundle(catalog: catalog) + let reference = try XCTUnwrap(context.soleRootModuleReference) + let converter = DocumentationNodeConverter(context: context) + + let renderNode = converter.convert(try context.entity(with: reference)) + let renderSection = try XCTUnwrap(renderNode.primaryContentSections.first as? ContentRenderSection) + + guard case .codeListing(let codeListing)? = renderSection.content.last else { + struct Error: DescribedError { + let errorDescription = "Didn't fide code block is known markup" + } + throw Error() + } + return CodeListing(language: codeListing.syntax, lines: codeListing.code) } } diff --git a/Tests/SwiftDocCTests/Rendering/DeprecationSummaryTests.swift b/Tests/SwiftDocCTests/Rendering/DeprecationSummaryTests.swift index 8bede3a237..68184ffba0 100644 --- a/Tests/SwiftDocCTests/Rendering/DeprecationSummaryTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DeprecationSummaryTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -30,21 +30,21 @@ class DeprecationSummaryTests: XCTestCase { /// This test verifies that a symbol's deprecation summary comes from its sidecar doc /// and it's preferred over the original deprecation note in the code docs. - func testAuthoredDeprecatedSummary() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/init()", sourceLanguage: .swift)) + func testAuthoredDeprecatedSummary() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass/init()", sourceLanguage: .swift)) // Compile docs and verify contents let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node") XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, [.text("This initializer has been deprecated.")]) } /// Test for a warning when symbol is not deprecated - func testIncorrectlyAuthoredDeprecatedSummary() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { url in + func testIncorrectlyAuthoredDeprecatedSummary() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { url in // Add a sidecar file with wrong deprecated summary try """ # ``SideKit/SideClass`` @@ -63,11 +63,11 @@ class DeprecationSummaryTests: XCTestCase { }) // Verify the deprecation is still rendered. - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift)) // Compile docs and verify contents let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node") XCTAssertEqual(renderNode.deprecationSummary?.firstParagraph, [.text("This class has been deprecated.")]) @@ -79,8 +79,8 @@ class DeprecationSummaryTests: XCTestCase { /// This test verifies that a symbol's deprecation summary comes from its documentation extension file /// and it's preferred over the original deprecation note in the code docs. /// (r69719494) - func testAuthoredDeprecatedSummaryAsSoleItemInFile() throws { - let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + func testAuthoredDeprecatedSummaryAsSoleItemInFile() async throws { + let (bundle, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") let node = try context.entity( with: ResolvedTopicReference( bundleID: bundle.id, @@ -91,7 +91,7 @@ class DeprecationSummaryTests: XCTestCase { // Compile docs and verify contents let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) guard let renderNode = translator.visit(symbol) as? RenderNode else { XCTFail("Could not compile the node") @@ -111,11 +111,11 @@ class DeprecationSummaryTests: XCTestCase { ]) } - func testSymbolDeprecatedSummary() throws { - let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + func testSymbolDeprecatedSummary() async throws { + let (_, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") let node = try context.entity( with: ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/CoolFramework/CoolClass/doUncoolThings(with:)", sourceLanguage: .swift ) @@ -123,7 +123,7 @@ class DeprecationSummaryTests: XCTestCase { // Compile docs and verify contents let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node") @@ -133,19 +133,19 @@ class DeprecationSummaryTests: XCTestCase { ]) } - func testDeprecationOverride() throws { - let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") - let node = try context.entity( - with: ResolvedTopicReference( - bundleID: bundle.id, - path: "/documentation/CoolFramework/CoolClass/init()", - sourceLanguage: .swift - ) - ) - + func testDeprecationOverride() async throws { + let (_, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + let node = try context.entity( + with: ResolvedTopicReference( + bundleID: context.inputs.id, + path: "/documentation/CoolFramework/CoolClass/init()", + sourceLanguage: .swift + ) + ) + // Compile docs and verify contents let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node") @@ -162,11 +162,11 @@ class DeprecationSummaryTests: XCTestCase { ]) } - func testDeprecationSummaryInDiscussionSection() throws { - let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + func testDeprecationSummaryInDiscussionSection() async throws { + let (_, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") let node = try context.entity( with: ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/CoolFramework/CoolClass/coolFunc()", sourceLanguage: .swift ) @@ -174,7 +174,7 @@ class DeprecationSummaryTests: XCTestCase { // Compile docs and verify contents let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node") @@ -191,11 +191,11 @@ class DeprecationSummaryTests: XCTestCase { ]) } - func testDeprecationSummaryWithMultiLineCommentSymbol() throws { - let (bundle, context) = try testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") + func testDeprecationSummaryWithMultiLineCommentSymbol() async throws { + let (_, context) = try await testBundleAndContext(named: "BundleWithLonelyDeprecationDirective") let node = try context.entity( with: ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/CoolFramework/CoolClass/init(config:cache:)", sourceLanguage: .swift ) @@ -203,7 +203,7 @@ class DeprecationSummaryTests: XCTestCase { // Compile docs and verify contents let symbol = try XCTUnwrap(node.semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(symbol) as? RenderNode, "Could not compile the node") diff --git a/Tests/SwiftDocCTests/Rendering/DocumentationContentRendererTests.swift b/Tests/SwiftDocCTests/Rendering/DocumentationContentRendererTests.swift index 6204727875..aba604d1f7 100644 --- a/Tests/SwiftDocCTests/Rendering/DocumentationContentRendererTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DocumentationContentRendererTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -14,8 +14,8 @@ import Markdown @testable import SwiftDocC class DocumentationContentRendererTests: XCTestCase { - func testReplacesTypeIdentifierSubHeadingFragmentWithIdentifierForSwift() throws { - let subHeadingFragments = try makeDocumentationContentRenderer() + func testReplacesTypeIdentifierSubHeadingFragmentWithIdentifierForSwift() async throws { + let subHeadingFragments = try await makeDocumentationContentRenderer() .subHeadingFragments(for: nodeWithSubheadingAndNavigatorVariants) XCTAssertEqual( @@ -45,8 +45,8 @@ class DocumentationContentRendererTests: XCTestCase { ) } - func testDoesNotReplaceSubHeadingFragmentsForOtherLanguagesThanSwift() throws { - let subHeadingFragments = try makeDocumentationContentRenderer() + func testDoesNotReplaceSubHeadingFragmentsForOtherLanguagesThanSwift() async throws { + let subHeadingFragments = try await makeDocumentationContentRenderer() .subHeadingFragments(for: nodeWithSubheadingAndNavigatorVariants) guard case .replace(let fragments) = subHeadingFragments.variants.first?.patch.first else { @@ -73,8 +73,8 @@ class DocumentationContentRendererTests: XCTestCase { ) } - func testReplacesTypeIdentifierNavigatorFragmentWithIdentifierForSwift() throws { - let navigatorFragments = try makeDocumentationContentRenderer() + func testReplacesTypeIdentifierNavigatorFragmentWithIdentifierForSwift() async throws { + let navigatorFragments = try await makeDocumentationContentRenderer() .navigatorFragments(for: nodeWithSubheadingAndNavigatorVariants) XCTAssertEqual( @@ -104,8 +104,8 @@ class DocumentationContentRendererTests: XCTestCase { ) } - func testDoesNotReplacesNavigatorFragmentsForOtherLanguagesThanSwift() throws { - let navigatorFragments = try makeDocumentationContentRenderer() + func testDoesNotReplacesNavigatorFragmentsForOtherLanguagesThanSwift() async throws { + let navigatorFragments = try await makeDocumentationContentRenderer() .navigatorFragments(for: nodeWithSubheadingAndNavigatorVariants) guard case .replace(let fragments) = navigatorFragments.variants.first?.patch.first else { @@ -138,9 +138,9 @@ private extension DocumentationDataVariantsTrait { } private extension DocumentationContentRendererTests { - func makeDocumentationContentRenderer() throws -> DocumentationContentRenderer { - let (bundle, context) = try testBundleAndContext() - return DocumentationContentRenderer(documentationContext: context, bundle: bundle) + func makeDocumentationContentRenderer() async throws -> DocumentationContentRenderer { + let (_, context) = try await testBundleAndContext() + return DocumentationContentRenderer(context: context) } var nodeWithSubheadingAndNavigatorVariants: DocumentationNode { @@ -155,7 +155,7 @@ private extension DocumentationContentRendererTests { sourceLanguage: .swift, availableSourceLanguages: [ .swift, - .init(id: DocumentationDataVariantsTrait.otherLanguage.interfaceLanguage!) + DocumentationDataVariantsTrait.otherLanguage.sourceLanguage! ], name: .symbol(name: ""), markup: Document(parsing: ""), diff --git a/Tests/SwiftDocCTests/Rendering/ExternalLinkTitleTests.swift b/Tests/SwiftDocCTests/Rendering/ExternalLinkTitleTests.swift index cd8d2481bf..34d78065fa 100644 --- a/Tests/SwiftDocCTests/Rendering/ExternalLinkTitleTests.swift +++ b/Tests/SwiftDocCTests/Rendering/ExternalLinkTitleTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -13,7 +13,7 @@ import XCTest import Markdown class ExternalLinkTitleTests: XCTestCase { - private func getTranslatorAndBlockContentForMarkup(_ markupSource: String) throws -> (translator: RenderNodeTranslator, content: [RenderBlockContent]) { + private func getTranslatorAndBlockContentForMarkup(_ markupSource: String) async throws -> (translator: RenderNodeTranslator, content: [RenderBlockContent]) { let document = Document(parsing: markupSource, options: [.parseBlockDirectives, .parseSymbolLinks]) let testReference = ResolvedTopicReference(bundleID: "org.swift.docc", path: "/test", sourceLanguage: .swift) let node = DocumentationNode(reference: testReference, @@ -24,21 +24,21 @@ class ExternalLinkTitleTests: XCTestCase { semantic: Semantic()) - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let (_, context) = try await testBundleAndContext() + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let result = translator.visit(MarkupContainer(document.children)) as! [RenderBlockContent] return (translator, result) } - func testPlainTextExternalLinkTitle() throws { + func testPlainTextExternalLinkTitle() async throws { let markupSource = """ # Test This is a plain text link: [Example](https://www.example.com). """ - let (translator, content) = try getTranslatorAndBlockContentForMarkup(markupSource) + let (translator, content) = try await getTranslatorAndBlockContentForMarkup(markupSource) guard case let .paragraph(firstParagraph) = content[1] else { XCTFail("Unexpected render tree.") @@ -58,14 +58,14 @@ class ExternalLinkTitleTests: XCTestCase { XCTAssertEqual(linkReference?.titleInlineContent, expectedLinkTitle, "Plain text title should have been rendered.") } - func testEmphasisExternalLinkTitle() throws { + func testEmphasisExternalLinkTitle() async throws { let markupSource = """ # Test This is an emphasized text link: [*Apple*](https://www.example.com). """ - let (translator, content) = try getTranslatorAndBlockContentForMarkup(markupSource) + let (translator, content) = try await getTranslatorAndBlockContentForMarkup(markupSource) guard case let .paragraph(firstParagraph) = content[1] else { XCTFail("Unexpected render tree.") @@ -85,14 +85,14 @@ class ExternalLinkTitleTests: XCTestCase { XCTAssertEqual(linkReference?.titleInlineContent, expectedLinkTitle, "Emphasized text title should have been rendered.") } - func testStrongExternalLinkTitle() throws { + func testStrongExternalLinkTitle() async throws { let markupSource = """ # Test This is a strong text link: [**Apple**](https://www.example.com). """ - let (translator, content) = try getTranslatorAndBlockContentForMarkup(markupSource) + let (translator, content) = try await getTranslatorAndBlockContentForMarkup(markupSource) guard case let .paragraph(firstParagraph) = content[1] else { XCTFail("Unexpected render tree.") @@ -112,14 +112,14 @@ class ExternalLinkTitleTests: XCTestCase { XCTAssertEqual(linkReference?.titleInlineContent, expectedLinkTitle, "Strong text title should have been rendered.") } - func testCodeVoiceExternalLinkTitle() throws { + func testCodeVoiceExternalLinkTitle() async throws { let markupSource = """ # Test This is a code voice text link: [`Apple`](https://www.example.com). """ - let (translator, content) = try getTranslatorAndBlockContentForMarkup(markupSource) + let (translator, content) = try await getTranslatorAndBlockContentForMarkup(markupSource) guard case let .paragraph(firstParagraph) = content[1] else { XCTFail("Unexpected render tree.") @@ -139,14 +139,14 @@ class ExternalLinkTitleTests: XCTestCase { XCTAssertEqual(linkReference?.titleInlineContent, expectedLinkTitle, "Code voice text title should have been rendered.") } - func testMixedExternalLinkTitle() throws { + func testMixedExternalLinkTitle() async throws { let markupSource = """ # Test This is a mixed text link: [**This** *is* a `fancy` _link_ title.](https://www.example.com). """ - let (translator, content) = try getTranslatorAndBlockContentForMarkup(markupSource) + let (translator, content) = try await getTranslatorAndBlockContentForMarkup(markupSource) guard case let .paragraph(firstParagraph) = content[1] else { XCTFail("Unexpected render tree.") @@ -174,7 +174,7 @@ class ExternalLinkTitleTests: XCTestCase { } - func testMultipleLinksWithEqualURL() throws { + func testMultipleLinksWithEqualURL() async throws { let markupSource = """ # Test @@ -182,7 +182,7 @@ class ExternalLinkTitleTests: XCTestCase { This is an emphasized text link: [*Apple*](https://www.example.com). """ - let (translator, content) = try getTranslatorAndBlockContentForMarkup(markupSource) + let (translator, content) = try await getTranslatorAndBlockContentForMarkup(markupSource) guard case let .paragraph(firstParagraph) = content[1] else { XCTFail("Unexpected render tree.") diff --git a/Tests/SwiftDocCTests/Rendering/HeadingAnchorTests.swift b/Tests/SwiftDocCTests/Rendering/HeadingAnchorTests.swift index bee43f167c..778e190577 100644 --- a/Tests/SwiftDocCTests/Rendering/HeadingAnchorTests.swift +++ b/Tests/SwiftDocCTests/Rendering/HeadingAnchorTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -14,7 +14,7 @@ import XCTest import SwiftDocCTestUtilities class HeadingAnchorTests: XCTestCase { - func testEncodeHeadingAnchor() throws { + func testEncodeHeadingAnchor() async throws { let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "Root.md", utf8Content: """ @@ -34,12 +34,12 @@ class HeadingAnchorTests: XCTestCase { """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let reference = try XCTUnwrap(context.soleRootModuleReference) let node = try context.entity(with: reference) - let renderContext = RenderContext(documentationContext: context, bundle: bundle) - let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + let renderContext = RenderContext(documentationContext: context) + let converter = DocumentationContextConverter(context: context, renderContext: renderContext) let renderNode = try XCTUnwrap(converter.renderNode(for: node)) // Check heading anchors are encoded diff --git a/Tests/SwiftDocCTests/Rendering/LinkTitleResolverTests.swift b/Tests/SwiftDocCTests/Rendering/LinkTitleResolverTests.swift index 2ec636ac11..a4f52acfbd 100644 --- a/Tests/SwiftDocCTests/Rendering/LinkTitleResolverTests.swift +++ b/Tests/SwiftDocCTests/Rendering/LinkTitleResolverTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -13,8 +13,8 @@ import XCTest @testable import SwiftDocC class LinkTitleResolverTests: XCTestCase { - func testSymbolTitleResolving() throws { - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testSymbolTitleResolving() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let resolver = LinkTitleResolver(context: context, source: nil) guard let reference = context.knownIdentifiers.filter({ ref -> Bool in return ref.path.hasSuffix("MyProtocol") diff --git a/Tests/SwiftDocCTests/Rendering/MentionsRenderSectionTests.swift b/Tests/SwiftDocCTests/Rendering/MentionsRenderSectionTests.swift index f61244750e..720ae88a47 100644 --- a/Tests/SwiftDocCTests/Rendering/MentionsRenderSectionTests.swift +++ b/Tests/SwiftDocCTests/Rendering/MentionsRenderSectionTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -15,21 +15,21 @@ import XCTest class MentionsRenderSectionTests: XCTestCase { /// Verify that the Mentioned In section is present when a symbol is mentioned, /// pointing to the correct article. - func testMentionedInSectionFull() throws { + func testMentionedInSectionFull() async throws { enableFeatureFlag(\.isMentionedInEnabled) - let (bundle, context) = try createMentionedInTestBundle() + let (_, context) = try await createMentionedInTestBundle() let identifier = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/MentionedIn/MyClass", sourceLanguage: .swift ) let mentioningArticle = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/MentionedIn/ArticleMentioningSymbol", sourceLanguage: .swift ) let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode let mentionsSection = try XCTUnwrap(renderNode.primaryContentSections.mapFirst { $0 as? MentionsRenderSection }) XCTAssertEqual(1, mentionsSection.mentions.count) @@ -38,16 +38,16 @@ class MentionsRenderSectionTests: XCTestCase { } /// If there are no qualifying mentions of a symbol, the Mentioned In section should not appear. - func testMentionedInSectionEmpty() throws { + func testMentionedInSectionEmpty() async throws { enableFeatureFlag(\.isMentionedInEnabled) - let (bundle, context) = try createMentionedInTestBundle() + let (_, context) = try await createMentionedInTestBundle() let identifier = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/MentionedIn/MyClass/myFunction()", sourceLanguage: .swift ) let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode let mentionsSection = renderNode.primaryContentSections.mapFirst { $0 as? MentionsRenderSection } XCTAssertNil(mentionsSection) diff --git a/Tests/SwiftDocCTests/Rendering/PageKindTests.swift b/Tests/SwiftDocCTests/Rendering/PageKindTests.swift index 0df89e7d21..ac9add76fc 100644 --- a/Tests/SwiftDocCTests/Rendering/PageKindTests.swift +++ b/Tests/SwiftDocCTests/Rendering/PageKindTests.swift @@ -15,20 +15,20 @@ import XCTest class PageKindTests: XCTestCase { - private func generateRenderNodeFromBundle(bundleName: String, resolvedTopicPath: String) throws -> RenderNode { - let (bundle, context) = try testBundleAndContext(named: bundleName) + private func generateRenderNodeFromBundle(bundleName: String, resolvedTopicPath: String) async throws -> RenderNode { + let (bundle, context) = try await testBundleAndContext(named: bundleName) let reference = ResolvedTopicReference( bundleID: bundle.id, path: resolvedTopicPath, sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) return try XCTUnwrap(translator.visitArticle(article) as? RenderNode) } - func testPageKindSampleCode() throws { - let renderNode = try generateRenderNodeFromBundle( + func testPageKindSampleCode() async throws { + let renderNode = try await generateRenderNodeFromBundle( bundleName: "SampleBundle", resolvedTopicPath: "/documentation/SampleBundle/MyLocalSample" ) @@ -36,8 +36,8 @@ class PageKindTests: XCTestCase { XCTAssertEqual(renderNode.metadata.roleHeading, Metadata.PageKind.Kind.sampleCode.titleHeading) } - func testPageKindArticle() throws { - let renderNode = try generateRenderNodeFromBundle( + func testPageKindArticle() async throws { + let renderNode = try await generateRenderNodeFromBundle( bundleName: "SampleBundle", resolvedTopicPath: "/documentation/SampleBundle/MySample" ) @@ -46,8 +46,8 @@ class PageKindTests: XCTestCase { XCTAssertEqual(renderNode.metadata.roleHeading, Metadata.PageKind.Kind.article.titleHeading) } - func testPageKindDefault() throws { - let renderNode = try generateRenderNodeFromBundle( + func testPageKindDefault() async throws { + let renderNode = try await generateRenderNodeFromBundle( bundleName: "AvailabilityBundle", resolvedTopicPath: "/documentation/AvailabilityBundle/ComplexAvailable" ) @@ -55,8 +55,8 @@ class PageKindTests: XCTestCase { XCTAssertEqual(renderNode.metadata.roleHeading, "Article") } - func testPageKindReference() throws { - let renderNode = try generateRenderNodeFromBundle( + func testPageKindReference() async throws { + let renderNode = try await generateRenderNodeFromBundle( bundleName: "SampleBundle", resolvedTopicPath: "/documentation/SomeSample" ) @@ -64,7 +64,7 @@ class PageKindTests: XCTestCase { XCTAssertEqual(sampleReference.role, RenderMetadata.Role.sampleCode.rawValue) } - func testValidMetadataWithOnlyPageKind() throws { + func testValidMetadataWithOnlyPageKind() async throws { let source = """ @Metadata { @PageKind(article) @@ -75,7 +75,7 @@ class PageKindTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "SampleBundle") + let (bundle, _) = try await testBundleAndContext(named: "SampleBundle") directive.map { directive in var problems = [Problem]() @@ -90,8 +90,8 @@ class PageKindTests: XCTestCase { // Verify that we assign the `Collection` role to the root article of a // documentation catalog that contains only one article. - func testRoleForSingleArticleCatalog() throws { - let renderNode = try generateRenderNodeFromBundle( + func testRoleForSingleArticleCatalog() async throws { + let renderNode = try await generateRenderNodeFromBundle( bundleName: "BundleWithSingleArticle", resolvedTopicPath: "/documentation/Article" ) @@ -100,8 +100,8 @@ class PageKindTests: XCTestCase { // Verify we assign the `Collection` role to the root article of an article-only // documentation catalog that doesn't include manual curation - func testRoleForArticleOnlyCatalogWithNoCuration() throws { - let renderNode = try generateRenderNodeFromBundle( + func testRoleForArticleOnlyCatalogWithNoCuration() async throws { + let renderNode = try await generateRenderNodeFromBundle( bundleName: "BundleWithArticlesNoCurated", resolvedTopicPath: "/documentation/Article" ) diff --git a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift index c36bc68f3d..f013c1e4f4 100644 --- a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -33,15 +33,15 @@ class PlatformAvailabilityTests: XCTestCase { } /// Ensure that adding `@Available` directives in an article causes the final RenderNode to contain the appropriate availability data. - func testPlatformAvailabilityFromArticle() throws { - let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + func testPlatformAvailabilityFromArticle() async throws { + let (bundle, context) = try await testBundleAndContext(named: "AvailabilityBundle") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/AvailableArticle", sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) XCTAssertEqual(availability.count, 1) @@ -52,15 +52,15 @@ class PlatformAvailabilityTests: XCTestCase { } /// Ensure that adding `@Available` directives in an extension file overrides the symbol's availability. - func testPlatformAvailabilityFromExtension() throws { - let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + func testPlatformAvailabilityFromExtension() async throws { + let (bundle, context) = try await testBundleAndContext(named: "AvailabilityBundle") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) XCTAssertEqual(availability.count, 1) @@ -70,15 +70,15 @@ class PlatformAvailabilityTests: XCTestCase { XCTAssert(iosAvailability.isBeta != true) } - func testMultiplePlatformAvailabilityFromArticle() throws { - let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + func testMultiplePlatformAvailabilityFromArticle() async throws { + let (bundle, context) = try await testBundleAndContext(named: "AvailabilityBundle") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/AvailabilityBundle/ComplexAvailable", sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) XCTAssertEqual(availability.count, 3) @@ -98,15 +98,15 @@ class PlatformAvailabilityTests: XCTestCase { }) } - func testArbitraryPlatformAvailability() throws { - let (bundle, context) = try testBundleAndContext(named: "AvailabilityBundle") + func testArbitraryPlatformAvailability() async throws { + let (bundle, context) = try await testBundleAndContext(named: "AvailabilityBundle") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/AvailabilityBundle/ArbitraryPlatforms", sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) XCTAssertEqual(availability.count, 2) @@ -123,8 +123,8 @@ class PlatformAvailabilityTests: XCTestCase { } // Test that the Info.plist default availability does not affect the deprecated/unavailable availabilities provided by the symbol graph. - func testAvailabilityParserWithInfoPlistDefaultAvailability() throws { - let (bundle, context) = try testBundleAndContext(named: "AvailabilityOverrideBundle") + func testAvailabilityParserWithInfoPlistDefaultAvailability() async throws { + let (bundle, context) = try await testBundleAndContext(named: "AvailabilityOverrideBundle") let reference = ResolvedTopicReference( bundleID: bundle.id, @@ -132,7 +132,7 @@ class PlatformAvailabilityTests: XCTestCase { sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) XCTAssertEqual(availability.count, 5) @@ -160,18 +160,18 @@ class PlatformAvailabilityTests: XCTestCase { } /// Ensure that adding `@Available` directives for platform versions marked as beta in an article causes the final RenderNode to contain the appropriate availability data. - func testBetaPlatformAvailabilityFromArticle() throws { + func testBetaPlatformAvailabilityFromArticle() async throws { let platformMetadata = [ "iOS": PlatformVersion(VersionTriplet(16, 0, 0), beta: true), ] - let (bundle, context) = try testBundleWithConfiguredPlatforms(named: "AvailabilityBundle", platformMetadata: platformMetadata) + let (_, context) = try await testBundleWithConfiguredPlatforms(named: "AvailabilityBundle", platformMetadata: platformMetadata) let reference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/AvailableArticle", sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) XCTAssertEqual(availability.count, 1) @@ -181,20 +181,20 @@ class PlatformAvailabilityTests: XCTestCase { XCTAssert(iosAvailability.isBeta == true) } - func testMultipleBetaPlatformAvailabilityFromArticle() throws { + func testMultipleBetaPlatformAvailabilityFromArticle() async throws { let platformMetadata = [ "iOS": PlatformVersion(VersionTriplet(15, 0, 0), beta: true), "macOS": PlatformVersion(VersionTriplet(12, 0, 0), beta: true), "watchOS": PlatformVersion(VersionTriplet(7, 0, 0), beta: true), ] - let (bundle, context) = try testBundleWithConfiguredPlatforms(named: "AvailabilityBundle", platformMetadata: platformMetadata) + let (_, context) = try await testBundleWithConfiguredPlatforms(named: "AvailabilityBundle", platformMetadata: platformMetadata) let reference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/AvailabilityBundle/ComplexAvailable", sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) XCTAssertEqual(availability.count, 3) @@ -215,18 +215,18 @@ class PlatformAvailabilityTests: XCTestCase { } /// Ensure that adding `@Available` directives in an extension file overrides the symbol's availability. - func testBetaPlatformAvailabilityFromExtension() throws { + func testBetaPlatformAvailabilityFromExtension() async throws { let platformMetadata = [ "iOS": PlatformVersion(VersionTriplet(16, 0, 0), beta: true), ] - let (bundle, context) = try testBundleWithConfiguredPlatforms(named: "AvailabilityBundle", platformMetadata: platformMetadata) + let (_, context) = try await testBundleWithConfiguredPlatforms(named: "AvailabilityBundle", platformMetadata: platformMetadata) let reference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue) XCTAssertEqual(availability.count, 1) @@ -237,11 +237,11 @@ class PlatformAvailabilityTests: XCTestCase { } - func testBundleWithConfiguredPlatforms(named testBundleName: String, platformMetadata: [String : PlatformVersion]) throws -> (DocumentationBundle, DocumentationContext) { + func testBundleWithConfiguredPlatforms(named testBundleName: String, platformMetadata: [String : PlatformVersion]) async throws -> (DocumentationBundle, DocumentationContext) { let bundleURL = try XCTUnwrap(Bundle.module.url(forResource: testBundleName, withExtension: "docc", subdirectory: "Test Bundles")) var configuration = DocumentationContext.Configuration() configuration.externalMetadata.currentPlatforms = platformMetadata - let (_, bundle, context) = try loadBundle(from: bundleURL, configuration: configuration) + let (_, bundle, context) = try await loadBundle(from: bundleURL, configuration: configuration) return (bundle, context) } diff --git a/Tests/SwiftDocCTests/Rendering/PropertyListDetailsRenderSectionTests.swift b/Tests/SwiftDocCTests/Rendering/PropertyListDetailsRenderSectionTests.swift index 7963b6d151..a4790d4d28 100644 --- a/Tests/SwiftDocCTests/Rendering/PropertyListDetailsRenderSectionTests.swift +++ b/Tests/SwiftDocCTests/Rendering/PropertyListDetailsRenderSectionTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -16,9 +16,9 @@ import SwiftDocCTestUtilities class PropertyListDetailsRenderSectionTests: XCTestCase { - func testDecoding() throws { + func testDecoding() async throws { - func getPlistDetailsSection(arrayMode: any CustomStringConvertible, baseType: any CustomStringConvertible, rawKey: any CustomStringConvertible) throws -> PropertyListDetailsRenderSection { + func getPlistDetailsSection(arrayMode: any CustomStringConvertible, baseType: any CustomStringConvertible, rawKey: any CustomStringConvertible) async throws -> PropertyListDetailsRenderSection { let symbolJSON = """ { "accessLevel" : "public", @@ -55,16 +55,17 @@ class PropertyListDetailsRenderSectionTests: XCTestCase { let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "MyModule.symbols.json", utf8Content: symbolGraphString) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let node = try XCTUnwrap(context.documentationCache["plist:propertylistkey"]) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(node) return try XCTUnwrap(renderNode.primaryContentSections.mapFirst(where: { $0 as? PropertyListDetailsRenderSection })) } // Assert that the Details section is correctly generated when passing valid values into the plistDetails JSON object. + let withArrayMode = try await getPlistDetailsSection(arrayMode: true, baseType: "\"string\"", rawKey: "\"property-list-key\"") XCTAssertEqual( - try getPlistDetailsSection(arrayMode: true, baseType: "\"string\"", rawKey: "\"property-list-key\""), + withArrayMode, PropertyListDetailsRenderSection( details: PropertyListDetailsRenderSection.Details( rawKey: "property-list-key", @@ -76,8 +77,9 @@ class PropertyListDetailsRenderSectionTests: XCTestCase { ) ) + let withoutArrayMode = try await getPlistDetailsSection(arrayMode: false, baseType: "\"string\"", rawKey: "\"property-list-key\"") XCTAssertEqual( - try getPlistDetailsSection(arrayMode: false, baseType: "\"string\"", rawKey: "\"property-list-key\""), + withoutArrayMode, PropertyListDetailsRenderSection( details: PropertyListDetailsRenderSection.Details( rawKey: "property-list-key", @@ -91,12 +93,14 @@ class PropertyListDetailsRenderSectionTests: XCTestCase { // Assert that the Details section does not decode unsupported values. do { - _ = try getPlistDetailsSection(arrayMode: true, baseType: true, rawKey: "\"property-list-key\"") + _ = try await getPlistDetailsSection(arrayMode: true, baseType: true, rawKey: "\"property-list-key\"") + XCTFail("Didn't raise an error") } catch { XCTAssertTrue(error.localizedDescription.contains("isn’t in the correct format")) } do { - _ = try getPlistDetailsSection(arrayMode: true, baseType: "\"string\"", rawKey: 1) + _ = try await getPlistDetailsSection(arrayMode: true, baseType: "\"string\"", rawKey: 1) + XCTFail("Didn't raise an error") } catch { XCTAssertTrue(error.localizedDescription.contains("isn’t in the correct format")) } diff --git a/Tests/SwiftDocCTests/Rendering/RESTSymbolsTests.swift b/Tests/SwiftDocCTests/Rendering/RESTSymbolsTests.swift index 4ab337313e..9de46ea5c3 100644 --- a/Tests/SwiftDocCTests/Rendering/RESTSymbolsTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RESTSymbolsTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -312,9 +312,8 @@ class RESTSymbolsTests: XCTestCase { AssertRoundtrip(for: object) } - func testReferenceOfEntitlementWithKeyName() throws { - - func createDocumentationTopicRenderReferenceForSymbol(keyCustomName: String?, extraFiles: [TextFile] = []) throws -> TopicRenderReference { + func testReferenceOfEntitlementWithKeyName() async throws { + func createDocumentationTopicRenderReferenceForSymbol(keyCustomName: String?, extraFiles: [TextFile] = []) async throws -> TopicRenderReference { let someSymbol = makeSymbol( id: "plist-key-symbolname", kind: .init(rawValue: "enum"), @@ -331,30 +330,32 @@ class RESTSymbolsTests: XCTestCase { )), ] + extraFiles ) - let (bundle, context) = try loadBundle(catalog: catalog) - let moduleReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleName", sourceLanguage: .swift) + let (_, context) = try await loadBundle(catalog: catalog) + let moduleReference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleName", sourceLanguage: .swift) let moduleSymbol = try XCTUnwrap((try context.entity(with: moduleReference)).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: moduleReference) + var translator = RenderNodeTranslator(context: context, identifier: moduleReference) let renderNode = translator.visit(moduleSymbol) as! RenderNode return try XCTUnwrap((renderNode.references["doc://unit-test/documentation/ModuleName/plist-key-symbolname"] as? TopicRenderReference)) } // The symbol has a custom title. - var propertyListKeyNames = try XCTUnwrap(createDocumentationTopicRenderReferenceForSymbol(keyCustomName: "Symbol Custom Title").propertyListKeyNames) + let topicReferenceWithCustomKeyName = try await createDocumentationTopicRenderReferenceForSymbol(keyCustomName: "Symbol Custom Title") + var propertyListKeyNames = try XCTUnwrap(topicReferenceWithCustomKeyName.propertyListKeyNames) // Check that the reference contains the key symbol name. XCTAssertEqual(propertyListKeyNames.titleStyle, .useRawKey) XCTAssertEqual(propertyListKeyNames.rawKey, "plist-key-symbolname") XCTAssertEqual(propertyListKeyNames.displayName, "Symbol Custom Title") // The symbol does not have a custom title. - propertyListKeyNames = try XCTUnwrap(createDocumentationTopicRenderReferenceForSymbol(keyCustomName: nil).propertyListKeyNames) + let topicReferenceWithoutCustomKeyName = try await createDocumentationTopicRenderReferenceForSymbol(keyCustomName: nil) + propertyListKeyNames = try XCTUnwrap(topicReferenceWithoutCustomKeyName.propertyListKeyNames) // Check that the reference does not contain the key symbol name. XCTAssertEqual(propertyListKeyNames.titleStyle, .useRawKey) XCTAssertEqual(propertyListKeyNames.rawKey, "plist-key-symbolname") XCTAssertNil(propertyListKeyNames.displayName) // The symbol has a custom title and is extended via markdown. - var referenceNode = try XCTUnwrap(createDocumentationTopicRenderReferenceForSymbol( + var referenceNode = try await createDocumentationTopicRenderReferenceForSymbol( keyCustomName: "Symbol Custom Title", extraFiles: [ TextFile(name: "plist-key-symbolname.md", utf8Content: @@ -365,7 +366,7 @@ class RESTSymbolsTests: XCTestCase { """ ) ] - )) + ) propertyListKeyNames = try XCTUnwrap(referenceNode.propertyListKeyNames) // Check that the reference contains the raw key and title matches the // key name. @@ -375,7 +376,7 @@ class RESTSymbolsTests: XCTestCase { XCTAssertEqual(propertyListKeyNames.displayName, "Symbol Custom Title") // The symbol has a custom title and is the markdown defines a `Display Name` directive. - referenceNode = try XCTUnwrap(createDocumentationTopicRenderReferenceForSymbol( + referenceNode = try await createDocumentationTopicRenderReferenceForSymbol( keyCustomName: "Symbol Custom Title", extraFiles: [ TextFile(name: "plist-key-symbolname.md", utf8Content: @@ -390,7 +391,7 @@ class RESTSymbolsTests: XCTestCase { """ ) ] - )) + ) propertyListKeyNames = try XCTUnwrap(referenceNode.propertyListKeyNames) // Check that the reference contains the raw key and the title matches the // markdown display name. @@ -400,7 +401,7 @@ class RESTSymbolsTests: XCTestCase { XCTAssertEqual(propertyListKeyNames.displayName, "Symbol Custom Title") // The symbol does not have a custom title and is extended via markdown using a `Display Name` directive. - referenceNode = try createDocumentationTopicRenderReferenceForSymbol( + referenceNode = try await createDocumentationTopicRenderReferenceForSymbol( keyCustomName: nil, extraFiles: [ TextFile(name: "plist-key-symbolname.md", utf8Content: @@ -424,6 +425,4 @@ class RESTSymbolsTests: XCTestCase { XCTAssertEqual(propertyListKeyNames.rawKey, "plist-key-symbolname") XCTAssertEqual(propertyListKeyNames.displayName, nil) } - - } diff --git a/Tests/SwiftDocCTests/Rendering/RenderBlockContent_ThematicBreakTests.swift b/Tests/SwiftDocCTests/Rendering/RenderBlockContent_ThematicBreakTests.swift index 3fe7834d70..5f2a70ee72 100644 --- a/Tests/SwiftDocCTests/Rendering/RenderBlockContent_ThematicBreakTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RenderBlockContent_ThematicBreakTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -25,7 +25,7 @@ class RenderBlockContent_ThematicBreakTests: XCTestCase { } // MARK: - Thematic Break Markdown Variants - func testThematicBreakVariants() throws { + func testThematicBreakVariants() async throws { let source = """ --- @@ -38,9 +38,9 @@ class RenderBlockContent_ThematicBreakTests: XCTestCase { XCTAssertEqual(markup.childCount, 3) - let (bundle, context) = try testBundleAndContext() + let (bundle, context) = try await testBundleAndContext() - var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestThematicBreak", sourceLanguage: .swift)) + var contentTranslator = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestThematicBreak", sourceLanguage: .swift)) let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) let expectedContent: [RenderBlockContent] = [ @@ -52,7 +52,7 @@ class RenderBlockContent_ThematicBreakTests: XCTestCase { XCTAssertEqual(expectedContent, renderContent) } - func testThematicBreakVariantsWithSpaces() throws { + func testThematicBreakVariantsWithSpaces() async throws { let source = """ - - - @@ -65,9 +65,9 @@ class RenderBlockContent_ThematicBreakTests: XCTestCase { XCTAssertEqual(markup.childCount, 3) - let (bundle, context) = try testBundleAndContext() + let (bundle, context) = try await testBundleAndContext() - var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestThematicBreak", sourceLanguage: .swift)) + var contentTranslator = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestThematicBreak", sourceLanguage: .swift)) let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) let expectedContent: [RenderBlockContent] = [ @@ -79,7 +79,7 @@ class RenderBlockContent_ThematicBreakTests: XCTestCase { XCTAssertEqual(expectedContent, renderContent) } - func testThematicBreakMoreThanThreeCharacters() throws { + func testThematicBreakMoreThanThreeCharacters() async throws { let source = """ ---- @@ -95,9 +95,9 @@ class RenderBlockContent_ThematicBreakTests: XCTestCase { XCTAssertEqual(markup.childCount, 6) - let (bundle, context) = try testBundleAndContext() + let (bundle, context) = try await testBundleAndContext() - var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestThematicBreak", sourceLanguage: .swift)) + var contentTranslator = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/TestThematicBreak", sourceLanguage: .swift)) let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) let expectedContent: [RenderBlockContent] = [ diff --git a/Tests/SwiftDocCTests/Rendering/RenderContentCompilerTests.swift b/Tests/SwiftDocCTests/Rendering/RenderContentCompilerTests.swift index 788d7c7386..29b3728990 100644 --- a/Tests/SwiftDocCTests/Rendering/RenderContentCompilerTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RenderContentCompilerTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023-2024 Apple Inc. and the Swift project authors + Copyright (c) 2023-2025 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 @@ -11,12 +11,29 @@ import Foundation import Markdown @testable import SwiftDocC +import SwiftDocCTestUtilities import XCTest +typealias Position = RenderBlockContent.CodeBlockOptions.Position + class RenderContentCompilerTests: XCTestCase { - func testLinkOverrideTitle() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var compiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testLinkOverrideTitle() async throws { + let catalog = Folder(name: "unit-test.docc", content: [ + TextFile(name: "article.md", utf8Content: """ + # First + """), + TextFile(name: "article2.md", utf8Content: """ + # Second + """), + TextFile(name: "article3.md", utf8Content: """ + # Third + """), + + InfoPlist(identifier: "org.swift.docc.example") + ]) + + let (_, context) = try await loadBundle(catalog: catalog) + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: context.inputs.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = """ [Example](http://example.com) @@ -87,7 +104,7 @@ class RenderContentCompilerTests: XCTestCase { return } let link = RenderInlineContent.reference( - identifier: .init("doc://org.swift.docc.example/documentation/Test-Bundle/article"), + identifier: .init("doc://org.swift.docc.example/documentation/unit-test/article"), isActive: true, overridingTitle: "Custom Title", overridingTitleInlineContent: [.text("Custom Title")]) @@ -99,7 +116,7 @@ class RenderContentCompilerTests: XCTestCase { return } let link = RenderInlineContent.reference( - identifier: .init("doc://org.swift.docc.example/documentation/Test-Bundle/article2"), + identifier: .init("doc://org.swift.docc.example/documentation/unit-test/article2"), isActive: true, overridingTitle: "Custom Image Content ", overridingTitleInlineContent: [ @@ -123,7 +140,7 @@ class RenderContentCompilerTests: XCTestCase { return } let link = RenderInlineContent.reference( - identifier: .init("doc://org.swift.docc.example/documentation/Test-Bundle/article3"), + identifier: .init("doc://org.swift.docc.example/documentation/unit-test/article3"), isActive: true, overridingTitle: nil, overridingTitleInlineContent: nil @@ -132,9 +149,9 @@ class RenderContentCompilerTests: XCTestCase { } } - func testLineBreak() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") - var compiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testLineBreak() async throws { + let (_, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: context.inputs.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = #""" Backslash before new line\ @@ -197,9 +214,9 @@ class RenderContentCompilerTests: XCTestCase { } } - func testThematicBreak() throws { - let (bundle, context) = try testBundleAndContext() - var compiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + func testThematicBreak() async throws { + let (bundle, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = #""" @@ -223,4 +240,488 @@ class RenderContentCompilerTests: XCTestCase { XCTAssertEqual(documentThematicBreak, thematicBreak) } } + + func testCopyToClipboard() async throws { + enableFeatureFlag(\.isExperimentalCodeBlockAnnotationsEnabled) + + let (bundle, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + + let source = #""" + ```swift + let x = 1 + ``` + """# + let document = Document(parsing: source) + + let result = document.children.flatMap { compiler.visit($0) } + + let renderCodeBlock = try XCTUnwrap(result[0] as? RenderBlockContent) + guard case let .codeListing(codeListing) = renderCodeBlock else { + XCTFail("Expected RenderBlockContent.codeListing") + return + } + + XCTAssertEqual(codeListing.options?.copyToClipboard, true) + } + + func testNoCopyToClipboard() async throws { + enableFeatureFlag(\.isExperimentalCodeBlockAnnotationsEnabled) + + let (bundle, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + + let source = #""" + ```swift, nocopy + let x = 1 + ``` + """# + let document = Document(parsing: source) + + let result = document.children.flatMap { compiler.visit($0) } + + let renderCodeBlock = try XCTUnwrap(result[0] as? RenderBlockContent) + guard case let .codeListing(codeListing) = renderCodeBlock else { + XCTFail("Expected RenderBlockContent.codeListing") + return + } + + XCTAssertEqual(codeListing.options?.copyToClipboard, false) + } + + func testCopyToClipboardNoLang() async throws { + enableFeatureFlag(\.isExperimentalCodeBlockAnnotationsEnabled) + + let (bundle, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + + let source = #""" + ```nocopy + let x = 1 + ``` + """# + let document = Document(parsing: source) + + let result = document.children.flatMap { compiler.visit($0) } + + let renderCodeBlock = try XCTUnwrap(result[0] as? RenderBlockContent) + guard case let .codeListing(codeListing) = renderCodeBlock else { + XCTFail("Expected RenderBlockContent.codeListing") + return + } + + XCTAssertEqual(codeListing.syntax, nil) + XCTAssertEqual(codeListing.options?.copyToClipboard, false) + } + + func testCopyToClipboardNoFeatureFlag() async throws { + let (bundle, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + + let source = #""" + ```swift + let x = 1 + ``` + """# + let document = Document(parsing: source) + let result = document.children.flatMap { compiler.visit($0) } + + let renderCodeBlock = try XCTUnwrap(result[0] as? RenderBlockContent) + guard case let .codeListing(codeListing) = renderCodeBlock else { + XCTFail("Expected RenderBlockContent.codeListing") + return + } + + XCTAssertEqual(codeListing.options?.copyToClipboard, nil) + } + + func testNoCopyToClipboardNoFeatureFlag() async throws { + let (bundle, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + + let source = #""" + ```swift, nocopy + let x = 1 + ``` + """# + let document = Document(parsing: source) + + let result = document.children.flatMap { compiler.visit($0) } + + let renderCodeBlock = try XCTUnwrap(result[0] as? RenderBlockContent) + guard case let .codeListing(codeListing) = renderCodeBlock else { + XCTFail("Expected RenderBlockContent.codeListing") + return + } + + XCTAssertEqual(codeListing.syntax, "swift, nocopy") + XCTAssertEqual(codeListing.options?.copyToClipboard, nil) + } + + func testShowLineNumbers() async throws { + enableFeatureFlag(\.isExperimentalCodeBlockAnnotationsEnabled) + + let (bundle, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + + let source = #""" + ```swift, showLineNumbers + let a = 1 + let b = 2 + let c = 3 + let d = 4 + let e = 5 + ``` + """# + let document = Document(parsing: source) + + let result = document.children.flatMap { compiler.visit($0) } + + let renderCodeBlock = try XCTUnwrap(result[0] as? RenderBlockContent) + guard case let .codeListing(codeListing) = renderCodeBlock else { + XCTFail("Expected RenderBlockContent.codeListing") + return + } + + XCTAssertEqual(codeListing.options?.showLineNumbers, true) + } + + func testLowercaseShowLineNumbers() async throws { + enableFeatureFlag(\.isExperimentalCodeBlockAnnotationsEnabled) + + let (bundle, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + + let source = #""" + ```swift, showlinenumbers + let a = 1 + let b = 2 + let c = 3 + let d = 4 + let e = 5 + ``` + """# + let document = Document(parsing: source) + + let result = document.children.flatMap { compiler.visit($0) } + + let renderCodeBlock = try XCTUnwrap(result[0] as? RenderBlockContent) + guard case let .codeListing(codeListing) = renderCodeBlock else { + XCTFail("Expected RenderBlockContent.codeListing") + return + } + + XCTAssertEqual(codeListing.options?.showLineNumbers, true) + } + + func testWrapAndHighlight() async throws { + enableFeatureFlag(\.isExperimentalCodeBlockAnnotationsEnabled) + + let (bundle, context) = try await testBundleAndContext() + var compiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + + let source = #""" + ```swift, wrap=20, highlight=[2] + let a = 1 + let b = 2 + let c = 3 + let d = 4 + let e = 5 + ``` + """# + + let document = Document(parsing: source) + + let result = document.children.flatMap { compiler.visit($0) } + + let renderCodeBlock = try XCTUnwrap(result[0] as? RenderBlockContent) + guard case let .codeListing(codeListing) = renderCodeBlock else { + XCTFail("Expected RenderBlockContent.codeListing") + return + } + + XCTAssertEqual(codeListing.syntax, "swift") + XCTAssertEqual(codeListing.options?.wrap, 20) + let line = Position(line: 2) + XCTAssertEqual(codeListing.options?.lineAnnotations, + [RenderBlockContent.CodeBlockOptions.LineAnnotation( + style: "highlight", + range: line.. DeclarationRenderSection { try XCTUnwrap( (renderNode.primaryContentSections.first as? DeclarationsRenderSection)?.declarations.first ) } - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.declarationVariants[.swift] = [ [.macOS]: SymbolGraph.Symbol.DeclarationFragments( @@ -401,7 +401,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { }, assertAfterApplyingVariant: { renderNode in let declarationSection = try declarationSection(in: renderNode) - XCTAssertEqual(declarationSection.platforms, [.iOS]) + XCTAssertEqual(Set(declarationSection.platforms), Set([.iOS, .iPadOS, .catalyst])) XCTAssertEqual( declarationSection.tokens, @@ -413,7 +413,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testReturnsSectionVariants() throws { + func testReturnsSectionVariants() async throws { func returnsSection(in renderNode: RenderNode) throws -> ContentRenderSection { let returnsSectionIndex = 1 @@ -425,7 +425,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { return try XCTUnwrap(renderNode.primaryContentSections[returnsSectionIndex] as? ContentRenderSection) } - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.returnsSectionVariants[.swift] = ReturnsSection( content: [Paragraph(Text("Swift Returns Section"))] @@ -458,7 +458,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testParametersSectionVariants() throws { + func testParametersSectionVariants() async throws { func parametersSection(in renderNode: RenderNode) throws -> ParametersRenderSection { let parametersSectionIndex = 1 @@ -470,7 +470,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { return try XCTUnwrap(renderNode.primaryContentSections[parametersSectionIndex] as? ParametersRenderSection) } - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.parametersSectionVariants[.swift] = ParametersSection( parameters: [Parameter(name: "Swift parameter", contents: [])] @@ -501,7 +501,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testDictionaryKeysSection() throws { + func testDictionaryKeysSection() async throws { let keySymbol = makeSymbol(id: "some-key", language: .data, kind: .dictionaryKey, pathComponents: ["SomeDictionary", "SomeKey"]) let catalog = Folder(name: "unit-test.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ @@ -510,7 +510,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ])) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let moduleReference = try XCTUnwrap(context.soleRootModuleReference) let dictionaryReference = moduleReference.appendingPath("SomeDictionary") @@ -521,7 +521,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { symbol.dictionaryKeysSection = dictionaryKeysSection context.documentationCache[dictionaryReference] = node - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(node) return try XCTUnwrap(renderNode.primaryContentSections.mapFirst(where: { $0 as? PropertiesRenderSection })) @@ -550,12 +550,12 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { } } - func testDiscussionSectionVariants() throws { + func testDiscussionSectionVariants() async throws { func discussionSection(in renderNode: RenderNode) throws -> ContentRenderSection { return try XCTUnwrap(renderNode.primaryContentSections.mapFirst { $0 as? ContentRenderSection }) } - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.discussionVariants[.swift] = DiscussionSection( content: [Paragraph(Text("Swift Discussion"))] @@ -590,7 +590,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testSourceFileURIVariants() throws { + func testSourceFileURIVariants() async throws { func makeLocation(uri: String) throws -> SymbolGraph.Symbol.Location { let location = """ { @@ -605,7 +605,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { return try JSONDecoder().decode(SymbolGraph.Symbol.Location.self, from: location) } - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.locationVariants[.swift] = try makeLocation(uri: "Swift URI") symbol.locationVariants[.objectiveC] = try makeLocation(uri: "Objective-C URI") @@ -622,8 +622,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testSymbolAccessLevelVariants() throws { - try assertMultiVariantSymbol( + func testSymbolAccessLevelVariants() async throws { + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.accessLevelVariants[.swift] = "Swift access level" symbol.accessLevelVariants[.objectiveC] = "Objective-C access level" @@ -640,8 +640,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testRelationshipSectionsVariants() throws { - try assertMultiVariantSymbol( + func testRelationshipSectionsVariants() async throws { + try await assertMultiVariantSymbol( configureContext: { context, _ in // Set up an Objective-C title for MyProtocol. @@ -691,8 +691,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testDoesNotEmitObjectiveCRelationshipsForTopicThatOnlyHasSwiftRelationships() throws { - try assertMultiVariantSymbol( + func testDoesNotEmitObjectiveCRelationshipsForTopicThatOnlyHasSwiftRelationships() async throws { + try await assertMultiVariantSymbol( configureContext: { context, _ in // Set up an Objective-C title for MyProtocol. @@ -732,8 +732,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testTopicsSectionVariants() throws { - try assertMultiVariantSymbol( + func testTopicsSectionVariants() async throws { + try await assertMultiVariantSymbol( configureContext: { context, reference in try makeSymbolAvailableInSwiftAndObjectiveC( symbolPath: "/documentation/MyKit/MyProtocol", @@ -777,8 +777,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testEncodesNilTopicsSectionsForArticleVariantIfDefaultIsNonEmpty() throws { - try assertMultiVariantArticle( + func testEncodesNilTopicsSectionsForArticleVariantIfDefaultIsNonEmpty() async throws { + try await assertMultiVariantArticle( configureArticle: { article in article.automaticTaskGroups = [] article.topics = makeTopicsSection( @@ -814,8 +814,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testEncodesNilTopicsSectionsForSymbolVariantIfDefaultIsNonEmpty() throws { - try assertMultiVariantSymbol( + func testEncodesNilTopicsSectionsForSymbolVariantIfDefaultIsNonEmpty() async throws { + try await assertMultiVariantSymbol( assertOriginalRenderNode: { renderNode in XCTAssertEqual(renderNode.topicSections.count, 6) }, @@ -834,8 +834,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testTopicsSectionVariantsNoUserProvidedTopics() throws { - try assertMultiVariantSymbol( + func testTopicsSectionVariantsNoUserProvidedTopics() async throws { + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.automaticTaskGroupsVariants[.fallback] = [] symbol.topicsVariants[.fallback] = nil @@ -861,7 +861,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testDefaultImplementationsSectionsVariants() throws { + func testDefaultImplementationsSectionsVariants() async throws { func createDefaultImplementationsSection(path: String) -> DefaultImplementationsSection { DefaultImplementationsSection( targetFallbacks: [:], @@ -882,7 +882,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.defaultImplementationsVariants[.swift] = createDefaultImplementationsSection( path: "/documentation/MyKit/MyProtocol" @@ -923,7 +923,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testSeeAlsoSectionVariants() throws { + func testSeeAlsoSectionVariants() async throws { func makeSeeAlsoSection(destination: String) -> SeeAlsoSection { SeeAlsoSection(content: [ UnorderedList( @@ -932,7 +932,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ]) } - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureContext: { context, reference in try makeSymbolAvailableInSwiftAndObjectiveC( symbolPath: "/documentation/MyKit/MyProtocol", @@ -972,7 +972,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testDoesNotEmitObjectiveCSeeAlsoIfEmpty() throws { + func testDoesNotEmitObjectiveCSeeAlsoIfEmpty() async throws { func makeSeeAlsoSection(destination: String) -> SeeAlsoSection { SeeAlsoSection(content: [ UnorderedList( @@ -981,7 +981,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ]) } - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.seeAlsoVariants[.swift] = makeSeeAlsoSection( destination: "doc://org.swift.docc.example/documentation/MyKit/MyProtocol" @@ -996,8 +996,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - func testDeprecationSummaryVariants() throws { - try assertMultiVariantSymbol( + func testDeprecationSummaryVariants() async throws { + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.deprecatedSummaryVariants[.swift] = DeprecatedSection( text: "Swift Deprecation Variant" @@ -1047,8 +1047,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { /// The `DeprecatedInOneLanguageOnly` catalog defines a symbol `MyClass` which has availability /// annotations in Swift but not in Objective-C. This test verifies that the Swift render node for `MyClass` does /// indeed include availability information, but that the Objective-C one doesn't. - func testDoesNotInheritAvailabilityFromOtherLanguage() throws { - try assertMultiVariantSymbol( + func testDoesNotInheritAvailabilityFromOtherLanguage() async throws { + try await assertMultiVariantSymbol( bundleName: "DeprecatedInOneLanguageOnly", assertOriginalRenderNode: { renderNode in XCTAssert(renderNode.metadata.platforms?.isEmpty == false) @@ -1062,7 +1062,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { } /// Tests that deprecation summaries only show up on variants of pages that are actually deprecated. - func testIncludesDeprecationSummaryOnlyInDeprecatedVariantOfSymbol() throws { + func testIncludesDeprecationSummaryOnlyInDeprecatedVariantOfSymbol() async throws { let deprecatedOnOnePlatform = SymbolGraph.Symbol.Availability.AvailabilityItem( domain: .init(rawValue: SymbolGraph.Symbol.Availability.Domain.macOS), introducedVersion: .init(major: 15, minor: 0, patch: 0), @@ -1088,7 +1088,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) for deprecatedAvailability in [deprecatedOnOnePlatform, unconditionallyDeprecated] { - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureSymbol: { symbol in symbol.deprecatedSummaryVariants[.swift] = DeprecatedSection( text: "Deprecation summary" @@ -1113,7 +1113,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { } } - func testTopicRenderReferenceVariants() throws { + func testTopicRenderReferenceVariants() async throws { func myFunctionReference(in renderNode: RenderNode) throws -> TopicRenderReference { return try XCTUnwrap( renderNode.references[ @@ -1122,7 +1122,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } - try assertMultiVariantSymbol( + try await assertMultiVariantSymbol( configureContext: { context, _ in // Set up a symbol with variants. @@ -1166,11 +1166,11 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { assertOriginalRenderNode: (RenderNode) throws -> (), assertAfterApplyingVariant: (RenderNode) throws -> () = { _ in }, assertDataAfterApplyingVariant: (Data) throws -> () = { _ in } - ) throws { - let (_, bundle, context) = try testBundleAndContext(copying: bundleName) + ) async throws { + let (_, _, context) = try await testBundleAndContext(copying: bundleName) let identifier = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/MyKit/MyClass", sourceLanguage: .swift ) @@ -1187,7 +1187,6 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { try assertMultiLanguageSemantic( symbol, context: context, - bundle: bundle, identifier: identifier, configureRenderNodeTranslator: configureRenderNodeTranslator, assertOriginalRenderNode: assertOriginalRenderNode, @@ -1203,11 +1202,11 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { assertOriginalRenderNode: (RenderNode) throws -> (), assertAfterApplyingVariant: (RenderNode) throws -> () = { _ in }, assertDataAfterApplyingVariant: (Data) throws -> () = { _ in } - ) throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") + ) async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") let identifier = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/documentation/Test-Bundle/article", sourceLanguage: .swift ) @@ -1224,7 +1223,6 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { try assertMultiLanguageSemantic( article, context: context, - bundle: bundle, identifier: identifier, assertOriginalRenderNode: assertOriginalRenderNode, assertAfterApplyingVariant: assertAfterApplyingVariant, @@ -1235,14 +1233,13 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { private func assertMultiLanguageSemantic( _ semantic: Semantic, context: DocumentationContext, - bundle: DocumentationBundle, identifier: ResolvedTopicReference, configureRenderNodeTranslator: (inout RenderNodeTranslator) -> () = { _ in }, assertOriginalRenderNode: (RenderNode) throws -> (), assertAfterApplyingVariant: (RenderNode) throws -> (), assertDataAfterApplyingVariant: (Data) throws -> () = { _ in } ) throws { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: identifier) + var translator = RenderNodeTranslator(context: context, identifier: identifier) configureRenderNodeTranslator(&translator) diff --git a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift b/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift index 12b479f98b..be2a95ea56 100644 --- a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift @@ -16,12 +16,12 @@ import Markdown import SymbolKit class RenderNodeTranslatorTests: XCTestCase { - private func findDiscussion(forSymbolPath: String, configureBundle: ((URL) throws -> Void)? = nil) throws -> ContentRenderSection? { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", configureBundle: configureBundle) + private func findDiscussion(forSymbolPath: String, configureBundle: ((URL) throws -> Void)? = nil) async throws -> ContentRenderSection? { + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", configureBundle: configureBundle) let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: forSymbolPath, sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic as! Symbol) as! RenderNode guard let section = renderNode.primaryContentSections.last(where: { section -> Bool in @@ -33,8 +33,8 @@ class RenderNodeTranslatorTests: XCTestCase { return discussion } - private func findParagraph(withPrefix: String, forSymbolPath: String) throws -> [RenderInlineContent]? { - guard let discussion = try findDiscussion(forSymbolPath: forSymbolPath) else { + private func findParagraph(withPrefix: String, forSymbolPath: String) async throws -> [RenderInlineContent]? { + guard let discussion = try await findDiscussion(forSymbolPath: forSymbolPath) else { return nil } @@ -59,8 +59,8 @@ class RenderNodeTranslatorTests: XCTestCase { return paragraph } - func testResolvingSymbolLinks() throws { - guard let paragraph = try findParagraph(withPrefix: "Exercise links to symbols", forSymbolPath: "/documentation/MyKit/MyProtocol") else { + func testResolvingSymbolLinks() async throws { + guard let paragraph = try await findParagraph(withPrefix: "Exercise links to symbols", forSymbolPath: "/documentation/MyKit/MyProtocol") else { XCTFail("Failed to fetch test content") return } @@ -78,8 +78,8 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(references.count, 2) } - func testExternalSymbolLink() throws { - guard let paragraph = try findParagraph(withPrefix: "Exercise unresolved symbols", forSymbolPath: "/documentation/MyKit/MyProtocol") else { + func testExternalSymbolLink() async throws { + guard let paragraph = try await findParagraph(withPrefix: "Exercise unresolved symbols", forSymbolPath: "/documentation/MyKit/MyProtocol") else { XCTFail("Failed to fetch test content") return } @@ -97,8 +97,8 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(references.count, 1) } - func testOrderedAndUnorderedList() throws { - guard let discussion = try findDiscussion(forSymbolPath: "/documentation/MyKit/MyProtocol") else { + func testOrderedAndUnorderedList() async throws { + guard let discussion = try await findDiscussion(forSymbolPath: "/documentation/MyKit/MyProtocol") else { return } @@ -144,8 +144,8 @@ class RenderNodeTranslatorTests: XCTestCase { })) } - func testAutomaticOverviewAndDiscussionHeadings() throws { - guard let myFunctionDiscussion = try findDiscussion(forSymbolPath: "/documentation/MyKit/MyClass/myFunction()", configureBundle: { url in + func testAutomaticOverviewAndDiscussionHeadings() async throws { + guard let myFunctionDiscussion = try await findDiscussion(forSymbolPath: "/documentation/MyKit/MyClass/myFunction()", configureBundle: { url in let sidecarURL = url.appendingPathComponent("/documentation/myFunction.md") try """ # ``MyKit/MyClass/myFunction()`` @@ -164,7 +164,7 @@ class RenderNodeTranslatorTests: XCTestCase { ] ) - guard let myClassDiscussion = try findDiscussion(forSymbolPath: "/documentation/MyKit/MyClass", configureBundle: { url in + guard let myClassDiscussion = try await findDiscussion(forSymbolPath: "/documentation/MyKit/MyClass", configureBundle: { url in let sidecarURL = url.appendingPathComponent("/documentation/myclass.md") XCTAssert(FileManager.default.fileExists(atPath: sidecarURL.path), "Make sure that this overrides the existing file.") try """ @@ -222,8 +222,8 @@ class RenderNodeTranslatorTests: XCTestCase { } } - func testArticleRoles() throws { - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testArticleRoles() async throws { + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() // Verify article's role @@ -279,9 +279,9 @@ class RenderNodeTranslatorTests: XCTestCase { } // Verifies that links to sections include their container's abstract rdar://72110558 - func testSectionAbstracts() throws { + func testSectionAbstracts() async throws { // Create an article including a link to a tutorial section - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { url in + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], configureBundle: { url in try """ # Article Article abstract @@ -294,7 +294,7 @@ class RenderNodeTranslatorTests: XCTestCase { let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Test-Bundle/article", sourceLanguage: .swift) let node = try context.entity(with: reference) let article = try XCTUnwrap(node.semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderedNode = translator.visit(article) as! RenderNode // Verify that the render reference to a section includes the container symbol's abstract @@ -302,8 +302,8 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(renderReference.abstract.first?.plainText, "This is the tutorial abstract.") } - func testEmptyTaskGroupsNotRendered() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testEmptyTaskGroupsNotRendered() async throws { + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let source = """ @@ -346,7 +346,7 @@ class RenderNodeTranslatorTests: XCTestCase { let topicGraphNode = TopicGraph.Node(reference: reference, kind: .article, source: .file(url: URL(fileURLWithPath: "/path/to/article.md")), title: "My Article") context.topicGraph.addNode(topicGraphNode) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let node = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) XCTAssertEqual(node.topicSections.count, 2) @@ -364,8 +364,8 @@ class RenderNodeTranslatorTests: XCTestCase { } /// Tests the ordering of automatic groups for symbols - func testAutomaticTaskGroupsOrderingInSymbols() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + func testAutomaticTaskGroupsOrderingInSymbols() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in try """ # ``SideKit/SideClass`` SideClass abstract @@ -376,7 +376,7 @@ class RenderNodeTranslatorTests: XCTestCase { }) let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let node = try XCTUnwrap(try? context.entity(with: reference)) // Test manual task groups and automatic symbol groups ordering @@ -491,8 +491,8 @@ class RenderNodeTranslatorTests: XCTestCase { } /// Tests the ordering of automatic groups for articles - func testAutomaticTaskGroupsOrderingInArticles() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + func testAutomaticTaskGroupsOrderingInArticles() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in try """ # Article Article abstract @@ -503,7 +503,7 @@ class RenderNodeTranslatorTests: XCTestCase { }) let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Test-Bundle/article", sourceLanguage: .swift) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let node = try XCTUnwrap(try? context.entity(with: reference)) // Test the manual curation task groups @@ -598,15 +598,15 @@ class RenderNodeTranslatorTests: XCTestCase { } /// Tests the ordering of automatic groups in defining protocol - func testOrderingOfAutomaticGroupsInDefiningProtocol() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + func testOrderingOfAutomaticGroupsInDefiningProtocol() async throws { + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in // }) // Verify "Default Implementations" group on the implementing type do { let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/Element", sourceLanguage: .swift) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let node = try XCTUnwrap(try? context.entity(with: reference)) let symbol = try XCTUnwrap(node.semantic as? Symbol) @@ -625,7 +625,7 @@ class RenderNodeTranslatorTests: XCTestCase { // Verify automatically generated api collection do { let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass/Element/Protocol-Implementations", sourceLanguage: .swift) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let node = try XCTUnwrap(try? context.entity(with: reference)) let article = try XCTUnwrap(node.semantic as? Article) @@ -644,17 +644,17 @@ class RenderNodeTranslatorTests: XCTestCase { } /// Verify that symbols with ellipsis operators don't get curated into an unnamed protocol implementation section. - func testAutomaticImplementationsWithExtraDots() throws { + func testAutomaticImplementationsWithExtraDots() async throws { let fancyProtocolSGFURL = Bundle.module.url( forResource: "FancyProtocol.symbols", withExtension: "json", subdirectory: "Test Resources")! // Create a test bundle copy with the symbol graph from above - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { url in + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: []) { url in try? FileManager.default.copyItem(at: fancyProtocolSGFURL, to: url.appendingPathComponent("FancyProtocol.symbols.json")) } let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/FancyProtocol/SomeClass", sourceLanguage: .swift) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let node = try context.entity(with: reference) let symbol = try XCTUnwrap(node.semantic as? Symbol) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) @@ -674,7 +674,7 @@ class RenderNodeTranslatorTests: XCTestCase { } - func testAutomaticImplementationsWithExtraDotsFromExternalModule() throws { + func testAutomaticImplementationsWithExtraDotsFromExternalModule() async throws { let inheritedDefaultImplementationsFromExternalModuleSGF = Bundle.module.url( forResource: "InheritedDefaultImplementationsFromExternalModule.symbols", withExtension: "json", @@ -689,21 +689,21 @@ class RenderNodeTranslatorTests: XCTestCase { ] ).write(inside: createTemporaryDirectory()) - try assertDefaultImplementationCollectionTitles( + try await assertDefaultImplementationCollectionTitles( in: try loadRenderNode(at: "/documentation/SecondTarget/FancyProtocolConformer", in: testBundle), [ "FancyProtocol Implementations", ] ) - try assertDefaultImplementationCollectionTitles( + try await assertDefaultImplementationCollectionTitles( in: try loadRenderNode(at: "/documentation/SecondTarget/OtherFancyProtocolConformer", in: testBundle), [ "OtherFancyProtocol Implementations", ] ) - try assertDefaultImplementationCollectionTitles( + try await assertDefaultImplementationCollectionTitles( in: try loadRenderNode(at: "/documentation/SecondTarget/FooConformer", in: testBundle), [ "Foo Implementations", @@ -711,7 +711,7 @@ class RenderNodeTranslatorTests: XCTestCase { ) } - func testAutomaticImplementationsFromCurrentModuleWithMixOfDocCoverage() throws { + func testAutomaticImplementationsFromCurrentModuleWithMixOfDocCoverage() async throws { let inheritedDefaultImplementationsSGF = Bundle.module.url( forResource: "InheritedDefaultImplementations.symbols", withExtension: "json", @@ -732,14 +732,14 @@ class RenderNodeTranslatorTests: XCTestCase { ] ).write(inside: createTemporaryDirectory()) - try assertDefaultImplementationCollectionTitles( + try await assertDefaultImplementationCollectionTitles( in: try loadRenderNode(at: "/documentation/FirstTarget/Bar", in: testBundle), [ "Foo Implementations", ] ) - try assertDefaultImplementationCollectionTitles( + try await assertDefaultImplementationCollectionTitles( in: try loadRenderNode(at: "/documentation/FirstTarget/OtherStruct", in: testBundle), [ "Comparable Implementations", @@ -747,7 +747,7 @@ class RenderNodeTranslatorTests: XCTestCase { ] ) - try assertDefaultImplementationCollectionTitles( + try await assertDefaultImplementationCollectionTitles( in: try loadRenderNode(at: "/documentation/FirstTarget/SomeStruct", in: testBundle), [ "Comparable Implementations", @@ -758,7 +758,7 @@ class RenderNodeTranslatorTests: XCTestCase { ) } - func testAutomaticImplementationsFromMultiPlatformSymbolGraphs() throws { + func testAutomaticImplementationsFromMultiPlatformSymbolGraphs() async throws { let inheritedDefaultImplementationsSGF = Bundle.module.url( forResource: "InheritedDefaultImplementations.symbols", withExtension: "json", @@ -807,14 +807,14 @@ class RenderNodeTranslatorTests: XCTestCase { ] ).write(inside: createTemporaryDirectory()) - try assertDefaultImplementationCollectionTitles( + try await assertDefaultImplementationCollectionTitles( in: try loadRenderNode(at: "/documentation/FirstTarget/Bar", in: testBundle), [ "Foo Implementations", ] ) - try assertDefaultImplementationCollectionTitles( + try await assertDefaultImplementationCollectionTitles( in: try loadRenderNode(at: "/documentation/FirstTarget/OtherStruct", in: testBundle), [ "Comparable Implementations", @@ -822,7 +822,7 @@ class RenderNodeTranslatorTests: XCTestCase { ] ) - try assertDefaultImplementationCollectionTitles( + try await assertDefaultImplementationCollectionTitles( in: try loadRenderNode(at: "/documentation/FirstTarget/SomeStruct", in: testBundle), [ "Comparable Implementations", @@ -853,25 +853,25 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(references.map(\.title), expectedTitles, file: file, line: line) } - func loadRenderNode(at path: String, in bundleURL: URL) throws -> RenderNode { - let (_, bundle, context) = try loadBundle(from: bundleURL) + func loadRenderNode(at path: String, in bundleURL: URL) async throws -> RenderNode { + let (_, bundle, context) = try await loadBundle(from: bundleURL) let reference = ResolvedTopicReference(bundleID: bundle.id, path: path, sourceLanguage: .swift) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let node = try context.entity(with: reference) let symbol = try XCTUnwrap(node.semantic as? Symbol) return try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) } - func testAutomaticTaskGroupTopicsAreSorted() throws { - let (bundle, context) = try testBundleAndContext(named: "DefaultImplementations") + func testAutomaticTaskGroupTopicsAreSorted() async throws { + let (bundle, context) = try await testBundleAndContext(named: "DefaultImplementations") let structReference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/DefaultImplementations/Foo", sourceLanguage: .swift) let structNode = try context.entity(with: structReference) let symbol = try XCTUnwrap(structNode.semantic as? Symbol) // Verify that the ordering of default implementations is deterministic for _ in 0..<100 { - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: structReference) + var translator = RenderNodeTranslator(context: context, identifier: structReference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let section = renderNode.topicSections.first(where: { $0.title == "Default Implementations" }) XCTAssertEqual(section?.identifiers, [ @@ -883,9 +883,9 @@ class RenderNodeTranslatorTests: XCTestCase { } // Verifies we don't render links to non linkable nodes. - func testNonLinkableNodes() throws { + func testNonLinkableNodes() async throws { // Create a bundle with variety absolute and relative links and symbol links to a non linkable node. - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in try """ # ``SideKit/SideClass`` Abstract. @@ -900,7 +900,7 @@ class RenderNodeTranslatorTests: XCTestCase { }) let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit/SideClass", sourceLanguage: .swift) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let node = try XCTUnwrap(try? context.entity(with: reference)) let symbol = try XCTUnwrap(node.semantic as? Symbol) @@ -922,15 +922,15 @@ class RenderNodeTranslatorTests: XCTestCase { } // Verifies we support rendering links in abstracts. - func testLinkInAbstract() throws { + func testLinkInAbstract() async throws { do { // First verify that `SideKit` page does not contain render reference to `SideKit/SideClass/Element`. - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit", sourceLanguage: .swift) let node = try context.entity(with: reference) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let symbol = try XCTUnwrap(node.semantic as? Symbol) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) @@ -940,7 +940,7 @@ class RenderNodeTranslatorTests: XCTestCase { do { // Create a bundle with a link in abstract, then verify the render reference is present in `SideKit` render node references. - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in + let (_, bundle, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests", excludingPaths: [], externalResolvers: [:], externalSymbolResolver: nil, configureBundle: { url in try """ # ``SideKit/SideClass`` This is a link to . @@ -950,7 +950,7 @@ class RenderNodeTranslatorTests: XCTestCase { let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/SideKit", sourceLanguage: .swift) let node = try context.entity(with: reference) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let symbol = try XCTUnwrap(node.semantic as? Symbol) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) @@ -959,11 +959,11 @@ class RenderNodeTranslatorTests: XCTestCase { } } - func testSnippetToCodeListing() throws { - let (bundle, context) = try testBundleAndContext(named: "Snippets") + func testSnippetToCodeListing() async throws { + let (bundle, context) = try await testBundleAndContext(named: "Snippets") let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Snippets/Snippets", sourceLanguage: .swift) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let discussion = try XCTUnwrap(renderNode.primaryContentSections.first(where: { $0.kind == .content }) as? ContentRenderSection) @@ -989,11 +989,11 @@ class RenderNodeTranslatorTests: XCTestCase { } } - func testSnippetSliceToCodeListing() throws { - let (bundle, context) = try testBundleAndContext(named: "Snippets") + func testSnippetSliceToCodeListing() async throws { + let (bundle, context) = try await testBundleAndContext(named: "Snippets") let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Snippets/Snippets", sourceLanguage: .swift) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let discussion = try XCTUnwrap(renderNode.primaryContentSections.first(where: { $0.kind == .content }) as? ContentRenderSection) @@ -1013,11 +1013,11 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(l.code, ["func foo() {}"]) } - func testNestedSnippetSliceToCodeListing() throws { - let (bundle, context) = try testBundleAndContext(named: "Snippets") + func testNestedSnippetSliceToCodeListing() async throws { + let (bundle, context) = try await testBundleAndContext(named: "Snippets") let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Snippets/Snippets", sourceLanguage: .swift) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let discussion = try XCTUnwrap(renderNode.primaryContentSections.first(where: { $0.kind == .content }) as? ContentRenderSection) @@ -1044,11 +1044,11 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(l.code, ["middle()"]) } - func testSnippetSliceTrimsIndentation() throws { - let (bundle, context) = try testBundleAndContext(named: "Snippets") + func testSnippetSliceTrimsIndentation() async throws { + let (bundle, context) = try await testBundleAndContext(named: "Snippets") let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/Snippets/SliceIndentation", sourceLanguage: .swift) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let discussion = try XCTUnwrap(renderNode.primaryContentSections.first(where: { $0.kind == .content }) as? ContentRenderSection) @@ -1069,15 +1069,15 @@ class RenderNodeTranslatorTests: XCTestCase { } - func testRowAndColumn() throws { - let (bundle, context) = try testBundleAndContext(named: "BookLikeContent") + func testRowAndColumn() async throws { + let (bundle, context) = try await testBundleAndContext(named: "BookLikeContent") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/BestBook/MyArticle", sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let discussion = try XCTUnwrap( @@ -1098,15 +1098,15 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(row.columns.last?.content.count, 3) } - func testSmall() throws { - let (bundle, context) = try testBundleAndContext(named: "BookLikeContent") + func testSmall() async throws { + let (bundle, context) = try await testBundleAndContext(named: "BookLikeContent") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/BestBook/MyArticle", sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let discussion = try XCTUnwrap( @@ -1126,15 +1126,15 @@ class RenderNodeTranslatorTests: XCTestCase { ) } - func testTabNavigator() throws { - let (bundle, context) = try testBundleAndContext(named: "BookLikeContent") + func testTabNavigator() async throws { + let (bundle, context) = try await testBundleAndContext(named: "BookLikeContent") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/BestBook/TabNavigatorArticle", sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let discussion = try XCTUnwrap( @@ -1163,15 +1163,15 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(tabNavigator.tabs[2].content.count, 1) } - func testRenderNodeMetadata() throws { - let (bundle, context) = try testBundleAndContext(named: "BookLikeContent") + func testRenderNodeMetadata() async throws { + let (bundle, context) = try await testBundleAndContext(named: "BookLikeContent") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/BestBook/MyArticle", sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode) let encodedArticle = try JSONEncoder().encode(renderNode) @@ -1239,15 +1239,15 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(roundTrippedArticle.metadata.role, "article") } - func testPageColorMetadataInSymbolExtension() throws { - let (bundle, context) = try testBundleAndContext(named: "MixedManualAutomaticCuration") + func testPageColorMetadataInSymbolExtension() async throws { + let (bundle, context) = try await testBundleAndContext(named: "MixedManualAutomaticCuration") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/TestBed", sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let encodedSymbol = try JSONEncoder().encode(renderNode) @@ -1255,15 +1255,15 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(roundTrippedSymbol.metadata.color?.standardColorIdentifier, "purple") } - func testTitleHeadingMetadataInSymbolExtension() throws { - let (bundle, context) = try testBundleAndContext(named: "MixedManualAutomaticCuration") + func testTitleHeadingMetadataInSymbolExtension() async throws { + let (bundle, context) = try await testBundleAndContext(named: "MixedManualAutomaticCuration") let reference = ResolvedTopicReference( bundleID: bundle.id, path: "/documentation/TestBed", sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) let encodedSymbol = try JSONEncoder().encode(renderNode) @@ -1272,7 +1272,7 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(roundTrippedSymbol.metadata.role, "collection") } - func testExpectedRoleHeadingIsAssigned() throws { + func testExpectedRoleHeadingIsAssigned() async throws { let catalog = Folder( name: "unit-test.docc", content: [ @@ -1334,35 +1334,26 @@ class RenderNodeTranslatorTests: XCTestCase { ), ] ) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) - func renderNodeArticleFromReferencePath( - referencePath: String - ) throws -> RenderNode { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: referencePath, sourceLanguage: .swift) - let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - return try XCTUnwrap(translator.visitArticle(symbol) as? RenderNode) - } - // Assert that articles that curates any symbol gets 'API Collection' assigned as the eyebrow title. - var renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/APICollection") + var renderNode = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/unit-test/APICollection") XCTAssertEqual(renderNode.metadata.roleHeading, "API Collection") // Assert that articles that curates only other articles don't get any value assigned as the eyebrow title. - renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Collection") + renderNode = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/unit-test/Collection") XCTAssertEqual(renderNode.metadata.roleHeading, nil) // Assert that articles that don't curate anything else get 'Article' assigned as the eyebrow title. - renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Article") + renderNode = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/unit-test/Article") XCTAssertEqual(renderNode.metadata.roleHeading, "Article") // Assert that articles that have a custom title heading the eyebrow title assigned properly. - renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/CustomRole") + renderNode = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/unit-test/CustomRole") XCTAssertEqual(renderNode.metadata.roleHeading, "Custom Role") // Assert that articles that have a custom page kind the eyebrow title assigned properly. - renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/SampleCode") + renderNode = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/unit-test/SampleCode") XCTAssertEqual(renderNode.metadata.roleHeading, "Sample Code") } - func testExpectedRoleHeadingWhenAutomaticRoleHeadingIsDisabled() throws { + func testExpectedRoleHeadingWhenAutomaticRoleHeadingIsDisabled() async throws { let catalog = Folder( name: "unit-test.docc", content: [ @@ -1426,42 +1417,33 @@ class RenderNodeTranslatorTests: XCTestCase { ), ] ) - let (bundle, context) = try loadBundle(catalog: catalog) - - func renderNodeArticleFromReferencePath( - referencePath: String - ) throws -> RenderNode { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: referencePath, sourceLanguage: .swift) - let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) - return try XCTUnwrap(translator.visitArticle(symbol) as? RenderNode) - } + let (_, context) = try await loadBundle(catalog: catalog) // Assert that API collections disabling automatic title headings don't get any value assigned as the eyebrow title, // but that the node's role itself is unaffected. - var renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/APICollection") + var renderNode = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/unit-test/APICollection") XCTAssertEqual(renderNode.metadata.roleHeading, nil) XCTAssertEqual(renderNode.metadata.role, RenderMetadata.Role.collectionGroup.rawValue) // Assert that articles disabling automatic title headings don't get any value assigned as the eyebrow title, // but that the node's role itself is unaffected. - renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Article") + renderNode = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/unit-test/Article") XCTAssertEqual(renderNode.metadata.roleHeading, nil) XCTAssertEqual(renderNode.metadata.role, RenderMetadata.Role.article.rawValue) // Assert that articles that have a custom title heading have the eyebrow title assigned properly, // even when automatic title headings are disabled. - renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/CustomRole") + renderNode = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/unit-test/CustomRole") XCTAssertEqual(renderNode.metadata.roleHeading, "Custom Role") XCTAssertEqual(renderNode.metadata.role, RenderMetadata.Role.article.rawValue) // Assert that articles that have a custom page kind have the eyebrow title assigned properly, // even when automatic title headings are disabled. - renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/SampleCode") + renderNode = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/unit-test/SampleCode") XCTAssertEqual(renderNode.metadata.roleHeading, "Sample Code") } - func testEncodesOverloadsInRenderNode() throws { + func testEncodesOverloadsInRenderNode() async throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) - let (bundle, context) = try testBundleAndContext(named: "OverloadedSymbols") + let (_, context) = try await testBundleAndContext(named: "OverloadedSymbols") let overloadPreciseIdentifiers = ["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSiF", "s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSfF", @@ -1474,7 +1456,7 @@ class RenderNodeTranslatorTests: XCTestCase { for (index, reference) in overloadReferences.indexed() { let documentationNode = try context.entity(with: reference) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let symbol = try XCTUnwrap(documentationNode.semantic as? Symbol) let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) @@ -1496,8 +1478,8 @@ class RenderNodeTranslatorTests: XCTestCase { } } - func testAlternateRepresentationsRenderedAsVariants() throws { - let (bundle, context) = try loadBundle(catalog: Folder( + func testAlternateRepresentationsRenderedAsVariants() async throws { + let (_, context) = try await loadBundle(catalog: Folder( name: "unit-test.docc", content: [ TextFile(name: "Symbol.md", utf8Content: """ @@ -1544,17 +1526,17 @@ class RenderNodeTranslatorTests: XCTestCase { ] )) - func renderNodeArticleFromReferencePath( + func renderNodeSymbolFromReferencePath( referencePath: String ) throws -> RenderNode { - let reference = ResolvedTopicReference(bundleID: bundle.id, path: referencePath, sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: referencePath, sourceLanguage: .swift) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) return try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) } // Assert that CounterpartSymbol's source languages have been added as source languages of Symbol - var renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Symbol") + var renderNode = try renderNodeSymbolFromReferencePath(referencePath: "/documentation/unit-test/Symbol") XCTAssertEqual(renderNode.variants?.count, 2) XCTAssertEqual(renderNode.variants, [ .init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unit-test/symbol"]), @@ -1562,17 +1544,61 @@ class RenderNodeTranslatorTests: XCTestCase { ]) // Assert that alternate representations which can't be resolved are ignored - renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/OtherSymbol") + renderNode = try renderNodeSymbolFromReferencePath(referencePath: "/documentation/unit-test/OtherSymbol") XCTAssertEqual(renderNode.variants?.count, 1) XCTAssertEqual(renderNode.variants, [ .init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unit-test/othersymbol"]), ]) // Assert that duplicate alternate representations are not added as variants - renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/MultipleSwiftVariantsSymbol") + renderNode = try renderNodeSymbolFromReferencePath(referencePath: "/documentation/unit-test/MultipleSwiftVariantsSymbol") XCTAssertEqual(renderNode.variants?.count, 1) XCTAssertEqual(renderNode.variants, [ .init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unit-test/multipleswiftvariantssymbol"]), ]) } + + // Tests if variants are emitted in catalogs with more than one root module. + func testEmitVariantsInCatalogWithMultipleModules() async throws { + let (_, context) = try await loadBundle(catalog: Folder( + name: "UnitTest.docc", + content: [ + TextFile(name: "UnitTest.md", utf8Content: """ + # Unit test + + @Metadata { + @TechnologyRoot + @SupportedLanguage(swift) + @SupportedLanguage(occ) + } + + This is an article in a catalog containing a module different from the article-only collection. + """), + // The correct way to configure a catalog is to have a single + // root module. If multiple modules are present, it is not + // possible to determine which module an article is supposed to + // be registered with. This test includes another module to + // verify if the variants are correctly emitted when there is + // no sole root module. + JSONFile(name: "foo.symbols.json", content: makeSymbolGraph(moduleName: "foo")), + ] + )) + + let article = try renderNodeArticleFromReferencePath(context: context, referencePath: "/documentation/UnitTest") + XCTAssertEqual(article.variants?.count, 2) + XCTAssertEqual(article.variants, [ + .init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unittest"]), + .init(traits: [.interfaceLanguage("occ")], paths: ["/documentation/unittest"]) + ]) + } + + private func renderNodeArticleFromReferencePath( + context: DocumentationContext, + referencePath: String + ) throws -> RenderNode { + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: referencePath, sourceLanguage: .swift) + let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) + var translator = RenderNodeTranslator(context: context, identifier: reference) + return try XCTUnwrap(translator.visitArticle(article) as? RenderNode) + } } diff --git a/Tests/SwiftDocCTests/Rendering/RoleTests.swift b/Tests/SwiftDocCTests/Rendering/RoleTests.swift index 2bea97e3de..c944b9cb34 100644 --- a/Tests/SwiftDocCTests/Rendering/RoleTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RoleTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -24,15 +24,15 @@ class RoleTests: XCTestCase { "/documentation/SideKit/SideClass/init()": "symbol", ] - func testNodeRoles() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") + func testNodeRoles() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") // Compile docs and verify contents for (path, expectedRole) in expectedRoles { let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: path, fragment: nil, sourceLanguage: .swift) do { let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertEqual(expectedRole, renderNode.metadata.role, "Unexpected role \(renderNode.metadata.role!.singleQuoted) for identifier \(identifier.path)") } catch { @@ -42,12 +42,12 @@ class RoleTests: XCTestCase { } } - func testDocumentationRenderReferenceRoles() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") + func testDocumentationRenderReferenceRoles() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit", fragment: nil, sourceLanguage: .swift) let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/MyKit"] as? TopicRenderReference)?.role, "collection") @@ -55,12 +55,12 @@ class RoleTests: XCTestCase { XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/documentation/Test-Bundle/article2"] as? TopicRenderReference)?.role, "collectionGroup") } - func testTutorialsRenderReferenceRoles() throws { - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") + func testTutorialsRenderReferenceRoles() async throws { + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") let identifier = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/tutorials/Test-Bundle/TestTutorial", fragment: nil, sourceLanguage: .swift) let node = try context.entity(with: identifier) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = translator.visit(node.semantic) as! RenderNode XCTAssertEqual((renderNode.references["doc://org.swift.docc.example/tutorials/TestOverview"] as? TopicRenderReference)?.role, "overview") diff --git a/Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift b/Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift index d139af629b..62089827e0 100644 --- a/Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift +++ b/Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -74,8 +74,8 @@ class SampleDownloadTests: XCTestCase { XCTAssertEqual(text, "You can experiment with the code. Just use WiFi Access on your Mac to download WiFi access sample code.") } - func testParseSampleDownload() throws { - let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MySample") + func testParseSampleDownload() async throws { + let renderNode = try await renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MySample") let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload) guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else { @@ -85,8 +85,8 @@ class SampleDownloadTests: XCTestCase { XCTAssertEqual(ident.identifier, "https://example.com/sample.zip") } - func testParseSampleLocalDownload() throws { - let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MyLocalSample") + func testParseSampleLocalDownload() async throws { + let renderNode = try await renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MyLocalSample") let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload) guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else { @@ -96,8 +96,8 @@ class SampleDownloadTests: XCTestCase { XCTAssertEqual(ident.identifier, "plus.svg") } - func testSampleDownloadRoundtrip() throws { - let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MySample") + func testSampleDownloadRoundtrip() async throws { + let renderNode = try await renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MySample") let encoder = JSONEncoder() let decoder = JSONDecoder() @@ -125,20 +125,20 @@ class SampleDownloadTests: XCTestCase { XCTAssertEqual(origIdent, decodedIdent) } - private func renderNodeFromSampleBundle(at referencePath: String) throws -> RenderNode { - let (bundle, context) = try testBundleAndContext(named: "SampleBundle") + private func renderNodeFromSampleBundle(at referencePath: String) async throws -> RenderNode { + let (_, context) = try await testBundleAndContext(named: "SampleBundle") let reference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: referencePath, sourceLanguage: .swift ) let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) return try XCTUnwrap(translator.visitArticle(article) as? RenderNode) } - func testSampleDownloadRelativeURL() throws { - let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/RelativeURLSample") + func testSampleDownloadRelativeURL() async throws { + let renderNode = try await renderNodeFromSampleBundle(at: "/documentation/SampleBundle/RelativeURLSample") let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload) guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else { XCTFail("Unexpected action in callToAction") @@ -152,8 +152,8 @@ class SampleDownloadTests: XCTestCase { XCTAssertEqual(downloadReference.url.description, "files/ExternalSample.zip") } - func testExternalLocationRoundtrip() throws { - let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/RelativeURLSample") + func testExternalLocationRoundtrip() async throws { + let renderNode = try await renderNodeFromSampleBundle(at: "/documentation/SampleBundle/RelativeURLSample") let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload) guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else { XCTFail("Unexpected action in callToAction") @@ -178,8 +178,8 @@ class SampleDownloadTests: XCTestCase { XCTAssertEqual(firstJson, finalJson) } - func testExternalLinkOnSampleCodePage() throws { - let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MyExternalSample") + func testExternalLinkOnSampleCodePage() async throws { + let renderNode = try await renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MyExternalSample") let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload) guard case .reference(identifier: let identifier, isActive: true, overridingTitle: "View Source", overridingTitleInlineContent: nil) = sampleCodeDownload.action else { XCTFail("Unexpected action in callToAction") @@ -191,8 +191,8 @@ class SampleDownloadTests: XCTestCase { XCTAssertEqual(reference.url.description, "https://www.example.com/source-repository.git") } - func testExternalLinkOnRegularArticlePage() throws { - let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MyArticle") + func testExternalLinkOnRegularArticlePage() async throws { + let renderNode = try await renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MyArticle") let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload) guard case .reference(identifier: let identifier, isActive: true, overridingTitle: "Visit", overridingTitleInlineContent: nil) = sampleCodeDownload.action else { XCTFail("Unexpected action in callToAction") @@ -265,10 +265,10 @@ class SampleDownloadTests: XCTestCase { XCTAssertEqual(decodedReference.url, newURL) } - func testProjectFilesForCallToActionDirectives() throws { + func testProjectFilesForCallToActionDirectives() async throws { // Make sure that the `projectFiles()` method correctly returns the DownloadReference // created by the `@CallToAction` directive. - let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MySample") + let renderNode = try await renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MySample") let downloadReference = try XCTUnwrap(renderNode.projectFiles()) XCTAssertEqual(downloadReference.url.description, "https://example.com/sample.zip") } diff --git a/Tests/SwiftDocCTests/Rendering/SymbolAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/SymbolAvailabilityTests.swift index a0c0b129e9..c3ea5e5ab5 100644 --- a/Tests/SwiftDocCTests/Rendering/SymbolAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Rendering/SymbolAvailabilityTests.swift @@ -16,39 +16,13 @@ import SwiftDocCTestUtilities class SymbolAvailabilityTests: XCTestCase { - private func symbolAvailability( - defaultAvailability: [DefaultAvailability.ModuleAvailability] = [], - symbolGraphOperatingSystemPlatformName: String, - symbols: [SymbolGraph.Symbol], - symbolName: String - ) throws -> [SymbolGraph.Symbol.Availability.AvailabilityItem] { - let catalog = Folder( - name: "unit-test.docc", - content: [ - InfoPlist(defaultAvailability: [ - "ModuleName": defaultAvailability - ]), - JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( - moduleName: "ModuleName", - platform: SymbolGraph.Platform(architecture: nil, vendor: nil, operatingSystem: SymbolGraph.OperatingSystem(name: symbolGraphOperatingSystemPlatformName), environment: nil), - symbols: symbols, - relationships: [] - )), - ] - ) - let (_, context) = try loadBundle(catalog: catalog) - let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath(symbolName) - let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) - return try XCTUnwrap(symbol.availability?.availability) - } - private func renderNodeAvailability( defaultAvailability: [DefaultAvailability.ModuleAvailability] = [], symbolGraphOperatingSystemPlatformName: String, symbolGraphEnvironmentName: String? = nil, symbols: [SymbolGraph.Symbol], symbolName: String - ) throws -> [AvailabilityRenderItem] { + ) async throws -> [AvailabilityRenderItem] { let catalog = Folder( name: "unit-test.docc", content: [ @@ -63,16 +37,16 @@ class SymbolAvailabilityTests: XCTestCase { )), ] ) - let (bundle, context) = try loadBundle(catalog: catalog) + let (_, context) = try await loadBundle(catalog: catalog) let reference = try XCTUnwrap(context.soleRootModuleReference).appendingPath(symbolName) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: reference.path, sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: reference.path, sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) return try XCTUnwrap((translator.visit(node.semantic as! Symbol) as! RenderNode).metadata.platformsVariants.defaultValue) } - func testSymbolGraphSymbolWithoutDeprecatedVersionAndIntroducedVersion() throws { + func testSymbolGraphSymbolWithoutDeprecatedVersionAndIntroducedVersion() async throws { - var availability = try renderNodeAvailability( + var availability = try await renderNodeAvailability( defaultAvailability: [], symbolGraphOperatingSystemPlatformName: "ios", symbols: [ @@ -93,7 +67,7 @@ class SymbolAvailabilityTests: XCTestCase { "Mac Catalyst - 1.2.3", ]) - availability = try renderNodeAvailability( + availability = try await renderNodeAvailability( defaultAvailability: [ DefaultAvailability.ModuleAvailability(platformName: PlatformName(operatingSystemName: "iOS"), platformVersion: "1.2.3") ], @@ -122,9 +96,9 @@ class SymbolAvailabilityTests: XCTestCase { ]) } - func testSymbolGraphSymbolWithObsoleteVersion() throws { + func testSymbolGraphSymbolWithObsoleteVersion() async throws { - let availability = try renderNodeAvailability( + let availability = try await renderNodeAvailability( defaultAvailability: [], symbolGraphOperatingSystemPlatformName: "ios", symbols: [ diff --git a/Tests/SwiftDocCTests/Rendering/TermListTests.swift b/Tests/SwiftDocCTests/Rendering/TermListTests.swift index fb6340b207..d5e496456c 100644 --- a/Tests/SwiftDocCTests/Rendering/TermListTests.swift +++ b/Tests/SwiftDocCTests/Rendering/TermListTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -47,7 +47,7 @@ class TermListTests: XCTestCase { XCTAssertEqual(l.items.count, 4) } - func testLinksAndCodeVoiceAsTerms() throws { + func testLinksAndCodeVoiceAsTerms() async throws { let catalog = Folder(name: "unit-test.docc", content: [ TextFile(name: "Article.md", utf8Content: """ @@ -86,12 +86,12 @@ class TermListTests: XCTestCase { var configuration = DocumentationContext.Configuration() configuration.externalDocumentationConfiguration.sources = ["com.external.testbundle": resolver] - let (bundle, context) = try loadBundle(catalog: catalog, configuration: configuration) + let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration) - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/unit-test/Article", sourceLanguage: .swift) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/unit-test/Article", sourceLanguage: .swift) let entity = try context.entity(with: reference) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(entity) let overviewSection = try XCTUnwrap(renderNode.primaryContentSections.first as? ContentRenderSection) @@ -154,15 +154,15 @@ class TermListTests: XCTestCase { } } - func testRenderingListWithAllTermListItems() throws { + func testRenderingListWithAllTermListItems() async throws { let jsonFixtureItems = try discussionContents(fileName: "term-lists-2") guard jsonFixtureItems.count == 1 else { XCTFail("Discussion section didn't have expected number of contents") return } - let (bundle, context) = try testBundleAndContext() - var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + let (bundle, context) = try await testBundleAndContext() + var renderContentCompiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = """ - term First term : A paragraph that @@ -197,15 +197,15 @@ class TermListTests: XCTestCase { XCTAssertEqual(jsonFixtureItems, result) } - func testRenderingListWithInterleavedListItems() throws { + func testRenderingListWithInterleavedListItems() async throws { let jsonFixtureItems = try discussionContents(fileName: "term-lists-3") guard jsonFixtureItems.count == 4 else { XCTFail("Discussion section didn't have expected number of contents") return } - let (bundle, context) = try testBundleAndContext() - var renderContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) + let (bundle, context) = try await testBundleAndContext() + var renderContentCompiler = RenderContentCompiler(context: context, identifier: ResolvedTopicReference(bundleID: bundle.id, path: "/path", fragment: nil, sourceLanguage: .swift)) let source = """ - Not a term list, and diff --git a/Tests/SwiftDocCTests/Semantics/ArticleSymbolMentionsTests.swift b/Tests/SwiftDocCTests/Semantics/ArticleSymbolMentionsTests.swift index 14cd755e81..856a3908be 100644 --- a/Tests/SwiftDocCTests/Semantics/ArticleSymbolMentionsTests.swift +++ b/Tests/SwiftDocCTests/Semantics/ArticleSymbolMentionsTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -9,7 +9,7 @@ */ import XCTest -@testable import SwiftDocC +@testable @preconcurrency import SwiftDocC import Markdown import SwiftDocCTestUtilities import SymbolKit @@ -86,8 +86,8 @@ class ArticleSymbolMentionsTests: XCTestCase { } } - func testSymbolLinkCollectorEnabled() throws { - let (bundle, context) = try createMentionedInTestBundle() + func testSymbolLinkCollectorEnabled() async throws { + let (bundle, context) = try await createMentionedInTestBundle() // The test bundle currently only has one article with symbol mentions // in the abstract/discussion. @@ -108,7 +108,7 @@ class ArticleSymbolMentionsTests: XCTestCase { XCTAssertEqual(mentioningArticle, gottenArticle) } - func testSymbolLinkCollectorDisabled() throws { + func testSymbolLinkCollectorDisabled() async throws { let currentFeatureFlags = FeatureFlags.current addTeardownBlock { FeatureFlags.current = currentFeatureFlags @@ -116,7 +116,7 @@ class ArticleSymbolMentionsTests: XCTestCase { FeatureFlags.current.isMentionedInEnabled = false - let (bundle, context) = try createMentionedInTestBundle() + let (bundle, context) = try await createMentionedInTestBundle() XCTAssertTrue(context.articleSymbolMentions.mentions.isEmpty) let mentionedSymbol = ResolvedTopicReference( diff --git a/Tests/SwiftDocCTests/Semantics/ArticleTests.swift b/Tests/SwiftDocCTests/Semantics/ArticleTests.swift index 9c32d5d4a0..84c032f057 100644 --- a/Tests/SwiftDocCTests/Semantics/ArticleTests.swift +++ b/Tests/SwiftDocCTests/Semantics/ArticleTests.swift @@ -13,7 +13,7 @@ import XCTest import Markdown class ArticleTests: XCTestCase { - func testValid() throws { + func testValid() async throws { let source = """ # This is my article @@ -22,7 +22,7 @@ class ArticleTests: XCTestCase { Here's an overview. """ let document = Document(parsing: source, options: []) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article) @@ -33,7 +33,7 @@ class ArticleTests: XCTestCase { XCTAssertEqual((article?.discussion?.content ?? []).map { $0.detachedFromParent.format() }.joined(separator: "\n"), "Here’s an overview.") } - func testWithExplicitOverviewHeading() throws { + func testWithExplicitOverviewHeading() async throws { let source = """ # This is my article @@ -44,7 +44,7 @@ class ArticleTests: XCTestCase { Here's an overview. """ let document = Document(parsing: source, options: []) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article) @@ -62,7 +62,7 @@ class ArticleTests: XCTestCase { } } - func testWithExplicitCustomHeading() throws { + func testWithExplicitCustomHeading() async throws { let source = """ # This is my article @@ -73,7 +73,7 @@ class ArticleTests: XCTestCase { Here's an overview. """ let document = Document(parsing: source, options: []) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article) @@ -92,12 +92,12 @@ class ArticleTests: XCTestCase { } } - func testOnlyTitleArticle() throws { + func testOnlyTitleArticle() async throws { let source = """ # This is my article """ let document = Document(parsing: source, options: []) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article) @@ -108,14 +108,14 @@ class ArticleTests: XCTestCase { XCTAssertNil(article?.discussion) } - func testNoAbstract() throws { + func testNoAbstract() async throws { let source = """ # This is my article - This is not an abstract. """ let document = Document(parsing: source, options: []) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article) @@ -126,14 +126,14 @@ class ArticleTests: XCTestCase { XCTAssertEqual((article?.discussion?.content ?? []).map { $0.detachedFromParent.format() }.joined(separator: "\n"), "- This is not an abstract.") } - func testSolutionForTitleMissingIndentation() throws { + func testSolutionForTitleMissingIndentation() async throws { let source = """ My article This is my article """ let document = Document(parsing: source, options: []) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) @@ -148,12 +148,12 @@ class ArticleTests: XCTestCase { XCTAssertEqual(replacement.replacement, "# My article") } - func testSolutionForEmptyArticle() throws { + func testSolutionForEmptyArticle() async throws { let source = """ """ let document = Document(parsing: source, options: []) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) @@ -168,7 +168,7 @@ class ArticleTests: XCTestCase { XCTAssertEqual(replacement.replacement, "# <#Title#>") } - func testArticleWithDuplicateOptions() throws { + func testArticleWithDuplicateOptions() async throws { let source = """ # Article @@ -185,7 +185,7 @@ class ArticleTests: XCTestCase { Here's an overview. """ let document = Document(parsing: source, options: [.parseBlockDirectives]) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article) @@ -209,7 +209,7 @@ class ArticleTests: XCTestCase { XCTAssertEqual(article?.options[.local]?.automaticSeeAlsoEnabled, false) } - func testDisplayNameDirectiveIsRemoved() throws { + func testDisplayNameDirectiveIsRemoved() async throws { let source = """ # Root @@ -222,7 +222,7 @@ class ArticleTests: XCTestCase { Adding @DisplayName to an article will result in a warning. """ let document = Document(parsing: source, options: [.parseBlockDirectives]) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) @@ -249,4 +249,26 @@ class ArticleTests: XCTestCase { XCTAssertNil(semantic.metadata?.pageKind) XCTAssertNil(semantic.metadata?.titleHeading) } + + func testSupportedLanguageDirective() async throws { + let source = """ + # Root + + @Metadata { + @SupportedLanguage(swift) + @SupportedLanguage(objc) + @SupportedLanguage(data) + } + """ + let document = Document(parsing: source, options: [.parseBlockDirectives]) + let (bundle, _) = try await testBundleAndContext() + var problems = [Problem]() + let article = Article(from: document, source: nil, for: bundle, problems: &problems) + + XCTAssert(problems.isEmpty, "Unexpectedly found problems: \(DiagnosticConsoleWriter.formattedDescription(for: problems))") + + XCTAssertNotNil(article) + XCTAssertNotNil(article?.metadata, "Article should have a metadata container since the markup has a @Metadata directive") + XCTAssertEqual(article?.metadata?.supportedLanguages.map(\.language), [.swift, .objectiveC, .data]) + } } diff --git a/Tests/SwiftDocCTests/Semantics/AssessmentsTests.swift b/Tests/SwiftDocCTests/Semantics/AssessmentsTests.swift index 52194e04fc..0d7e254769 100644 --- a/Tests/SwiftDocCTests/Semantics/AssessmentsTests.swift +++ b/Tests/SwiftDocCTests/Semantics/AssessmentsTests.swift @@ -13,13 +13,13 @@ import XCTest import Markdown class AssessmentsTests: XCTestCase { - func testEmptyAndLonely() throws { + func testEmptyAndLonely() async throws { let source = "@Assessments" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() diff --git a/Tests/SwiftDocCTests/Semantics/CallToActionTests.swift b/Tests/SwiftDocCTests/Semantics/CallToActionTests.swift index e44c14ac82..3c59b82ba7 100644 --- a/Tests/SwiftDocCTests/Semantics/CallToActionTests.swift +++ b/Tests/SwiftDocCTests/Semantics/CallToActionTests.swift @@ -15,13 +15,13 @@ import Markdown @testable import SwiftDocC class CallToActionTests: XCTestCase { - func testInvalidWithNoArguments() throws { + func testInvalidWithNoArguments() async throws { let source = "@CallToAction" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "SampleBundle") + let (bundle, _) = try await testBundleAndContext(named: "SampleBundle") directive.map { directive in var problems = [Problem]() @@ -35,13 +35,13 @@ class CallToActionTests: XCTestCase { } } - func testInvalidWithoutLink() throws { - func assertMissingLink(source: String) throws { + func testInvalidWithoutLink() async throws { + func assertMissingLink(source: String) async throws { let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "SampleBundle") + let (bundle, _) = try await testBundleAndContext(named: "SampleBundle") directive.map { directive in var problems = [Problem]() @@ -53,17 +53,17 @@ class CallToActionTests: XCTestCase { XCTAssertTrue(diagnosticIdentifiers.contains("org.swift.docc.\(CallToAction.self).missingLink")) } } - try assertMissingLink(source: "@CallToAction(label: \"Button\")") - try assertMissingLink(source: "@CallToAction(purpose: download)") + try await assertMissingLink(source: "@CallToAction(label: \"Button\")") + try await assertMissingLink(source: "@CallToAction(purpose: download)") } - func testInvalidWithoutLabel() throws { - func assertMissingLabel(source: String) throws { + func testInvalidWithoutLabel() async throws { + func assertMissingLabel(source: String) async throws { let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "SampleBundle") + let (bundle, _) = try await testBundleAndContext(named: "SampleBundle") directive.map { directive in var problems = [Problem]() @@ -75,17 +75,17 @@ class CallToActionTests: XCTestCase { XCTAssertTrue(diagnosticIdentifiers.contains("org.swift.docc.\(CallToAction.self).missingLabel")) } } - try assertMissingLabel(source: "@CallToAction(url: \"https://example.com/sample.zip\"") - try assertMissingLabel(source: "@CallToAction(file: \"Downloads/plus.svg\"") + try await assertMissingLabel(source: "@CallToAction(url: \"https://example.com/sample.zip\"") + try await assertMissingLabel(source: "@CallToAction(file: \"Downloads/plus.svg\"") } - func testInvalidTooManyLinks() throws { + func testInvalidTooManyLinks() async throws { let source = "@CallToAction(url: \"https://example.com/sample.zip\", file: \"Downloads/plus.svg\", purpose: download)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "SampleBundle") + let (bundle, _) = try await testBundleAndContext(named: "SampleBundle") directive.map { directive in var problems = [Problem]() @@ -98,13 +98,13 @@ class CallToActionTests: XCTestCase { } } - func testValidDirective() throws { - func assertValidDirective(source: String) throws { + func testValidDirective() async throws { + func assertValidDirective(source: String) async throws { let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "SampleBundle") + let (bundle, _) = try await testBundleAndContext(named: "SampleBundle") directive.map { directive in var problems = [Problem]() @@ -131,17 +131,17 @@ class CallToActionTests: XCTestCase { for link in validLinks { for label in validLabels { - try assertValidDirective(source: "@CallToAction(\(link), \(label))") + try await assertValidDirective(source: "@CallToAction(\(link), \(label))") } } } - func testDefaultLabel() throws { - func assertExpectedLabel(source: String, expectedDefaultLabel: String, expectedSampleCodeLabel: String) throws { + func testDefaultLabel() async throws { + func assertExpectedLabel(source: String, expectedDefaultLabel: String, expectedSampleCodeLabel: String) async throws { let document = Document(parsing: source, options: .parseBlockDirectives) let directive = try XCTUnwrap(document.child(at: 0) as? BlockDirective) - let (bundle, _) = try testBundleAndContext(named: "SampleBundle") + let (bundle, _) = try await testBundleAndContext(named: "SampleBundle") var problems = [Problem]() XCTAssertEqual(CallToAction.directiveName, directive.name) @@ -173,7 +173,7 @@ class CallToActionTests: XCTestCase { for (arg, defaultLabel, sampleCodeLabel) in validLabels { let directive = "@CallToAction(file: \"Downloads/plus.svg\", \(arg))" - try assertExpectedLabel( + try await assertExpectedLabel( source: directive, expectedDefaultLabel: defaultLabel, expectedSampleCodeLabel: sampleCodeLabel diff --git a/Tests/SwiftDocCTests/Semantics/ChapterTests.swift b/Tests/SwiftDocCTests/Semantics/ChapterTests.swift index f47f64ed6c..ddff9733fb 100644 --- a/Tests/SwiftDocCTests/Semantics/ChapterTests.swift +++ b/Tests/SwiftDocCTests/Semantics/ChapterTests.swift @@ -13,13 +13,13 @@ import XCTest import Markdown class ChapterTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = """ @Chapter """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let chapter = Chapter(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(chapter) @@ -31,7 +31,7 @@ class ChapterTests: XCTestCase { XCTAssert(problems.map { $0.diagnostic.severity }.allSatisfy { $0 == .warning }) } - func testMultipleMedia() throws { + func testMultipleMedia() async throws { let chapterName = "Chapter 1" let source = """ @Chapter(name: "\(chapterName)") { @@ -42,7 +42,7 @@ class ChapterTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let chapter = Chapter(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertEqual(1, problems.count) @@ -61,7 +61,7 @@ class ChapterTests: XCTestCase { } } - func testValid() throws { + func testValid() async throws { let chapterName = "Chapter 1" let source = """ @Chapter(name: "\(chapterName)") { @@ -71,7 +71,7 @@ class ChapterTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let chapter = Chapter(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertTrue(problems.isEmpty) @@ -82,8 +82,8 @@ class ChapterTests: XCTestCase { } } - func testDuplicateTutorialReferences() throws { - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testDuplicateTutorialReferences() async throws { + let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") /* The test bundle contains the duplicate tutorial references in TestOverview: diff --git a/Tests/SwiftDocCTests/Semantics/ChoiceTests.swift b/Tests/SwiftDocCTests/Semantics/ChoiceTests.swift index 12902ede40..7be45b6176 100644 --- a/Tests/SwiftDocCTests/Semantics/ChoiceTests.swift +++ b/Tests/SwiftDocCTests/Semantics/ChoiceTests.swift @@ -14,13 +14,13 @@ import Markdown import SwiftDocCTestUtilities class ChoiceTests: XCTestCase { - func testInvalidEmpty() throws { + func testInvalidEmpty() async throws { let source = "@Choice" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -35,7 +35,7 @@ class ChoiceTests: XCTestCase { } } - func testInvalidMissingContent() throws { + func testInvalidMissingContent() async throws { let source = """ @Choice(isCorrect: true) { @Justification { @@ -47,7 +47,7 @@ class ChoiceTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -60,7 +60,7 @@ class ChoiceTests: XCTestCase { } } - func testInvalidMissingJustification() throws { + func testInvalidMissingJustification() async throws { let source = """ @Choice(isCorrect: true) { This is some content. @@ -70,7 +70,7 @@ class ChoiceTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -84,7 +84,7 @@ class ChoiceTests: XCTestCase { } } - func testInvalidMissingIsCorrect() throws { + func testInvalidMissingIsCorrect() async throws { let source = """ @Choice { This is some content. @@ -96,7 +96,7 @@ class ChoiceTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -110,7 +110,7 @@ class ChoiceTests: XCTestCase { } } - func testInvalidIsCorrect() throws { + func testInvalidIsCorrect() async throws { let source = """ @Choice(isCorrect: blah) { This is some content. @@ -122,7 +122,7 @@ class ChoiceTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -139,7 +139,7 @@ class ChoiceTests: XCTestCase { } } - func testValidParagraph() throws { + func testValidParagraph() async throws { let source = """ @Choice(isCorrect: true) { This is some content. @@ -152,7 +152,7 @@ class ChoiceTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -172,7 +172,7 @@ Choice @1:1-6:2 isCorrect: true } } - func testValidCode() throws { + func testValidCode() async throws { let source = """ @Choice(isCorrect: true) { ```swift @@ -188,7 +188,7 @@ Choice @1:1-6:2 isCorrect: true let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -208,7 +208,7 @@ Choice @1:1-9:2 isCorrect: true } } - func testValidImage() throws { + func testValidImage() async throws { let source = """ @Choice(isCorrect: true) { @Image(source: blah.png, alt: blah) @@ -222,7 +222,7 @@ Choice @1:1-9:2 isCorrect: true let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try loadBundle(catalog: Folder(name: "unit-test.docc", content: [ + let (bundle, _) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: [ InfoPlist(identifier: "org.swift.docc.example"), DataFile(name: "blah.png", data: Data()), ])) diff --git a/Tests/SwiftDocCTests/Semantics/CodeTests.swift b/Tests/SwiftDocCTests/Semantics/CodeTests.swift index 7971bc2e05..136d1796ba 100644 --- a/Tests/SwiftDocCTests/Semantics/CodeTests.swift +++ b/Tests/SwiftDocCTests/Semantics/CodeTests.swift @@ -15,11 +15,11 @@ import XCTest import Markdown class CodeTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Code" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let code = Code(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(code) diff --git a/Tests/SwiftDocCTests/Semantics/ContentAndMediaTests.swift b/Tests/SwiftDocCTests/Semantics/ContentAndMediaTests.swift index cbc1267921..8ba8c47a86 100644 --- a/Tests/SwiftDocCTests/Semantics/ContentAndMediaTests.swift +++ b/Tests/SwiftDocCTests/Semantics/ContentAndMediaTests.swift @@ -13,20 +13,20 @@ import XCTest import Markdown class ContentAndMediaTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = """ @ContentAndMedia { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let contentAndMedia = ContentAndMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(contentAndMedia) XCTAssertEqual(0, problems.count) } - func testValid() throws { + func testValid() async throws { let source = """ @ContentAndMedia { @@ -37,7 +37,7 @@ class ContentAndMediaTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let contentAndMedia = ContentAndMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(contentAndMedia) @@ -47,7 +47,7 @@ class ContentAndMediaTests: XCTestCase { } } - func testTrailingMiddleMediaPosition() throws { + func testTrailingMiddleMediaPosition() async throws { let source = """ @ContentAndMedia { @@ -58,7 +58,7 @@ class ContentAndMediaTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let contentAndMedia = ContentAndMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(contentAndMedia) @@ -68,7 +68,7 @@ class ContentAndMediaTests: XCTestCase { } } - func testTrailingMediaPosition() throws { + func testTrailingMediaPosition() async throws { let source = """ @ContentAndMedia { @@ -81,7 +81,7 @@ class ContentAndMediaTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let contentAndMedia = ContentAndMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(contentAndMedia) @@ -91,7 +91,7 @@ class ContentAndMediaTests: XCTestCase { } } - func testDeprecatedArguments() throws { + func testDeprecatedArguments() async throws { let source = """ @ContentAndMedia(layout: horizontal, eyebrow: eyebrow, title: title) { @@ -102,7 +102,7 @@ class ContentAndMediaTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let contentAndMedia = ContentAndMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(contentAndMedia) diff --git a/Tests/SwiftDocCTests/Semantics/DisplayNameTests.swift b/Tests/SwiftDocCTests/Semantics/DisplayNameTests.swift index acefd33686..3f927018cd 100644 --- a/Tests/SwiftDocCTests/Semantics/DisplayNameTests.swift +++ b/Tests/SwiftDocCTests/Semantics/DisplayNameTests.swift @@ -15,11 +15,11 @@ import XCTest import Markdown class DisplayNameTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@DisplayName" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let displayName = DisplayName(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(displayName) @@ -28,11 +28,11 @@ class DisplayNameTests: XCTestCase { XCTAssertEqual("org.swift.docc.HasArgument.unlabeled", problems.first?.diagnostic.identifier) } - func testUnlabeledArgumentValue() throws { + func testUnlabeledArgumentValue() async throws { let source = "@DisplayName(\"Custom Symbol Name\")" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let displayName = DisplayName(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(displayName) @@ -40,11 +40,11 @@ class DisplayNameTests: XCTestCase { XCTAssertEqual(displayName?.style, .conceptual) } - func testConceptualStyleArgumentValue() throws { + func testConceptualStyleArgumentValue() async throws { let source = "@DisplayName(\"Custom Symbol Name\", style: conceptual)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let displayName = DisplayName(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(displayName) @@ -52,11 +52,11 @@ class DisplayNameTests: XCTestCase { XCTAssertEqual(displayName?.style, .conceptual) } - func testSymbolStyleArgumentValue() throws { + func testSymbolStyleArgumentValue() async throws { let source = "@DisplayName(\"Custom Symbol Name\", style: symbol)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let displayName = DisplayName(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(displayName) @@ -64,11 +64,11 @@ class DisplayNameTests: XCTestCase { XCTAssertEqual(displayName?.style, .symbol) } - func testUnknownStyleArgumentValue() throws { + func testUnknownStyleArgumentValue() async throws { let source = "@DisplayName(\"Custom Symbol Name\", style: somethingUnknown)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let displayName = DisplayName(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(displayName) @@ -77,11 +77,11 @@ class DisplayNameTests: XCTestCase { XCTAssertEqual("org.swift.docc.HasArgument.style.ConversionFailed", problems.first?.diagnostic.identifier) } - func testExtraArguments() throws { + func testExtraArguments() async throws { let source = "@DisplayName(\"Custom Symbol Name\", argument: value)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let displayName = DisplayName(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(displayName, "Even if there are warnings we can create a displayName value") @@ -90,7 +90,7 @@ class DisplayNameTests: XCTestCase { XCTAssertEqual("org.swift.docc.UnknownArgument", problems.first?.diagnostic.identifier) } - func testExtraDirective() throws { + func testExtraDirective() async throws { let source = """ @DisplayName(\"Custom Symbol Name\") { @Image @@ -98,7 +98,7 @@ class DisplayNameTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let displayName = DisplayName(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(displayName, "Even if there are warnings we can create a DisplayName value") @@ -108,7 +108,7 @@ class DisplayNameTests: XCTestCase { XCTAssertEqual("org.swift.docc.DisplayName.NoInnerContentAllowed", problems.last?.diagnostic.identifier) } - func testExtraContent() throws { + func testExtraContent() async throws { let source = """ @DisplayName(\"Custom Symbol Name\") { Some text @@ -116,7 +116,7 @@ class DisplayNameTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let displayName = DisplayName(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(displayName, "Even if there are warnings we can create a DisplayName value") diff --git a/Tests/SwiftDocCTests/Semantics/DocumentationExtensionTests.swift b/Tests/SwiftDocCTests/Semantics/DocumentationExtensionTests.swift index 9cbd234b3a..6c6d5a5d9c 100644 --- a/Tests/SwiftDocCTests/Semantics/DocumentationExtensionTests.swift +++ b/Tests/SwiftDocCTests/Semantics/DocumentationExtensionTests.swift @@ -15,11 +15,11 @@ import XCTest import Markdown class DocumentationExtensionTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@DocumentationExtension" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let options = DocumentationExtension(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(options) @@ -28,11 +28,11 @@ class DocumentationExtensionTests: XCTestCase { XCTAssertEqual("org.swift.docc.HasArgument.mergeBehavior", problems.first?.diagnostic.identifier) } - func testAppendArgumentValue() throws { + func testAppendArgumentValue() async throws { let source = "@DocumentationExtension(mergeBehavior: append)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let options = DocumentationExtension(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(options) @@ -41,11 +41,11 @@ class DocumentationExtensionTests: XCTestCase { XCTAssertEqual(options?.behavior, .append) } - func testOverrideArgumentValue() throws { + func testOverrideArgumentValue() async throws { let source = "@DocumentationExtension(mergeBehavior: override)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let options = DocumentationExtension(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(options) @@ -53,11 +53,11 @@ class DocumentationExtensionTests: XCTestCase { XCTAssertEqual(options?.behavior, .override) } - func testUnknownArgumentValue() throws { + func testUnknownArgumentValue() async throws { let source = "@DocumentationExtension(mergeBehavior: somethingUnknown )" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let options = DocumentationExtension(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(options) @@ -66,11 +66,11 @@ class DocumentationExtensionTests: XCTestCase { XCTAssertEqual("org.swift.docc.HasArgument.mergeBehavior.ConversionFailed", problems.first?.diagnostic.identifier) } - func testExtraArguments() throws { + func testExtraArguments() async throws { let source = "@DocumentationExtension(mergeBehavior: override, argument: value)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let options = DocumentationExtension(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(options, "Even if there are warnings we can create an options value") @@ -79,7 +79,7 @@ class DocumentationExtensionTests: XCTestCase { XCTAssertEqual("org.swift.docc.UnknownArgument", problems.first?.diagnostic.identifier) } - func testExtraDirective() throws { + func testExtraDirective() async throws { let source = """ @DocumentationExtension(mergeBehavior: override) { @Image @@ -87,7 +87,7 @@ class DocumentationExtensionTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let options = DocumentationExtension(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(options, "Even if there are warnings we can create a DocumentationExtension value") @@ -97,7 +97,7 @@ class DocumentationExtensionTests: XCTestCase { XCTAssertEqual("org.swift.docc.DocumentationExtension.NoInnerContentAllowed", problems.last?.diagnostic.identifier) } - func testExtraContent() throws { + func testExtraContent() async throws { let source = """ @DocumentationExtension(mergeBehavior: override) { Some text @@ -105,7 +105,7 @@ class DocumentationExtensionTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let options = DocumentationExtension(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(options, "Even if there are warnings we can create a DocumentationExtension value") @@ -114,14 +114,14 @@ class DocumentationExtensionTests: XCTestCase { XCTAssertEqual("org.swift.docc.DocumentationExtension.NoInnerContentAllowed", problems.first?.diagnostic.identifier) } - func testIncorrectArgumentLabel() throws { + func testIncorrectArgumentLabel() async throws { let source = """ @DocumentationExtension(merge: override) """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let options = DocumentationExtension(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(options) diff --git a/Tests/SwiftDocCTests/Semantics/DoxygenTests.swift b/Tests/SwiftDocCTests/Semantics/DoxygenTests.swift index 99b7d85950..9832537c9e 100644 --- a/Tests/SwiftDocCTests/Semantics/DoxygenTests.swift +++ b/Tests/SwiftDocCTests/Semantics/DoxygenTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -16,7 +16,7 @@ import SwiftDocCTestUtilities @testable import SymbolKit class DoxygenTests: XCTestCase { - func testDoxygenDiscussionAndNote() throws { + func testDoxygenDiscussionAndNote() async throws { let documentationLines: [SymbolGraph.LineList.Line] = """ This is an abstract. @abstract This is description with abstract. @@ -88,22 +88,22 @@ class DoxygenTests: XCTestCase { )), ]) - let (bundle, context) = try loadBundle(catalog: catalog) - let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/ModuleName/SomeClass", sourceLanguage: .swift) + let (_, context) = try await loadBundle(catalog: catalog) + let reference = ResolvedTopicReference(bundleID: context.inputs.id, path: "/documentation/ModuleName/SomeClass", sourceLanguage: .swift) // Verify the expected content in the in-memory model let node = try context.entity(with: reference) let symbol = try XCTUnwrap(node.semantic as? Symbol) XCTAssertEqual(symbol.abstract?.format(), "This is an abstract.") - XCTAssertEqual(symbol.discussion?.content.map { $0.format() }, [ + XCTAssertEqual(symbol.discussion?.content.map { $0.format().trimmingCharacters(in: .whitespacesAndNewlines) }, [ #"\abstract This is description with abstract."#, #"\discussion This is a discussion linking to ``doc://unit-test/documentation/ModuleName/AnotherClass`` and ``doc://unit-test/documentation/ModuleName/AnotherClass/prop``."#, #"\note This is a note linking to ``doc://unit-test/documentation/ModuleName/Class3`` and ``Class3/prop2``."# ]) // Verify the expected content in the render model - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) let renderNode = try XCTUnwrap(translator.visit(node.semantic) as? RenderNode) XCTAssertEqual(renderNode.abstract, [.text("This is an abstract.")]) diff --git a/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasAtLeastOneTests.swift b/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasAtLeastOneTests.swift index 81037df4ab..7e73ab2cc2 100644 --- a/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasAtLeastOneTests.swift +++ b/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasAtLeastOneTests.swift @@ -49,13 +49,13 @@ final class TestChild: Semantic, DirectiveConvertible { } class HasAtLeastOneTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Parent" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() do { var problems = [Problem]() @@ -83,7 +83,7 @@ class HasAtLeastOneTests: XCTestCase { } } - func testOne() throws { + func testOne() async throws { let source = """ @Parent { @Child @@ -94,7 +94,7 @@ class HasAtLeastOneTests: XCTestCase { var problems = [Problem]() XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in let (matches, remainder) = Semantic.Analyses.HasAtLeastOne(severityIfNotFound: .error).analyze(directive, children: directive.children, source: nil, for: bundle, problems: &problems) @@ -104,7 +104,7 @@ class HasAtLeastOneTests: XCTestCase { XCTAssertTrue(problems.isEmpty) } - func testMany() throws { + func testMany() async throws { let source = """ @Parent { @Child @@ -117,7 +117,7 @@ class HasAtLeastOneTests: XCTestCase { var problems = [Problem]() XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in let (matches, remainder) = Semantic.Analyses.HasAtLeastOne(severityIfNotFound: .error).analyze(directive, children: directive.children, source: nil, for: bundle, problems: &problems) @@ -127,7 +127,7 @@ class HasAtLeastOneTests: XCTestCase { XCTAssertTrue(problems.isEmpty) } - func testAlternateDirectiveTitle() throws { + func testAlternateDirectiveTitle() async throws { let source = """ @AlternateParent { @AlternateChild @@ -138,7 +138,7 @@ class HasAtLeastOneTests: XCTestCase { var problems = [Problem]() XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in let (matches, remainder) = Semantic.Analyses.HasAtLeastOne(severityIfNotFound: .error).analyze(directive, children: directive.children, source: nil, for: bundle, problems: &problems) diff --git a/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasAtMostOneTests.swift b/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasAtMostOneTests.swift index 95482570fe..6a90fb619a 100644 --- a/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasAtMostOneTests.swift +++ b/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasAtMostOneTests.swift @@ -13,13 +13,13 @@ import XCTest import Markdown class HasAtMostOneTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Parent" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -30,7 +30,7 @@ class HasAtMostOneTests: XCTestCase { } } - func testHasOne() throws { + func testHasOne() async throws { let source = """ @Parent { @Child @@ -40,7 +40,7 @@ class HasAtMostOneTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -51,7 +51,7 @@ class HasAtMostOneTests: XCTestCase { } } - func testHasMany() throws { + func testHasMany() async throws { let source = """ @Parent { @Child @@ -63,7 +63,7 @@ class HasAtMostOneTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -80,7 +80,7 @@ class HasAtMostOneTests: XCTestCase { } } - func testAlternateDirectiveTitle() throws { + func testAlternateDirectiveTitle() async throws { let source = """ @AlternateParent { @AlternateChild @@ -90,7 +90,7 @@ class HasAtMostOneTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() diff --git a/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasExactlyOneTests.swift b/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasExactlyOneTests.swift index 9bd2646077..b64e8c7f19 100644 --- a/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasExactlyOneTests.swift +++ b/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasExactlyOneTests.swift @@ -13,13 +13,13 @@ import XCTest import Markdown class HasExactlyOneTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Parent" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -40,7 +40,7 @@ class HasExactlyOneTests: XCTestCase { } } - func testHasOne() throws { + func testHasOne() async throws { let source = """ @Parent { @Child @@ -50,7 +50,7 @@ class HasExactlyOneTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -61,7 +61,7 @@ class HasExactlyOneTests: XCTestCase { } } - func testHasMany() throws { + func testHasMany() async throws { let source = """ @Parent { @Child @@ -73,7 +73,7 @@ class HasExactlyOneTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -92,7 +92,7 @@ class HasExactlyOneTests: XCTestCase { } } - func testAlternateDirectiveTitle() throws { + func testAlternateDirectiveTitle() async throws { let source = """ @AlternateParent { @AlternateChild @@ -102,7 +102,7 @@ class HasExactlyOneTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() diff --git a/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasOnlySequentialHeadingsTests.swift b/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasOnlySequentialHeadingsTests.swift index 6658d8621d..85b351d8be 100644 --- a/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasOnlySequentialHeadingsTests.swift +++ b/Tests/SwiftDocCTests/Semantics/General Purpose Analyses/HasOnlySequentialHeadingsTests.swift @@ -15,7 +15,7 @@ import Markdown class HasOnlySequentialHeadingsTests: XCTestCase { private let containerDirective = BlockDirective(name: "TestContainer") - func testNoHeadings() throws { + func testNoHeadings() async throws { let source = """ asdf @@ -27,7 +27,7 @@ some more *stuff* """ let document = Document(parsing: source, options: .parseBlockDirectives) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems: [Problem] = [] Semantic.Analyses.HasOnlySequentialHeadings(severityIfFound: .warning, startingFromLevel: 2).analyze(containerDirective, children: document.children, source: nil, for: bundle, problems: &problems) @@ -35,7 +35,7 @@ some more *stuff* XCTAssertTrue(problems.isEmpty) } - func testValidHeadings() throws { + func testValidHeadings() async throws { let source = """ ## H2 ### H3 @@ -50,7 +50,7 @@ some more *stuff* """ let document = Document(parsing: source, options: .parseBlockDirectives) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems: [Problem] = [] Semantic.Analyses.HasOnlySequentialHeadings(severityIfFound: .warning, startingFromLevel: 2).analyze(containerDirective, children: document.children, source: nil, for: bundle, problems: &problems) @@ -58,14 +58,14 @@ some more *stuff* XCTAssertTrue(problems.isEmpty) } - func testHeadingLevelTooLow() throws { + func testHeadingLevelTooLow() async throws { let source = """ # H1 # H1 """ let document = Document(parsing: source, options: .parseBlockDirectives) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems: [Problem] = [] Semantic.Analyses.HasOnlySequentialHeadings(severityIfFound: .warning, startingFromLevel: 2).analyze(containerDirective, children: document.children, source: nil, for: bundle, problems: &problems) @@ -77,7 +77,7 @@ some more *stuff* ]) } - func testHeadingSkipsLevel() throws { + func testHeadingSkipsLevel() async throws { let source = """ ## H2 #### H4 @@ -86,7 +86,7 @@ some more *stuff* """ let document = Document(parsing: source, options: .parseBlockDirectives) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems: [Problem] = [] Semantic.Analyses.HasOnlySequentialHeadings(severityIfFound: .warning, startingFromLevel: 2).analyze(containerDirective, children: document.children, source: nil, for: bundle, problems: &problems) diff --git a/Tests/SwiftDocCTests/Semantics/ImageMediaTests.swift b/Tests/SwiftDocCTests/Semantics/ImageMediaTests.swift index f850ded4f8..e46f80845c 100644 --- a/Tests/SwiftDocCTests/Semantics/ImageMediaTests.swift +++ b/Tests/SwiftDocCTests/Semantics/ImageMediaTests.swift @@ -13,13 +13,13 @@ import XCTest import Markdown class ImageMediaTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = """ @Image """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let image = ImageMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(image) @@ -30,7 +30,7 @@ class ImageMediaTests: XCTestCase { ], problems.map { $0.diagnostic.identifier }) } - func testValid() throws { + func testValid() async throws { let imageSource = "/path/to/image" let alt = "This is an image" let source = """ @@ -38,7 +38,7 @@ class ImageMediaTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let image = ImageMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(image) @@ -49,7 +49,7 @@ class ImageMediaTests: XCTestCase { } } - func testSpacesInSource() throws { + func testSpacesInSource() async throws { for imageSource in ["my image.png", "my%20image.png"] { let alt = "This is an image" let source = """ @@ -57,7 +57,7 @@ class ImageMediaTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let image = ImageMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(image) @@ -69,13 +69,13 @@ class ImageMediaTests: XCTestCase { } } - func testIncorrectArgumentLabels() throws { + func testIncorrectArgumentLabels() async throws { let source = """ @Image(imgSource: "/img/path", altText: "Text") """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let image = ImageMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(image) @@ -94,47 +94,41 @@ class ImageMediaTests: XCTestCase { } } - func testRenderImageDirectiveInReferenceMarkup() throws { + func testRenderImageDirectiveInReferenceMarkup() async throws { do { - let (renderedContent, problems, image) = try parseDirective(ImageMedia.self, in: "BookLikeContent") { + let (renderedContent, problems, image) = try await parseDirective(ImageMedia.self, withAvailableAssetNames: ["figure1.jpg"]) { """ @Image(source: "figure1") """ } XCTAssertNotNil(image) - XCTAssertEqual(problems, []) - - XCTAssertEqual( - renderedContent, - [ - RenderBlockContent.paragraph(RenderBlockContent.Paragraph( - inlineContent: [.image( - identifier: RenderReferenceIdentifier("figure1"), - metadata: nil - )] - )) - ] - ) + XCTAssertEqual(renderedContent, [ + RenderBlockContent.paragraph(RenderBlockContent.Paragraph( + inlineContent: [.image( + identifier: RenderReferenceIdentifier("figure1"), + metadata: nil + )] + )) + ]) } do { - let (renderedContent, problems, image) = try parseDirective(ImageMedia.self, in: "BookLikeContent") { + let (renderedContent, problems, image) = try await parseDirective(ImageMedia.self, withAvailableAssetNames: []) { """ @Image(source: "unknown-image") """ } XCTAssertNotNil(image) - XCTAssertEqual(problems, ["1: warning – org.swift.docc.unresolvedResource.Image"]) XCTAssertEqual(renderedContent, []) } } - func testRenderImageDirectiveWithCaption() throws { - let (renderedContent, problems, image) = try parseDirective(ImageMedia.self, in: "BookLikeContent") { + func testRenderImageDirectiveWithCaption() async throws { + let (renderedContent, problems, image) = try await parseDirective(ImageMedia.self, withAvailableAssetNames: ["figure1.jpg"]) { """ @Image(source: "figure1") { This is my caption. @@ -143,76 +137,61 @@ class ImageMediaTests: XCTestCase { } XCTAssertNotNil(image) - XCTAssertEqual(problems, []) - - XCTAssertEqual( - renderedContent, - [ - RenderBlockContent.paragraph(RenderBlockContent.Paragraph( - inlineContent: [.image( - identifier: RenderReferenceIdentifier("figure1"), - metadata: RenderContentMetadata(abstract: [.text("This is my caption.")]) - )] - )) - ] - ) + XCTAssertEqual(renderedContent, [ + RenderBlockContent.paragraph(RenderBlockContent.Paragraph( + inlineContent: [.image( + identifier: RenderReferenceIdentifier("figure1"), + metadata: RenderContentMetadata(abstract: [.text("This is my caption.")]) + )] + )) + ]) } - func testImageDirectiveDiagnosesDeviceFrameByDefault() throws { - let (renderedContent, problems, image) = try parseDirective(ImageMedia.self, in: "BookLikeContent") { + func testImageDirectiveDiagnosesDeviceFrameByDefault() async throws { + let (renderedContent, problems, image) = try await parseDirective(ImageMedia.self, withAvailableAssetNames: ["figure1.jpg"]) { """ @Image(source: "figure1", deviceFrame: phone) """ } XCTAssertNotNil(image) - XCTAssertEqual(problems, ["1: warning – org.swift.docc.UnknownArgument"]) - - XCTAssertEqual( - renderedContent, - [ - RenderBlockContent.paragraph(RenderBlockContent.Paragraph( - inlineContent: [.image( - identifier: RenderReferenceIdentifier("figure1"), - metadata: nil - )] - )) - ] - ) + XCTAssertEqual(renderedContent, [ + RenderBlockContent.paragraph(RenderBlockContent.Paragraph( + inlineContent: [.image( + identifier: RenderReferenceIdentifier("figure1"), + metadata: nil + )] + )) + ]) } - func testRenderImageDirectiveWithDeviceFrame() throws { + func testRenderImageDirectiveWithDeviceFrame() async throws { enableFeatureFlag(\.isExperimentalDeviceFrameSupportEnabled) - let (renderedContent, problems, image) = try parseDirective(ImageMedia.self, in: "BookLikeContent") { + let (renderedContent, problems, image) = try await parseDirective(ImageMedia.self, withAvailableAssetNames: ["figure1.jpg"]) { """ @Image(source: "figure1", deviceFrame: phone) """ } XCTAssertNotNil(image) - XCTAssertEqual(problems, []) - - XCTAssertEqual( - renderedContent, - [ - RenderBlockContent.paragraph(RenderBlockContent.Paragraph( - inlineContent: [.image( - identifier: RenderReferenceIdentifier("figure1"), - metadata: RenderContentMetadata(deviceFrame: "phone") - )] - )) - ] - ) + XCTAssertEqual(renderedContent, [ + RenderBlockContent.paragraph(RenderBlockContent.Paragraph( + inlineContent: [.image( + identifier: RenderReferenceIdentifier("figure1"), + metadata: RenderContentMetadata(deviceFrame: "phone") + )] + )) + ]) } - func testRenderImageDirectiveWithDeviceFrameAndCaption() throws { + func testRenderImageDirectiveWithDeviceFrameAndCaption() async throws { enableFeatureFlag(\.isExperimentalDeviceFrameSupportEnabled) - let (renderedContent, problems, image) = try parseDirective(ImageMedia.self, in: "BookLikeContent") { + let (renderedContent, problems, image) = try await parseDirective(ImageMedia.self, withAvailableAssetNames: ["figure1.jpg"]) { """ @Image(source: "figure1", deviceFrame: laptop) { This is my caption. @@ -221,43 +200,35 @@ class ImageMediaTests: XCTestCase { } XCTAssertNotNil(image) - XCTAssertEqual(problems, []) - - XCTAssertEqual( - renderedContent, - [ - RenderBlockContent.paragraph(RenderBlockContent.Paragraph( - inlineContent: [.image( - identifier: RenderReferenceIdentifier("figure1"), - metadata: RenderContentMetadata(abstract: [.text("This is my caption.")], deviceFrame: "laptop") - )] - )) - ] - ) + XCTAssertEqual(renderedContent, [ + RenderBlockContent.paragraph(RenderBlockContent.Paragraph( + inlineContent: [.image( + identifier: RenderReferenceIdentifier("figure1"), + metadata: RenderContentMetadata(abstract: [.text("This is my caption.")], deviceFrame: "laptop") + )] + )) + ]) } - func testImageDirectiveDoesNotResolveVideoReference() throws { + func testImageDirectiveDoesNotResolveVideoReference() async throws { // First check that the Video exists - let (_, videoProblems, _) = try parseDirective(VideoMedia.self, in: "LegacyBundle_DoNotUseInNewTests") { + let (_, videoProblems, _) = try await parseDirective(VideoMedia.self, withAvailableAssetNames: ["introvideo.mp4"]) { """ @Video(source: "introvideo") """ } - XCTAssertEqual(videoProblems, []) // Then check that it doesn't resolve as an image - let (renderedContent, imageProblems, image) = try parseDirective(ImageMedia.self, in: "LegacyBundle_DoNotUseInNewTests") { + let (renderedContent, imageProblems, image) = try await parseDirective(ImageMedia.self, withAvailableAssetNames: ["introvideo.mp4"]) { """ @Image(source: "introvideo") """ } - XCTAssertNotNil(image) - XCTAssertEqual(imageProblems, ["1: warning – org.swift.docc.unresolvedResource.Image"]) - XCTAssertEqual(renderedContent, []) } } + diff --git a/Tests/SwiftDocCTests/Semantics/IntroTests.swift b/Tests/SwiftDocCTests/Semantics/IntroTests.swift index c395f64943..6b38105a28 100644 --- a/Tests/SwiftDocCTests/Semantics/IntroTests.swift +++ b/Tests/SwiftDocCTests/Semantics/IntroTests.swift @@ -13,11 +13,11 @@ import XCTest import Markdown class IntroTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Intro" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let intro = Intro(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(intro) @@ -26,7 +26,7 @@ class IntroTests: XCTestCase { XCTAssertEqual("org.swift.docc.HasArgument.title", problems[0].diagnostic.identifier) } - func testValid() throws { + func testValid() async throws { let videoPath = "/path/to/video" let imagePath = "/path/to/image" let posterPath = "/path/to/poster" @@ -42,7 +42,7 @@ class IntroTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let intro = Intro(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(intro) @@ -56,7 +56,7 @@ class IntroTests: XCTestCase { } } - func testIncorrectArgumentLabel() throws { + func testIncorrectArgumentLabel() async throws { let source = """ @Intro(titleText: "Title") { Here is a paragraph. @@ -68,7 +68,7 @@ class IntroTests: XCTestCase { let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let intro = Intro(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(intro) diff --git a/Tests/SwiftDocCTests/Semantics/JustificationTests.swift b/Tests/SwiftDocCTests/Semantics/JustificationTests.swift index 544afc629e..247169b9f9 100644 --- a/Tests/SwiftDocCTests/Semantics/JustificationTests.swift +++ b/Tests/SwiftDocCTests/Semantics/JustificationTests.swift @@ -13,13 +13,13 @@ import XCTest import Markdown class JustificationTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Justification" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -34,7 +34,7 @@ class JustificationTests: XCTestCase { } } - func testValid() throws { + func testValid() async throws { let source = """ @Justification(reaction: "Correct!") { Here is some content. @@ -44,7 +44,7 @@ class JustificationTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() diff --git a/Tests/SwiftDocCTests/Semantics/MarkupReferenceResolverTests.swift b/Tests/SwiftDocCTests/Semantics/MarkupReferenceResolverTests.swift index 5ffdbf18d4..97f3b48ab2 100644 --- a/Tests/SwiftDocCTests/Semantics/MarkupReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Semantics/MarkupReferenceResolverTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022-2023 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -11,10 +11,15 @@ import XCTest @testable import SwiftDocC import Markdown +import SwiftDocCTestUtilities class MarkupReferenceResolverTests: XCTestCase { - func testArbitraryReferenceInComment() throws { - let (bundle, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + func testArbitraryReferenceInComment() async throws { + let catalog = Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName")) + ]) + + let (_, context) = try await loadBundle(catalog: catalog) let source = """ @Comment { ``hello`` and ``world`` are 2 arbitrary symbol links. @@ -23,13 +28,13 @@ class MarkupReferenceResolverTests: XCTestCase { } """ let document = Document(parsing: source, options: [.parseBlockDirectives, .parseSymbolLinks]) - var resolver = MarkupReferenceResolver(context: context, bundle: bundle, rootReference: context.rootModules[0]) + var resolver = MarkupReferenceResolver(context: context, rootReference: context.rootModules[0]) _ = resolver.visit(document) XCTAssertEqual(0, resolver.problems.count) } - func testDuplicatedDiagnosticForExtensionFile() throws { - let (_, context) = try testBundleAndContext(named: "ExtensionArticleBundle") + func testDuplicatedDiagnosticForExtensionFile() async throws { + let (_, context) = try await testBundleAndContext(named: "ExtensionArticleBundle") // Before #733, symbols with documentation extension files emitted duplicated problems: // - one with a source location in the in-source documentation comment // - one with a source location in the documentation extension file. diff --git a/Tests/SwiftDocCTests/Semantics/MetadataAlternateRepresentationTests.swift b/Tests/SwiftDocCTests/Semantics/MetadataAlternateRepresentationTests.swift index 9fc696e6eb..ea3d2647c9 100644 --- a/Tests/SwiftDocCTests/Semantics/MetadataAlternateRepresentationTests.swift +++ b/Tests/SwiftDocCTests/Semantics/MetadataAlternateRepresentationTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -15,9 +15,9 @@ import Markdown @testable import SwiftDocC class MetadataAlternateRepresentationTests: XCTestCase { - func testValidLocalLink() throws { + func testValidLocalLink() async throws { for link in ["``MyClass/property``", "MyClass/property"] { - let (problems, metadata) = try parseDirective(Metadata.self) { + let (problems, metadata) = try await parseDirective(Metadata.self) { """ @Metadata { @AlternateRepresentation(\(link)) @@ -33,8 +33,8 @@ class MetadataAlternateRepresentationTests: XCTestCase { } } - func testValidExternalLinkReference() throws { - let (problems, metadata) = try parseDirective(Metadata.self) { + func testValidExternalLinkReference() async throws { + let (problems, metadata) = try await parseDirective(Metadata.self) { """ @Metadata { @AlternateRepresentation("doc://com.example/documentation/MyClass/property") @@ -49,8 +49,8 @@ class MetadataAlternateRepresentationTests: XCTestCase { XCTAssertEqual(alternateRepresentation.reference.url, URL(string: "doc://com.example/documentation/MyClass/property")) } - func testInvalidTopicReference() throws { - let (problems, _) = try parseDirective(Metadata.self) { + func testInvalidTopicReference() async throws { + let (problems, _) = try await parseDirective(Metadata.self) { """ @Metadata { @AlternateRepresentation("doc://") diff --git a/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift index 5d7afb5996..52292a079e 100644 --- a/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift +++ b/Tests/SwiftDocCTests/Semantics/MetadataAvailabilityTests.swift @@ -15,10 +15,10 @@ import Markdown @testable import SwiftDocC class MetadataAvailabilityTests: XCTestCase { - func testInvalidWithNoArguments() throws { + func testInvalidWithNoArguments() async throws { let source = "@Available" - try assertDirective(Metadata.Availability.self, source: source) { directive, problems in + try await assertDirective(Metadata.Availability.self, source: source) { directive, problems in XCTAssertNil(directive) XCTAssertEqual(2, problems.count) @@ -32,7 +32,7 @@ class MetadataAvailabilityTests: XCTestCase { } } - func testInvalidDuplicateIntroduced() throws { + func testInvalidDuplicateIntroduced() async throws { for platform in Metadata.Availability.Platform.defaultCases { let source = """ @Metadata { @@ -40,7 +40,7 @@ class MetadataAvailabilityTests: XCTestCase { @Available(\(platform.rawValue), introduced: \"2.0\") } """ - try assertDirective(Metadata.self, source: source) { directive, problems in + try await assertDirective(Metadata.self, source: source) { directive, problems in XCTAssertEqual(2, problems.count) let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) XCTAssertEqual(diagnosticIdentifiers, ["org.swift.docc.\(Metadata.Availability.self).DuplicateIntroduced"]) @@ -48,7 +48,7 @@ class MetadataAvailabilityTests: XCTestCase { } } - func testInvalidIntroducedFormat() throws { + func testInvalidIntroducedFormat() async throws { let source = """ @Metadata { @TechnologyRoot @@ -63,7 +63,7 @@ class MetadataAvailabilityTests: XCTestCase { } """ - try assertDirective(Metadata.self, source: source) { directive, problems in + try await assertDirective(Metadata.self, source: source) { directive, problems in XCTAssertEqual(8, problems.count) let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) let diagnosticExplanations = Set(problems.map { $0.diagnostic.explanation }) @@ -74,7 +74,7 @@ class MetadataAvailabilityTests: XCTestCase { } } - func testValidSemanticVersionFormat() throws { + func testValidSemanticVersionFormat() async throws { let source = """ @Metadata { @Available(iOS, introduced: \"3.5.2\", deprecated: \"5.6.7\") @@ -83,7 +83,7 @@ class MetadataAvailabilityTests: XCTestCase { } """ - try assertDirective(Metadata.self, source: source) { directive, problems in + try await assertDirective(Metadata.self, source: source) { directive, problems in XCTAssertEqual(0, problems.count) let directive = try XCTUnwrap(directive) @@ -113,7 +113,7 @@ class MetadataAvailabilityTests: XCTestCase { } } - func testValidIntroducedDirective() throws { + func testValidIntroducedDirective() async throws { // Assemble all the combinations of arguments you could give let validArguments: [String] = [ "deprecated: \"1.0\"", @@ -135,13 +135,13 @@ class MetadataAvailabilityTests: XCTestCase { for platform in checkPlatforms { for args in validArgumentsWithVersion { - try assertValidAvailability(source: "@Available(\(platform), \(args))") + try await assertValidAvailability(source: "@Available(\(platform), \(args))") } } } /// Basic validity test for giving several directives. - func testMultipleAvailabilityDirectives() throws { + func testMultipleAvailabilityDirectives() async throws { let source = """ @Metadata { @Available(macOS, introduced: "11.0") @@ -150,15 +150,15 @@ class MetadataAvailabilityTests: XCTestCase { @Available("My Package", introduced: "0.1", deprecated: "1.0") } """ - try assertValidMetadata(source: source) + try await assertValidMetadata(source: source) } - func assertDirective(_ type: Directive.Type, source: String, assertion assert: (Directive?, [Problem]) throws -> Void) throws { + func assertDirective(_ type: Directive.Type, source: String, assertion assert: (Directive?, [Problem]) throws -> Void) async throws { let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "AvailabilityBundle") + let (bundle, _) = try await testBundleAndContext(named: "AvailabilityBundle") try directive.map { directive in var problems = [Problem]() @@ -168,18 +168,18 @@ class MetadataAvailabilityTests: XCTestCase { } } - func assertValidDirective(_ type: Directive.Type, source: String) throws { - try assertDirective(type, source: source) { directive, problems in + func assertValidDirective(_ type: Directive.Type, source: String) async throws { + try await assertDirective(type, source: source) { directive, problems in XCTAssertNotNil(directive) XCTAssert(problems.isEmpty) } } - func assertValidAvailability(source: String) throws { - try assertValidDirective(Metadata.Availability.self, source: source) + func assertValidAvailability(source: String) async throws { + try await assertValidDirective(Metadata.Availability.self, source: source) } - func assertValidMetadata(source: String) throws { - try assertValidDirective(Metadata.self, source: source) + func assertValidMetadata(source: String) async throws { + try await assertValidDirective(Metadata.self, source: source) } } diff --git a/Tests/SwiftDocCTests/Semantics/MetadataTests.swift b/Tests/SwiftDocCTests/Semantics/MetadataTests.swift index c807d451ad..63c7225b17 100644 --- a/Tests/SwiftDocCTests/Semantics/MetadataTests.swift +++ b/Tests/SwiftDocCTests/Semantics/MetadataTests.swift @@ -15,11 +15,11 @@ import XCTest import Markdown class MetadataTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Metadata" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata, "Even if a Metadata directive is empty we can create it") @@ -29,11 +29,11 @@ class MetadataTests: XCTestCase { XCTAssertNotNil(problems.first?.possibleSolutions.first) } - func testUnexpectedArgument() throws { + func testUnexpectedArgument() async throws { let source = "@Metadata(argument: value)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata, "Even if there are warnings we can create a metadata value") @@ -42,7 +42,7 @@ class MetadataTests: XCTestCase { XCTAssertEqual("org.swift.docc.Metadata.NoConfiguration", problems.last?.diagnostic.identifier) } - func testUnexpectedDirective() throws { + func testUnexpectedDirective() async throws { let source = """ @Metadata { @Image @@ -50,7 +50,7 @@ class MetadataTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata, "Even if there are warnings we can create a Metadata value") @@ -61,7 +61,7 @@ class MetadataTests: XCTestCase { } - func testExtraContent() throws { + func testExtraContent() async throws { let source = """ @Metadata { Some text @@ -69,7 +69,7 @@ class MetadataTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata, "Even if there are warnings we can create a Metadata value") @@ -80,7 +80,7 @@ class MetadataTests: XCTestCase { // MARK: - Supported metadata directives - func testDocumentationExtensionSupport() throws { + func testDocumentationExtensionSupport() async throws { let source = """ @Metadata { @DocumentationExtension(mergeBehavior: override) @@ -88,7 +88,7 @@ class MetadataTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata) @@ -96,7 +96,7 @@ class MetadataTests: XCTestCase { XCTAssertEqual(metadata?.documentationOptions?.behavior, .override) } - func testRepeatDocumentationExtension() throws { + func testRepeatDocumentationExtension() async throws { let source = """ @Metadata { @DocumentationExtension(mergeBehavior: append) @@ -105,7 +105,7 @@ class MetadataTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata) @@ -117,7 +117,7 @@ class MetadataTests: XCTestCase { XCTAssertEqual(metadata?.documentationOptions?.behavior, .append) } - func testDisplayNameSupport() throws { + func testDisplayNameSupport() async throws { let source = """ @Metadata { @DisplayName("Custom Name") @@ -125,7 +125,7 @@ class MetadataTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata) @@ -134,7 +134,7 @@ class MetadataTests: XCTestCase { XCTAssertEqual(metadata?.displayName?.name, "Custom Name") } - func testTitleHeadingSupport() throws { + func testTitleHeadingSupport() async throws { let source = """ @Metadata { @TitleHeading("Custom Heading") @@ -142,7 +142,7 @@ class MetadataTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata) @@ -151,7 +151,7 @@ class MetadataTests: XCTestCase { XCTAssertEqual(metadata?.titleHeading?.heading, "Custom Heading") } - func testCustomMetadataSupport() throws { + func testCustomMetadataSupport() async throws { let source = """ @Metadata { @CustomMetadata(key: "country", value: "Belgium") @@ -160,7 +160,7 @@ class MetadataTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata) @@ -168,7 +168,7 @@ class MetadataTests: XCTestCase { XCTAssertEqual(problems.count, 0) } - func testRedirectSupport() throws { + func testRedirectSupport() async throws { let source = """ @Metadata { @Redirected(from: "some/other/path") @@ -176,7 +176,7 @@ class MetadataTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let metadata = Metadata(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(metadata) @@ -186,7 +186,7 @@ class MetadataTests: XCTestCase { // MARK: - Metadata Support - func testArticleSupportsMetadata() throws { + func testArticleSupportsMetadata() async throws { let source = """ # Plain article @@ -197,7 +197,7 @@ class MetadataTests: XCTestCase { The abstract of this article """ let document = Document(parsing: source, options: .parseBlockDirectives) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "An Article value can be created with a Metadata child.") @@ -208,7 +208,7 @@ class MetadataTests: XCTestCase { XCTAssert(analyzer.problems.isEmpty, "Expected no problems. Got:\n \(DiagnosticConsoleWriter.formattedDescription(for: analyzer.problems))") } - func testSymbolArticleSupportsMetadataDisplayName() throws { + func testSymbolArticleSupportsMetadataDisplayName() async throws { let source = """ # ``SomeSymbol`` @@ -219,7 +219,7 @@ class MetadataTests: XCTestCase { The abstract of this documentation extension """ let document = Document(parsing: source, options: [.parseBlockDirectives, .parseSymbolLinks]) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "An Article value can be created with a Metadata child with a DisplayName child.") @@ -232,7 +232,7 @@ class MetadataTests: XCTestCase { XCTAssert(analyzer.problems.isEmpty, "Expected no problems. Got:\n \(DiagnosticConsoleWriter.formattedDescription(for: analyzer.problems))") } - func testArticleDoesNotSupportsMetadataDisplayName() throws { + func testArticleDoesNotSupportsMetadataDisplayName() async throws { let source = """ # Article title @@ -243,7 +243,7 @@ class MetadataTests: XCTestCase { The abstract of this documentation extension """ let document = Document(parsing: source, options: [.parseBlockDirectives, .parseSymbolLinks]) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "An Article value can be created with a Metadata child with a DisplayName child.") @@ -268,7 +268,7 @@ class MetadataTests: XCTestCase { XCTAssertEqual(solution.replacements.last?.replacement, "# Custom Name") } - func testArticleSupportsMetadataTitleHeading() throws { + func testArticleSupportsMetadataTitleHeading() async throws { let source = """ # Article title @@ -279,7 +279,7 @@ class MetadataTests: XCTestCase { The abstract of this documentation extension """ let document = Document(parsing: source, options: [.parseBlockDirectives, .parseSymbolLinks]) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "An Article value can be created with a Metadata child with a TitleHeading child.") @@ -293,7 +293,7 @@ class MetadataTests: XCTestCase { XCTAssert(analyzer.problems.isEmpty, "Expected no problems. Got:\n \(DiagnosticConsoleWriter.formattedDescription(for: analyzer.problems))") } - func testDuplicateMetadata() throws { + func testDuplicateMetadata() async throws { let source = """ # Article title @@ -307,7 +307,7 @@ class MetadataTests: XCTestCase { The abstract of this documentation extension """ let document = Document(parsing: source, options: [.parseBlockDirectives, .parseSymbolLinks]) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "An Article value can be created with a Metadata child with a DisplayName child.") @@ -323,8 +323,8 @@ class MetadataTests: XCTestCase { ) } - func testPageImageSupport() throws { - let (problems, metadata) = try parseMetadataFromSource( + func testPageImageSupport() async throws { + let (problems, metadata) = try await parseMetadataFromSource( """ # Article title @@ -353,8 +353,8 @@ class MetadataTests: XCTestCase { XCTAssertEqual(slothImage?.alt, "A sloth on a branch.") } - func testDuplicatePageImage() throws { - let (problems, _) = try parseMetadataFromSource( + func testDuplicatePageImage() async throws { + let (problems, _) = try await parseMetadataFromSource( """ # Article title @@ -376,9 +376,9 @@ class MetadataTests: XCTestCase { ) } - func testPageColorSupport() throws { + func testPageColorSupport() async throws { do { - let (problems, metadata) = try parseMetadataFromSource( + let (problems, metadata) = try await parseMetadataFromSource( """ # Article title @@ -395,7 +395,7 @@ class MetadataTests: XCTestCase { } do { - let (problems, metadata) = try parseMetadataFromSource( + let (problems, metadata) = try await parseMetadataFromSource( """ # Article title @@ -416,12 +416,12 @@ class MetadataTests: XCTestCase { _ source: String, file: StaticString = #filePath, line: UInt = #line - ) throws -> (problems: [String], metadata: Metadata) { + ) async throws -> (problems: [String], metadata: Metadata) { let document = Document(parsing: source, options: [.parseBlockDirectives, .parseSymbolLinks]) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (inputs, _) = try await testBundleAndContext() var problems = [Problem]() - let article = Article(from: document, source: nil, for: bundle, problems: &problems) + let article = Article(from: document, source: nil, for: inputs, problems: &problems) let problemIDs = problems.map { problem -> String in let line = problem.diagnostic.range?.lowerBound.line.description ?? "unknown-line" diff --git a/Tests/SwiftDocCTests/Semantics/MultipleChoiceTests.swift b/Tests/SwiftDocCTests/Semantics/MultipleChoiceTests.swift index 853a9bb92a..042cad6bd8 100644 --- a/Tests/SwiftDocCTests/Semantics/MultipleChoiceTests.swift +++ b/Tests/SwiftDocCTests/Semantics/MultipleChoiceTests.swift @@ -11,20 +11,21 @@ import XCTest @testable import SwiftDocC import Markdown +import SwiftDocCTestUtilities class MultipleChoiceTests: XCTestCase { - func testInvalidEmpty() throws { + func testInvalidEmpty() async throws { let source = "@MultipleChoice" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (inputs, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() XCTAssertEqual(MultipleChoice.directiveName, directive.name) - let multipleChoice = MultipleChoice(from: directive, source: nil, for: bundle, problems: &problems) + let multipleChoice = MultipleChoice(from: directive, source: nil, for: inputs, problems: &problems) XCTAssertNil(multipleChoice) XCTAssertEqual(3, problems.count) let diagnosticIdentifiers = Set(problems.map { $0.diagnostic.identifier }) @@ -34,7 +35,7 @@ class MultipleChoiceTests: XCTestCase { } } - func testInvalidTooFewChoices() throws { + func testInvalidTooFewChoices() async throws { let source = """ @MultipleChoice { What is your favorite color? @@ -53,12 +54,12 @@ class MultipleChoiceTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (inputs, _) = try await testBundleAndContext() try directive.map { directive in var problems = [Problem]() XCTAssertEqual(MultipleChoice.directiveName, directive.name) - let multipleChoice = MultipleChoice(from: directive, source: nil, for: bundle, problems: &problems) + let multipleChoice = MultipleChoice(from: directive, source: nil, for: inputs, problems: &problems) XCTAssertNotNil(multipleChoice) XCTAssertEqual(1, problems.count) let problem = try XCTUnwrap( @@ -70,7 +71,7 @@ class MultipleChoiceTests: XCTestCase { } } - func testInvalidCodeAndImage() throws { + func testInvalidCodeAndImage() async throws { let source = """ @MultipleChoice { Question 1 @@ -101,12 +102,15 @@ class MultipleChoiceTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ + DataFile(name: "blah.png", data: Data()), + InfoPlist(identifier: "org.swift.docc.example") + ])) directive.map { directive in var problems = [Problem]() XCTAssertEqual(MultipleChoice.directiveName, directive.name) - let multipleChoice = MultipleChoice(from: directive, source: nil, for: bundle, problems: &problems) + let multipleChoice = MultipleChoice(from: directive, source: nil, for: context.inputs, problems: &problems) XCTAssertNotNil(multipleChoice) XCTAssertFalse(problems.isEmpty) problems.first.map { @@ -133,7 +137,7 @@ MultipleChoice @1:1-24:2 title: 'SwiftDocC.MarkupContainer' } - func testValidNoCodeOrMedia() throws { + func testValidNoCodeOrMedia() async throws { let source = """ @MultipleChoice { Question 1 @@ -158,12 +162,12 @@ MultipleChoice @1:1-24:2 title: 'SwiftDocC.MarkupContainer' let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (inputs, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() XCTAssertEqual(MultipleChoice.directiveName, directive.name) - let multipleChoice = MultipleChoice(from: directive, source: nil, for: bundle, problems: &problems) + let multipleChoice = MultipleChoice(from: directive, source: nil, for: inputs, problems: &problems) XCTAssertNotNil(multipleChoice) XCTAssertTrue(problems.isEmpty) @@ -185,7 +189,7 @@ MultipleChoice @1:1-18:2 title: 'SwiftDocC.MarkupContainer' } } - func testValidCode() throws { + func testValidCode() async throws { let source = """ @MultipleChoice { Question 1 @@ -214,12 +218,12 @@ MultipleChoice @1:1-18:2 title: 'SwiftDocC.MarkupContainer' let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (inputs, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() XCTAssertEqual(MultipleChoice.directiveName, directive.name) - let multipleChoice = MultipleChoice(from: directive, source: nil, for: bundle, problems: &problems) + let multipleChoice = MultipleChoice(from: directive, source: nil, for: inputs, problems: &problems) XCTAssertNotNil(multipleChoice) XCTAssertTrue(problems.isEmpty) @@ -246,7 +250,7 @@ MultipleChoice @1:1-22:2 title: 'SwiftDocC.MarkupContainer' } - func testMultipleCorrectAnswers() throws { + func testMultipleCorrectAnswers() async throws { let source = """ @MultipleChoice { Question 1 @@ -274,12 +278,12 @@ MultipleChoice @1:1-22:2 title: 'SwiftDocC.MarkupContainer' let document = Document(parsing: source, options: .parseBlockDirectives) let directive = try XCTUnwrap(document.child(at: 0) as? BlockDirective) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (inputs, _) = try await testBundleAndContext() var problems = [Problem]() XCTAssertEqual(MultipleChoice.directiveName, directive.name) - let multipleChoice = MultipleChoice(from: directive, source: nil, for: bundle, problems: &problems) + let multipleChoice = MultipleChoice(from: directive, source: nil, for: inputs, problems: &problems) XCTAssertNotNil(multipleChoice) XCTAssertEqual(1, problems.count) diff --git a/Tests/SwiftDocCTests/Semantics/Options/OptionsTests.swift b/Tests/SwiftDocCTests/Semantics/Options/OptionsTests.swift index a573683e95..49fe9608f5 100644 --- a/Tests/SwiftDocCTests/Semantics/Options/OptionsTests.swift +++ b/Tests/SwiftDocCTests/Semantics/Options/OptionsTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -15,8 +15,8 @@ import XCTest import Markdown class OptionsTests: XCTestCase { - func testDefaultOptions() throws { - let (problems, options) = try parseDirective(Options.self) { + func testDefaultOptions() async throws { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @@ -33,9 +33,9 @@ class OptionsTests: XCTestCase { XCTAssertEqual(unwrappedOptions.scope, .local) } - func testOptionsParameters() throws { + func testOptionsParameters() async throws { do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options(scope: global) { @@ -48,7 +48,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options(scope: local) { @@ -61,7 +61,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options(scope: global, random: foo) { @@ -79,9 +79,9 @@ class OptionsTests: XCTestCase { } } - func testAutomaticSeeAlso() throws { + func testAutomaticSeeAlso() async throws { do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticSeeAlso(disabled) @@ -94,7 +94,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticSeeAlso(enabled) @@ -107,7 +107,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticSeeAlso(foo) @@ -128,9 +128,9 @@ class OptionsTests: XCTestCase { } } - func testTopicsVisualStyle() throws { + func testTopicsVisualStyle() async throws { do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @TopicsVisualStyle(detailedGrid) @@ -143,7 +143,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @TopicsVisualStyle(compactGrid) @@ -156,7 +156,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @TopicsVisualStyle(list) @@ -169,7 +169,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @TopicsVisualStyle(hidden) @@ -182,7 +182,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticSeeAlso(foo) @@ -203,9 +203,9 @@ class OptionsTests: XCTestCase { } } - func testAutomaticTitleHeading() throws { + func testAutomaticTitleHeading() async throws { do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticTitleHeading(disabled) @@ -218,7 +218,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticTitleHeading(enabled) @@ -231,7 +231,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticTitleHeading(foo) @@ -252,8 +252,8 @@ class OptionsTests: XCTestCase { } } - func testMixOfOptions() throws { - let (problems, options) = try parseDirective(Options.self) { + func testMixOfOptions() async throws { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticTitleHeading(enabled) @@ -271,8 +271,8 @@ class OptionsTests: XCTestCase { XCTAssertEqual(options?.automaticArticleSubheadingEnabled, true) } - func testUnsupportedChild() throws { - let (problems, options) = try parseDirective(Options.self) { + func testUnsupportedChild() async throws { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticTitleHeading(enabled) @@ -295,9 +295,9 @@ class OptionsTests: XCTestCase { ) } - func testAutomaticArticleSubheading() throws { + func testAutomaticArticleSubheading() async throws { do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { } @@ -310,7 +310,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticArticleSubheading(randomArgument) @@ -324,7 +324,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticArticleSubheading(disabled) @@ -338,7 +338,7 @@ class OptionsTests: XCTestCase { } do { - let (problems, options) = try parseDirective(Options.self) { + let (problems, options) = try await parseDirective(Options.self) { """ @Options { @AutomaticArticleSubheading(enabled) diff --git a/Tests/SwiftDocCTests/Semantics/RedirectedTests.swift b/Tests/SwiftDocCTests/Semantics/RedirectedTests.swift index 6ceffb037a..ccf72c18fe 100644 --- a/Tests/SwiftDocCTests/Semantics/RedirectedTests.swift +++ b/Tests/SwiftDocCTests/Semantics/RedirectedTests.swift @@ -15,11 +15,11 @@ import XCTest import Markdown class RedirectedTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Redirected" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let redirected = Redirect(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(redirected) @@ -28,12 +28,12 @@ class RedirectedTests: XCTestCase { XCTAssertEqual("org.swift.docc.HasArgument.from", problems.first?.diagnostic.identifier) } - func testValid() throws { + func testValid() async throws { let oldPath = "/old/path/to/this/page" let source = "@Redirected(from: \(oldPath))" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let redirected = Redirect(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(redirected) @@ -41,12 +41,12 @@ class RedirectedTests: XCTestCase { XCTAssertEqual(redirected?.oldPath.path, oldPath) } - func testExtraArguments() throws { + func testExtraArguments() async throws { let oldPath = "/old/path/to/this/page" let source = "@Redirected(from: \(oldPath), argument: value)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let redirected = Redirect(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(redirected, "Even if there are warnings we can create a Redirected value") @@ -55,7 +55,7 @@ class RedirectedTests: XCTestCase { XCTAssertEqual("org.swift.docc.UnknownArgument", problems.first?.diagnostic.identifier) } - func testExtraDirective() throws { + func testExtraDirective() async throws { let oldPath = "/old/path/to/this/page" let source = """ @Redirected(from: \(oldPath)) { @@ -64,7 +64,7 @@ class RedirectedTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let redirected = Redirect(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(redirected, "Even if there are warnings we can create a Redirected value") @@ -74,7 +74,7 @@ class RedirectedTests: XCTestCase { XCTAssertEqual("org.swift.docc.Redirected.NoInnerContentAllowed", problems.last?.diagnostic.identifier) } - func testExtraContent() throws { + func testExtraContent() async throws { let oldPath = "/old/path/to/this/page" let source = """ @Redirected(from: \(oldPath)) { @@ -83,7 +83,7 @@ class RedirectedTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let redirected = Redirect(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(redirected, "Even if there are warnings we can create a Redirected value") @@ -94,7 +94,7 @@ class RedirectedTests: XCTestCase { // MARK: - Redirect support - func testTechnologySupportsRedirect() throws { + func testTechnologySupportsRedirect() async throws { let source = """ @Tutorials(name: "Technology X") { @Intro(title: "Technology X") { @@ -107,7 +107,7 @@ class RedirectedTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let tutorialTableOfContents = TutorialTableOfContents(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(tutorialTableOfContents, "A tutorial table-of-contents value can be created with a Redirected child.") @@ -118,7 +118,7 @@ class RedirectedTests: XCTestCase { XCTAssert(analyzer.problems.isEmpty, "Expected no problems. Got \(DiagnosticConsoleWriter.formattedDescription(for: analyzer.problems))") } - func testVolumeAndChapterSupportsRedirect() throws { + func testVolumeAndChapterSupportsRedirect() async throws { let source = """ @Volume(name: "Name of this volume") { @Image(source: image.png, alt: image) @@ -139,14 +139,14 @@ class RedirectedTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let volume = Volume(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(volume, "A Volume value can be created with a Redirected child.") XCTAssert(problems.isEmpty, "There shouldn't be any problems. Got:\n\(problems.map { $0.diagnostic.summary })") } - func testTutorialAndSectionsSupportsRedirect() throws { + func testTutorialAndSectionsSupportsRedirect() async throws { let source = """ @Tutorial(time: 20, projectFiles: project.zip) { @Intro(title: "Basic Augmented Reality App") { @@ -204,7 +204,7 @@ class RedirectedTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let tutorial = Tutorial(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(tutorial, "A Tutorial value can be created with a Redirected child.") @@ -215,7 +215,7 @@ class RedirectedTests: XCTestCase { XCTAssert(analyzer.problems.isEmpty, "Expected no problems. Got \(DiagnosticConsoleWriter.formattedDescription(for: analyzer.problems))") } - func testTutorialArticleSupportsRedirect() throws { + func testTutorialArticleSupportsRedirect() async throws { let source = """ @Article(time: 20) { @Intro(title: "Making an Augmented Reality App") { @@ -232,7 +232,7 @@ class RedirectedTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let article = TutorialArticle(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "A TutorialArticle value can be created with a Redirected child.") @@ -243,7 +243,7 @@ class RedirectedTests: XCTestCase { XCTAssert(analyzer.problems.isEmpty, "Expected no problems. Got \(DiagnosticConsoleWriter.formattedDescription(for: analyzer.problems))") } - func testResourcesSupportsRedirect() throws { + func testResourcesSupportsRedirect() async throws { let source = """ @Resources(technology: doc:/TestOverview) { Find the tools and a comprehensive set of resources for creating AR experiences on iOS. @@ -281,14 +281,14 @@ class RedirectedTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let article = Resources(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "A Resources value can be created with a Redirected child.") XCTAssert(problems.isEmpty, "There shouldn't be any problems. Got:\n\(problems.map { $0.diagnostic.summary })") } - func testArticleSupportsRedirect() throws { + func testArticleSupportsRedirect() async throws { let source = """ # Plain article @@ -302,7 +302,7 @@ class RedirectedTests: XCTestCase { ![full width image](referenced-article-image.png) """ let document = Document(parsing: source, options: .parseBlockDirectives) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "An Article value can be created with a Redirected child.") @@ -321,7 +321,7 @@ class RedirectedTests: XCTestCase { ], oldPaths) } - func testArticleSupportsRedirectInMetadata() throws { + func testArticleSupportsRedirectInMetadata() async throws { let source = """ # Plain article @@ -337,7 +337,7 @@ class RedirectedTests: XCTestCase { ![full width image](referenced-article-image.png) """ let document = Document(parsing: source, options: .parseBlockDirectives) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "An Article value can be created with a Redirected child.") @@ -356,7 +356,7 @@ class RedirectedTests: XCTestCase { ], oldPaths) } - func testArticleSupportsBothRedirects() throws { + func testArticleSupportsBothRedirects() async throws { let source = """ # Plain article @@ -374,7 +374,7 @@ class RedirectedTests: XCTestCase { ![full width image](referenced-article-image.png) """ let document = Document(parsing: source, options: .parseBlockDirectives) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let article = Article(from: document, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(article, "An Article value can be created with a Redirected child.") @@ -394,11 +394,11 @@ class RedirectedTests: XCTestCase { ], oldPaths) } - func testIncorrectArgumentLabel() throws { + func testIncorrectArgumentLabel() async throws { let source = "@Redirected(fromURL: /old/path)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let redirected = Redirect(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(redirected) diff --git a/Tests/SwiftDocCTests/Semantics/Reference/LinksTests.swift b/Tests/SwiftDocCTests/Semantics/Reference/LinksTests.swift index f2238b08d1..3a26a0b1f5 100644 --- a/Tests/SwiftDocCTests/Semantics/Reference/LinksTests.swift +++ b/Tests/SwiftDocCTests/Semantics/Reference/LinksTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -15,9 +15,9 @@ import XCTest import Markdown class LinksTests: XCTestCase { - func testMissingBasicRequirements() throws { + func testMissingBasicRequirements() async throws { do { - let (renderedContent, problems, links) = try parseDirective(Links.self, in: "BookLikeContent") { + let (renderedContent, problems, links) = try await parseDirective(Links.self) { """ @Links(visualStyle: compactGrid) """ @@ -34,7 +34,7 @@ class LinksTests: XCTestCase { } do { - let (renderedContent, problems, links) = try parseDirective(Links.self, in: "BookLikeContent") { + let (renderedContent, problems, links) = try await parseDirective(Links.self, in: "BookLikeContent") { """ @Links { - @@ -55,9 +55,9 @@ class LinksTests: XCTestCase { } } - func testInvalidBodyContent() throws { + func testInvalidBodyContent() async throws { do { - let (renderedContent, problems, links) = try parseDirective(Links.self, in: "BookLikeContent") { + let (renderedContent, problems, links) = try await parseDirective(Links.self) { """ @Links(visualStyle: compactGrid) { This is a paragraph of text in 'Links' directive. @@ -82,7 +82,7 @@ class LinksTests: XCTestCase { } do { - let (renderedContent, problems, links) = try parseDirective(Links.self, in: "BookLikeContent") { + let (renderedContent, problems, links) = try await parseDirective(Links.self, in: "BookLikeContent") { """ @Links(visualStyle: compactGrid) { This is a paragraph of text in 'Links' directive. @@ -116,7 +116,7 @@ class LinksTests: XCTestCase { } do { - let (renderedContent, problems, links) = try parseDirective(Links.self, in: "BookLikeContent") { + let (renderedContent, problems, links) = try await parseDirective(Links.self, in: "BookLikeContent") { """ @Links(visualStyle: compactGrid) { - Link with some trailing content. @@ -147,9 +147,9 @@ class LinksTests: XCTestCase { } } - func testLinkResolution() throws { + func testLinkResolution() async throws { do { - let (renderedContent, problems, links) = try parseDirective(Links.self, in: "BookLikeContent") { + let (renderedContent, problems, links) = try await parseDirective(Links.self, in: "BookLikeContent") { """ @Links(visualStyle: compactGrid) { - @@ -185,7 +185,7 @@ class LinksTests: XCTestCase { } do { - let (renderedContent, problems, links) = try parseDirective(Links.self, in: "LegacyBundle_DoNotUseInNewTests") { + let (renderedContent, problems, links) = try await parseDirective(Links.self, in: "LegacyBundle_DoNotUseInNewTests") { """ @Links(visualStyle: compactGrid) { - ``MyKit/MyClass`` diff --git a/Tests/SwiftDocCTests/Semantics/Reference/RowTests.swift b/Tests/SwiftDocCTests/Semantics/Reference/RowTests.swift index 0c13264955..79ffd45816 100644 --- a/Tests/SwiftDocCTests/Semantics/Reference/RowTests.swift +++ b/Tests/SwiftDocCTests/Semantics/Reference/RowTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -15,8 +15,8 @@ import XCTest import Markdown class RowTests: XCTestCase { - func testNoColumns() throws { - let (renderBlockContent, problems, row) = try parseDirective(Row.self) { + func testNoColumns() async throws { + let (renderBlockContent, problems, row) = try await parseDirective(Row.self) { """ @Row """ @@ -36,9 +36,9 @@ class RowTests: XCTestCase { ) } - func testInvalidParameters() throws { + func testInvalidParameters() async throws { do { - let (renderBlockContent, problems, row) = try parseDirective(Row.self) { + let (renderBlockContent, problems, row) = try await parseDirective(Row.self) { """ @Row(columns: 3) { @Column(what: true) { @@ -81,7 +81,7 @@ class RowTests: XCTestCase { } do { - let (_, problems, row) = try parseDirective(Row.self) { + let (_, problems, row) = try await parseDirective(Row.self) { """ @Row(numberOfColumns: 3) { @Column(size: 3) { @@ -107,9 +107,9 @@ class RowTests: XCTestCase { } } - func testInvalidChildren() throws { + func testInvalidChildren() async throws { do { - let (renderBlockContent, problems, row) = try parseDirective(Row.self) { + let (renderBlockContent, problems, row) = try await parseDirective(Row.self) { """ @Row { @Row { @@ -142,7 +142,7 @@ class RowTests: XCTestCase { } do { - let (renderBlockContent, problems, row) = try parseDirective(Row.self) { + let (renderBlockContent, problems, row) = try await parseDirective(Row.self) { """ @Row { @Column { @@ -175,7 +175,7 @@ class RowTests: XCTestCase { } do { - let (renderBlockContent, problems, row) = try parseDirective(Row.self) { + let (renderBlockContent, problems, row) = try await parseDirective(Row.self) { """ @Row { @@ -199,8 +199,8 @@ class RowTests: XCTestCase { } } - func testEmptyColumn() throws { - let (renderBlockContent, problems, row) = try parseDirective(Row.self) { + func testEmptyColumn() async throws { + let (renderBlockContent, problems, row) = try await parseDirective(Row.self) { """ @Row { @Column @@ -236,8 +236,8 @@ class RowTests: XCTestCase { ) } - func testNestedRowAndColumns() throws { - let (renderBlockContent, problems, row) = try parseDirective(Row.self) { + func testNestedRowAndColumns() async throws { + let (renderBlockContent, problems, row) = try await parseDirective(Row.self) { """ @Row { @Column { diff --git a/Tests/SwiftDocCTests/Semantics/Reference/SmallTests.swift b/Tests/SwiftDocCTests/Semantics/Reference/SmallTests.swift index 1500e22ad0..904f330eb2 100644 --- a/Tests/SwiftDocCTests/Semantics/Reference/SmallTests.swift +++ b/Tests/SwiftDocCTests/Semantics/Reference/SmallTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -15,8 +15,8 @@ import XCTest import Markdown class SmallTests: XCTestCase { - func testNoContent() throws { - let (renderBlockContent, problems, small) = try parseDirective(Small.self) { + func testNoContent() async throws { + let (renderBlockContent, problems, small) = try await parseDirective(Small.self) { """ @Small """ @@ -32,9 +32,9 @@ class SmallTests: XCTestCase { XCTAssertEqual(renderBlockContent, []) } - func testHasContent() throws { + func testHasContent() async throws { do { - let (renderBlockContent, problems, small) = try parseDirective(Small.self) { + let (renderBlockContent, problems, small) = try await parseDirective(Small.self) { """ @Small { This is my copyright text. @@ -56,7 +56,7 @@ class SmallTests: XCTestCase { } do { - let (renderBlockContent, problems, small) = try parseDirective(Small.self) { + let (renderBlockContent, problems, small) = try await parseDirective(Small.self) { """ @Small { This is my copyright text. @@ -85,7 +85,7 @@ class SmallTests: XCTestCase { } do { - let (renderBlockContent, problems, small) = try parseDirective(Small.self) { + let (renderBlockContent, problems, small) = try await parseDirective(Small.self) { """ @Small { This is my *formatted* `copyright` **text**. @@ -115,9 +115,9 @@ class SmallTests: XCTestCase { } } - func testEmitsWarningWhenContainsStructuredMarkup() throws { + func testEmitsWarningWhenContainsStructuredMarkup() async throws { do { - let (renderBlockContent, problems, small) = try parseDirective(Small.self) { + let (renderBlockContent, problems, small) = try await parseDirective(Small.self) { """ @Small { This is my copyright text. @@ -143,9 +143,9 @@ class SmallTests: XCTestCase { } } - func testSmallInsideOfColumn() throws { + func testSmallInsideOfColumn() async throws { do { - let (renderBlockContent, problems, row) = try parseDirective(Row.self) { + let (renderBlockContent, problems, row) = try await parseDirective(Row.self) { """ @Row { @Column { diff --git a/Tests/SwiftDocCTests/Semantics/Reference/TabNavigatorTests.swift b/Tests/SwiftDocCTests/Semantics/Reference/TabNavigatorTests.swift index 73b8e928bd..cef67f3caf 100644 --- a/Tests/SwiftDocCTests/Semantics/Reference/TabNavigatorTests.swift +++ b/Tests/SwiftDocCTests/Semantics/Reference/TabNavigatorTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -15,8 +15,8 @@ import XCTest import Markdown class TabNavigatorTests: XCTestCase { - func testNoTabs() throws { - let (renderBlockContent, problems, tabNavigator) = try parseDirective(TabNavigator.self) { + func testNoTabs() async throws { + let (renderBlockContent, problems, tabNavigator) = try await parseDirective(TabNavigator.self) { """ @TabNavigator """ @@ -36,8 +36,8 @@ class TabNavigatorTests: XCTestCase { ) } - func testEmptyTab() throws { - let (renderBlockContent, problems, tabNavigator) = try parseDirective(TabNavigator.self) { + func testEmptyTab() async throws { + let (renderBlockContent, problems, tabNavigator) = try await parseDirective(TabNavigator.self) { """ @TabNavigator { @Tab("hiya") { @@ -63,8 +63,8 @@ class TabNavigatorTests: XCTestCase { } - func testInvalidParametersAndContent() throws { - let (renderBlockContent, problems, tabNavigator) = try parseDirective(TabNavigator.self) { + func testInvalidParametersAndContent() async throws { + let (renderBlockContent, problems, tabNavigator) = try await parseDirective(TabNavigator.self) { """ @TabNavigator(tabs: 3) { @Tab("hi") { @@ -127,8 +127,8 @@ class TabNavigatorTests: XCTestCase { ) } - func testNestedStructuredMarkup() throws { - let (renderBlockContent, problems, tabNavigator) = try parseDirective(TabNavigator.self) { + func testNestedStructuredMarkup() async throws { + let (renderBlockContent, problems, tabNavigator) = try await parseDirective(TabNavigator.self) { """ @TabNavigator { @Tab("hi") { @@ -160,15 +160,11 @@ class TabNavigatorTests: XCTestCase { XCTAssertNotNil(tabNavigator) - // UnresolvedTopicReference warning expected since the reference to the snippet "Snippets/Snippets/MySnippet" - // should fail to resolve here and then nothing would be added to the content. - XCTAssertEqual( - problems, - ["23: warning – org.swift.docc.unresolvedTopicReference"] - ) + // One warning is expected. This empty context has no snippets so the "Snippets/Snippets/MySnippet" path should fail to resolve. + XCTAssertEqual(problems, [ + "23: warning – org.swift.docc.unresolvedSnippetPath" + ]) - - XCTAssertEqual(renderBlockContent.count, 1) XCTAssertEqual( renderBlockContent.first, @@ -202,6 +198,8 @@ class TabNavigatorTests: XCTestCase { "Hey there.", .small(RenderBlockContent.Small(inlineContent: [.text("Hey but small.")])), + + // Because the the "Snippets/Snippets/MySnippet" snippet failed to resolve, we're not including any snippet content here. ] ), ] diff --git a/Tests/SwiftDocCTests/Semantics/ResourcesTests.swift b/Tests/SwiftDocCTests/Semantics/ResourcesTests.swift index 48e218fc2d..e80b876609 100644 --- a/Tests/SwiftDocCTests/Semantics/ResourcesTests.swift +++ b/Tests/SwiftDocCTests/Semantics/ResourcesTests.swift @@ -13,11 +13,11 @@ import XCTest import Markdown class ResourcesTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@\(Resources.directiveName)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let resources = Resources(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(resources) @@ -33,7 +33,7 @@ class ResourcesTests: XCTestCase { XCTAssert(problems.map { $0.diagnostic.severity }.allSatisfy { $0 == .warning }) } - func testValid() throws { + func testValid() async throws { let source = """ @\(Resources.directiveName) { Find the tools and a comprehensive set of resources for creating AR experiences on iOS. @@ -67,7 +67,7 @@ class ResourcesTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let resources = Resources(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(resources) @@ -92,7 +92,7 @@ Resources @1:1-29:2 } } - func testMissingLinksWarning() throws { + func testMissingLinksWarning() async throws { let source = """ @\(Resources.directiveName) { Find the tools and a comprehensive set of resources for creating AR experiences on iOS. @@ -120,7 +120,7 @@ Resources @1:1-29:2 """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let resources = Resources(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(resources) diff --git a/Tests/SwiftDocCTests/Semantics/SectionTests.swift b/Tests/SwiftDocCTests/Semantics/SectionTests.swift index 9852a10b49..fbbd367833 100644 --- a/Tests/SwiftDocCTests/Semantics/SectionTests.swift +++ b/Tests/SwiftDocCTests/Semantics/SectionTests.swift @@ -15,11 +15,11 @@ import XCTest import Markdown class TutorialSectionTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Section" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let section = TutorialSection(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(section) diff --git a/Tests/SwiftDocCTests/Semantics/SnippetTests.swift b/Tests/SwiftDocCTests/Semantics/SnippetTests.swift index cc5750d3fd..a692c5511a 100644 --- a/Tests/SwiftDocCTests/Semantics/SnippetTests.swift +++ b/Tests/SwiftDocCTests/Semantics/SnippetTests.swift @@ -15,8 +15,8 @@ import XCTest import Markdown class SnippetTests: XCTestCase { - func testNoPath() throws { - let (bundle, _) = try testBundleAndContext(named: "Snippets") + func testWarningAboutMissingPathPath() async throws { + let (bundle, _) = try await testBundleAndContext() let source = """ @Snippet() """ @@ -29,8 +29,8 @@ class SnippetTests: XCTestCase { XCTAssertEqual("org.swift.docc.HasArgument.path", problems[0].diagnostic.identifier) } - func testHasInnerContent() throws { - let (bundle, _) = try testBundleAndContext(named: "Snippets") + func testWarningAboutInnerContent() async throws { + let (bundle, _) = try await testBundleAndContext() let source = """ @Snippet(path: "path/to/snippet") { This content shouldn't be here. @@ -45,8 +45,8 @@ class SnippetTests: XCTestCase { XCTAssertEqual("org.swift.docc.Snippet.NoInnerContentAllowed", problems[0].diagnostic.identifier) } - func testLinkResolves() throws { - let (bundle, _) = try testBundleAndContext(named: "Snippets") + func testParsesPath() async throws { + let (bundle, _) = try await testBundleAndContext() let source = """ @Snippet(path: "Test/Snippets/MySnippet") """ @@ -58,23 +58,50 @@ class SnippetTests: XCTestCase { XCTAssertNotNil(snippet) XCTAssertTrue(problems.isEmpty) } + func testLinkResolvesWithoutOptionalPrefix() async throws { + let (_, context) = try await testBundleAndContext(named: "Snippets") + + for snippetPath in [ + "/Test/Snippets/MySnippet", + "Test/Snippets/MySnippet", + "Snippets/MySnippet", + "MySnippet", + ] { + let source = """ + @Snippet(path: "\(snippetPath)") + """ + let document = Document(parsing: source, options: .parseBlockDirectives) + var resolver = MarkupReferenceResolver(context: context, rootReference: try XCTUnwrap(context.soleRootModuleReference)) + _ = resolver.visit(document) + XCTAssertTrue(resolver.problems.isEmpty, "Unexpected problems: \(resolver.problems.map(\.diagnostic.summary))") + } + } - func testUnresolvedSnippetPathDiagnostic() throws { - let (bundle, context) = try testBundleAndContext(named: "Snippets") - let source = """ - @Snippet(path: "Test/Snippets/DoesntExist") - """ - let document = Document(parsing: source, options: .parseBlockDirectives) - var resolver = MarkupReferenceResolver(context: context, bundle: bundle, rootReference: context.rootModules[0]) - _ = resolver.visit(document) - XCTAssertEqual(1, resolver.problems.count) - resolver.problems.first.map { - XCTAssertEqual("org.swift.docc.unresolvedTopicReference", $0.diagnostic.identifier) + func testWarningAboutUnresolvedSnippetPath() async throws { + let (_, context) = try await testBundleAndContext(named: "Snippets") + + for snippetPath in [ + "/Test/Snippets/DoesNotExist", + "Test/Snippets/DoesNotExist", + "Snippets/DoesNotExist", + "DoesNotExist", + ] { + let source = """ + @Snippet(path: "\(snippetPath)") + """ + let document = Document(parsing: source, options: .parseBlockDirectives) + var resolver = MarkupReferenceResolver(context: context, rootReference: try XCTUnwrap(context.soleRootModuleReference)) + _ = resolver.visit(document) + XCTAssertEqual(1, resolver.problems.count) + let problem = try XCTUnwrap(resolver.problems.first) + XCTAssertEqual(problem.diagnostic.identifier, "org.swift.docc.unresolvedSnippetPath") + XCTAssertEqual(problem.diagnostic.summary, "Snippet named 'DoesNotExist' couldn't be found") + XCTAssertEqual(problem.possibleSolutions.count, 0) } } - func testSliceResolves() throws { - let (bundle, _) = try testBundleAndContext(named: "Snippets") + func testParsesSlice() async throws { + let (bundle, _) = try await testBundleAndContext() let source = """ @Snippet(path: "Test/Snippets/MySnippet", slice: "foo") """ diff --git a/Tests/SwiftDocCTests/Semantics/StackTests.swift b/Tests/SwiftDocCTests/Semantics/StackTests.swift index 92665f6594..721db9aa3f 100644 --- a/Tests/SwiftDocCTests/Semantics/StackTests.swift +++ b/Tests/SwiftDocCTests/Semantics/StackTests.swift @@ -11,15 +11,16 @@ import XCTest @testable import SwiftDocC import Markdown +import SwiftDocCTestUtilities class StackTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Stack" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") directive.map { directive in var problems = [Problem]() @@ -34,7 +35,7 @@ class StackTests: XCTestCase { } } - func testValid() throws { + func testValid() async throws { let source = """ @Stack { @ContentAndMedia { @@ -48,7 +49,7 @@ class StackTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") directive.map { directive in var problems = [Problem]() @@ -59,7 +60,7 @@ class StackTests: XCTestCase { } } - func testTooManyChildren() throws { + func testTooManyChildren() async throws { var source = "@Stack {" for _ in 0...Stack.childrenLimit { source += """ @@ -78,12 +79,14 @@ class StackTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ + DataFile(name: "code4.png", data: Data()) + ])) directive.map { directive in var problems = [Problem]() XCTAssertEqual(Stack.directiveName, directive.name) - let stack = Stack(from: directive, source: nil, for: bundle, problems: &problems) + let stack = Stack(from: directive, source: nil, for: context.inputs, problems: &problems) XCTAssertNotNil(stack) XCTAssertEqual(1, problems.count) XCTAssertEqual( diff --git a/Tests/SwiftDocCTests/Semantics/StepTests.swift b/Tests/SwiftDocCTests/Semantics/StepTests.swift index 9fec08b2e2..d8a1b576df 100644 --- a/Tests/SwiftDocCTests/Semantics/StepTests.swift +++ b/Tests/SwiftDocCTests/Semantics/StepTests.swift @@ -11,15 +11,16 @@ import XCTest @testable import SwiftDocC import Markdown +import SwiftDocCTestUtilities class StepTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = """ @Step """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let step = Step(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertEqual([ @@ -32,7 +33,7 @@ class StepTests: XCTestCase { } } - func testValid() throws { + func testValid() async throws { let source = """ @Step { This is the step's content. @@ -46,9 +47,11 @@ class StepTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ + DataFile(name: "test.png", data: Data()) + ])) var problems = [Problem]() - let step = Step(from: directive, source: nil, for: bundle, problems: &problems) + let step = Step(from: directive, source: nil, for: context.inputs, problems: &problems) XCTAssertTrue(problems.isEmpty) XCTAssertNotNil(step) @@ -83,7 +86,7 @@ Step @1:1-9:2 } } - func testExtraneousContent() throws { + func testExtraneousContent() async throws { let source = """ @Step { This is the step's content. @@ -104,7 +107,7 @@ Step @1:1-9:2 """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let step = Step(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertEqual(2, problems.count) diff --git a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift index 5b59db8a3a..a2ea0fdf21 100644 --- a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift +++ b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift @@ -16,8 +16,8 @@ import SwiftDocCTestUtilities class SymbolTests: XCTestCase { - func testDocCommentWithoutArticle() throws { - let (withoutArticle, problems) = try makeDocumentationNodeSymbol( + func testDocCommentWithoutArticle() async throws { + let (withoutArticle, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @@ -25,10 +25,10 @@ class SymbolTests: XCTestCase { - name: A parameter - Returns: Return value """, - articleContent: nil + extensionFileContent: nil ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") XCTAssertEqual(withoutArticle.abstract?.format(), "A cool API to call.") XCTAssertEqual((withoutArticle.discussion?.content ?? []).map { $0.format() }.joined(), "") @@ -43,9 +43,9 @@ class SymbolTests: XCTestCase { XCTAssertNil(withoutArticle.topics) } - func testOverridingInSourceDocumentationWithEmptyArticle() throws { + func testOverridingInSourceDocumentationWithEmptyArticle() async throws { // The article heading—which should always be the symbol link header—is not considered part of the article's content - let (withArticleOverride, problems) = try makeDocumentationNodeSymbol( + let (withArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @@ -53,20 +53,18 @@ class SymbolTests: XCTestCase { - name: A parameter - Returns: Return value """, - articleContent: """ - # Leading heading is ignored - + extensionFileContent: """ @Metadata { @DocumentationExtension(mergeBehavior: override) } """ ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") XCTAssertNil(withArticleOverride.abstract, - "The article overrides—and removes—the abstract from the in-source documenation") + "The article overrides—and removes—the abstract from the in-source documentation") XCTAssertNil(withArticleOverride.discussion, - "The article overries the discussion.") + "The article overrides the discussion.") XCTAssertNil(withArticleOverride.parametersSection?.parameters, "The article overrides—and removes—the parameter section from the in-source documentation.") XCTAssertEqual((withArticleOverride.returnsSection?.content ?? []).map { $0.format() }.joined(), "", @@ -75,8 +73,8 @@ class SymbolTests: XCTestCase { "The article did override the topics section.") } - func testOverridingInSourceDocumentationWithDetailedArticle() throws { - let (withArticleOverride, problems) = try makeDocumentationNodeSymbol( + func testOverridingInSourceDocumentationWithDetailedArticle() async throws { + let (withArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @@ -84,9 +82,7 @@ class SymbolTests: XCTestCase { - name: A parameter - Returns: Return value """, - articleContent: """ - # This is my article - + extensionFileContent: """ @Metadata { @DocumentationExtension(mergeBehavior: override) } @@ -106,15 +102,18 @@ class SymbolTests: XCTestCase { ### Name of a topic - - ``MyKit`` - - ``MyKit/MyClass`` + - ``ModuleName`` + - ``ModuleName/SomeClass`` """ ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.map(\.diagnostic.summary), [ + "Organizing the module 'ModuleName' under 'ModuleName/SomeClass/someMethod(name:)' isn't allowed", + "Organizing 'ModuleName/SomeClass' under 'ModuleName/SomeClass/someMethod(name:)' forms a cycle", + ]) XCTAssertEqual(withArticleOverride.abstract?.plainText, "This is an abstract.", - "The article overrides the abstract from the in-source documenation") + "The article overrides the abstract from the in-source documentation") XCTAssertEqual((withArticleOverride.discussion?.content ?? []).filter({ markup -> Bool in return !(markup.isEmpty) && !(markup is BlockDirective) }).map { $0.format().trimmingLines() }, ["This is a multi-paragraph overview.", "It continues here."], @@ -128,7 +127,7 @@ class SymbolTests: XCTestCase { } XCTAssertEqual((withArticleOverride.returnsSection?.content ?? []).map { $0.format() }, ["Return value is explained here."], - "The article overries—and removes—the return section from the in-source documentation.") + "The article overrides—and removes—the return section from the in-source documentation.") if let topicContent = withArticleOverride.topics?.content, let heading = topicContent.first as? Heading, let topics = topicContent.last as? UnorderedList { XCTAssertEqual(heading.plainText, "Name of a topic") @@ -138,9 +137,9 @@ class SymbolTests: XCTestCase { } } - func testAppendingInSourceDocumentationWithArticle() throws { + func testAppendingInSourceDocumentationWithArticle() async throws { // The article heading—which should always be the symbol link header—is not considered part of the article's content - let (withEmptyArticleOverride, problems) = try makeDocumentationNodeSymbol( + let (withEmptyArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @@ -148,11 +147,9 @@ class SymbolTests: XCTestCase { - name: A parameter - Returns: Return value """, - articleContent: """ - # Leading heading is ignored - """ + extensionFileContent: "" // just the H1 symbol link and no other content ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") XCTAssertEqual(withEmptyArticleOverride.abstract?.format(), "A cool API to call.") XCTAssertEqual((withEmptyArticleOverride.discussion?.content.filter({ markup -> Bool in @@ -169,13 +166,29 @@ class SymbolTests: XCTestCase { XCTAssertNil(withEmptyArticleOverride.topics) } - func testAppendingArticleToInSourceDocumentation() throws { + func testAppendingArticleToInSourceDocumentation() async throws { // When no DocumentationExtension behavior is specified, the default behavior is "append to doc comment". let withAndWithoutAppendConfiguration = ["", "@Metadata { \n @DocumentationExtension(mergeBehavior: append) \n }"] + func verifyExtensionProblem(_ problems: [Problem], forMetadata metadata: String, file: StaticString = #filePath, line: UInt = #line) { + XCTAssertEqual( + !metadata.isEmpty, + problems.map(\.diagnostic.summary).contains("'DocumentationExtension' doesn't change default configuration and has no effect"), + "When there is a \"append\" extension configuration, there should be a warning about it.", + file: file, line: line + ) + } + func verifyProblems(_ problems: [Problem], forMetadata metadata: String, file: StaticString = #filePath, line: UInt = #line) { + verifyExtensionProblem(problems, forMetadata: metadata, file: file, line: line) + XCTAssertEqual(problems.suffix(2).map(\.diagnostic.summary), [ + "Organizing the module 'ModuleName' under 'ModuleName/SomeClass/someMethod(name:)' isn't allowed", + "Organizing 'ModuleName/SomeClass' under 'ModuleName/SomeClass/someMethod(name:)' forms a cycle", + ], file: file, line: line) + } + // Append curation to doc comment for metadata in withAndWithoutAppendConfiguration { - let (withArticleOverride, problems) = try makeDocumentationNodeSymbol( + let (withArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @@ -183,21 +196,18 @@ class SymbolTests: XCTestCase { - name: A parameter - Returns: Return value """, - articleContent: """ - # This is my article - + extensionFileContent: """ \(metadata) ## Topics ### Name of a topic - - ``MyKit`` - - ``MyKit/MyClass`` - + - ``ModuleName`` + - ``ModuleName/SomeClass`` """ ) - XCTAssert(problems.isEmpty) + verifyProblems(problems, forMetadata: metadata) XCTAssertEqual(withArticleOverride.abstract?.format(), "A cool API to call.") XCTAssertEqual((withArticleOverride.discussion?.content.filter({ markup -> Bool in @@ -221,7 +231,7 @@ class SymbolTests: XCTestCase { // Append overview and curation to doc comment for metadata in withAndWithoutAppendConfiguration { - let (withArticleOverride, problems) = try makeDocumentationNodeSymbol( + let (withArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @@ -229,9 +239,7 @@ class SymbolTests: XCTestCase { - name: A parameter - Returns: Return value """, - articleContent: """ - # This is my article - + extensionFileContent: """ \(metadata) This is a multi-paragraph overview. @@ -242,19 +250,18 @@ class SymbolTests: XCTestCase { ### Name of a topic - - ``MyKit`` - - ``MyKit/MyClass`` - + - ``ModuleName`` + - ``ModuleName/SomeClass`` """ ) - XCTAssert(problems.isEmpty) + verifyProblems(problems, forMetadata: metadata) XCTAssertEqual(withArticleOverride.abstract?.format(), "A cool API to call.") XCTAssertEqual((withArticleOverride.discussion?.content.filter({ markup -> Bool in return !(markup.isEmpty) && !(markup is BlockDirective) }) ?? []).map { $0.format().trimmingLines() }, ["This is a multi-paragraph overview.", "It continues here."], - "The article overries—and adds—a discussion.") + "The article overrides—and adds—a discussion.") if let parameter = withArticleOverride.parametersSection?.parameters.first, withArticleOverride.parametersSection?.parameters.count == 1 { XCTAssertEqual(parameter.name, "name") @@ -274,7 +281,7 @@ class SymbolTests: XCTestCase { // Append overview and curation to doc comment for metadata in withAndWithoutAppendConfiguration { - let (withArticleOverride, problems) = try makeDocumentationNodeSymbol( + let (withArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @@ -282,9 +289,7 @@ class SymbolTests: XCTestCase { - name: A parameter - Returns: Return value """, - articleContent: """ - # This is my article - + extensionFileContent: """ \(metadata) This is a multi-paragraph overview. @@ -295,19 +300,18 @@ class SymbolTests: XCTestCase { ### Name of a topic - - ``MyKit`` - - ``MyKit/MyClass`` - + - ``ModuleName`` + - ``ModuleName/SomeClass`` """ ) - XCTAssert(problems.isEmpty) + verifyProblems(problems, forMetadata: metadata) XCTAssertEqual(withArticleOverride.abstract?.format(), "A cool API to call.") XCTAssertEqual((withArticleOverride.discussion?.content.filter({ markup -> Bool in return !(markup.isEmpty) && !(markup is BlockDirective) }) ?? []).map { $0.format().trimmingLines() }, ["This is a multi-paragraph overview.", "It continues here."], - "The article overries—and adds—a discussion.") + "The article overrides—and adds—a discussion.") if let parameter = withArticleOverride.parametersSection?.parameters.first, withArticleOverride.parametersSection?.parameters.count == 1 { XCTAssertEqual(parameter.name, "name") @@ -327,13 +331,11 @@ class SymbolTests: XCTestCase { // Append with only abstract in doc comment for metadata in withAndWithoutAppendConfiguration { - let (withArticleOverride, problems) = try makeDocumentationNodeSymbol( + let (withArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. """, - articleContent: """ - # This is my article - + extensionFileContent: """ \(metadata) This is a multi-paragraph overview. @@ -349,19 +351,18 @@ class SymbolTests: XCTestCase { ### Name of a topic - - ``MyKit`` - - ``MyKit/MyClass`` - + - ``ModuleName`` + - ``ModuleName/SomeClass`` """ ) - XCTAssert(problems.isEmpty) + verifyProblems(problems, forMetadata: metadata) XCTAssertEqual(withArticleOverride.abstract?.format(), "A cool API to call.") XCTAssertEqual((withArticleOverride.discussion?.content.filter({ markup -> Bool in return !(markup.isEmpty) && !(markup is BlockDirective) }) ?? []).map { $0.format().trimmingLines() }, ["This is a multi-paragraph overview.", "It continues here."], - "The article overries—and adds—a discussion.") + "The article overrides—and adds—a discussion.") if let parameter = withArticleOverride.parametersSection?.parameters.first, withArticleOverride.parametersSection?.parameters.count == 1 { XCTAssertEqual(parameter.name, "name") @@ -381,15 +382,13 @@ class SymbolTests: XCTestCase { // Append by extending overview and adding parameters for metadata in withAndWithoutAppendConfiguration { - let (withArticleOverride, problems) = try makeDocumentationNodeSymbol( + let (withArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. The overview stats in the doc comment. """, - articleContent: """ - # This is my article - + extensionFileContent: """ \(metadata) And continues here in the article. @@ -399,14 +398,14 @@ class SymbolTests: XCTestCase { - Returns: Return value """ ) - XCTAssert(problems.isEmpty) + verifyExtensionProblem(problems, forMetadata: metadata) XCTAssertEqual(withArticleOverride.abstract?.format(), "A cool API to call.") XCTAssertEqual((withArticleOverride.discussion?.content.filter({ markup -> Bool in return !(markup.isEmpty) && !(markup is BlockDirective) }) ?? []).map { $0.format().trimmingLines() }, ["The overview stats in the doc comment.", "And continues here in the article."], - "The article overries—and adds—a discussion.") + "The article overrides—and adds—a discussion.") if let parameter = withArticleOverride.parametersSection?.parameters.first, withArticleOverride.parametersSection?.parameters.count == 1 { XCTAssertEqual(parameter.name, "name") @@ -421,7 +420,7 @@ class SymbolTests: XCTestCase { // Append by extending the overview (with parameters in the doc comment) for metadata in withAndWithoutAppendConfiguration { - let (withArticleOverride, problems) = try makeDocumentationNodeSymbol( + let (withArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @@ -431,22 +430,20 @@ class SymbolTests: XCTestCase { - name: A parameter - Returns: Return value """, - articleContent: """ - # This is my article - + extensionFileContent: """ \(metadata) This continues the overview from the doc comment. """ ) - XCTAssert(problems.isEmpty) + verifyExtensionProblem(problems, forMetadata: metadata) XCTAssertEqual(withArticleOverride.abstract?.format(), "A cool API to call.") XCTAssertEqual((withArticleOverride.discussion?.content.filter({ markup -> Bool in return !(markup.isEmpty) && !(markup is BlockDirective) }) ?? []).map { $0.format().trimmingLines() }, ["The overview starts in the doc comment.", "This continues the overview from the doc comment."], - "The article overries—and adds—a discussion.") + "The article overrides—and adds—a discussion.") if let parameter = withArticleOverride.parametersSection?.parameters.first, withArticleOverride.parametersSection?.parameters.count == 1 { XCTAssertEqual(parameter.name, "name") @@ -460,57 +457,51 @@ class SymbolTests: XCTestCase { } } - func testRedirectFromArticle() throws { - let (withRedirectInArticle, problems) = try makeDocumentationNodeSymbol( + func testRedirectFromArticle() async throws { + let (withRedirectInArticle, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. """, - articleContent: """ - # This is my article - + extensionFileContent: """ @Redirected(from: "some/previous/path/to/this/symbol") """ ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") XCTAssertEqual(withRedirectInArticle.redirects?.map { $0.oldPath.absoluteString }, ["some/previous/path/to/this/symbol"]) } - func testWarningWhenDocCommentContainsUnsupportedDirective() throws { - let (withRedirectInArticle, problems) = try makeDocumentationNodeSymbol( + func testWarningWhenDocCommentContainsUnsupportedDirective() async throws { + let (withRedirectInArticle, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @Redirected(from: "some/previous/path/to/this/symbol") """, - articleContent: """ - # This is my article - """ + extensionFileContent: nil ) XCTAssertFalse(problems.isEmpty) XCTAssertEqual(withRedirectInArticle.redirects, nil) XCTAssertEqual(problems.first?.diagnostic.identifier, "org.swift.docc.UnsupportedDocCommentDirective") - XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.line, 3) - XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.column, 1) + XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.line, 14) + XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.column, 18) } - func testNoWarningWhenDocCommentContainsDirective() throws { - let (_, problems) = try makeDocumentationNodeSymbol( + func testNoWarningWhenDocCommentContainsDirective() async throws { + let (_, problems) = try await makeDocumentationNodeSymbol( docComment: """ A cool API to call. @Snippet(from: "Snippets/Snippets/MySnippet") """, - articleContent: """ - # This is my article - """ + extensionFileContent: nil ) - XCTAssertTrue(problems.isEmpty) + XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") } - func testNoWarningWhenDocCommentContainsDoxygen() throws { + func testNoWarningWhenDocCommentContainsDoxygen() async throws { let tempURL = try createTemporaryDirectory() let bundleURL = try Folder(name: "Inheritance.docc", content: [ @@ -520,18 +511,18 @@ class SymbolTests: XCTestCase { subdirectory: "Test Resources")!), ]).write(inside: tempURL) - let (_, _, context) = try loadBundle(from: bundleURL) + let (_, _, context) = try await loadBundle(from: bundleURL) let problems = context.diagnosticEngine.problems XCTAssertEqual(problems.count, 0) } - func testParseDoxygen() throws { + func testParseDoxygen() async throws { let deckKitSymbolGraph = Bundle.module.url( forResource: "DeckKit-Objective-C", withExtension: "symbols.json", subdirectory: "Test Resources" )! - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in try? FileManager.default.copyItem(at: deckKitSymbolGraph, to: url.appendingPathComponent("DeckKit.symbols.json")) } let symbol = try XCTUnwrap(context.documentationCache["c:objc(cs)PlayingCard(cm)newWithRank:ofSuit:"]?.semantic as? Symbol) @@ -548,8 +539,8 @@ class SymbolTests: XCTestCase { XCTAssertEqual(symbol.returnsSection?.content.map({ $0.format() }), ["A new card with the given configuration."]) } - func testUnresolvedReferenceWarningsInDocumentationExtension() throws { - let (url, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + func testUnresolvedReferenceWarningsInDocumentationExtension() async throws { + let (url, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in let myKitDocumentationExtensionComment = """ # ``MyKit/MyClass`` @@ -940,7 +931,7 @@ class SymbolTests: XCTestCase { """) } - func testUnresolvedReferenceWarningsInDocComment() throws { + func testUnresolvedReferenceWarningsInDocComment() async throws { let docComment = """ A cool API to call. @@ -959,7 +950,7 @@ class SymbolTests: XCTestCase { - """ - let (_, _, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in var graph = try JSONDecoder().decode(SymbolGraph.self, from: Data(contentsOf: url.appendingPathComponent("mykit-iOS.symbols.json"))) let myFunctionUSR = "s:5MyKit0A5ClassC10myFunctionyyF" @@ -1036,8 +1027,8 @@ class SymbolTests: XCTestCase { XCTAssertTrue(unresolvedTopicProblems.contains(where: { $0.diagnostic.summary == "No external resolver registered for 'com.test.external'." })) } - func testTopicSectionInDocComment() throws { - let (withArticleOverride, problems) = try makeDocumentationNodeSymbol( + func testTopicSectionInDocComment() async throws { + let (withArticleOverride, problems) = try await makeDocumentationNodeSymbol( docComment: """ This is an abstract. @@ -1054,17 +1045,20 @@ class SymbolTests: XCTestCase { ### Name of a topic - - ``MyKit`` - - ``MyKit/MyClass`` + - ``ModuleName`` + - ``ModuleName/SomeClass`` """, - articleContent: nil + extensionFileContent: nil ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.map(\.diagnostic.summary), [ + "Organizing the module 'ModuleName' under 'ModuleName/SomeClass/someMethod(name:)' isn't allowed", + "Organizing 'ModuleName/SomeClass' under 'ModuleName/SomeClass/someMethod(name:)' forms a cycle", + ]) XCTAssertEqual(withArticleOverride.abstract?.format(), "This is an abstract.", - "The article overrides the abstract from the in-source documenation") + "The article overrides the abstract from the in-source documentation") XCTAssertEqual((withArticleOverride.discussion?.content ?? []).map { $0.detachedFromParent.format() }, ["This is a multi-paragraph overview.", "It continues here."], - "The article overries—and adds—a discussion.") + "The article overrides—and adds—a discussion.") if let parameter = withArticleOverride.parametersSection?.parameters.first, withArticleOverride.parametersSection?.parameters.count == 1 { XCTAssertEqual(parameter.name, "name") @@ -1074,7 +1068,7 @@ class SymbolTests: XCTestCase { } XCTAssertEqual((withArticleOverride.returnsSection?.content ?? []).map { $0.format() }, ["Return value is explained here."], - "The article overries—and removes—the return section from the in-source documentation.") + "The article overrides—and removes—the return section from the in-source documentation.") if let topicContent = withArticleOverride.topics?.content, let heading = topicContent.first as? Heading, let topics = topicContent.last as? UnorderedList { XCTAssertEqual(heading.detachedFromParent.format(), "### Name of a topic") @@ -1085,7 +1079,7 @@ class SymbolTests: XCTestCase { } func testCreatesSourceURLFromLocationMixin() throws { - let identifer = SymbolGraph.Symbol.Identifier(precise: "s:5MyKit0A5ClassC10myFunctionyyF", interfaceLanguage: "swift") + let identifier = SymbolGraph.Symbol.Identifier(precise: "s:5MyKit0A5ClassC10myFunctionyyF", interfaceLanguage: "swift") let names = SymbolGraph.Symbol.Names(title: "", navigator: nil, subHeading: nil, prose: nil) let pathComponents = ["path", "to", "my file.swift"] let range = SymbolGraph.LineList.SourceRange( @@ -1095,7 +1089,7 @@ class SymbolTests: XCTestCase { let line = SymbolGraph.LineList.Line(text: "@Image this is a known directive", range: range) let docComment = SymbolGraph.LineList([line]) let symbol = SymbolGraph.Symbol( - identifier: identifer, + identifier: identifier, names: names, pathComponents: pathComponents, docComment: docComment, @@ -1111,18 +1105,32 @@ class SymbolTests: XCTestCase { XCTAssertEqual(engine.problems.count, 0) } - func testAddingConstraintsToSymbol() throws { - let (withoutArticle, _) = try makeDocumentationNodeSymbol( - docComment: """ + func testAddingConstraintsToSymbol() async throws { + let myFunctionUSR = "s:5MyKit0A5ClassC10myFunctionyyF" + let (_, _, context) = try await testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in + var graph = try JSONDecoder().decode(SymbolGraph.self, from: Data(contentsOf: url.appendingPathComponent("mykit-iOS.symbols.json"))) + + let newDocComment = self.makeLineList( + docComment: """ A cool API to call. - Parameters: - name: A parameter - Returns: Return value """, - articleContent: nil - ) + moduleName: nil, + startOffset: .init(line: 0, character: 0), + url: URL(string: "file:///tmp/File.swift")! + ) + + // The `guard` statement` below will handle the `nil` case by failing the test and + graph.symbols[myFunctionUSR]?.docComment = newDocComment + + let newGraphData = try JSONEncoder().encode(graph) + try newGraphData.write(to: url.appendingPathComponent("mykit-iOS.symbols.json")) + } + let withoutArticle = try XCTUnwrap(context.documentationCache[myFunctionUSR]?.semantic as? Symbol) // The original symbol has 3 generic constraints: // { // "extendedModule": "MyKit", @@ -1199,8 +1207,8 @@ class SymbolTests: XCTestCase { XCTAssertEqual(1, withoutArticle.declarationVariants[trait]!.count) } - func testParsesMetadataDirectiveFromDocComment() throws { - let (node, problems) = try makeDocumentationNodeForSymbol( + func testParsesMetadataDirectiveFromDocComment() async throws { + let (node, problems) = try await makeDocumentationNodeForSymbol( docComment: """ The symbol's abstract. @@ -1208,25 +1216,25 @@ class SymbolTests: XCTestCase { @Available(customOS, introduced: 1.2.3) } """, - articleContent: nil + extensionFileContent: nil ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") let availability = try XCTUnwrap(node.metadata?.availability.first) XCTAssertEqual(availability.platform, .other("customOS")) XCTAssertEqual(availability.introduced.description, "1.2.3") } - func testEmitsWarningsInMetadataDirectives() throws { - let (_, problems) = try makeDocumentationNodeForSymbol( + func testEmitsWarningsInMetadataDirectives() async throws { + let (_, problems) = try await makeDocumentationNodeForSymbol( docComment: """ The symbol's abstract. @Metadata """, - docCommentLineOffset: 12, - articleContent: nil, + docCommentLineStart: 12, + extensionFileContent: nil, diagnosticEngineFilterLevel: .information ) @@ -1234,13 +1242,13 @@ class SymbolTests: XCTestCase { let diagnostic = try XCTUnwrap(problems.first).diagnostic XCTAssertEqual(diagnostic.identifier, "org.swift.docc.Metadata.NoConfiguration") - XCTAssertEqual(diagnostic.source?.absoluteString, "file:///tmp/File.swift") + XCTAssertEqual(diagnostic.source?.path, "/Users/username/path/to/SomeFile.swift") XCTAssertEqual(diagnostic.range?.lowerBound.line, 15) - XCTAssertEqual(diagnostic.range?.lowerBound.column, 1) + XCTAssertEqual(diagnostic.range?.lowerBound.column, 18) } - func testEmitsWarningForDuplicateMetadata() throws { - let (node, problems) = try makeDocumentationNodeForSymbol( + func testEmitsWarningForDuplicateMetadata() async throws { + let (node, problems) = try await makeDocumentationNodeForSymbol( docComment: """ The symbol's abstract. @@ -1248,10 +1256,8 @@ class SymbolTests: XCTestCase { @Available("Platform from doc comment", introduced: 1.2.3) } """, - docCommentLineOffset: 12, - articleContent: """ - # Title - + docCommentLineStart: 12, + extensionFileContent: """ @Metadata { @Available("Platform from documentation extension", introduced: 1.2.3) } @@ -1262,16 +1268,16 @@ class SymbolTests: XCTestCase { let diagnostic = try XCTUnwrap(problems.first).diagnostic XCTAssertEqual(diagnostic.identifier, "org.swift.docc.DuplicateMetadata") - XCTAssertEqual(diagnostic.source?.absoluteString, "file:///tmp/File.swift") + XCTAssertEqual(diagnostic.source?.path, "/Users/username/path/to/SomeFile.swift") XCTAssertEqual(diagnostic.range?.lowerBound.line, 15) - XCTAssertEqual(diagnostic.range?.lowerBound.column, 1) + XCTAssertEqual(diagnostic.range?.lowerBound.column, 18) let availability = try XCTUnwrap(node.metadata?.availability.first) XCTAssertEqual(availability.platform, .other("Platform from documentation extension")) } - func testEmitsWarningsForInvalidMetadataChildrenInDocumentationComments() throws { - let (_, problems) = try makeDocumentationNodeForSymbol( + func testEmitsWarningsForInvalidMetadataChildrenInDocumentationComments() async throws { + let (_, problems) = try await makeDocumentationNodeForSymbol( docComment: """ The symbol's abstract. @@ -1293,7 +1299,7 @@ class SymbolTests: XCTestCase { @Redirected(from: "old/path/to/this/page") } """, - articleContent: nil + extensionFileContent: nil ) XCTAssertEqual( @@ -1311,10 +1317,23 @@ class SymbolTests: XCTestCase { "org.swift.docc.Metadata.InvalidRedirectedInDocumentationComment", ] ) + + // Verify that each problem has exactly one solution to remove the directive + for problem in problems where problem.diagnostic.identifier.hasPrefix("org.swift.docc.Metadata.") { + XCTAssertEqual(problem.possibleSolutions.count, 1, "Each invalid metadata directive should have exactly one solution") + + let solution = try XCTUnwrap(problem.possibleSolutions.first) + XCTAssertTrue(solution.summary.hasPrefix("Remove invalid"), "Solution summary should start with 'Remove invalid'") + XCTAssertEqual(solution.replacements.count, 1, "Solution should have exactly one replacement") + + let replacement = try XCTUnwrap(solution.replacements.first) + XCTAssertEqual(replacement.replacement, "", "Replacement should be empty string to remove the directive") + XCTAssertNotNil(replacement.range, "Replacement should have a valid range") + } } - func testParsesDeprecationSummaryDirectiveFromDocComment() throws { - let (node, problems) = try makeDocumentationNodeForSymbol( + func testParsesDeprecationSummaryDirectiveFromDocComment() async throws { + let (node, problems) = try await makeDocumentationNodeForSymbol( docComment: """ The symbol's abstract. @@ -1322,10 +1341,10 @@ class SymbolTests: XCTestCase { This is the deprecation summary. } """, - articleContent: nil + extensionFileContent: nil ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") XCTAssertEqual( (node.semantic as? Symbol)? @@ -1339,17 +1358,47 @@ class SymbolTests: XCTestCase { ) } - func testAllowsCommentDirectiveInDocComment() throws { - let (_, problems) = try makeDocumentationNodeForSymbol( + func testAllowsCommentDirectiveInDocComment() async throws { + let (_, problems) = try await makeDocumentationNodeForSymbol( docComment: """ The symbol's abstract. @Comment(This is a comment) """, - articleContent: nil + extensionFileContent: nil ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") + } + + func testSolutionForInvalidMetadataDirectiveRemovesDirective() async throws { + let (_, problems) = try await makeDocumentationNodeForSymbol( + docComment: """ + The symbol's abstract. + + @Metadata { + @DisplayName("Invalid Display Name") + } + """, + extensionFileContent: nil + ) + + XCTAssertEqual(problems.count, 1) + let problem = try XCTUnwrap(problems.first) + + XCTAssertEqual(problem.diagnostic.identifier, "org.swift.docc.Metadata.InvalidDisplayNameInDocumentationComment") + XCTAssertEqual(problem.possibleSolutions.count, 1) + + let solution = try XCTUnwrap(problem.possibleSolutions.first) + XCTAssertEqual(solution.summary, "Remove invalid 'DisplayName' directive") + XCTAssertEqual(solution.replacements.count, 1) + + let replacement = try XCTUnwrap(solution.replacements.first) + XCTAssertEqual(replacement.replacement, "", "Replacement should be empty string to remove the directive") + XCTAssertNotNil(replacement.range, "Replacement should have a valid range") + + // Verify that the replacement range covers the expected content + XCTAssertEqual(replacement.range, problem.diagnostic.range, "Replacement range should match the problem's diagnostic range to ensure it removes the entire @DisplayName directive") } // MARK: - Leading Whitespace in Doc Comments @@ -1481,8 +1530,8 @@ class SymbolTests: XCTestCase { XCTAssertEqual(lines.linesWithoutLeadingWhitespace(), linesWithoutLeadingWhitespace) } - func testLeadingWhitespaceInDocComment() throws { - let (semanticWithLeadingWhitespace, problems) = try makeDocumentationNodeSymbol( + func testLeadingWhitespaceInDocComment() async throws { + let (semanticWithLeadingWhitespace, problems) = try await makeDocumentationNodeSymbol( docComment: """ This is an abstract. @@ -1490,9 +1539,9 @@ class SymbolTests: XCTestCase { It continues here. """, - articleContent: nil + extensionFileContent: nil ) - XCTAssert(problems.isEmpty) + XCTAssertEqual(problems.count, 0, "Unexpected problems: \(problems.map(\.diagnostic.summary).sorted())") XCTAssertEqual(semanticWithLeadingWhitespace.abstract?.format(), "This is an abstract.") let lines = semanticWithLeadingWhitespace.discussion?.content.map{ $0.format() } ?? [] let expectedDiscussion = """ @@ -1506,79 +1555,76 @@ class SymbolTests: XCTestCase { // MARK: - Helpers - func makeDocumentationNodeForSymbol( + private func makeDocumentationNodeForSymbol( docComment: String, - docCommentLineOffset: Int = 0, - articleContent: String?, + docCommentLineStart: Int = 11, // an arbitrary non-zero start line + extensionFileContent: String?, diagnosticEngineFilterLevel: DiagnosticSeverity = .warning, file: StaticString = #filePath, line: UInt = #line - ) throws -> (DocumentationNode, [Problem]) { - let myFunctionUSR = "s:5MyKit0A5ClassC10myFunctionyyF" - let (_, bundle, context) = try testBundleAndContext(copying: "LegacyBundle_DoNotUseInNewTests") { url in - var graph = try JSONDecoder().decode(SymbolGraph.self, from: Data(contentsOf: url.appendingPathComponent("mykit-iOS.symbols.json"))) - - let newDocComment = self.makeLineList( - docComment: docComment, - moduleName: nil, - startOffset: .init( - line: docCommentLineOffset, - character: 0 - ), - url: URL(string: "file:///tmp/File.swift")! + ) async throws -> (DocumentationNode, [Problem]) { + let classUSR = "some-class-id" + let methodUSR = "some-method-id" + var catalogContent: [any File] = [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: [ + makeSymbol(id: classUSR, kind: .class, pathComponents: ["SomeClass"]), + + makeSymbol( + id: methodUSR, + kind: .method, + pathComponents: ["SomeClass", "someMethod(name:)"], + docComment: docComment, + location: ( + position: .init(line: docCommentLineStart, character: 17), // an arbitrary non-zero start column/character + url: URL(fileURLWithPath: "/Users/username/path/to/SomeFile.swift") + ), + signature: .init( + parameters: [ + .init(name: "name", externalName: nil, declarationFragments: [ + .init(kind: .internalParameter, spelling: "name", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS") + ], children: []) + ], + returns: [ + .init(kind: .typeIdentifier, spelling: "ReturnValue", preciseIdentifier: "return-value-id") + ] + ) + ) + ], + relationships: [ + .init(source: methodUSR, target: classUSR, kind: .memberOf, targetFallback: nil) + ] + )) + ] + if let extensionFileContent { + catalogContent.append( + TextFile(name: "Extension.md", utf8Content: """ + # ``SomeClass/someMethod(name:)`` + + \(extensionFileContent) + """) ) - - // The `guard` statement` below will handle the `nil` case by failing the test and - graph.symbols[myFunctionUSR]?.docComment = newDocComment - - let newGraphData = try JSONEncoder().encode(graph) - try newGraphData.write(to: url.appendingPathComponent("mykit-iOS.symbols.json")) - } - - guard let original = context.documentationCache[myFunctionUSR], - let unifiedSymbol = original.unifiedSymbol, - let symbolSemantic = original.semantic as? Symbol - else { - XCTFail("Couldn't find the expected symbol", file: (file), line: line) - enum TestHelperError: Error { case missingExpectedMyFuctionSymbol } - throw TestHelperError.missingExpectedMyFuctionSymbol } - let article: Article? = articleContent.flatMap { - let document = Document(parsing: $0, options: .parseBlockDirectives) - var problems = [Problem]() - let article = Article(from: document, source: nil, for: bundle, problems: &problems) - XCTAssertNotNil(article, "The sidecar Article couldn't be created.", file: (file), line: line) - return article - } - - let engine = DiagnosticEngine(filterLevel: diagnosticEngineFilterLevel) + let (_, context) = try await loadBundle(catalog: Folder(name: "unit-test.docc", content: catalogContent), diagnosticFilterLevel: diagnosticEngineFilterLevel) - var node = DocumentationNode( - reference: original.reference, - unifiedSymbol: unifiedSymbol, - moduleData: unifiedSymbol.modules.first!.value, - moduleReference: symbolSemantic.moduleReference - ) - - node.initializeSymbolContent( - documentationExtension: article, - engine: engine, - bundle: bundle - ) + let node = try XCTUnwrap(context.documentationCache[methodUSR], file: file, line: line) - return (node, engine.problems) + return (node, context.problems) } - func makeDocumentationNodeSymbol( + private func makeDocumentationNodeSymbol( docComment: String, - articleContent: String?, + extensionFileContent: String?, file: StaticString = #filePath, line: UInt = #line - ) throws -> (Symbol, [Problem]) { - let (node, problems) = try makeDocumentationNodeForSymbol( + ) async throws -> (Symbol, [Problem]) { + let (node, problems) = try await makeDocumentationNodeForSymbol( docComment: docComment, - articleContent: articleContent, + extensionFileContent: extensionFileContent, file: file, line: line ) diff --git a/Tests/SwiftDocCTests/Semantics/TechnologyTests.swift b/Tests/SwiftDocCTests/Semantics/TechnologyTests.swift index 55a11b9471..e3507fc4b6 100644 --- a/Tests/SwiftDocCTests/Semantics/TechnologyTests.swift +++ b/Tests/SwiftDocCTests/Semantics/TechnologyTests.swift @@ -15,11 +15,11 @@ import XCTest import Markdown class TechnologyTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Tutorials" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let technology = TutorialTableOfContents(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(technology) diff --git a/Tests/SwiftDocCTests/Semantics/TileTests.swift b/Tests/SwiftDocCTests/Semantics/TileTests.swift index ef133b8731..979badc857 100644 --- a/Tests/SwiftDocCTests/Semantics/TileTests.swift +++ b/Tests/SwiftDocCTests/Semantics/TileTests.swift @@ -14,7 +14,7 @@ import Markdown class TileTests: XCTestCase { - func testComplex() throws { + func testComplex() async throws { let directiveNamesAndTitles = [ (Tile.DirectiveNames.documentation, Tile.Semantics.Title.documentation), (Tile.DirectiveNames.sampleCode, Tile.Semantics.Title.sampleCode), @@ -26,7 +26,7 @@ class TileTests: XCTestCase { let source = "@\(directiveName)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let tile = Tile(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(tile) @@ -49,7 +49,7 @@ class TileTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let tile = Tile(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(tile) @@ -62,7 +62,7 @@ class TileTests: XCTestCase { } } - func testGeneric() throws { + func testGeneric() async throws { let directiveNamesAndTitles = [ (Tile.DirectiveNames.downloads, Tile.Semantics.Title.downloads), (Tile.DirectiveNames.videos, Tile.Semantics.Title.videos), @@ -75,7 +75,7 @@ class TileTests: XCTestCase { let source = "@\(directiveName)" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let tile = Tile(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(tile) @@ -97,7 +97,7 @@ class TileTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let tile = Tile(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(tile) @@ -110,7 +110,7 @@ class TileTests: XCTestCase { } } - func testDestination() throws { + func testDestination() async throws { do { let destination = URL(string: "https://www.example.com/documentation/technology")! let source = """ @@ -120,7 +120,7 @@ class TileTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let tile = Tile(from: directive, source: nil, for: bundle, problems: &problems) // Destination is set. @@ -136,7 +136,7 @@ class TileTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let tile = Tile(from: directive, source: nil, for: bundle, problems: &problems) // Destination is nil. @@ -145,11 +145,11 @@ class TileTests: XCTestCase { } } - func testUnknownTile() throws { + func testUnknownTile() async throws { let source = "@UnknownTile" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let tile = Tile(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(tile) diff --git a/Tests/SwiftDocCTests/Semantics/TutorialArticleTests.swift b/Tests/SwiftDocCTests/Semantics/TutorialArticleTests.swift index ea5ccf6fcc..086f0a7488 100644 --- a/Tests/SwiftDocCTests/Semantics/TutorialArticleTests.swift +++ b/Tests/SwiftDocCTests/Semantics/TutorialArticleTests.swift @@ -11,15 +11,16 @@ import XCTest @testable import SwiftDocC import Markdown +import SwiftDocCTestUtilities class TutorialArticleTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Article" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") directive.map { directive in var problems = [Problem]() @@ -35,7 +36,7 @@ class TutorialArticleTests: XCTestCase { } } - func testSimpleNoIntro() throws { + func testSimpleNoIntro() async throws { let source = """ @Article { ## The first section @@ -56,7 +57,7 @@ class TutorialArticleTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") directive.map { directive in var problems = [Problem]() @@ -75,7 +76,7 @@ TutorialArticle @1:1-13:2 } /// Tests that we parse correctly and emit proper warnings when the author provides non-sequential headers. - func testHeaderMix() throws { + func testHeaderMix() async throws { let source = """ @Article { ## The first section @@ -106,7 +107,7 @@ TutorialArticle @1:1-13:2 let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") directive.map { directive in var problems = [Problem]() @@ -124,7 +125,7 @@ TutorialArticle @1:1-23:2 } } - func testIntroAndContent() throws { + func testIntroAndContent() async throws { let source = """ @Article(time: 20) { @@ -155,12 +156,15 @@ TutorialArticle @1:1-23:2 let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ + InfoPlist(identifier: "org.swift.docc.example"), + DataFile(name: "myimage.png", data: Data()) + ])) directive.map { directive in var problems = [Problem]() XCTAssertEqual(TutorialArticle.directiveName, directive.name) - let article = TutorialArticle(from: directive, source: nil, for: bundle, problems: &problems) + let article = TutorialArticle(from: directive, source: nil, for: context.inputs, problems: &problems) XCTAssertNotNil(article) XCTAssertEqual(0, problems.count) article.map { article in @@ -176,7 +180,7 @@ TutorialArticle @1:1-23:2 title: 'Basic Augmented Reality App' time: '20' } } - func testLayouts() throws { + func testLayouts() async throws { let source = """ @Article { @@ -265,12 +269,18 @@ TutorialArticle @1:1-23:2 title: 'Basic Augmented Reality App' time: '20' let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ + InfoPlist(identifier: "org.swift.docc.example"), + DataFile(name: "customize-text-view.png", data: Data()), + DataFile(name: "this-is-leading.png", data: Data()), + DataFile(name: "this-is-trailing.png", data: Data()), + DataFile(name: "this-is-still-trailing.png", data: Data()) + ])) directive.map { directive in var problems = [Problem]() XCTAssertEqual(TutorialArticle.directiveName, directive.name) - let article = TutorialArticle(from: directive, source: nil, for: bundle, problems: &problems) + let article = TutorialArticle(from: directive, source: nil, for: context.inputs, problems: &problems) XCTAssertNotNil(article) XCTAssertEqual(3, problems.count) let arbitraryMarkupProblem = problems.first(where: { $0.diagnostic.identifier == "org.swift.docc.Stack.UnexpectedContent" }) @@ -311,7 +321,7 @@ TutorialArticle @1:1-81:2 } } - func testAssessment() throws { + func testAssessment() async throws { let source = """ @Article(time: 20) { @Intro(title: "Basic Augmented Reality App") { @@ -361,12 +371,15 @@ TutorialArticle @1:1-81:2 let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ + InfoPlist(identifier: "org.swift.docc.example"), + DataFile(name: "myimage.png", data: Data()) + ])) directive.map { directive in var problems = [Problem]() XCTAssertEqual(TutorialArticle.directiveName, directive.name) - let article = TutorialArticle(from: directive, source: nil, for: bundle, problems: &problems) + let article = TutorialArticle(from: directive, source: nil, for: context.inputs, problems: &problems) XCTAssertNotNil(article) XCTAssertEqual(0, problems.count) article.map { article in @@ -393,12 +406,12 @@ TutorialArticle @1:1-42:2 title: 'Basic Augmented Reality App' time: '20' } } - func testAnalyzeNode() throws { + func testAnalyzeNode() async throws { let title = "unreferenced-tutorial" let reference = ResolvedTopicReference(bundleID: "org.swift.docc.TopicGraphTests", path: "/\(title)", sourceLanguage: .swift) let node = TopicGraph.Node(reference: reference, kind: .tutorialTableOfContents, source: .file(url: URL(fileURLWithPath: "/path/to/\(title)")), title: title) - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await testBundleAndContext() context.topicGraph.addNode(node) let engine = DiagnosticEngine() @@ -412,12 +425,12 @@ TutorialArticle @1:1-42:2 title: 'Basic Augmented Reality App' time: '20' XCTAssertTrue(source.isFileURL) } - func testAnalyzeExternalNode() throws { + func testAnalyzeExternalNode() async throws { let title = "unreferenced-tutorial" let reference = ResolvedTopicReference(bundleID: "org.swift.docc.TopicGraphTests", path: "/\(title)", sourceLanguage: .swift) let node = TopicGraph.Node(reference: reference, kind: .tutorialTableOfContents, source: .external, title: title) - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await testBundleAndContext() context.topicGraph.addNode(node) let engine = DiagnosticEngine() @@ -430,14 +443,14 @@ TutorialArticle @1:1-42:2 title: 'Basic Augmented Reality App' time: '20' XCTAssertNil(problem.diagnostic.source) } - func testAnalyzeFragmentNode() throws { + func testAnalyzeFragmentNode() async throws { let title = "unreferenced-tutorial" let url = URL(fileURLWithPath: "/path/to/\(title)") let reference = ResolvedTopicReference(bundleID: "org.swift.docc.TopicGraphTests", path: "/\(title)", sourceLanguage: .swift) let range = SourceLocation(line: 1, column: 1, source: url).. TopicGraph.Node { let url = URL(fileURLWithPath: "/path/to/\(title)") let reference = ResolvedTopicReference(bundleID: "org.swift.docc.TutorialArticleTests", path: "/\(title)", sourceLanguage: .swift) @@ -459,7 +472,7 @@ TutorialArticle @1:1-42:2 title: 'Basic Augmented Reality App' time: '20' return TopicGraph.Node(reference: reference, kind: kind, source: .range(range, url: url) , title: title) } - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await testBundleAndContext() let tutorialArticleNode = node(withTitle: "tutorial-article", ofKind: .tutorialArticle) diff --git a/Tests/SwiftDocCTests/Semantics/TutorialReferenceTests.swift b/Tests/SwiftDocCTests/Semantics/TutorialReferenceTests.swift index 38d10354bb..d7f35bb485 100644 --- a/Tests/SwiftDocCTests/Semantics/TutorialReferenceTests.swift +++ b/Tests/SwiftDocCTests/Semantics/TutorialReferenceTests.swift @@ -13,13 +13,13 @@ import XCTest import Markdown class TutorialReferenceTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = """ @TutorialReference """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let tutorialReference = TutorialReference(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(tutorialReference) @@ -30,14 +30,14 @@ class TutorialReferenceTests: XCTestCase { } } - func testValid() throws { + func testValid() async throws { let tutorialLink = "doc:MyTutorial" let source = """ @TutorialReference(tutorial: "\(tutorialLink)") """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let tutorialReference = TutorialReference(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(tutorialReference) @@ -50,14 +50,14 @@ class TutorialReferenceTests: XCTestCase { XCTAssertTrue(problems.isEmpty) } - func testMissingPath() throws { + func testMissingPath() async throws { let tutorialLink = "doc:" let source = """ @TutorialReference(tutorial: "\(tutorialLink)") """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") var problems = [Problem]() let tutorialReference = TutorialReference(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(tutorialReference) diff --git a/Tests/SwiftDocCTests/Semantics/TutorialTests.swift b/Tests/SwiftDocCTests/Semantics/TutorialTests.swift index fb545712a1..33683991a4 100644 --- a/Tests/SwiftDocCTests/Semantics/TutorialTests.swift +++ b/Tests/SwiftDocCTests/Semantics/TutorialTests.swift @@ -11,15 +11,16 @@ import XCTest @testable import SwiftDocC import Markdown +import SwiftDocCTestUtilities class TutorialTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@Tutorial" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (bundle, _) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") directive.map { directive in var problems = [Problem]() @@ -38,7 +39,7 @@ class TutorialTests: XCTestCase { } } - func testValid() throws { + func testValid() async throws { let source = """ @Tutorial(time: 20) { @XcodeRequirement(title: "Xcode X.Y Beta Z", destination: "https://www.example.com/download") @@ -195,12 +196,24 @@ class TutorialTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ + InfoPlist(identifier: "org.swift.docc.example"), + + DataFile(name: "app.mov", data: Data()), + DataFile(name: "app2.mov", data: Data()), + DataFile(name: "figure1.png", data: Data()), + DataFile(name: "myimage.png", data: Data()), + DataFile(name: "poster.png", data: Data()), + DataFile(name: "screenshot.png", data: Data()), + DataFile(name: "xcode.png", data: Data()), + DataFile(name: "xcode1.png", data: Data()), + DataFile(name: "test.mp4", data: Data()), + ])) directive.map { directive in var problems = [Problem]() XCTAssertEqual(Tutorial.directiveName, directive.name) - let tutorial = Tutorial(from: directive, source: nil, for: bundle, problems: &problems) + let tutorial = Tutorial(from: directive, source: nil, for: context.inputs, problems: &problems) XCTAssertNotNil(tutorial) XCTAssertTrue(problems.isEmpty) tutorial.map { tutorial in @@ -280,7 +293,7 @@ Tutorial @1:1-150:2 projectFiles: nil } } - func testDuplicateSectionTitle() throws { + func testDuplicateSectionTitle() async throws { let source = """ @Tutorial(time: 20) { @XcodeRequirement(title: "Xcode X.Y Beta Z", destination: "https://www.example.com/download") @@ -354,12 +367,18 @@ Tutorial @1:1-150:2 projectFiles: nil let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: [ + InfoPlist(identifier: "org.swift.docc.example"), + + DataFile(name: "myimage.png", data: Data()), + DataFile(name: "poster.png", data: Data()), + DataFile(name: "test.mp4", data: Data()), + ])) directive.map { directive in var problems = [Problem]() XCTAssertEqual(Tutorial.directiveName, directive.name) - let tutorial = Tutorial(from: directive, source: nil, for: bundle, problems: &problems) + let tutorial = Tutorial(from: directive, source: nil, for: context.inputs, problems: &problems) XCTAssertNotNil(tutorial) XCTAssertEqual(1, tutorial?.sections.count) XCTAssertEqual([ @@ -368,12 +387,12 @@ Tutorial @1:1-150:2 projectFiles: nil } } - func testAnalyzeNode() throws { + func testAnalyzeNode() async throws { let title = "unreferenced-tutorial" let reference = ResolvedTopicReference(bundleID: "org.swift.docc.TopicGraphTests", path: "/\(title)", sourceLanguage: .swift) let node = TopicGraph.Node(reference: reference, kind: .tutorialTableOfContents, source: .file(url: URL(fileURLWithPath: "/path/to/\(title)")), title: title) - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await testBundleAndContext() context.topicGraph.addNode(node) let engine = DiagnosticEngine() @@ -387,12 +406,12 @@ Tutorial @1:1-150:2 projectFiles: nil XCTAssertTrue(source.isFileURL) } - func testAnalyzeExternalNode() throws { + func testAnalyzeExternalNode() async throws { let title = "unreferenced-tutorial" let reference = ResolvedTopicReference(bundleID: "org.swift.docc.TopicGraphTests", path: "/\(title)", sourceLanguage: .swift) let node = TopicGraph.Node(reference: reference, kind: .tutorialTableOfContents, source: .external, title: title) - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await testBundleAndContext() context.topicGraph.addNode(node) let engine = DiagnosticEngine() @@ -405,14 +424,14 @@ Tutorial @1:1-150:2 projectFiles: nil XCTAssertNil(problem.diagnostic.source) } - func testAnalyzeFragmentNode() throws { + func testAnalyzeFragmentNode() async throws { let title = "unreferenced-tutorial" let url = URL(fileURLWithPath: "/path/to/\(title)") let reference = ResolvedTopicReference(bundleID: "org.swift.docc.TopicGraphTests", path: "/\(title)", sourceLanguage: .swift) let range = SourceLocation(line: 1, column: 1, source: url).. TopicGraph.Node { let url = URL(fileURLWithPath: "/path/to/\(title)") let reference = ResolvedTopicReference(bundleID: "org.swift.docc.TutorialArticleTests", path: "/\(title)", sourceLanguage: .swift) @@ -434,7 +453,7 @@ Tutorial @1:1-150:2 projectFiles: nil return TopicGraph.Node(reference: reference, kind: kind, source: .range(range, url: url) , title: title) } - let (_, context) = try testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests") + let (_, context) = try await testBundleAndContext() let tutorialNode = node(withTitle: "tutorial-article", ofKind: .tutorial) diff --git a/Tests/SwiftDocCTests/Semantics/VideoMediaTests.swift b/Tests/SwiftDocCTests/Semantics/VideoMediaTests.swift index eacae09d49..3fb78b1444 100644 --- a/Tests/SwiftDocCTests/Semantics/VideoMediaTests.swift +++ b/Tests/SwiftDocCTests/Semantics/VideoMediaTests.swift @@ -14,13 +14,13 @@ import Markdown import SwiftDocCTestUtilities class VideoMediaTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = """ @Video """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let video = VideoMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(video) @@ -31,7 +31,7 @@ class VideoMediaTests: XCTestCase { } } - func testValid() throws { + func testValid() async throws { let videoSource = "/path/to/video" let poster = "/path/to/poster" let source = """ @@ -39,7 +39,7 @@ class VideoMediaTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let video = VideoMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(video) @@ -50,7 +50,7 @@ class VideoMediaTests: XCTestCase { } } - func testSpacesInSourceAndPoster() throws { + func testSpacesInSourceAndPoster() async throws { for videoSource in ["my image.mov", "my%20image.mov"] { let poster = videoSource.replacingOccurrences(of: ".mov", with: ".png") let source = """ @@ -58,7 +58,7 @@ class VideoMediaTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let video = VideoMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(video) @@ -70,14 +70,14 @@ class VideoMediaTests: XCTestCase { } } - func testIncorrectArgumentLabels() throws { + func testIncorrectArgumentLabels() async throws { let source = """ @Video(sourceURL: "/video/path", posterURL: "/poster/path") """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let video = VideoMedia(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(video) @@ -100,10 +100,10 @@ class VideoMediaTests: XCTestCase { DataFile(name: "introvideo~dark.mp4", data: Data()), ]) - func testRenderVideoDirectiveInReferenceMarkup() throws { + func testRenderVideoDirectiveInReferenceMarkup() async throws { do { - let (renderedContent, problems, video, _) = try parseDirective(VideoMedia.self, catalog: catalog) { + let (renderedContent, problems, video, _) = try await parseDirective(VideoMedia.self, catalog: catalog) { """ @Video(source: "introvideo") """ @@ -125,7 +125,7 @@ class VideoMediaTests: XCTestCase { } do { - let (renderedContent, problems, video, _) = try parseDirective(VideoMedia.self, catalog: catalog) { + let (renderedContent, problems, video, _) = try await parseDirective(VideoMedia.self, catalog: catalog) { """ @Video(source: "unknown-video") """ @@ -139,7 +139,7 @@ class VideoMediaTests: XCTestCase { } do { - let (renderedContent, problems, video, _) = try parseDirective(VideoMedia.self, catalog: catalog) { + let (renderedContent, problems, video, _) = try await parseDirective(VideoMedia.self, catalog: catalog) { """ @Video(source: "introvideo", poster: "unknown-poster") """ @@ -161,8 +161,8 @@ class VideoMediaTests: XCTestCase { } } - func testRenderVideoDirectiveWithCaption() throws { - let (renderedContent, problems, video, _) = try parseDirective(VideoMedia.self, catalog: catalog) { + func testRenderVideoDirectiveWithCaption() async throws { + let (renderedContent, problems, video, _) = try await parseDirective(VideoMedia.self, catalog: catalog) { """ @Video(source: "introvideo") { This is my caption. @@ -185,8 +185,8 @@ class VideoMediaTests: XCTestCase { ) } - func testRenderVideoDirectiveWithCaptionAndPosterImage() throws { - let (renderedContent, problems, video, references) = try parseDirective(VideoMedia.self, catalog: catalog) { + func testRenderVideoDirectiveWithCaptionAndPosterImage() async throws { + let (renderedContent, problems, video, references) = try await parseDirective(VideoMedia.self, catalog: catalog) { """ @Video(source: "introvideo", alt: "An introductory video", poster: "introposter") { This is my caption. @@ -217,8 +217,8 @@ class VideoMediaTests: XCTestCase { XCTAssertTrue(references.keys.contains("introposter")) } - func testVideoMediaDiagnosesDeviceFrameByDefault() throws { - let (renderedContent, problems, video, _) = try parseDirective(VideoMedia.self, catalog: catalog) { + func testVideoMediaDiagnosesDeviceFrameByDefault() async throws { + let (renderedContent, problems, video, _) = try await parseDirective(VideoMedia.self, catalog: catalog) { """ @Video(source: "introvideo", deviceFrame: watch) """ @@ -239,10 +239,10 @@ class VideoMediaTests: XCTestCase { ) } - func testRenderVideoDirectiveWithDeviceFrame() throws { + func testRenderVideoDirectiveWithDeviceFrame() async throws { enableFeatureFlag(\.isExperimentalDeviceFrameSupportEnabled) - let (renderedContent, problems, video, _) = try parseDirective(VideoMedia.self, catalog: catalog) { + let (renderedContent, problems, video, _) = try await parseDirective(VideoMedia.self, catalog: catalog) { """ @Video(source: "introvideo", deviceFrame: watch) """ @@ -263,10 +263,10 @@ class VideoMediaTests: XCTestCase { ) } - func testRenderVideoDirectiveWithCaptionAndDeviceFrame() throws { + func testRenderVideoDirectiveWithCaptionAndDeviceFrame() async throws { enableFeatureFlag(\.isExperimentalDeviceFrameSupportEnabled) - let (renderedContent, problems, video, references) = try parseDirective(VideoMedia.self, catalog: catalog) { + let (renderedContent, problems, video, references) = try await parseDirective(VideoMedia.self, catalog: catalog) { """ @Video(source: "introvideo", alt: "An introductory video", poster: "introposter", deviceFrame: laptop) { This is my caption. @@ -297,11 +297,11 @@ class VideoMediaTests: XCTestCase { XCTAssertTrue(references.keys.contains("introposter")) } - func testVideoDirectiveDoesNotResolveImageMedia() throws { + func testVideoDirectiveDoesNotResolveImageMedia() async throws { // The rest of the test in this file will fail if 'introposter' and 'introvideo' // do not exist. We just reverse them here to make sure the reference resolving is // media-type specific. - let (renderedContent, problems, video, _) = try parseDirective(VideoMedia.self, catalog: catalog) { + let (renderedContent, problems, video, _) = try await parseDirective(VideoMedia.self, catalog: catalog) { """ @Video(source: "introposter", poster: "introvideo") """ @@ -320,25 +320,25 @@ class VideoMediaTests: XCTestCase { XCTAssertEqual(renderedContent, []) } - func testVideoDirectiveWithAltText() throws { + func testVideoDirectiveWithAltText() async throws { let source = """ @Video(source: "introvideo", alt: "A short video of a sloth jumping down from a branch and smiling.") """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, context) = try loadBundle( + let (_, context) = try await loadBundle( catalog: Folder(name: "unit-test.docc", content: [ DataFile(name: "introvideo.mov", data: Data()) ]) ) var problems = [Problem]() - let video = VideoMedia(from: directive, source: nil, for: bundle, problems: &problems) + let video = VideoMedia(from: directive, source: nil, for: context.inputs, problems: &problems) let reference = ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "", sourceLanguage: .swift ) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + var translator = RenderNodeTranslator(context: context, identifier: reference) let videoMediaReference = translator.visitVideoMedia(video!) as! RenderReferenceIdentifier let videoMedia = translator.videoReferences[videoMediaReference.identifier] // Check that the video references in the node translator contains the alt text. diff --git a/Tests/SwiftDocCTests/Semantics/VolumeTests.swift b/Tests/SwiftDocCTests/Semantics/VolumeTests.swift index 5c23045a5b..827ab38f7d 100644 --- a/Tests/SwiftDocCTests/Semantics/VolumeTests.swift +++ b/Tests/SwiftDocCTests/Semantics/VolumeTests.swift @@ -14,13 +14,13 @@ import Markdown import SwiftDocCTestUtilities class VolumeTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = """ @Volume """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let volume = Volume(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNil(volume) @@ -33,7 +33,7 @@ class VolumeTests: XCTestCase { ], problems.map { $0.diagnostic.identifier }) } - func testValid() throws { + func testValid() async throws { let name = "Always Be Voluming" let expectedContent = "Here is some content explaining what this volume is." let source = """ @@ -51,7 +51,7 @@ class VolumeTests: XCTestCase { """ let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0)! as! BlockDirective - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() var problems = [Problem]() let volume = Volume(from: directive, source: nil, for: bundle, problems: &problems) XCTAssertNotNil(volume) @@ -62,7 +62,7 @@ class VolumeTests: XCTestCase { } } - func testChapterWithSameName() throws { + func testChapterWithSameName() async throws { let name = "Always Be Voluming" let catalog = Folder(name: "unit-test.docc", content: [ @@ -97,7 +97,7 @@ class VolumeTests: XCTestCase { """) ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) let node = try context.entity( with: ResolvedTopicReference(bundleID: bundle.id, path: "/tutorials/TestOverview", sourceLanguage: .swift) ) diff --git a/Tests/SwiftDocCTests/Semantics/XcodeRequirementTests.swift b/Tests/SwiftDocCTests/Semantics/XcodeRequirementTests.swift index 240d2b780a..f477495bf8 100644 --- a/Tests/SwiftDocCTests/Semantics/XcodeRequirementTests.swift +++ b/Tests/SwiftDocCTests/Semantics/XcodeRequirementTests.swift @@ -13,13 +13,13 @@ import XCTest import Markdown class XcodeRequirementTests: XCTestCase { - func testEmpty() throws { + func testEmpty() async throws { let source = "@XcodeRequirement" let document = Document(parsing: source, options: .parseBlockDirectives) let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() @@ -38,7 +38,7 @@ class XcodeRequirementTests: XCTestCase { } } - func testValid() throws { + func testValid() async throws { let title = "Xcode 10.2 Beta 3" let destination = "https://www.example.com/download" let source = """ @@ -48,7 +48,7 @@ class XcodeRequirementTests: XCTestCase { let directive = document.child(at: 0) as? BlockDirective XCTAssertNotNil(directive) - let (bundle, _) = try testBundleAndContext() + let (bundle, _) = try await testBundleAndContext() directive.map { directive in var problems = [Problem]() diff --git a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json index ec1390b35a..a845cec637 100644 --- a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json +++ b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json @@ -136,6 +136,12 @@ }, "names" : { "title" : "Bar", + "navigator": [ + { + "kind": "identifier", + "spelling": "Bar (objective c)" + } + ], "subHeading" : [ { "kind" : "keyword", @@ -195,7 +201,7 @@ }, { "kind" : "text", - "spelling" : " *)string" + "spelling" : " *)string " }, { "kind" : "identifier", @@ -366,46 +372,13 @@ } ], "subHeading" : [ - { - "kind" : "keyword", - "spelling" : "typedef" - }, - { - "kind" : "text", - "spelling" : " " - }, - { - "kind" : "keyword", - "spelling" : "enum" - }, { "kind" : "text", - "spelling" : " " + "spelling" : "+ " }, { "kind" : "identifier", - "spelling" : "Foo" - }, - { - "kind" : "text", - "spelling" : " : " - }, - { - "kind" : "typeIdentifier", - "spelling" : "NSString", - "preciseIdentifier": "c:@T@NSInteger" - }, - { - "kind": "text", - "spelling": " {\n ...\n} " - }, - { - "kind": "identifier", - "spelling": "Foo" - }, - { - "kind": "text", - "spelling": ";" + "spelling" : "myStringFunction:error:" } ] }, @@ -485,7 +458,7 @@ "text" : "This is the foo's description." } ] - }, + } }, { "accessLevel" : "public", @@ -570,7 +543,7 @@ { "kind" : "typeIdentifier", "spelling" : "NSString", - "preciseIdentifier": "c:@T@NSInteger", + "preciseIdentifier": "c:@T@NSInteger" }, { "kind": "text", @@ -630,7 +603,7 @@ "kind" : "identifier", "spelling" : "first" } - ], + ] }, "pathComponents" : [ "Foo", @@ -677,7 +650,7 @@ "kind" : "identifier", "spelling" : "fourth" } - ], + ] }, "pathComponents" : [ "Foo", @@ -724,7 +697,7 @@ "kind" : "identifier", "spelling" : "second" } - ], + ] }, "pathComponents" : [ "Foo", @@ -771,7 +744,7 @@ "kind" : "identifier", "spelling" : "third" } - ], + ] }, "pathComponents" : [ "Foo", diff --git a/Tests/SwiftDocCTests/Test Resources/SameShapeConstraint.symbols.json b/Tests/SwiftDocCTests/Test Resources/SameShapeConstraint.symbols.json new file mode 100644 index 0000000000..183b29cd2e --- /dev/null +++ b/Tests/SwiftDocCTests/Test Resources/SameShapeConstraint.symbols.json @@ -0,0 +1,321 @@ +{ + "metadata": { + "formatVersion": { + "major": 0, + "minor": 6, + "patch": 0 + }, + "generator": "Apple Swift version 6.2 (swiftlang-6.2.0.19.9 clang-1700.3.19.1)" + }, + "module": { + "name": "SameShapeConstraint", + "platform": { + "architecture": "arm64", + "vendor": "apple", + "operatingSystem": { + "name": "macosx", + "minimumVersion": { + "major": 12, + "minor": 4 + } + } + } + }, + "symbols": [ + { + "kind": { + "identifier": "swift.func", + "displayName": "Function" + }, + "identifier": { + "precise": "s:13SameShapeConstraint8functionyyx_q_txQp_t_tRvzRv_q_Rhzr0_lF", + "interfaceLanguage": "swift" + }, + "pathComponents": [ + "function(_:)" + ], + "names": { + "title": "function(_:)", + "subHeading": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "function" + }, + { + "kind": "text", + "spelling": "((" + }, + { + "kind": "keyword", + "spelling": "repeat" + }, + { + "kind": "text", + "spelling": " (" + }, + { + "kind": "keyword", + "spelling": "each" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "typeIdentifier", + "spelling": "Element0", + "preciseIdentifier": "s:13SameShapeConstraint8functionyyx_q_txQp_t_tRvzRv_q_Rhzr0_lF8Element0L_xmfp" + }, + { + "kind": "text", + "spelling": ", " + }, + { + "kind": "keyword", + "spelling": "each" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "typeIdentifier", + "spelling": "Element1", + "preciseIdentifier": "s:13SameShapeConstraint8functionyyx_q_txQp_t_tRvzRv_q_Rhzr0_lF8Element1L_q_mfp" + }, + { + "kind": "text", + "spelling": ")))" + } + ] + }, + "functionSignature": { + "parameters": [ + { + "name": "_", + "declarationFragments": [ + { + "kind": "identifier", + "spelling": "_" + }, + { + "kind": "text", + "spelling": ": (" + }, + { + "kind": "keyword", + "spelling": "repeat" + }, + { + "kind": "text", + "spelling": " (" + }, + { + "kind": "keyword", + "spelling": "each" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "typeIdentifier", + "spelling": "Element0", + "preciseIdentifier": "s:13SameShapeConstraint8functionyyx_q_txQp_t_tRvzRv_q_Rhzr0_lF8Element0L_xmfp" + }, + { + "kind": "text", + "spelling": ", " + }, + { + "kind": "keyword", + "spelling": "each" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "typeIdentifier", + "spelling": "Element1", + "preciseIdentifier": "s:13SameShapeConstraint8functionyyx_q_txQp_t_tRvzRv_q_Rhzr0_lF8Element1L_q_mfp" + }, + { + "kind": "text", + "spelling": "))" + } + ] + } + ], + "returns": [ + { + "kind": "text", + "spelling": "()" + } + ] + }, + "swiftGenerics": { + "parameters": [ + { + "name": "Element0", + "index": 0, + "depth": 0 + }, + { + "name": "Element1", + "index": 1, + "depth": 0 + } + ], + "constraints": [ + { + "kind": "sameShape", + "lhs": "each Element0", + "rhs": "each Element1" + } + ] + }, + "declarationFragments": [ + { + "kind": "keyword", + "spelling": "func" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "identifier", + "spelling": "function" + }, + { + "kind": "text", + "spelling": "(" + }, + { + "kind": "externalParam", + "spelling": "_" + }, + { + "kind": "text", + "spelling": ": (" + }, + { + "kind": "keyword", + "spelling": "repeat" + }, + { + "kind": "text", + "spelling": " (" + }, + { + "kind": "keyword", + "spelling": "each" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "typeIdentifier", + "spelling": "Element0", + "preciseIdentifier": "s:13SameShapeConstraint8functionyyx_q_txQp_t_tRvzRv_q_Rhzr0_lF8Element0L_xmfp" + }, + { + "kind": "text", + "spelling": ", " + }, + { + "kind": "keyword", + "spelling": "each" + }, + { + "kind": "text", + "spelling": " " + }, + { + "kind": "typeIdentifier", + "spelling": "Element1", + "preciseIdentifier": "s:13SameShapeConstraint8functionyyx_q_txQp_t_tRvzRv_q_Rhzr0_lF8Element1L_q_mfp" + }, + { + "kind": "text", + "spelling": "))) " + }, + { + "kind": "keyword", + "spelling": "where" + }, + { + "kind": "text", + "spelling": " (repeat (each " + }, + { + "kind": "typeIdentifier", + "spelling": "Element0" + }, + { + "kind": "text", + "spelling": ", each " + }, + { + "kind": "typeIdentifier", + "spelling": "Element1" + }, + { + "kind": "text", + "spelling": ")) : Any" + } + ], + "accessLevel": "public", + "location": { + "uri": "file:///path/to/SameShapeConstraint/SwiftClass.swift", + "position": { + "line": 9, + "character": 12 + } + } + } + ], + "relationships": [] +} diff --git a/Tests/SwiftDocCTests/TestRenderNodeOutputConsumer.swift b/Tests/SwiftDocCTests/TestRenderNodeOutputConsumer.swift index d1a1088880..c9eca9a9e9 100644 --- a/Tests/SwiftDocCTests/TestRenderNodeOutputConsumer.swift +++ b/Tests/SwiftDocCTests/TestRenderNodeOutputConsumer.swift @@ -12,7 +12,7 @@ import Foundation @testable import SwiftDocC import XCTest -class TestRenderNodeOutputConsumer: ConvertOutputConsumer { +class TestRenderNodeOutputConsumer: ConvertOutputConsumer, ExternalNodeConsumer { var renderNodes = Synchronized<[RenderNode]>([]) func consume(renderNode: RenderNode) throws { @@ -30,6 +30,7 @@ class TestRenderNodeOutputConsumer: ConvertOutputConsumer { func consume(renderReferenceStore: RenderReferenceStore) throws { } func consume(buildMetadata: BuildMetadata) throws { } func consume(linkResolutionInformation: SerializableLinkResolutionInformation) throws { } + func consume(externalRenderNode: ExternalRenderNode) throws { } } extension TestRenderNodeOutputConsumer { @@ -87,16 +88,14 @@ extension XCTestCase { for bundleName: String, sourceRepository: SourceRepository? = nil, configureBundle: ((URL) throws -> Void)? = nil - ) throws -> TestRenderNodeOutputConsumer { - let (_, bundle, context) = try testBundleAndContext( + ) async throws -> TestRenderNodeOutputConsumer { + let (_, _, context) = try await testBundleAndContext( copying: bundleName, configureBundle: configureBundle ) - let outputConsumer = TestRenderNodeOutputConsumer() _ = try ConvertActionConverter.convert( - bundle: bundle, context: context, outputConsumer: outputConsumer, sourceRepository: sourceRepository, diff --git a/Tests/SwiftDocCTests/Utility/LMDBTests.swift b/Tests/SwiftDocCTests/Utility/LMDBTests.swift index 5461909586..dd50b29604 100644 --- a/Tests/SwiftDocCTests/Utility/LMDBTests.swift +++ b/Tests/SwiftDocCTests/Utility/LMDBTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -258,10 +258,6 @@ final class SwiftLMDBTests: XCTestCase { XCTAssertEqual(value, [1,2,3,4,5,6,7,8,9,10,11,12,13,14]) #endif } - - static var allTests = [ - ("testVersion", testVersion), - ] } // MARK: - Custom Objects diff --git a/Tests/SwiftDocCTests/Utility/ListItemExtractorTests.swift b/Tests/SwiftDocCTests/Utility/ListItemExtractorTests.swift index d653800be1..8639fd62a8 100644 --- a/Tests/SwiftDocCTests/Utility/ListItemExtractorTests.swift +++ b/Tests/SwiftDocCTests/Utility/ListItemExtractorTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -74,8 +74,8 @@ class ListItemExtractorTests: XCTestCase { XCTAssert(extractedTags("- PossibleValue: Missing value name.").possiblePropertyListValues.isEmpty) } - func testExtractingTags() throws { - try assertExtractsRichContentFor( + func testExtractingTags() async throws { + try await assertExtractsRichContentFor( tagName: "Returns", findModelContent: { semantic in semantic.returnsSection?.content @@ -83,7 +83,7 @@ class ListItemExtractorTests: XCTestCase { renderContentSectionTitle: "Return Value" ) - try assertExtractsRichContentFor( + try await assertExtractsRichContentFor( tagName: "Note", isAside: true, findModelContent: { semantic in @@ -105,7 +105,7 @@ class ListItemExtractorTests: XCTestCase { }) ) - try assertExtractsRichContentFor( + try await assertExtractsRichContentFor( tagName: "Precondition", isAside: true, findModelContent: { semantic in @@ -127,7 +127,7 @@ class ListItemExtractorTests: XCTestCase { }) ) - try assertExtractsRichContentFor( + try await assertExtractsRichContentFor( tagName: "Parameter someParameterName", findModelContent: { semantic in semantic.parametersSection?.parameters.first?.contents @@ -140,7 +140,7 @@ class ListItemExtractorTests: XCTestCase { }) ) - try assertExtractsRichContentOutlineFor( + try await assertExtractsRichContentOutlineFor( tagName: "Parameters", findModelContent: { semantic in semantic.parametersSection?.parameters.first?.contents @@ -156,7 +156,7 @@ class ListItemExtractorTests: XCTestCase { // Dictionary and HTTP tags are filtered out from the rendering without symbol information. // These test helpers can't easily set up a bundle that supports general tags, REST tags, and HTTP tags. - try assertExtractsRichContentFor( + try await assertExtractsRichContentFor( tagName: "DictionaryKey someKey", findModelContent: { semantic in semantic.dictionaryKeysSection?.dictionaryKeys.first?.contents @@ -164,7 +164,7 @@ class ListItemExtractorTests: XCTestCase { renderVerification: .skip ) - try assertExtractsRichContentOutlineFor( + try await assertExtractsRichContentOutlineFor( tagName: "DictionaryKeys", findModelContent: { semantic in semantic.dictionaryKeysSection?.dictionaryKeys.first?.contents @@ -172,7 +172,7 @@ class ListItemExtractorTests: XCTestCase { renderVerification: .skip ) - try assertExtractsRichContentFor( + try await assertExtractsRichContentFor( tagName: "HTTPResponse 200", findModelContent: { semantic in semantic.httpResponsesSection?.responses.first?.contents @@ -180,7 +180,7 @@ class ListItemExtractorTests: XCTestCase { renderVerification: .skip ) - try assertExtractsRichContentOutlineFor( + try await assertExtractsRichContentOutlineFor( tagName: "HTTPResponses", findModelContent: { semantic in semantic.httpResponsesSection?.responses.first?.contents @@ -188,7 +188,7 @@ class ListItemExtractorTests: XCTestCase { renderVerification: .skip ) - try assertExtractsRichContentFor( + try await assertExtractsRichContentFor( tagName: "httpBody", findModelContent: { semantic in semantic.httpBodySection?.body.contents @@ -196,7 +196,7 @@ class ListItemExtractorTests: XCTestCase { renderVerification: .skip ) - try assertExtractsRichContentFor( + try await assertExtractsRichContentFor( tagName: "HTTPParameter someParameter", findModelContent: { semantic in semantic.httpParametersSection?.parameters.first?.contents @@ -204,7 +204,7 @@ class ListItemExtractorTests: XCTestCase { renderVerification: .skip ) - try assertExtractsRichContentOutlineFor( + try await assertExtractsRichContentOutlineFor( tagName: "HTTPParameters", findModelContent: { semantic in semantic.httpParametersSection?.parameters.first?.contents @@ -212,7 +212,7 @@ class ListItemExtractorTests: XCTestCase { renderVerification: .skip ) - try assertExtractsRichContentFor( + try await assertExtractsRichContentFor( tagName: "HTTPBodyParameter someParameter", findModelContent: { semantic in semantic.httpBodySection?.body.parameters.first?.contents @@ -220,7 +220,7 @@ class ListItemExtractorTests: XCTestCase { renderVerification: .skip ) - try assertExtractsRichContentOutlineFor( + try await assertExtractsRichContentOutlineFor( tagName: "HTTPBodyParameters", findModelContent: { semantic in semantic.httpBodySection?.body.parameters.first?.contents @@ -237,8 +237,8 @@ class ListItemExtractorTests: XCTestCase { renderContentSectionTitle: String, file: StaticString = #filePath, line: UInt = #line - ) throws { - try assertExtractsRichContentFor( + ) async throws { + try await assertExtractsRichContentFor( tagName: tagName, isAside: false, findModelContent: findModelContent, @@ -270,10 +270,9 @@ class ListItemExtractorTests: XCTestCase { renderVerification: RenderVerification, file: StaticString = #filePath, line: UInt = #line - ) throws { + ) async throws { // Build documentation for a module page with one tagged item with a lot of different - - let (bundle, context) = try loadBundle( + let (_, context) = try await loadBundle( catalog: Folder(name: "Something.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName")), TextFile(name: "Extension.md", utf8Content: """ @@ -303,7 +302,7 @@ class ListItemExtractorTests: XCTestCase { ]) ) - try _assertExtractsRichContentFor(tagName: tagName, findModelContent: findModelContent, renderVerification: renderVerification, isAside: isAside, bundle: bundle, context: context, expectedLogText: """ + try _assertExtractsRichContentFor(tagName: tagName, findModelContent: findModelContent, renderVerification: renderVerification, isAside: isAside, context: context, expectedLogText: """ \u{001B}[1;33mwarning: 'FirstNotFoundSymbol' doesn't exist at '/ModuleName'\u{001B}[0;0m --> /Something.docc/Extension.md:5:\(49+tagName.count)-5:\(68+tagName.count) 3 | Some description of this module. @@ -336,10 +335,9 @@ class ListItemExtractorTests: XCTestCase { renderVerification: RenderVerification, file: StaticString = #filePath, line: UInt = #line - ) throws { + ) async throws { // Build documentation for a module page with one tagged item with a lot of different - - let (bundle, context) = try loadBundle( + let (_, context) = try await loadBundle( catalog: Folder(name: "Something.docc", content: [ JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName")), TextFile(name: "Extension.md", utf8Content: """ @@ -370,7 +368,7 @@ class ListItemExtractorTests: XCTestCase { ]) ) - try _assertExtractsRichContentFor(tagName: tagName, findModelContent: findModelContent, renderVerification: renderVerification, isAside: false, bundle: bundle, context: context, expectedLogText: """ + try _assertExtractsRichContentFor(tagName: tagName, findModelContent: findModelContent, renderVerification: renderVerification, isAside: false, context: context, expectedLogText: """ \u{001B}[1;33mwarning: 'FirstNotFoundSymbol' doesn't exist at '/ModuleName'\u{001B}[0;0m --> /Something.docc/Extension.md:6:60-6:79 4 | @@ -401,7 +399,6 @@ class ListItemExtractorTests: XCTestCase { findModelContent: (Symbol) -> [any Markup]?, renderVerification: RenderVerification, isAside: Bool, - bundle: DocumentationBundle, context: DocumentationContext, expectedLogText: String, file: StaticString = #filePath, @@ -457,7 +454,7 @@ class ListItemExtractorTests: XCTestCase { return } - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let converter = DocumentationNodeConverter(context: context) let renderNode = converter.convert(node) let renderContent = try XCTUnwrap(findRenderContent(renderNode), "Didn't find any rendered content", file: file, line: line) @@ -514,8 +511,8 @@ class ListItemExtractorTests: XCTestCase { // ``` // Inner code block // ``` - .codeListing(.init(syntax: nil, code: ["Inner code block"], metadata: nil)), - + .codeListing(.init(syntax: nil, code: ["Inner code block"], metadata: nil, options: nil)), + // > Warning: Inner aside, with ``ThirdNotFoundSymbol`` link .aside(.init(style: .init(asideKind: .warning), content: [ .paragraph(.init(inlineContent: [ diff --git a/Tests/SwiftDocCTests/Utility/NearMissTests.swift b/Tests/SwiftDocCTests/Utility/NearMissTests.swift index 3f39321cd8..0c9ba39ea4 100644 --- a/Tests/SwiftDocCTests/Utility/NearMissTests.swift +++ b/Tests/SwiftDocCTests/Utility/NearMissTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2022 Apple Inc. and the Swift project authors + Copyright (c) 2022-2025 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 @@ -109,7 +109,6 @@ class NearMissTests: XCTestCase { "init(help:)", "init(exclusivity:help:)", "init(wrappedValue:exclusivity:help:)", - "init(wrappedValue:help:)", // Infrequently Used APIs "init(from:)", "wrappedValue", @@ -122,13 +121,12 @@ class NearMissTests: XCTestCase { checkBestMatches(for: flagSubpaths, against: "wrappedValue", expectedMatches: [ // These need to be in the best matches in this order. "wrappedValue", - "init(wrappedValue:help:)", - "init(wrappedValue:name:help:)", - "init(wrappedValue:exclusivity:help:)", ], acceptedMatches: [ // These don't need to be in the best matches but it's acceptable if they are. // // Most of the string doesn't match but it contains the full 'wrappedValue'. + "init(wrappedValue:name:help:)", + "init(wrappedValue:exclusivity:help:)", "init(wrappedValue:name:inversion:exclusivity:help:)", ]) } diff --git a/Tests/SwiftDocCTests/Utility/SemanticVersion+ComparableTests.swift b/Tests/SwiftDocCTests/Utility/SemanticVersion+ComparableTests.swift index 83875cc263..e68dfba3db 100644 --- a/Tests/SwiftDocCTests/Utility/SemanticVersion+ComparableTests.swift +++ b/Tests/SwiftDocCTests/Utility/SemanticVersion+ComparableTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -10,7 +10,6 @@ import XCTest import SymbolKit -@testable import SwiftDocC class SemanticVersion_ComparableTests: XCTestCase { private typealias Version = SymbolGraph.SemanticVersion diff --git a/Tests/SwiftDocCTests/Utility/XCTestCase+MentionedIn.swift b/Tests/SwiftDocCTests/Utility/XCTestCase+MentionedIn.swift index b11e588e88..a7306396b9 100644 --- a/Tests/SwiftDocCTests/Utility/XCTestCase+MentionedIn.swift +++ b/Tests/SwiftDocCTests/Utility/XCTestCase+MentionedIn.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -15,7 +15,7 @@ import SymbolKit extension XCTestCase { /// Creates a test bundle for testing "Mentioned In" features. - func createMentionedInTestBundle() throws -> (DocumentationBundle, DocumentationContext) { + func createMentionedInTestBundle() async throws -> (DocumentationBundle, DocumentationContext) { let catalog = Folder(name: "MentionedIn.docc", content: [ JSONFile(name: "MentionedIn.symbols.json", content: makeSymbolGraph( moduleName: "MentionedIn", @@ -72,7 +72,7 @@ extension XCTestCase { """), ]) - let (bundle, context) = try loadBundle(catalog: catalog) + let (bundle, context) = try await loadBundle(catalog: catalog) return (bundle, context) } } diff --git a/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift b/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift index 6937a6c6e5..4103f4f92b 100644 --- a/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift +++ b/Tests/SwiftDocCTests/XCTestCase+LoadingTestData.swift @@ -24,7 +24,7 @@ extension XCTestCase { fallbackResolver: (any ConvertServiceFallbackResolver)? = nil, diagnosticEngine: DiagnosticEngine = .init(filterLevel: .hint), configuration: DocumentationContext.Configuration = .init() - ) throws -> (URL, DocumentationBundle, DocumentationContext) { + ) async throws -> (URL, DocumentationBundle, DocumentationContext) { var configuration = configuration configuration.externalDocumentationConfiguration.sources = externalResolvers configuration.externalDocumentationConfiguration.globalSymbolResolver = externalSymbolResolver @@ -34,7 +34,7 @@ extension XCTestCase { let (bundle, dataProvider) = try DocumentationContext.InputsProvider() .inputsAndDataProvider(startingPoint: catalogURL, options: .init()) - let context = try DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) + let context = try await DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) return (catalogURL, bundle, context) } @@ -43,21 +43,30 @@ extension XCTestCase { /// - Parameters: /// - catalog: The directory structure of the documentation catalog /// - otherFileSystemDirectories: Any other directories in the test file system. - /// - diagnosticEngine: The diagnostic engine for the created context. + /// - diagnosticFilterLevel: The minimum severity for diagnostics to emit. + /// - logOutput: An output stream to capture log output from creating the context. /// - configuration: Configuration for the created context. /// - Returns: The loaded documentation bundle and context for the given catalog input. func loadBundle( catalog: Folder, otherFileSystemDirectories: [Folder] = [], - diagnosticEngine: DiagnosticEngine = .init(), + diagnosticFilterLevel: DiagnosticSeverity = .warning, + logOutput: some TextOutputStream = LogHandle.none, configuration: DocumentationContext.Configuration = .init() - ) throws -> (DocumentationBundle, DocumentationContext) { + ) async throws -> (DocumentationBundle, DocumentationContext) { let fileSystem = try TestFileSystem(folders: [catalog] + otherFileSystemDirectories) + let catalogURL = URL(fileURLWithPath: "/\(catalog.name)") + + let diagnosticEngine = DiagnosticEngine(filterLevel: diagnosticFilterLevel) + diagnosticEngine.add(DiagnosticConsoleWriter(logOutput, formattingOptions: [], baseURL: catalogURL, highlight: true, dataProvider: fileSystem)) let (bundle, dataProvider) = try DocumentationContext.InputsProvider(fileManager: fileSystem) - .inputsAndDataProvider(startingPoint: URL(fileURLWithPath: "/\(catalog.name)"), options: .init()) + .inputsAndDataProvider(startingPoint: catalogURL, options: .init()) - let context = try DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) + let context = try await DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration) + + diagnosticEngine.flush() // Write to the logOutput + return (bundle, context) } @@ -77,7 +86,7 @@ extension XCTestCase { diagnosticEngine: DiagnosticEngine = .init(filterLevel: .hint), configuration: DocumentationContext.Configuration = .init(), configureBundle: ((URL) throws -> Void)? = nil - ) throws -> (URL, DocumentationBundle, DocumentationContext) { + ) async throws -> (URL, DocumentationBundle, DocumentationContext) { let sourceURL = try testCatalogURL(named: name) let sourceExists = FileManager.default.fileExists(atPath: sourceURL.path) @@ -96,7 +105,7 @@ extension XCTestCase { // Do any additional setup to the custom bundle - adding, modifying files, etc try configureBundle?(bundleURL) - return try loadBundle( + return try await loadBundle( from: bundleURL, externalResolvers: externalResolvers, externalSymbolResolver: externalSymbolResolver, @@ -111,25 +120,25 @@ extension XCTestCase { externalResolvers: [DocumentationBundle.Identifier: any ExternalDocumentationSource] = [:], fallbackResolver: (any ConvertServiceFallbackResolver)? = nil, configuration: DocumentationContext.Configuration = .init() - ) throws -> (URL, DocumentationBundle, DocumentationContext) { + ) async throws -> (URL, DocumentationBundle, DocumentationContext) { let catalogURL = try testCatalogURL(named: name) - return try loadBundle(from: catalogURL, externalResolvers: externalResolvers, fallbackResolver: fallbackResolver, configuration: configuration) + return try await loadBundle(from: catalogURL, externalResolvers: externalResolvers, fallbackResolver: fallbackResolver, configuration: configuration) } - func testBundleAndContext(named name: String, externalResolvers: [DocumentationBundle.Identifier: any ExternalDocumentationSource] = [:]) throws -> (DocumentationBundle, DocumentationContext) { - let (_, bundle, context) = try testBundleAndContext(named: name, externalResolvers: externalResolvers) + func testBundleAndContext(named name: String, externalResolvers: [DocumentationBundle.Identifier: any ExternalDocumentationSource] = [:]) async throws -> (DocumentationBundle, DocumentationContext) { + let (_, bundle, context) = try await testBundleAndContext(named: name, externalResolvers: externalResolvers) return (bundle, context) } - func renderNode(atPath path: String, fromTestBundleNamed testBundleName: String) throws -> RenderNode { - let (bundle, context) = try testBundleAndContext(named: testBundleName) - let node = try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: path, sourceLanguage: .swift)) - var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) + func renderNode(atPath path: String, fromTestBundleNamed testBundleName: String) async throws -> RenderNode { + let (_, context) = try await testBundleAndContext(named: testBundleName) + let node = try context.entity(with: ResolvedTopicReference(bundleID: context.inputs.id, path: path, sourceLanguage: .swift)) + var translator = RenderNodeTranslator(context: context, identifier: node.reference) return try XCTUnwrap(translator.visit(node.semantic) as? RenderNode) } - func testBundle(named name: String) throws -> DocumentationBundle { - let (bundle, _) = try testBundleAndContext(named: name) + func testBundle(named name: String) async throws -> DocumentationBundle { + let (bundle, _) = try await testBundleAndContext(named: name) return bundle } @@ -140,7 +149,7 @@ extension XCTestCase { return try inputProvider.makeInputs(contentOf: catalogURL, options: .init()) } - func testBundleAndContext() throws -> (bundle: DocumentationBundle, context: DocumentationContext) { + func testBundleAndContext() async throws -> (bundle: DocumentationBundle, context: DocumentationContext) { let bundle = DocumentationBundle( info: DocumentationBundle.Info( displayName: "Test", @@ -152,7 +161,7 @@ extension XCTestCase { miscResourceURLs: [] ) - let context = try DocumentationContext(bundle: bundle, dataProvider: TestFileSystem(folders: [])) + let context = try await DocumentationContext(bundle: bundle, dataProvider: TestFileSystem(folders: [])) return (bundle, context) } @@ -161,8 +170,8 @@ extension XCTestCase { content: () -> String, file: StaticString = #filePath, line: UInt = #line - ) throws -> (problemIdentifiers: [String], directive: Directive?) { - let (bundle, _) = try testBundleAndContext() + ) async throws -> (problemIdentifiers: [String], directive: Directive?) { + let (bundle, _) = try await testBundleAndContext() let source = URL(fileURLWithPath: "/path/to/test-source-\(ProcessInfo.processInfo.globallyUniqueString)") let document = Document(parsing: content(), source: source, options: .parseBlockDirectives) @@ -193,14 +202,14 @@ extension XCTestCase { content: () -> String, file: StaticString = #filePath, line: UInt = #line - ) throws -> ( + ) async throws -> ( renderBlockContent: [RenderBlockContent], problemIdentifiers: [String], directive: Directive?, collectedReferences: [String : any RenderReference] ) { - let (bundle, context) = try loadBundle(catalog: catalog) - return try parseDirective(directive, bundle: bundle, context: context, content: content, file: file, line: line) + let (_, context) = try await loadBundle(catalog: catalog) + return try parseDirective(directive, context: context, content: content, file: file, line: line) } func parseDirective( @@ -209,8 +218,8 @@ extension XCTestCase { content: () -> String, file: StaticString = #filePath, line: UInt = #line - ) throws -> (renderBlockContent: [RenderBlockContent], problemIdentifiers: [String], directive: Directive?) { - let (renderedContent, problems, directive, _) = try parseDirective( + ) async throws -> (renderBlockContent: [RenderBlockContent], problemIdentifiers: [String], directive: Directive?) { + let (renderedContent, problems, directive, _) = try await parseDirective( directive, in: bundleName, content: content, @@ -227,26 +236,38 @@ extension XCTestCase { content: () -> String, file: StaticString = #filePath, line: UInt = #line - ) throws -> ( + ) async throws -> ( renderBlockContent: [RenderBlockContent], problemIdentifiers: [String], directive: Directive?, collectedReferences: [String : any RenderReference] ) { - let bundle: DocumentationBundle let context: DocumentationContext - if let bundleName { - (bundle, context) = try testBundleAndContext(named: bundleName) + (_, context) = try await testBundleAndContext(named: bundleName) } else { - (bundle, context) = try testBundleAndContext() + (_, context) = try await testBundleAndContext() } - return try parseDirective(directive, bundle: bundle, context: context, content: content, file: file, line: line) + return try parseDirective(directive, context: context, content: content, file: file, line: line) + } + + func parseDirective( + _ directive: Directive.Type, + withAvailableAssetNames assetNames: [String], + content: () -> String, + file: StaticString = #filePath, + line: UInt = #line + ) async throws -> (renderBlockContent: [RenderBlockContent], problemIdentifiers: [String], directive: Directive?) { + let (_, context) = try await loadBundle(catalog: Folder(name: "Something.docc", content: assetNames.map { + DataFile(name: $0, data: Data()) + })) + + let (renderedContent, problems, directive, _) = try parseDirective(directive, context: context, content: content) + return (renderedContent, problems, directive) } private func parseDirective( _ directive: Directive.Type, - bundle: DocumentationBundle, context: DocumentationContext, content: () -> String, file: StaticString = #filePath, @@ -264,15 +285,11 @@ extension XCTestCase { let blockDirectiveContainer = try XCTUnwrap(document.child(at: 0) as? BlockDirective, file: file, line: line) - var analyzer = SemanticAnalyzer(source: source, bundle: bundle) + var analyzer = SemanticAnalyzer(source: source, bundle: context.inputs) let result = analyzer.visit(blockDirectiveContainer) context.diagnosticEngine.emit(analyzer.problems) - var referenceResolver = MarkupReferenceResolver( - context: context, - bundle: bundle, - rootReference: bundle.rootReference - ) + var referenceResolver = MarkupReferenceResolver(context: context, rootReference: context.inputs.rootReference) _ = referenceResolver.visit(blockDirectiveContainer) context.diagnosticEngine.emit(referenceResolver.problems) @@ -304,9 +321,8 @@ extension XCTestCase { var contentCompiler = RenderContentCompiler( context: context, - bundle: bundle, identifier: ResolvedTopicReference( - bundleID: bundle.id, + bundleID: context.inputs.id, path: "/test-path-123", sourceLanguage: .swift ) diff --git a/Tests/SwiftDocCUtilitiesTests/ArgumentParsing/MergeSubcommandTests.swift b/Tests/SwiftDocCUtilitiesTests/ArgumentParsing/MergeSubcommandTests.swift index 3260ed4d2d..85731b8e9c 100644 --- a/Tests/SwiftDocCUtilitiesTests/ArgumentParsing/MergeSubcommandTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/ArgumentParsing/MergeSubcommandTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -11,7 +11,6 @@ import XCTest import ArgumentParser @testable import SwiftDocCUtilities -@testable import SwiftDocC import SwiftDocCTestUtilities class MergeSubcommandTests: XCTestCase { diff --git a/Tests/SwiftDocCUtilitiesTests/ConvertActionIndexerTests.swift b/Tests/SwiftDocCUtilitiesTests/ConvertActionIndexerTests.swift index 031e9f6a10..4d3db2bdbf 100644 --- a/Tests/SwiftDocCUtilitiesTests/ConvertActionIndexerTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/ConvertActionIndexerTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -16,12 +16,12 @@ import Foundation class ConvertActionIndexerTests: XCTestCase { // Tests the standalone indexer - func testConvertActionIndexer() throws { - let (bundle, dataProvider) = try DocumentationContext.InputsProvider() + func testConvertActionIndexer() async throws { + let (inputs, dataProvider) = try DocumentationContext.InputsProvider() .inputsAndDataProvider(startingPoint: testCatalogURL(named: "LegacyBundle_DoNotUseInNewTests"), options: .init()) - let context = try DocumentationContext(bundle: bundle, dataProvider: dataProvider) - let converter = DocumentationNodeConverter(bundle: bundle, context: context) + let context = try await DocumentationContext(bundle: inputs, dataProvider: dataProvider) + let converter = DocumentationNodeConverter(context: context) // Add /documentation/MyKit to the index, verify the tree dump do { @@ -29,7 +29,7 @@ class ConvertActionIndexerTests: XCTestCase { let renderNode = try converter.convert(context.entity(with: reference)) let tempIndexURL = try createTemporaryDirectory(named: "index") - let indexer = try ConvertAction.Indexer(outputURL: tempIndexURL, bundleID: bundle.id) + let indexer = try ConvertAction.Indexer(outputURL: tempIndexURL, bundleID: inputs.id) indexer.index(renderNode) XCTAssertTrue(indexer.finalize(emitJSON: false, emitLMDB: false).isEmpty) let treeDump = try XCTUnwrap(indexer.dumpTree()) @@ -54,7 +54,7 @@ class ConvertActionIndexerTests: XCTestCase { let renderNode2 = try converter.convert(context.entity(with: reference2)) let tempIndexURL = try createTemporaryDirectory(named: "index") - let indexer = try ConvertAction.Indexer(outputURL: tempIndexURL, bundleID: bundle.id) + let indexer = try ConvertAction.Indexer(outputURL: tempIndexURL, bundleID: inputs.id) indexer.index(renderNode1) indexer.index(renderNode2) XCTAssertTrue(indexer.finalize(emitJSON: false, emitLMDB: false).isEmpty) diff --git a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift index 1e51718c0c..a4f76bcd53 100644 --- a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift @@ -609,7 +609,7 @@ class ConvertActionTests: XCTestCase { start: nil, source: nil, severity: .warning, - summary: "The 'diagnostics.json' digest file is deprecated and will be removed after 6.2 is released. Pass a `--diagnostics-file ` to specify a custom location where DocC will write a diagnostics JSON file with more information.", + summary: "The 'diagnostics.json' digest file is deprecated and will be removed after 6.3 is released. Pass a `--diagnostics-file ` to specify a custom location where DocC will write a diagnostics JSON file with more information.", explanation: nil, notes: [] ), @@ -672,7 +672,7 @@ class ConvertActionTests: XCTestCase { start: nil, source: nil, severity: .warning, - summary: "The 'diagnostics.json' digest file is deprecated and will be removed after 6.2 is released. Pass a `--diagnostics-file ` to specify a custom location where DocC will write a diagnostics JSON file with more information.", + summary: "The 'diagnostics.json' digest file is deprecated and will be removed after 6.3 is released. Pass a `--diagnostics-file ` to specify a custom location where DocC will write a diagnostics JSON file with more information.", explanation: nil, notes: [] ), @@ -764,7 +764,7 @@ class ConvertActionTests: XCTestCase { start: nil, source: nil, severity: .warning, - summary: "The 'diagnostics.json' digest file is deprecated and will be removed after 6.2 is released. Pass a `--diagnostics-file ` to specify a custom location where DocC will write a diagnostics JSON file with more information.", + summary: "The 'diagnostics.json' digest file is deprecated and will be removed after 6.3 is released. Pass a `--diagnostics-file ` to specify a custom location where DocC will write a diagnostics JSON file with more information.", explanation: nil, notes: [] ), @@ -2898,7 +2898,7 @@ class ConvertActionTests: XCTestCase { ) let (_, context) = try await action.perform(logHandle: .none) - let bundle = try XCTUnwrap(context.bundle, "Should have registered the generated test bundle.") + let bundle = try XCTUnwrap(context.inputs, "Should have registered the generated test bundle.") XCTAssertEqual(bundle.displayName, "MyKit") XCTAssertEqual(bundle.id, "MyKit") } @@ -2976,7 +2976,7 @@ class ConvertActionTests: XCTestCase { ) let (_, context) = try await action.perform(logHandle: .none) - let bundle = try XCTUnwrap(context.bundle, "Should have registered the generated test bundle.") + let bundle = try XCTUnwrap(context.inputs, "Should have registered the generated test bundle.") XCTAssertEqual(bundle.displayName, "Something") XCTAssertEqual(bundle.id, "com.example.test") } @@ -3218,7 +3218,7 @@ private extension LinkDestinationSummary { platforms: platforms, taskGroups: taskGroups, usr: usr, - declarationFragments: nil, + subheadingDeclarationFragments: nil, redirects: redirects, topicImages: topicImages, references: references, diff --git a/Tests/SwiftDocCUtilitiesTests/DirectoryMonitorTests.swift b/Tests/SwiftDocCUtilitiesTests/DirectoryMonitorTests.swift index 10bd4ac73b..efd9b23b15 100644 --- a/Tests/SwiftDocCUtilitiesTests/DirectoryMonitorTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/DirectoryMonitorTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -88,10 +88,13 @@ class DirectoryMonitorTests: XCTestCase { } } - /// - Warning: Please do not overuse this method as it takes 10s of wait time and can potentially slow down running the test suite. private func monitorNoUpdates(url: URL, testBlock: @escaping () throws -> Void, file: StaticString = #filePath, line: UInt = #line) throws { + let fileUpdateEvent = expectation(description: "Unexpectedly triggered an update event") + // This test does not expect any file change events + fileUpdateEvent.isInverted = true + let monitor = try DirectoryMonitor(root: url) { rootURL, url in - XCTFail("Did produce file update event for a hidden file") + fileUpdateEvent.fulfill() } try monitor.start() @@ -99,17 +102,11 @@ class DirectoryMonitorTests: XCTestCase { monitor.stop() } - let didNotTriggerUpdateForHiddenFile = expectation(description: "Doesn't trigger update") - DispatchQueue.global().async { - try? testBlock() - } - - // For the test purposes we assume a file change event will be delivered within generous 10 seconds. - DispatchQueue.global().asyncAfter(deadline: .now() + 10) { - didNotTriggerUpdateForHiddenFile.fulfill() - } - - wait(for: [didNotTriggerUpdateForHiddenFile], timeout: 20) + // For test purposes, we assume a file change event will be delivered within 1.5 seconds. + // This also aligns with the `monitor()` method above, that ensures that file change events + // in tests are received within 1.5 seconds. If this works too eagerly, then the other tests + // in this suite will fail. + waitForExpectations(timeout: 1.5) } #endif diff --git a/Tests/SwiftDocCUtilitiesTests/FolderStructureTests.swift b/Tests/SwiftDocCUtilitiesTests/FolderStructureTests.swift index 8a9c6ad396..148da86252 100644 --- a/Tests/SwiftDocCUtilitiesTests/FolderStructureTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/FolderStructureTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -9,7 +9,6 @@ */ import XCTest -@testable import SwiftDocC import SwiftDocCTestUtilities class FolderStructureTests: XCTestCase { diff --git a/Tests/SwiftDocCUtilitiesTests/HTMLTemplateDirectory.swift b/Tests/SwiftDocCUtilitiesTests/HTMLTemplateDirectory.swift index 09ee4a1797..e3f25527c0 100644 --- a/Tests/SwiftDocCUtilitiesTests/HTMLTemplateDirectory.swift +++ b/Tests/SwiftDocCUtilitiesTests/HTMLTemplateDirectory.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -9,7 +9,6 @@ */ import Foundation -@testable import SwiftDocC import SwiftDocCTestUtilities /// A folder that represents a fake html-build directory for testing. diff --git a/Tests/SwiftDocCUtilitiesTests/MergeActionTests.swift b/Tests/SwiftDocCUtilitiesTests/MergeActionTests.swift index e03f03330d..88acc2a5c2 100644 --- a/Tests/SwiftDocCUtilitiesTests/MergeActionTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/MergeActionTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -659,7 +659,50 @@ class MergeActionTests: XCTestCase { "doc://org.swift.test/documentation/second.json", ]) } - + + func testSingleReferenceOnlyArchiveMerging() async throws { + let fileSystem = try TestFileSystem( + folders: [ + Folder(name: "Output.doccarchive", content: []), + Self.makeArchive( + name: "First", + documentationPages: [ + "First", + "First/SomeClass", + "First/SomeClass/someProperty", + "First/SomeClass/someFunction(:_)", + ], + tutorialPages: [] + ), + ] + ) + + let logStorage = LogHandle.LogStorage() + let action = MergeAction( + archives: [ + URL(fileURLWithPath: "/First.doccarchive"), + ], + landingPageInfo: testLandingPageInfo, + outputURL: URL(fileURLWithPath: "/Output.doccarchive"), + fileManager: fileSystem + ) + + _ = try await action.perform(logHandle: .memory(logStorage)) + XCTAssertEqual(logStorage.text, "", "The action didn't log anything") + + let synthesizedRootNode = try fileSystem.renderNode(atPath: "/Output.doccarchive/data/documentation.json") + XCTAssertEqual(synthesizedRootNode.metadata.title, "Test Landing Page Name") + XCTAssertEqual(synthesizedRootNode.metadata.roleHeading, "Test Landing Page Kind") + XCTAssertEqual(synthesizedRootNode.topicSectionsStyle, .detailedGrid) + XCTAssertEqual(synthesizedRootNode.topicSections.flatMap { [$0.title].compactMap({ $0 }) + $0.identifiers }, [ + // No title + "doc://org.swift.test/documentation/first.json", + ]) + XCTAssertEqual(synthesizedRootNode.references.keys.sorted(), [ + "doc://org.swift.test/documentation/first.json", + ]) + } + func testErrorWhenArchivesContainOverlappingData() async throws { let fileSystem = try TestFileSystem( folders: [ @@ -849,7 +892,7 @@ class MergeActionTests: XCTestCase { let baseOutputDir = URL(fileURLWithPath: "/path/to/some-output-dir") try fileSystem.createDirectory(at: baseOutputDir, withIntermediateDirectories: true) - func convertCatalog(named name: String, file: StaticString = #filePath, line: UInt = #line) throws -> URL { + func convertCatalog(named name: String, file: StaticString = #filePath, line: UInt = #line) async throws -> URL { let catalog = Folder(name: "\(name).docc", content: [ TextFile(name: "\(name).md", utf8Content: """ # My root @@ -876,13 +919,13 @@ class MergeActionTests: XCTestCase { try fileSystem.createDirectory(at: catalogDir, withIntermediateDirectories: true) try fileSystem.addFolder(catalog, basePath: catalogDir.deletingLastPathComponent()) - let (bundle, dataProvider) = try DocumentationContext.InputsProvider(fileManager: fileSystem) + let (inputs, dataProvider) = try DocumentationContext.InputsProvider(fileManager: fileSystem) .inputsAndDataProvider(startingPoint: catalogDir, options: .init()) - XCTAssertEqual(bundle.miscResourceURLs.map(\.lastPathComponent), [ + XCTAssertEqual(inputs.miscResourceURLs.map(\.lastPathComponent), [ "\(name.lowercased())-card.png", ]) - let context = try DocumentationContext(bundle: bundle, dataProvider: dataProvider, configuration: .init()) + let context = try await DocumentationContext(bundle: inputs, dataProvider: dataProvider, configuration: .init()) XCTAssert( context.problems.filter { $0.diagnostic.identifier != "org.swift.docc.SummaryContainsLink" }.isEmpty, @@ -893,11 +936,11 @@ class MergeActionTests: XCTestCase { let outputPath = baseOutputDir.appendingPathComponent("\(name).doccarchive", isDirectory: true) let realTempURL = try createTemporaryDirectory() // The navigator builder only support real file systems - let indexer = try ConvertAction.Indexer(outputURL: realTempURL, bundleID: bundle.id) + let indexer = try ConvertAction.Indexer(outputURL: realTempURL, bundleID: inputs.id) - let outputConsumer = ConvertFileWritingConsumer(targetFolder: outputPath, bundleRootFolder: catalogDir, fileManager: fileSystem, context: context, indexer: indexer, transformForStaticHostingIndexHTML: nil, bundleID: bundle.id) + let outputConsumer = ConvertFileWritingConsumer(targetFolder: outputPath, bundleRootFolder: catalogDir, fileManager: fileSystem, context: context, indexer: indexer, transformForStaticHostingIndexHTML: nil, bundleID: inputs.id) - let convertProblems = try ConvertActionConverter.convert(bundle: bundle, context: context, outputConsumer: outputConsumer, sourceRepository: nil, emitDigest: false, documentationCoverageOptions: .noCoverage) + let convertProblems = try ConvertActionConverter.convert(context: context, outputConsumer: outputConsumer, sourceRepository: nil, emitDigest: false, documentationCoverageOptions: .noCoverage) XCTAssert(convertProblems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary).joined(separator: "\n"))", file: file, line: line) let navigatorProblems = indexer.finalize(emitJSON: true, emitLMDB: false) @@ -933,8 +976,8 @@ class MergeActionTests: XCTestCase { return outputPath } - let firstArchiveDir = try convertCatalog(named: "First") - let secondArchiveDir = try convertCatalog(named: "Second") + let firstArchiveDir = try await convertCatalog(named: "First") + let secondArchiveDir = try await convertCatalog(named: "Second") let combinedArchiveDir = URL(fileURLWithPath: "/Output.doccarchive") let action = MergeAction( @@ -953,14 +996,13 @@ class MergeActionTests: XCTestCase { Output.doccarchive/ ├─ data/ │ ├─ documentation.json - │ ├─ documentation/ - │ │ ├─ first.json - │ │ ├─ first/ - │ │ │ ╰─ article.json - │ │ ├─ second.json - │ │ ╰─ second/ - │ │ ╰─ article.json - │ ╰─ tutorials/ + │ ╰─ documentation/ + │ ├─ first.json + │ ├─ first/ + │ │ ╰─ article.json + │ ├─ second.json + │ ╰─ second/ + │ ╰─ article.json ├─ downloads/ │ ├─ First/ │ ╰─ Second/ diff --git a/Tests/SwiftDocCUtilitiesTests/PreviewServer/PreviewHTTPHandlerTests.swift b/Tests/SwiftDocCUtilitiesTests/PreviewServer/PreviewHTTPHandlerTests.swift index 0a142f0372..95878b0e7f 100644 --- a/Tests/SwiftDocCUtilitiesTests/PreviewServer/PreviewHTTPHandlerTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/PreviewServer/PreviewHTTPHandlerTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,7 +11,6 @@ #if canImport(NIOHTTP1) import Foundation import XCTest -@testable import SwiftDocC @testable import SwiftDocCUtilities import SwiftDocCTestUtilities diff --git a/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/DefaultRequestHandlerTests.swift b/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/DefaultRequestHandlerTests.swift index e0321e1a83..634ba02b9e 100644 --- a/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/DefaultRequestHandlerTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/DefaultRequestHandlerTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,7 +11,6 @@ #if canImport(NIOHTTP1) import Foundation import XCTest -@testable import SwiftDocC @testable import SwiftDocCUtilities import SwiftDocCTestUtilities diff --git a/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/ErrorRequestHandlerTests.swift b/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/ErrorRequestHandlerTests.swift index d73d8d8cc1..552b51e715 100644 --- a/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/ErrorRequestHandlerTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/ErrorRequestHandlerTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,7 +11,6 @@ #if canImport(NIOHTTP1) import Foundation import XCTest -@testable import SwiftDocC @testable import SwiftDocCUtilities import NIO diff --git a/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/FileRequestHandlerTests.swift b/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/FileRequestHandlerTests.swift index 2f7b3202f1..9d4fefa8e2 100644 --- a/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/FileRequestHandlerTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/PreviewServer/RequestHandler/FileRequestHandlerTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -11,7 +11,6 @@ #if canImport(NIOHTTP1) import Foundation import XCTest -@testable import SwiftDocC @testable import SwiftDocCUtilities import SwiftDocCTestUtilities @@ -19,8 +18,6 @@ import NIO import NIOHTTP1 class FileRequestHandlerTests: XCTestCase { - let fileIO = NonBlockingFileIO(threadPool: NIOThreadPool(numberOfThreads: 2)) - private func verifyAsset(root: URL, path: String, body: String, type: String, file: StaticString = #filePath, line: UInt = #line) throws { let request = makeRequestHead(uri: path) let factory = FileRequestHandler(rootURL: root) diff --git a/Tests/SwiftDocCUtilitiesTests/SemanticAnalyzerTests.swift b/Tests/SwiftDocCUtilitiesTests/SemanticAnalyzerTests.swift index 745ce771cb..034b935152 100644 --- a/Tests/SwiftDocCUtilitiesTests/SemanticAnalyzerTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/SemanticAnalyzerTests.swift @@ -54,14 +54,14 @@ class SemanticAnalyzerTests: XCTestCase { InfoPlist(displayName: "TestBundle", identifier: "com.test.example"), ]) - func testDoNotCrashOnInvalidContent() throws { - let (bundle, context) = try loadBundle(catalog: catalogHierarchy) + func testDoNotCrashOnInvalidContent() async throws { + let (bundle, context) = try await loadBundle(catalog: catalogHierarchy) XCTAssertThrowsError(try context.entity(with: ResolvedTopicReference(bundleID: bundle.id, path: "/Oops", sourceLanguage: .swift))) } - func testWarningsAboutDirectiveSupport() throws { - func problemsConvertingTestContent(withFileExtension fileExtension: String) throws -> (unsupportedTopLevelChildProblems: [Problem], missingTopLevelChildProblems: [Problem]) { + func testWarningsAboutDirectiveSupport() async throws { + func problemsConvertingTestContent(withFileExtension fileExtension: String) async throws -> (unsupportedTopLevelChildProblems: [Problem], missingTopLevelChildProblems: [Problem]) { let catalogHierarchy = Folder(name: "SemanticAnalyzerTests.docc", content: [ TextFile(name: "FileWithDirective.\(fileExtension)", utf8Content: """ @Article @@ -73,7 +73,7 @@ class SemanticAnalyzerTests: XCTestCase { """), InfoPlist(displayName: "TestBundle", identifier: "com.test.example"), ]) - let (_, context) = try loadBundle(catalog: catalogHierarchy) + let (_, context) = try await loadBundle(catalog: catalogHierarchy) return ( context.problems.filter({ $0.diagnostic.identifier == "org.swift.docc.unsupportedTopLevelChild" }), @@ -82,7 +82,7 @@ class SemanticAnalyzerTests: XCTestCase { } do { - let problems = try problemsConvertingTestContent(withFileExtension: "md") + let problems = try await problemsConvertingTestContent(withFileExtension: "md") XCTAssertEqual(problems.missingTopLevelChildProblems.count, 0) XCTAssertEqual(problems.unsupportedTopLevelChildProblems.count, 1) @@ -95,7 +95,7 @@ class SemanticAnalyzerTests: XCTestCase { } do { - let problems = try problemsConvertingTestContent(withFileExtension: "tutorial") + let problems = try await problemsConvertingTestContent(withFileExtension: "tutorial") XCTAssertEqual(problems.missingTopLevelChildProblems.count, 1) XCTAssertEqual(problems.unsupportedTopLevelChildProblems.count, 0) @@ -109,8 +109,8 @@ class SemanticAnalyzerTests: XCTestCase { } } - func testDoesNotWarnOnEmptyTutorials() throws { - let (bundle, _) = try loadBundle(catalog: catalogHierarchy) + func testDoesNotWarnOnEmptyTutorials() async throws { + let (bundle, _) = try await loadBundle(catalog: catalogHierarchy) let document = Document(parsing: "", options: .parseBlockDirectives) var analyzer = SemanticAnalyzer(source: URL(string: "/empty.tutorial"), bundle: bundle) diff --git a/Tests/SwiftDocCUtilitiesTests/SignalTests.swift b/Tests/SwiftDocCUtilitiesTests/SignalTests.swift index 564d0bc938..2bcd26c593 100644 --- a/Tests/SwiftDocCUtilitiesTests/SignalTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/SignalTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -9,7 +9,6 @@ */ import XCTest -@testable import SwiftDocCUtilities class SignalTests: XCTestCase { #if os(macOS) || os(Linux) || os(Android) diff --git a/Tests/SwiftDocCUtilitiesTests/StaticHostingBaseTest.swift b/Tests/SwiftDocCUtilitiesTests/StaticHostingBaseTest.swift index 3e50fae03f..430ccd8b9f 100644 --- a/Tests/SwiftDocCUtilitiesTests/StaticHostingBaseTest.swift +++ b/Tests/SwiftDocCUtilitiesTests/StaticHostingBaseTest.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -10,8 +10,6 @@ import XCTest import Foundation -@testable import SwiftDocC -@testable import SwiftDocCUtilities class StaticHostingBaseTests: XCTestCase { diff --git a/Tests/SwiftDocCUtilitiesTests/Utility/FileTests.swift b/Tests/SwiftDocCUtilitiesTests/Utility/FileTests.swift index c8f3f2ead0..cfabe527a4 100644 --- a/Tests/SwiftDocCUtilitiesTests/Utility/FileTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/Utility/FileTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + Copyright (c) 2021-2025 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 @@ -9,8 +9,6 @@ */ import XCTest -@testable import SwiftDocC -@testable import SwiftDocCUtilities import SwiftDocCTestUtilities class FileTests: XCTestCase { diff --git a/Tests/SwiftDocCUtilitiesTests/XCTestCase+LoadingData.swift b/Tests/SwiftDocCUtilitiesTests/XCTestCase+LoadingData.swift index f6da874d1d..cc50e4ca1f 100644 --- a/Tests/SwiftDocCUtilitiesTests/XCTestCase+LoadingData.swift +++ b/Tests/SwiftDocCUtilitiesTests/XCTestCase+LoadingData.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2024 Apple Inc. and the Swift project authors + Copyright (c) 2024-2025 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 @@ -25,13 +25,13 @@ extension XCTestCase { catalog: Folder, otherFileSystemDirectories: [Folder] = [], configuration: DocumentationContext.Configuration = .init() - ) throws -> (DocumentationBundle, DocumentationContext) { + ) async throws -> (DocumentationBundle, DocumentationContext) { let fileSystem = try TestFileSystem(folders: [catalog] + otherFileSystemDirectories) let (bundle, dataProvider) = try DocumentationContext.InputsProvider(fileManager: fileSystem) .inputsAndDataProvider(startingPoint: URL(fileURLWithPath: "/\(catalog.name)"), options: .init()) - let context = try DocumentationContext(bundle: bundle, dataProvider: dataProvider, configuration: configuration) + let context = try await DocumentationContext(bundle: bundle, dataProvider: dataProvider, configuration: configuration) return (bundle, context) } diff --git a/bin/benchmark/Package.swift b/bin/benchmark/Package.swift index 7d52848664..046395b282 100644 --- a/bin/benchmark/Package.swift +++ b/bin/benchmark/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.5 +// swift-tools-version: 6.0 /* This source file is part of the Swift.org open source project @@ -14,7 +14,7 @@ import PackageDescription let package = Package( name: "benchmark", platforms: [ - .macOS(.v12) + .macOS(.v13) ], products: [ .executable( diff --git a/bin/benchmark/Sources/benchmark/BenchmarkResultSeries.swift b/bin/benchmark/Sources/benchmark/BenchmarkResultSeries.swift index c0f2d7582f..008a206e46 100644 --- a/bin/benchmark/Sources/benchmark/BenchmarkResultSeries.swift +++ b/bin/benchmark/Sources/benchmark/BenchmarkResultSeries.swift @@ -77,7 +77,7 @@ struct BenchmarkResultSeries: Codable, Equatable { /// The list of metrics gathered in these benchmark runs. public var metrics: [MetricSeries] - static var empty = BenchmarkResultSeries(platformName: "", timestamp: Date(), doccArguments: [], metrics: []) + static let empty = BenchmarkResultSeries(platformName: "", timestamp: Date(), doccArguments: [], metrics: []) enum Error: Swift.Error, CustomStringConvertible { case addedResultHasDifferentConfiguration diff --git a/bin/benchmark/Sources/benchmark/Commands.swift b/bin/benchmark/Sources/benchmark/Commands.swift index 3c41f28a27..4eb1fff713 100644 --- a/bin/benchmark/Sources/benchmark/Commands.swift +++ b/bin/benchmark/Sources/benchmark/Commands.swift @@ -12,15 +12,16 @@ import ArgumentParser import Foundation @main -struct BenchmarkCommand: ParsableCommand { - static var configuration = CommandConfiguration( +struct BenchmarkCommand: @MainActor ParsableCommand { + @MainActor + static let configuration = CommandConfiguration( abstract: "A utility for performing benchmarks for Swift-DocC.", subcommands: [Measure.self, Diff.self, CompareTo.self, MeasureCommits.self, RenderTrend.self], defaultSubcommand: Measure.self) } let doccProjectRootURL: URL = { - let url = URL(fileURLWithPath: #file) + let url = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() // Commands.swift .deletingLastPathComponent() // benchmark .deletingLastPathComponent() // Sources diff --git a/bin/benchmark/Sources/benchmark/Diff/DiffAnalysis.swift b/bin/benchmark/Sources/benchmark/Diff/DiffAnalysis.swift index cffe3e6c49..a6874188da 100644 --- a/bin/benchmark/Sources/benchmark/Diff/DiffAnalysis.swift +++ b/bin/benchmark/Sources/benchmark/Diff/DiffAnalysis.swift @@ -22,6 +22,7 @@ extension DiffResults { } } + @MainActor static func analyze(before beforeMetric: BenchmarkResultSeries.MetricSeries?, after afterMetric: BenchmarkResultSeries.MetricSeries) throws -> DiffResults.MetricAnalysis { guard let beforeMetric else { return DiffResults.MetricAnalysis( @@ -129,6 +130,7 @@ extension DiffResults { ) } + @MainActor private static func inputBiasDescription(metric: BenchmarkResultSeries.MetricSeries, sampleName: String, numbers: [Double]) -> String { // Turn the single metric series into an array of single values metric series to render the trend bars. @@ -164,6 +166,7 @@ extension DiffResults { } extension BenchmarkResultSeries.MetricSeries.ValueSeries { + @MainActor func formatted() -> String { switch self { case .duration(let value): @@ -190,6 +193,7 @@ extension BenchmarkResultSeries.MetricSeries.ValueSeries { } #if os(macOS) || os(iOS) +@MainActor private let durationFormatter: MeasurementFormatter = { let fmt = MeasurementFormatter() fmt.unitStyle = .medium diff --git a/bin/benchmark/Sources/benchmark/Diff/DiffResultsTable.swift b/bin/benchmark/Sources/benchmark/Diff/DiffResultsTable.swift index be8c39db52..2060053326 100644 --- a/bin/benchmark/Sources/benchmark/Diff/DiffResultsTable.swift +++ b/bin/benchmark/Sources/benchmark/Diff/DiffResultsTable.swift @@ -11,18 +11,40 @@ import Foundation struct DiffResultsTable { - static var columns: [(name: String, width: Int)] = [ - ("Metric", 40), - ("Change", 15), - ("Before", 20), - ("After", 20), - ] - static var totalWidth: Int { - return columns.reduce(0, { $0 + $1.width + 3 }) - 1 + struct Columns { + typealias Column = (name: String, width: Int) + var data: [Column] + + init() { + data = [ + ("Metric", 40), + ("Change", 15), + ("Before", 20), + ("After", 20), + ] + } + + var beforeInfo: Column { + get { data[2] } + set { data[2] = newValue } + } + + var afterInfo: Column { + get { data[3] } + set { data[3] = newValue } + } + + var totalWidth: Int { + data.reduce(0, { $0 + $1.width + 3 }) - 1 + } + + var names: [String] { + data.map { $0.name } + } } private(set) var output: String - init(results: DiffResults) { + init(results: DiffResults, columns: Columns) { var output = "" let allWarnings = results.analysis.flatMap { $0.warnings ?? [] } @@ -30,9 +52,10 @@ struct DiffResultsTable { output += "\(warning)\n" } - output += "┌\(String(repeating: "─", count: Self.totalWidth))┐\n" - output += Self.formattedRow(columnValues: Self.columns.map { $0.name }) - output += "├\(String(repeating: "─", count: Self.totalWidth))┤\n" + let totalWidth = columns.totalWidth + output += "┌\(String(repeating: "─", count: totalWidth))┐\n" + output += Self.formattedRow(columns: columns) + output += "├\(String(repeating: "─", count: totalWidth))┤\n" var footnoteCounter = 0 @@ -61,10 +84,16 @@ struct DiffResultsTable { footnoteCounter += footnotes.count } - output += Self.formattedRow(columnValues: [analysis.metricName, change, analysis.before ?? "-", analysis.after], colorInfo: colorInfo) + var analysisColumns = columns + analysisColumns.data[0].name = analysis.metricName + analysisColumns.data[1].name = change + analysisColumns.data[2].name = analysis.before ?? "-" + analysisColumns.data[3].name = analysis.after + + output += Self.formattedRow(columns: analysisColumns, colorInfo: colorInfo) } - output += "└\(String(repeating: "─", count: Self.totalWidth))┘\n" + output += "└\(String(repeating: "─", count: totalWidth))┘\n" let allFootnotes = results.analysis.flatMap { $0.footnotes ?? [] } if !allFootnotes.isEmpty { @@ -117,9 +146,9 @@ struct DiffResultsTable { let upTo: String.Index } - private static func formattedRow(columnValues: [String], colorInfo: [ColumnColorInfo] = []) -> String { - let values: [String] = columnValues.enumerated().map { (index, value) in - let row = value.padding(toLength: Self.columns[index].width, withPad: " ", startingAt: 0) + private static func formattedRow(columns: Columns, colorInfo: [ColumnColorInfo] = []) -> String { + let values: [String] = columns.names.enumerated().map { (index, value) in + let row = value.padding(toLength: columns.data[index].width, withPad: " ", startingAt: 0) if let colorInfo = colorInfo.first(where: { $0.index == index }) { return String(row[.. String { var output = "" diff --git a/bin/check-source b/bin/check-source index 61780116e2..7ab7fa1de5 100755 --- a/bin/check-source +++ b/bin/check-source @@ -134,8 +134,7 @@ EOF ( cd "$here/.." find . \ - \( \! -path './.build/*' -a \ - \! -path './bin/benchmark/.build/*' -a \ + \( \! -path '*/.build/*' -a \ \! -name '.' -a \ \( "${matching_files[@]}" \) -a \ \( \! \( "${exceptions[@]}" \) \) \) | while read line; do diff --git a/bin/make-test-bundle/Sources/make-test-bundle/Node/PropertyNode.swift b/bin/make-test-bundle/Sources/make-test-bundle/Node/PropertyNode.swift index 13ae2c18ca..d3c23dd386 100644 --- a/bin/make-test-bundle/Sources/make-test-bundle/Node/PropertyNode.swift +++ b/bin/make-test-bundle/Sources/make-test-bundle/Node/PropertyNode.swift @@ -53,7 +53,7 @@ class PropertyNode: TypeMemberNode { if isDynamic { result += "\(levelString) \(kindString) var \(name.lowercased()): \(propertyType) { return \(propertyType)(\(propertyValue)) }\n" } else { - result += "\(levelString) \(kindString) var \(name.lowercased()): \(propertyType) = \(propertyValue)\n" + result += "\(levelString) \(kindString) \(kind == .static ? "let" : "var") \(name.lowercased()): \(propertyType) = \(propertyValue)\n" } if kind == .interface { diff --git a/bin/test-data-external-resolver b/bin/test-data-external-resolver index 23d9f9d13d..d38e2db513 100755 --- a/bin/test-data-external-resolver +++ b/bin/test-data-external-resolver @@ -2,7 +2,7 @@ # # This source file is part of the Swift.org open source project # -# Copyright (c) 2021 Apple Inc. and the Swift project authors +# Copyright (c) 2021-2025 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 @@ -18,108 +18,177 @@ # absolute documentation links with the "com.test.bundle" identifier. For example: RESPONSE='{ - "resolvedInformation" : { - "abstract" : "Resolved abstract.", - "availableLanguages" : [ + "resolved" : { + "abstract" : [ { - "id" : "swift", - "name" : "Language Name", - "idAliases" : [], - "linkDisambiguationID": "swift" + "text" : "Resolved ", + "type" : "text" }, { - "id" : "occ", - "name" : "Variant Language Name", - "idAliases" : [ - "objective-c", - "c" + "inlineContent" : [ + { + "text" : "formatted", + "type" : "text" + } ], - "linkDisambiguationID" : "c" + "type" : "strong" + }, + { + "text" : " abstract with ", + "type" : "text" + }, + { + "identifier" : "doc://com.test.bundle/path/to/other-page", + "isActive" : true, + "type" : "reference" + }, + { + "text" : ".", + "type" : "text" } ], - "declarationFragments" : [ + "availableLanguages" : [ + "swift", + "data", + { + "id" : "language-id", + "idAliases" : [ + "language-alias-id" + ], + "linkDisambiguationID" : "language-id", + "name" : "Language name" + }, + { + "id" : "language-id-2", + "linkDisambiguationID" : "language-id-2", + "name" : "Other language name" + }, + "occ" + ], + "fragments" : [ + { + "kind" : "keyword", + "text" : "resolved" + }, { "kind" : "text", - "spelling" : "declaration fragment" + "text" : " " + }, + { + "kind" : "identifier", + "text" : "fragment" } ], "kind" : { - "id" : "com.test.kind.id", + "id" : "kind-id", "isSymbol" : true, - "name" : "Kind Name" + "name" : "Kind name" }, "language" : { - "id" : "swift", - "name" : "Language Name", - "idAliases" : [], - "linkDisambiguationID": "swift" - + "id" : "language-id", + "idAliases" : [ + "language-alias-id" + ], + "linkDisambiguationID" : "language-id", + "name" : "Language name" }, + "path" : "/documentation/something", "platforms" : [ { "beta" : false, - "introducedAt" : "1.0.0", - "name" : "Platform Name" + "introducedAt" : "1.2.3", + "name" : "Platform name" } ], - "topicImages": [ + "referenceURL" : "doc://com.test.bundle/documentation/something", + "references" : [ { - "type": "card", - "identifier": "some-external-card-image-identifier" - } - ], - "references": [ + "abstract" : [ + { + "text" : "The abstract of another page that is linked to", + "type" : "text" + } + ], + "identifier" : "doc://com.test.bundle/path/to/other-page", + "kind" : "article", + "title" : "Linked from abstract", + "type" : "topic", + "url" : "/path/to/other-page" + }, { - "type": "image", - "identifier": "some-external-card-image-identifier", - "variants": [ + "alt" : "Resolved image alt text", + "identifier" : "some-external-card-image-identifier", + "type" : "image", + "variants" : [ { - "url": "http:\/\/example.com\/some-image-1x.jpg", - "traits": [ + "traits" : [ "1x" - ] - }, - { - "url": "http:\/\/example.com\/some-image-1x-dark.jpg", - "traits": [ - "1x", "dark" - ] + ], + "url" : "http://example.com/some-image.jpg" }, { - "url": "http:\/\/example.com\/some-image-2x.jpg", - "traits": [ - "2x" - ] + "traits" : [ + "2x", + "dark" + ], + "url" : "http://example.com/some-image@2x~dark.jpg" } ] } ], - "title" : "Resolved Title", - "url" : "doc:\/\/com.test.bundle\/resolved/path\/", + "title" : "Resolved title", + "topicImages" : [ + { + "identifier" : "some-external-card-image-identifier", + "type" : "card" + } + ], + "usr" : "resolved-unique-symbol-id", "variants" : [ { - "abstract" : "Resolved variant abstract for this topic.", - "declarationFragments" : [ + "abstract" : [ + { + "text" : "Resolved abstract", + "type" : "text" + }, + { + "code" : "variant", + "type" : "codeVoice" + }, + { + "text" : "Resolved abstract", + "type" : "text" + } + ], + "fragments" : [ + { + "kind" : "keyword", + "text" : "resolved" + }, + { + "kind" : "text", + "text" : " " + }, + { + "kind" : "identifier", + "text" : "variant" + }, { "kind" : "text", - "spelling" : "variant declaration fragment" + "text" : ": " + }, + { + "kind" : "typeIdentifier", + "text" : "fragment" } ], "kind" : { - "id" : "com.test.other-kind.id", + "id" : "variant-kind-id", "isSymbol" : true, - "name" : "Variant Kind Name" - }, - "language" : { - "id" : "occ", - "name" : "Variant Language Name", - "idAliases" : [ - "objective-c", - "c" - ], - "linkDisambiguationID" : "c" + "name" : "Variant kind name" }, - "title" : "Resolved Variant Title", + "language" : "occ", + "title" : "Resolved variant title", "traits" : [ { "interfaceLanguage" : "occ" @@ -130,8 +199,11 @@ RESPONSE='{ } }' -# Write this resolver's bundle identifier -echo '{"bundleIdentifier":"com.test.bundle"}' +# Write this resolver's identifier and capabilities +echo '{ + "identifier": "com.test.bundle", + "capabilities": 0 +}' # Forever, wait for DocC to send a request and respond the resolved information while true diff --git a/bin/update-gh-pages-documentation-site b/bin/update-gh-pages-documentation-site index c1469ed8ae..149911c7d4 100755 --- a/bin/update-gh-pages-documentation-site +++ b/bin/update-gh-pages-documentation-site @@ -74,7 +74,7 @@ swift package \ --hosting-base-path swift-docc \ --output-path "$DOCC_UTILITIES_OUTPUT_DIR" -echo -e "\033[34;1m Merging docs \033q[0m" +echo -e "\033[34;1m Merging docs \033[0m" # Remove the output directory so that the merge command can output there rm -rf "$SWIFT_DOCC_ROOT/gh-pages/docs" @@ -90,6 +90,7 @@ CURRENT_COMMIT_HASH=`git rev-parse --short HEAD` # Commit and push our changes to the gh-pages branch cd gh-pages +touch docs/.nojekyll # We need this (empty) file so that GitHub Pages will publish our prebuilt documentation. git add docs if [ -n "$(git status --porcelain)" ]; then diff --git a/bin/update-license-comments/Package.swift b/bin/update-license-comments/Package.swift new file mode 100644 index 0000000000..69e55f230a --- /dev/null +++ b/bin/update-license-comments/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.1 +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 PackageDescription + +let package = Package( + name: "update-license-for-modified-files", + platforms: [ + .macOS(.v13) + ], + targets: [ + .executableTarget( + name: "update-license-for-modified-files" + ), + ] +) diff --git a/bin/update-license-comments/Sources/update-license-for-modified-files/main.swift b/bin/update-license-comments/Sources/update-license-for-modified-files/main.swift new file mode 100644 index 0000000000..9cdcc74d05 --- /dev/null +++ b/bin/update-license-comments/Sources/update-license-for-modified-files/main.swift @@ -0,0 +1,141 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 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 + +// Determine what changes to consider + +enum DiffStrategy { + case stagedFiles + case comparingTo(treeish: String) +} + +let arguments = ProcessInfo.processInfo.arguments.dropFirst() +let diffStrategy: DiffStrategy +switch arguments.first { + case "-h", "--help": + print(""" + OVERVIEW: Update the year in the license comment of modified files + + USAGE: swift run update-license-for-modified-files [--staged | ] + + To update the year for staged, but not yet committed files, run: + swift run update-license-for-modified-files --staged + + To update the year for all already committed changes that are different from the 'main' branch, run: + swift run update-license-for-modified-files + + To update the year for the already committed changes in the last commit, run: + swift run update-license-for-modified-files HEAD~ + + You can specify any other branch or commit for this argument but I don't know if there's a real use case for doing so. + """) + exit(0) + + case nil: + diffStrategy = .comparingTo(treeish: "main") + case "--staged", "--cached": + diffStrategy = .stagedFiles + case let treeish?: + diffStrategy = .comparingTo(treeish: treeish) +} + +// Find which files are modified + +let repoURL: URL = { + let url = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() // main.swift + .deletingLastPathComponent() // update-license-for-modified-files + .deletingLastPathComponent() // Sources + .deletingLastPathComponent() // update-license-comments + .deletingLastPathComponent() // bin + guard FileManager.default.fileExists(atPath: url.appendingPathComponent("Package.swift").path) else { + fatalError("The path to the Swift-DocC source root has changed. This should only happen if the 'update-license-comments' sources have moved relative to the Swift-DocC repo.") + } + return url +}() + +let modifiedFiles = try findModifiedFiles(in: repoURL, strategy: diffStrategy) + +// Update the years in the license comment where necessary + +// An optional lower range of years for the license comment (including the hyphen) +// │ The upper range of years for the license comment +// │ │ The markdown files don't have a "." but the Swift files do +// │ │ │ The markdown files capitalize the P but the Swift files don't +// │ │ │ │ +// ╭─────┴──────╮╭────┴─────╮ ╭┴╮ ╭┴─╮ +let licenseRegex = /Copyright \(c\) (20[0-9]{2}-)?(20[0-9]{2}) Apple Inc\.? and the Swift [Pp]roject authors/ + +let currentYear = Calendar.current.component(.year, from: .now) + +for file in modifiedFiles { + guard var content = try? String(contentsOf: file, encoding: .utf8), + let licenseMatch = try? licenseRegex.firstMatch(in: content) + else { + // Didn't encounter a license comment in this file, do nothing + continue + } + + let upperYearSubstring = licenseMatch.2 + guard let upperYear = Int(upperYearSubstring) else { + print("Couldn't find license year in \(content[licenseMatch.range])") + continue + } + + guard upperYear < currentYear else { + // The license for this file is already up to date. No need to update it. + continue + } + + if licenseMatch.1 == nil { + // The existing license comment only contains a single year. Add the new year after + content.insert(contentsOf: "-\(currentYear)", at: upperYearSubstring.endIndex) + } else { + // The existing license comment contains both a start year and an end year. Update the second year. + content.replaceSubrange(upperYearSubstring.startIndex ..< upperYearSubstring.endIndex, with: "\(currentYear)") + } + try content.write(to: file, atomically: true, encoding: .utf8) +} + +// MARK: Modified files + +private func findModifiedFiles(in repoURL: URL, strategy: DiffStrategy) throws -> [URL] { + let diffCommand = Process() + diffCommand.currentDirectoryURL = repoURL + diffCommand.executableURL = URL(fileURLWithPath: "/usr/bin/git") + + let comparisonFlag: String = switch strategy { + case .stagedFiles: + "--cached" + case .comparingTo(let treeish): + treeish + } + + diffCommand.arguments = ["diff", "--name-only", comparisonFlag] + + let output = Pipe() + diffCommand.standardOutput = output + + try diffCommand.run() + + guard let outputData = try output.fileHandleForReading.readToEnd(), + let outputString = String(data: outputData, encoding: .utf8) + else { + return [] + } + + return outputString + .components(separatedBy: .newlines) + .compactMap { line in + guard !line.isEmpty else { return nil } + return repoURL.appendingPathComponent(line, isDirectory: false) + } +} diff --git a/features.json b/features.json index a14d784fe4..014b9bf65c 100644 --- a/features.json +++ b/features.json @@ -1,5 +1,8 @@ { "features": [ + { + "name": "code-block-annotations" + }, { "name": "diagnostics-file" },