diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift index e5b9c23b32..1f1dc72577 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+DisambiguatedPaths.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023 Apple Inc. and the Swift project authors + Copyright (c) 2023-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 @@ -19,19 +19,25 @@ private func symbolFileName(_ symbolName: String) -> String { extension PathHierarchy { /// Determines the least disambiguated paths for all symbols in the path hierarchy. /// + /// The path hierarchy is capable of producing shorter, less disambiguated, and more readable paths than what's used for topic references and URLs. + /// Each disambiguation improvement has a boolean parameter to disable it so that DocC can emit the same topic references and URLs as it used to. + /// /// - Parameters: /// - includeDisambiguationForUnambiguousChildren: Whether or not descendants unique to a single collision should maintain the containers disambiguation. /// - includeLanguage: Whether or not kind disambiguation information should include the source language. + /// - allowAdvancedDisambiguation: Whether or not to support more advanced and more human readable types of disambiguation. /// - Returns: A map of unique identifier strings to disambiguated file paths. func caseInsensitiveDisambiguatedPaths( includeDisambiguationForUnambiguousChildren: Bool = false, - includeLanguage: Bool = false + includeLanguage: Bool = false, + allowAdvancedDisambiguation: Bool = true ) -> [String: String] { return disambiguatedPaths( caseSensitive: false, transformToFileNames: true, includeDisambiguationForUnambiguousChildren: includeDisambiguationForUnambiguousChildren, - includeLanguage: includeLanguage + includeLanguage: includeLanguage, + allowAdvancedDisambiguation: allowAdvancedDisambiguation ) } @@ -47,7 +53,7 @@ extension PathHierarchy { func gatherLinksFrom(_ containers: some Sequence) { for container in containers { - let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: false) + let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: false, allowAdvancedDisambiguation: true) for (node, disambiguation) in disambiguatedChildren { guard let id = node.identifier, let symbolID = node.symbol?.identifier.precise else { continue } @@ -83,7 +89,8 @@ extension PathHierarchy { caseSensitive: true, transformToFileNames: false, includeDisambiguationForUnambiguousChildren: false, - includeLanguage: false + includeLanguage: false, + allowAdvancedDisambiguation: true ) } @@ -91,7 +98,8 @@ extension PathHierarchy { caseSensitive: Bool, transformToFileNames: Bool, includeDisambiguationForUnambiguousChildren: Bool, - includeLanguage: Bool + includeLanguage: Bool, + allowAdvancedDisambiguation: Bool ) -> [String: String] { let nameTransform: (String) -> String if transformToFileNames { @@ -111,8 +119,8 @@ extension PathHierarchy { }, uniquingKeysWith: { $0.merge(with: $1) }) for (_, container) in children { - let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: includeLanguage) - let uniqueNodesWithChildren = Set(disambiguatedChildren.filter { $0.disambiguation.value() != nil && !$0.value.children.isEmpty }.map { $0.value.symbol?.identifier.precise }) + let disambiguatedChildren = container.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: includeLanguage, allowAdvancedDisambiguation: allowAdvancedDisambiguation) + let uniqueNodesWithChildren = Set(disambiguatedChildren.filter { $0.disambiguation != .none && !$0.value.children.isEmpty }.map { $0.value.symbol?.identifier.precise }) for (node, disambiguation) in disambiguatedChildren { var path: String @@ -182,7 +190,22 @@ extension PathHierarchy.DisambiguationContainer { static func disambiguatedValues( for elements: some Sequence, - includeLanguage: Bool = false + includeLanguage: Bool = false, + allowAdvancedDisambiguation: Bool = true + ) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { + var collisions = _disambiguatedValues(for: elements, includeLanguage: includeLanguage, allowAdvancedDisambiguation: allowAdvancedDisambiguation) + + // If all but one of the collisions are disfavored, remove the disambiguation for the only favored element. + if let onlyFavoredElementIndex = collisions.onlyIndex(where: { !$0.value.isDisfavoredInLinkCollisions }) { + collisions[onlyFavoredElementIndex].disambiguation = .none + } + return collisions + } + + private static func _disambiguatedValues( + for elements: some Sequence, + includeLanguage: Bool, + allowAdvancedDisambiguation: Bool ) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { var collisions: [(value: PathHierarchy.Node, disambiguation: Disambiguation)] = [] @@ -203,6 +226,31 @@ extension PathHierarchy.DisambiguationContainer { return collisions } + if allowAdvancedDisambiguation { + let elementsThatSupportAdvancedDisambiguation = elements.filter { !$0.node.isExcludedFromAdvancedLinkDisambiguation } + + // Next, if a symbol returns a tuple with a unique number of values, disambiguate by that (without specifying what those arguments are) + collisions += _disambiguateByTypeSignature( + elementsThatSupportAdvancedDisambiguation, + types: \.returnTypes, + makeDisambiguation: Disambiguation.returnTypes, + remainingIDs: &remainingIDs + ) + if remainingIDs.isEmpty { + return collisions + } + + collisions += _disambiguateByTypeSignature( + elementsThatSupportAdvancedDisambiguation, + types: \.parameterTypes, + makeDisambiguation: Disambiguation.parameterTypes, + remainingIDs: &remainingIDs + ) + if remainingIDs.isEmpty { + return collisions + } + } + for element in elements where remainingIDs.contains(element.node.identifier) { collisions.append((value: element.node, disambiguation: element.hash.map { .hash($0) } ?? .none)) } @@ -211,19 +259,29 @@ extension PathHierarchy.DisambiguationContainer { /// Returns all values paired with their disambiguation suffixes. /// - /// - Parameter includeLanguage: Whether or not the kind disambiguation information should include the language, for example: "swift". - func disambiguatedValues(includeLanguage: Bool = false) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { + /// - Parameters: + /// - includeLanguage: Whether or not the kind disambiguation information should include the language, for example: "swift". + /// - allowAdvancedDisambiguation: Whether or not to support more advanced and more human readable types of disambiguation. + func disambiguatedValues( + includeLanguage: Bool = false, + allowAdvancedDisambiguation: Bool = true + ) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { if storage.count == 1 { return [(storage.first!.node, .none)] } - return Self.disambiguatedValues(for: storage, includeLanguage: includeLanguage) + return Self.disambiguatedValues(for: storage, includeLanguage: includeLanguage, allowAdvancedDisambiguation: allowAdvancedDisambiguation) } /// Returns all values paired with their disambiguation suffixes without needing to disambiguate between two different versions of the same symbol. /// - /// - Parameter includeLanguage: Whether or not the kind disambiguation information should include the language, for example: "swift". - func disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: Bool) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { + /// - Parameters: + /// - includeLanguage: Whether or not the kind disambiguation information should include the language, for example: "swift". + /// - allowAdvancedDisambiguation: Whether or not to support more advanced and more human readable types of disambiguation. + fileprivate func disambiguatedValuesWithCollapsedUniqueSymbols( + includeLanguage: Bool, + allowAdvancedDisambiguation: Bool + ) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { typealias DisambiguationPair = (String, String) var uniqueSymbolIDs = [String: [Element]]() @@ -244,24 +302,25 @@ extension PathHierarchy.DisambiguationContainer { var new = PathHierarchy.DisambiguationContainer() for element in nonSymbols { - new.add(element.node, kind: element.kind, hash: element.hash) + new.add(element.node, kind: element.kind, hash: element.hash, parameterTypes: element.parameterTypes, returnTypes: element.returnTypes) } for (id, symbolDisambiguations) in uniqueSymbolIDs { let element = symbolDisambiguations.first! - new.add(element.node, kind: element.kind, hash: element.hash) + new.add(element.node, kind: element.kind, hash: element.hash, parameterTypes: element.parameterTypes, returnTypes: element.returnTypes) if symbolDisambiguations.count > 1 { duplicateSymbols[id] = symbolDisambiguations.dropFirst() } } - - var disambiguated = new.disambiguatedValues(includeLanguage: includeLanguage) + + var disambiguated = new.disambiguatedValues(includeLanguage: includeLanguage, allowAdvancedDisambiguation: allowAdvancedDisambiguation) guard !duplicateSymbols.isEmpty else { return disambiguated } for (id, disambiguations) in duplicateSymbols { let primaryDisambiguation = disambiguated.first(where: { $0.value.symbol?.identifier.precise == id })!.disambiguation + for element in disambiguations { disambiguated.append((element.node, primaryDisambiguation.updated(kind: element.kind, hash: element.hash))) } @@ -271,30 +330,37 @@ extension PathHierarchy.DisambiguationContainer { } /// The computed disambiguation for a given path hierarchy node. - enum Disambiguation { + enum Disambiguation: Equatable { /// No disambiguation is needed. case none /// This node is disambiguated by its kind. case kind(String) /// This node is disambiguated by its hash. case hash(String) - - /// Returns the kind or hash value that disambiguates this node. - func value() -> String! { - switch self { - case .none: - return nil - case .kind(let value), .hash(let value): - return value - } - } + /// This node is disambiguated by its parameter types. + case parameterTypes([String]) + /// This node is disambiguated by its return types. + case returnTypes([String]) + /// Makes a new disambiguation suffix string. func makeSuffix() -> String { switch self { case .none: return "" case .kind(let value), .hash(let value): + // For example: "-enum.case" or "-h1a2s3h" return "-"+value + + case .returnTypes(let types): + // For example: "->String" (returns String) or "->()" (returns Void). + return switch types.count { + case 0: "->()" + case 1: "->\(types.first!)" + default: "->(\(types.joined(separator: ",")))" + } + case .parameterTypes(let types): + // For example: "-(String,_)" or "-(_,Int)"` (a certain parameter has a certain type), or "-()" (has no parameters). + return "-(\(types.joined(separator: ",")))" } } @@ -307,7 +373,65 @@ extension PathHierarchy.DisambiguationContainer { return kind.map { .kind($0) } ?? self case .hash: return hash.map { .hash($0) } ?? self + case .parameterTypes, .returnTypes: + return self } } } + + private static func _disambiguateByTypeSignature( + _ elements: [Element], + types: (Element) -> [String]?, + makeDisambiguation: ([String]) -> Disambiguation, + remainingIDs: inout Set + ) -> [(value: PathHierarchy.Node, disambiguation: Disambiguation)] { + var collisions: [(value: PathHierarchy.Node, disambiguation: Disambiguation)] = [] + + let groupedByTypeCount = [Int?: [Element]](grouping: elements, by: { types($0)?.count }) + for (typesCount, elements) in groupedByTypeCount { + guard let typesCount else { continue } + guard elements.count > 1 else { + // Only one element has this number of types. Disambiguate with only underscores. + let element = elements.first! + guard remainingIDs.contains(element.node.identifier) else { continue } // Don't disambiguate the same element more than once + collisions.append((value: element.node, disambiguation: makeDisambiguation(.init(repeating: "_", count: typesCount)))) + remainingIDs.remove(element.node.identifier) + continue + } + guard typesCount > 0 else { continue } // Need at least one return value to disambiguate + + for typeIndex in 0.. Bool) rethrows -> Index? { + guard let matchingIndex = try firstIndex(where: predicate), + // Ensure that there are no other matches in the rest of the collection + try !self[index(after: matchingIndex)...].contains(where: predicate) + else { + return nil + } + + return matchingIndex + } } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Dump.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Dump.swift index 6cf6455c7c..e6ec00f565 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Dump.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Dump.swift @@ -11,6 +11,8 @@ // This API isn't exposed anywhere and is only used from a debugger. #if DEBUG +import SymbolKit + /// A node in a tree structure that can be printed into a visual representation for debugging. private struct DumpableNode { var name: String @@ -21,20 +23,25 @@ private extension PathHierarchy.Node { /// Maps the path hierarchy subtree into a representation that can be printed into a visual form for debugging. func dumpableNode() -> DumpableNode { // Each node is printed as 3-layer hierarchy with the child names, their kind disambiguation, and their hash disambiguation. + + // One layer for the node itself that displays information about the symbol return DumpableNode( - name: symbol.map { "{ \($0.identifier.precise) : \($0.kind.identifier.identifier) [\(languages.map(\.name).joined(separator: ", "))] }" } ?? "[ \(name) ]", - children: children.sorted(by: \.key).map { (key, disambiguationTree) -> DumpableNode in + name: symbol.map(describe(_:)) ?? "[ \(name) ]", + children: children.sorted(by: \.key).map { (childName, disambiguationTree) -> DumpableNode in + + // A second layer that displays the kind disambiguation let grouped = [String: [PathHierarchy.DisambiguationContainer.Element]](grouping: disambiguationTree.storage, by: { $0.kind ?? "_" }) return DumpableNode( - name: key, + name: childName, children: grouped.sorted(by: \.key).map { (kind, kindTree) -> DumpableNode in + + // A third layer that displays the hash disambiguation DumpableNode( name: kind, children: kindTree.sorted(by: { lhs, rhs in (lhs.hash ?? "_") < (rhs.hash ?? "_") }).map { (element) -> DumpableNode in - DumpableNode( - name: element.hash ?? "_", - children: [element.node.dumpableNode()] - ) + + // Recursively dump the subtree + DumpableNode(name: element.hash ?? "_", children: [element.node.dumpableNode()]) } ) } @@ -44,6 +51,14 @@ private extension PathHierarchy.Node { } } +private func describe(_ symbol: SymbolGraph.Symbol) -> String { + guard let (parameterTypes, returnValueTypes) = PathHierarchy.functionSignatureTypeNames(for: symbol) else { + return "{ \(symbol.identifier.precise) }" + } + + return "{ \(symbol.identifier.precise) : (\(parameterTypes.joined(separator: ", "))) -> (\(returnValueTypes.joined(separator: ", "))) }" +} + extension PathHierarchy { /// Creates a visual representation or the path hierarchy for debugging. func dump() -> String { diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Error.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Error.swift index 7ec787f3db..e874dbcc21 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Error.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Error.swift @@ -8,6 +8,7 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ +import Foundation import struct Markdown.SourceRange import struct Markdown.SourceLocation import SymbolKit @@ -80,10 +81,38 @@ extension PathHierarchy.Error { /// - fullNameOfNode: A closure that determines the full name of a node, to be displayed in collision diagnostics to precisely identify symbols and other pages. /// - Note: `Replacement`s produced by this function use `SourceLocation`s relative to the link text excluding its surrounding syntax. func makeTopicReferenceResolutionErrorInfo(fullNameOfNode: (PathHierarchy.Node) -> String) -> TopicReferenceResolutionErrorInfo { + // Both `.unknownDisambiguation(...)` and `.lookupCollisions(...)` create solutions on the same format from the same information. // This is defined inline because it captures `fullNameOfNode`. - func collisionIsBefore(_ lhs: (node: PathHierarchy.Node, disambiguation: String), _ rhs: (node: PathHierarchy.Node, disambiguation: String)) -> Bool { - return fullNameOfNode(lhs.node) + lhs.disambiguation - < fullNameOfNode(rhs.node) + rhs.disambiguation + func makeCollisionSolutions( + from candidates: [(node: PathHierarchy.Node, disambiguation: String)], + nextPathComponent: PathHierarchy.PathComponent, + partialResultPrefix: Substring + ) -> ( + pathPrefix: Substring, + foundDisambiguation: Substring, + solutions: [Solution] + ) { + let pathPrefix = partialResultPrefix + nextPathComponent.name + let foundDisambiguation = nextPathComponent.full.dropFirst(nextPathComponent.name.count) + let replacementRange = SourceRange.makeRelativeRange(startColumn: pathPrefix.count, length: foundDisambiguation.count) + + let solutions: [Solution] = candidates + .map { (fullName: fullNameOfNode($0.node), disambiguation: $0.disambiguation) } + .sorted { lhs, rhs in + // Sort by name first and disambiguation second + if lhs.fullName == rhs.fullName { + return lhs.disambiguation < rhs.disambiguation + } + return lhs.fullName < rhs.fullName + } + .map { (fullName: String, suggestedDisambiguation: String) -> Solution in + // In contexts that display the solution message on a single line by removing newlines, this extra whitespace makes it look correct ─────────────╮ + // ▼ + return Solution(summary: "\(Self.replacementOperationDescription(from: foundDisambiguation, to: suggestedDisambiguation, forCollision: true)) for \n\(fullName.singleQuoted)", replacements: [ + Replacement(range: replacementRange, replacement: suggestedDisambiguation) + ]) + } + return (pathPrefix, foundDisambiguation, solutions) } switch self { @@ -127,7 +156,7 @@ extension PathHierarchy.Error { case .unfindableMatch(let node): return TopicReferenceResolutionErrorInfo(""" - \(node.name.singleQuoted) can't be linked to in a partial documentation build + \(node.name.singleQuoted) can't be linked to in a partial documentation build """) case .nonSymbolMatchForSymbolLink(path: let path): @@ -142,24 +171,13 @@ extension PathHierarchy.Error { case .unknownDisambiguation(partialResult: let partialResult, remaining: let remaining, candidates: let candidates): let nextPathComponent = remaining.first! - let validPrefix = partialResult.pathPrefix + nextPathComponent.name - - let disambiguations = nextPathComponent.full.dropFirst(nextPathComponent.name.count) - let replacementRange = SourceRange.makeRelativeRange(startColumn: validPrefix.count, length: disambiguations.count) - - let solutions: [Solution] = candidates - .sorted(by: collisionIsBefore) - .map { (node: PathHierarchy.Node, disambiguation: String) -> Solution in - return Solution(summary: "\(Self.replacementOperationDescription(from: disambiguations, to: disambiguation)) for\n\(fullNameOfNode(node).singleQuoted)", replacements: [ - Replacement(range: replacementRange, replacement: disambiguation) - ]) - } + let (pathPrefix, foundDisambiguation, solutions) = makeCollisionSolutions(from: candidates, nextPathComponent: nextPathComponent, partialResultPrefix: partialResult.pathPrefix) return TopicReferenceResolutionErrorInfo(""" - \(disambiguations.dropFirst().singleQuoted) isn't a disambiguation for \(nextPathComponent.name.singleQuoted) at \(partialResult.node.pathWithoutDisambiguation().singleQuoted) + \(foundDisambiguation.dropFirst().singleQuoted) isn't a disambiguation for \(nextPathComponent.name.singleQuoted) at \(partialResult.node.pathWithoutDisambiguation().singleQuoted) """, solutions: solutions, - rangeAdjustment: .makeRelativeRange(startColumn: validPrefix.count, length: disambiguations.count) + rangeAdjustment: .makeRelativeRange(startColumn: pathPrefix.count, length: foundDisambiguation.count) ) case .unknownName(partialResult: let partialResult, remaining: let remaining, availableChildren: let availableChildren): @@ -201,17 +219,7 @@ extension PathHierarchy.Error { case .lookupCollision(partialResult: let partialResult, remaining: let remaining, collisions: let collisions): let nextPathComponent = remaining.first! - - let pathPrefix = partialResult.pathPrefix + nextPathComponent.name - - let disambiguations = nextPathComponent.full.dropFirst(nextPathComponent.name.count) - let replacementRange = SourceRange.makeRelativeRange(startColumn: pathPrefix.count, length: disambiguations.count) - - let solutions: [Solution] = collisions.sorted(by: collisionIsBefore).map { (node: PathHierarchy.Node, disambiguation: String) -> Solution in - return Solution(summary: "\(Self.replacementOperationDescription(from: disambiguations.dropFirst(), to: disambiguation)) for\n\(fullNameOfNode(node).singleQuoted)", replacements: [ - Replacement(range: replacementRange, replacement: "-" + disambiguation) - ]) - } + let (pathPrefix, _, solutions) = makeCollisionSolutions(from: collisions, nextPathComponent: nextPathComponent, partialResultPrefix: partialResult.pathPrefix) return TopicReferenceResolutionErrorInfo(""" \(nextPathComponent.full.singleQuoted) is ambiguous at \(partialResult.node.pathWithoutDisambiguation().singleQuoted) @@ -222,14 +230,25 @@ extension PathHierarchy.Error { } } - private static func replacementOperationDescription(from: some StringProtocol, to: some StringProtocol) -> String { + private static func replacementOperationDescription(from: some StringProtocol, to: some StringProtocol, forCollision: Bool = false) -> String { if from.isEmpty { return "Insert \(to.singleQuoted)" } if to.isEmpty { return "Remove \(from.singleQuoted)" } - return "Replace \(from.singleQuoted) with \(to.singleQuoted)" + + guard forCollision else { + return "Replace \(from.singleQuoted) with \(to.singleQuoted)" + } + + if to.hasPrefix("->") || from.hasPrefix("->") { + // If either the "to" or "from" descriptions are a return type disambiguation, include the full arrow for both. + // Only a ">" prefix doesn't read as an "arrow", and it looks incorrect when only of the descriptions have a "-" prefix. + return "Replace \(from.singleQuoted) with \(to.singleQuoted)" + } + // For other replacement descriptions, drop the leading "-" to focus on the text that's different. + return "Replace \(from.dropFirst().singleQuoted) with \(to.dropFirst().singleQuoted)" } } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift index 10d7989729..b58ec12b27 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Find.swift @@ -8,6 +8,9 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ +import Foundation +import SymbolKit + extension PathHierarchy { /// Attempts to find an element in the path hierarchy for a given path relative to another element. /// @@ -250,6 +253,7 @@ extension PathHierarchy { // When there's a collision, use the remaining path components to try and narrow down the possible collisions. + // See if the collision can be resolved by looking ahead one level deeper. guard let nextPathComponent = remaining.dropFirst().first else { // This was the last path component so there's nothing to look ahead. // @@ -333,7 +337,7 @@ extension PathHierarchy { onlyFindSymbols: Bool, rawPathForError: String ) throws -> Node { - if let favoredMatch = collisions.singleMatch({ $0.node.specialBehaviors.contains(.disfavorInLinkCollision) == false }) { + if let favoredMatch = collisions.singleMatch({ !$0.node.isDisfavoredInLinkCollisions }) { return favoredMatch.node } // If a module has the same name as the article root (which is named after the bundle display name) then its possible @@ -382,41 +386,18 @@ extension PathHierarchy { ) } - // Use a empty disambiguation suffix for the preferred symbol, if there - // is one, which will trigger the warning to suggest removing the - // suffix entirely. - let candidates = disambiguationTree.disambiguatedValues() - let favoredSuffix = favoredSuffix(from: candidates) - let suffixes = candidates.map { $0.disambiguation.makeSuffix() } - let candidatesAndSuffixes = zip(candidates, suffixes).map { (candidate, suffix) in - if suffix == favoredSuffix { - return (node: candidate.value, disambiguation: "") - } else { - return (node: candidate.value, disambiguation: suffix) - } - } return Error.unknownDisambiguation( partialResult: ( node, pathForError(of: rawPathForError, droppingLast: remaining.count) ), remaining: Array(remaining), - candidates: candidatesAndSuffixes + candidates: disambiguationTree.disambiguatedValues().map { + (node: $0.value, disambiguation: String($0.disambiguation.makeSuffix())) + } ) } - /// Check if exactly one of the given candidate symbols is preferred, because it is not disfavored - /// for link resolution and all the other symbols are. - /// - Parameters: - /// - from: An array of candidate node and disambiguation tuples. - /// - Returns: An optional string set to the disambiguation suffix string, without the hyphen separator e.g. "abc123", - /// or nil if there is no preferred symbol. - private func favoredSuffix(from candidates: [(value: PathHierarchy.Node, disambiguation: PathHierarchy.DisambiguationContainer.Disambiguation)]) -> String? { - return candidates.singleMatch({ - !$0.value.specialBehaviors.contains(PathHierarchy.Node.SpecialBehaviors.disfavorInLinkCollision) - })?.disambiguation.makeSuffix() - } - private func pathForError( of rawPath: String, droppingLast trailingComponentsToDrop: Int @@ -473,8 +454,12 @@ extension PathHierarchy.DisambiguationContainer { /// - 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? { - if storage.count <= 1, disambiguation == nil { - return storage.first?.node + if disambiguation == nil { + if storage.count <= 1 { + return storage.first?.node + } else if let favoredMatch = storage.singleMatch({ !$0.node.isDisfavoredInLinkCollisions }) { + return favoredMatch.node + } } switch disambiguation { @@ -486,13 +471,33 @@ 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.value()) }) + throw Error.lookupCollision(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 Error.lookupCollision(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): + break + } + case .typeSignature(let parameterTypes, let returnTypes): + let storage = storage.filter { !$0.node.specialBehaviors.contains(.excludeFromAdvancedLinkDisambiguation )} + switch (parameterTypes, returnTypes) { + case (let parameterTypes?, let returnTypes?): + return storage.first(where: { typesMatch(provided: parameterTypes, actual: $0.parameterTypes) && typesMatch(provided: returnTypes, actual: $0.returnTypes) })?.node + 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. + } + 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. } return matches.first?.node case (nil, nil): @@ -501,13 +506,23 @@ extension PathHierarchy.DisambiguationContainer { case nil: break } + // Disambiguate by a mix of kinds and USRs - throw Error.lookupCollision(self.disambiguatedValues().map { ($0.value, $0.disambiguation.value()) }) + throw Error.lookupCollision(self.disambiguatedValues().map { ($0.value, $0.disambiguation.makeSuffix()) }) } } // MARK: Private helper extensions +private func formattedTypes(_ types: [String]?) -> String? { + guard let types = types else { return nil } + switch types.count { + case 0: return "()" + case 1: return types[0] + default: return "(\(types.joined(separator: ","))" + } +} + // Allow optional substrings to be compared to non-optional strings private func == (lhs: (some StringProtocol)?, rhs: some StringProtocol) -> Bool { guard let lhs else { return false } @@ -546,6 +561,11 @@ private extension PathHierarchy.Node { return name == component.name && (kind == nil || kind! == symbol.kind.identifier.identifier) && (hash == nil || hash! == symbol.identifier.precise.stableHashString) + case .typeSignature(let parameterTypes, let returnTypes): + let functionSignatureTypeNames = PathHierarchy.functionSignatureTypeNames(for: symbol) + return name == component.name + && (parameterTypes == nil || typesMatch(provided: parameterTypes!, actual: functionSignatureTypeNames?.parameterTypeNames)) + && (returnTypes == nil || typesMatch(provided: returnTypes!, actual: functionSignatureTypeNames?.returnTypeNames)) } } @@ -558,3 +578,10 @@ private extension PathHierarchy.Node { || keys.contains(String(component.name)) } } + +private func typesMatch(provided: [Substring], actual: [String]?) -> Bool { + guard let actual, provided.count == actual.count else { return false } + return zip(provided, actual).allSatisfy { providedType, actualType in + providedType == "_" || providedType == actualType + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+PathComponent.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+PathComponent.swift index 80ae86fe89..de670d4f2e 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+PathComponent.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+PathComponent.swift @@ -8,6 +8,7 @@ See https://swift.org/CONTRIBUTORS.txt for Swift project authors */ +import Foundation import SymbolKit /// All known symbol kind identifiers. @@ -44,6 +45,8 @@ extension PathHierarchy { enum Disambiguation { /// This path component uses a combination of kind and hash disambiguation case kindAndHash(kind: Substring?, hash: Substring?) + /// This path component uses type signature information for disambiguation. + case typeSignature(parameterTypes: [Substring]?, returnTypes: [Substring]?) } } @@ -89,11 +92,12 @@ extension PathHierarchy.PathParser { /// and a path component like `"/=(_:_:)-abc123"` will be split into `(name: "/=(_:_:)", hash: "abc123")`. static func parse(pathComponent original: Substring) -> PathComponent { let full = String(original) + // Path components may include a trailing disambiguation, separated by a dash. guard let dashIndex = original.lastIndex(of: "-") else { return PathComponent(full: full, name: full[...], disambiguation: nil) } - let hash = original[dashIndex...].dropFirst() + let disambiguation = original[dashIndex...].dropFirst() let name = original[.. Bool { @@ -106,30 +110,35 @@ extension PathHierarchy.PathParser { return index > 0 } - if knownSymbolKinds.contains(String(hash)) { - // The parsed hash value is a symbol kind - return PathComponent(full: full, name: name, disambiguation: .kindAndHash(kind: hash, hash: nil)) + if knownSymbolKinds.contains(String(disambiguation)) { + // The parsed hash value is a symbol kind. If the last disambiguation is a kind, then the path component doesn't contain a hash disambiguation. + return PathComponent(full: full, name: name, disambiguation: .kindAndHash(kind: disambiguation, hash: nil)) } - if let languagePrefix = knownLanguagePrefixes.first(where: { hash.starts(with: $0) }) { + if let languagePrefix = knownLanguagePrefixes.first(where: { disambiguation.starts(with: $0) }) { // The hash is actually a symbol kind with a language prefix - return PathComponent(full: full, name: name, disambiguation: .kindAndHash(kind: hash.dropFirst(languagePrefix.count), hash: nil)) + return PathComponent(full: full, name: name, disambiguation: .kindAndHash(kind: disambiguation.dropFirst(languagePrefix.count), hash: nil)) } - if !isValidHash(hash) { - // The parsed hash is neither a symbol not a valid hash. It's probably a hyphen-separated name. - return PathComponent(full: full, name: full[...], disambiguation: nil) + if isValidHash(disambiguation) { + if let dashIndex = name.lastIndex(of: "-") { + let kind = name[dashIndex...].dropFirst() + let name = name[.. Substring? { + var scanner = PathComponentScanner(component) + return scanner._scanOperatorName() + } } private struct PathComponentScanner { @@ -202,7 +216,7 @@ private struct PathComponentScanner { static let separator: Character = "/" private static let anchorSeparator: Character = "#" - private static let swiftOperatorEnd: Character = ")" + static let swiftOperatorEnd: Character = ")" private static let cxxOperatorPrefix = "operator" private static let cxxOperatorPrefixLength = cxxOperatorPrefix.count @@ -216,19 +230,8 @@ private struct PathComponentScanner { } mutating func scanPathComponent() -> Substring { - // If the next component is a Swift operator, parse the full operator before splitting on "/" ("/" may appear in the operator name) - if remaining.unicodeScalars.prefix(3).allSatisfy(\.isValidSwiftOperatorHead) { - return scanUntil(index: remaining.firstIndex(of: Self.swiftOperatorEnd)) - + scanUntilSeparatorAndThenSkipIt() - } - - // If the next component is a C++ operator, parse the full operator before splitting on "/" ("/" may appear in the operator name) - if remaining.starts(with: Self.cxxOperatorPrefix), - remaining.unicodeScalars.dropFirst(Self.cxxOperatorPrefixLength).first?.isValidCxxOperatorSymbol == true - { - return scan(length: Self.cxxOperatorPrefixLength) - + scanUntil(index: remaining.unicodeScalars.firstIndex(where: { !$0.isValidCxxOperatorSymbol })) - + scanUntilSeparatorAndThenSkipIt() + if let operatorName = _scanOperatorName() { + return operatorName + scanUntilSeparatorAndThenSkipIt() } // To enable the path parser to identify absolute links, include any leading "/" in the scanned component substring. @@ -243,6 +246,65 @@ private struct PathComponentScanner { return scanUntilSeparatorAndThenSkipIt() } + mutating func _scanOperatorName() -> Substring? { + // If the next component is a Swift operator, parse the full operator before splitting on "/" ("/" may appear in the operator name) + if remaining.unicodeScalars.prefix(3).allSatisfy(\.isValidSwiftOperatorHead) { + return scanUntil(index: remaining.firstIndex(of: Self.swiftOperatorEnd)) + scan(length: 1) + } + + // If the next component is a C++ operator, parse the full operator before splitting on "/" ("/" may appear in the operator name) + if remaining.starts(with: Self.cxxOperatorPrefix), + remaining.unicodeScalars.dropFirst(Self.cxxOperatorPrefixLength).first?.isValidCxxOperatorSymbol == true + { + let base = scan(length: Self.cxxOperatorPrefixLength + 1) + // Because C++ operators don't include the parameters in the name, + // a trailing "-" could either be part of the name or be the disambiguation separator. + // + // The only valid C++ operators that include a "-" do so at the start. + // However, "-=", "->", and "->*" don't have a trailing "-", so they're unambiguous. + // Only "-" and "--" need special parsing to address the ambiguity. + + if base.last == "-", remaining.first == "-" { + // In this scope we start with the following state: + // + // operator--???.. + // ╰───┬───╯╰─┬╌╌╌ + // base remaining + // + // There are 3 possible cases that we can be in: + switch remaining.dropFirst().first { + // The decrement operator with disambiguation. + // operator---h1a2s3h + // ╰────┬───╯│╰──┬──╯ + // name │ disambiguation + // separator + case "-": + return base + scan(length: 1) + + // The decrement operator without disambiguation. + // Either "operator--", "operator--/", or "operator--#" + case nil, "/", "#": + return base + scan(length: 1) + + // The minus operator with disambiguation. + // operator--h1a2s3h + // ╰───┬───╯│╰──┬──╯ + // name │ disambiguation + // separator + default: + return base + } + } else { + // In all other cases, scan as long as there are valid C++ operator characters + return base + + scanUntil(index: remaining.unicodeScalars.firstIndex(where: { $0 == "-" || !$0.isValidCxxOperatorSymbol })) + } + } + + // Not an operator name + return nil + } + mutating func scanAnchorComponentAtEnd() -> Substring? { guard let index = remaining.firstIndex(of: Self.anchorSeparator) else { return nil diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Serialization.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Serialization.swift index 60ccfceff9..878e31da63 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Serialization.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+Serialization.swift @@ -46,8 +46,14 @@ extension PathHierarchy.FileRepresentation { children: node.children.values.flatMap({ tree in var disambiguations = [Node.Disambiguation]() for element in tree.storage where element.node.identifier != nil { // nodes without identifiers can't be found in the tree - disambiguations.append(.init(kind: element.kind, hash: element.hash, nodeID: identifierMap[element.node.identifier]!)) - } + disambiguations.append(.init( + kind: element.kind, + hash: element.hash, + parameterTypes: element.parameterTypes, + returnTypes: element.returnTypes, + nodeID: identifierMap[element.node.identifier]! + )) + } return disambiguations }), symbolID: node.symbol?.identifier @@ -96,6 +102,8 @@ extension PathHierarchy { struct Disambiguation: Codable { var kind: String? var hash: String? + var parameterTypes: [String]? + var returnTypes: [String]? var nodeID: Int } } diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignature.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignature.swift new file mode 100644 index 0000000000..0bca18c4d1 --- /dev/null +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy+TypeSignature.swift @@ -0,0 +1,410 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2023-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 SymbolKit + +// MARK: From symbols + +extension PathHierarchy { + /// Returns the lists of the type names for the symbol's parameters and return values. + static func functionSignatureTypeNames(for symbol: SymbolGraph.Symbol) -> (parameterTypeNames: [String], returnTypeNames: [String])? { + guard let signature = symbol[mixin: SymbolGraph.Symbol.FunctionSignature.self] else { + return nil + } + + return ( + signature.parameters.compactMap { parameterTypeSpellings(for: $0.declarationFragments) }, + returnTypeSpellings(for: signature.returns).map { [$0] } ?? [] + ) + } + + private static func parameterTypeSpellings(for fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment]) -> String? { + typeSpellings(for: fragments) + } + + private static func returnTypeSpellings(for fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment]) -> String? { + if fragments.count == 1, knownVoidReturnValues.contains(fragments.first!) { + // We don't want to list "void" return values as type disambiguation + return nil + } + return typeSpellings(for: fragments) + } + + private static let knownVoidReturnValues = ParametersAndReturnValidator.knownVoidReturnValuesByLanguage.flatMap { $0.value } + + private static func typeSpellings(for fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment]) -> String? { + var accumulated = Substring() + + for fragment in fragments { + switch fragment.kind { + case .identifier where fragment.preciseIdentifier != nil, + .typeIdentifier, + .text: + accumulated += fragment.spelling + + default: + continue + } + } + + accumulated.removeAll(where: \.isWhitespace) + + if accumulated.first == ":" { + _ = accumulated.removeFirst() + } + + while accumulated.first == "(", accumulated.last == ")", !accumulated.isTuple() { + // Remove extra layers of parenthesis unless the type is a tuple + accumulated = accumulated.dropFirst().dropLast() + } + + return accumulated.withSwiftSyntacticSugar() + } +} + +private extension StringProtocol { + /// Checks if the string looks like a tuple with comma separated values. + /// + /// This is used to remove redundant parenthesis around expressions. + func isTuple() -> Bool { + guard first == "(", last == ")", contains(",")else { return false } + var depth = 0 + for char in self { + switch char { + case "(": + depth += 1 + case ")": + depth -= 1 + case "," where depth == 1: + return true + default: + continue + } + } + return false + } + + /// Transforms the string to apply Swift syntactic sugar. + /// + /// The transformed string has all occurrences of `Array`, `Optional`, and `Dictionary` replaced with `[Element]`, `Wrapped?`, and `[Key:Value]`. + func withSwiftSyntacticSugar() -> String { + // If this type uses known Objective-C types, return the original type name. + if contains("NSArray<") || contains("NSDictionary<") { + return String(self) + } + // Don't need to do any processing unless this type contains some string that type name that Swift has syntactic sugar for. + guard contains("Array<") || contains("Optional<") || contains("Dictionary<") else { + return String(self) + } + + var result = "" + result.reserveCapacity(count) + + var scanner = StringScanner(String(self)[...]) + + while let prefix = scanner.scan(until: { $0 == "A" || $0 == "O" || $0 == "D" }) { + result += prefix + + if scanner.hasPrefix("Array<") { + _ = scanner.take("Array".count) + guard let elementType = scanner.scanGenericSingle() else { + // The type is unexpected. Return the original value. + return String(self) + } + result.append("[\(elementType.withSwiftSyntacticSugar())]") + assert(scanner.peek() == ">") + _ = scanner.take() + } + else if scanner.hasPrefix("Optional<") { + _ = scanner.take("Optional".count) + guard let wrappedType = scanner.scanGenericSingle() else { + // The type is unexpected. Return the original value. + return String(self) + } + result.append("\(wrappedType.withSwiftSyntacticSugar())?") + assert(scanner.peek() == ">") + _ = scanner.take() + } + else if scanner.hasPrefix("Dictionary<") { + _ = scanner.take("Dictionary".count) + guard let (keyType, valueType) = scanner.scanGenericPair() else { + // The type is unexpected. Return the original value. + return String(self) + } + result.append("[\(keyType.withSwiftSyntacticSugar()):\(valueType.withSwiftSyntacticSugar())]") + assert(scanner.peek() == ">") + _ = scanner.take() + } + } + result += scanner.takeAll() + + return result + } +} + +// MARK: Parsing links + +extension PathHierarchy.PathParser { + + /// Attempts to parse a path component with type signature disambiguation from a substring. + /// + /// - Parameter original: The substring to parse into a path component + /// - Returns: A path component with type signature disambiguation or `nil` it the substring doesn't contain type signature disambiguation. + static func parseTypeSignatureDisambiguation(pathComponent original: Substring) -> PathComponent? { + // Declaration disambiguation is parsed differently from symbol kind or a FNV-1 hash disambiguation. + // Instead of inspecting the components separated by "-" in reverse order, declaration disambiguation + // scans the path component from start to end to look for the beginning of the declaration. + // + // We parse this way to support closure types, in both parameter types or return types, which may look + // like additional arguments (if the closure takes multiple arguments) or as return type separator ("->"). + // For example, a link to: + // + // func reduce( + // _ initialResult: Result, + // _ nextPartialResult: (Result, Self.Element) throws -> Result + // ) rethrows -> Result + // + // written as ``reduce(_:_:)-(Result,(Result,Element)->Result)->Result`` has the following components: + // ╰──────────────┬────────────────╯ ╰─┬──╯ + // parameter types Result,(Result,Element)->Result │ + // ╰─┬──╯ ╰──────────┬───────────╯ │ + // first parameter type Result │ │ + // second parameter type (Result,Element)->Result │ + // │ + // return type(s) Result + + let possibleDisambiguationText: Substring + if let name = parseOperatorName(original) { + possibleDisambiguationText = original[name.endIndex...] + } else { + possibleDisambiguationText = original + } + + // Look for the start of the parameter disambiguation. + if let parameterStartRange = possibleDisambiguationText.range(of: "-(") { + let name = original[..") { + _ = scanner.take(2) + let returnTypes = scanner.scanArguments() // The return types (tuple or not) can be parsed the same as the arguments + return PathComponent(full: String(original), name: name, disambiguation: .typeSignature(parameterTypes: parameterTypes, returnTypes: returnTypes)) + } + } else if let parameterStartRange = possibleDisambiguationText.range(of: "->") { + let name = original[.. Character? { + remaining.first + } + + mutating func take() -> Character { + remaining.removeFirst() + } + + mutating func take(_ count: Int) -> Substring { + defer { remaining = remaining.dropFirst(count) } + return remaining.prefix(count) + } + + mutating func takeAll() -> Substring { + defer { remaining.removeAll() } + return remaining + } + + mutating func scan(until predicate: (Character) -> Bool) -> Substring? { + guard let index = remaining.firstIndex(where: predicate) else { + return nil + } + defer { remaining = remaining[index...] } + return remaining[.. Bool { + remaining.hasPrefix(prefix) + } + + // MARK: Parsing argument types by scanning + + mutating func scanArguments() -> [Substring] { + guard peek() != ")" else { + _ = take() // drop the ")" + return [] + } + + var arguments = [Substring]() + repeat { + guard let argument = scanArgument() else { + break + } + arguments.append(argument) + } while !isAtEnd && take() == "," + + return arguments + } + + + mutating func scanArgumentAndSkip() -> Substring? { + guard !remaining.isEmpty, !remaining.hasPrefix("->") else { + return nil + } + defer { remaining = remaining.dropFirst() } + return scanArgument() + } + + 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() + } + + guard var argumentString = scanTuple() else { + return nil + } + guard remaining.hasPrefix("->") else { + // This wasn't a closure type, so the scanner has already scanned the full argument. + assert(peek() == "," || peek() == ")", "The argument should be followed by a ',' or ')'.") + return argumentString + } + argumentString.append(contentsOf: "->") + remaining = remaining.dropFirst(2) + + guard peek() == "(" else { + // This closure type has a simple return type. + guard let returnValue = scan(until: { $0 == "," || $0 == ")" }) else { + return nil + } + return argumentString + returnValue + } + guard let returnValue = scanTuple() else { + return nil + } + return argumentString + returnValue + } + + 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. + var depth = 0 + let predicate: (Character) -> Bool = { + if $0 == "(" { + depth += 1 + return false // keep scanning + } + if depth > 0 { + if $0 == ")" { + depth -= 1 + } + return false // keep scanning + } + return $0 == "," || $0 == ")" + } + return scan(until: predicate) + } + + // MARK: Parsing syntactic sugar by scanning + + mutating func scanGenericSingle() -> Substring? { + assert(peek() == "<", "The caller should have checked that this is a generic") + _ = take() + + // The generic may contain any number of nested generics. Keep track of the open and close parenthesis while scanning. + var depth = 0 + let predicate: (Character) -> Bool = { + if $0 == "<" { + depth += 1 + return false // keep scanning + } + if depth > 0 { + if $0 == ">" { + depth -= 1 + } + return false // keep scanning + } + return $0 == ">" + } + return scan(until: predicate) + } + + mutating func scanGenericPair() -> (Substring, Substring)? { + assert(peek() == "<", "The caller should have checked that this is a generic") + _ = take() // Discard the opening "<" + + // The generic may contain any number of nested generics. Keep track of the open and close parenthesis while scanning. + var depth = 0 + let firstPredicate: (Character) -> Bool = { + if $0 == "<" || $0 == "(" { + depth += 1 + return false // keep scanning + } + if depth > 0 { + if $0 == ">" || $0 == ")" { + depth -= 1 + } + return false // keep scanning + } + return $0 == "," + } + guard let first = scan(until: firstPredicate) else { return nil } + _ = take() // Discard the "," + + assert(depth == 0, "Scanning the first generic should encountered a balanced number of brackets.") + let secondPredicate: (Character) -> Bool = { + if $0 == "<" || $0 == "(" { + depth += 1 + return false // keep scanning + } + if depth > 0 { + if $0 == ">" || $0 == ")" { + depth -= 1 + } + return false // keep scanning + } + return $0 == ">" + } + guard let second = scan(until: secondPredicate) else { return nil } + + return (first, second) + } +} diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift index ee7a651e7e..a570e2c4b4 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchy.swift @@ -36,7 +36,6 @@ struct ResolvedIdentifier: Equatable, Hashable { /// /// After a path hierarchy has been fully created---with both symbols and non-symbols---it can be used to find elements in the hierarchy and to determine the least disambiguated paths for all elements. struct PathHierarchy { - /// The list of module nodes. private(set) var modules: [Node] /// The container of top-level articles in the documentation hierarchy. @@ -124,7 +123,7 @@ struct PathHierarchy { // Disfavor synthesized symbols when they collide with other symbol with the same path. // FIXME: Get information about synthesized symbols from SymbolKit https://github.com/swiftlang/swift-docc-symbolkit/issues/58 if symbol.identifier.precise.contains("::SYNTHESIZED::") { - node.specialBehaviors = [.disfavorInLinkCollision, .excludeFromAutomaticCuration] + node.specialBehaviors.formUnion([.disfavorInLinkCollision, .excludeFromAutomaticCuration]) } nodes[id] = node @@ -258,6 +257,8 @@ struct PathHierarchy { switch component.disambiguation { case .kindAndHash(kind: let kind, hash: let hash): parent.add(child: nodeWithoutSymbol, kind: kind.map(String.init), hash: hash.map(String.init)) + case .typeSignature(let parameterTypes, let returnTypes): + parent.add(child: nodeWithoutSymbol, kind: nil, hash: nil, parameterTypes: parameterTypes?.map(String.init), returnTypes: returnTypes?.map(String.init)) case nil: parent.add(child: nodeWithoutSymbol, kind: nil, hash: nil) } @@ -285,6 +286,10 @@ struct PathHierarchy { guard let groupNode = overloadGroupNodes[relationship.target], let overloadedSymbolNodes = allNodes[relationship.source] else { continue } + + // The overload group symbol is cloned from a real symbol and has the same type signature as the clone. This prevents either symbol from using + // parameter type or return type disambiguation. Exclude the overload group from this, so that the real symbol can use it. + groupNode.specialBehaviors.insert(.excludeFromAdvancedLinkDisambiguation) for overloadedSymbolNode in overloadedSymbolNodes { // We want to disfavor the individual overload symbols in favor of resolving links to their overload group symbol. @@ -489,10 +494,26 @@ extension PathHierarchy { /// /// If a favored node collides with a disfavored node the link will resolve to the favored node without requiring any disambiguation. /// Referencing the disfavored node requires disambiguation unless it's the only match for that link. - static let disfavorInLinkCollision = SpecialBehaviors(rawValue: 1 << 0) + static let disfavorInLinkCollision = Self(rawValue: 1 << 0) /// This node is excluded from automatic curation. - static let excludeFromAutomaticCuration = SpecialBehaviors(rawValue: 1 << 1) + static let excludeFromAutomaticCuration = Self(rawValue: 1 << 1) + + /// This node is excluded from advanced link disambiguation, for example type-signature disambiguation. + static let excludeFromAdvancedLinkDisambiguation = Self(rawValue: 1 << 2) + } + + /// A Boolean value indicating whether this node is disfavored in link collisions. + var isDisfavoredInLinkCollisions: Bool { + specialBehaviors.contains(.disfavorInLinkCollision) + } + /// A Boolean value indicating whether this node is excluded from automatic curation. + var isExcludedFromAutomaticCuration: Bool { + specialBehaviors.contains(.excludeFromAutomaticCuration) + } + /// A Boolean value indicating whether this node is excluded from advanced link disambiguation, for example type-signature disambiguation. + var isExcludedFromAdvancedLinkDisambiguation: Bool { + specialBehaviors.contains(.excludeFromAdvancedLinkDisambiguation) } /// Initializes a symbol node. @@ -515,17 +536,23 @@ extension PathHierarchy { /// Adds a descendant to this node, providing disambiguation information from the node's symbol. fileprivate func add(symbolChild: Node) { precondition(symbolChild.symbol != nil) + let symbol = symbolChild.symbol! + + let functionSignatureTypeNames = PathHierarchy.functionSignatureTypeNames(for: symbol) add( child: symbolChild, - kind: symbolChild.symbol!.kind.identifier.identifier, - hash: symbolChild.symbol!.identifier.precise.stableHashString + kind: symbol.kind.identifier.identifier, + hash: symbol.identifier.precise.stableHashString, + parameterTypes: functionSignatureTypeNames?.parameterTypeNames, + returnTypes: functionSignatureTypeNames?.returnTypeNames ) } /// Adds a descendant of this node. - fileprivate func add(child: Node, kind: String?, hash: String?) { + fileprivate func add(child: Node, kind: String?, hash: String?, parameterTypes: [String]? = nil, returnTypes: [String]? = nil) { guard child.parent !== self else { assert( + children.keys.contains(child.name) && (try? children[child.name]?.find(.kindAndHash(kind: kind?[...], hash: hash?[...]))) === child, "If the new child node already has this node as its parent it should already exist among this node's children." ) @@ -533,13 +560,13 @@ extension PathHierarchy { } // If the name was passed explicitly, then the node could have spaces in its name child.parent = self - children[child.name, default: .init()].add(child, kind: kind, hash: hash) + children[child.name, default: .init()].add(child, kind: kind, hash: hash, parameterTypes: parameterTypes, returnTypes: returnTypes) assert(child.parent === self, "Potentially merging nodes shouldn't break the child node's reference to its parent.") } /// Combines this node with another node. - fileprivate func merge(with other: Node) { + func merge(with other: Node) { assert(self.parent?.symbol?.identifier.precise == other.parent?.symbol?.identifier.precise) self.children = self.children.merging(other.children, uniquingKeysWith: { $0.merge(with: $1) }) @@ -614,6 +641,8 @@ extension PathHierarchy.DisambiguationContainer { let node: PathHierarchy.Node let kind: String? let hash: String? + let parameterTypes: [String]? + let returnTypes: [String]? func matches(kind: String?, hash: String?) -> Bool { // The 'hash' is more unique than the 'kind', so compare the 'hash' first. @@ -632,14 +661,15 @@ extension PathHierarchy.DisambiguationContainer { kind == nil && hash == nil } } - - /// Add a new value to the tree for a given pair of kind and hash disambiguations. + + /// Add a new value and its disambiguation information to the container. /// - Parameters: - /// - value: The new value - /// - kind: The kind disambiguation for this value. - /// - hash: The hash disambiguation for this value. - /// - Returns: If a value already exist with the same pair of kind and hash disambiguations. - mutating func add(_ value: PathHierarchy.Node, kind: String?, hash: String?) { + /// - value: The new value. + /// - kind: The kind disambiguation for this value, if any. + /// - hash: The hash disambiguation for this value, if any. + /// - parameterTypes: The type names of the parameter disambiguation for this value, if any. + /// - returnTypes: The type names of the return value disambiguation for this value, if any. + mutating func add(_ value: PathHierarchy.Node, kind: String?, hash: String?, parameterTypes: [String]?, returnTypes: [String]?) { // When adding new elements to the container, it's sufficient to check if the hash and kind match. if let existing = storage.first(where: { $0.matches(kind: kind, hash: hash) }) { // If the container already has a version of this node, merge the new value with the existing value. @@ -649,9 +679,9 @@ extension PathHierarchy.DisambiguationContainer { // When this happens, remove the placeholder node and move its children to the real (non-symbol) node. let existing = storage.removeFirst() value.merge(with: existing.node) - storage = [Element(node: value, kind: kind, hash: hash)] + storage = [Element(node: value, kind: kind, hash: hash, parameterTypes: parameterTypes, returnTypes: returnTypes)] } else { - storage.append(Element(node: value, kind: kind, hash: hash)) + storage.append(Element(node: value, kind: kind, hash: hash, parameterTypes: parameterTypes, returnTypes: returnTypes)) } } @@ -733,7 +763,13 @@ extension PathHierarchy { for child in fileNode.children { let childNode = lookup[identifiers[child.nodeID]]! // Even if this is a symbol node, explicitly pass the kind and hash disambiguation. - node.add(child: childNode, kind: child.kind, hash: child.hash) + node.add( + child: childNode, + kind: child.kind, + hash: child.hash, + parameterTypes: child.parameterTypes, + returnTypes: child.returnTypes + ) if let kind = child.kind { // Since the symbol was created with an empty symbol kind, fill in its kind identifier here. childNode.symbol?.kind.identifier = .init(identifier: kind) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift index dcdac4b4c0..5cf488e502 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/PathHierarchyBasedLinkResolver.swift @@ -79,7 +79,7 @@ final class PathHierarchyBasedLinkResolver { return node.children.flatMap { _, container in container.storage.compactMap { element in guard let childID = element.node.identifier, // Don't include sparse nodes - !element.node.specialBehaviors.contains(.excludeFromAutomaticCuration), + !element.node.isExcludedFromAutomaticCuration, element.node.matches(languagesFilter: languagesFilter) else { return nil @@ -279,7 +279,7 @@ final class PathHierarchyBasedLinkResolver { /// - 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] { - let disambiguatedPaths = pathHierarchy.caseInsensitiveDisambiguatedPaths(includeDisambiguationForUnambiguousChildren: true, includeLanguage: true) + let disambiguatedPaths = pathHierarchy.caseInsensitiveDisambiguatedPaths(includeDisambiguationForUnambiguousChildren: true, includeLanguage: true, allowAdvancedDisambiguation: false) var result: [SymbolGraph.Symbol.Identifier: ResolvedTopicReference] = [:] diff --git a/Sources/SwiftDocC/Model/ParametersAndReturnValidator.swift b/Sources/SwiftDocC/Model/ParametersAndReturnValidator.swift index 5ba924c0d5..8eb714d5b1 100644 --- a/Sources/SwiftDocC/Model/ParametersAndReturnValidator.swift +++ b/Sources/SwiftDocC/Model/ParametersAndReturnValidator.swift @@ -399,7 +399,7 @@ struct ParametersAndReturnValidator { } /// The known declaration fragment alternatives that represents "void" in each programming language. - private static var knownVoidReturnValuesByLanguage: [SourceLanguage: [SymbolGraph.Symbol.DeclarationFragments.Fragment]] = [ + static var knownVoidReturnValuesByLanguage: [SourceLanguage: [SymbolGraph.Symbol.DeclarationFragments.Fragment]] = [ .swift: [ // The Swift symbol graph extractor uses one of these values depending on if the return value is explicitly defined or not. .init(kind: .text, spelling: "()", preciseIdentifier: nil), diff --git a/Tests/SwiftDocCTests/Catalog Processing/GeneratedCurationWriterTests.swift b/Tests/SwiftDocCTests/Catalog Processing/GeneratedCurationWriterTests.swift index 51c50b62f8..1fa85133c7 100644 --- a/Tests/SwiftDocCTests/Catalog Processing/GeneratedCurationWriterTests.swift +++ b/Tests/SwiftDocCTests/Catalog Processing/GeneratedCurationWriterTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023 Apple Inc. and the Swift project authors + Copyright (c) 2023-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 @@ -299,10 +299,10 @@ class GeneratedCurationWriterTests: XCTestCase { ### Instance Methods - - ``fourthTestMemberName(test:)-1h173`` - - ``fourthTestMemberName(test:)-8iuz7`` - - ``fourthTestMemberName(test:)-91hxs`` - - ``fourthTestMemberName(test:)-961zx`` + - ``fourthTestMemberName(test:)->Float`` + - ``fourthTestMemberName(test:)->Double`` + - ``fourthTestMemberName(test:)->Int`` + - ``fourthTestMemberName(test:)->String`` """) } diff --git a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift index ca1d1d245d..3c4b6eece4 100644 --- a/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift +++ b/Tests/SwiftDocCTests/DocumentationService/ConvertService/ConvertServiceTests.swift @@ -1434,7 +1434,7 @@ class ConvertServiceTests: XCTestCase { "/documentation/FillIntroduced/macCatalystOnlyDeprecated()", "/documentation/MyKit/MyClass/myFunction()", "/documentation/SideKit", - "/documentation/SideKit/SideProtocol/func()-6ijsi", + "/documentation/SideKit/SideProtocol/func()", "/documentation/SideKit/SideClass/myFunction()", "/documentation/SideKit/SideProtocol", ], diff --git a/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift b/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift index 50ecbda514..5adc4864ef 100644 --- a/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift @@ -735,11 +735,11 @@ class AutomaticCurationTests: XCTestCase { XCTAssertEqual(protocolTopicSection.title, "Instance Methods") XCTAssertEqual(protocolTopicSection.identifiers, [ - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-9b6be" + "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)" ]) let overloadGroupRenderNode = try renderNode( - atPath: "/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-9b6be", + atPath: "/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)", fromTestBundleNamed: "OverloadedSymbols") XCTAssertEqual( @@ -778,7 +778,7 @@ class AutomaticCurationTests: XCTestCase { XCTAssertEqual(overloadTopic.title, "Instance Methods", file: file, line: line) XCTAssertEqual(overloadTopic.references.map(\.absoluteString), [ - "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-9b6be" + "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)" ], file: file, line: line) } diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift index 4376bafb08..7fd2123fc8 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift @@ -1365,7 +1365,7 @@ let expected = """ │ ├ doc://org.swift.docc.example/documentation/SideKit/SideClass/path │ ╰ doc://org.swift.docc.example/documentation/SideKit/SideClass/url ├ doc://org.swift.docc.example/documentation/SideKit/SideProtocol - │ ╰ doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func()-6ijsi + │ ╰ doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func() │ ╰ doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func()-2dxqn ╰ doc://org.swift.docc.example/documentation/SideKit/UncuratedClass doc://org.swift.docc.example/tutorials/TestOverview @@ -1793,7 +1793,7 @@ let expected = """ // "/" is a separator in URL paths so it's replaced with with "_" (adding disambiguation if the replacement introduces conflicts) XCTAssertEqual("/(_:_:)", pageIdentifiersAndNames["/documentation/Operators/MyNumber/_(_:_:)-7am4"]) - XCTAssertEqual("/=(_:_:)", pageIdentifiersAndNames["/documentation/Operators/MyNumber/_=(_:_:)-3m4ko"]) + XCTAssertEqual("/=(_:_:)", pageIdentifiersAndNames["/documentation/Operators/MyNumber/_=(_:_:)"]) } func testFileNamesWithDifferentPunctuation() throws { @@ -2391,7 +2391,7 @@ let expected = """ XCTAssertEqual(context.documentationCache.reference(symbolID: "s:7SideKit0A5ClassC10testEE")?.path, "/documentation/SideKit/SideClass/Test-swift.enum/NestedEnum-swift.enum") XCTAssertEqual(context.documentationCache.reference(symbolID: "s:7SideKit0A5ClassC10tEstPP")?.path, "/documentation/SideKit/SideClass/Test-swift.enum/NestedEnum-swift.enum/path") - XCTAssertEqual(context.documentationCache.reference(symbolID: "s:5MyKit0A5MyProtocol0Afunc()")?.path, "/documentation/SideKit/SideProtocol/func()-6ijsi") + XCTAssertEqual(context.documentationCache.reference(symbolID: "s:5MyKit0A5MyProtocol0Afunc()")?.path, "/documentation/SideKit/SideProtocol/func()") XCTAssertEqual(context.documentationCache.reference(symbolID: "s:5MyKit0A5MyProtocol0Afunc()DefaultImp")?.path, "/documentation/SideKit/SideProtocol/func()-2dxqn") } diff --git a/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift index 02e5ad6c51..809a29225f 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift @@ -201,7 +201,33 @@ class ExternalPathHierarchyResolverTests: XCTestCase { try linkResolvers.assertSuccessfullyResolves(authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments") try linkResolvers.assertSuccessfullyResolves(authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-1cyvp") try linkResolvers.assertSuccessfullyResolves(authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-2vke2") - + + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(Int)", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-1cyvp" + ) + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(String)", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-2vke2" + ) + + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(Int)->Int", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-1cyvp" + ) + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(String)->Int", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-2vke2" + ) + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(Int)->_", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-1cyvp" + ) + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(String)->_", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-2vke2" + ) + // public enum CollisionsWithDifferentSubscriptArguments { // public subscript(something: Int) -> Int { 0 } // public subscript(somethingElse: String) -> Int { 0 } @@ -210,6 +236,32 @@ class ExternalPathHierarchyResolverTests: XCTestCase { try linkResolvers.assertSuccessfullyResolves(authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-4fd0l") try linkResolvers.assertSuccessfullyResolves(authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-757cj") + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(Int)", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-4fd0l" + ) + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(String)", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-757cj" + ) + + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(Int)->Int", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-4fd0l" + ) + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(String)->Int", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-757cj" + ) + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(Int)->_", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-4fd0l" + ) + try linkResolvers.assertSuccessfullyResolves( + authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(String)->_", + to: "doc://org.swift.MixedFramework/documentation/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-757cj" + ) + // @objc(MySwiftClassObjectiveCName) // public class MySwiftClassSwiftName: NSObject { // @objc(myPropertyObjectiveCName) @@ -370,8 +422,8 @@ class ExternalPathHierarchyResolverTests: XCTestCase { authoredLink: "/MixedFramework/CollisionsWithDifferentKinds/something", errorMessage: "'something' is ambiguous at '/MixedFramework/CollisionsWithDifferentKinds'", solutions: [ - .init(summary: "Insert 'enum.case' for\n'case something'", replacement: ("-enum.case", 54, 54)), - .init(summary: "Insert 'property' for\n'var something: String { get }'", replacement: ("-property", 54, 54)), + .init(summary: "Insert '-enum.case' for \n'case something'", replacement: ("-enum.case", 54, 54)), + .init(summary: "Insert '-property' for \n'var something: String { get }'", replacement: ("-property", 54, 54)), ] ) @@ -379,8 +431,8 @@ class ExternalPathHierarchyResolverTests: XCTestCase { authoredLink: "/MixedFramework/CollisionsWithDifferentKinds/something-class", errorMessage: "'class' isn't a disambiguation for 'something' at '/MixedFramework/CollisionsWithDifferentKinds'", solutions: [ - .init(summary: "Replace '-class' with '-enum.case' for\n'case something'", replacement: ("-enum.case", 54, 60)), - .init(summary: "Replace '-class' with '-property' for\n'var something: String { get }'", replacement: ("-property", 54, 60)), + .init(summary: "Replace 'class' with 'enum.case' for \n'case something'", replacement: ("-enum.case", 54, 60)), + .init(summary: "Replace 'class' with 'property' for \n'var something: String { get }'", replacement: ("-property", 54, 60)), ] ) @@ -398,18 +450,18 @@ class ExternalPathHierarchyResolverTests: XCTestCase { authoredLink: "/MixedFramework/CollisionsWithEscapedKeywords/init()", errorMessage: "'init()' is ambiguous at '/MixedFramework/CollisionsWithEscapedKeywords'", solutions: [ - .init(summary: "Insert 'method' for\n'func `init`()'", replacement: ("-method", 52, 52)), - .init(summary: "Insert 'init' for\n'init()'", replacement: ("-init", 52, 52)), - .init(summary: "Insert 'type.method' for\n'static func `init`()'", replacement: ("-type.method", 52, 52)), + .init(summary: "Insert '-method' for \n'func `init`()'", replacement: ("-method", 52, 52)), + .init(summary: "Insert '-init' for \n'init()'", replacement: ("-init", 52, 52)), + .init(summary: "Insert '-type.method' for \n'static func `init`()'", replacement: ("-type.method", 52, 52)), ] ) try linkResolvers.assertFailsToResolve( authoredLink: "/MixedFramework/CollisionsWithEscapedKeywords/init()-abc123", errorMessage: "'abc123' isn't a disambiguation for 'init()' at '/MixedFramework/CollisionsWithEscapedKeywords'", solutions: [ - .init(summary: "Replace '-abc123' with '-method' for\n'func `init`()'", replacement: ("-method", 52, 59)), - .init(summary: "Replace '-abc123' with '-init' for\n'init()'", replacement: ("-init", 52, 59)), - .init(summary: "Replace '-abc123' with '-type.method' for\n'static func `init`()'", replacement: ("-type.method", 52, 59)), + .init(summary: "Replace 'abc123' with 'method' for \n'func `init`()'", replacement: ("-method", 52, 59)), + .init(summary: "Replace 'abc123' with 'init' for \n'init()'", replacement: ("-init", 52, 59)), + .init(summary: "Replace 'abc123' with 'type.method' for \n'static func `init`()'", replacement: ("-type.method", 52, 59)), ] ) // Providing disambiguation will narrow down the suggestions. Note that `()` is missing in the last path component @@ -439,9 +491,9 @@ class ExternalPathHierarchyResolverTests: XCTestCase { authoredLink: "/MixedFramework/CollisionsWithEscapedKeywords/subscript()", errorMessage: "'subscript()' is ambiguous at '/MixedFramework/CollisionsWithEscapedKeywords'", solutions: [ - .init(summary: "Insert 'method' for\n'func `subscript`()'", replacement: ("-method", 57, 57)), - .init(summary: "Insert 'type.method' for\n'static func `subscript`()'", replacement: ("-type.method", 57, 57)), - .init(summary: "Insert 'subscript' for\n'subscript() -> Int { get }'", replacement: ("-subscript", 57, 57)), + .init(summary: "Insert '-method' for \n'func `subscript`()'", replacement: ("-method", 57, 57)), + .init(summary: "Insert '-type.method' for \n'static func `subscript`()'", replacement: ("-type.method", 57, 57)), + .init(summary: "Insert '-subscript' for \n'subscript() -> Int { get }'", replacement: ("-subscript", 57, 57)), ] ) @@ -453,24 +505,24 @@ class ExternalPathHierarchyResolverTests: XCTestCase { authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)", errorMessage: "'something(argument:)' is ambiguous at '/MixedFramework/CollisionsWithDifferentFunctionArguments'", solutions: [ - .init(summary: "Insert '1cyvp' for\n'func something(argument: Int) -> Int'", replacement: ("-1cyvp", 77, 77)), - .init(summary: "Insert '2vke2' for\n'func something(argument: String) -> Int'", replacement: ("-2vke2", 77, 77)), + .init(summary: "Insert '-(Int)' for \n'func something(argument: Int) -> Int'", replacement: ("-(Int)", 77, 77)), + .init(summary: "Insert '-(String)' for \n'func something(argument: String) -> Int'", replacement: ("-(String)", 77, 77)), ] ) try linkResolvers.assertFailsToResolve( authoredLink: "/documentation/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)", errorMessage: "'something(argument:)' is ambiguous at '/MixedFramework/CollisionsWithDifferentFunctionArguments'", solutions: [ - .init(summary: "Insert '1cyvp' for\n'func something(argument: Int) -> Int'", replacement: ("-1cyvp", 91, 91)), - .init(summary: "Insert '2vke2' for\n'func something(argument: String) -> Int'", replacement: ("-2vke2", 91, 91)), + .init(summary: "Insert '-(Int)' for \n'func something(argument: Int) -> Int'", replacement: ("-(Int)", 91, 91)), + .init(summary: "Insert '-(String)' for \n'func something(argument: String) -> Int'", replacement: ("-(String)", 91, 91)), ] ) try linkResolvers.assertFailsToResolve( authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-abc123", errorMessage: "'abc123' isn't a disambiguation for 'something(argument:)' at '/MixedFramework/CollisionsWithDifferentFunctionArguments'", solutions: [ - .init(summary: "Replace '-abc123' with '-1cyvp' for\n'func something(argument: Int) -> Int'", replacement: ("-1cyvp", 77, 84)), - .init(summary: "Replace '-abc123' with '-2vke2' for\n'func something(argument: String) -> Int'", replacement: ("-2vke2", 77, 84)), + .init(summary: "Replace 'abc123' with '(Int)' for \n'func something(argument: Int) -> Int'", replacement: ("-(Int)", 77, 84)), + .init(summary: "Replace 'abc123' with '(String)' for \n'func something(argument: String) -> Int'", replacement: ("-(String)", 77, 84)), ] ) // Providing disambiguation will narrow down the suggestions. Note that `argument` label is missing in the last path component @@ -492,16 +544,16 @@ class ExternalPathHierarchyResolverTests: XCTestCase { authoredLink: "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-method", errorMessage: "'something(argument:)-method' is ambiguous at '/MixedFramework/CollisionsWithDifferentFunctionArguments'", solutions: [ - .init(summary: "Replace 'method' with '1cyvp' for\n'func something(argument: Int) -> Int'", replacement: ("-1cyvp", 77, 84)), - .init(summary: "Replace 'method' with '2vke2' for\n'func something(argument: String) -> Int'", replacement: ("-2vke2", 77, 84)), + .init(summary: "Replace 'method' with '(Int)' for \n'func something(argument: Int) -> Int'", replacement: ("-(Int)", 77, 84)), + .init(summary: "Replace 'method' with '(String)' for \n'func something(argument: String) -> Int'", replacement: ("-(String)", 77, 84)), ] ) try linkResolvers.assertFailsToResolve( authoredLink: "/documentation/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-method", errorMessage: "'something(argument:)-method' is ambiguous at '/MixedFramework/CollisionsWithDifferentFunctionArguments'", solutions: [ - .init(summary: "Replace 'method' with '1cyvp' for\n'func something(argument: Int) -> Int'", replacement: ("-1cyvp", 91, 98)), - .init(summary: "Replace 'method' with '2vke2' for\n'func something(argument: String) -> Int'", replacement: ("-2vke2", 91, 98)), + .init(summary: "Replace 'method' with '(Int)' for \n'func something(argument: Int) -> Int'", replacement: ("-(Int)", 91, 98)), + .init(summary: "Replace 'method' with '(String)' for \n'func something(argument: String) -> Int'", replacement: ("-(String)", 91, 98)), ] ) @@ -513,16 +565,16 @@ class ExternalPathHierarchyResolverTests: XCTestCase { authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)", errorMessage: "'subscript(_:)' is ambiguous at '/MixedFramework/CollisionsWithDifferentSubscriptArguments'", solutions: [ - .init(summary: "Insert '4fd0l' for\n'subscript(something: Int) -> Int { get }'", replacement: ("-4fd0l", 71, 71)), - .init(summary: "Insert '757cj' for\n'subscript(somethingElse: String) -> Int { get }'", replacement: ("-757cj", 71, 71)), + .init(summary: "Insert '-(Int)' for \n'subscript(something: Int) -> Int { get }'", replacement: ("-(Int)", 71, 71)), + .init(summary: "Insert '-(String)' for \n'subscript(somethingElse: String) -> Int { get }'", replacement: ("-(String)", 71, 71)), ] ) try linkResolvers.assertFailsToResolve( authoredLink: "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-subscript", errorMessage: "'subscript(_:)-subscript' is ambiguous at '/MixedFramework/CollisionsWithDifferentSubscriptArguments'", solutions: [ - .init(summary: "Replace 'subscript' with '4fd0l' for\n'subscript(something: Int) -> Int { get }'", replacement: ("-4fd0l", 71, 81)), - .init(summary: "Replace 'subscript' with '757cj' for\n'subscript(somethingElse: String) -> Int { get }'", replacement: ("-757cj", 71, 81)), + .init(summary: "Replace 'subscript' with '(Int)' for \n'subscript(something: Int) -> Int { get }'", replacement: ("-(Int)", 71, 81)), + .init(summary: "Replace 'subscript' with '(String)' for \n'subscript(somethingElse: String) -> Int { get }'", replacement: ("-(String)", 71, 81)), ] ) } @@ -817,7 +869,7 @@ class ExternalPathHierarchyResolverTests: XCTestCase { // This overloaded protocol method should be able to resolve without a suffix at all, since it doesn't conflict with anything try linkResolvers.assertSuccessfullyResolves( authoredLink: "/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)", - to: "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-9b6be" + to: "doc://com.shapes.ShapeKit/documentation/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)" ) } diff --git a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift index bb0a85521f..c4d3a67c33 100644 --- a/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/PathHierarchyTests.swift @@ -166,6 +166,9 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-1cyvp", in: tree, asSymbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentS2i_tF") try assertFindsPath("/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-2vke2", in: tree, asSymbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentSiSS_tF") + try assertFindsPath("/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(Int)", in: tree, asSymbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentS2i_tF") + try assertFindsPath("/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(String)", in: tree, asSymbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentSiSS_tF") + // public enum CollisionsWithDifferentSubscriptArguments { // public subscript(something: Int) -> Int { 0 } // public subscript(somethingElse: String) -> Int { 0 } @@ -174,6 +177,9 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-4fd0l", in: tree, asSymbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOyS2icip") try assertFindsPath("/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-757cj", in: tree, asSymbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOySiSScip") + try assertFindsPath("/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(Int)", in: tree, asSymbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOyS2icip") + try assertFindsPath("/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(String)", in: tree, asSymbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOySiSScip") + // @objc(MySwiftClassObjectiveCName) // public class MySwiftClassSwiftName: NSObject { // @objc(myPropertyObjectiveCName) @@ -312,23 +318,23 @@ class PathHierarchyTests: XCTestCase { // public typealias Something = Int // } try assertPathCollision("/MixedFramework/CollisionsWithDifferentKinds/something", in: tree, collisions: [ - (symbolID: "s:14MixedFramework28CollisionsWithDifferentKindsO9somethingyA2CmF", disambiguation: "enum.case"), - (symbolID: "s:14MixedFramework28CollisionsWithDifferentKindsO9somethingSSvp", disambiguation: "property"), + (symbolID: "s:14MixedFramework28CollisionsWithDifferentKindsO9somethingyA2CmF", disambiguation: "-enum.case"), + (symbolID: "s:14MixedFramework28CollisionsWithDifferentKindsO9somethingSSvp", disambiguation: "-property"), ]) try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithDifferentKinds/something", in: tree, context: context, expectedErrorMessage: """ 'something' is ambiguous at '/MixedFramework/CollisionsWithDifferentKinds' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Insert 'enum.case' for\n'case something'", replacements: [("-enum.case", 54, 54)]), - .init(summary: "Insert 'property' for\n'var something: String { get }'", replacements: [("-property", 54, 54)]), + .init(summary: "Insert '-enum.case' for \n'case something'", replacements: [("-enum.case", 54, 54)]), + .init(summary: "Insert '-property' for \n'var something: String { get }'", replacements: [("-property", 54, 54)]), ]) } try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithDifferentKinds/something-class", in: tree, context: context, expectedErrorMessage: """ 'class' isn't a disambiguation for 'something' at '/MixedFramework/CollisionsWithDifferentKinds' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Replace '-class' with '-enum.case' for\n'case something'", replacements: [("-enum.case", 54, 60)]), - .init(summary: "Replace '-class' with '-property' for\n'var something: String { get }'", replacements: [("-property", 54, 60)]), + .init(summary: "Replace 'class' with 'enum.case' for \n'case something'", replacements: [("-enum.case", 54, 60)]), + .init(summary: "Replace 'class' with 'property' for \n'var something: String { get }'", replacements: [("-property", 54, 60)]), ]) } @@ -342,26 +348,26 @@ class PathHierarchyTests: XCTestCase { // public static func `init`() { } // } try assertPathCollision("/MixedFramework/CollisionsWithEscapedKeywords/init()", in: tree, collisions: [ - (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsCACycfc", disambiguation: "init"), - (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC4inityyF", disambiguation: "method"), - (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC4inityyFZ", disambiguation: "type.method"), + (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsCACycfc", disambiguation: "-init"), + (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC4inityyF", disambiguation: "-method"), + (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC4inityyFZ", disambiguation: "-type.method"), ]) try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithEscapedKeywords/init()-abc123", in: tree, context: context, expectedErrorMessage: """ 'abc123' isn't a disambiguation for 'init()' at '/MixedFramework/CollisionsWithEscapedKeywords' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Replace '-abc123' with '-method' for\n'func `init`()'", replacements: [("-method", 52, 59)]), - .init(summary: "Replace '-abc123' with '-init' for\n'init()'", replacements: [("-init", 52, 59)]), - .init(summary: "Replace '-abc123' with '-type.method' for\n'static func `init`()'", replacements: [("-type.method", 52, 59)]), + .init(summary: "Replace 'abc123' with 'method' for \n'func `init`()'", replacements: [("-method", 52, 59)]), + .init(summary: "Replace 'abc123' with 'init' for \n'init()'", replacements: [("-init", 52, 59)]), + .init(summary: "Replace 'abc123' with 'type.method' for \n'static func `init`()'", replacements: [("-type.method", 52, 59)]), ]) } try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithEscapedKeywords/init()", in: tree, context: context, expectedErrorMessage: """ 'init()' is ambiguous at '/MixedFramework/CollisionsWithEscapedKeywords' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Insert 'method' for\n'func `init`()'", replacements: [("-method", 52, 52)]), - .init(summary: "Insert 'init' for\n'init()'", replacements: [("-init", 52, 52)]), - .init(summary: "Insert 'type.method' for\n'static func `init`()'", replacements: [("-type.method", 52, 52)]), + .init(summary: "Insert '-method' for \n'func `init`()'", replacements: [("-method", 52, 52)]), + .init(summary: "Insert '-init' for \n'init()'", replacements: [("-init", 52, 52)]), + .init(summary: "Insert '-type.method' for \n'static func `init`()'", replacements: [("-type.method", 52, 52)]), ]) } // Providing disambiguation will narrow down the suggestions. Note that `()` is missing in the last path component @@ -388,17 +394,17 @@ class PathHierarchyTests: XCTestCase { } try assertPathCollision("/MixedFramework/CollisionsWithEscapedKeywords/subscript()", in: tree, collisions: [ - (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC9subscriptyyF", disambiguation: "method"), - (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsCSiycip", disambiguation: "subscript"), - (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC9subscriptyyFZ", disambiguation: "type.method"), + (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC9subscriptyyF", disambiguation: "-method"), + (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsCSiycip", disambiguation: "-subscript"), + (symbolID: "s:14MixedFramework29CollisionsWithEscapedKeywordsC9subscriptyyFZ", disambiguation: "-type.method"), ]) try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithEscapedKeywords/subscript()", in: tree, context: context, expectedErrorMessage: """ 'subscript()' is ambiguous at '/MixedFramework/CollisionsWithEscapedKeywords' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Insert 'method' for\n'func `subscript`()'", replacements: [("-method", 57, 57)]), - .init(summary: "Insert 'type.method' for\n'static func `subscript`()'", replacements: [("-type.method", 57, 57)]), - .init(summary: "Insert 'subscript' for\n'subscript() -> Int { get }'", replacements: [("-subscript", 57, 57)]), + .init(summary: "Insert '-method' for \n'func `subscript`()'", replacements: [("-method", 57, 57)]), + .init(summary: "Insert '-type.method' for \n'static func `subscript`()'", replacements: [("-type.method", 57, 57)]), + .init(summary: "Insert '-subscript' for \n'subscript() -> Int { get }'", replacements: [("-subscript", 57, 57)]), ]) } @@ -407,15 +413,15 @@ class PathHierarchyTests: XCTestCase { // public func something(argument: String) -> Int { 0 } // } try assertPathCollision("/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)", in: tree, collisions: [ - (symbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentS2i_tF", disambiguation: "1cyvp"), - (symbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentSiSS_tF", disambiguation: "2vke2"), + (symbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentS2i_tF", disambiguation: "-(Int)"), + (symbolID: "s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentSiSS_tF", disambiguation: "-(String)"), ]) try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)", in: tree, context: context, expectedErrorMessage: """ 'something(argument:)' is ambiguous at '/MixedFramework/CollisionsWithDifferentFunctionArguments' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Insert '1cyvp' for\n'func something(argument: Int) -> Int'", replacements: [("-1cyvp", 77, 77)]), - .init(summary: "Insert '2vke2' for\n'func something(argument: String) -> Int'", replacements: [("-2vke2", 77, 77)]), + .init(summary: "Insert '-(Int)' for \n'func something(argument: Int) -> Int'", replacements: [("-(Int)", 77, 77)]), + .init(summary: "Insert '-(String)' for \n'func something(argument: String) -> Int'", replacements: [("-(String)", 77, 77)]), ]) } // The path starts with "/documentation" which is optional @@ -423,16 +429,16 @@ class PathHierarchyTests: XCTestCase { 'something(argument:)' is ambiguous at '/MixedFramework/CollisionsWithDifferentFunctionArguments' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Insert '1cyvp' for\n'func something(argument: Int) -> Int'", replacements: [("-1cyvp", 91, 91)]), - .init(summary: "Insert '2vke2' for\n'func something(argument: String) -> Int'", replacements: [("-2vke2", 91, 91)]), + .init(summary: "Insert '-(Int)' for \n'func something(argument: Int) -> Int'", replacements: [("-(Int)", 91, 91)]), + .init(summary: "Insert '-(String)' for \n'func something(argument: String) -> Int'", replacements: [("-(String)", 91, 91)]), ]) } try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-abc123", in: tree, context: context, expectedErrorMessage: """ 'abc123' isn't a disambiguation for 'something(argument:)' at '/MixedFramework/CollisionsWithDifferentFunctionArguments' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Replace '-abc123' with '-1cyvp' for\n'func something(argument: Int) -> Int'", replacements: [("-1cyvp", 77, 84)]), - .init(summary: "Replace '-abc123' with '-2vke2' for\n'func something(argument: String) -> Int'", replacements: [("-2vke2", 77, 84)]), + .init(summary: "Replace 'abc123' with '(Int)' for \n'func something(argument: Int) -> Int'", replacements: [("-(Int)", 77, 84)]), + .init(summary: "Replace 'abc123' with '(String)' for \n'func something(argument: String) -> Int'", replacements: [("-(String)", 77, 84)]), ]) } // Providing disambiguation will narrow down the suggestions. Note that `argument` label is missing in the last path component @@ -455,8 +461,8 @@ class PathHierarchyTests: XCTestCase { 'something(argument:)-method' is ambiguous at '/MixedFramework/CollisionsWithDifferentFunctionArguments' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Replace 'method' with '1cyvp' for\n'func something(argument: Int) -> Int'", replacements: [("-1cyvp", 77, 84)]), - .init(summary: "Replace 'method' with '2vke2' for\n'func something(argument: String) -> Int'", replacements: [("-2vke2", 77, 84)]), + .init(summary: "Replace 'method' with '(Int)' for \n'func something(argument: Int) -> Int'", replacements: [("-(Int)", 77, 84)]), + .init(summary: "Replace 'method' with '(String)' for \n'func something(argument: String) -> Int'", replacements: [("-(String)", 77, 84)]), ]) } // The path starts with "/documentation" which is optional @@ -464,8 +470,8 @@ class PathHierarchyTests: XCTestCase { 'something(argument:)-method' is ambiguous at '/MixedFramework/CollisionsWithDifferentFunctionArguments' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Replace 'method' with '1cyvp' for\n'func something(argument: Int) -> Int'", replacements: [("-1cyvp", 91, 98)]), - .init(summary: "Replace 'method' with '2vke2' for\n'func something(argument: String) -> Int'", replacements: [("-2vke2", 91, 98)]), + .init(summary: "Replace 'method' with '(Int)' for \n'func something(argument: Int) -> Int'", replacements: [("-(Int)", 91, 98)]), + .init(summary: "Replace 'method' with '(String)' for \n'func something(argument: String) -> Int'", replacements: [("-(String)", 91, 98)]), ]) } @@ -474,15 +480,15 @@ class PathHierarchyTests: XCTestCase { // public subscript(somethingElse: String) -> Int { 0 } // } try assertPathCollision("/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)", in: tree, collisions: [ - (symbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOyS2icip", disambiguation: "4fd0l"), - (symbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOySiSScip", disambiguation: "757cj"), + (symbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOyS2icip", disambiguation: "-(Int)"), + (symbolID: "s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOySiSScip", disambiguation: "-(String)"), ]) try assertPathRaisesErrorMessage("/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)", in: tree, context: context, expectedErrorMessage: """ 'subscript(_:)' is ambiguous at '/MixedFramework/CollisionsWithDifferentSubscriptArguments' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Insert '4fd0l' for\n'subscript(something: Int) -> Int { get }'", replacements: [("-4fd0l", 71, 71)]), - .init(summary: "Insert '757cj' for\n'subscript(somethingElse: String) -> Int { get }'", replacements: [("-757cj", 71, 71)]), + .init(summary: "Insert '-(Int)' for \n'subscript(something: Int) -> Int { get }'", replacements: [("-(Int)", 71, 71)]), + .init(summary: "Insert '-(String)' for \n'subscript(somethingElse: String) -> Int { get }'", replacements: [("-(String)", 71, 71)]), ]) } @@ -490,8 +496,8 @@ class PathHierarchyTests: XCTestCase { 'subscript(_:)-subscript' is ambiguous at '/MixedFramework/CollisionsWithDifferentSubscriptArguments' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Replace 'subscript' with '4fd0l' for\n'subscript(something: Int) -> Int { get }'", replacements: [("-4fd0l", 71, 81)]), - .init(summary: "Replace 'subscript' with '757cj' for\n'subscript(somethingElse: String) -> Int { get }'", replacements: [("-757cj", 71, 81)]), + .init(summary: "Replace 'subscript' with '(Int)' for \n'subscript(something: Int) -> Int { get }'", replacements: [("-(Int)", 71, 81)]), + .init(summary: "Replace 'subscript' with '(String)' for \n'subscript(somethingElse: String) -> Int { get }'", replacements: [("-(String)", 71, 81)]), ]) } @@ -699,7 +705,7 @@ class PathHierarchyTests: XCTestCase { // The protocol requirement and the default implementation both exist at the @_export imported Something protocol. let paths = tree.caseInsensitiveDisambiguatedPaths() - XCTAssertEqual(paths["s:5Inner9SomethingP02doB0yyF"], "/DefaultImplementationsWithExportedImport/Something/doSomething()-8skxc") + XCTAssertEqual(paths["s:5Inner9SomethingP02doB0yyF"], "/DefaultImplementationsWithExportedImport/Something/doSomething()") // This is the only favored symbol so it doesn't require any disambiguation XCTAssertEqual(paths["s:5Inner9SomethingPAAE02doB0yyF"], "/DefaultImplementationsWithExportedImport/Something/doSomething()-scj9") // Test disfavoring a default implementation in a symbol collision @@ -814,9 +820,18 @@ class PathHierarchyTests: XCTestCase { // } XCTAssertEqual( paths["s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentS2i_tF"], - "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-1cyvp") + "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(Int)") XCTAssertEqual( paths["s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentSiSS_tF"], + "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-(String)") + + let hashAndKindDisambiguatedPaths = tree.caseInsensitiveDisambiguatedPaths(allowAdvancedDisambiguation: false) + + XCTAssertEqual( + hashAndKindDisambiguatedPaths["s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentS2i_tF"], + "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-1cyvp") + XCTAssertEqual( + hashAndKindDisambiguatedPaths["s:14MixedFramework40CollisionsWithDifferentFunctionArgumentsO9something8argumentSiSS_tF"], "/MixedFramework/CollisionsWithDifferentFunctionArguments/something(argument:)-2vke2") // public enum CollisionsWithDifferentSubscriptArguments { @@ -825,9 +840,16 @@ class PathHierarchyTests: XCTestCase { // } XCTAssertEqual( paths["s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOyS2icip"], - "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-4fd0l") + "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(Int)") XCTAssertEqual( paths["s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOySiSScip"], + "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-(String)") + + XCTAssertEqual( + hashAndKindDisambiguatedPaths["s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOyS2icip"], + "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-4fd0l") + XCTAssertEqual( + hashAndKindDisambiguatedPaths["s:14MixedFramework41CollisionsWithDifferentSubscriptArgumentsOySiSScip"], "/MixedFramework/CollisionsWithDifferentSubscriptArguments/subscript(_:)-757cj") } @@ -836,6 +858,7 @@ class PathHierarchyTests: XCTestCase { let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() + let hashAndKindDisambiguatedPaths = tree.caseInsensitiveDisambiguatedPaths(allowAdvancedDisambiguation: false) // Operators where all characters in the operator name are also allowed in URL paths @@ -869,6 +892,10 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual( // static func > (lhs: Self, rhs: Self) -> Bool paths["s:SLsE1goiySbx_xtFZ::SYNTHESIZED::s:9Operators8MyNumberV"], + "/Operators/MyNumber/_(_:_:)-(Self,_)") + XCTAssertEqual( + // static func > (lhs: Self, rhs: Self) -> Bool + hashAndKindDisambiguatedPaths["s:SLsE1goiySbx_xtFZ::SYNTHESIZED::s:9Operators8MyNumberV"], "/Operators/MyNumber/_(_:_:)-21jxf") XCTAssertEqual( // static func >= (lhs: Self, rhs: Self) -> Bool @@ -880,11 +907,20 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual( // static func / (lhs: MyNumber, rhs: MyNumber) -> MyNumber paths["s:9Operators8MyNumberV1doiyA2C_ACtFZ"], - "/Operators/MyNumber/_(_:_:)-7am4") + "/Operators/MyNumber/_(_:_:)->MyNumber") XCTAssertEqual( // static func /= (lhs: inout MyNumber, rhs: MyNumber) -> MyNumber paths["s:9Operators8MyNumberV2deoiyA2Cz_ACtFZ"], - "/Operators/MyNumber/_=(_:_:)-3m4ko") + "/Operators/MyNumber/_=(_:_:)") // This is the only favored symbol so it doesn't require any disambiguation + XCTAssertEqual( + // static func / (lhs: MyNumber, rhs: MyNumber) -> MyNumber + hashAndKindDisambiguatedPaths["s:9Operators8MyNumberV1doiyA2C_ACtFZ"], + "/Operators/MyNumber/_(_:_:)-7am4") + XCTAssertEqual( + // static func /= (lhs: inout MyNumber, rhs: MyNumber) -> MyNumber + hashAndKindDisambiguatedPaths["s:9Operators8MyNumberV2deoiyA2Cz_ACtFZ"], + "/Operators/MyNumber/_=(_:_:)") // This is the only favored symbol so it doesn't require any disambiguation + } func testFindingRelativePaths() throws { @@ -1148,17 +1184,17 @@ class PathHierarchyTests: XCTestCase { try assertFindsPath("MixedLanguageFramework/Bar/myStringFunction:error:", in: tree, asSymbolID: "c:objc(cs)Bar(cm)myStringFunction:error:") try assertPathCollision("MixedLanguageFramework/Foo", in: tree, collisions: [ - ("c:@E@Foo", "enum"), - ("c:@E@Foo", "struct"), - ("c:MixedLanguageFramework.h@T@Foo", "typealias"), + ("c:@E@Foo", "-enum"), + ("c:@E@Foo", "-struct"), + ("c:MixedLanguageFramework.h@T@Foo", "-typealias"), ]) try assertPathRaisesErrorMessage("MixedLanguageFramework/Foo", in: tree, context: context, expectedErrorMessage: """ 'Foo' is ambiguous at '/MixedLanguageFramework' """) { error in XCTAssertEqual(error.solutions, [ - .init(summary: "Insert 'struct' for\n'struct Foo'", replacements: [("-struct", 26, 26)]), - .init(summary: "Insert 'enum' for\n'typedef enum Foo : NSString { ... } Foo;'", replacements: [("-enum", 26, 26)]), - .init(summary: "Insert 'typealias' for\n'typedef enum Foo : NSString { ... } Foo;'", replacements: [("-typealias", 26, 26)]), + .init(summary: "Insert '-struct' for \n'struct Foo'", replacements: [("-struct", 26, 26)]), + .init(summary: "Insert '-enum' for \n'typedef enum Foo : NSString { ... } Foo;'", replacements: [("-enum", 26, 26)]), + .init(summary: "Insert '-typealias' for \n'typedef enum Foo : NSString { ... } Foo;'", replacements: [("-typealias", 26, 26)]), ]) } // The 'enum' and 'typealias' symbols have multi-line declarations that are presented on a single line @@ -1254,17 +1290,46 @@ class PathHierarchyTests: XCTestCase { // This is the only enum case and can be disambiguated as such XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameyACSScACmF"], "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-enum.case") - // These are all methods and can only be disambiguated with the USR hash + + // These methods have different parameter types and use that for disambiguation. XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSiF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14g8s") + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-(Int)") XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSfF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14ife") + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-(Float)") XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSSF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14ob0") + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-(String)") XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameyS2dF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-4ja8m") + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-(Double)") XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSaySdGF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-88rbf") + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-([Double])") + + let hashAndKindDisambiguatedPaths = tree.caseInsensitiveDisambiguatedPaths(allowAdvancedDisambiguation: false) + + XCTAssertEqual(hashAndKindDisambiguatedPaths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSiF"], + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14g8s") + XCTAssertEqual(hashAndKindDisambiguatedPaths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSfF"], + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14ife") + XCTAssertEqual(hashAndKindDisambiguatedPaths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSSF"], + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14ob0") + + // Verify suggested parameter type value disambiguation + try assertPathCollision("/ShapeKit/OverloadedEnum/firstTestMemberName(_:)", in: tree, collisions: [ + (symbolID: "s:8ShapeKit14OverloadedEnumO19firstTestMemberNameyS2dF", disambiguation: "-(Double)"), + (symbolID: "s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSfF", disambiguation: "-(Float)"), + (symbolID: "s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSiF", disambiguation: "-(Int)"), + (symbolID: "s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSSF", disambiguation: "-(String)"), + (symbolID: "s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSaySdGF", disambiguation: "-([Double])"), + // This enum case is in the same collision as the functions are + (symbolID: "s:8ShapeKit14OverloadedEnumO19firstTestMemberNameyACSScACmF", disambiguation: "-enum.case"), + ]) + + // Verify suggested return type disambiguation + try assertPathCollision("/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)", in: tree, collisions: [ + (symbolID: "s:8ShapeKit18OverloadedProtocolP20fourthTestMemberName4testSdSS_tF", disambiguation: "->Double"), + (symbolID: "s:8ShapeKit18OverloadedProtocolP20fourthTestMemberName4testSfSS_tF", disambiguation: "->Float"), + (symbolID: "s:8ShapeKit18OverloadedProtocolP20fourthTestMemberName4testSiSS_tF", disambiguation: "->Int"), + (symbolID: "s:8ShapeKit18OverloadedProtocolP20fourthTestMemberName4testS2S_tF", disambiguation: "->String"), + ]) } func testOverloadedSymbolsWithOverloadGroups() throws { @@ -1289,19 +1354,376 @@ class PathHierarchyTests: XCTestCase { // This is the only enum case and can be disambiguated as such XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameyACSScACmF"], "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-enum.case") - // These are all methods and can only be disambiguated with the USR hash + // These 4 methods have different parameter types and use that for disambiguation. XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSiF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14g8s") + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-(Int)") XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSfF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14ife") + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-(Float)") XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSSF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-14ob0") - XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameyS2dF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-4ja8m") + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-(String)") XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameySdSaySdGF"], - "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-88rbf") + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-([Double])") + XCTAssertEqual(paths["s:8ShapeKit14OverloadedEnumO19firstTestMemberNameyS2dF"], + "/ShapeKit/OverloadedEnum/firstTestMemberName(_:)-(Double)") } - + + func testApplyingSyntaxSugarToTypeName() { + func functionSignatureParameterTypeName(_ fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment]) -> String? { + return PathHierarchy.functionSignatureTypeNames(for: SymbolGraph.Symbol( + identifier: SymbolGraph.Symbol.Identifier(precise: "some-symbol-id", interfaceLanguage: SourceLanguage.swift.id), + names: .init(title: "SymbolName", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["SymbolName"], docComment: nil, accessLevel: .public, kind: .init(parsedIdentifier: .class, displayName: "Kind Display NAme"), mixins: [ + SymbolGraph.Symbol.FunctionSignature.mixinKey: SymbolGraph.Symbol.FunctionSignature( + parameters: [ + .init(name: "someName", externalName: "with", declarationFragments: [ + .init(kind: .identifier, spelling: "someName", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ", preciseIdentifier: nil), + ] + fragments, children: []) + ], + returns: [] + ) + ]) + )?.parameterTypeNames.first + } + + // Int + XCTAssertEqual("Int", functionSignatureParameterTypeName([ + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + ])) + + // Array + XCTAssertEqual("[Int]", functionSignatureParameterTypeName([ + .init(kind: .typeIdentifier, spelling: "Array", preciseIdentifier: "s:Sa"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ">", preciseIdentifier: nil), + ])) + + // Array<(Int,Double)> + XCTAssertEqual("[(Int,Double)]", functionSignatureParameterTypeName([ + .init(kind: .typeIdentifier, spelling: "Array", preciseIdentifier: "s:Sa"), + .init(kind: .text, spelling: "<(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ",", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"), + .init(kind: .text, spelling: ")>", preciseIdentifier: nil), + ])) + + // Optional + XCTAssertEqual("Int?", functionSignatureParameterTypeName([ + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ">", preciseIdentifier: nil), + ])) + + // Optional<(Int,Double)> + XCTAssertEqual("(Int,Double)?", functionSignatureParameterTypeName([ + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ",", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"), + .init(kind: .text, spelling: ")>", preciseIdentifier: nil), + ])) + + // Array<(Array,Optional>)> + XCTAssertEqual("[([Int],Double??)]", functionSignatureParameterTypeName([ + .init(kind: .typeIdentifier, spelling: "Array", preciseIdentifier: "s:Sa"), + .init(kind: .text, spelling: "<(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Array", preciseIdentifier: "s:Sa"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ">,", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"), + .init(kind: .text, spelling: ">>)>", preciseIdentifier: nil), + ])) + + // Dictionary + XCTAssertEqual("[Double:Int]", functionSignatureParameterTypeName([ + .init(kind: .typeIdentifier, spelling: "Dictionary", preciseIdentifier: "s:SD"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"), + .init(kind: .text, spelling: ",", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ">", preciseIdentifier: nil), + ])) + + // Dictionary<(Optional,String),Array>> + XCTAssertEqual("[(Int?,String):[Double?]]", functionSignatureParameterTypeName([ + .init(kind: .typeIdentifier, spelling: "Dictionary", preciseIdentifier: "s:SD"), + .init(kind: .text, spelling: "<(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ">,", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"), + .init(kind: .text, spelling: "),", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Array", preciseIdentifier: "s:Sa"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"), + .init(kind: .text, spelling: ">>>", preciseIdentifier: nil), + ])) + + // Dictionary>>,Array>>> + XCTAssertEqual("[[Int:[String:Double]]?:[[Int:[String:Double]]]]", functionSignatureParameterTypeName([ + .init(kind: .typeIdentifier, spelling: "Dictionary", preciseIdentifier: "s:SD"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Dictionary", preciseIdentifier: "s:SD"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ",", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Dictionary", preciseIdentifier: "s:SD"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"), + .init(kind: .text, spelling: ",", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"), + .init(kind: .text, spelling: ">>>,", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Array", preciseIdentifier: "s:Sa"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Dictionary", preciseIdentifier: "s:SD"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ",", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Dictionary", preciseIdentifier: "s:SD"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"), + .init(kind: .text, spelling: ",", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"), + .init(kind: .text, spelling: ">>>>", preciseIdentifier: nil), + ])) + } + + func testTypeNamesFromSymbolSignature() throws { + func functionSignatureTypeNames(_ signature: SymbolGraph.Symbol.FunctionSignature) -> (parameterTypeNames: [String], returnTypeNames: [String])? { + return PathHierarchy.functionSignatureTypeNames(for: SymbolGraph.Symbol( + identifier: SymbolGraph.Symbol.Identifier(precise: "some-symbol-id", interfaceLanguage: SourceLanguage.swift.id), + names: .init(title: "SymbolName", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["SymbolName"], docComment: nil, accessLevel: .public, kind: .init(parsedIdentifier: .class, displayName: "Kind Display NAme"), mixins: [ + SymbolGraph.Symbol.FunctionSignature.mixinKey: signature + ]) + ) + } + + // Objective-C types + do { + // - (id)doSomething:(NSString *)someName; + let stringArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "someName", externalName: nil, declarationFragments: [ + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "NSString", preciseIdentifier: "c:objc(cs)NSString"), + .init(kind: .text, spelling: " * )", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "someName", preciseIdentifier: nil), + ], children: []) + ], + returns: [ + .init(kind: .typeIdentifier, spelling: "id", preciseIdentifier: "c:*Qo"), + ]) + ) + XCTAssertEqual(stringArgument?.parameterTypeNames, ["NSString*"]) + XCTAssertEqual(stringArgument?.returnTypeNames, ["id"]) + + // - (void)doSomething:(NSArray *)someName; + let genericArrayArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "someName", externalName: nil, declarationFragments: [ + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "NSArray", preciseIdentifier: "c:Q$objc(cs)NSArray"), + .init(kind: .text, spelling: " * )", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "someName", preciseIdentifier: nil), + ], children: []) + ], + returns: [ + .init(kind: .typeIdentifier, spelling: "void", preciseIdentifier: "c:v"), + ]) + ) + XCTAssertEqual(genericArrayArgument?.parameterTypeNames, ["NSArray*"]) + XCTAssertEqual(genericArrayArgument?.returnTypeNames, []) + + // // - (void)doSomething:(id)someName; + let protocolArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "someName", externalName: nil, declarationFragments: [ + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "id", preciseIdentifier: "c:Qoobjc(pl)MyProtocol"), + .init(kind: .text, spelling: ")", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "someName", preciseIdentifier: nil), + ], children: []) + ], + returns: []) + ) + XCTAssertEqual(protocolArgument?.parameterTypeNames, ["id"]) + + // - (void)doSomething:(NSError **)someName; + let errorArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "someName", externalName: nil, declarationFragments: [ + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "NSError", preciseIdentifier: "c:objc(cs)NSError"), + .init(kind: .text, spelling: " * *)", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "someName", preciseIdentifier: nil), + ], children: []) + ], + returns: []) + ) + XCTAssertEqual(errorArgument?.parameterTypeNames, ["NSError**"]) + + // - (void)doSomething:(NSString * (^)(CGFloat, NSInteger))blockName; + let blockArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "blockName", externalName: nil, declarationFragments: [ + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "NSString", preciseIdentifier: "c:objc(cs)NSString"), + .init(kind: .text, spelling: " * (^", preciseIdentifier: nil), + .init(kind: .text, spelling: ")(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "CGFloat", preciseIdentifier: "c:@T@CGFloat"), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "", preciseIdentifier: nil), + .init(kind: .text, spelling: ", ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "NSInteger", preciseIdentifier: "c:@T@NSInteger"), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "", preciseIdentifier: nil), + .init(kind: .text, spelling: "))", preciseIdentifier: nil), + .init(kind: .internalParameter, spelling: "blockName", preciseIdentifier: nil), + ], children: []) + ], + returns: []) + ) + XCTAssertEqual(blockArgument?.parameterTypeNames, ["NSString*(^)(CGFloat,NSInteger)"]) + } + + // Swift types + do { + // func doSomething(someName: ((Int, String), Date)) -> ([Int, String?]) + let tupleArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "someName", externalName: nil, declarationFragments: [ + .init(kind: .identifier, spelling: "someName", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ((", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ", ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"), + .init(kind: .text, spelling: "), ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Date", preciseIdentifier: "s:10Foundation4DateV"), + .init(kind: .text, spelling: ")", preciseIdentifier: nil), + ], children: []) + ], + returns: [ + .init(kind: .text, spelling: "([", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: "], ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"), + .init(kind: .text, spelling: "?)", preciseIdentifier: nil), + ]) + ) + XCTAssertEqual(tupleArgument?.parameterTypeNames, ["((Int,String),Date)"]) + XCTAssertEqual(tupleArgument?.returnTypeNames, ["([Int],String?)"]) + + // func doSomething(with someName: [Int?: String??]) + let dictionaryWithOptionalsArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "someName", externalName: "with", declarationFragments: [ + .init(kind: .identifier, spelling: "someName", preciseIdentifier: nil), + .init(kind: .text, spelling: ": [", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: "? : ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"), + .init(kind: .text, spelling: "??]", preciseIdentifier: nil), + ], children: []) + ], + returns: [ + .init(kind: .typeIdentifier, spelling: "Void", preciseIdentifier: "s:s4Voida"), + ]) + ) + XCTAssertEqual(dictionaryWithOptionalsArgument?.parameterTypeNames, ["[Int?:String??]"]) + XCTAssertEqual(dictionaryWithOptionalsArgument?.returnTypeNames, []) + + // func doSomething(with someName: Dictionary, Optional<(Optional, Array)>>) + let unsugaredDictionaryWithOptionalsArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "someName", externalName: "with", declarationFragments: [ + .init(kind: .identifier, spelling: "someName", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Dictionary", preciseIdentifier: "s:SD"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ">, ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Optional", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"), + .init(kind: .text, spelling: ">, ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Array", preciseIdentifier: "s:Sa"), + .init(kind: .text, spelling: "<", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"), + .init(kind: .text, spelling: ">)>>", preciseIdentifier: nil), + ], children: []) + ], + returns: []) + ) + XCTAssertEqual(unsugaredDictionaryWithOptionalsArgument?.parameterTypeNames, ["[Int?:(String?,[Double])?]"]) + + // doSomething(someName: repeat each Value) {} + let parameterPackArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "someName", externalName: nil, declarationFragments: [ + .init(kind: .identifier, spelling: "someName", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ", preciseIdentifier: nil), + .init(kind: .keyword, spelling: "repeat", preciseIdentifier: "s:SD"), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .keyword, spelling: "each", preciseIdentifier: "s:Sq"), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Value", preciseIdentifier: "s:24ComplicatedArgumentTypes11doSomething8someNameyxxQp_tRvzlF5ValueL_xmfp"), + ], children: []) + ], + returns: []) + ) + XCTAssertEqual(parameterPackArgument?.parameterTypeNames, ["Value"]) + + // func doSomething(someName: @escaping ((inout Int?, consuming Double, (String, Value)) -> ((Int) -> Value?))) + let complicatedClosureArgument = functionSignatureTypeNames(.init( + parameters: [ + .init(name: "someName", externalName: nil, declarationFragments: [ + .init(kind: .identifier, spelling: "someName", preciseIdentifier: nil), + .init(kind: .text, spelling: ": ", preciseIdentifier: nil), + .init(kind: .attribute, spelling: "@escaping", preciseIdentifier: nil), + .init(kind: .text, spelling: " ((", preciseIdentifier: nil), + .init(kind: .keyword, spelling: "inout", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: "?, ", preciseIdentifier: nil), + .init(kind: .keyword, spelling: "consuming", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Double", preciseIdentifier: "s:Sd"), + .init(kind: .text, spelling: ", (", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "String", preciseIdentifier: "s:SS"), + .init(kind: .text, spelling: ", ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Value", preciseIdentifier: "s:24ComplicatedArgumentTypes11doSomething8someNameyxSgSicSiSgz_SdnSS_xttcSg_tlF5ValueL_xmfp"), + .init(kind: .text, spelling: ")) -> ((", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Int", preciseIdentifier: "s:Si"), + .init(kind: .text, spelling: ") -> ", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "Value", preciseIdentifier: "s:24ComplicatedArgumentTypes11doSomething8someNameyxSgSicSiSgz_SdnSS_xttcSg_tlF5ValueL_xmfp"), + .init(kind: .text, spelling: "))", preciseIdentifier: nil), + ], children: []) + ], + returns: []) + ) + XCTAssertEqual(complicatedClosureArgument?.parameterTypeNames, ["(Int?,Double,(String,Value))->((Int)->Value)"]) + } + } + func testOverloadGroupSymbolsResolveLinksWithoutHash() throws { enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled) @@ -1328,13 +1750,15 @@ class PathHierarchyTests: XCTestCase { try assertPathRaisesErrorMessage("/ShapeKit/OverloadedProtocol/fourthTestMemberName(test:)-abc123", in: tree, context: context, expectedErrorMessage: """ 'abc123' isn't a disambiguation for 'fourthTestMemberName(test:)' at '/ShapeKit/OverloadedProtocol' """) { error in - XCTAssertEqual(error.solutions, [ - .init(summary: "Remove '-abc123' for\n'fourthTestMemberName(test:)'", replacements: [("", 56, 63)]), - .init(summary: "Replace '-abc123' with '-8iuz7' for\n'func fourthTestMemberName(test: String) -> Double\'", replacements: [("-8iuz7", 56, 63)]), - .init(summary: "Replace '-abc123' with '-1h173' for\n'func fourthTestMemberName(test: String) -> Float\'", replacements: [("-1h173", 56, 63)]), - .init(summary: "Replace '-abc123' with '-91hxs' for\n'func fourthTestMemberName(test: String) -> Int\'", replacements: [("-91hxs", 56, 63)]), - .init(summary: "Replace '-abc123' with '-961zx' for\n'func fourthTestMemberName(test: String) -> String\'", replacements: [("-961zx", 56, 63)]), - ]) + XCTAssertEqual(error.solutions.count, 5) + + XCTAssertEqual(error.solutions.dropFirst(0).first, .init(summary: "Remove '-abc123' for \n'fourthTestMemberName(test:)'", replacements: [("", 56, 63)])) + // The overload group is cloned from this symbol and therefore have the same function signature. + // Because there are two collisions with the same signature, this method can only be uniquely disambiguated with its hash. + XCTAssertEqual(error.solutions.dropFirst(1).first, .init(summary: "Replace '-abc123' with '->Double' for \n'func fourthTestMemberName(test: String) -> Double\'", replacements: [("->Double", 56, 63)])) + XCTAssertEqual(error.solutions.dropFirst(2).first, .init(summary: "Replace '-abc123' with '->Float' for \n'func fourthTestMemberName(test: String) -> Float\'", replacements: [("->Float", 56, 63)])) + XCTAssertEqual(error.solutions.dropFirst(3).first, .init(summary: "Replace '-abc123' with '->Int' for \n'func fourthTestMemberName(test: String) -> Int\'", replacements: [("->Int", 56, 63)])) + XCTAssertEqual(error.solutions.dropFirst(4).first, .init(summary: "Replace '-abc123' with '->String' for \n'func fourthTestMemberName(test: String) -> String\'", replacements: [("->String", 56, 63)])) } } @@ -1430,13 +1854,13 @@ class PathHierarchyTests: XCTestCase { let tree = context.linkResolver.localResolver.pathHierarchy try assertPathCollision("Outer/Inner", in: tree, collisions: [ - ("s:m:s:e:s:5Inner0A5ClassC5OuterE9somethingyyF", "module.extension"), - ("s:5Outer5InnerV", "struct"), + ("s:m:s:e:s:5Inner0A5ClassC5OuterE9somethingyyF", "-module.extension"), + ("s:5Outer5InnerV", "-struct"), ]) // If the first path component is ambiguous, it should have the same error as if that was a later path component. try assertPathCollision("Inner", in: tree, collisions: [ - ("s:m:s:e:s:5Inner0A5ClassC5OuterE9somethingyyF", "module.extension"), - ("s:5Outer5InnerV", "struct"), + ("s:m:s:e:s:5Inner0A5ClassC5OuterE9somethingyyF", "-module.extension"), + ("s:5Outer5InnerV", "-struct"), ]) try assertFindsPath("Inner-struct", in: tree, asSymbolID: "s:5Outer5InnerV") @@ -1549,8 +1973,11 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(try tree.findSymbol(path: "/(_:_:)", parent: myNumberID).identifier.precise, "s:9Operators8MyNumberV1doiyA2C_ACtFZ") XCTAssertEqual(try tree.findSymbol(path: "/=(_:_:)", parent: myNumberID).identifier.precise, "s:9Operators8MyNumberV2deoiyA2Cz_ACtFZ") + XCTAssertEqual(try tree.findSymbol(path: "...(_:)->PartialRangeFrom", parent: myNumberID).identifier.precise, "s:SLsE3zzzoPys16PartialRangeFromVyxGxFZ::SYNTHESIZED::s:9Operators8MyNumberV") XCTAssertEqual(try tree.findSymbol(path: "...(_:)-28faz", parent: myNumberID).identifier.precise, "s:SLsE3zzzoPys16PartialRangeFromVyxGxFZ::SYNTHESIZED::s:9Operators8MyNumberV") + XCTAssertEqual(try tree.findSymbol(path: "...(_:)->PartialRangeThrough", parent: myNumberID).identifier.precise, "s:SLsE3zzzopys19PartialRangeThroughVyxGxFZ::SYNTHESIZED::s:9Operators8MyNumberV") XCTAssertEqual(try tree.findSymbol(path: "...(_:)-8ooeh", parent: myNumberID).identifier.precise, "s:SLsE3zzzopys19PartialRangeThroughVyxGxFZ::SYNTHESIZED::s:9Operators8MyNumberV") + XCTAssertEqual(try tree.findSymbol(path: "...(_:_:)", parent: myNumberID).identifier.precise, "s:SLsE3zzzoiySNyxGx_xtFZ::SYNTHESIZED::s:9Operators8MyNumberV") XCTAssertEqual(try tree.findSymbol(path: "..<(_:)", parent: myNumberID).identifier.precise, "s:SLsE3zzlopys16PartialRangeUpToVyxGxFZ::SYNTHESIZED::s:9Operators8MyNumberV") XCTAssertEqual(try tree.findSymbol(path: "..<(_:_:)", parent: myNumberID).identifier.precise, "s:SLsE3zzloiySnyxGx_xtFZ::SYNTHESIZED::s:9Operators8MyNumberV") @@ -1568,9 +1995,9 @@ class PathHierarchyTests: XCTestCase { XCTAssertEqual(try tree.findSymbol(path: "-(_:)-func.op", parent: myNumberID).identifier.precise, "s:s13SignedNumericPsE1sopyxxFZ::SYNTHESIZED::s:9Operators8MyNumberV") XCTAssertEqual(try tree.findSymbol(path: "-=(_:_:)-func.op", parent: myNumberID).identifier.precise, "s:s18AdditiveArithmeticPsE2seoiyyxz_xtFZ::SYNTHESIZED::s:9Operators8MyNumberV") - let paths = tree.caseInsensitiveDisambiguatedPaths() + let paths = tree.caseInsensitiveDisambiguatedPaths(allowAdvancedDisambiguation: false) - // Unmodified operator name in URL + // Unmodified operator name in the path XCTAssertEqual("/Operators/MyNumber/!=(_:_:)", paths["s:SQsE2neoiySbx_xtFZ::SYNTHESIZED::s:9Operators8MyNumberV"]) XCTAssertEqual("/Operators/MyNumber/+(_:_:)", paths["s:9Operators8MyNumberV1poiyA2C_ACtFZ"]) XCTAssertEqual("/Operators/MyNumber/+(_:)", paths["s:s18AdditiveArithmeticPsE1popyxxFZ::SYNTHESIZED::s:9Operators8MyNumberV"]) @@ -1596,7 +2023,29 @@ class PathHierarchyTests: XCTestCase { // "/" is an allowed character in URL paths. XCTAssertEqual("/Operators/MyNumber/_(_:_:)-7am4", paths["s:9Operators8MyNumberV1doiyA2C_ACtFZ"]) - XCTAssertEqual("/Operators/MyNumber/_=(_:_:)-3m4ko", paths["s:9Operators8MyNumberV2deoiyA2Cz_ACtFZ"]) + XCTAssertEqual("/Operators/MyNumber/_=(_:_:)", paths["s:9Operators8MyNumberV2deoiyA2Cz_ACtFZ"]) // This is the only favored symbol so it doesn't require any disambiguation + + // Some of these have more human readable disambiguation alternatives + let humanReadablePaths = tree.caseInsensitiveDisambiguatedPaths() + + XCTAssertEqual("/Operators/MyNumber/...(_:)->PartialRangeFrom", humanReadablePaths["s:SLsE3zzzoPys16PartialRangeFromVyxGxFZ::SYNTHESIZED::s:9Operators8MyNumberV"]) + XCTAssertEqual("/Operators/MyNumber/...(_:)->PartialRangeThrough", humanReadablePaths["s:SLsE3zzzopys19PartialRangeThroughVyxGxFZ::SYNTHESIZED::s:9Operators8MyNumberV"]) + + XCTAssertEqual("/Operators/MyNumber/_(_:_:)-(Self,_)", /* >(_:_:) */ humanReadablePaths["s:SLsE1goiySbx_xtFZ::SYNTHESIZED::s:9Operators8MyNumberV"]) + + XCTAssertEqual("/Operators/MyNumber/_(_:_:)->MyNumber", humanReadablePaths["s:9Operators8MyNumberV1doiyA2C_ACtFZ"]) + XCTAssertEqual("/Operators/MyNumber/_=(_:_:)", humanReadablePaths["s:9Operators8MyNumberV2deoiyA2Cz_ACtFZ"]) // This is the only favored symbol so it doesn't require any disambiguation + + // Verify that all paths are unique + let repeatedPaths: [String: Int] = paths.values.reduce(into: [:], { acc, path in acc[path, default: 0] += 1 }) + .filter { _, frequency in frequency > 1 } + + XCTAssertEqual(repeatedPaths.keys.sorted(), [], "Every path should be unique") + + let repeatedHumanReadablePaths: [String: Int] = humanReadablePaths.values.reduce(into: [:], { acc, path in acc[path, default: 0] += 1 }) + .filter { _, frequency in frequency > 1 } + + XCTAssertEqual(repeatedHumanReadablePaths.keys.sorted(), [], "Every path should be unique") } func testSameNameForSymbolAndContainer() throws { @@ -2190,7 +2639,7 @@ class PathHierarchyTests: XCTestCase { let tree = context.linkResolver.localResolver.pathHierarchy let paths = tree.caseInsensitiveDisambiguatedPaths() - XCTAssertEqual(paths[protocolRequirementID], "/ModuleName/SomeProtocolName/someProtocolRequirement()-8lcpm") + XCTAssertEqual(paths[protocolRequirementID], "/ModuleName/SomeProtocolName/someProtocolRequirement()") // This is the only favored symbol so it doesn't require any disambiguation XCTAssertEqual(paths[defaultImplementationID], "/ModuleName/SomeProtocolName/someProtocolRequirement()-3docm") // Verify that the multi platform paths are the same as the single platform paths @@ -2218,30 +2667,45 @@ class PathHierarchyTests: XCTestCase { // MyClass operator+() const; // unary plus // MyClass operator+(const MyClass& other) const; // addition try assertPathCollision("/CxxOperators/MyClass/operator+", in: tree, collisions: [ - (symbolID: "c:@S@MyClass@F@operator+#1", disambiguation: "15qb6"), - (symbolID: "c:@S@MyClass@F@operator+#&1$@S@MyClass#1", disambiguation: "8k1ef"), + (symbolID: "c:@S@MyClass@F@operator+#1", disambiguation: "-()"), + (symbolID: "c:@S@MyClass@F@operator+#&1$@S@MyClass#1", disambiguation: "-(_)"), ]) try assertFindsPath("/CxxOperators/MyClass/operator+-15qb6", in: tree, asSymbolID: "c:@S@MyClass@F@operator+#1") try assertFindsPath("/CxxOperators/MyClass/operator+-8k1ef", in: tree, asSymbolID: "c:@S@MyClass@F@operator+#&1$@S@MyClass#1") + try assertFindsPath("/CxxOperators/MyClass/operator+-()", in: tree, asSymbolID: "c:@S@MyClass@F@operator+#1") + try assertFindsPath("/CxxOperators/MyClass/operator+-(_)", in: tree, asSymbolID: "c:@S@MyClass@F@operator+#&1$@S@MyClass#1") + try assertFindsPath("/CxxOperators/MyClass/operator+-(MyClass&)", in: tree, asSymbolID: "c:@S@MyClass@F@operator+#&1$@S@MyClass#1") + // MyClass operator-() const; // unary minus // MyClass operator-(const MyClass& other) const; // subtraction try assertPathCollision("/CxxOperators/MyClass/operator-", in: tree, collisions: [ - (symbolID: "c:@S@MyClass@F@operator-#1", disambiguation: "1c6gw"), - (symbolID: "c:@S@MyClass@F@operator-#&1$@S@MyClass#1", disambiguation: "6knvo"), + (symbolID: "c:@S@MyClass@F@operator-#1", disambiguation: "-()"), + (symbolID: "c:@S@MyClass@F@operator-#&1$@S@MyClass#1", disambiguation: "-(_)"), ]) try assertFindsPath("/CxxOperators/MyClass/operator--1c6gw", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#1") try assertFindsPath("/CxxOperators/MyClass/operator--6knvo", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#&1$@S@MyClass#1") + try assertFindsPath("/CxxOperators/MyClass/operator--()", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#1") + try assertFindsPath("/CxxOperators/MyClass/operator--(_)", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#&1$@S@MyClass#1") + try assertFindsPath("/CxxOperators/MyClass/operator--(MyClass&)", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#&1$@S@MyClass#1") + // MyClass& operator*(); // indirect access // MyClass operator*(const MyClass& other) const; // multiplication try assertPathCollision("/CxxOperators/MyClass/operator*", in: tree, collisions: [ - (symbolID: "c:@S@MyClass@F@operator*#&1$@S@MyClass#1", disambiguation: "6oso3"), - (symbolID: "c:@S@MyClass@F@operator*#", disambiguation: "8vjwm"), + (symbolID: "c:@S@MyClass@F@operator*#&1$@S@MyClass#1", disambiguation: "->MyClass"), + (symbolID: "c:@S@MyClass@F@operator*#", disambiguation: "->MyClass&"), ]) try assertFindsPath("/CxxOperators/MyClass/operator*-6oso3", in: tree, asSymbolID: "c:@S@MyClass@F@operator*#&1$@S@MyClass#1") try assertFindsPath("/CxxOperators/MyClass/operator*-8vjwm", in: tree, asSymbolID: "c:@S@MyClass@F@operator*#") - + + try assertFindsPath("/CxxOperators/MyClass/operator*->MyClass", in: tree, asSymbolID: "c:@S@MyClass@F@operator*#&1$@S@MyClass#1") + try assertFindsPath("/CxxOperators/MyClass/operator*->MyClass&", in: tree, asSymbolID: "c:@S@MyClass@F@operator*#") + + try assertFindsPath("/CxxOperators/MyClass/operator*-(_)", in: tree, asSymbolID: "c:@S@MyClass@F@operator*#&1$@S@MyClass#1") + try assertFindsPath("/CxxOperators/MyClass/operator*-(MyClass&)", in: tree, asSymbolID: "c:@S@MyClass@F@operator*#&1$@S@MyClass#1") + try assertFindsPath("/CxxOperators/MyClass/operator*-()", in: tree, asSymbolID: "c:@S@MyClass@F@operator*#") + // MyClass operator/(const MyClass& other) const; try assertFindsPath("/CxxOperators/MyClass/operator/", in: tree, asSymbolID: "c:@S@MyClass@F@operator/#&1$@S@MyClass#1") @@ -2254,12 +2718,15 @@ class PathHierarchyTests: XCTestCase { // MyClass* operator&(); // address-of // MyClass operator&(const MyClass& other) const; // bitwise and try assertPathCollision("/CxxOperators/MyClass/operator&", in: tree, collisions: [ - (symbolID: "c:@S@MyClass@F@operator&#&1$@S@MyClass#1", disambiguation: "3ob2f"), - (symbolID: "c:@S@MyClass@F@operator&#", disambiguation: "8vnp2"), + (symbolID: "c:@S@MyClass@F@operator&#&1$@S@MyClass#1", disambiguation: "->MyClass"), + (symbolID: "c:@S@MyClass@F@operator&#", disambiguation: "->MyClass*"), ]) try assertFindsPath("/CxxOperators/MyClass/operator&-3ob2f", in: tree, asSymbolID: "c:@S@MyClass@F@operator&#&1$@S@MyClass#1") try assertFindsPath("/CxxOperators/MyClass/operator&-8vnp2", in: tree, asSymbolID: "c:@S@MyClass@F@operator&#") - + + try assertFindsPath("/CxxOperators/MyClass/operator&->MyClass", in: tree, asSymbolID: "c:@S@MyClass@F@operator&#&1$@S@MyClass#1") + try assertFindsPath("/CxxOperators/MyClass/operator&->MyClass*", in: tree, asSymbolID: "c:@S@MyClass@F@operator&#") + // MyClass operator|(const MyClass& other) const; try assertFindsPath("/CxxOperators/MyClass/operator|", in: tree, asSymbolID: "c:@S@MyClass@F@operator|#&1$@S@MyClass#1") @@ -2275,20 +2742,34 @@ class PathHierarchyTests: XCTestCase { // MyClass operator++(int); // post-increment // MyClass& operator++(); // pre-increment try assertPathCollision("/CxxOperators/MyClass/operator++", in: tree, collisions: [ - (symbolID: "c:@S@MyClass@F@operator++#", disambiguation: "15swg"), - (symbolID: "c:@S@MyClass@F@operator++#I#", disambiguation: "68oe0"), + (symbolID: "c:@S@MyClass@F@operator++#I#", disambiguation: "->MyClass"), + (symbolID: "c:@S@MyClass@F@operator++#", disambiguation: "->MyClass&"), ]) try assertFindsPath("/CxxOperators/MyClass/operator++-68oe0", in: tree, asSymbolID: "c:@S@MyClass@F@operator++#I#") try assertFindsPath("/CxxOperators/MyClass/operator++-15swg", in: tree, asSymbolID: "c:@S@MyClass@F@operator++#") + + try assertFindsPath("/CxxOperators/MyClass/operator++->MyClass", in: tree, asSymbolID: "c:@S@MyClass@F@operator++#I#") + try assertFindsPath("/CxxOperators/MyClass/operator++->MyClass&", in: tree, asSymbolID: "c:@S@MyClass@F@operator++#") + try assertFindsPath("/CxxOperators/MyClass/operator++-(_)", in: tree, asSymbolID: "c:@S@MyClass@F@operator++#I#") + try assertFindsPath("/CxxOperators/MyClass/operator++-(int)", in: tree, asSymbolID: "c:@S@MyClass@F@operator++#I#") + try assertFindsPath("/CxxOperators/MyClass/operator++-()", in: tree, asSymbolID: "c:@S@MyClass@F@operator++#") + // MyClass operator--(int); // post-decrement // MyClass& operator--(); // pre-decrement try assertPathCollision("/CxxOperators/MyClass/operator--", in: tree, collisions: [ - (symbolID: "c:@S@MyClass@F@operator-#", disambiguation: "8vk0i"), - (symbolID: "c:@S@MyClass@F@operator-#I#", disambiguation: "9wv7m"), + (symbolID: "c:@S@MyClass@F@operator-#I#", disambiguation: "->MyClass"), + (symbolID: "c:@S@MyClass@F@operator-#", disambiguation: "->MyClass&"), ]) try assertFindsPath("/CxxOperators/MyClass/operator---9wv7m", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#I#") try assertFindsPath("/CxxOperators/MyClass/operator---8vk0i", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#") + + try assertFindsPath("/CxxOperators/MyClass/operator--->MyClass", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#I#") + try assertFindsPath("/CxxOperators/MyClass/operator--->MyClass&", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#") + + try assertFindsPath("/CxxOperators/MyClass/operator---(_)", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#I#") + try assertFindsPath("/CxxOperators/MyClass/operator---(int)", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#I#") + try assertFindsPath("/CxxOperators/MyClass/operator---()", in: tree, asSymbolID: "c:@S@MyClass@F@operator-#") // bool operator!() const; try assertFindsPath("/CxxOperators/MyClass/operator!", in: tree, asSymbolID: "c:@S@MyClass@F@operator!#1") @@ -2324,13 +2805,19 @@ class PathHierarchyTests: XCTestCase { // MyClass& operator=(const MyClass& other); // copy assignment // MyClass& operator=(const MyClass&& other); // move assignment try assertPathCollision("/CxxOperators/MyClass/operator=", in: tree, collisions: [ - (symbolID: "c:@S@MyClass@F@operator=#&1$@S@MyClass#", disambiguation: "36ink"), - (symbolID: "c:@S@MyClass@F@operator=#1$@S@MyClass#", disambiguation: "5360m"), - (symbolID: "c:@S@MyClass@F@operator=#&&1$@S@MyClass#", disambiguation: "6e1gm"), + (symbolID: "c:@S@MyClass@F@operator=#&&1$@S@MyClass#", disambiguation: "-(MyClass&&)"), + (symbolID: "c:@S@MyClass@F@operator=#&1$@S@MyClass#", disambiguation: "-(MyClass&)"), + (symbolID: "c:@S@MyClass@F@operator=#1$@S@MyClass#", disambiguation: "->MyClass"), ]) try assertFindsPath("/CxxOperators/MyClass/operator=-5360m", in: tree, asSymbolID: "c:@S@MyClass@F@operator=#1$@S@MyClass#") try assertFindsPath("/CxxOperators/MyClass/operator=-36ink", in: tree, asSymbolID: "c:@S@MyClass@F@operator=#&1$@S@MyClass#") try assertFindsPath("/CxxOperators/MyClass/operator=-6e1gm", in: tree, asSymbolID: "c:@S@MyClass@F@operator=#&&1$@S@MyClass#") + + try assertFindsPath("/CxxOperators/MyClass/operator=->MyClass", in: tree, asSymbolID: "c:@S@MyClass@F@operator=#1$@S@MyClass#") + + try assertFindsPath("/CxxOperators/MyClass/operator=-(MyClass)", in: tree, asSymbolID: "c:@S@MyClass@F@operator=#1$@S@MyClass#") + try assertFindsPath("/CxxOperators/MyClass/operator=-(MyClass&)", in: tree, asSymbolID: "c:@S@MyClass@F@operator=#&1$@S@MyClass#") + try assertFindsPath("/CxxOperators/MyClass/operator=-(MyClass&&)", in: tree, asSymbolID: "c:@S@MyClass@F@operator=#&&1$@S@MyClass#") // MyClass& operator+=(const MyClass& other); try assertFindsPath("/CxxOperators/MyClass/operator+=", in: tree, asSymbolID: "c:@S@MyClass@F@operator+=#&1$@S@MyClass#") @@ -2365,12 +2852,24 @@ class PathHierarchyTests: XCTestCase { // MyClass& operator[](std::string& key); // subscript // static void operator[](MyClass& lhs, MyClass& rhs); // subscript try assertPathCollision("/CxxOperators/MyClass/operator[]", in: tree, collisions: [ - (symbolID: "c:@S@MyClass@F@operator[]#&$@S@MyClass#S0_#S", disambiguation: "8qcye"), - (symbolID: "c:@S@MyClass@F@operator[]#&$@N@std@N@__1@S@basic_string>#C#$@N@std@N@__1@S@char_traits>#C#$@N@std@N@__1@S@allocator>#C#", disambiguation: "9758f"), + (symbolID: "c:@S@MyClass@F@operator[]#&$@S@MyClass#S0_#S", disambiguation: "->()"), + (symbolID: "c:@S@MyClass@F@operator[]#&$@N@std@N@__1@S@basic_string>#C#$@N@std@N@__1@S@char_traits>#C#$@N@std@N@__1@S@allocator>#C#", disambiguation: "->_"), ]) try assertFindsPath("/CxxOperators/MyClass/operator[]-9758f", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@N@std@N@__1@S@basic_string>#C#$@N@std@N@__1@S@char_traits>#C#$@N@std@N@__1@S@allocator>#C#") try assertFindsPath("/CxxOperators/MyClass/operator[]-8qcye", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@S@MyClass#S0_#S") + + try assertFindsPath("/CxxOperators/MyClass/operator[]->_", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@N@std@N@__1@S@basic_string>#C#$@N@std@N@__1@S@char_traits>#C#$@N@std@N@__1@S@allocator>#C#") + try assertFindsPath("/CxxOperators/MyClass/operator[]->MyClass&", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@N@std@N@__1@S@basic_string>#C#$@N@std@N@__1@S@char_traits>#C#$@N@std@N@__1@S@allocator>#C#") + try assertFindsPath("/CxxOperators/MyClass/operator[]->()", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@S@MyClass#S0_#S") + try assertFindsPath("/CxxOperators/MyClass/operator[]-(_)", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@N@std@N@__1@S@basic_string>#C#$@N@std@N@__1@S@char_traits>#C#$@N@std@N@__1@S@allocator>#C#") + try assertFindsPath("/CxxOperators/MyClass/operator[]-(_,_)", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@S@MyClass#S0_#S") + + try assertFindsPath("/CxxOperators/MyClass/operator[]-(std::string&)", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@N@std@N@__1@S@basic_string>#C#$@N@std@N@__1@S@char_traits>#C#$@N@std@N@__1@S@allocator>#C#") + try assertFindsPath("/CxxOperators/MyClass/operator[]-(MyClass&,_)", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@S@MyClass#S0_#S") + try assertFindsPath("/CxxOperators/MyClass/operator[]-(_,MyClass&)", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@S@MyClass#S0_#S") + try assertFindsPath("/CxxOperators/MyClass/operator[]-(MyClass&,MyClass&)", in: tree, asSymbolID: "c:@S@MyClass@F@operator[]#&$@S@MyClass#S0_#S") + // MyClass& operator->(); try assertFindsPath("/CxxOperators/MyClass/operator->", in: tree, asSymbolID: "c:@S@MyClass@F@operator->#") @@ -2380,12 +2879,16 @@ class PathHierarchyTests: XCTestCase { // MyClass& operator()(MyClass& arg1, MyClass& arg2, MyClass& arg3); // function-call // static void operator()(MyClass& lhs, MyClass& rhs); // function-call try assertPathCollision("/CxxOperators/MyClass/operator()", in: tree, collisions: [ - (symbolID: "c:@S@MyClass@F@operator()#&$@S@MyClass#S0_#S", disambiguation: "212ks"), - (symbolID: "c:@S@MyClass@F@operator()#&$@S@MyClass#S0_#S0_#", disambiguation: "65g9a"), + (symbolID: "c:@S@MyClass@F@operator()#&$@S@MyClass#S0_#S", disambiguation: "->()"), + (symbolID: "c:@S@MyClass@F@operator()#&$@S@MyClass#S0_#S0_#", disambiguation: "->_"), ]) try assertFindsPath("/CxxOperators/MyClass/operator()-65g9a", in: tree, asSymbolID: "c:@S@MyClass@F@operator()#&$@S@MyClass#S0_#S0_#") try assertFindsPath("/CxxOperators/MyClass/operator()-212ks", in: tree, asSymbolID: "c:@S@MyClass@F@operator()#&$@S@MyClass#S0_#S") + try assertFindsPath("/CxxOperators/MyClass/operator()->_", in: tree, asSymbolID: "c:@S@MyClass@F@operator()#&$@S@MyClass#S0_#S0_#") + try assertFindsPath("/CxxOperators/MyClass/operator()->()", in: tree, asSymbolID: "c:@S@MyClass@F@operator()#&$@S@MyClass#S0_#S") + + // MyClass& operator,(MyClass& other); try assertFindsPath("/CxxOperators/MyClass/operator,", in: tree, asSymbolID: "c:@S@MyClass@F@operator,#&$@S@MyClass#") } @@ -2399,7 +2902,7 @@ class PathHierarchyTests: XCTestCase { assertParsedPathComponents("first/", [("first", nil)]) assertParsedPathComponents("first/second/third", [("first", nil), ("second", nil), ("third", nil)]) assertParsedPathComponents("/first/second/third", [("first", nil), ("second", nil), ("third", nil)]) - assertParsedPathComponents("first/", [("first", nil)]) + assertParsedPathComponents("first/", [("first", nil)]) assertParsedPathComponents("first//second", [("first", nil), ("/second", nil)]) assertParsedPathComponents("first/second#third", [("first", nil), ("second", nil), ("third", nil)]) assertParsedPathComponents("#first", [("first", nil)]) @@ -2430,10 +2933,54 @@ class PathHierarchyTests: XCTestCase { assertParsedPathComponents("+/-(_:_:)-func.op-hash", [("+/-(_:_:)", .kindAndHash(kind: "func.op", hash: "hash"))]) assertParsedPathComponents("+/-(_:_:)/+/-(_:_:)/+/-(_:_:)/+/-(_:_:)", [("+/-(_:_:)", nil), ("+/-(_:_:)", nil), ("+/-(_:_:)", nil), ("+/-(_:_:)", nil)]) assertParsedPathComponents("+/-(_:_:)-hash/+/-(_:_:)-func.op/+/-(_:_:)-func.op-hash/+/-(_:_:)", [("+/-(_:_:)", .kindAndHash(kind: nil, hash: "hash")), ("+/-(_:_:)", .kindAndHash(kind: "func.op", hash: nil)), ("+/-(_:_:)", .kindAndHash(kind: "func.op", hash: "hash")), ("+/-(_:_:)", nil)]) - + assertParsedPathComponents("MyNumber//=(_:_:)", [("MyNumber", nil), ("/=(_:_:)", nil)]) assertParsedPathComponents("MyNumber////=(_:_:)", [("MyNumber", nil), ("///=(_:_:)", nil)]) assertParsedPathComponents("MyNumber/+/-(_:_:)", [("MyNumber", nil), ("+/-(_:_:)", nil)]) + + // Check parsing return values and parameter types + assertParsedPathComponents("..<(_:_:)->Bool", [("..<(_:_:)", .typeSignature(parameterTypes: nil, returnTypes: ["Bool"]))]) + assertParsedPathComponents("..<(_:_:)-(_,Int)", [("..<(_:_:)", .typeSignature(parameterTypes: ["_", "Int"], returnTypes: nil))]) + + assertParsedPathComponents("something(first:second:third:)->(_,_,_)", [("something(first:second:third:)", .typeSignature(parameterTypes: nil, returnTypes: ["_", "_", "_"]))]) + + assertParsedPathComponents("something(first:second:third:)->(String,_,_)", [("something(first:second:third:)", .typeSignature(parameterTypes: nil, returnTypes: ["String", "_", "_"]))]) + assertParsedPathComponents("something(first:second:third:)->(_,Int,_)", [("something(first:second:third:)", .typeSignature(parameterTypes: nil, returnTypes: ["_", "Int", "_"]))]) + assertParsedPathComponents("something(first:second:third:)->(_,_,Bool)", [("something(first:second:third:)", .typeSignature(parameterTypes: nil, returnTypes: ["_", "_", "Bool"]))]) + + assertParsedPathComponents("something(first:second:third:)->(String,Int,_)", [("something(first:second:third:)", .typeSignature(parameterTypes: nil, returnTypes: ["String", "Int", "_"]))]) + assertParsedPathComponents("something(first:second:third:)->(String,_,Bool)", [("something(first:second:third:)", .typeSignature(parameterTypes: nil, returnTypes: ["String", "_", "Bool"]))]) + assertParsedPathComponents("something(first:second:third:)->(_,Int,Bool)", [("something(first:second:third:)", .typeSignature(parameterTypes: nil, returnTypes: ["_", "Int", "Bool"]))]) + + assertParsedPathComponents("something(first:second:third:)->(String,Int,Bool)", [("something(first:second:third:)", .typeSignature(parameterTypes: nil, returnTypes: ["String", "Int", "Bool"]))]) + + // Check closure parameters + assertParsedPathComponents("map(_:)-((Element)->T)", [("map(_:)", .typeSignature(parameterTypes: ["(Element)->T"], returnTypes: nil))]) + assertParsedPathComponents("map(_:)->[T]", [("map(_:)", .typeSignature(parameterTypes: nil, returnTypes: ["[T]"]))]) + + assertParsedPathComponents("filter(_:)-((Element)->Bool)", [("filter(_:)", .typeSignature(parameterTypes: ["(Element)->Bool"], returnTypes: nil))]) + assertParsedPathComponents("filter(_:)->[Element]", [("filter(_:)", .typeSignature(parameterTypes: nil, returnTypes: ["[Element]"]))]) + + assertParsedPathComponents("reduce(_:_:)-(Result,_)", [("reduce(_:_:)", .typeSignature(parameterTypes: ["Result", "_"], returnTypes: nil))]) + assertParsedPathComponents("reduce(_:_:)-(_,(Result,Element)->Result)", [("reduce(_:_:)", .typeSignature(parameterTypes: ["_", "(Result,Element)->Result"], returnTypes: nil))]) + + assertParsedPathComponents("partition(by:)-((Element)->Bool)", [("partition(by:)", .typeSignature(parameterTypes: ["(Element)->Bool"], returnTypes: nil))]) + assertParsedPathComponents("partition(by:)->Index", [("partition(by:)", .typeSignature(parameterTypes: nil, returnTypes: ["Index"]))]) + + assertParsedPathComponents("max(by:)-((Element,Element)->Bool)", [("max(by:)", .typeSignature(parameterTypes: ["(Element,Element)->Bool"], returnTypes: nil))]) + assertParsedPathComponents("max(by:)->Element?", [("max(by:)", .typeSignature(parameterTypes: nil, returnTypes: ["Element?"]))]) + + // Nested tuples + assertParsedPathComponents("functionName->((A,(B,C),D),(E,F),G)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["(A,(B,C),D)", "(E,F)", "G"]))]) + assertParsedPathComponents("functionName-((A,(B,C),D),(E,F),G)", [("functionName", .typeSignature(parameterTypes: ["(A,(B,C),D)", "(E,F)", "G"], returnTypes: nil))]) + + // Nested closures + assertParsedPathComponents("functionName->((A)->B,(C,(D)->E),(F,(G)->H)->I)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["(A)->B", "(C,(D)->E)", "(F,(G)->H)->I"]))]) + + // Unicode characters and accents + assertParsedPathComponents("functionName->((Å,(𝔹,©),Δ),(∃,⨍),𝄞)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["(Å,(𝔹,©),Δ)", "(∃,⨍)", "𝄞"]))]) + assertParsedPathComponents("functionName-((Å,(𝔹,©),Δ),(∃,⨍),𝄞)", [("functionName", .typeSignature(parameterTypes: ["(Å,(𝔹,©),Δ)", "(∃,⨍)", "𝄞"], returnTypes: nil))]) + assertParsedPathComponents("functionName->((Å)->𝔹,(©,(Δ)->∃),(⨍,(𝄞)->ℌ)->𝓲)", [("functionName", .typeSignature(parameterTypes: nil, returnTypes: ["(Å)->𝔹", "(©,(Δ)->∃)", "(⨍,(𝄞)->ℌ)->𝓲"]))]) let knownCxxOperators = [ // Arithmetic @@ -2469,11 +3016,23 @@ class PathHierarchyTests: XCTestCase { assertParsedPathComponents("\(operatorName)-c++.func.op", [(operatorName, .kindAndHash(kind: "func.op", hash: nil))]) assertParsedPathComponents("\(operatorName)-func.op-hash", [(operatorName, .kindAndHash(kind: "func.op", hash: "hash"))]) + // With type disambiguation + assertParsedPathComponents("\(operatorName)->()", [(operatorName, .typeSignature(parameterTypes: nil, returnTypes: []))]) + assertParsedPathComponents("\(operatorName)->_", [(operatorName, .typeSignature(parameterTypes: nil, returnTypes: ["_"]))]) + assertParsedPathComponents("\(operatorName)->ReturnType", [(operatorName, .typeSignature(parameterTypes: nil, returnTypes: ["ReturnType"]))]) + + assertParsedPathComponents("\(operatorName)-()", [(operatorName, .typeSignature(parameterTypes: [], returnTypes: nil))]) + assertParsedPathComponents("\(operatorName)-(_,_)", [(operatorName, .typeSignature(parameterTypes: ["_","_"], returnTypes: nil))]) + assertParsedPathComponents("\(operatorName)-(ParameterType,_)", [(operatorName, .typeSignature(parameterTypes: ["ParameterType","_"], returnTypes: nil))]) + assertParsedPathComponents("\(operatorName)-(_,ParameterType)", [(operatorName, .typeSignature(parameterTypes: ["_","ParameterType"], returnTypes: nil))]) + // With a trailing anchor component assertParsedPathComponents("\(operatorName)#SomeAnchor", [(operatorName, nil), ("SomeAnchor", nil)]) assertParsedPathComponents("\(operatorName)-hash#SomeAnchor", [(operatorName, .kindAndHash(kind: nil, hash: "hash")), ("SomeAnchor", nil)]) assertParsedPathComponents("\(operatorName)-func.op#SomeAnchor", [(operatorName, .kindAndHash(kind: "func.op", hash: nil)), ("SomeAnchor", nil)]) } + + assertParsedPathComponents("operator[]-(std::string&)->std::string&", [("operator[]", .typeSignature(parameterTypes: ["std::string&"], returnTypes: ["std::string&"]))]) } func testResolveExternalLinkFromTechnologyRoot() throws { @@ -2574,6 +3133,9 @@ class PathHierarchyTests: XCTestCase { case (.kindAndHash(let actualKind, let actualHash), .kindAndHash(let expectedKind, let expectedHash)): XCTAssertEqual(actualKind, expectedKind, "Incorrect kind disambiguation for \(path.singleQuoted)", file: file, line: line) XCTAssertEqual(actualHash, expectedHash, "Incorrect hash disambiguation for \(path.singleQuoted)", file: file, line: line) + case (.typeSignature(let actualParameters, let actualReturns), .typeSignature(let expectedParameters, let expectedReturns)): + XCTAssertEqual(actualParameters, expectedParameters, "Incorrect parameter type disambiguation", file: file, line: line) + XCTAssertEqual(actualReturns, expectedReturns, "Incorrect return type disambiguation", file: file, line: line) case (nil, nil): continue default: @@ -2608,7 +3170,7 @@ private extension TopicReferenceResolutionErrorInfo { } } -private struct SimplifiedSolution: Equatable { +private struct SimplifiedSolution: Equatable, CustomStringConvertible { let summary: String let replacements: [(String, start: Int, end: Int)] @@ -2616,4 +3178,15 @@ private struct SimplifiedSolution: Equatable { return lhs.summary == rhs.summary && lhs.replacements.elementsEqual(rhs.replacements, by: ==) } + + var description: String { + """ + { + summary: \(summary.components(separatedBy: .newlines).joined(separator: "\n ")) + replacements: [ + \(replacements.map { " \"\($0.0)\" at \($0.start)-\($0.end)" }.joined(separator: "\n")) + ] + } + """ + } } diff --git a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift index ce517898cb..bd34e077e9 100644 --- a/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/Symbol Link Resolution/AbsoluteSymbolLinkTests.swift @@ -475,24 +475,24 @@ class AbsoluteSymbolLinkTests: XCTestCase { basePathComponents: [] } """, - // doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func()-2dxqn: + // 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: (idHash: '2dxqn'))] + basePathComponents: [(name: 'func()', suffix: (none))] } """, - // doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func()-6ijsi: + // 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: '6ijsi'))] + basePathComponents: [(name: 'func()', suffix: (idHash: '2dxqn'))] } """, // doc://org.swift.docc.example/documentation/SideKit/UncuratedClass: diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index dbb7d72e4d..2281957a19 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -744,7 +744,7 @@ class ExternalLinkableTests: XCTestCase { let bundle = context.bundle(identifier: "com.example.mymodule")! let converter = DocumentationNodeConverter(bundle: bundle, context: context) - let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MyModule/MyClass/myFunc()-9sdsh", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/MyModule/MyClass/myFunc()", sourceLanguage: .swift)) let renderNode = try converter.convert(node) let summaries = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode) diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift index 2cd3c98652..7eb586bc8d 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift @@ -2165,10 +2165,7 @@ Document var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode - guard let requiredFuncReference = renderNode.references["doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func()-6ijsi"] else { - XCTFail("Could not find reference for ") - return - } + let requiredFuncReference = try XCTUnwrap(renderNode.references["doc://org.swift.docc.example/documentation/SideKit/SideProtocol/func()"]) XCTAssertEqual((requiredFuncReference as? TopicRenderReference)?.required, true) XCTAssertEqual((requiredFuncReference as? TopicRenderReference)?.defaultImplementationCount, 1) @@ -2176,7 +2173,7 @@ Document // Verify that a required symbol includes a required metadata and default implementations do { - let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/SideKit/SideProtocol/func()-6ijsi", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/SideKit/SideProtocol/func()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode @@ -2196,7 +2193,7 @@ Document // Verify that a required symbol does not include default implementations in Topics groups do { - let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/SideKit/SideProtocol/func()-6ijsi", sourceLanguage: .swift)) + let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/SideKit/SideProtocol/func()", sourceLanguage: .swift)) let symbol = node.semantic as! Symbol var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: node.reference) let renderNode = translator.visit(symbol) as! RenderNode diff --git a/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift b/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift index f7070f97bc..adbafad02d 100644 --- a/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift +++ b/Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift @@ -176,7 +176,7 @@ class DeclarationsRenderSectionTests: XCTestCase { // func overload1(param: [Int: Int]) {} let reference = ResolvedTopicReference( bundleIdentifier: bundle.identifier, - path: "/documentation/FancyOverloads/overload1(param:)-8nk5z", + path: "/documentation/FancyOverloads/overload1(param:)", sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) @@ -227,7 +227,7 @@ class DeclarationsRenderSectionTests: XCTestCase { // func overload2(p1: ((Int) -> Int)?, p2: Int) {} // <- overload group let reference = ResolvedTopicReference( bundleIdentifier: bundle.identifier, - path: "/documentation/FancyOverloads/overload2(p1:p2:)-4p1sq", + path: "/documentation/FancyOverloads/overload2(p1:p2:)", sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) @@ -282,7 +282,7 @@ class DeclarationsRenderSectionTests: XCTestCase { // func overload3(_ p: [K: V]) {} let reference = ResolvedTopicReference( bundleIdentifier: bundle.identifier, - path: "/documentation/FancyOverloads/overload3(_:)-xql2", + path: "/documentation/FancyOverloads/overload3(_:)", sourceLanguage: .swift ) let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) diff --git a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift index 2357fa61c1..b2657b0360 100644 --- a/Tests/SwiftDocCTests/Semantics/SymbolTests.swift +++ b/Tests/SwiftDocCTests/Semantics/SymbolTests.swift @@ -617,8 +617,8 @@ class SymbolTests: XCTestCase { XCTAssertEqual(problem.possibleSolutions.count, 2) XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ - ["Insert '33vaw' for\n'init()'", "-33vaw"], - ["Insert '3743d' for\n'init()'", "-3743d"], + ["Insert '-33vaw' for \n'init()'", "-33vaw"], + ["Insert '-3743d' for \n'init()'", "-3743d"], ]) XCTAssertEqual(try problem.possibleSolutions.first!.applyTo(contentsOf: url.appendingPathComponent("documentation/myclass.md")), """ # ``MyKit/MyClass`` @@ -665,8 +665,8 @@ class SymbolTests: XCTestCase { XCTAssertEqual(problem.possibleSolutions.count, 2) XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ - ["Replace 'swift.init' with '33vaw' for\n'init()'", "-33vaw"], - ["Replace 'swift.init' with '3743d' for\n'init()'", "-3743d"], + ["Replace 'swift.init' with '33vaw' for \n'init()'", "-33vaw"], + ["Replace 'swift.init' with '3743d' for \n'init()'", "-3743d"], ]) XCTAssertEqual(try problem.possibleSolutions.first!.applyTo(contentsOf: url.appendingPathComponent("documentation/myclass.md")), """ # ``MyKit/MyClass`` @@ -713,8 +713,8 @@ class SymbolTests: XCTestCase { XCTAssertEqual(problem.possibleSolutions.count, 2) XCTAssert(problem.possibleSolutions.map(\.replacements.count).allSatisfy { $0 == 1 }) XCTAssertEqual(problem.possibleSolutions.map { [$0.summary, $0.replacements.first!.replacement] }, [ - ["Replace 'swift.init' with '33vaw' for\n'init()'", "-33vaw"], - ["Replace 'swift.init' with '3743d' for\n'init()'", "-3743d"], + ["Replace 'swift.init' with '33vaw' for \n'init()'", "-33vaw"], + ["Replace 'swift.init' with '3743d' for \n'init()'", "-3743d"], ]) XCTAssertEqual(try problem.possibleSolutions.first!.applyTo(contentsOf: url.appendingPathComponent("documentation/myclass.md")), """ # ``MyKit/MyClass`` diff --git a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFrameworkWithLanguageRefinements.docc/symbol-graph/swift/x86_64-apple-macos/MixedFramework.symbols.json b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFrameworkWithLanguageRefinements.docc/symbol-graph/swift/x86_64-apple-macos/MixedFramework.symbols.json index f603c35f4e..06b42f3fb9 100644 --- a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFrameworkWithLanguageRefinements.docc/symbol-graph/swift/x86_64-apple-macos/MixedFramework.symbols.json +++ b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFrameworkWithLanguageRefinements.docc/symbol-graph/swift/x86_64-apple-macos/MixedFramework.symbols.json @@ -6070,6 +6070,35 @@ } ] }, + "functionSignature": { + "parameters": [ + { + "name": "something", + "declarationFragments": [ + { + "kind": "identifier", + "spelling": "something" + }, + { + "kind": "text", + "spelling": ": " + }, + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + } + ] + } + ], + "returns": [ + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + } + ] + }, "declarationFragments": [ { "kind": "keyword", @@ -9821,6 +9850,35 @@ } ] }, + "functionSignature": { + "parameters": [ + { + "name": "somethingElse", + "declarationFragments": [ + { + "kind": "identifier", + "spelling": "somethingElse" + }, + { + "kind": "text", + "spelling": ": " + }, + { + "kind": "typeIdentifier", + "spelling": "String", + "preciseIdentifier": "s:SS" + } + ] + } + ], + "returns": [ + { + "kind": "typeIdentifier", + "spelling": "Int", + "preciseIdentifier": "s:Si" + } + ] + }, "declarationFragments": [ { "kind": "keyword", @@ -15396,4 +15454,4 @@ "targetFallback": "Swift.CustomDebugStringConvertible" } ] -} \ No newline at end of file +} diff --git a/Tests/SwiftDocCTests/Test Resources/TestBundle-RenderIndex.json b/Tests/SwiftDocCTests/Test Resources/TestBundle-RenderIndex.json index 70d3d01c43..2069a8f765 100644 --- a/Tests/SwiftDocCTests/Test Resources/TestBundle-RenderIndex.json +++ b/Tests/SwiftDocCTests/Test Resources/TestBundle-RenderIndex.json @@ -730,7 +730,7 @@ "type" : "groupMarker" } ], - "path" : "\/documentation\/sidekit\/sideprotocol\/func()-6ijsi", + "path" : "\/documentation\/sidekit\/sideprotocol\/func()", "title" : "func1()", "type" : "method" } @@ -740,13 +740,13 @@ "type" : "protocol" }, { - "title": "Classes", - "type": "groupMarker" + "title" : "Classes", + "type" : "groupMarker" }, { - "path": "/documentation/sidekit/uncuratedclass", - "title": "UncuratedClass", - "type": "class", + "path" : "\/documentation\/sidekit\/uncuratedclass", + "title" : "UncuratedClass", + "type" : "class" }, { "title" : "Chapter 1",