From 991208e3334aa70daea40e98f58417602f14e74e Mon Sep 17 00:00:00 2001 From: Omar Yusuf Abdi Date: Sat, 14 Mar 2026 18:04:34 +0100 Subject: [PATCH 1/3] feat: add --fail-on-unfixable flag for single-pass fix and validation (#6450) --- .../LintOrAnalyzeCommand.swift | 54 ++++++++++++++++++- Source/swiftlint/Commands/Analyze.swift | 3 +- Source/swiftlint/Commands/Lint.swift | 5 +- .../Common/LintOrAnalyzeArguments.swift | 2 + 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift index 6444adc84e..06fca24696 100644 --- a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift +++ b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift @@ -58,6 +58,7 @@ package struct LintOrAnalyzeOptions { let compilerLogPath: String? let compileCommands: String? let checkForUpdates: Bool + let failOnUnfixable: Bool package init(mode: LintOrAnalyzeMode, paths: [String], @@ -86,7 +87,8 @@ package struct LintOrAnalyzeOptions { disableSourceKit: Bool, compilerLogPath: String?, compileCommands: String?, - checkForUpdates: Bool) { + checkForUpdates: Bool, + failOnUnfixable: Bool = false) { self.mode = mode self.paths = paths self.useSTDIN = useSTDIN @@ -115,6 +117,42 @@ package struct LintOrAnalyzeOptions { self.compilerLogPath = compilerLogPath self.compileCommands = compileCommands self.checkForUpdates = checkForUpdates + self.failOnUnfixable = failOnUnfixable + } + + /// Returns a copy of these options with `autocorrect` set to `false` and `failOnUnfixable` set to `false`, + /// suitable for a re-lint pass after autocorrection. + func asLintOnly() -> LintOrAnalyzeOptions { + LintOrAnalyzeOptions( + mode: mode, + paths: paths, + useSTDIN: useSTDIN, + configurationFiles: configurationFiles, + strict: strict, + lenient: lenient, + forceExclude: forceExclude, + useExcludingByPrefix: useExcludingByPrefix, + useScriptInputFiles: useScriptInputFiles, + useScriptInputFileLists: useScriptInputFileLists, + benchmark: benchmark, + reporter: reporter, + baseline: baseline, + writeBaseline: writeBaseline, + workingDirectory: nil, // Already changed in run() + quiet: quiet, + output: output, + progress: progress, + cachePath: cachePath, + ignoreCache: true, // Must ignore cache since files were just modified + enableAllRules: enableAllRules, + onlyRule: onlyRule, + autocorrect: false, + format: false, + disableSourceKit: disableSourceKit, + compilerLogPath: compilerLogPath, + compileCommands: compileCommands, + checkForUpdates: false // Already checked in autocorrect pass + ) } var verb: String { @@ -140,7 +178,19 @@ package struct LintOrAnalyzeCommand { } } try await Signposts.record(name: "LintOrAnalyzeCommand.run") { - try await options.autocorrect ? autocorrect(options) : lintOrAnalyze(options) + if options.autocorrect { + try await autocorrect(options) + if options.failOnUnfixable { + try await lintOrAnalyze(options.asLintOnly()) + } + } else { + if options.failOnUnfixable { + queuedPrintError( + "warning: The option --fail-on-unfixable has no effect without --fix." + ) + } + try await lintOrAnalyze(options) + } } } diff --git a/Source/swiftlint/Commands/Analyze.swift b/Source/swiftlint/Commands/Analyze.swift index 968fb99e27..fb79b60f1c 100644 --- a/Source/swiftlint/Commands/Analyze.swift +++ b/Source/swiftlint/Commands/Analyze.swift @@ -47,7 +47,8 @@ extension SwiftLint { disableSourceKit: false, compilerLogPath: compilerLogPath, compileCommands: compileCommands, - checkForUpdates: common.checkForUpdates + checkForUpdates: common.checkForUpdates, + failOnUnfixable: common.failOnUnfixable ) try await LintOrAnalyzeCommand.run(options) diff --git a/Source/swiftlint/Commands/Lint.swift b/Source/swiftlint/Commands/Lint.swift index 56c8cca192..475a529448 100644 --- a/Source/swiftlint/Commands/Lint.swift +++ b/Source/swiftlint/Commands/Lint.swift @@ -30,7 +30,7 @@ extension SwiftLint { func run() async throws { Issue.printDeprecationWarnings = !silenceDeprecationWarnings - if common.fix, let leniency = common.leniency { + if common.fix, !common.failOnUnfixable, let leniency = common.leniency { Issue.genericWarning("The option --\(leniency) has no effect together with --fix.").print() } @@ -64,7 +64,8 @@ extension SwiftLint { disableSourceKit: disableSourceKit, compilerLogPath: nil, compileCommands: nil, - checkForUpdates: common.checkForUpdates + checkForUpdates: common.checkForUpdates, + failOnUnfixable: common.failOnUnfixable ) try await LintOrAnalyzeCommand.run(options) } diff --git a/Source/swiftlint/Common/LintOrAnalyzeArguments.swift b/Source/swiftlint/Common/LintOrAnalyzeArguments.swift index b47e4162a6..bfdc8249af 100644 --- a/Source/swiftlint/Common/LintOrAnalyzeArguments.swift +++ b/Source/swiftlint/Common/LintOrAnalyzeArguments.swift @@ -62,6 +62,8 @@ struct LintOrAnalyzeArguments: ParsableArguments { """ ) var onlyRule: [String] = [] + @Flag(help: "Fail with a non-zero exit code if there are remaining violations after fix.") + var failOnUnfixable = false } // MARK: - Common Argument Help From 5cccd67ea6c49ecb48e4171f86aaaac3c4637e0b Mon Sep 17 00:00:00 2001 From: Omar Yusuf Abdi Date: Sat, 14 Mar 2026 19:08:20 +0100 Subject: [PATCH 2/3] docs: add CHANGELOG entry for --fail-on-unfixable flag --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52cafe5b3e..e0f3f789b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,11 @@ ### Enhancements -* Print fixed code read from stdin to stdout. +* Add `--fail-on-unfixable` flag for single-pass fix and validation. + [omar-y-abdi](https://github.com/omar-y-abdi) + [#6450](https://github.com/realm/SwiftLint/issues/6450) + +* Print fixed code read from stdin to stdout. [SimplyDanny](https://github.com/SimplyDanny) [#6501](https://github.com/realm/SwiftLint/issues/6501) From a731c87648ad58a26f774f814338e30d464b0915 Mon Sep 17 00:00:00 2001 From: Omar Yusuf Abdi Date: Sat, 14 Mar 2026 19:42:36 +0100 Subject: [PATCH 3/3] style: use Self instead of LintOrAnalyzeOptions in asLintOnly() Fixes prefer_self_in_static_references violations caught by SwiftLint's integration tests. --- Source/SwiftLintFramework/LintOrAnalyzeCommand.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift index 06fca24696..062fb3b7fc 100644 --- a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift +++ b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift @@ -122,8 +122,8 @@ package struct LintOrAnalyzeOptions { /// Returns a copy of these options with `autocorrect` set to `false` and `failOnUnfixable` set to `false`, /// suitable for a re-lint pass after autocorrection. - func asLintOnly() -> LintOrAnalyzeOptions { - LintOrAnalyzeOptions( + func asLintOnly() -> Self { + Self( mode: mode, paths: paths, useSTDIN: useSTDIN,