Skip to content
105 changes: 71 additions & 34 deletions Sources/CartonHelpers/Parsers/DiagnosticsParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ public struct DiagnosticsParser: ProcessOutputParser {
struct CustomDiagnostic {
let kind: Kind
let file: String
let line: String.SubSequence
/// The number of the row in the source file that the diagnosis is for.
let lineNumber: Int
let char: String.SubSequence
let code: String
let message: String
Expand Down Expand Up @@ -124,13 +125,11 @@ public struct DiagnosticsParser: ProcessOutputParser {
.replacingOccurrences(of: ":", with: "") == String(currFile)
else { continue }
fileMessages.append(
.init(
kind: CustomDiagnostic
.Kind(rawValue: String(components[2]
.trimmingCharacters(in: .whitespaces))) ??
.note,
CustomDiagnostic(
kind: CustomDiagnostic.Kind(rawValue: String(components[2].trimmingCharacters(in: .whitespaces))) ?? .note,
file: file,
line: components[0],
// FIXME: We should handle this more gracefully than force-unwrapping it.
lineNumber: Int(components[0])!,
char: components[1],
code: String(lines[lineIdx]),
message: components.dropFirst(3).joined(separator: ":")
Expand All @@ -157,14 +156,13 @@ public struct DiagnosticsParser: ProcessOutputParser {
for (file, messages) in diagnostics.sorted(by: { $0.key < $1.key }) {
guard messages.count > 0 else { continue }
terminal.write("\(" \(file) ", color: "[1m", "[7m")") // bold, reversed
terminal.write(" \(messages.first!.file)\(messages.first!.line)\n\n", inColor: .grey)
// Group messages that occur on sequential lines to provie a more readable output
terminal.write(" \(messages.first!.file)\(messages.first!.lineNumber)\n\n", inColor: .grey)
// Group messages that occur on sequential lines to provide a more readable output
var groupedMessages = [[CustomDiagnostic]]()
for message in messages {
if let lastLineStr = groupedMessages.last?.last?.line,
let lastLine = Int(lastLineStr),
let line = Int(message.line),
lastLine == line - 1 || lastLine == line
if let finalLineNumber = groupedMessages.last?.last?.lineNumber,
// `message.lineNumber` is the current line number.
finalLineNumber == message.lineNumber - 1 || finalLineNumber == message.lineNumber
{
groupedMessages[groupedMessages.count - 1].append(message)
} else {
Expand All @@ -180,51 +178,90 @@ public struct DiagnosticsParser: ProcessOutputParser {
" \(" \(kind) ", color: message.kind.color, "[37;1m") \(message.message)\n"
) // 37;1: bright white
}
let maxLine = messages.map(\.line.count).max() ?? 0
let greatestLineNumber = messages.map(\.lineNumber).max() ?? 0
let numberOfDigitsInGreatestLineNumber: Int = {
let (quotient, remainder) = greatestLineNumber.quotientAndRemainder(dividingBy: 10)
return quotient + (remainder == 0 ? 0 : 1)
}()
for (offset, message) in messages.enumerated() {
if offset > 0 {
// Make sure we don't log the same line twice
if messages[offset - 1].line != message.line {
flush(messages: messages, message: message, maxLine: maxLine, terminal)
if messages[offset - 1].lineNumber != message.lineNumber {
flush(
messages: messages,
message: message,
minimumSizeForLineNumbering: numberOfDigitsInGreatestLineNumber,
terminal: terminal
)
}
} else {
flush(messages: messages, message: message, maxLine: maxLine, terminal)
flush(
messages: messages,
message: message,
minimumSizeForLineNumbering: numberOfDigitsInGreatestLineNumber,
terminal: terminal
)
}
}
terminal.write("\n")
}
terminal.write("\n")
}
}


/// <#Description#>
/// - Parameters:
/// - messages: <#messages description#>
/// - message: <#message description#>
/// - minimumSizeForLineNumbering: The minimum space that must be reserved for line numbers, so that they are well-aligned in the output.
/// - terminal: <#terminal description#>
func flush(
messages: [CustomDiagnostic],
message: CustomDiagnostic,
maxLine: Int,
_ terminal: InteractiveWriter
minimumSizeForLineNumbering: Int,
terminal: InteractiveWriter
) {
// Get all diagnostics for a particular line.
let allChars = messages.filter { $0.line == message.line }.map(\.char)
let allChars = messages.filter { $0.lineNumber == message.lineNumber }.map(\.char)
// Output the code for this line, syntax highlighted
let paddedLine = message.line.padding(toLength: maxLine, withPad: " ", startingAt: 0)
let highlightedCode = Self.highlighter.highlight(message.code)
terminal
.write(
" \("\(paddedLine) | ", color: "[36m")\(highlightedCode)\n"
) // 36: cyan
terminal.write(
" " + "".padding(toLength: maxLine, withPad: " ", startingAt: 0) + " | ",
inColor: .cyan
)
/// A base-10 representation of the number of the row that the diagnosis is for, aligned vertically with all other rows.
let verticallyAlignedLineNumber = String(message.lineNumber, radix: 10).padding(toLength: minimumSizeForLineNumbering, withPad: " ", startingAt: 0)
/// The line of code that the diagnostics message is for.
let sourceLine = message.code
// The following 2 assignments remove the leading whitespace from each line of code in the diagnostics.
// Although technically, we should remove only horizontal whitespace characters, but since there is no vertical whitespace in a continuous line, we can safely remove all whitespace characters.
/// The position of the first non-whitespace character in the line of code that the diagnostics message is for.
///
/// If no such character exists, then the position is the same as the line's `endIndex`.
let firstIndexOfNonWhitespaceCharacterInSourceLine = sourceLine.firstIndex(where: { !$0.isWhitespace } ) ?? sourceLine.endIndex
/// The line of code that the diagnostics message is for, with leading whitespace removed.
let sourceLineSansWhitespace = sourceLine[firstIndexOfNonWhitespaceCharacterInSourceLine...]
let highlightedCode = Self.highlighter.highlight(String(sourceLineSansWhitespace))
// Each line of diagnostics output is indented with 2 spaces.
terminal.write(" \(verticallyAlignedLineNumber) | ", inColor: .cyan)
terminal.write(" \(highlightedCode)\n")
terminal.write(" \(String(repeating: " ", count: minimumSizeForLineNumbering)) | ", inColor: .cyan)

// Aggregate the indicators (^ point to the error) onto a single line
var charIndicators = String(repeating: " ", count: Int(message.char)!) + "^"

// Remove leading whitespace.
var charIndicators = String(repeating: " ", count: Int(message.char)! - (sourceLine.count - sourceLineSansWhitespace.count)) + "^"
if allChars.count > 0 {
for char in allChars.dropFirst() {
let idx = Int(char)!
if idx >= charIndicators.count {
charIndicators
.append(String(repeating: " ", count: idx - charIndicators.count) + "^")
for index in charIndicators.count..<idx {
// If the character above the current position is a whitespace character,
// then copy it. If not, then append a space (U+0020).
// Different terminals deal with whitespace characters differently.
// It's better to let the terminal decide how it wants to do,
// so that the "^" and the error location are well-aligned.
charIndicators.append(
sourceLine[sourceLine.index(sourceLine.startIndex, offsetBy: idx)].isWhitespace ?
sourceLine[sourceLine.index(sourceLine.startIndex, offsetBy: idx)] : " "
)
}
charIndicators.append("^")
} else {
var arr = Array(charIndicators)
arr[idx] = "^"
Expand Down
17 changes: 8 additions & 9 deletions Sources/CartonHelpers/Parsers/TestsParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,16 @@ public struct TestsParser: ProcessOutputParser {
)
} else if let problem = line.matches(regex: Regex.problem),
let path = line.match(of: Regex.problem, labelled: .path),
let lineNum = line.match(of: Regex.problem, labelled: .line),
let lineNumberInBase10 = line.match(of: Regex.problem, labelled: .line),
let lineNumber = Int(lineNumberInBase10),
let status = line.match(of: Regex.problem, labelled: .status),
let suite = line.match(of: Regex.problem, labelled: .suite),
let testCase = line.match(of: Regex.problem, labelled: .testCase)
{
let diag = DiagnosticsParser.CustomDiagnostic(
kind: DiagnosticsParser.CustomDiagnostic.Kind(rawValue: String(status)) ?? .note,
file: String(path),
line: lineNum,
lineNumber: lineNumber,
char: "0",
code: "",
message: String(problem)
Expand Down Expand Up @@ -255,7 +256,7 @@ public struct TestsParser: ProcessOutputParser {
"\(testCase.name) \("(\(Int(Double(testCase.duration)! * 1000))ms)", color: "[90m")\n"
) // gray
for problem in testCase.problems {
terminal.write("\n \(problem.file, color: "[90m"):\(problem.line)\n")
terminal.write("\n \(problem.file, color: "[90m"):\(problem.lineNumber)\n")
terminal.write(" \(problem.message)\n\n")
// Format XCTAssert functions
for assertion in Regex.Assertion.allCases {
Expand All @@ -268,9 +269,7 @@ public struct TestsParser: ProcessOutputParser {
}
}
// Get the line of code from the file and output it for context.
if let lineNum = Int(problem.line),
lineNum > 0
{
if problem.lineNumber > 0 {
var fileContents: String?
if let fileBuf = fileBufs.first(where: { $0.path == problem.file })?.contents {
fileContents = fileBuf
Expand All @@ -283,9 +282,9 @@ public struct TestsParser: ProcessOutputParser {
}
if let fileContents = fileContents {
let fileLines = fileContents.components(separatedBy: .newlines)
guard fileLines.count >= lineNum else { break }
let highlightedCode = Self.highlighter.highlight(String(fileLines[lineNum - 1]))
terminal.write(" \("\(problem.line) | ", color: "[36m")\(highlightedCode)\n")
guard fileLines.count >= problem.lineNumber else { break }
let highlightedCode = Self.highlighter.highlight(String(fileLines[problem.lineNumber - 1]))
terminal.write(" \("\(problem.lineNumber) | ", color: "[36m")\(highlightedCode)\n")
}
}
}
Expand Down