Skip to content

Commit 7a5bb68

Browse files
authored
Fix crash when emitted diagnostics have incorrect source ranges (#840)
1 parent de1cdc2 commit 7a5bb68

File tree

3 files changed

+39
-31
lines changed

3 files changed

+39
-31
lines changed

Sources/SwiftDocC/Infrastructure/Diagnostics/ANSIAnnotation.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ struct ANSIAnnotation {
3838
self.trait = trait
3939
}
4040

41-
func applied(to message: String) -> String {
42-
"\(code)\(message)\(ANSIAnnotation.normal.code)"
41+
func applied<S: StringProtocol>(to message: S) -> String {
42+
guard !message.isEmpty else {
43+
return ""
44+
}
45+
return "\(code)\(message)\(ANSIAnnotation.normal.code)"
4346
}
4447

4548
static var normal: ANSIAnnotation {

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticConsoleWriter.swift

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,8 @@ extension DefaultDiagnosticConsoleFormatter {
391391
let highlightedSource = highlightSource(
392392
sourceLine: sourceLine,
393393
lineNumber: lineNumber,
394-
range: diagnosticRange
394+
range: diagnosticRange,
395+
_diagnostic: diagnostic
395396
)
396397

397398
let separator: String
@@ -469,42 +470,46 @@ extension DefaultDiagnosticConsoleFormatter {
469470
private func highlightSource(
470471
sourceLine: String,
471472
lineNumber: Int,
472-
range: SourceRange
473+
range: SourceRange,
474+
_diagnostic: Diagnostic // used in a debug assertion to identify diagnostics with incorrect source ranges
473475
) -> String {
474476
guard highlight,
475477
lineNumber >= range.lowerBound.line && lineNumber <= range.upperBound.line,
476478
!sourceLine.isEmpty
477-
else { return sourceLine }
478-
479-
var startColumn: Int
480-
if lineNumber == range.lowerBound.line {
481-
startColumn = range.lowerBound.column
482-
} else {
483-
startColumn = 1
479+
else {
480+
return sourceLine
484481
}
485482

486-
var endColumn: Int
487-
if lineNumber == range.upperBound.line {
488-
endColumn = range.upperBound.column
489-
} else {
490-
endColumn = sourceLine.count + 1
483+
guard range.lowerBound.line == range.upperBound.line else {
484+
// When highlighting multiple lines, highlight the full line
485+
return ANSIAnnotation.sourceHighlight.applied(to: sourceLine)
491486
}
492487

493488
let sourceLineUTF8 = sourceLine.utf8
494-
495-
let columnRange = startColumn..<endColumn
496-
let startIndex = sourceLineUTF8.index(sourceLineUTF8.startIndex, offsetBy: columnRange.lowerBound - 1)
497-
let endIndex = sourceLineUTF8.index(startIndex, offsetBy: columnRange.count)
498-
let highlightRange = startIndex..<endIndex
499489

500-
let ansiAnnotation = ANSIAnnotation.sourceHighlight
501-
502-
var result = ""
503-
result += sourceLine[sourceLine.startIndex..<highlightRange.lowerBound]
504-
result += ansiAnnotation.applied(to: String(sourceLine[highlightRange]))
505-
result += sourceLine[highlightRange.upperBound..<sourceLine.endIndex]
490+
let highlightStart = range.lowerBound.column - 1
491+
let highlightEnd = range.upperBound.column - 1
506492

507-
return result
493+
assert(highlightStart <= sourceLineUTF8.count, {
494+
"""
495+
Received diagnostic with incorrect source range; (\(range.lowerBound.column) ..< \(range.upperBound.column)) extends beyond the text on line \(lineNumber) (\(sourceLineUTF8.count) characters)
496+
\(sourceLine)
497+
\(String(repeating: " ", count: range.lowerBound.column))\(String(repeating: "~", count: range.upperBound.column - range.lowerBound.column))
498+
Use this diagnostic information to reproduce the issue and correct the diagnostic range where it's emitted.
499+
ID : \(_diagnostic.identifier)
500+
SUMMARY : \(_diagnostic.summary)
501+
SOURCE : \(_diagnostic.source?.path ?? _diagnostic.range?.source?.path ?? "<nil>")
502+
"""
503+
}())
504+
505+
guard let before = String(sourceLineUTF8.prefix(highlightStart)),
506+
let highlighted = String(sourceLineUTF8.dropFirst(highlightStart).prefix(highlightEnd - highlightStart)),
507+
let after = String(sourceLineUTF8.dropFirst(highlightEnd))
508+
else {
509+
return sourceLine
510+
}
511+
512+
return "\(before)\(ANSIAnnotation.sourceHighlight.applied(to: highlighted))\(after)"
508513
}
509514

510515
private func readSourceLines(_ url: URL) -> [String] {

Tests/SwiftDocCTests/Diagnostics/DiagnosticConsoleWriterDefaultFormattingTest.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ class DiagnosticConsoleWriterDefaultFormattingTest: XCTestCase {
270270
let bundle = try XCTUnwrap(fs.bundles().first)
271271
let baseURL = bundle.baseURL
272272
let source = try XCTUnwrap(bundle.markupURLs.first)
273-
let range = SourceLocation(line: 3, column: 18, source: source)..<SourceLocation(line: 3, column: 32, source: source)
273+
let range = SourceLocation(line: 3, column: 18, source: source)..<SourceLocation(line: 3, column: 36, source: source)
274274

275275
let logStorage = LogHandle.LogStorage()
276276
let consumer = DiagnosticConsoleWriter(LogHandle.memory(logStorage), baseURL: baseURL, highlight: true, fileManager: fs)
@@ -282,10 +282,10 @@ class DiagnosticConsoleWriterDefaultFormattingTest: XCTestCase {
282282
XCTAssertEqual(logStorage.text, """
283283
\u{001B}[1;33mwarning: \(summary)\u{001B}[0;0m
284284
\(explanation)
285-
--> Something.docc/Nested folder/Article.md:3:18-3:32
285+
--> Something.docc/Nested folder/Article.md:3:18-3:36
286286
1 | # Title
287287
2 |
288-
3 + A short abstract \u{001B}[1;32mwith emoji \u{001B}[0;0m💻 in it.
288+
3 + A short abstract \u{001B}[1;32mwith emoji 💻 in\u{001B}[0;0m it.
289289
4 |
290290
5 | @Metadata {
291291
""")

0 commit comments

Comments
 (0)