Skip to content

Pretty print diagnostics #122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 14, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@
"version": "4.2.0"
}
},
{
"package": "Splash",
"repositoryURL": "https://github.com/JohnSundell/Splash",
"state": {
"branch": null,
"revision": "ca9a1b7bff35381fe5ae9fe2ec9333cc5f2a586c",
"version": "0.13.0"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ let package = Package(
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.10.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.29.3"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.0"),
.package(url: "https://github.com/JohnSundell/Splash.git", from: "0.14.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module
Expand Down Expand Up @@ -58,6 +59,7 @@ let package = Package(
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
openCombineProduct,
"Splash",
]
),
// This target is used only for release automation tasks and
Expand Down
261 changes: 261 additions & 0 deletions Sources/CartonHelpers/DiagnosticsParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// Copyright 2020 Carton contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation
import Splash
import TSCBasic

private extension StringProtocol {
func matches(regex: NSRegularExpression) -> String.SubSequence? {
let str = String(self)
guard let range = str.range(of: regex),
range.upperBound < str.endIndex
else { return nil }
return str[range.upperBound..<str.endIndex]
}

func range(of regex: NSRegularExpression) -> Range<String.Index>? {
let str = String(self)
let range = NSRange(location: 0, length: utf16.count)
guard let match = regex.firstMatch(in: str, options: [], range: range),
let matchRange = Range(match.range, in: str)
else {
return nil
}
return matchRange
}
}

private extension String.StringInterpolation {
mutating func appendInterpolation<T>(_ value: T, color: String...) {
appendInterpolation("\(color.map { "\u{001B}\($0)" }.joined())\(value)\u{001B}[0m")
}
}

private extension TokenType {
var color: String {
// Reference on escape codes: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
switch self {
case .keyword: return "[35;1m" // magenta;bold
case .comment: return "[90m" // bright black
case .call, .dotAccess, .property, .type: return "[94m" // bright blue
case .number, .preprocessing: return "[33m" // yellow
case .string: return "[91;1m" // bright red;bold
default: return "[0m" // reset
}
}
}

private struct TerminalOutputFormat: OutputFormat {
func makeBuilder() -> TerminalOutputBuilder {
.init()
}

struct TerminalOutputBuilder: OutputBuilder {
var output: String = ""

mutating func addToken(_ token: String, ofType type: TokenType) {
output.append("\(token, color: type.color)")
}

mutating func addPlainText(_ text: String) {
output.append(text)
}

mutating func addWhitespace(_ whitespace: String) {
output.append(whitespace)
}

mutating func build() -> String {
output
}
}
}

/// Parses and re-formats diagnostics output by the Swift compiler.
///
/// The compiler output often repeats iteself, and the diagnostics can sometimes be
/// difficult to read.
/// This reformats them to a more readable output.
struct DiagnosticsParser {
// swiftlint:disable force_try
enum Regex {
/// The output has moved to a new file
static let enterFile = try! NSRegularExpression(pattern: #"\[\d+\/\d+\] Compiling \w+ "#)
/// A message is beginning with the line # following the `:`
static let line = try! NSRegularExpression(pattern: #"(\/\w+)+\.\w+:"#)
}

// swiftlint:enable force_try

struct CustomDiagnostic {
let kind: Kind
let file: String
let line: String.SubSequence
let char: String.SubSequence
let code: String
let message: String.SubSequence

enum Kind: String {
case error, warning, note
var color: String {
switch self {
case .error: return "[41;1m" // bright red background
case .warning: return "[43;1m" // bright yellow background
case .note: return "[7m" // reversed
}
}
}
}

fileprivate static let highlighter = SyntaxHighlighter(format: TerminalOutputFormat())

func parse(_ output: String, _ terminal: InteractiveWriter) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 🚫 Function body should span 50 lines or less excluding comments and whitespace: currently spans 72 lines (function_body_length)

let lines = output.split(separator: "\n")
var lineIdx = 0

var diagnostics = [String.SubSequence: [CustomDiagnostic]]()

var currFile: String.SubSequence?
var fileMessages = [CustomDiagnostic]()

while lineIdx < lines.count {
let line = lines[lineIdx]
if let file = line.matches(regex: Regex.enterFile) {
if let currFile = currFile {
diagnostics[currFile] = fileMessages
}
currFile = file
fileMessages = []
} else if let currFile = currFile {
if let message = line.matches(regex: Regex.line) {
let components = message.split(separator: ":")
if components.count > 3 {
lineIdx += 1
let file = line.replacingOccurrences(of: message, with: "")
guard file.split(separator: "/").last?
.replacingOccurrences(of: ":", with: "") == String(currFile)
else { continue }
fileMessages.append(
.init(
kind: CustomDiagnostic
.Kind(rawValue: String(components[2]
.trimmingCharacters(in: .whitespaces))) ??
.note,
file: file,
line: components[0],
char: components[1],
code: String(lines[lineIdx]),
message: components[3]
)
)
}
}
} else {
terminal.write(String(line) + "\n", inColor: .cyan)
}
lineIdx += 1
}
if let currFile = currFile {
diagnostics[currFile] = fileMessages
}

outputDiagnostics(diagnostics, terminal)
}

func outputDiagnostics(
_ diagnostics: [String.SubSequence: [CustomDiagnostic]],
_ terminal: InteractiveWriter
) {
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
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
{
groupedMessages[groupedMessages.count - 1].append(message)
} else {
groupedMessages.append([message])
}
}
for messages in groupedMessages {
// Output the diagnostic message
for message in messages {
let kind = message.kind.rawValue.uppercased()
terminal
.write(
" \(" \(kind) ", color: message.kind.color, "[37;1m") \(message.message)\n"
) // 37;1: bright white
}
let maxLine = messages.map(\.line.count).max() ?? 0
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)
}
} else {
flush(messages: messages, message: message, maxLine: maxLine, terminal)
}
}
terminal.write("\n")
}
terminal.write("\n")
}
}

