Skip to content

Commit c04e18f

Browse files
authored
SR-15316: Emit warning when topics section does not have 1+ subheading (#29)
* SR-15316: Emit warning when topics section does not have 1+ subheading * SR-15316: Code changes * SR-15316: Add isTopic computed property * SR-15316: Rename checker for better clarity * SR-15316: Move check into guard statement * SR-15316: Add convenience initializer for problem * SR-15316: Invert method * SR-15316: Rename method
1 parent 3cbb7ce commit c04e18f

File tree

8 files changed

+174
-4
lines changed

8 files changed

+174
-4
lines changed

Sources/SwiftDocC/Checker/Checker.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,12 @@ public struct CompositeChecker: Checker {
413413
descendInto(text)
414414
}
415415
}
416+
417+
/*
418+
A collection of `Heading` properties utilized by `Checker`s.
419+
*/
420+
extension Heading {
421+
var isTopicsSection: Bool {
422+
return level == 2 && title == "Topics"
423+
}
424+
}

Sources/SwiftDocC/Checker/Checkers/DuplicateTopicsSection.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@ public struct DuplicateTopicsSections: Checker {
4949
}
5050

5151
public mutating func visitHeading(_ heading: Heading) {
52-
guard heading.level == 2,
53-
heading.plainText == "Topics",
54-
heading.parent is Document? else {
52+
guard heading.isTopicsSection,
53+
heading.parent is Document? else {
5554
return
5655
}
5756
foundTopicsHeadings.append(heading)

Sources/SwiftDocC/Checker/Checkers/NonOverviewHeadingChecker.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public struct NonOverviewHeadingChecker: Checker {
5959

6060
public mutating func visitHeading(_ heading: Heading) {
6161
// We don't want to flag the Topics H2 and the See Also H2.
62-
guard heading.level == 2, !["Topics", "See Also"].contains(heading.plainText) else {
62+
guard heading.level == 2, !heading.isTopicsSection, heading.title != "See Also" else {
6363
return
6464
}
6565

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
import Markdown
13+
14+
/**
15+
A Topics section should have at least one subheading.
16+
*/
17+
public struct TopicsSectionWithoutSubheading: Checker {
18+
public var problems = [Problem]()
19+
20+
private var sourceFile: URL?
21+
22+
/// Creates a new checker that detects Topics sections without subheadings.
23+
///
24+
/// - Parameter sourceFile: The URL to the documentation file that the checker checks.
25+
public init(sourceFile: URL?) {
26+
self.sourceFile = sourceFile
27+
}
28+
29+
public mutating func visitDocument(_ document: Document) -> () {
30+
let headings = document.children.compactMap { $0 as? Heading }
31+
for (index, heading) in headings.enumerated() {
32+
guard heading.isTopicsSection, isMissingSubheading(heading, remainingHeadings: headings.dropFirst(index + 1)) else {
33+
continue
34+
}
35+
36+
let explanation = """
37+
A Topics section requires at least one topic, represented by a level-3 subheading. A Topics section without topics won’t render any content.”
38+
"""
39+
40+
let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: heading.range, identifier: "org.swift.docc.TopicsSectionWithoutSubheading", summary: "Missing required subheading for Topics section.", explanation: explanation)
41+
problems.append(Problem(diagnostic: diagnostic))
42+
}
43+
}
44+
45+
private func isMissingSubheading(_ heading: Heading, remainingHeadings: ArraySlice<Heading>) -> Bool {
46+
if let nextHeading = remainingHeadings.first {
47+
return nextHeading.level <= heading.level
48+
}
49+
50+
return true
51+
}
52+
}

Sources/SwiftDocC/Infrastructure/Diagnostics/Problem.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public struct Problem {
2121
self.diagnostic = diagnostic
2222
self.possibleSolutions = Array(possibleSolutions)
2323
}
24+
25+
public init(diagnostic: Diagnostic) {
26+
self.init(diagnostic: diagnostic, possibleSolutions: [])
27+
}
2428
}
2529

2630
extension Problem {

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
366366
MissingAbstract(sourceFile: source).any(),
367367
NonOverviewHeadingChecker(sourceFile: source).any(),
368368
SeeAlsoInTopicsHeadingChecker(sourceFile: source).any(),
369+
TopicsSectionWithoutSubheading(sourceFile: source).any(),
369370
])
370371
checker.visit(document)
371372
diagnosticEngine.emit(checker.problems)

Sources/SwiftDocC/SwiftDocC.docc/SwiftDocC/StaticAnalysis.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ Run static analysis checks on markup files.
1919
- ``NonInclusiveLanguageChecker``
2020
- ``NonOverviewHeadingChecker``
2121
- ``SeeAlsoInTopicsHeadingChecker``
22+
- ``TopicsSectionWithoutSubheading``
2223

2324
<!-- Copyright (c) 2021 Apple Inc and the Swift Project authors. All Rights Reserved. -->
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import XCTest
12+
@testable import SwiftDocC
13+
import Markdown
14+
15+
class TopicsSectionWithoutSubheadingTests: XCTestCase {
16+
func testEmptyDocument() {
17+
let checker = visitDocument(Document())
18+
XCTAssertTrue(checker.problems.isEmpty)
19+
}
20+
21+
func testTopicsSectionHasSubheading() {
22+
let markupSource = """
23+
# Title
24+
25+
Testing One
26+
27+
## Topics
28+
29+
### Test 2
30+
31+
Testing Two
32+
33+
# Test
34+
"""
35+
let checker = visitSource(markupSource)
36+
XCTAssertTrue(checker.problems.isEmpty)
37+
}
38+
39+
func testTopicsSectionHasNoSubheading() {
40+
let markupSource = """
41+
# Title
42+
43+
Abstract.
44+
45+
## Topics
46+
47+
## Information
48+
49+
### Topic B
50+
"""
51+
52+
let document = Document(parsing: markupSource)
53+
let checker = visitDocument(document)
54+
XCTAssertEqual(1, checker.problems.count)
55+
56+
let problem = checker.problems[0]
57+
XCTAssertTrue(problem.possibleSolutions.isEmpty)
58+
59+
let noSubheadingHeading = document.child(at: 2)! as! Heading
60+
let diagnostic = problem.diagnostic
61+
XCTAssertEqual("org.swift.docc.TopicsSectionWithoutSubheading", diagnostic.identifier)
62+
XCTAssertEqual(noSubheadingHeading.range, diagnostic.range)
63+
}
64+
65+
func testTopicsSectionIsFinalHeading() {
66+
let markupSource = """
67+
# Title
68+
69+
Abstract.
70+
71+
## User
72+
73+
## Information
74+
75+
## Topics
76+
"""
77+
78+
let document = Document(parsing: markupSource)
79+
let checker = visitDocument(document)
80+
XCTAssertEqual(1, checker.problems.count)
81+
82+
let problem = checker.problems[0]
83+
XCTAssertTrue(problem.possibleSolutions.isEmpty)
84+
85+
let noSubheadingHeading = document.child(at: 4)! as! Heading
86+
let diagnostic = problem.diagnostic
87+
XCTAssertEqual("org.swift.docc.TopicsSectionWithoutSubheading", diagnostic.identifier)
88+
XCTAssertEqual(noSubheadingHeading.range, diagnostic.range)
89+
}
90+
}
91+
92+
extension TopicsSectionWithoutSubheadingTests {
93+
func visitSource(_ source: String) -> TopicsSectionWithoutSubheading {
94+
let document = Document(parsing: source)
95+
return visitDocument(document)
96+
}
97+
98+
func visitDocument(_ document: Document) -> TopicsSectionWithoutSubheading {
99+
var checker = TopicsSectionWithoutSubheading(sourceFile: nil)
100+
checker.visit(document)
101+
102+
return checker
103+
}
104+
}

0 commit comments

Comments
 (0)