Skip to content

Commit 1e7820c

Browse files
committed
Check out of order sections properly
1 parent cc6a749 commit 1e7820c

File tree

9 files changed

+234
-23
lines changed

9 files changed

+234
-23
lines changed

Sources/DocExtractor/SourceFileDocumentor.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@ public struct SpecialSectionMoreThanOneListWarning {}
1515
public struct ListSpecialSectionHasNoListError {}
1616
@Diagnostify
1717
public struct UnexpectedSpecialSectionTitleError {}
18+
19+
public struct OutOfOrderError: HDCDiagnostic {
20+
public let level: HDCDiagnosticLevel
21+
public let message: String
22+
public let site: SourceRange
23+
public let notes: [AnyHashable]
24+
public let found: SpecialSectionType
25+
public let expectedBefore: SpecialSectionType
26+
27+
public init(
28+
_ level: HDCDiagnosticLevel, message: String, at site: SourceRange, notes: [AnyHashable] = [],
29+
found: SpecialSectionType, expectedBefore: SpecialSectionType
30+
) {
31+
self.level = level
32+
self.message = message
33+
self.site = site
34+
self.notes = notes
35+
self.found = found
36+
self.expectedBefore = expectedBefore
37+
}
38+
}
1839
@Diagnostify
1940
public struct InlineSpecialSectionTitleRemoveFailureWarning {}
2041
@Diagnostify
@@ -160,6 +181,11 @@ private func parseSeeAlsoSection(
160181
return Array(blocks)
161182
}
162183

