Skip to content

Commit 4f90887

Browse files
committed
Add support for color diagnostics.
By default, the diagnostic printer will detect whether stderr is connected to a TTY and use ANSI color sequences if so. We use the same colors that `swiftc` uses for its diagnostics: white for the main text, red for error labels, magenta for warning labels, and gray for note labels. Additionally, we use yellow to emphasize the finding category for diagnostics that come from linter findings. This behavior can be controlled manually using the `--color-diagnostics/--no-color-diagnostics` flag pair.
1 parent 5add2e0 commit 4f90887

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)