diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ed688681..098f24b248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ accordingly._ [JP Simard](https://github.com/jpsim) [#2495](https://github.com/realm/SwiftLint/issues/2495) +* Add `--output` option to lint and analyze commands to write to a file instead + of to stdout. + [JP Simard](https://github.com/jpsim) + [#4048](https://github.com/realm/SwiftLint/issues/4048) + #### Bug Fixes * Migrate `empty_xctest_method` rule to SwiftSyntax fixing some false positives. @@ -36,6 +41,10 @@ accordingly._ [Martin Hosna](https://github.com/mhosna) [#4142](https://github.com/realm/SwiftLint/issues/4142) +* Consistently print error/info messages to stderr instead of stdout, + which wasn't being done for errors regarding remote configurations. + [JP Simard](https://github.com/jpsim) + ## 0.49.0: Asynchronous Defuzzer _Note: The default branch for the SwiftLint git repository will be renamed from diff --git a/Source/SwiftLintFramework/Extensions/Configuration+Remote.swift b/Source/SwiftLintFramework/Extensions/Configuration+Remote.swift index 647d20e583..972c5228d8 100644 --- a/Source/SwiftLintFramework/Extensions/Configuration+Remote.swift +++ b/Source/SwiftLintFramework/Extensions/Configuration+Remote.swift @@ -119,7 +119,7 @@ internal extension Configuration.FileGraph.FilePath { private mutating func handleMissingNetwork(urlString: String, cachedFilePath: String?) throws -> String { if let cachedFilePath = cachedFilePath { - queuedPrint( + queuedPrintError( "warning: No internet connectivity: Unable to load remote config from \"\(urlString)\". " + "Using cached version as a fallback." ) @@ -141,11 +141,11 @@ internal extension Configuration.FileGraph.FilePath { ) throws -> String { if let cachedFilePath = cachedFilePath { if taskDone { - queuedPrint( + queuedPrintError( "warning: Unable to load remote config from \"\(urlString)\". Using cached version as a fallback." ) } else { - queuedPrint( + queuedPrintError( "warning: Timeout (\(timeout) sec): Unable to load remote config from \"\(urlString)\". " + "Using cached version as a fallback." ) @@ -170,7 +170,7 @@ internal extension Configuration.FileGraph.FilePath { private mutating func handleFileWriteFailure(urlString: String, cachedFilePath: String?) throws -> String { if let cachedFilePath = cachedFilePath { - queuedPrint("Unable to cache remote config from \"\(urlString)\". Using cached version as a fallback.") + queuedPrintError("Unable to cache remote config from \"\(urlString)\". Using cached version as a fallback.") self = .existing(path: cachedFilePath) return cachedFilePath } else { diff --git a/Source/swiftlint/Commands/Analyze.swift b/Source/swiftlint/Commands/Analyze.swift index e5b24570d7..5e4bef090d 100644 --- a/Source/swiftlint/Commands/Analyze.swift +++ b/Source/swiftlint/Commands/Analyze.swift @@ -43,6 +43,7 @@ extension SwiftLint { benchmark: common.benchmark, reporter: common.reporter, quiet: quiet, + output: common.output, cachePath: nil, ignoreCache: true, enableAllRules: false, diff --git a/Source/swiftlint/Commands/Lint.swift b/Source/swiftlint/Commands/Lint.swift index 27e4ee0051..4b52790449 100644 --- a/Source/swiftlint/Commands/Lint.swift +++ b/Source/swiftlint/Commands/Lint.swift @@ -47,6 +47,7 @@ extension SwiftLint { benchmark: common.benchmark, reporter: common.reporter, quiet: quiet, + output: common.output, cachePath: cachePath, ignoreCache: noCache, enableAllRules: enableAllRules, diff --git a/Source/swiftlint/Extensions/Reporter+CommandLine.swift b/Source/swiftlint/Extensions/Reporter+CommandLine.swift deleted file mode 100644 index f63908cba7..0000000000 --- a/Source/swiftlint/Extensions/Reporter+CommandLine.swift +++ /dev/null @@ -1,16 +0,0 @@ -import SwiftLintFramework - -extension Reporter { - static func report(violations: [StyleViolation], realtimeCondition: Bool) { - if isRealtime == realtimeCondition { - let report = generateReport(violations) - if !report.isEmpty { - queuedPrint(report) - } - } - } -} - -func reporterFrom(optionsReporter: String?, configuration: Configuration) -> Reporter.Type { - return reporterFrom(identifier: optionsReporter ?? configuration.reporter) -} diff --git a/Source/swiftlint/Helpers/LintOrAnalyzeArguments.swift b/Source/swiftlint/Helpers/LintOrAnalyzeArguments.swift index c1055788c8..22960009ff 100644 --- a/Source/swiftlint/Helpers/LintOrAnalyzeArguments.swift +++ b/Source/swiftlint/Helpers/LintOrAnalyzeArguments.swift @@ -39,6 +39,8 @@ struct LintOrAnalyzeArguments: ParsableArguments { var reporter: String? @Flag(help: "Use the in-process version of SourceKit.") var inProcessSourcekit = false + @Option(help: "The file where violations should be saved. Prints to stdout by default.") + var output: String? } // MARK: - Common Argument Help diff --git a/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift b/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift index cec5c1e989..3a01644640 100644 --- a/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift +++ b/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift @@ -77,7 +77,7 @@ struct LintOrAnalyzeCommand { } } linter.file.invalidateCache() - builder.reporter.report(violations: currentViolations, realtimeCondition: true) + builder.report(violations: currentViolations, realtimeCondition: true) } } @@ -89,9 +89,9 @@ struct LintOrAnalyzeCommand { builder.violations.append( createThresholdViolation(threshold: configuration.warningThreshold!) ) - builder.reporter.report(violations: [builder.violations.last!], realtimeCondition: true) + builder.report(violations: [builder.violations.last!], realtimeCondition: true) } - builder.reporter.report(violations: builder.violations, realtimeCondition: false) + builder.report(violations: builder.violations, realtimeCondition: false) let numberOfSeriousViolations = builder.violations.filter({ $0.severity == .error }).count if !options.quiet { printStatus(violations: builder.violations, files: files, serious: numberOfSeriousViolations, @@ -183,8 +183,8 @@ struct LintOrAnalyzeCommand { let corrections = linter.correct(using: storage) if !corrections.isEmpty && !options.quiet && !options.useSTDIN { - let correctionLogs = corrections.map({ $0.consoleDescription }) - queuedPrint(correctionLogs.joined(separator: "\n")) + let correctionLogs = corrections.map(\.consoleDescription) + options.writeToOutput(correctionLogs.joined(separator: "\n")) } } @@ -210,6 +210,7 @@ struct LintOrAnalyzeOptions { let benchmark: Bool let reporter: String? let quiet: Bool + let output: String? let cachePath: String? let ignoreCache: Bool let enableAllRules: Bool @@ -243,8 +244,44 @@ private class LintOrAnalyzeResultBuilder { Configuration(options: options) } configuration = config - reporter = reporterFrom(optionsReporter: options.reporter, configuration: config) + reporter = reporterFrom(identifier: options.reporter ?? config.reporter) cache = options.ignoreCache ? nil : LinterCache(configuration: config) self.options = options + + if let outFile = options.output { + do { + try Data().write(to: URL(fileURLWithPath: outFile)) + } catch { + queuedPrintError("Could not write to file at path \(outFile)") + } + } + } + + func report(violations: [StyleViolation], realtimeCondition: Bool) { + if reporter.isRealtime == realtimeCondition { + let report = reporter.generateReport(violations) + if !report.isEmpty { + options.writeToOutput(report) + } + } + } +} + +private extension LintOrAnalyzeOptions { + func writeToOutput(_ string: String) { + guard let outFile = output else { + queuedPrint(string) + return + } + + do { + let outFileURL = URL(fileURLWithPath: outFile) + let fileUpdater = try FileHandle(forUpdating: outFileURL) + fileUpdater.seekToEndOfFile() + fileUpdater.write(Data((string + "\n").utf8)) + fileUpdater.closeFile() + } catch { + queuedPrintError("Could not write to file at path \(outFile)") + } } }