184+
let strictOrdering: [SpecialSectionType] = [
185+
.fileLevel, .parameter, .generic, .precondition, .postcondition, .invariant,
186+
.returns, .throws, .yields, .projects, .complexity, .seeAlso,
187+
]
188+
163189
public enum SpecialSectionType {
164190
case `parameter`
165191
case `returns`
@@ -298,6 +324,7 @@ private func validateAllowedSpecialSections(
298324
allowedSectionTitles = []
299325
}
300326

327+
// Check for unexpected special sections
301328
for section in comment.value.specialSections {
302329
if !allowedSectionTitles.contains(where: { section.name.lowercased().starts(with: $0) }) {
303330
diagnostics.insert(
@@ -307,6 +334,50 @@ private func validateAllowedSpecialSections(
307334
)
308335
}
309336
}
337+
338+
// Check if sections that are present are in the correct order
339+
var highestSectionIndexFound = -1
340+
for i in 0..<comment.value.specialSections.count {
341+
let section = comment.value.specialSections[i]
342+
let lowercasedName = section.name.lowercased()
343+
guard
344+
let sectionType = strictOrdering.first(where: {
345+
lowercasedName.starts(with: $0.inlineName) || lowercasedName.starts(with: $0.headingName)
346+
})
347+
else {
348+
continue // invalid section title, already diagnosed
349+
}
350+
351+
let expectedSectionIndex = strictOrdering.firstIndex(of: sectionType)!
352+
353+
if expectedSectionIndex < highestSectionIndexFound {
354+
let earliestSectionPresentThatShouldBeStillAfterThis: Int = (0..<i).map { (sectionI: Int) in
355+
let rawSection = comment.value.specialSections[sectionI]
356+
let lowercasedName = rawSection.name.lowercased()
357+
return strictOrdering.firstIndex { sectionType in
358+
lowercasedName.starts(with: sectionType.inlineName)
359+
|| lowercasedName.starts(with: sectionType.headingName)
360+
}!
361+
} // map to index of each section
362+
.first { index in
363+
return expectedSectionIndex < index
364+
}!
365+
366+
diagnostics.insert(
367+
OutOfOrderError.init(
368+
.error,
369+
message:
370+
"'\(sectionType.inlineName)' expected before '\(strictOrdering[highestSectionIndexFound].inlineName)'.",
371+
at: comment.site,
372+
found: sectionType,
373+
expectedBefore: strictOrdering[earliestSectionPresentThatShouldBeStillAfterThis]
374+
)
375+
)
376+
} else {
377+
highestSectionIndexFound = expectedSectionIndex
378+
}
379+
}
380+
310381
}
311382

312383
public struct DummySourceFileDocumentor: SourceFileDocumentor {

Sources/HDCUtils/HDCDiagnostic.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public enum HDCDiagnosticLevel: Hashable {
1616

1717
extension HDCDiagnostic {
1818
public var description: String {
19-
"\(site.gnuStandardText): \(level): \(message)"
19+
"\(site.vscodeFriendlyDescription): \(level): \(message)"
2020
}
2121
}
2222

@@ -34,3 +34,23 @@ extension HDCDiagnostic {
3434
)
3535
@attached(extension, conformances: HDCDiagnostic)
3636
public macro Diagnostify() = #externalMacro(module: "HDCMacros", type: "DiagnostifyMacro")
37+
38+
extension SourceRange {
39+
40+
/// A textual representation per the
41+
/// [Gnu-standard](https://www.gnu.org/prep/standards/html_node/Errors.html).
42+
public var vscodeFriendlyDescription: String {
43+
let start = self.start.lineAndColumn
44+
let head = "\(file.url.relativePath):\(start.line):\(start.column)"
45+
if regionOfFile.isEmpty { return head }
46+
47+
let end = file.position(endIndex).lineAndColumn
48+
if end.line == start.line {
49+
return head + "-\(end.column)"
50+
}
51+
return head + "-\(end.line):\(end.column)"
52+
}
53+
54+
public var description: String { gnuStandardText }
55+
56+
}

Tests/DocExtractorTests/SourceFileDocumentorTests/SimpleTests/ProductTypeExtractionTest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ final class ProductTypeExtractionTest: XCTestCase {
1717
/// Summary of the product type.
1818
///
1919
/// This is the description of the product type.
20-
/// # Invariant: x and y must always be positive.
2120
/// # Generic T: This is a generic.
21+
/// # Invariant: x and y must always be positive.
2222
type A<T: D>{
2323
var x: Int
2424
var y: Int
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import DocExtractor
2+
import DocumentationDB
3+
import FrontEnd
4+
import HDCUtils
5+
import TestUtils
6+
import XCTest
7+
8+
final class SpecialSectionOutOfOrderTest: XCTestCase {
9+
10+
struct OutOfOrderErrorPayload {
11+
let found: SpecialSectionType
12+
let expectedBefore: SpecialSectionType
13+
}
14+
func testOutOfOrder(
15+
in sourceFile: SourceFile, expectedErrors: [OutOfOrderErrorPayload], file: StaticString = #filePath, line: UInt = #line
16+
) throws {
17+
let commentParser = RealCommentParser(lowLevelCommentParser: RealLowLevelCommentParser())
18+
let sourceFileDocumentor = RealSourceFileDocumentor(
19+
commentParser: commentParser, markdownParser: HyloDocMarkdownParser.standard)
20+
21+
let ast = try checkNoDiagnostic { d in
22+
try AST(fromSingleSourceFile: sourceFile, diagnostics: &d)
23+
}
24+
25+
var store = SymbolDocStore()
26+
27+
var d = HDCDiagnosticSet()
28+
let _ = sourceFileDocumentor.document(
29+
ast: ast,
30+
translationUnitId: ast.resolveTranslationUnit(by: sourceFile.baseName)!,
31+
into: &store,
32+
diagnostics: &d
33+
)
34+
35+
let outOfOrderDiagnostics = d.elements.filter { $0 is OutOfOrderError }
36+
XCTAssertEqual(
37+
expectedErrors.count, outOfOrderDiagnostics.count,
38+
"The number of out of order diagnostics does not match the expected count. Diagnostic: \n\(outOfOrderDiagnostics.map { $0.description }.joined(separator: "\n"))",
39+
file: file, line: line)
40+
41+
for expectedError in expectedErrors {
42+
let match = outOfOrderDiagnostics.first { diagnostic in
43+
let error = diagnostic as! OutOfOrderError
44+
return error.found == expectedError.found && error.expectedBefore == expectedError.expectedBefore
45+
}
46+
XCTAssertNotNil(
47+
match,
48+
"Expected out of order error with found: \(expectedError.found) and expectedBefore: \(expectedError.expectedBefore) not found.",
49+
file: file, line: line
50+
)
51+
}
52+
}
53+
54+
func testOutOfOrder1() throws {
55+
try testOutOfOrder(
56+
in: """
57+
/// Summary of function.
58+
///
59+
/// This is the description.
60+
/// - Note: This is still the description.
61+
/// # Generic T: This is a generic.
62+
/// # Parameter x: This is a parameter.
63+
fun id(x: Int) -> Int { x }
64+
""",
65+
expectedErrors: [
66+
.init(found: .parameter, expectedBefore: .generic)
67+
])
68+
}
69+
70+
func testOutOfOrder2() throws {
71+
try testOutOfOrder(
72+
in: """
73+
/// Summary of function.
74+
///
75+
/// This is the description.
76+
/// - Note: This is still the description.
77+
/// # Returns: something.
78+
/// # Parameter x: This is a parameter.
79+
/// # Generic y: This is a generic parameter.
80+
fun id(x: Int) -> Int { x }
81+
""",
82+
expectedErrors: [
83+
.init(found: .parameter, expectedBefore: .returns),
84+
.init(found: .generic, expectedBefore: .returns),
85+
])
86+
}
87+
func testOutOfOrder3() throws {
88+
try testOutOfOrder(
89+
in: """
90+
/// Summary of subscript.
91+
///
92+
/// This is the description.
93+
/// - Note: This is still the description.
94+
/// # Parameter param: sample param desc
95+
/// # Yields: some stuff.
96+
/// # Projects:
97+
/// - Insecurity
98+
/// # Complexity: O(2)
99+
subscript foo(param: Int): T { }
100+
""",
101+
expectedErrors: [])
102+
}
103+
104+
func testInOrder() throws {
105+
try testOutOfOrder(
106+
in: """
107+
/// Summary of function.
108+
///
109+
/// This is the description.
110+
/// - Note: This is still the description.
111+
/// # Parameter x: This is a parameter.
112+
/// # Generic y: This is a generic parameter.
113+
/// # Returns: something.
114+
fun id(x: Int) -> Int { x }
115+
""",
116+
expectedErrors: []
117+
)
118+
}
119+
}

Tests/DocExtractorTests/SourceFileDocumentorTests/SimpleTests/SubscriptExtractionTest.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ final class SubscriptExtractionTest: XCTestCase {
1010
let sourceFileDocumentor = RealSourceFileDocumentor(
1111
commentParser: commentParser, markdownParser: HyloDocMarkdownParser.standard)
1212

13-
let sourceFile = SourceFile(
14-
synthesizedText: """
13+
// todo projects and yields should not be possible to add to a subscript at the same time.
14+
let sourceFile: SourceFile =
15+
"""
1516
/// # File-level:
1617
/// This is the summary of the file.
1718
///
@@ -26,23 +27,23 @@ final class SubscriptExtractionTest: XCTestCase {
2627
///
2728
/// This is the description.
2829
/// - Note: This is still the description.
29-
/// # Yields: some stuff.
3030
/// # Parameter param: sample param desc
31-
/// # Complexity: O(2)
31+
/// # Yields: some stuff.
3232
/// # Projects:
3333
/// - Insecurity
34+
/// # Complexity: O(2)
3435
subscript foo(param: Int): T {
3536
/// Summary of subscript implementation.
3637
///
3738
/// This is the description2.
3839
/// - Note: This is still the description2.
3940
/// # Yields: some other stuff.
40-
/// # Complexity: O(2)
4141
/// # Projects:
4242
/// - Insecurity
43+
/// # Complexity: O(2)
4344
T()
4445
}
45-
""", named: "testFile11.hylo")
46+
"""
4647

4748
let ast = try checkNoDiagnostic { d in
4849
try AST(fromSingleSourceFile: sourceFile, diagnostics: &d)
@@ -53,7 +54,7 @@ final class SubscriptExtractionTest: XCTestCase {
5354
let fileLevel = checkNoHDCDiagnostic { d in
5455
sourceFileDocumentor.document(
5556
ast: ast,
56-
translationUnitId: ast.resolveTranslationUnit(by: "testFile11.hylo")!,
57+
translationUnitId: ast.resolveTranslationUnit(by: sourceFile.baseName)!,
5758
into: &store,
5859
diagnostics: &d
5960
)

Tests/TestUtils/ASTExtension.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,6 @@ extension AST {
337337

338338
/// - Parameter name: file name without extension
339339
public func resolveTranslationUnit(by name: String) -> TranslationUnit.ID? {
340-
precondition(name.hasSuffix(".hylo"), "Name should be passed with extension.")
341-
342340
struct ASTWalker: ASTWalkObserver {
343341
var result: TranslationUnit.ID?
344342
let targetName: String

Tests/TestUtils/DiagnosticChecks.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ public func checkNoHDCDiagnostic<R>(
6666
var d = HDCDiagnosticSet()
6767
do {
6868
let r = try f(&d)
69-
checkHDCEmpty(d)
69+
checkHDCEmpty(d, file: testFile, line: line)
7070
return r
7171
} catch let d1 as HDCDiagnosticSet {
7272
XCTAssertEqual(
7373
d1, d, "thrown diagnostics don't match mutated diagnostics",
7474
file: testFile, line: line)
75-
checkHDCEmpty(d)
75+
checkHDCEmpty(d, file: testFile, line: line)
7676
throw d
7777
}
7878
}
@@ -110,10 +110,12 @@ public func expectHDCDiagnostic<R, D: HDCDiagnostic>(
110110
}
111111

112112
/// Reports any diagnostics in `s` as XCTest issues.
113-
public func checkHDCEmpty(_ s: HDCDiagnosticSet) {
113+
public func checkHDCEmpty(_ s: HDCDiagnosticSet, file: StaticString = #filePath, line: UInt = #line)
114+
{
114115
if !s.elements.isEmpty {
115116
XCTFail(
116-
"Unexpected diagnostics: \n\(s.elements.map{ "- " + $0.description }.joined(separator: "\n\n"))"
117+
"Unexpected diagnostics: \n\(s.elements.map{ "- " + $0.description }.joined(separator: "\n\n"))",
118+
file: file, line: line
117119
)
118120
}
119121
}

Tests/WebsiteGenTests/SimpleFullPipelineTest/ExampleModule/ModuleA/c.hylo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ trait D {}
5050
/// Summary of the product type.
5151
///
5252
/// This is the description of the product type.
53-
/// # Invariant: x and y must always be positive.
5453
/// # Generic T: This is a generic.
54+
/// # Invariant: x and y must always be positive.
5555
type A<T: D>{
5656
var x: Int
5757
var y: Int

Tests/WebsiteGenTests/SimpleFullPipelineTest/SimpleFullPipelineTest.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import XCTest
1010

1111
@testable import WebsiteGen
1212

13-
func runFullPipelineWithoutErrors(at sourceUrl: URL) throws {
13+
func runFullPipelineWithoutErrors(
14+
at sourceUrl: URL, file: StaticString = #filePath, line: UInt = #line
15+
) throws {
1416
let outputURL = URL(fileURLWithPath: "./test-output/" + UUID.init().uuidString)
1517

1618
let fileManager = FileManager.default
@@ -50,13 +52,11 @@ func runFullPipelineWithoutErrors(at sourceUrl: URL) throws {
5052
typedProgram: typedProgram,
5153
exportPath: outputURL
5254
)
53-
else { return XCTFail("failed to generate documentation") }
54-
55-
print("Documentation successfully generated at \(outputURL).")
55+
else {
56+
return XCTFail("failed to generate documentation", file: file, line: line)
57+
}
5658
case .failure(let error):
57-
print("Failed to extract documentation: \(error)")
58-
59-
XCTFail()
59+
XCTFail("Failed to extract documentation: \(error)", file: file, line: line)
6060
}
6161
}
6262

0 commit comments

Comments
 (0)