Skip to content

Commit e23f8eb

Browse files
authored
Merge pull request swiftlang#274 from allevato/rainbow
Add support for color diagnostics.
2 parents dd1fa22 + 4f90887 commit e23f8eb

File tree

5 files changed

+152
-48
lines changed

5 files changed

+152
-48
lines changed

Sources/swift-format/Frontend/Frontend.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ class Frontend {
5353
}
5454
}
5555

56+
/// Prints diagnostics to standard error, optionally with color.
57+
final let diagnosticPrinter: StderrDiagnosticPrinter
58+
5659
/// The diagnostic engine to which warnings and errors will be emitted.
57-
final let diagnosticsEngine =
58-
UnifiedDiagnosticsEngine(diagnosticsHandlers: [printDiagnosticToStderr])
60+
final let diagnosticsEngine: UnifiedDiagnosticsEngine
5961

6062
/// Options that apply during formatting or linting.
6163
final let lintFormatOptions: LintFormatOptions
@@ -77,6 +79,11 @@ class Frontend {
7779
/// - Parameter lintFormatOptions: Options that apply during formatting or linting.
7880
init(lintFormatOptions: LintFormatOptions) {
7981
self.lintFormatOptions = lintFormatOptions
82+
83+
self.diagnosticPrinter = StderrDiagnosticPrinter(
84+
colorMode: lintFormatOptions.colorDiagnostics.map { $0 ? .on : .off } ?? .auto)
85+
self.diagnosticsEngine =
86+
UnifiedDiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic])
8087
}
8188

8289
/// Runs the linter or formatter over the inputs.

Sources/swift-format/Subcommands/LintFormatOptions.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ struct LintFormatOptions: ParsableArguments {
5555
help: "Process files in parallel, simultaneously across multiple cores.")
5656
var parallel: Bool = false
5757

58+
/// Whether colors should be used in diagnostics printed to standard error.
59+
///
60+
/// If nil, color usage will be automatically detected based on whether standard error is
61+
/// connected to a terminal or not.
62+
@Flag(
63+
inversion: .prefixedNo,
64+
help: """
65+
Enables or disables color diagnostics when printing to standard error. The default behavior \
66+
if this flag is omitted is to use colors if standard error is connected to a terminal, and \
67+
to not use colors otherwise.
68+
""")
69+
var colorDiagnostics: Bool?
70+
5871
/// The list of paths to Swift source files that should be formatted or linted.
5972
@Argument(help: "Zero or more input filenames.")
6073
var paths: [String] = []

Sources/swift-format/Utilities/DiagnosticPrinting.swift

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Dispatch
14+
import Foundation
15+
import TSCBasic
16+
17+
/// Manages printing of diagnostics to standard error.
18+
final class StderrDiagnosticPrinter {
19+
/// Determines how colors are used in printed diagnostics.
20+
enum ColorMode {
21+
/// Colors are used if stderr is detected to be connected to a TTY; otherwise, colors will not
22+
/// be used (for example, if stderr is redirected to a file).
23+
case auto
24+
25+
/// Colors will not be used.
26+
case off
27+
28+
/// Colors will always be used.
29+
case on
30+
}
31+
32+
/// Definitions of the ANSI "Select Graphic Rendition" sequences used in diagnostics.
33+
private enum ANSISGR: String {
34+
case boldRed = "1;31"
35+
case boldYellow = "1;33"
36+
case boldMagenta = "1;35"
37+
case boldWhite = "1;37"
38+
case boldGray = "1;90"
39+
case reset = "0"
40+
}
41+
42+
/// The queue used to synchronize printing uninterrupted diagnostic messages.
43+
private let printQueue = DispatchQueue(label: "com.apple.swift-format.StderrDiagnosticPrinter")
44+
45+
/// Indicates whether colors should be used when printing diagnostics.
46+
private let useColors: Bool
47+
48+
/// Creates a new standard error diagnostic printer with the given color mode.
49+
init(colorMode: ColorMode) {
50+
switch colorMode {
51+
case .auto:
52+
if let stream = stderrStream.stream as? LocalFileOutputByteStream {
53+
useColors = TerminalController.isTTY(stream)
54+
} else {
55+
useColors = false
56+
}
57+
case .off:
58+
useColors = false
59+
case .on:
60+
useColors = true
61+
}
62+
}
63+
64+
/// Prints a diagnostic to standard error.
65+
func printDiagnostic(_ diagnostic: TSCBasic.Diagnostic) {
66+
printQueue.sync {
67+
let stderr = FileHandle.standardError
68+
69+
stderr.write("\(ansiSGR(.boldWhite))\(diagnostic.location): ")
70+
71+
switch diagnostic.behavior {
72+
case .error: stderr.write("\(ansiSGR(.boldRed))error: ")
73+
case .warning: stderr.write("\(ansiSGR(.boldMagenta))warning: ")
74+
case .note: stderr.write("\(ansiSGR(.boldGray))note: ")
75+
case .remark, .ignored: break
76+
}
77+
78+
let data = diagnostic.data as! UnifiedDiagnosticData
79+
if let category = data.category {
80+
stderr.write("\(ansiSGR(.boldYellow))[\(category)] ")
81+
}
82+
stderr.write("\(ansiSGR(.boldWhite))\(data.message)\(ansiSGR(.reset))\n")
83+
}
84+
}
85+
86+
/// Returns the complete ANSI sequence used to enable the given SGR if colors are enabled in the
87+
/// printer, or the empty string if colors are not enabled.
88+
private func ansiSGR(_ ansiSGR: ANSISGR) -> String {
89+
guard useColors else { return "" }
90+
return "\u{001b}[\(ansiSGR.rawValue)m"
91+
}
92+
}

Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,30 @@ import SwiftSyntax
1515
import SwiftSyntaxParser
1616
import TSCBasic
1717

