Skip to content

Commit 3b39d07

Browse files
authored
Filter out language-specific topic sections when creating See Also sections (#1082)
* Filter out language-specific topic sections from See Also inputs rdar://137311688 * Add FIXME about code that needs to take language into consideration * Use a set of languages (instead of language IDs) for more readable code
1 parent 6ace3ef commit 3b39d07

File tree

3 files changed

+110
-19
lines changed

3 files changed

+110
-19
lines changed

Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ public struct AutomaticCuration {
163163
return nil
164164
}
165165

166+
// FIXME: The shortest path to the reference may not be applicable to the given variants traits.
166167
// First try getting the canonical path from a render context, default to the documentation context
167168
guard let canonicalPath = renderContext?.store.content(for: node.reference)?.canonicalPath ?? context.shortestFinitePath(to: node.reference),
168169
let parentReference = canonicalPath.last
@@ -171,17 +172,32 @@ public struct AutomaticCuration {
171172
return nil
172173
}
173174

175+
let variantLanguages = Set(variantsTraits.compactMap { traits in
176+
traits.interfaceLanguage.map { SourceLanguage(id: $0) }
177+
})
178+
179+
func isRelevant(_ filteredGroup: DocumentationContentRenderer.ReferenceGroup) -> Bool {
180+
// Check if the task group is filtered to a subset of languages
181+
if let languageFilter = filteredGroup.languageFilter,
182+
languageFilter.isDisjoint(with: variantLanguages)
183+
{
184+
// This group is only applicable to other languages than the given variant traits.
185+
return false
186+
}
187+
188+
// Otherwise, check that the group contains the this reference.
189+
return filteredGroup.references.contains(node.reference)
190+
}
191+
174192
func filterReferences(_ references: [ResolvedTopicReference]) -> [ResolvedTopicReference] {
175193
Array(
176194
references
177-
// Don't include the current node.
178-
.filter { $0 != node.reference }
179-
180-
// Filter out nodes that aren't available in any of the given traits.
181195
.filter { reference in
182-
context.sourceLanguages(for: reference).contains(where: { language in
183-
variantsTraits.contains(where: { $0.interfaceLanguage == language.id})
184-
})
196+
// Don't include the current node.
197+
reference != node.reference
198+
&&
199+
// Don't include nodes that aren't available in any of the given traits.
200+
!context.sourceLanguages(for: reference).isDisjoint(with: variantLanguages)
185201
}
186202
// Don't create too long See Also sections
187203
.prefix(automaticSeeAlsoLimit)
@@ -190,7 +206,7 @@ public struct AutomaticCuration {
190206

191207
// Look up the render context first
192208
if let taskGroups = renderContext?.store.content(for: parentReference)?.taskGroups,
193-
let linkingGroup = taskGroups.first(where: { $0.references.contains(node.reference) })
209+
let linkingGroup = taskGroups.first(where: isRelevant)
194210
{
195211
// Group match in render context, verify if there are any other references besides the current one.
196212
guard linkingGroup.references.count > 1 else { return nil }
@@ -203,9 +219,7 @@ public struct AutomaticCuration {
203219
}
204220

205221
// Find the group where the current symbol is curated
206-
let linkingGroup = taskGroups.first { group -> Bool in
207-
group.references.contains(node.reference)
208-
}
222+
let linkingGroup = taskGroups.first(where: isRelevant)
209223

210224
// Verify there is a matching linking group and more references than just the current one.
211225
guard let group = linkingGroup, group.references.count > 1 else {

Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ public class DocumentationContentRenderer {
470470
public struct ReferenceGroup: Codable {
471471
public let title: String?
472472
public let references: [ResolvedTopicReference]
473+
var languageFilter: Set<SourceLanguage>?
473474
}
474475

475476
/// Returns the task groups for a given node reference.
@@ -489,10 +490,7 @@ public class DocumentationContentRenderer {
489490

490491
guard let taskGroups = groups, !taskGroups.isEmpty else { return nil }
491492

492-
// Find the linking group
493-
var resolvedTaskGroups = [ReferenceGroup]()
494-
495-
for group in taskGroups {
493+
return taskGroups.map { group in
496494
let resolvedReferences = group.links.compactMap { link -> ResolvedTopicReference? in
497495
guard let destination = link.destination.flatMap(URL.init(string:)),
498496
destination.scheme != nil,
@@ -515,12 +513,16 @@ public class DocumentationContentRenderer {
515513
)
516514
}
517515

518-
resolvedTaskGroups.append(
519-
ReferenceGroup(title: group.heading?.plainText, references: resolvedReferences)
516+
let supportedLanguages = group.directives[SupportedLanguage.directiveName]?.compactMap {
517+
SupportedLanguage(from: $0, source: nil, for: bundle, in: documentationContext)?.language
518+
}
519+
520+
return ReferenceGroup(
521+
title: group.heading?.plainText,
522+
references: resolvedReferences,
523+
languageFilter: supportedLanguages.map { Set($0) }
520524
)
521525
}
522-
523-
return resolvedTaskGroups
524526
}
525527
}
526528

Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3349,6 +3349,81 @@ Document
33493349
])
33503350
}
33513351

3352+
func testLanguageSpecificTopicSectionDoesNotAppearInAutomaticSeeAlso() throws {
3353+
let catalog = Folder(name: "Something.docc", content: [
3354+
JSONFile(name: "Something-swift.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: (1...4).map {
3355+
makeSymbol(id: "symbol-id-\($0)", language: .swift, kind: .class, pathComponents: ["SomeClass\($0)"])
3356+
})),
3357+
3358+
JSONFile(name: "Something-objc.symbols.json", content: makeSymbolGraph(moduleName: "Something", symbols: (1...4).map {
3359+
makeSymbol(id: "symbol-id-\($0)", language: .objectiveC, kind: .class, pathComponents: ["SomeClass\($0)"])
3360+
})),
3361+
3362+
TextFile(name: "ModuleExtension.md", utf8Content: """
3363+
# ``Something``
3364+
3365+
## Topics
3366+
3367+
### Something Swift only
3368+
3369+
@SupportedLanguage(swift)
3370+
3371+
- ``SomeClass1``
3372+
- ``SomeClass2``
3373+
- ``SomeClass3``
3374+
3375+
### Something Objective-C only
3376+
3377+
@SupportedLanguage(objc)
3378+
3379+
- ``SomeClass2``
3380+
- ``SomeClass3``
3381+
- ``SomeClass4``
3382+
"""),
3383+
])
3384+
let (bundle, context) = try loadBundle(catalog: catalog)
3385+
XCTAssert(context.problems.isEmpty, "\(context.problems.map(\.diagnostic.summary))")
3386+
3387+
let moduleReference = try XCTUnwrap(context.soleRootModuleReference)
3388+
let reference = moduleReference.appendingPath("SomeClass3")
3389+
3390+
let documentationNode = try context.entity(with: reference)
3391+
XCTAssertEqual(documentationNode.availableVariantTraits.count, 2, "This page has Swift and Objective-C variants")
3392+
3393+
// There's a behavioral difference between DocumentationContextConverter and DocumentationNodeConverter so we check both.
3394+
// DocumentationContextConverter may use pre-rendered content but the DocumentationNodeConverter computes task groups as-needed.
3395+
3396+
func assertExpectedTopicSections(_ renderNode: RenderNode, file: StaticString = #filePath, line: UInt = #line) {
3397+
let topicSectionsVariants = renderNode.seeAlsoSectionsVariants
3398+
3399+
let swiftSeeAlsoSection = topicSectionsVariants.defaultValue
3400+
3401+
XCTAssertEqual(swiftSeeAlsoSection.first?.title, "Something Swift only", file: file, line: line)
3402+
XCTAssertEqual(swiftSeeAlsoSection.first?.identifiers, [
3403+
"doc://Something/documentation/Something/SomeClass1",
3404+
"doc://Something/documentation/Something/SomeClass2",
3405+
], file: file, line: line)
3406+
3407+
let objcSeeAlsoSection = topicSectionsVariants.value(for: [.interfaceLanguage("occ")])
3408+
3409+
XCTAssertEqual(objcSeeAlsoSection.first?.title, "Something Objective-C only", file: file, line: line)
3410+
XCTAssertEqual(objcSeeAlsoSection.first?.identifiers, [
3411+
"doc://Something/documentation/Something/SomeClass2",
3412+
"doc://Something/documentation/Something/SomeClass4",
3413+
], file: file, line: line)
3414+
}
3415+
3416+
let nodeConverter = DocumentationNodeConverter(bundle: bundle, context: context)
3417+
try assertExpectedTopicSections(nodeConverter.convert(documentationNode))
3418+
3419+
let contextConverter = DocumentationContextConverter(
3420+
bundle: bundle,
3421+
context: context,
3422+
renderContext: RenderContext(documentationContext: context, bundle: bundle)
3423+
)
3424+
try assertExpectedTopicSections(XCTUnwrap(contextConverter.renderNode(for: documentationNode)))
3425+
}
3426+
33523427
func testTopicSectionWithUnsupportedDirectives() throws {
33533428
let exampleDocumentation = Folder(name: "unit-test.docc", content: [
33543429
TextFile(name: "root.md", utf8Content: """

0 commit comments

Comments
 (0)