From f3e5cc55435b9ba5b5aad6e24ab003d896ba7788 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Mon, 10 Apr 2023 16:16:18 -0700 Subject: [PATCH] Remove the `swift-tools-support-core` dependency from swift-format. This was only being used for the diagnostics engine, which is easy enough to roll by hand and it's one less dependency to keep in sync with the rest of the toolchain. In the future, we may want an output mode that uses the new prettier diagnostic printer from swift-syntax. --- Package.swift | 6 - .../Frontend/FormatFrontend.swift | 3 +- Sources/swift-format/Frontend/Frontend.swift | 4 +- .../swift-format/Utilities/Diagnostic.swift | 80 ++++++++++ .../Utilities/DiagnosticsEngine.swift | 126 +++++++++++++++ .../Utilities/StderrDiagnosticPrinter.swift | 28 ++-- Sources/swift-format/Utilities/TTY.swift | 29 ++++ .../Utilities/UnifiedDiagnosticsEngine.swift | 151 ------------------ 8 files changed, 254 insertions(+), 173 deletions(-) create mode 100644 Sources/swift-format/Utilities/Diagnostic.swift create mode 100644 Sources/swift-format/Utilities/DiagnosticsEngine.swift create mode 100644 Sources/swift-format/Utilities/TTY.swift delete mode 100644 Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift diff --git a/Package.swift b/Package.swift index 99e0630ef..68d0d41e9 100644 --- a/Package.swift +++ b/Package.swift @@ -143,7 +143,6 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftParser", package: "swift-syntax"), - .product(name: "TSCBasic", package: "swift-tools-support-core"), ] ), @@ -223,15 +222,10 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { url: "https://github.com/apple/swift-syntax.git", branch: "main" ), - .package( - url: "https://github.com/apple/swift-tools-support-core.git", - exact: Version("0.4.0") - ), ] } else { package.dependencies += [ .package(path: "../swift-argument-parser"), .package(path: "../swift-syntax"), - .package(path: "../swift-tools-support-core"), ] } diff --git a/Sources/swift-format/Frontend/FormatFrontend.swift b/Sources/swift-format/Frontend/FormatFrontend.swift index 996b1a924..aac36856b 100644 --- a/Sources/swift-format/Frontend/FormatFrontend.swift +++ b/Sources/swift-format/Frontend/FormatFrontend.swift @@ -40,7 +40,8 @@ class FormatFrontend: Frontend { return } - let diagnosticHandler: (Diagnostic, SourceLocation) -> () = { (diagnostic, location) in + let diagnosticHandler: (SwiftDiagnostics.Diagnostic, SourceLocation) -> () = { + (diagnostic, location) in guard !self.lintFormatOptions.ignoreUnparsableFiles else { // No diagnostics should be emitted in this mode. return diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index fa9611fac..0c8c4a50f 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -57,7 +57,7 @@ class Frontend { final let diagnosticPrinter: StderrDiagnosticPrinter /// The diagnostic engine to which warnings and errors will be emitted. - final let diagnosticsEngine: UnifiedDiagnosticsEngine + final let diagnosticsEngine: DiagnosticsEngine /// Options that apply during formatting or linting. final let lintFormatOptions: LintFormatOptions @@ -83,7 +83,7 @@ class Frontend { self.diagnosticPrinter = StderrDiagnosticPrinter( colorMode: lintFormatOptions.colorDiagnostics.map { $0 ? .on : .off } ?? .auto) self.diagnosticsEngine = - UnifiedDiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic]) + DiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic]) } /// Runs the linter or formatter over the inputs. diff --git a/Sources/swift-format/Utilities/Diagnostic.swift b/Sources/swift-format/Utilities/Diagnostic.swift new file mode 100644 index 000000000..3a80333a9 --- /dev/null +++ b/Sources/swift-format/Utilities/Diagnostic.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftFormatCore +import SwiftSyntax + +/// 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 Diagnostic { + /// The severity of the diagnostic. + enum Severity { + case note + case warning + case error + } + + /// Represents the location of a diagnostic. + struct Location { + /// The file path associated with the diagnostic. + var file: String + + /// The 1-based line number where the diagnostic occurred. + var line: Int + + /// The 1-based column number where the diagnostic occurred. + var column: Int + + /// Creates a new diagnostic location from the given source location. + init(_ sourceLocation: SourceLocation) { + self.file = sourceLocation.file! + self.line = sourceLocation.line! + self.column = sourceLocation.column! + } + + /// Creates a new diagnostic location with the given finding location. + init(_ findingLocation: Finding.Location) { + self.file = findingLocation.file + self.line = findingLocation.line + self.column = findingLocation.column + } + } + + /// The severity of the diagnostic. + var severity: Severity + + /// The location where the diagnostic occurred, if known. + var location: Location? + + /// 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 diagnostic with the given severity, location, optional category, and + /// message. + init(severity: Severity, location: Location?, category: String? = nil, message: String) { + self.severity = severity + self.location = location + self.category = category + self.message = message + } +} diff --git a/Sources/swift-format/Utilities/DiagnosticsEngine.swift b/Sources/swift-format/Utilities/DiagnosticsEngine.swift new file mode 100644 index 000000000..52e0b8909 --- /dev/null +++ b/Sources/swift-format/Utilities/DiagnosticsEngine.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftFormatCore +import SwiftSyntax +import SwiftDiagnostics + +/// Unifies the handling of findings from the linter, parsing errors from the syntax parser, and +/// generic errors from the frontend so that they are emitted in a uniform fashion. +final class DiagnosticsEngine { + /// The handler functions that will be called to process diagnostics that are emitted. + private let handlers: [(Diagnostic) -> Void] + + /// A Boolean value indicating whether any errors were emitted by the diagnostics engine. + private(set) var hasErrors: Bool + + /// A Boolean value indicating whether any warnings were emitted by the diagnostics engine. + private(set) var hasWarnings: Bool + + /// Creates a new diagnostics engine with the given diagnostic handlers. + /// + /// - Parameter diagnosticsHandlers: An array of functions, each of which takes a `Diagnostic` as + /// its sole argument and returns `Void`. The functions are called whenever a diagnostic is + /// received by the engine. + init(diagnosticsHandlers: [(Diagnostic) -> Void]) { + self.handlers = diagnosticsHandlers + self.hasErrors = false + self.hasWarnings = false + } + + /// Emits the diagnostic by passing it to the registered handlers, and tracks whether it was an + /// error or warning diagnostic. + private func emit(_ diagnostic: Diagnostic) { + switch diagnostic.severity { + case .error: self.hasErrors = true + case .warning: self.hasWarnings = true + default: break + } + + for handler in handlers { + handler(diagnostic) + } + } + + /// Emits a generic error message. + /// + /// - Parameters: + /// - message: The message associated with the error. + /// - 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) { + emit( + Diagnostic( + severity: .error, + location: location.map(Diagnostic.Location.init), + message: message)) + } + + /// Emits a finding from the linter and any of its associated notes as diagnostics. + /// + /// - Parameter finding: The finding that should be emitted. + func consumeFinding(_ finding: Finding) { + emit(diagnosticMessage(for: finding)) + + for note in finding.notes { + emit( + Diagnostic( + severity: .note, + location: note.location.map(Diagnostic.Location.init), + message: "\(note.message)")) + } + } + + /// Emits a diagnostic from the syntax parser and any of its associated notes. + /// + /// - Parameter diagnostic: The syntax parser diagnostic that should be emitted. + func consumeParserDiagnostic( + _ diagnostic: SwiftDiagnostics.Diagnostic, + _ location: SourceLocation + ) { + emit(diagnosticMessage(for: diagnostic.diagMessage, at: location)) + } + + /// Converts a diagnostic message from the syntax parser into a diagnostic message that can be + /// used by the `TSCBasic` diagnostics engine and returns it. + private func diagnosticMessage( + for message: SwiftDiagnostics.DiagnosticMessage, + at location: SourceLocation + ) -> Diagnostic { + let severity: Diagnostic.Severity + switch message.severity { + case .error: severity = .error + case .warning: severity = .warning + case .note: severity = .note + } + return Diagnostic( + severity: severity, + location: Diagnostic.Location(location), + category: nil, + message: message.message) + } + + /// 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) -> Diagnostic { + let severity: Diagnostic.Severity + switch finding.severity { + case .error: severity = .error + case .warning: severity = .warning + } + return Diagnostic( + severity: severity, + location: finding.location.map(Diagnostic.Location.init), + category: "\(finding.category)", + message: "\(finding.message.text)") + } +} diff --git a/Sources/swift-format/Utilities/StderrDiagnosticPrinter.swift b/Sources/swift-format/Utilities/StderrDiagnosticPrinter.swift index f6452be82..f7730f00c 100644 --- a/Sources/swift-format/Utilities/StderrDiagnosticPrinter.swift +++ b/Sources/swift-format/Utilities/StderrDiagnosticPrinter.swift @@ -12,7 +12,6 @@ import Dispatch import Foundation -import TSCBasic /// Manages printing of diagnostics to standard error. final class StderrDiagnosticPrinter { @@ -49,11 +48,7 @@ final class StderrDiagnosticPrinter { init(colorMode: ColorMode) { switch colorMode { case .auto: - if let stream = stderrStream.stream as? LocalFileOutputByteStream { - useColors = TerminalController.isTTY(stream) - } else { - useColors = false - } + useColors = isTTY(FileHandle.standardError) case .off: useColors = false case .on: @@ -62,25 +57,32 @@ final class StderrDiagnosticPrinter { } /// Prints a diagnostic to standard error. - func printDiagnostic(_ diagnostic: TSCBasic.Diagnostic) { + func printDiagnostic(_ diagnostic: Diagnostic) { printQueue.sync { let stderr = FileHandleTextOutputStream(FileHandle.standardError) - stderr.write("\(ansiSGR(.boldWhite))\(diagnostic.location): ") + stderr.write("\(ansiSGR(.boldWhite))\(description(of: diagnostic.location)): ") - switch diagnostic.behavior { + switch diagnostic.severity { 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 { + if let category = diagnostic.category { stderr.write("\(ansiSGR(.boldYellow))[\(category)] ") } - stderr.write("\(ansiSGR(.boldWhite))\(data.message)\(ansiSGR(.reset))\n") + stderr.write("\(ansiSGR(.boldWhite))\(diagnostic.message)\(ansiSGR(.reset))\n") + } + } + + /// Returns a string representation of the given diagnostic location, or a fallback string if the + /// location was not known. + private func description(of location: Diagnostic.Location?) -> String { + if let location = location { + return "\(location.file):\(location.line):\(location.column)" } + return "" } /// Returns the complete ANSI sequence used to enable the given SGR if colors are enabled in the diff --git a/Sources/swift-format/Utilities/TTY.swift b/Sources/swift-format/Utilities/TTY.swift new file mode 100644 index 000000000..35fc35841 --- /dev/null +++ b/Sources/swift-format/Utilities/TTY.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 Foundation + +/// Returns a value indicating whether or not the stream is a TTY. +func isTTY(_ fileHandle: FileHandle) -> Bool { + // The implementation of this function is adapted from `TerminalController.swift` in + // swift-tools-support-core. + #if os(Windows) + // The TSC implementation of this function only returns `.file` or `.dumb` for Windows, + // neither of which is a TTY. + return false + #else + if ProcessInfo.processInfo.environment["TERM"] == "dumb" { + return false + } + return isatty(fileHandle.fileDescriptor) != 0 + #endif +} diff --git a/Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift b/Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift deleted file mode 100644 index de6963f58..000000000 --- a/Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift +++ /dev/null @@ -1,151 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 SwiftFormatCore -import SwiftSyntax -import SwiftDiagnostics -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. -final class UnifiedDiagnosticsEngine { - /// Represents a location from either the linter or the syntax parser and supports converting it - /// to a string representation for printing. - private enum UnifiedLocation: DiagnosticLocation { - /// A location received from the swift parser. - case parserLocation(SourceLocation) - - /// A location received from the linter. - case findingLocation(Finding.Location) - - var description: String { - switch self { - case .parserLocation(let location): - // SwiftSyntax's old diagnostic printer also force-unwrapped these, so we assume that they - // will always be present if the location itself is non-nil. - return "\(location.file!):\(location.line!):\(location.column!)" - case .findingLocation(let location): - return "\(location.file):\(location.line):\(location.column)" - } - } - } - - /// The underlying diagnostics engine. - private let diagnosticsEngine: DiagnosticsEngine - - /// A Boolean value indicating whether any errors were emitted by the diagnostics engine. - var hasErrors: Bool { diagnosticsEngine.hasErrors } - - /// A Boolean value indicating whether any warnings were emitted by the diagnostics engine. - var hasWarnings: Bool { - diagnosticsEngine.diagnostics.contains { $0.behavior == .warning } - } - - /// Creates a new unified diagnostics engine with the given diagnostic handlers. - /// - /// - Parameter diagnosticsHandlers: An array of functions, each of which takes a `Diagnostic` as - /// its sole argument and returns `Void`. The functions are called whenever a diagnostic is - /// received by the engine. - init(diagnosticsHandlers: [DiagnosticsEngine.DiagnosticsHandler]) { - self.diagnosticsEngine = DiagnosticsEngine(handlers: diagnosticsHandlers) - } - - /// Emits a generic error message. - /// - /// - Parameters: - /// - message: The message associated with the error. - /// - 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(UnifiedDiagnosticData(message: message)), - location: location.map(UnifiedLocation.parserLocation)) - } - - /// Emits a finding from the linter and any of its associated notes as diagnostics. - /// - /// - Parameter finding: The finding that should be emitted. - func consumeFinding(_ finding: Finding) { - diagnosticsEngine.emit( - diagnosticMessage(for: finding), - location: finding.location.map(UnifiedLocation.findingLocation)) - - for note in finding.notes { - diagnosticsEngine.emit( - .note(UnifiedDiagnosticData(message: "\(note.message)")), - location: note.location.map(UnifiedLocation.findingLocation)) - } - } - - /// Emits a diagnostic from the syntax parser and any of its associated notes. - /// - /// - Parameter diagnostic: The syntax parser diagnostic that should be emitted. - func consumeParserDiagnostic( - _ diagnostic: SwiftDiagnostics.Diagnostic, - _ location: SourceLocation - ) { - diagnosticsEngine.emit( - diagnosticMessage(for: diagnostic.diagMessage), - location: UnifiedLocation.parserLocation(location)) - } - - /// Converts a diagnostic message from the syntax parser into a diagnostic message that can be - /// used by the `TSCBasic` diagnostics engine and returns it. - private func diagnosticMessage(for message: SwiftDiagnostics.DiagnosticMessage) - -> TSCBasic.Diagnostic.Message - { - let data = UnifiedDiagnosticData(category: nil, message: message.message) - - switch message.severity { - 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 data = - UnifiedDiagnosticData(category: "\(finding.category)", message: "\(finding.message.text)") - - switch finding.severity { - case .error: return .error(data) - case .warning: return .warning(data) - } - } -}