Skip to content

Commit 0571862

Browse files
committed
add tests relevant to handling extensions to external types
- test detection of the Extension Block Symbol Graph Format and application of the ExtendedTypesFormatTransformation in SymbolGraphLoader - test the ExtendedTypesFormatTransformation - test handling of collisions resulting from extensions to nested external types in DocumentationContext - test tests for absolute/relative reference resolution for ambiguous relative references
1 parent f1dda04 commit 0571862

21 files changed

+862
-293
lines changed

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ let package = Package(
7979
// Test utility library
8080
.target(
8181
name: "SwiftDocCTestUtilities",
82-
dependencies: []),
82+
dependencies: [
83+
"SymbolKit"
84+
]),
8385

8486
// Command-line tool
8587
.executableTarget(

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 0 additions & 238 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,244 +1021,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
10211021

10221022
return ((reference, symbol.uniqueIdentifier, graphNode, documentation), [])
10231023
}
1024-
1025-
/// Returns a map between symbol identifiers and topic references.
1026-
///
1027-
/// - Parameters:
1028-
/// - symbolGraph: The complete symbol graph to walk through.
1029-
/// - bundle: The bundle to use when creating symbol references.
1030-
func referencesForSymbols(in unifiedGraphs: [String: UnifiedSymbolGraph], bundle: DocumentationBundle) -> [SymbolGraph.Symbol.Identifier: [ResolvedTopicReference]] {
1031-
// The implementation of this function is fairly tricky because in most cases it has to preserve past behavior.
1032-
//
1033-
// This is because symbol references bake the disambiguators into the path, making it the only version of that
1034-
// path that resolves to that symbol. In other words, a reference with "too few" or "too many" disambiguators
1035-
// will fail to resolve. Changing what's considered the "correct" disambiguators for a symbol means that links
1036-
// that used to resolve will break with the new behavior.
1037-
//
1038-
// The tests in `SymbolDisambiguationTests` cover the known behaviors that should be preserved.
1039-
//
1040-
// The real solution to this problem is to allow symbol links to over-specify disambiguators and improve the
1041-
// diagnostics when symbol links are ambiguous. (rdar://78518537)
1042-
// That will allow for fixes to the least amount of disambiguation without breaking existing links.
1043-
1044-
1045-
// The current implementation works in 3 phases:
1046-
// - First, it computes the paths without disambiguators to identify colliding paths.
1047-
// - Second, it computes the "correct" disambiguators for each collision.
1048-
// - Lastly, it joins together the results in a stable order to avoid indeterministic behavior.
1049-
1050-
1051-
let totalSymbolCount = unifiedGraphs.values.map { $0.symbols.count }.reduce(0, +)
1052-
1053-
/// Temporary data structure to hold input to compute paths with or without disambiguation.
1054-
struct PathCollisionInfo {
1055-
let symbol: UnifiedSymbolGraph.Symbol
1056-
let moduleName: String
1057-
var languages: Set<SourceLanguage>
1058-
}
1059-
var pathCollisionInfo = [[String]: [PathCollisionInfo]]()
1060-
pathCollisionInfo.reserveCapacity(totalSymbolCount)
1061-
1062-
// Group symbols by path from all of the available symbol graphs
1063-
for (moduleName, symbolGraph) in unifiedGraphs {
1064-
let symbols = Array(symbolGraph.symbols.values)
1065-
1066-
let referenceMap = symbols.concurrentMap { symbol in
1067-
(symbol, referencesWithoutDisambiguationFor(symbol, moduleName: moduleName, bundle: bundle))
1068-
}.reduce(into: [String: [SourceLanguage: ResolvedTopicReference]](), { result, next in
1069-
let (symbol, references) = next
1070-
for reference in references {
1071-
result[symbol.uniqueIdentifier, default: [:]][reference.sourceLanguage] = reference
1072-
}
1073-
})
1074-
1075-
let parentMap = symbolGraph.relationshipsByLanguage.reduce(into: [String: [SourceLanguage: String]](), { parentMap, next in
1076-
let (selector, relationships) = next
1077-
guard let language = SourceLanguage(knownLanguageIdentifier: selector.interfaceLanguage) else {
1078-
return
1079-
}
1080-
1081-
for relationship in relationships {
1082-
switch relationship.kind {
1083-
case .memberOf, .requirementOf, .declaredIn:
1084-
parentMap[relationship.source, default: [:]][language] = relationship.target
1085-
default:
1086-
break
1087-
}
1088-
}
1089-
})
1090-
1091-
let pathsAndLanguages: [[([String], SourceLanguage)]] = symbols.concurrentMap { symbol in
1092-
guard let references = referenceMap[symbol.uniqueIdentifier] else {
1093-
return []
1094-
}
1095-
1096-
return references.map { language, reference in
1097-
var prefixLength: Int
1098-
if let parentId = parentMap[symbol.uniqueIdentifier]?[language],
1099-
let parentReference = referenceMap[parentId]?[language] ?? referenceMap[parentId]?.values.first {
1100-
// This is a child of some other symbol
1101-
prefixLength = parentReference.pathComponents.count
1102-
} else {
1103-
// This is a top-level symbol or another symbol without parent (e.g. default implementation)
1104-
prefixLength = reference.pathComponents.count-1
1105-
}
1106-
1107-
// PathComponents can have prefixes which are not known locally. In that case,
1108-
// the "empty" segments will be cut out later on. We follow the same logic here, as otherwise
1109-
// some collisions would not be detected.
1110-
// E.g. consider an extension to an external nested type `SomeModule.SomeStruct.SomeStruct`. The
1111-
// parent of this extended type symbol is `SomeModule`, however, the path for the extended type symbol
1112-
// is `SomeModule/SomeStruct/SomeStruct`, later on, this will change to `SomeModule/SomeStruct`. Now, if
1113-
// we also extend `SomeModule.SomeStruct`, the paths for both extensions could collide. To recognize (and resolve)
1114-
// the collision here, we work with the same, shortened paths.
1115-
return ((reference.pathComponents[0..<prefixLength] + [reference.pathComponents.last!]).map{ $0.lowercased() }, reference.sourceLanguage)
1116-
}
1117-
}
1118-
1119-
for (symbol, symbolPathsAndLanguages) in zip(symbols, pathsAndLanguages) {
1120-
for (path, language) in symbolPathsAndLanguages {
1121-
if let existingReferences = pathCollisionInfo[path] {
1122-
if existingReferences.allSatisfy({ $0.symbol.uniqueIdentifier != symbol.uniqueIdentifier}) {
1123-
// A collision - different symbol but same paths
1124-
pathCollisionInfo[path]!.append(PathCollisionInfo(symbol: symbol, moduleName: moduleName, languages: [language]))
1125-
} else {
1126-
// Same symbol but in a different source language.
1127-
pathCollisionInfo[path]! = pathCollisionInfo[path]!.map { PathCollisionInfo(symbol: $0.symbol, moduleName: $0.moduleName, languages: $0.languages.union([language])) }
1128-
}
1129-
} else {
1130-
// First occurrence of this path
1131-
pathCollisionInfo[path] = [PathCollisionInfo(symbol: symbol, moduleName: moduleName, languages: [language])]
1132-
}
1133-
}
1134-
}
1135-
}
1136-
1137-
/// Temporary data structure to hold groups of disambiguated references
1138-
///
1139-
/// Since the order of `pathCollisionInfo` isn't stable across program executions, simply joining the results for a given symbol that has different
1140-
/// paths in different source languages would also result in an unstable order (depending on the order that the different paths were processed).
1141-
/// Instead we gather all groups of results and join them in a stable order.
1142-
struct IntermediateResultGroup {
1143-
let conflictingSymbolLanguage: SourceLanguage
1144-
let disambiguatedReferences: [ResolvedTopicReference]
1145-
}
1146-
var resultGroups = [SymbolGraph.Symbol.Identifier: [IntermediateResultGroup]]()
1147-
resultGroups.reserveCapacity(totalSymbolCount)
1148-
1149-
// Translate symbols to topic references, adjust paths where necessary.
1150-
for collisions in pathCollisionInfo.values {
1151-
let disambiguationSuffixes = collisions.map(\.symbol).requiredDisambiguationSuffixes
1152-
1153-
for (collisionInfo, disambiguationSuffix) in zip(collisions, disambiguationSuffixes) {
1154-
let language = collisionInfo.languages.contains(.swift) ? .swift : collisionInfo.languages.first!
1155-
1156-
// If the symbol has externally provided disambiguated path components, trust that those are accurate.
1157-
if let knownDisambiguatedComponents = knownDisambiguatedSymbolPathComponents?[collisionInfo.symbol.uniqueIdentifier],
1158-
collisionInfo.symbol.defaultSymbol?.pathComponents.count == knownDisambiguatedComponents.count
1159-
{
1160-
let symbolReference = SymbolReference(pathComponents: knownDisambiguatedComponents, interfaceLanguages: collisionInfo.symbol.sourceLanguages)
1161-
resultGroups[collisionInfo.symbol.defaultIdentifier, default: []].append(
1162-
IntermediateResultGroup(
1163-
conflictingSymbolLanguage: language,
1164-
disambiguatedReferences: [ResolvedTopicReference(symbolReference: symbolReference, moduleName: collisionInfo.moduleName, bundle: bundle)]
1165-
)
1166-
)
1167-
continue
1168-
}
1169-
1170-
// If this is a multi-language collision that doesn't need disambiguation, only emit that symbol once.
1171-
if collisionInfo.languages.count > 1, disambiguationSuffix == (false, false) {
1172-
let symbolReference = SymbolReference(
1173-
collisionInfo.symbol.uniqueIdentifier,
1174-
interfaceLanguages: collisionInfo.symbol.sourceLanguages,
1175-
defaultSymbol: collisionInfo.symbol.defaultSymbol,
1176-
shouldAddHash: false,
1177-
shouldAddKind: false
1178-
)
1179-
1180-
resultGroups[collisionInfo.symbol.defaultIdentifier, default: []].append(
1181-
IntermediateResultGroup(
1182-
conflictingSymbolLanguage: language,
1183-
disambiguatedReferences:[ResolvedTopicReference(symbolReference: symbolReference, moduleName: collisionInfo.moduleName, bundle: bundle)]
1184-
)
1185-
)
1186-
continue
1187-
}
1188-
1189-
// Emit the disambiguated references for all languages for this symbol's collision.
1190-
var symbolSelectors = [collisionInfo.symbol.defaultSelector!]
1191-
for selector in collisionInfo.symbol.mainGraphSelectors where !symbolSelectors.contains(selector) {
1192-
symbolSelectors.append(selector)
1193-
}
1194-
symbolSelectors = symbolSelectors.filter { collisionInfo.languages.contains(SourceLanguage(id: $0.interfaceLanguage)) }
1195-
1196-
resultGroups[collisionInfo.symbol.defaultIdentifier, default: []].append(
1197-
IntermediateResultGroup(
1198-
conflictingSymbolLanguage: language,
1199-
disambiguatedReferences: symbolSelectors.map { selector in
1200-
let symbolReference = SymbolReference(
1201-
collisionInfo.symbol.uniqueIdentifier,
1202-
interfaceLanguages: collisionInfo.symbol.sourceLanguages,
1203-
defaultSymbol: collisionInfo.symbol.symbol(forSelector: selector),
1204-
shouldAddHash: disambiguationSuffix.shouldAddIdHash,
1205-
shouldAddKind: disambiguationSuffix.shouldAddKind
1206-
)
1207-
return ResolvedTopicReference(symbolReference: symbolReference, moduleName: collisionInfo.moduleName, bundle: bundle)
1208-
}
1209-
)
1210-
)
1211-
}
1212-
}
1213-
1214-
return resultGroups.mapValues({
1215-
return $0.sorted(by: { lhs, rhs in
1216-
switch (lhs.conflictingSymbolLanguage, rhs.conflictingSymbolLanguage) {
1217-
// If only one result group is Swift, that comes before the other result.
1218-
case (.swift, let other) where other != .swift:
1219-
return true
1220-
case (let other, .swift) where other != .swift:
1221-
return false
1222-
1223-
// Otherwise, compare the first path to ensure a deterministic order.
1224-
default:
1225-
return lhs.disambiguatedReferences[0].path < rhs.disambiguatedReferences[0].path
1226-
}
1227-
}).flatMap({ $0.disambiguatedReferences })
1228-
})
1229-
}
1230-
1231-
private func referencesWithoutDisambiguationFor(_ symbol: UnifiedSymbolGraph.Symbol, moduleName: String, bundle: DocumentationBundle) -> [ResolvedTopicReference] {
1232-
if let pathComponents = knownDisambiguatedSymbolPathComponents?[symbol.uniqueIdentifier],
1233-
let componentsCount = symbol.defaultSymbol?.pathComponents.count,
1234-
pathComponents.count == componentsCount
1235-
{
1236-
let symbolReference = SymbolReference(pathComponents: pathComponents, interfaceLanguages: symbol.sourceLanguages)
1237-
return [ResolvedTopicReference(symbolReference: symbolReference, moduleName: moduleName, bundle: bundle)]
1238-
}
1239-
1240-
// A unified symbol that exist in multiple languages may have multiple references.
1241-
1242-
// Find all of the relevant selectors, starting with the `defaultSelector`.
1243-
// Any reference after the first is considered an alias/alternative to the first reference
1244-
// and will resolve to the first reference.
1245-
var symbolSelectors = [symbol.defaultSelector]
1246-
for selector in symbol.mainGraphSelectors where !symbolSelectors.contains(selector) {
1247-
symbolSelectors.append(selector)
1248-
}
1249-
1250-
return symbolSelectors.map { selector in
1251-
let defaultSymbol = symbol.symbol(forSelector: selector)!
1252-
let symbolReference = SymbolReference(
1253-
symbol.uniqueIdentifier,
1254-
interfaceLanguages: symbol.sourceLanguages.filter { $0 == SourceLanguage(id: defaultSymbol.identifier.interfaceLanguage) },
1255-
defaultSymbol: defaultSymbol,
1256-
shouldAddHash: false,
1257-
shouldAddKind: false
1258-
)
1259-
return ResolvedTopicReference(symbolReference: symbolReference, moduleName: moduleName, bundle: bundle)
1260-
}
1261-
}
12621024

12631025
private func parentChildRelationship(from edge: SymbolGraph.Relationship) -> (ResolvedTopicReference, ResolvedTopicReference)? {
12641026
// Filter only parent <-> child edges

0 commit comments

Comments
 (0)