Skip to content

Commit ea09951

Browse files
committed
Added a formatter for the diagnostics
1 parent 093e5ee commit ea09951

File tree

2 files changed

+134
-1
lines changed

2 files changed

+134
-1
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Felix Wehnert on 02.10.22.
6+
//
7+
8+
import Foundation
9+
import SwiftParser
10+
import SwiftSyntax
11+
import SwiftDiagnostics
12+
13+
14+
// To put `Diagnostic`s into a set they must conform to hashable.
15+
// Two `Diagnostic` instances are the same, if they contain the same diagnosticID at the same node.
16+
extension Diagnostic: Hashable {
17+
public func hash(into hasher: inout Hasher) {
18+
hasher.combine(self.node)
19+
}
20+
21+
public static func ==(lhs: Diagnostic, rhs: Diagnostic) -> Bool {
22+
return lhs.node == rhs.node && lhs.diagnosticID == rhs.diagnosticID
23+
}
24+
}
25+
26+
struct DiagnosticsFormatter {
27+
28+
/**
29+
A wrapper struct for a source line and its diagnostics
30+
*/
31+
private struct SourceLine {
32+
var diagnostics = Set<Diagnostic>()
33+
var sourceString: String
34+
}
35+
36+
/**
37+
Number of lines which should be printed before and after the diagnostic message
38+
*/
39+
static let contextSize = 2
40+
41+
/**
42+
Print given diagnostics for a given syntax tree on the command line
43+
*/
44+
static func printDiagnostics(tree: SourceFileSyntax, diags: [Diagnostic]) {
45+
46+
let slc = SourceLocationConverter(file: "", tree: tree)
47+
48+
// First, we need to put each line and its diagnostics together
49+
var sourceLines = [SourceLine]()
50+
51+
for sourceLine in tree.description.split(separator: "\n", omittingEmptySubsequences: false) {
52+
53+
let diagsForLine = diags.filter { diag in
54+
let line = diag.location(converter: slc).line
55+
if line == (sourceLines.count + 1) {
56+
return true
57+
}
58+
return false
59+
}
60+
61+
sourceLines.append(SourceLine(diagnostics: .init(diagsForLine), sourceString: String(sourceLine)))
62+
}
63+
64+
// Only lines with diagnostic messages should be printed, but including some context
65+
let rangesToPrint = sourceLines.enumerated().compactMap { (index, sourceLine) -> Range<Int>? in
66+
if sourceLine.diagnostics.isEmpty == false {
67+
return Range<Int>(uncheckedBounds: (lower: index - Self.contextSize, upper: index + Self.contextSize))
68+
}
69+
return nil
70+
}
71+
72+
/// Keep track if a line missing char should be printed
73+
var printLineMissingChar = false
74+
/// Keep track if the first block of source lines were already printed
75+
var isFirstLinePrinted = false
76+
77+
for (lineNumber, line) in sourceLines.enumerated() {
78+
guard rangesToPrint.contains(where: { range in
79+
range.contains(lineNumber)
80+
}) else {
81+
printLineMissingChar = true
82+
continue
83+
}
84+
85+
// line numbers should be right aligned
86+
let numberOfDigits = Int(floor(log10(Float(abs(sourceLines.count))))) + 1
87+
let lineNumberString = String(lineNumber + 1)
88+
let leadingSpaces = Array(repeating: " ", count: numberOfDigits - lineNumberString.count).joined(separator: "")
89+
let linePrefix = "\(leadingSpaces)\(lineNumberString)"
90+
91+
// If necessary, print a line that indicates that there was lines skipped in the source code
92+
if printLineMissingChar && isFirstLinePrinted {
93+
let lineMissingInfoLine = Array(repeating: " ", count: numberOfDigits).joined(separator: "") + ""
94+
print("\(lineMissingInfoLine)")
95+
}
96+
printLineMissingChar = false
97+
98+
// print the source line
99+
print(linePrefix, line.sourceString, separator: "")
100+
isFirstLinePrinted = true
101+
102+
let columnsWithDiagnostics = Set(line.diagnostics.map { $0.location(converter: slc).column ?? 0 })
103+
let diagsPerColumn = Dictionary(grouping: line.diagnostics) { diag in
104+
diag.location(converter: slc).column ?? 0
105+
}.sorted { lhs, rhs in
106+
lhs.key > rhs.key
107+
}
108+
109+
for (column, diags) in diagsPerColumn {
110+
111+
// compute the string that is shown before each message
112+
var preMessage = Array(repeating: " ", count: numberOfDigits).joined(separator: "") + ""
113+
for c in 0..<column {
114+
if columnsWithDiagnostics.contains(c) {
115+
preMessage.append("")
116+
} else {
117+
preMessage.append(" ")
118+
}
119+
}
120+
121+
for diag in diags.dropLast(1) {
122+
print(preMessage, "├─ ", diag.message, separator: "")
123+
}
124+
print(preMessage, "╰─ ", diags.last!.message, separator: "")
125+
126+
}
127+
}
128+
print("")
129+
130+
}
131+
}

Sources/swift-parser-test/swift-parser-test.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,10 @@ class PrintDiags: ParsableCommand {
163163
languageVersion: swiftVersion,
164164
enableBareSlashRegexLiteral: enableBareSlashRegex
165165
)
166+
166167
var diags = ParseDiagnosticsGenerator.diagnostics(for: tree)
167-
168+
DiagnosticsFormatter.printDiagnostics(tree: tree, diags: diags)
169+
168170
if foldSequences {
169171
diags += foldAllSequences(tree).1
170172
}

0 commit comments

Comments
 (0)