Skip to content

Add support for color diagnostics. #274

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 1 commit into from
Nov 13, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 9 additions & 2 deletions Sources/swift-format/Frontend/Frontend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ class Frontend {
}
}

/// Prints diagnostics to standard error, optionally with color.
final let diagnosticPrinter: StderrDiagnosticPrinter

/// The diagnostic engine to which warnings and errors will be emitted.
final let diagnosticsEngine =
UnifiedDiagnosticsEngine(diagnosticsHandlers: [printDiagnosticToStderr])
final let diagnosticsEngine: UnifiedDiagnosticsEngine

/// Options that apply during formatting or linting.
final let lintFormatOptions: LintFormatOptions
Expand All @@ -77,6 +79,11 @@ class Frontend {
/// - Parameter lintFormatOptions: Options that apply during formatting or linting.
init(lintFormatOptions: LintFormatOptions) {
self.lintFormatOptions = lintFormatOptions

self.diagnosticPrinter = StderrDiagnosticPrinter(
colorMode: lintFormatOptions.colorDiagnostics.map { $0 ? .on : .off } ?? .auto)
self.diagnosticsEngine =
UnifiedDiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic])
}

/// Runs the linter or formatter over the inputs.
Expand Down
13 changes: 13 additions & 0 deletions Sources/swift-format/Subcommands/LintFormatOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ struct LintFormatOptions: ParsableArguments {
help: "Process files in parallel, simultaneously across multiple cores.")
var parallel: Bool = false

/// Whether colors should be used in diagnostics printed to standard error.
///
/// If nil, color usage will be automatically detected based on whether standard error is
/// connected to a terminal or not.
@Flag(
inversion: .prefixedNo,
help: """
Enables or disables color diagnostics when printing to standard error. The default behavior \
if this flag is omitted is to use colors if standard error is connected to a terminal, and \
to not use colors otherwise.
""")
var colorDiagnostics: Bool?

/// The list of paths to Swift source files that should be formatted or linted.
@Argument(help: "Zero or more input filenames.")
var paths: [String] = []
Expand Down
37 changes: 0 additions & 37 deletions Sources/swift-format/Utilities/DiagnosticPrinting.swift

This file was deleted.

92 changes: 92 additions & 0 deletions Sources/swift-format/Utilities/StderrDiagnosticPrinter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Dispatch
import Foundation
import TSCBasic

/// Manages printing of diagnostics to standard error.
final class StderrDiagnosticPrinter {
/// Determines how colors are used in printed diagnostics.
enum ColorMode {
/// Colors are used if stderr is detected to be connected to a TTY; otherwise, colors will not
/// be used (for example, if stderr is redirected to a file).
case auto

/// Colors will not be used.
case off

/// Colors will always be used.
case on
}

/// Definitions of the ANSI "Select Graphic Rendition" sequences used in diagnostics.
private enum ANSISGR: String {
case boldRed = "1;31"
case boldYellow = "1;33"
case boldMagenta = "1;35"
case boldWhite = "1;37"
case boldGray = "1;90"
case reset = "0"
}

/// The queue used to synchronize printing uninterrupted diagnostic messages.
private let printQueue = DispatchQueue(label: "com.apple.swift-format.StderrDiagnosticPrinter")

/// Indicates whether colors should be used when printing diagnostics.
private let useColors: Bool

/// Creates a new standard error diagnostic printer with the given color mode.
init(colorMode: ColorMode) {
switch colorMode {
case .auto:
if let stream = stderrStream.stream as? LocalFileOutputByteStream {
useColors = TerminalController.isTTY(stream)
} else {
useColors = false
}
case .off:
useColors = false
case .on:
useColors = true
}
}

/// Prints a diagnostic to standard error.
func printDiagnostic(_ diagnostic: TSCBasic.Diagnostic) {
printQueue.sync {
let stderr = FileHandle.standardError

stderr.write("\(ansiSGR(.boldWhite))\(diagnostic.location): ")

switch diagnostic.behavior {
case .error: stderr.write("\(ansiSGR(.boldRed))error: ")
case .warning: stderr.write("\(ansiSGR(.boldMagenta))warning: ")
case .note: stderr.write("\(ansiSGR(.boldGray))note: ")
case .remark, .ignored: break
}

let data = diagnostic.data as! UnifiedDiagnosticData
if let category = data.category {
stderr.write("\(ansiSGR(.boldYellow))[\(category)] ")
}
stderr.write("\(ansiSGR(.boldWhite))\(data.message)\(ansiSGR(.reset))\n")
}
}

/// Returns the complete ANSI sequence used to enable the given SGR if colors are enabled in the
/// printer, or the empty string if colors are not enabled.
private func ansiSGR(_ ansiSGR: ANSISGR) -> String {
guard useColors else { return "" }
return "\u{001b}[\(ansiSGR.rawValue)m"
}
}
47 changes: 38 additions & 9 deletions Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ import SwiftSyntax
import SwiftSyntaxParser
import TSCBasic

/// Diagnostic data that retains the separation of a finding category (if present) from the rest of
/// the message, allowing diagnostic printers that want to print those values separately to do so.
struct UnifiedDiagnosticData: DiagnosticData {
/// The category of the diagnostic, if any.
var category: String?

/// The message text associated with the diagnostic.
var message: String

var description: String {
if let category = category {
return "[\(category)] \(message)"
} else {
return message
}
}

/// Creates a new unified diagnostic with the given optional category and message.
init(category: String? = nil, message: String) {
self.category = category
self.message = message
}
}

/// Unifies the handling of findings from the linter, parsing errors from the syntax parser, and
/// generic errors from the frontend so that they are treated uniformly by the underlying
/// diagnostics engine from the `swift-tools-support-core` package.
Expand Down Expand Up @@ -67,7 +91,9 @@ final class UnifiedDiagnosticsEngine {
/// - location: The location in the source code associated with the error, or nil if there is no
/// location associated with the error.
func emitError(_ message: String, location: SourceLocation? = nil) {
diagnosticsEngine.emit(.error(message), location: location.map(UnifiedLocation.parserLocation))
diagnosticsEngine.emit(
.error(UnifiedDiagnosticData(message: message)),
location: location.map(UnifiedLocation.parserLocation))
}

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

for note in finding.notes {
diagnosticsEngine.emit(
.note("\(note.message)"),
.note(UnifiedDiagnosticData(message: "\(note.message)")),
location: note.location.map(UnifiedLocation.findingLocation))
}
}
Expand All @@ -95,7 +121,7 @@ final class UnifiedDiagnosticsEngine {

for note in diagnostic.notes {
diagnosticsEngine.emit(
.note(note.message.text),
.note(UnifiedDiagnosticData(message: note.message.text)),
location: note.location.map(UnifiedLocation.parserLocation))
}
}
Expand All @@ -105,21 +131,24 @@ final class UnifiedDiagnosticsEngine {
private func diagnosticMessage(for message: SwiftSyntaxParser.Diagnostic.Message)
-> TSCBasic.Diagnostic.Message
{
let data = UnifiedDiagnosticData(category: nil, message: message.text)

switch message.severity {
case .error: return .error(message.text)
case .warning: return .warning(message.text)
case .note: return .note(message.text)
case .error: return .error(data)
case .warning: return .warning(data)
case .note: return .note(data)
}
}

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

switch finding.severity {
case .error: return .error(message)
case .warning: return .warning(message)
case .error: return .error(data)
case .warning: return .warning(data)
}
}
}