Skip to content

Commit aeba5ab

Browse files
authored
Fix a crash when a topic section contained an unsupported directive (#763)
* Fix a crash when a topic section contained an unsupported directive rdar://119279890 * Use optional `supportedLanguages` value to require handling `nil` case
1 parent 617867d commit aeba5ab

File tree

4 files changed

+137
-12
lines changed

4 files changed

+137
-12
lines changed

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,12 +1022,12 @@ public struct RenderNodeTranslator: SemanticVisitor {
10221022
contentCompiler: inout RenderContentCompiler
10231023
) -> [TaskGroupRenderSection] {
10241024
return topics.taskGroups.compactMap { group in
1025-
let supportedLanguages = Set(group.directives.compactMap {
1025+
let supportedLanguages = group.directives[SupportedLanguage.directiveName]?.compactMap {
10261026
SupportedLanguage(from: $0, source: nil, for: bundle, in: context)?.language
1027-
})
1027+
}
10281028

10291029
// If the task group has a set of supported languages, see if it should render for the allowed traits.
1030-
guard supportedLanguages.isEmpty || supportedLanguages.matchesOneOf(traits: allowedTraits) else {
1030+
if supportedLanguages?.matchesOneOf(traits: allowedTraits) == false {
10311031
return nil
10321032
}
10331033

@@ -1985,7 +1985,7 @@ extension ContentLayout: RenderTree {}
19851985

19861986
extension ContentRenderSection: RenderTree {}
19871987

1988-
private extension Set where Element == SourceLanguage {
1988+
private extension Sequence where Element == SourceLanguage {
19891989
func matchesOneOf(traits: Set<DocumentationDataVariantsTrait>) -> Bool {
19901990
traits.contains(where: {
19911991
guard let languageID = $0.interfaceLanguage,

Sources/SwiftDocC/Model/TaskGroup.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,8 @@ public struct TaskGroup {
223223
self.originalContent = content
224224
}
225225

226-
var directives: [BlockDirective] {
227-
originalContent.compactMap { $0 as? BlockDirective }
226+
var directives: [String: [BlockDirective]] {
227+
.init(grouping: originalContent.compactMap { $0 as? BlockDirective }, by: \.name)
228228
}
229229
}
230230

Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import Markdown
1313
import XCTest
1414
import SymbolKit
15+
import SwiftDocCTestUtilities
1516

1617
class SemaToRenderNodeTests: XCTestCase {
1718
func testCompileTutorial() throws {
@@ -3345,4 +3346,56 @@ Document
33453346
"doc://org.swift.MixedFramework/documentation/MixedFramework/MyObjectiveCClassSwiftName/myMethod(argument:)"
33463347
])
33473348
}
3349+
3350+
func testTopicSectionWithUnsupportedDirectives() throws {
3351+
let exampleDocumentation = Folder(name: "unit-test.docc", content: [
3352+
TextFile(name: "root.md", utf8Content: """
3353+
# Main article
3354+
3355+
@Metadata {
3356+
@TechnologyRoot
3357+
}
3358+
3359+
## Topics
3360+
3361+
### Something
3362+
3363+
A mix of different directives that aren't supported in task groups.
3364+
3365+
@Comment {
3366+
Some commented out markup
3367+
}
3368+
3369+
@SomeUnknownDirective()
3370+
3371+
- <doc:article>
3372+
"""),
3373+
3374+
TextFile(name: "article.md", utf8Content: """
3375+
# An article
3376+
"""),
3377+
])
3378+
let tempURL = try createTemporaryDirectory()
3379+
let bundleURL = try exampleDocumentation.write(inside: tempURL)
3380+
3381+
let (_, bundle, context) = try loadBundle(from: bundleURL) { context in
3382+
context.diagnosticEngine.consumers.sync({ $0.removeAll() })
3383+
}
3384+
3385+
let reference = try XCTUnwrap(context.soleRootModuleReference)
3386+
3387+
let documentationNode = try context.entity(with: reference)
3388+
XCTAssertEqual(documentationNode.availableVariantTraits.count, 1)
3389+
3390+
let converter = DocumentationNodeConverter(bundle: bundle, context: context)
3391+
let renderNode = try converter.convert(documentationNode, at: nil)
3392+
3393+
let topicSection = renderNode.topicSectionsVariants.defaultValue
3394+
3395+
XCTAssertEqual(topicSection.first?.title, "Something")
3396+
XCTAssertEqual(topicSection.first?.abstract?.plainText, "A mix of different directives that aren’t supported in task groups.")
3397+
XCTAssertEqual(topicSection.first?.identifiers, [
3398+
"doc://unit-test/documentation/unit-test/article"
3399+
])
3400+
}
33483401
}

Tests/SwiftDocCTests/Model/TaskGroupTests.swift

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,8 @@ class TaskGroupTests: XCTestCase {
274274
XCTAssertEqual(taskGroup.heading?.detachedFromParent.format(), "### Something Swift only")
275275
XCTAssertEqual(taskGroup.abstract?.paragraph.detachedFromParent.format(), "This link is only for Swift")
276276
XCTAssertEqual(taskGroup.directives.count, 1)
277-
for directive in taskGroup.directives {
277+
XCTAssertEqual(taskGroup.directives[SupportedLanguage.directiveName]?.count, 1)
278+
for directive in taskGroup.directives[SupportedLanguage.directiveName] ?? [] {
278279
XCTAssertEqual(directive.name, "SupportedLanguage")
279280
XCTAssertEqual(directive.arguments().count, 1)
280281
}
@@ -286,7 +287,8 @@ class TaskGroupTests: XCTestCase {
286287
XCTAssertEqual(taskGroup.heading?.detachedFromParent.format(), "### Something Objective-C only")
287288
XCTAssertEqual(taskGroup.abstract?.paragraph.detachedFromParent.format(), "This link is only for Objective-C")
288289
XCTAssertEqual(taskGroup.directives.count, 1)
289-
for directive in taskGroup.directives {
290+
XCTAssertEqual(taskGroup.directives[SupportedLanguage.directiveName]?.count, 1)
291+
for directive in taskGroup.directives[SupportedLanguage.directiveName] ?? [] {
290292
XCTAssertEqual(directive.name, "SupportedLanguage")
291293
XCTAssertEqual(directive.arguments().count, 1)
292294
}
@@ -297,8 +299,9 @@ class TaskGroupTests: XCTestCase {
297299
let taskGroup = try XCTUnwrap(topicSection.taskGroups.dropFirst(2).first)
298300
XCTAssertEqual(taskGroup.heading?.detachedFromParent.format(), "### Something for both")
299301
XCTAssertEqual(taskGroup.abstract?.paragraph.detachedFromParent.format(), "This link is for both Swift and Objective-C")
300-
XCTAssertEqual(taskGroup.directives.count, 2)
301-
for directive in taskGroup.directives {
302+
XCTAssertEqual(taskGroup.directives.count, 1)
303+
XCTAssertEqual(taskGroup.directives[SupportedLanguage.directiveName]?.count, 2)
304+
for directive in taskGroup.directives[SupportedLanguage.directiveName] ?? [] {
302305
XCTAssertEqual(directive.name, "SupportedLanguage")
303306
XCTAssertEqual(directive.arguments().count, 1)
304307
}
@@ -314,6 +317,74 @@ class TaskGroupTests: XCTestCase {
314317
}
315318
}
316319

320+
func testOtherDirectivesAreIgnored() throws {
321+
let markupSource = """
322+
# Title
323+
324+
Abstract.
325+
326+
## Topics
327+
328+
### Something
329+
330+
A mix of different directives that aren't supported in task groups.
331+
332+
@Comment {
333+
Some commented out markup
334+
}
335+
336+
@Metadata {
337+
}
338+
339+
@SomeUnknownDirective()
340+
341+
@SupportedLanguage(swift)
342+
343+
- ``SomeLink``
344+
345+
"""
346+
let document = Document(parsing: markupSource, options: [.parseBlockDirectives, .parseSymbolLinks])
347+
let markupModel = DocumentationMarkup(markup: document)
348+
349+
XCTAssertEqual("Abstract.", Paragraph(markupModel.abstractSection?.content.compactMap { $0 as? InlineMarkup } ?? []).detachedFromParent.format())
350+
351+
let topicSection = try XCTUnwrap(markupModel.topicsSection)
352+
XCTAssertEqual(topicSection.taskGroups.count, 1)
353+
354+
let taskGroup = try XCTUnwrap(topicSection.taskGroups.first)
355+
XCTAssertEqual(taskGroup.heading?.detachedFromParent.format(), "### Something")
356+
XCTAssertEqual(taskGroup.abstract?.paragraph.detachedFromParent.format(), "A mix of different directives that aren’t supported in task groups.")
357+
XCTAssertEqual(taskGroup.directives.count, 4)
358+
359+
XCTAssertEqual(taskGroup.directives[Comment.directiveName]?.count, 1)
360+
if let comment = taskGroup.directives[Comment.directiveName]?.first {
361+
XCTAssertEqual(comment.name, "Comment")
362+
XCTAssertEqual(comment.childCount, 1)
363+
XCTAssert(comment.arguments().isEmpty)
364+
}
365+
366+
XCTAssertEqual(taskGroup.directives[Metadata.directiveName]?.count, 1)
367+
if let metadata = taskGroup.directives[Metadata.directiveName]?.first {
368+
XCTAssertEqual(metadata.name, "Metadata")
369+
XCTAssertEqual(metadata.childCount, 0)
370+
XCTAssertEqual(metadata.childCount, 0)
371+
XCTAssert(metadata.arguments().isEmpty)
372+
}
373+
374+
if let directive = taskGroup.directives["SomeUnknownDirective"]?.first {
375+
XCTAssertEqual(directive.childCount, 0)
376+
XCTAssert(directive.arguments().isEmpty)
377+
}
378+
XCTAssertEqual(taskGroup.directives[SupportedLanguage.directiveName]?.count, 1)
379+
if let supportedLanguage = taskGroup.directives[SupportedLanguage.directiveName]?.first {
380+
XCTAssertEqual(supportedLanguage.name, "SupportedLanguage")
381+
XCTAssertEqual(supportedLanguage.childCount, 0)
382+
XCTAssertEqual(supportedLanguage.arguments().count, 1)
383+
}
384+
385+
XCTAssertEqual(taskGroup.links.count, 1)
386+
}
387+
317388
func testTopicContentOrder() throws {
318389
func assertExpectedParsedTaskGroupContent(_ content: String, file: StaticString = #file, line: UInt = #line) throws {
319390
let document = Document(parsing: """
@@ -340,8 +411,9 @@ class TaskGroupTests: XCTestCase {
340411
"Discussion paragraph 2",
341412
], file: file, line: line)
342413
XCTAssertEqual(taskGroup.directives.count, 1, file: file, line: line)
343-
XCTAssertEqual(taskGroup.directives.first?.name, "SupportedLanguage", file: file, line: line)
344-
XCTAssertEqual(taskGroup.directives.first?.arguments().count, 1, file: file, line: line)
414+
XCTAssertEqual(taskGroup.directives.keys.first, SupportedLanguage.directiveName, file: file, line: line)
415+
XCTAssertEqual(taskGroup.directives[SupportedLanguage.directiveName]?.first?.name, "SupportedLanguage", file: file, line: line)
416+
XCTAssertEqual(taskGroup.directives[SupportedLanguage.directiveName]?.first?.arguments().count, 1, file: file, line: line)
345417
XCTAssertEqual(taskGroup.links.map(\.destination), ["Link1", "Link2"], file: file, line: line)
346418
}
347419

0 commit comments

Comments
 (0)