Skip to content

Commit 1615c39

Browse files
authored
Merge pull request #1350 from DougGregor/grouped-diagnostics
2 parents bcaad6d + 3803ca7 commit 1615c39

File tree

5 files changed

+664
-15
lines changed

5 files changed

+664
-15
lines changed

Sources/SwiftDiagnostics/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ add_swift_host_library(SwiftDiagnostics
1010
Diagnostic.swift
1111
DiagnosticsFormatter.swift
1212
FixIt.swift
13+
GroupedDiagnostics.swift
1314
Message.swift
1415
Note.swift
1516
)

Sources/SwiftDiagnostics/DiagnosticsFormatter.swift

Lines changed: 230 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,61 @@
1212

1313
import SwiftSyntax
1414

15+
extension Sequence where Element == Range<Int> {
16+
/// Given a set of ranges that are sorted in order of nondecreasing lower
17+
/// bound, merge any overlapping ranges to produce a sequence of
18+
/// nonoverlapping ranges.
19+
fileprivate func mergingOverlappingRanges() -> [Range<Int>] {
20+
var result: [Range<Int>] = []
21+
22+
var prior: Range<Int>? = nil
23+
for range in self {
24+
// If this is the first range we've seen, note it as the prior and
25+
// continue.
26+
guard let priorRange = prior else {
27+
prior = range
28+
continue
29+
}
30+
31+
// If the ranges overlap, expand the prior range.
32+
if priorRange.overlaps(range) {
33+
let lower = Swift.min(priorRange.lowerBound, range.lowerBound)
34+
let upper = Swift.max(priorRange.upperBound, range.upperBound)
35+
prior = lower..<upper
36+
continue
37+
}
38+
39+
// Append the prior range, then take this new range as the prior
40+
result.append(priorRange)
41+
prior = range
42+
}
43+
44+
if let priorRange = prior {
45+
result.append(priorRange)
46+
}
47+
return result
48+
}
49+
}
50+
1551
public struct DiagnosticsFormatter {
1652

17-
/// A wrapper struct for a source line and its diagnostics
53+
/// A wrapper struct for a source line, its diagnostics, and any
54+
/// non-diagnostic text that follows the line.
1855
private struct AnnotatedSourceLine {
1956
var diagnostics: [Diagnostic]
2057
var sourceString: String
58+
59+
/// Non-diagnostic text that is appended after this source line.
60+
///
61+
/// Suffix text can be used to provide more information following a source
62+
/// line, such as to provide an inset source buffer for a macro expansion
63+
/// that occurs on that line.
64+
var suffixText: String
65+
66+
/// Whether this line is free of annotations.
67+
var isFreeOfAnnotations: Bool {
68+
return diagnostics.isEmpty && suffixText.isEmpty
69+
}
2170
}
2271

2372
/// Number of lines which should be printed before and after the diagnostic message
@@ -41,9 +90,108 @@ public struct DiagnosticsFormatter {
4190
return formatter.annotatedSource(tree: tree, diags: diags)
4291
}
4392

93+
/// Colorize the given source line by applying highlights from diagnostics.
94+
private func colorizeSourceLine<SyntaxType: SyntaxProtocol>(
95+
_ annotatedLine: AnnotatedSourceLine,
96+
lineNumber: Int,
97+
tree: SyntaxType,
98+
sourceLocationConverter slc: SourceLocationConverter
99+
) -> String {
100+
guard colorize, !annotatedLine.diagnostics.isEmpty else {
101+
return annotatedLine.sourceString
102+
}
103+
104+
// Compute the set of highlight ranges that land on this line. These
105+
// are column ranges, sorted in order of increasing starting column, and
106+
// with overlapping ranges merged.
107+
let highlightRanges: [Range<Int>] = annotatedLine.diagnostics.map {
108+
$0.highlights
109+
}.joined().compactMap { (highlight) -> Range<Int>? in
110+
if highlight.root != Syntax(tree) {
111+
return nil
112+
}
113+
114+
let startLoc = highlight.startLocation(converter: slc, afterLeadingTrivia: true);
115+
guard let startLine = startLoc.line else {
116+
return nil
117+
}
118+
119+
// Find the starting column.
120+
let startColumn: Int
121+
if startLine < lineNumber {
122+
startColumn = 1
123+
} else if startLine == lineNumber, let column = startLoc.column {
124+
startColumn = column
125+
} else {
126+
return nil
127+
}
128+
129+
// Find the ending column.
130+
let endLoc = highlight.endLocation(converter: slc, afterTrailingTrivia: false)
131+
guard let endLine = endLoc.line else {
132+
return nil
133+
}
134+
135+
let endColumn: Int
136+
if endLine > lineNumber {
137+
endColumn = annotatedLine.sourceString.count
138+
} else if endLine == lineNumber, let column = endLoc.column {
139+
endColumn = column
140+
} else {
141+
return nil
142+
}
143+
144+
if startColumn == endColumn {
145+
return nil
146+
}
147+
148+
return startColumn..<endColumn
149+
}.sorted { (lhs, rhs) in
150+
lhs.lowerBound < rhs.lowerBound
151+
}.mergingOverlappingRanges()
152+
153+
// Map the column ranges into index ranges within the source string itself.
154+
let sourceString = annotatedLine.sourceString
155+
let highlightIndexRanges: [Range<String.Index>] = highlightRanges.map { highlightRange in
156+
let startIndex = sourceString.index(sourceString.startIndex, offsetBy: highlightRange.lowerBound - 1)
157+
let endIndex = sourceString.index(startIndex, offsetBy: highlightRange.count)
158+
return startIndex..<endIndex
159+
}
160+
161+
// Form the annotated string by copying in text from the original source,
162+
// highlighting the column ranges.
163+
var resultSourceString: String = ""
164+
var sourceIndex = sourceString.startIndex
165+
let annotation = ANSIAnnotation.sourceHighlight
166+
for highlightRange in highlightIndexRanges {
167+
// Text before the highlight range
168+
resultSourceString += sourceString[sourceIndex..<highlightRange.lowerBound]
169+
170+
// Highlighted source text
171+
let highlightString = String(sourceString[highlightRange])
172+
resultSourceString += annotation.applied(to: highlightString)
173+
174+
sourceIndex = highlightRange.upperBound
175+
}
176+
177+
resultSourceString += sourceString[sourceIndex...]
178+
return resultSourceString
179+
}
180+
44181
/// Print given diagnostics for a given syntax tree on the command line
45-
public func annotatedSource<SyntaxType: SyntaxProtocol>(tree: SyntaxType, diags: [Diagnostic]) -> String {
46-
let slc = SourceLocationConverter(file: "", tree: tree)
182+
///
183+
/// - Parameters:
184+
/// - suffixTexts: suffix text to be printed at the given absolute
185+
/// locations within the source file.
186+
func annotatedSource<SyntaxType: SyntaxProtocol>(
187+
fileName: String?,
188+
tree: SyntaxType,
189+
diags: [Diagnostic],
190+
indentString: String,
191+
suffixTexts: [(AbsolutePosition, String)],
192+
sourceLocationConverter: SourceLocationConverter? = nil
193+
) -> String {
194+
let slc = sourceLocationConverter ?? SourceLocationConverter(file: fileName ?? "", tree: tree)
47195

48196
// First, we need to put each line and its diagnostics together
49197
var annotatedSourceLines = [AnnotatedSourceLine]()
@@ -52,20 +200,34 @@ public struct DiagnosticsFormatter {
52200
let diagsForLine = diags.filter { diag in
53201
return diag.location(converter: slc).line == (sourceLineIndex + 1)
54202
}
55-
annotatedSourceLines.append(AnnotatedSourceLine(diagnostics: diagsForLine, sourceString: sourceLine))
203+
let suffixText = suffixTexts.compactMap { (position, text) in
204+
if slc.location(for: position).line == (sourceLineIndex + 1) {
205+
return text
206+
}
207+
208+
return nil
209+
}.joined()
210+
211+
annotatedSourceLines.append(AnnotatedSourceLine(diagnostics: diagsForLine, sourceString: sourceLine, suffixText: suffixText))
56212
}
57213

58214
// Only lines with diagnostic messages should be printed, but including some context
59215
let rangesToPrint = annotatedSourceLines.enumerated().compactMap { (lineIndex, sourceLine) -> Range<Int>? in
60216
let lineNumber = lineIndex + 1
61-
if !sourceLine.diagnostics.isEmpty {
217+
if !sourceLine.isFreeOfAnnotations {
62218
return Range<Int>(uncheckedBounds: (lower: lineNumber - contextSize, upper: lineNumber + contextSize + 1))
63219
}
64220
return nil
65221
}
66222

67223
var annotatedSource = ""
68224

225+
// If there was a filename, add it first.
226+
if let fileName = fileName {
227+
let header = colorizeBufferOutline("===")
228+
annotatedSource.append("\(indentString)\(header) \(fileName) \(header)\n")
229+
}
230+
69231
/// Keep track if a line missing char should be printed
70232
var hasLineBeenSkipped = false
71233

@@ -85,17 +247,28 @@ public struct DiagnosticsFormatter {
85247
// line numbers should be right aligned
86248
let lineNumberString = String(lineNumber)
87249
let leadingSpaces = String(repeating: " ", count: maxNumberOfDigits - lineNumberString.count)
88-
let linePrefix = "\(leadingSpaces)\(lineNumberString)"
250+
let linePrefix = "\(leadingSpaces)\(colorizeBufferOutline("\(lineNumberString)")) "
89251

90252
// If necessary, print a line that indicates that there was lines skipped in the source code
91253
if hasLineBeenSkipped && !annotatedSource.isEmpty {
92-
let lineMissingInfoLine = String(repeating: " ", count: maxNumberOfDigits) + " "
254+
let lineMissingInfoLine = indentString + String(repeating: " ", count: maxNumberOfDigits) + " \(colorizeBufferOutline(""))"
93255
annotatedSource.append("\(lineMissingInfoLine)\n")
94256
}
95257
hasLineBeenSkipped = false
96258

259+
// add indentation
260+
annotatedSource.append(indentString)
261+
97262
// print the source line
98-
annotatedSource.append("\(linePrefix)\(annotatedLine.sourceString)")
263+
annotatedSource.append(linePrefix)
264+
annotatedSource.append(
265+
colorizeSourceLine(
266+
annotatedLine,
267+
lineNumber: lineNumber,
268+
tree: tree,
269+
sourceLocationConverter: slc
270+
)
271+
)
99272

100273
// If the line did not end with \n (e.g. the last line), append it manually
101274
if annotatedSource.last != "\n" {
@@ -111,7 +284,7 @@ public struct DiagnosticsFormatter {
111284

112285
for (column, diags) in diagsPerColumn {
113286
// compute the string that is shown before each message
114-
var preMessage = String(repeating: " ", count: maxNumberOfDigits) + " "
287+
var preMessage = indentString + String(repeating: " ", count: maxNumberOfDigits) + " " + colorizeBufferOutline("")
115288
for c in 0..<column {
116289
if columnsWithDiagnostics.contains(c) {
117290
preMessage.append("")
@@ -125,10 +298,30 @@ public struct DiagnosticsFormatter {
125298
}
126299
annotatedSource.append("\(preMessage)╰─ \(colorizeIfRequested(diags.last!.diagMessage))\n")
127300
}
301+
302+
// Add suffix text.
303+
annotatedSource.append(annotatedLine.suffixText)
304+
if annotatedSource.last != "\n" {
305+
annotatedSource.append("\n")
306+
}
128307
}
129308
return annotatedSource
130309
}
131310

311+
/// Print given diagnostics for a given syntax tree on the command line
312+
public func annotatedSource<SyntaxType: SyntaxProtocol>(
313+
tree: SyntaxType,
314+
diags: [Diagnostic]
315+
) -> String {
316+
return annotatedSource(
317+
fileName: nil,
318+
tree: tree,
319+
diags: diags,
320+
indentString: "",
321+
suffixTexts: []
322+
)
323+
}
324+
132325
/// Annotates the given ``DiagnosticMessage`` with an appropriate ANSI color code (if the value of the `colorize`
133326
/// property is `true`) and returns the result as a printable string.
134327
private func colorizeIfRequested(_ message: DiagnosticMessage) -> String {
@@ -148,6 +341,24 @@ public struct DiagnosticsFormatter {
148341
return message.message
149342
}
150343
}
344+
345+
/// Apply the given color and trait to the specified text, when we are
346+
/// supposed to color the output.
347+
private func colorizeIfRequested(
348+
_ text: String,
349+
annotation: ANSIAnnotation
350+
) -> String {
351+
guard colorize, !text.isEmpty else {
352+
return text
353+
}
354+
355+
return annotation.applied(to: text)
356+
}
357+
358+
/// Colorize for the buffer outline and line numbers.
359+
func colorizeBufferOutline(_ text: String) -> String {
360+
colorizeIfRequested(text, annotation: .bufferOutline)
361+
}
151362
}
152363

153364
struct ANSIAnnotation {
@@ -195,4 +406,14 @@ struct ANSIAnnotation {
195406
static var normal: ANSIAnnotation {
196407
self.init(color: .normal, trait: .normal)
197408
}
409+
410+
/// Annotation used for the outline and line numbers of a buffer.
411+
static var bufferOutline: ANSIAnnotation {
412+
ANSIAnnotation(color: .cyan, trait: .normal)
413+
}
414+
415+
/// Annotation used for highlighting source text.
416+
static var sourceHighlight: ANSIAnnotation {
417+
ANSIAnnotation(color: .white, trait: .underline)
418+
}
198419
}

0 commit comments

Comments
 (0)