diff --git a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/AbsoluteSymbolLink.swift b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/AbsoluteSymbolLink.swift index b32852e88d..e4d962db07 100644 --- a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/AbsoluteSymbolLink.swift +++ b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/AbsoluteSymbolLink.swift @@ -14,6 +14,7 @@ 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 @@ -130,8 +131,10 @@ public struct AbsoluteSymbolLink: CustomStringConvertible { } } +@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 @@ -207,6 +210,7 @@ extension AbsoluteSymbolLink { } } +@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. diff --git a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/DocCSymbolRepresentable.swift b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/DocCSymbolRepresentable.swift index 113cc96074..8244c8041a 100644 --- a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/DocCSymbolRepresentable.swift +++ b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/DocCSymbolRepresentable.swift @@ -12,6 +12,7 @@ import Foundation 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. /// @@ -31,6 +32,7 @@ public protocol DocCSymbolRepresentable: Equatable { 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. /// @@ -49,6 +51,7 @@ public extension DocCSymbolRepresentable { } } +@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. @@ -135,6 +138,7 @@ extension AbsoluteSymbolLink.LinkComponent { } } +@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. @@ -173,6 +177,7 @@ 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 @@ -191,6 +196,7 @@ extension SymbolGraph.Symbol: DocCSymbolRepresentable { } } +@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") extension UnifiedSymbolGraph.Symbol: DocCSymbolRepresentable { public var preciseIdentifier: String? { self.uniqueIdentifier diff --git a/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/LinkCompletionTools.swift b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/LinkCompletionTools.swift new file mode 100644 index 0000000000..707dada8ae --- /dev/null +++ b/Sources/SwiftDocC/DocumentationService/Convert/Symbol Link Resolution/LinkCompletionTools.swift @@ -0,0 +1,205 @@ +/* + 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 Foundation + +/// A collection of API for link completion. +/// +/// An example link completion workflow could look something like this; +/// Assume that there's already an partial link in progress: `First/Second-enum/` +/// +/// - First, parse the link into link components using ``parse(linkString:)``. +/// - Second, narrow down the possible symbols to suggest as completion using ``SymbolInformation/matches(_:)`` +/// - 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 + + /// Parses link string into link components; each consisting of a base name and a disambiguation suffix. + /// + /// - Parameter linkString: The link string to parse. + /// - Returns: A list of link components, each consisting of a base name and a disambiguation suffix. + public static func parse(linkString: String) -> [(name: String, disambiguation: ParsedDisambiguation)] { + PathHierarchy.PathParser.parse(path: linkString).components.map { pathComponent in + (name: String(pathComponent.name), disambiguation: ParsedDisambiguation(pathComponent.disambiguation) ) + } + } + + /// A disambiguation suffix for a parsed link component. + public enum ParsedDisambiguation: Equatable { + /// This link component isn't disambiguated. + case none + + /// This path component uses a combination of kind and hash disambiguation. + /// + /// At least one of `kind` and `hash` will be non-`nil`. + /// It's never _necessary_ to specify both a `kind` and a `hash` to disambiguate a link component, but it's supported for the developer to include both. + case kindAndOrHash(kind: String?, hash: String?) + + /// This path component uses type signature information for disambiguation. + /// + /// At least one of `parameterTypes` and `returnTypes` will be non-`nil`. + case typeSignature(parameterTypes: [String]?, returnTypes: [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 instead of matching this one") + case _nonFrozenEnum_useDefaultCase + + init(_ disambiguation: PathHierarchy.PathComponent.Disambiguation?) { + // This initializer is intended to be internal-only. + switch disambiguation { + case .kindAndHash(let kind, let hash): + self = .kindAndOrHash( + kind: kind.map { String($0) }, + hash: hash.map { String($0) } + ) + case .typeSignature(let parameterTypes, let returnTypes): + self = .typeSignature( + parameterTypes: parameterTypes?.map { String($0) }, + returnTypes: returnTypes?.map { String($0) } + ) + case nil: + self = .none + } + } + } + + /// Suggests the minimal most readable disambiguation string for each symbol with the same name. + /// - Parameters: + /// - collidingSymbols: A list of symbols that all have the same name. + /// - Returns: A collection of disambiguation strings in the same order as the provided symbol information. + /// + /// - Important: It's the callers responsibility to create symbol information that matches what the compilers emit in symbol graph files. + /// If there are mismatches, DocC may suggest disambiguation that won't resolve with the real compiler emitted symbol data. + public static func suggestedDisambiguation(forCollidingSymbols collidingSymbols: [SymbolInformation]) -> [String] { + // Track the order of the symbols so that the disambiguations can be ordered to align with their respective symbols. + var identifiersInOrder: [ResolvedIdentifier] = [] + identifiersInOrder.reserveCapacity(collidingSymbols.count) + + // Construct a disambiguation container with all the symbol's information. + var disambiguationContainer = PathHierarchy.DisambiguationContainer() + for symbol in collidingSymbols { + let (node, identifier) = Self._makeNodeAndIdentifier(name: "unused") + identifiersInOrder.append(identifier) + + disambiguationContainer.add( + node, + kind: symbol.kind, + hash: symbol.symbolIDHash, + parameterTypes: symbol.parameterTypes, + returnTypes: symbol.returnTypes + ) + } + + let disambiguatedValues = disambiguationContainer.disambiguatedValues() + // Compute the minimal suggested disambiguation for each symbol and return their string suffixes in the original symbol's order. + return identifiersInOrder.map { identifier in + guard let (_, disambiguation) = disambiguatedValues.first(where: { $0.value.identifier == identifier }) else { + fatalError("Each node in the `DisambiguationContainer` should always have a entry in the `disambiguatedValues`") + } + return disambiguation.makeSuffix() + } + } + + /// Information about a symbol for link completion purposes. + /// + /// > Note: + /// > This symbol information doesn't include the name. + /// > It's the callers responsibility to group symbols by their name. + /// + /// > Important: + /// > It's the callers responsibility to create symbol information that matches what the compilers emit in symbol graph files. + /// > If there are mismatches, DocC may suggest disambiguation that won't resolve with the real compiler emitted symbol data. + public struct SymbolInformation { + /// The kind of symbol, for example `"class"` or `"func.op`. + /// + /// ## See Also + /// - ``/SymbolKit/SymbolGraph/Symbol/KindIdentifier`` + public var kind: String + /// A hash of the symbol's unique identifier. + /// + /// ## See Also + /// - ``hash(uniqueSymbolID:)`` + public var symbolIDHash: String + /// The type names of this symbol's parameters, or `nil` if this symbol has no function signature information. + /// + /// A function without parameters represents i + public var parameterTypes: [String]? + /// The type names of this symbol's return value, or `nil` if this symbol has no function signature information. + public var returnTypes: [String]? + + public init( + kind: String, + symbolIDHash: String, + parameterTypes: [String]? = nil, + returnTypes: [String]? = nil + ) { + self.kind = kind + self.symbolIDHash = symbolIDHash + self.parameterTypes = parameterTypes + self.returnTypes = returnTypes + } + + /// Creates a hashed representation of a symbol's unique identifier. + /// + /// # See Also + /// - ``symbolIDHash`` + public static func hash(uniqueSymbolID: String) -> String { + uniqueSymbolID.stableHashString + } + + // MARK: Filtering + + /// Returns a Boolean value that indicates whether this symbol information matches the parsed disambiguation from one of the link components of a parsed link string. + public func matches(_ parsedDisambiguation: LinkCompletionTools.ParsedDisambiguation) -> Bool { + guard let disambiguation = PathHierarchy.PathComponent.Disambiguation(parsedDisambiguation) else { + return true // No disambiguation to match against. + } + + var disambiguationContainer = PathHierarchy.DisambiguationContainer() + let (node, _) = LinkCompletionTools._makeNodeAndIdentifier(name: "unused") + + disambiguationContainer.add( + node, + kind: self.kind, + hash: self.symbolIDHash, + parameterTypes: self.parameterTypes, + returnTypes: self.returnTypes + ) + + do { + return try disambiguationContainer.find(disambiguation) != nil + } catch { + return false + } + } + } +} + +private extension PathHierarchy.PathComponent.Disambiguation { + init?(_ parsedDisambiguation: LinkCompletionTools.ParsedDisambiguation) { + switch parsedDisambiguation { + case .kindAndOrHash(let kind, let hash): + self = .kindAndHash(kind: kind.map { $0[...] }, hash: hash.map { $0[...] }) + + case .typeSignature(let parameterTypes, let returnTypes): + self = .typeSignature(parameterTypes: parameterTypes?.map { $0[...] }, returnTypes: returnTypes?.map { $0[...] }) + + // Since this is within DocC we want to have an error if we don't handle new future cases. + case .none, ._nonFrozenEnum_useDefaultCase: + return nil + } + } +} diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index d616de1bb3..304f343ab2 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -1577,14 +1577,14 @@ public class DocumentationContext { { switch (source.kind, target.kind) { case (.dictionaryKey, .dictionary): - let dictionaryKey = DictionaryKey(name: sourceSymbol.title, contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf)) + let dictionaryKey = DictionaryKey(name: sourceSymbol.names.title, contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf)) if keysByTarget[edge.target] == nil { keysByTarget[edge.target] = [dictionaryKey] } else { keysByTarget[edge.target]?.append(dictionaryKey) } case (.httpParameter, .httpRequest): - let parameter = HTTPParameter(name: sourceSymbol.title, source: (sourceSymbol.httpParameterSource ?? "query"), contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf)) + let parameter = HTTPParameter(name: sourceSymbol.names.title, source: (sourceSymbol.httpParameterSource ?? "query"), contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf)) if parametersByTarget[edge.target] == nil { parametersByTarget[edge.target] = [parameter] } else { @@ -1594,14 +1594,14 @@ public class DocumentationContext { let body = HTTPBody(mediaType: sourceSymbol.httpMediaType, contents: [], symbol: sourceSymbol) bodyByTarget[edge.target] = body case (.httpParameter, .httpBody): - let parameter = HTTPParameter(name: sourceSymbol.title, source: "body", contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf)) + let parameter = HTTPParameter(name: sourceSymbol.names.title, source: "body", contents: [], symbol: sourceSymbol, required: (edge.kind == .memberOf)) if bodyParametersByTarget[edge.target] == nil { bodyParametersByTarget[edge.target] = [parameter] } else { bodyParametersByTarget[edge.target]?.append(parameter) } case (.httpResponse, .httpRequest): - let statusParts = sourceSymbol.title.split(separator: " ", maxSplits: 1) + let statusParts = sourceSymbol.names.title.split(separator: " ", maxSplits: 1) let statusCode = UInt(statusParts[0]) ?? 0 let reason = statusParts.count > 1 ? String(statusParts[1]) : nil let response = HTTPResponse(statusCode: statusCode, reason: reason, mediaType: sourceSymbol.httpMediaType, contents: [], symbol: sourceSymbol) @@ -1649,7 +1649,7 @@ public class DocumentationContext { if let semantic = target?.semantic as? Symbol { // Add any body parameters to existing body record var localBody = body - if let identifier = body.symbol?.preciseIdentifier, let bodyParameters = bodyParametersByTarget[identifier] { + if let identifier = body.symbol?.identifier.precise, let bodyParameters = bodyParametersByTarget[identifier] { localBody.parameters = bodyParameters.sorted(by: \.name) } if semantic.httpBodySection == nil { diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift index 2d476ba0a0..7341f1b785 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift @@ -806,3 +806,21 @@ private extension SymbolGraph.Relationship.Kind { } } } + +// MARK: Link completion + +// This extension can't be defined in another file because it uses file-private API. +extension LinkCompletionTools { + /// Creates a new path hierarchy node for link completion purposes. + /// + /// Use these nodes to compute disambiguation and match against parsed link components. + /// + /// - Important: The nodes and identifier are only intended for link completion purposes. _Don't_ add them to the path hierarchy or try and resolve links for them. + static func _makeNodeAndIdentifier(name: String) -> (PathHierarchy.Node, ResolvedIdentifier) { + let node = PathHierarchy.Node(name: name) + let id = ResolvedIdentifier() + + node.identifier = id + return (node, id) + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift index 3c199b9147..1c534ee981 100644 --- a/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift +++ b/Sources/SwiftDocC/Infrastructure/Symbol Graph/ExtendedTypeFormatTransformation.swift @@ -491,7 +491,7 @@ extension ExtendedTypeFormatTransformation { relationships.append(.init(source: symbol.identifier.precise, target: parent.identifier.precise, kind: .inContextOf, - targetFallback: parent.title)) + targetFallback: parent.names.title)) symbolIsConnectedToParent[symbol.identifier.precise] = true } diff --git a/Sources/generate-symbol-graph/main.swift b/Sources/generate-symbol-graph/main.swift index 9ae4ff064b..ec6cbbc115 100644 --- a/Sources/generate-symbol-graph/main.swift +++ b/Sources/generate-symbol-graph/main.swift @@ -258,7 +258,7 @@ func extractDocumentationCommentsForDirectives() throws -> [String : SymbolGraph } let directiveSymbols = Set(directiveSymbolUSRs) .compactMap { swiftDocCFrameworkSymbolGraph.symbols[$0] } - .map { (String($0.title.split(separator: ".").last ?? $0.title[...]), $0) } + .map { (String($0.names.title.split(separator: ".").last ?? $0.names.title[...]), $0) } let missingDirectiveSymbolNames: [String] = swiftDocCFrameworkSymbolGraph.relationships.compactMap { relationship in guard relationship.kind == .conformsTo, @@ -297,7 +297,7 @@ func extractDocumentationCommentsForDirectives() throws -> [String : SymbolGraph } let directiveSymbolMembers = swiftDocCFrameworkSymbolGraph.relationships.filter { - return $0.kind == .memberOf && $0.target == directiveSymbol.preciseIdentifier! + return $0.kind == .memberOf && $0.target == directiveSymbol.identifier.precise } .map(\.source) .compactMap { swiftDocCFrameworkSymbolGraph.symbols[$0] } @@ -313,9 +313,9 @@ func extractDocumentationCommentsForDirectives() throws -> [String : SymbolGraph } let argumentSymbol = directiveSymbolMembers.first { member in - member.title == argument.propertyLabel && member.docComment != nil + member.names.title == argument.propertyLabel && member.docComment != nil } ?? directiveSymbolMembers.first { member in - member.title == argument.name && member.docComment != nil + member.names.title == argument.name && member.docComment != nil } guard let argumentDocComment = argumentSymbol?.docComment else { @@ -369,10 +369,10 @@ func extractDocumentationCommentsForDirectives() throws -> [String : SymbolGraph ).first! let allowedValueType = directiveSymbolMembers.first { member in - member.title.split(separator: ".").last == argumentType[...] + member.names.title.split(separator: ".").last == argumentType[...] } - guard let allowedValueType = allowedValueType?.preciseIdentifier else { + guard let allowedValueType = allowedValueType?.identifier.precise else { continue } @@ -384,7 +384,7 @@ func extractDocumentationCommentsForDirectives() throws -> [String : SymbolGraph for allowedValue in allowedValues { guard let allowedValueDocComment = childrenOfAllowedValueType.first(where: { - $0.title.contains(allowedValue) + $0.names.title.contains(allowedValue) })?.docComment else { continue } for (index, line) in allowedValueDocComment.lines.map(\.text).enumerated() { diff --git a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift index 47ef5ef172..009caf03fd 100644 --- a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift @@ -12,6 +12,9 @@ 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 = [ diff --git a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/DocCSymbolRepresentableTests.swift b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/DocCSymbolRepresentableTests.swift index f67be4cd17..d18bf2da17 100644 --- a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/DocCSymbolRepresentableTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/DocCSymbolRepresentableTests.swift @@ -15,6 +15,9 @@ 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( diff --git a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/LinkCompletionToolsTests.swift b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/LinkCompletionToolsTests.swift new file mode 100644 index 0000000000..0c3803835a --- /dev/null +++ b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/LinkCompletionToolsTests.swift @@ -0,0 +1,164 @@ +/* + 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 Foundation +import XCTest +@_spi(LinkCompletion) @testable import SwiftDocC + +class LinkCompletionToolsTests: XCTestCase { + func testParsingLinkStrings() { + func assertParsing( + _ linkString: String, + equal expected: [(name: String, disambiguation: LinkCompletionTools.ParsedDisambiguation)], + file: StaticString = #file, + line: UInt = #line + ) { + let got = LinkCompletionTools.parse(linkString: linkString) + XCTAssertEqual(got.count, expected.count, "Incorrect number of link components", file: file, line: line) + for (index, (got, expected)) in zip(got, expected).enumerated() { + XCTAssertEqual(got.name, expected.name, "Incorrect base name for link component #\(index)", file: file, line: line) + XCTAssertEqual(got.disambiguation, expected.disambiguation, "Incorrect disambiguation for link component #\(index)", file: file, line: line) + } + } + + assertParsing("", equal: []) + + assertParsing("SomeClass", equal: [ + ("SomeClass", .none), + ]) + // The leading slash indicate an absolute symbol link + assertParsing("/SomeModule", equal: [ + ("SomeModule", .none), + ]) + // Trailing slash + assertParsing("SomeClass/", equal: [ + ("SomeClass", .none), + ]) + + // Disambiguation + assertParsing("SomeClass-class", equal: [ + ("SomeClass", .kindAndOrHash(kind: "class", hash: nil)), + ]) + assertParsing("SomeClass-swift.class", equal: [ + ("SomeClass", .kindAndOrHash(kind: "class", hash: nil)), + ]) + assertParsing("SomeClass-p2kr1", equal: [ + ("SomeClass", .kindAndOrHash(kind: nil, hash: "p2kr1")), + ]) + + // Slash in symbol names + assertParsing("Something//=(_:_:)", equal: [ + ("Something", .none), + ("/=(_:_:)", .none), + ]) + assertParsing("Something/operator/=", equal: [ + ("Something", .none), + ("operator/=", .none), + ]) + + // Type signature disambiguation + assertParsing("doSomething(with:and:)->()", equal: [ + ("doSomething(with:and:)", .typeSignature(parameterTypes: nil, returnTypes: [])), + ]) + assertParsing("doSomething(with:and:)->_", equal: [ + ("doSomething(with:and:)", .typeSignature(parameterTypes: nil, returnTypes: ["_"])), + ]) + assertParsing("doSomething(with:and:)->Bool", equal: [ + ("doSomething(with:and:)", .typeSignature(parameterTypes: nil, returnTypes: ["Bool"])), + ]) + assertParsing("doSomething(with:and:)->(Int,_,Double)", equal: [ + ("doSomething(with:and:)", .typeSignature(parameterTypes: nil, returnTypes: ["Int", "_", "Double"])), + ]) + assertParsing("doSomething(with:and:)-(_,_)", equal: [ + ("doSomething(with:and:)", .typeSignature(parameterTypes: ["_", "_"], returnTypes: nil)), + ]) + assertParsing("doSomething(with:and:)-(String,_)", equal: [ + ("doSomething(with:and:)", .typeSignature(parameterTypes: ["String", "_"], returnTypes: nil)), + ]) + assertParsing("doSomething()-()", equal: [ + ("doSomething()", .typeSignature(parameterTypes: [], returnTypes: nil)), + ]) + assertParsing("doSomething(with:and:)-(String,_)->Bool", equal: [ + ("doSomething(with:and:)", .typeSignature(parameterTypes: ["String", "_"], returnTypes: ["Bool"])), + ]) + } + + func testFilteringSymbols() { + let symbol = LinkCompletionTools.SymbolInformation(kind: "func.op", symbolIDHash: "vt1x", parameterTypes: ["Int", "String"], returnTypes: ["Bool"]) + + XCTAssert(symbol.matches(.none)) + XCTAssert(symbol.matches(.kindAndOrHash(kind: "func.op", hash: nil))) + XCTAssert(symbol.matches(.kindAndOrHash(kind: nil, hash: "vt1x"))) + XCTAssert(symbol.matches(.kindAndOrHash(kind: "func.op", hash: "vt1x"))) + XCTAssert(symbol.matches(.typeSignature(parameterTypes: ["_", "_"], returnTypes: ["_"]))) + XCTAssert(symbol.matches(.typeSignature(parameterTypes: ["_", "_"], returnTypes: ["Bool"]))) + XCTAssert(symbol.matches(.typeSignature(parameterTypes: ["Int", "_"], returnTypes: ["_"]))) + XCTAssert(symbol.matches(.typeSignature(parameterTypes: ["_", "String"], returnTypes: ["_"]))) + XCTAssert(symbol.matches(.typeSignature(parameterTypes: ["Int", "String"], returnTypes: ["_"]))) + XCTAssert(symbol.matches(.typeSignature(parameterTypes: ["Int", "String"], returnTypes: ["Bool"]))) + + XCTAssertFalse(symbol.matches(.kindAndOrHash(kind: "method", hash: nil))) + XCTAssertFalse(symbol.matches(.kindAndOrHash(kind: nil, hash: "pfi6"))) + XCTAssertFalse(symbol.matches(.typeSignature(parameterTypes: [], returnTypes: []))) + XCTAssertFalse(symbol.matches(.typeSignature(parameterTypes: ["_"], returnTypes: ["_"]))) + XCTAssertFalse(symbol.matches(.typeSignature(parameterTypes: ["_"], returnTypes: ["_", "_"]))) + XCTAssertFalse(symbol.matches(.typeSignature(parameterTypes: ["Bool", "_"], returnTypes: ["_"]))) + XCTAssertFalse(symbol.matches(.typeSignature(parameterTypes: ["_", "Bool"], returnTypes: ["_"]))) + XCTAssertFalse(symbol.matches(.typeSignature(parameterTypes: ["_", "_"], returnTypes: ["Int"]))) + XCTAssertFalse(symbol.matches(.typeSignature(parameterTypes: ["String", "Int"], returnTypes: ["Bool"]))) + } + + func testUSRHashing() { + for id in ["some", "unique", "symbol", "identifiers"] { + XCTAssertEqual(LinkCompletionTools.SymbolInformation.hash(uniqueSymbolID: id), id.stableHashString) + } + } + + func testSuggestDisambiguation() { + let enumCase = LinkCompletionTools.SymbolInformation(kind: "enum.case", symbolIDHash: "lhk2x", parameterTypes: nil, returnTypes: nil) + let property = LinkCompletionTools.SymbolInformation(kind: "property", symbolIDHash: "j56x", parameterTypes: nil, returnTypes: nil) + + XCTAssertEqual(LinkCompletionTools.suggestedDisambiguation(forCollidingSymbols: [enumCase]), [""]) + XCTAssertEqual(LinkCompletionTools.suggestedDisambiguation(forCollidingSymbols: [property]), [""]) + + XCTAssertEqual(LinkCompletionTools.suggestedDisambiguation(forCollidingSymbols: [ + enumCase, property + ]), [ + "-enum.case", "-property" + ]) + + let operator1 = LinkCompletionTools.SymbolInformation(kind: "func.op", symbolIDHash: "vt1x", parameterTypes: ["Int", "String"], returnTypes: ["Bool"]) + let operator2 = LinkCompletionTools.SymbolInformation(kind: "func.op", symbolIDHash: "pfi6", parameterTypes: ["Wrapped", "Wrapped"], returnTypes: ["Wrapped"]) + let method = LinkCompletionTools.SymbolInformation(kind: "method", symbolIDHash: "w7ti9", parameterTypes: ["Int", "String"], returnTypes: []) + + XCTAssertEqual(LinkCompletionTools.suggestedDisambiguation(forCollidingSymbols: [ + operator1, operator2, method, + ]), [ + "->Bool", "->Wrapped", "-method", + ]) + + var operator3 = operator1 + operator3.symbolIDHash = "da50" + + XCTAssertEqual(LinkCompletionTools.suggestedDisambiguation(forCollidingSymbols: [ + operator1, operator2, operator3, + ]), [ + "-vt1x", "->Wrapped", "-da50", + ]) + + operator3.parameterTypes = ["Int", "Double"] + + XCTAssertEqual(LinkCompletionTools.suggestedDisambiguation(forCollidingSymbols: [ + operator1, operator2, operator3, + ]), [ + "-(_,String)", "->Wrapped", "-(_,Double)", + ]) + } +} diff --git a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/ExtendedTypesFormatTransformationTests.swift b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/ExtendedTypesFormatTransformationTests.swift index b00535d96a..5c2fbe83a0 100644 --- a/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/ExtendedTypesFormatTransformationTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/SymbolGraph/ExtendedTypesFormatTransformationTests.swift @@ -29,11 +29,11 @@ class ExtendedTypesFormatTransformationTests: XCTestCase { XCTAssert(try ExtendedTypeFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph, moduleName: "A")) // check the expected symbols exist - let extendedModuleA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedModule && symbol.title == "A" })) + let extendedModuleA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedModule && symbol.names.title == "A" })) - let extendedTypeA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "A" })) - let extendedTypeATwo = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "ATwo" })) - let extendedTypeB = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "B" })) + let extendedTypeA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.names.title == "A" })) + let extendedTypeATwo = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.names.title == "ATwo" })) + let extendedTypeB = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.names.title == "B" })) let addedMemberSymbolsTypeA = graph.symbols.values.filter({ symbol in symbol.kind.identifier == .property && symbol.pathComponents[symbol.pathComponents.count-2] == "A" }) XCTAssertEqual(addedMemberSymbolsTypeA.count, 2) @@ -87,16 +87,16 @@ class ExtendedTypesFormatTransformationTests: XCTestCase { XCTAssert(try ExtendedTypeFormatTransformation.transformExtensionBlockFormatToExtendedTypeFormat(&graph, moduleName: "A")) // check the expected symbols exist - let extendedModuleA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedModule && symbol.title == "A" })) + let extendedModuleA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedModule && symbol.names.title == "A" })) - let extendedTypeUnextended = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .unknownExtendedType && symbol.title == "Unextended" })) + let extendedTypeUnextended = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .unknownExtendedType && symbol.names.title == "Unextended" })) // this ancestor is also extended so its kind should be known - let extendedTypeUnextendedDotExtended = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "Unextended.Extended" })) + let extendedTypeUnextendedDotExtended = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.names.title == "Unextended.Extended" })) - let extendedTypeUnextendedDotExtendedDotUnextendedInner = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .unknownExtendedType && symbol.title == "Unextended.Extended.UnextendedInner" })) + let extendedTypeUnextendedDotExtendedDotUnextendedInner = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .unknownExtendedType && symbol.names.title == "Unextended.Extended.UnextendedInner" })) - let extendedTypeUnextendedDotExtendedDotUnextendedInnerDotA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.title == "Unextended.Extended.UnextendedInner.A" })) + let extendedTypeUnextendedDotExtendedDotUnextendedInnerDotA = try XCTUnwrap(graph.symbols.values.first(where: { symbol in symbol.kind.identifier == .extendedStructure && symbol.names.title == "Unextended.Extended.UnextendedInner.A" })) // check the expected relationships exist XCTAssertNotNil(graph.relationships.first(where: { relationship in