Skip to content

Commit 2f88d53

Browse files
improve detection for inherited symbol references (#257)
rdar://93203894 Symbols for operator functions that include multiple dots (e.g. the ClosedRange `...` operator) are currently curated into an unnamed "Implementations" section due to improper string splitting behavior. This enhances the detection two ways: 1. For symbols where the "source origin" is available in our given symbol graphs, it loads the name information from the parent symbol. 2. For symbols where the "source origin" is not available, it improved the string splitting behavior by dropping empty-string components before reading names from it. This allows Comparable and Equatable implementations to curate their inherited operators properly.
1 parent cd15ec3 commit 2f88d53

File tree

3 files changed

+3584
-15
lines changed

3 files changed

+3584
-15
lines changed

Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2022 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -40,25 +40,35 @@ enum GeneratedDocumentationTopics {
4040
/// - reference: The parent type reference.
4141
/// - originDisplayName: The origin display name as provided by the symbol graph.
4242
/// - extendedModuleName: Extended module name.
43-
mutating func add(_ childReference: ResolvedTopicReference, to reference: ResolvedTopicReference, originDisplayName: String, extendedModuleName: String) throws {
44-
// Detect the path components of the providing the default implementation.
45-
let typeComponents = originDisplayName.components(separatedBy: ".")
46-
47-
// Verify that the fully qualified name contains at least a type name and default implementation name.
48-
guard typeComponents.count >= 2 else { return }
49-
43+
mutating func add(_ childReference: ResolvedTopicReference, to reference: ResolvedTopicReference, originDisplayName: String, originParentSymbol: ResolvedTopicReference?, extendedModuleName: String) throws {
44+
let fromType: String
45+
let typeSimpleName: String
46+
if let originParentSymbol = originParentSymbol, !originParentSymbol.pathComponents.isEmpty {
47+
// If we have a resolved symbol for the parent of `sourceOrigin`, use that for the names
48+
fromType = originParentSymbol.pathComponents.joined(separator: ".")
49+
typeSimpleName = originParentSymbol.pathComponents.last!
50+
} else {
51+
// If we don't have a resolved `sourceOrigin` parent, fall back to parsing its display name
52+
53+
// Detect the path components of the providing the default implementation.
54+
let typeComponents = originDisplayName.split(separator: ".")
55+
56+
// Verify that the fully qualified name contains at least a type name and default implementation name.
57+
guard typeComponents.count >= 2 else { return }
58+
59+
// Get the fully qualified type.
60+
fromType = typeComponents.dropLast().joined(separator: ".")
61+
// The name of the type is second to last.
62+
typeSimpleName = String(typeComponents[typeComponents.count-2])
63+
}
64+
5065
// Create a type with inherited symbols, if needed.
5166
if !implementingTypes.keys.contains(reference) {
5267
implementingTypes[reference] = Collections()
5368
}
5469

55-
// Get the fully qualified type.
56-
let fromType = typeComponents.dropLast().joined(separator: ".")
57-
5870
// Create a new default implementations provider, if needed.
5971
if !implementingTypes[reference]!.inheritedFromTypeName.keys.contains(fromType) {
60-
// The name of the type is second to last.
61-
let typeSimpleName = typeComponents[typeComponents.count-2]
6272
implementingTypes[reference]!.inheritedFromTypeName[fromType] = Collections.APICollection(title: "\(typeSimpleName) Implementations", parentReference: reference)
6373
}
6474

@@ -215,8 +225,12 @@ enum GeneratedDocumentationTopics {
215225
let child = context.symbolIndex[relationship.source],
216226
// Get the swift extension data
217227
let extends = child.symbol?.mixins[SymbolGraph.Symbol.Swift.Extension.mixinKey] as? SymbolGraph.Symbol.Swift.Extension {
228+
var originParentSymbol: ResolvedTopicReference? = nil
229+
if let originSymbol = context.symbolIndex[origin.identifier] {
230+
originParentSymbol = try? symbolsURLHierarchy.parent(of: originSymbol.reference)
231+
}
218232
// Add the inherited symbol to the index.
219-
try inheritanceIndex.add(child.reference, to: parent.reference, originDisplayName: origin.displayName, extendedModuleName: extends.extendedModule)
233+
try inheritanceIndex.add(child.reference, to: parent.reference, originDisplayName: origin.displayName, originParentSymbol: originParentSymbol, extendedModuleName: extends.extendedModule)
220234
}
221235
}
222236

Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2022 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -623,6 +623,37 @@ class RenderNodeTranslatorTests: XCTestCase {
623623
}
624624

625625
}
626+
627+
/// Verify that symbols with ellipsis operators don't get curated into an unnamed protocol implementation section.
628+
func testAutomaticImplementationsWithExtraDots() throws {
629+
let fancyProtocolSGFURL = Bundle.module.url(
630+
forResource: "FancyProtocol.symbols", withExtension: "json", subdirectory: "Test Resources")!
631+
632+
// Create a test bundle copy with the symbol graph from above
633+
let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle", excludingPaths: [], codeListings: [:]) { url in
634+
try? FileManager.default.copyItem(at: fancyProtocolSGFURL, to: url.appendingPathComponent("FancyProtocol.symbols.json"))
635+
}
636+
637+
let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/FancyProtocol/SomeClass", sourceLanguage: .swift)
638+
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference, source: nil)
639+
let node = try context.entity(with: reference)
640+
let symbol = try XCTUnwrap(node.semantic as? Symbol)
641+
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
642+
643+
let defaultImplementationSection = try XCTUnwrap(renderNode.topicSections.first(where: { $0.title == "Default Implementations" }))
644+
XCTAssertEqual(defaultImplementationSection.identifiers, [
645+
"doc://org.swift.docc.example/documentation/FancyProtocol/SomeClass/Comparable-Implementations",
646+
"doc://org.swift.docc.example/documentation/FancyProtocol/SomeClass/Equatable-Implementations",
647+
"doc://org.swift.docc.example/documentation/FancyProtocol/SomeClass/FancyProtocol-Implementations",
648+
])
649+
let implReferences = defaultImplementationSection.identifiers.compactMap({ renderNode.references[$0] as? TopicRenderReference })
650+
XCTAssertEqual(implReferences.map({ $0.title }), [
651+
"Comparable Implementations",
652+
"Equatable Implementations",
653+
"FancyProtocol Implementations",
654+
])
655+
656+
}
626657

627658
func testAutomaticTaskGroupTopicsAreSorted() throws {
628659
let (bundle, context) = try testBundleAndContext(named: "DefaultImplementations")

0 commit comments

Comments
 (0)