18+
/// Diagnostic data that retains the separation of a finding category (if present) from the rest of
19+
/// the message, allowing diagnostic printers that want to print those values separately to do so.
20+
struct UnifiedDiagnosticData: DiagnosticData {
21+
/// The category of the diagnostic, if any.
22+
var category: String?
23+
24+
/// The message text associated with the diagnostic.
25+
var message: String
26+
27+
var description: String {
28+
if let category = category {
29+
return "[\(category)] \(message)"
30+
} else {
31+
return message
32+
}
33+
}
34+
35+
/// Creates a new unified diagnostic with the given optional category and message.
36+
init(category: String? = nil, message: String) {
37+
self.category = category
38+
self.message = message
39+
}
40+
}
41+
1842
/// Unifies the handling of findings from the linter, parsing errors from the syntax parser, and
1943
/// generic errors from the frontend so that they are treated uniformly by the underlying
2044
/// diagnostics engine from the `swift-tools-support-core` package.
@@ -67,7 +91,9 @@ final class UnifiedDiagnosticsEngine {
6791
/// - location: The location in the source code associated with the error, or nil if there is no
6892
/// location associated with the error.
6993
func emitError(_ message: String, location: SourceLocation? = nil) {
70-
diagnosticsEngine.emit(.error(message), location: location.map(UnifiedLocation.parserLocation))
94+
diagnosticsEngine.emit(
95+
.error(UnifiedDiagnosticData(message: message)),
96+
location: location.map(UnifiedLocation.parserLocation))
7197
}
7298

7399
/// Emits a finding from the linter and any of its associated notes as diagnostics.
@@ -80,7 +106,7 @@ final class UnifiedDiagnosticsEngine {
80106

81107
for note in finding.notes {
82108
diagnosticsEngine.emit(
83-
.note("\(note.message)"),
109+
.note(UnifiedDiagnosticData(message: "\(note.message)")),
84110
location: note.location.map(UnifiedLocation.findingLocation))
85111
}
86112
}
@@ -95,7 +121,7 @@ final class UnifiedDiagnosticsEngine {
95121

96122
for note in diagnostic.notes {
97123
diagnosticsEngine.emit(
98-
.note(note.message.text),
124+
.note(UnifiedDiagnosticData(message: note.message.text)),
99125
location: note.location.map(UnifiedLocation.parserLocation))
100126
}
101127
}
@@ -105,21 +131,24 @@ final class UnifiedDiagnosticsEngine {
105131
private func diagnosticMessage(for message: SwiftSyntaxParser.Diagnostic.Message)
106132
-> TSCBasic.Diagnostic.Message
107133
{
134+
let data = UnifiedDiagnosticData(category: nil, message: message.text)
135+
108136
switch message.severity {
109-
case .error: return .error(message.text)
110-
case .warning: return .warning(message.text)
111-
case .note: return .note(message.text)
137+
case .error: return .error(data)
138+
case .warning: return .warning(data)
139+
case .note: return .note(data)
112140
}
113141
}
114142

115143
/// Converts a lint finding into a diagnostic message that can be used by the `TSCBasic`
116144
/// diagnostics engine and returns it.
117145
private func diagnosticMessage(for finding: Finding) -> TSCBasic.Diagnostic.Message {
118-
let message = "[\(finding.category)] \(finding.message.text)"
146+
let data =
147+
UnifiedDiagnosticData(category: "\(finding.category)", message: "\(finding.message.text)")
119148

120149
switch finding.severity {
121-
case .error: return .error(message)
122-
case .warning: return .warning(message)
150+
case .error: return .error(data)
151+
case .warning: return .warning(data)
123152
}
124153
}
125154
}

0 commit comments

Comments
 (0)