Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split PathHierarchy implementation into different files. #662

Merged
merged 4 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2023 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

private let nonAllowedPathCharacters = CharacterSet.urlPathAllowed.inverted

private func symbolFileName(_ symbolName: String) -> String {
return symbolName.components(separatedBy: nonAllowedPathCharacters).joined(separator: "_")
}

extension PathHierarchy {
/// Determines the least disambiguated paths for all symbols in the path hierarchy.
///
/// - 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.
/// - Returns: A map of unique identifier strings to disambiguated file paths
func caseInsensitiveDisambiguatedPaths(
includeDisambiguationForUnambiguousChildren: Bool = false,
includeLanguage: Bool = false
) -> [String: String] {
func descend(_ node: Node, accumulatedPath: String) -> [(String, (String, Bool))] {
var results: [(String, (String, Bool))] = []
let caseInsensitiveChildren = [String: DisambiguationContainer](node.children.map { (symbolFileName($0.key.lowercased()), $0.value) }, uniquingKeysWith: { $0.merge(with: $1) })

for (_, tree) in caseInsensitiveChildren {
let disambiguatedChildren = tree.disambiguatedValuesWithCollapsedUniqueSymbols(includeLanguage: includeLanguage)
let uniqueNodesWithChildren = Set(disambiguatedChildren.filter { $0.disambiguation.value() != nil && !$0.value.children.isEmpty }.map { $0.value.symbol?.identifier.precise })
for (node, disambiguation) in disambiguatedChildren {
var path: String
if node.identifier == nil && disambiguatedChildren.count == 1 {
// When descending through placeholder nodes, we trust that the known disambiguation
// that they were created with is necessary.
var knownDisambiguation = ""
let (kind, subtree) = tree.storage.first!
if kind != "_" {
knownDisambiguation += "-\(kind)"
}
let hash = subtree.keys.first!
if hash != "_" {
knownDisambiguation += "-\(hash)"
}
path = accumulatedPath + "/" + symbolFileName(node.name) + knownDisambiguation
} else {
path = accumulatedPath + "/" + symbolFileName(node.name)
}
if let symbol = node.symbol {
results.append(
(symbol.identifier.precise, (path + disambiguation.makeSuffix(), symbol.identifier.interfaceLanguage == "swift"))
)
}
if includeDisambiguationForUnambiguousChildren || uniqueNodesWithChildren.count > 1 {
path += disambiguation.makeSuffix()
}
results += descend(node, accumulatedPath: path)
}
}
return results
}

var gathered: [(String, (String, Bool))] = []

for (moduleName, node) in modules {
let path = "/" + moduleName
gathered.append(
(moduleName, (path, node.symbol == nil || node.symbol!.identifier.interfaceLanguage == "swift"))
)
gathered += descend(node, accumulatedPath: path)
}

// If a symbol node exist in multiple languages, prioritize the Swift variant.
let result = [String: (String, Bool)](gathered, uniquingKeysWith: { lhs, rhs in lhs.1 ? lhs : rhs }).mapValues({ $0.0 })

assert(
Set(result.values).count == result.keys.count,
{
let collisionDescriptions = result
.reduce(into: [String: [String]](), { $0[$1.value, default: []].append($1.key) })
.filter({ $0.value.count > 1 })
.map { "\($0.key)\n\($0.value.map({ " " + $0 }).joined(separator: "\n"))" }
return """
Disambiguated paths contain \(collisionDescriptions.count) collision(s):
\(collisionDescriptions.joined(separator: "\n"))
"""
}()
)

return result
}
}

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)] {
if storage.count == 1 {
let tree = storage.values.first!
if tree.count == 1 {
return [(tree.values.first!, .none)]
}
}

var collisions: [(value: PathHierarchy.Node, disambiguation: Disambiguation)] = []
for (kind, kindTree) in storage {
if kindTree.count == 1 {
// No other match has this kind
if includeLanguage, let symbol = kindTree.first!.value.symbol {
collisions.append((value: kindTree.first!.value, disambiguation: .kind("\(SourceLanguage(id: symbol.identifier.interfaceLanguage).linkDisambiguationID).\(kind)")))
} else {
collisions.append((value: kindTree.first!.value, disambiguation: .kind(kind)))
}
continue
}
for (usr, value) in kindTree {
collisions.append((value: value, disambiguation: .hash(usr)))
}
}
return collisions
}