func flush(
messages: [CustomDiagnostic],
message: CustomDiagnostic,
maxLine: Int,
_ terminal: InteractiveWriter
) {
// Get all diagnostics for a particular line.
let allChars = messages.filter { $0.line == message.line }.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
)

// Aggregate the indicators (^ point to the error) onto a single line
var charIndicators = String(repeating: " ", count: Int(message.char)!) + "^"
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) + "^")
} else {
var arr = Array(charIndicators)
arr[idx] = "^"
charIndicators = String(arr)
}
}
}
terminal.write("\(charIndicators)\n", inColor: .red, bold: true)
}
}
8 changes: 8 additions & 0 deletions Sources/CartonHelpers/InteractiveWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,12 @@ public final class InteractiveWriter {
stream.flush()
}
}

public func saveCursor() {
term?.write("\u{001B}[s")
}

public func revertCursorAndClear() {
term?.write("\u{001B}[u\u{001B}[2J\u{001B}H")
}
}
19 changes: 15 additions & 4 deletions Sources/CartonHelpers/ProcessRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,23 @@ public final class ProcessRunner {

private var subscription: AnyCancellable?

// swiftlint:disable:next function_body_length
public init(
_ arguments: [String],
clearOutputLines: Bool = true,
loadingMessage: String = "Running...",
_ terminal: InteractiveWriter
) {
let subject = PassthroughSubject<String, Error>()
var tmpOutput = ""
publisher = subject
.handleEvents(
receiveOutput: {
if clearOutputLines {
// Aggregate this for formatting later
terminal.clearLine()
terminal.write(String($0.dropLast()))
terminal.write(loadingMessage, inColor: .yellow)
tmpOutput += $0
} else {
terminal.write($0)
}
Expand All @@ -69,12 +74,17 @@ public final class ProcessRunner {
case let .failure(error):
let errorString = String(describing: error)
if errorString.isEmpty {
terminal.clearLine()
terminal.write(
"\nProcess failed, check the build process output above.\n",
"Compilation failed.\n\n",
inColor: .red
)
DiagnosticsParser().parse(tmpOutput, terminal)
} else {
terminal.write("\nProcess failed and produced following output: \n", inColor: .red)
terminal.write(
"\nProcess failed and produced following output: \n",
inColor: .red
)
print(error)
}
}
Expand Down Expand Up @@ -113,7 +123,8 @@ public final class ProcessRunner {
subject.send(completion: .failure(error))
default:
let errorDescription = String(data: Data(stderrBuffer), encoding: .utf8) ?? ""
return subject.send(completion: .failure(ProcessRunnerError(description: errorDescription)))
return subject
.send(completion: .failure(ProcessRunnerError(description: errorDescription)))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CartonHelpers/TerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import TSCBasic

private extension String {
static var home = "\u{001B}[H"
static var clearScreen = "\u{001B}[2J"
static var clearScreen = "\u{001B}[2J\u{001B}[H\u{001B}[3J"
static var clear = "\u{001B}[J"
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftToolchain/Toolchain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,10 @@ public final class Toolchain {
let builderArguments = try [
swiftPath.pathString, "build", "-c", isRelease ? "release" : "debug", "--product", product,
"--enable-test-discovery", "--destination", destination ?? inferDestinationPath().pathString,
"-Xswiftc", "-color-diagnostics",
]

try ProcessRunner(builderArguments, terminal).waitUntilFinished()
try ProcessRunner(builderArguments, loadingMessage: "Compiling...", terminal)
.waitUntilFinished()

guard localFileSystem.exists(mainWasmPath) else {
terminal.write(
Expand Down
Loading