Skip to content

Commit 9e212e5

Browse files
authored
Handle various out-of-bounds diagnostic ranges in default diagnostics formatter (#947)
* Clamp lines to highlight in a diagnostic to the source lines * Add test about highlight for in-bounds range of a line * Clamp diagnostic line highlight to the bounds of each source line rdar://129586253
1 parent a76ddce commit 9e212e5

File tree

2 files changed

+115
-15
lines changed

2 files changed

+115
-15
lines changed

Sources/SwiftDocC/Infrastructure/Diagnostics/DiagnosticConsoleWriter.swift

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -343,19 +343,18 @@ extension DefaultDiagnosticConsoleFormatter {
343343

344344
let sourceLines = readSourceLines(url)
345345

346-
guard !sourceLines.isEmpty
347-
else {
348-
return "\n--> \(formattedSourcePath(url)):\(diagnosticRange.lowerBound.line):\(diagnosticRange.lowerBound.column)-\(diagnosticRange.upperBound.line):\(diagnosticRange.upperBound.column)"
346+
guard sourceLines.indices.contains(diagnosticRange.lowerBound.line - 1), sourceLines.indices.contains(diagnosticRange.upperBound.line - 1) else {
347+
return "\n--> \(formattedSourcePath(url)):\(max(1, diagnosticRange.lowerBound.line)):\(max(1, diagnosticRange.lowerBound.column))-\(max(1, diagnosticRange.upperBound.line)):\(max(1, diagnosticRange.upperBound.column))"
349348
}
350349

351350
// A range containing the source lines and some surrounding context.
352-
let sourceRange = Range(
351+
let sourceLinesToDisplay = Range(
353352
uncheckedBounds: (
354-
lower: max(1, diagnosticRange.lowerBound.line - Self.contextSize) - 1,
355-
upper: min(sourceLines.count, diagnosticRange.upperBound.line + Self.contextSize)
353+
lower: diagnosticRange.lowerBound.line - Self.contextSize - 1,
354+
upper: diagnosticRange.upperBound.line + Self.contextSize
356355
)
357-
)
358-
let maxLinePrefixWidth = String(sourceRange.upperBound).count
356+
).clamped(to: sourceLines.indices)
357+
let maxLinePrefixWidth = String(sourceLinesToDisplay.upperBound).count
359358

360359
var suggestionsPerLocation = [SourceLocation: [String]]()
361360
for solution in solutions {
@@ -377,11 +376,10 @@ extension DefaultDiagnosticConsoleFormatter {
377376
// Example:
378377
// --> /path/to/file.md:1:10-2:20
379378
result.append("\n\(String(repeating: " ", count: maxLinePrefixWidth))--> ")
380-
result.append( "\(formattedSourcePath(url)):\(diagnosticRange.lowerBound.line):\(diagnosticRange.lowerBound.column)-\(diagnosticRange.upperBound.line):\(diagnosticRange.upperBound.column)"
381-
)
379+
result.append( "\(formattedSourcePath(url)):\(max(1, diagnosticRange.lowerBound.line)):\(max(1, diagnosticRange.lowerBound.column))-\(max(1, diagnosticRange.upperBound.line)):\(max(1, diagnosticRange.upperBound.column))")
382380

383-
for (sourceLineIndex, sourceLine) in sourceLines[sourceRange].enumerated() {
384-
let lineNumber = sourceLineIndex + sourceRange.lowerBound + 1
381+
for (sourceLineIndex, sourceLine) in sourceLines[sourceLinesToDisplay].enumerated() {
382+
let lineNumber = sourceLineIndex + sourceLinesToDisplay.lowerBound + 1
385383
let linePrefix = "\(lineNumber)".padding(toLength: maxLinePrefixWidth, withPad: " ", startingAt: 0)
386384

387385
let highlightedSource = highlightSource(
@@ -483,7 +481,7 @@ extension DefaultDiagnosticConsoleFormatter {
483481

484482
let sourceLineUTF8 = sourceLine.utf8
485483

486-
let highlightStart = range.lowerBound.column - 1
484+
let highlightStart = max(0, range.lowerBound.column - 1)
487485
let highlightEnd = range.upperBound.column - 1
488486

489487
assert(highlightStart <= sourceLineUTF8.count, {

Tests/SwiftDocCTests/Diagnostics/DiagnosticConsoleWriterDefaultFormattingTest.swift

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,108 @@ class DiagnosticConsoleWriterDefaultFormattingTest: XCTestCase {
390390
}
391391
}
392392

393+
func testClampsDiagnosticRangeToSourceRange() throws {
394+
let fs = try TestFileSystem(folders: [
395+
Folder(name: "Something.docc", content: [
396+
TextFile(name: "Article.md", utf8Content: """
397+
# Title
398+
399+
A very short article with only an abstract.
400+
""")
401+
])
402+
])
403+
404+
let summary = "Test diagnostic summary"
405+
let explanation = "Test diagnostic explanation."
406+
407+
let bundle = try XCTUnwrap(fs.bundles().first)
408+
let baseURL = bundle.baseURL
409+
let source = try XCTUnwrap(bundle.markupURLs.first)
410+
411+
typealias Location = (line: Int, column: Int)
412+
func logMessageFor(start: Location, end: Location) throws -> String {
413+
let range = SourceLocation(line: start.line, column: start.column, source: source)..<SourceLocation(line: end.line, column: end.column, source: source)
414+
415+
let logStorage = LogHandle.LogStorage()
416+
let consumer = DiagnosticConsoleWriter(LogHandle.memory(logStorage), baseURL: baseURL, highlight: true, fileManager: fs)
417+
418+
let diagnostic = Diagnostic(source: source, severity: .warning, range: range, identifier: "org.swift.docc.test-identifier", summary: summary, explanation: explanation)
419+
consumer.receive([Problem(diagnostic: diagnostic, possibleSolutions: [])])
420+
try consumer.flush()
421+
422+
// There are no lines before line 1
423+
return logStorage.text
424+
}
425+
426+
// Highlight the "Title" word on line 1
427+
XCTAssertEqual(try logMessageFor(start: (line: 1, column: 3), end: (line: 1, column: 8)), """
428+
\u{001B}[1;33mwarning: \(summary)\u{001B}[0;0m
429+
\(explanation)
430+
--> Something.docc/Article.md:1:3-1:8
431+
1 + # \u{001B}[1;32mTitle\u{001B}[0;0m
432+
2 |
433+
3 | A very short article with only an abstract.
434+
""")
435+
436+
// Highlight the "short" word on line 3
437+
XCTAssertEqual(try logMessageFor(start: (line: 3, column: 8), end: (line: 3, column: 13)), """
438+
\u{001B}[1;33mwarning: \(summary)\u{001B}[0;0m
439+
\(explanation)
440+
--> Something.docc/Article.md:3:8-3:13
441+
1 | # Title
442+
2 |
443+
3 + A very \u{001B}[1;32mshort\u{001B}[0;0m article with only an abstract.
444+
""")
445+
446+
// Extend the highlight beyond the end of that line
447+
XCTAssertEqual(try logMessageFor(start: (line: 3, column: 8), end: (line: 3, column: 100)), """
448+
\u{001B}[1;33mwarning: \(summary)\u{001B}[0;0m
449+
\(explanation)
450+
--> Something.docc/Article.md:3:8-3:100
451+
1 | # Title
452+
2 |
453+
3 + A very \u{001B}[1;32mshort article with only an abstract.\u{001B}[0;0m
454+
""")
455+
456+
// Extend the highlight beyond the start of that line
457+
XCTAssertEqual(try logMessageFor(start: (line: 3, column: -4), end: (line: 3, column: 13)), """
458+
\u{001B}[1;33mwarning: \(summary)\u{001B}[0;0m
459+
\(explanation)
460+
--> Something.docc/Article.md:3:1-3:13
461+
1 | # Title
462+
2 |
463+
3 + \u{001B}[1;32mA very short\u{001B}[0;0m article with only an abstract.
464+
""")
465+
466+
// Highlight a line before the start of the file
467+
XCTAssertEqual(try logMessageFor(start: (line: -4, column: 1), end: (line: -4, column: 5)), """
468+
\u{001B}[1;33mwarning: \(summary)\u{001B}[0;0m
469+
\(explanation)
470+
--> Something.docc/Article.md:1:1-1:5
471+
""")
472+
473+
// Highlight a line after the end of the file
474+
XCTAssertEqual(try logMessageFor(start: (line: 100, column: 1), end: (line: 100, column: 5)), """
475+
\u{001B}[1;33mwarning: \(summary)\u{001B}[0;0m
476+
\(explanation)
477+
--> Something.docc/Article.md:100:1-100:5
478+
""")
479+
480+
// Extended the highlighted lines before the start of the file
481+
XCTAssertEqual(try logMessageFor(start: (line: -4, column: 1), end: (line: 1, column: 5)), """
482+
\u{001B}[1;33mwarning: \(summary)\u{001B}[0;0m
483+
\(explanation)
484+
--> Something.docc/Article.md:1:1-1:5
485+
""")
486+
487+
// Extended the highlighted lines after the end of the file
488+
XCTAssertEqual(try logMessageFor(start: (line: 1, column: 1), end: (line: 100, column: 5)), """
489+
\u{001B}[1;33mwarning: \(summary)\u{001B}[0;0m
490+
\(explanation)
491+
--> Something.docc/Article.md:1:1-100:5
492+
""")
493+
}
494+
393495
func testEmitAdditionReplacementSolution() throws {
394496
func problemsLoggerOutput(possibleSolutions: [Solution]) -> String {
395497
let logger = Logger()
@@ -399,8 +501,8 @@ class DiagnosticConsoleWriterDefaultFormattingTest: XCTestCase {
399501
try? consumer.flush()
400502
return logger.output
401503
}
402-
let sourcelocation = SourceLocation(line: 1, column: 1, source: nil)
403-
let range = sourcelocation..<sourcelocation
504+
let sourceLocation = SourceLocation(line: 1, column: 1, source: nil)
505+
let range = sourceLocation..<sourceLocation
404506
XCTAssertEqual(
405507
problemsLoggerOutput(possibleSolutions: [
406508
Solution(summary: "Create a sloth.", replacements: [

0 commit comments

Comments
 (0)