/// 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)] {
typealias DisambiguationPair = (String, String)

var uniqueSymbolIDs = [String: [DisambiguationPair]]()
var nonSymbols = [DisambiguationPair]()
for (kind, kindTree) in storage {
for (hash, value) in kindTree {
guard let symbol = value.symbol else {
nonSymbols.append((kind, hash))
continue
}
if symbol.identifier.interfaceLanguage == "swift" {
uniqueSymbolIDs[symbol.identifier.precise, default: []].insert((kind, hash), at: 0)
} else {
uniqueSymbolIDs[symbol.identifier.precise, default: []].append((kind, hash))
}
}
}

var duplicateSymbols = [String: ArraySlice<DisambiguationPair>]()

var new = Self()
for (kind, hash) in nonSymbols {
new.add(kind, hash, storage[kind]![hash]!)
}
for (id, symbolDisambiguations) in uniqueSymbolIDs {
let (kind, hash) = symbolDisambiguations[0]
new.add(kind, hash, storage[kind]![hash]!)

if symbolDisambiguations.count > 1 {
duplicateSymbols[id] = symbolDisambiguations.dropFirst()
}
}

var disambiguated = new.disambiguatedValues(includeLanguage: includeLanguage)
guard !duplicateSymbols.isEmpty else {
return disambiguated
}

for (id, disambiguations) in duplicateSymbols {
let primaryDisambiguation = disambiguated.first(where: { $0.value.symbol?.identifier.precise == id })!.disambiguation
for (kind, hash) in disambiguations {
disambiguated.append((storage[kind]![hash]!, primaryDisambiguation.updated(kind: kind, hash: hash)))
}
}

return disambiguated
}

/// The computed disambiguation for a given path hierarchy node.
enum Disambiguation {
/// 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
}
}
/// Makes a new disambiguation suffix string.
func makeSuffix() -> String {
switch self {
case .none:
return ""
case .kind(let value), .hash(let value):
return "-"+value
}
}

/// Creates a new disambiguation with a new kind or hash value.
func updated(kind: String, hash: String) -> Self {
switch self {
case .none:
return .none
case .kind:
return .kind(kind)
case .hash:
return .hash(hash)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2023 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

// This API isn't exposed anywhere and is only used from a debugger.
#if DEBUG

/// A node in a tree structure that can be printed into a visual representation for debugging.
private struct DumpableNode {
var name: String
var children: [DumpableNode]
}

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.
return DumpableNode(
name: symbol.map { "{ \($0.identifier.precise) : \($0.identifier.interfaceLanguage).\($0.kind.identifier.identifier) }" } ?? "[ \(name) ]",
children:
children.sorted(by: \.key).map { (key, disambiguationTree) -> DumpableNode in
DumpableNode(
name: key,
children: disambiguationTree.storage.sorted(by: \.key).map { (kind, kindTree) -> DumpableNode in
DumpableNode(
name: kind,
children: kindTree.sorted(by: \.key).map { (usr, node) -> DumpableNode in
DumpableNode(
name: usr,
children: [node.dumpableNode()]
)
}
)
}
)
}
)
}
}

extension PathHierarchy {
/// Creates a visual representation or the path hierarchy for debugging.
func dump() -> String {
var children = modules.sorted(by: \.key).map { $0.value.dumpableNode() }
if articlesContainer.symbol == nil {
children.append(articlesContainer.dumpableNode()) // The article parent can be the same node as the module
}
children.append(contentsOf: [tutorialContainer.dumpableNode(), tutorialOverviewContainer.dumpableNode()])

let root = DumpableNode(name: ".", children: children)
return Self.dump(root)
}

fileprivate static func dump(_ node: DumpableNode, decorator: String = "") -> String {
var result = ""
result.append("\(decorator) \(node.name)\n")

let children = node.children
for (index, child) in children.enumerated() {
var decorator = decorator
if decorator.hasSuffix("├") {
decorator = decorator.dropLast() + "│"
}
if decorator.hasSuffix("╰") {
decorator = decorator.dropLast() + " "
}
let newDecorator = decorator + " " + (index == children.count-1 ? "╰" : "├")
result.append(dump(child, decorator: newDecorator))
}
return result
}
}

#endif
Loading