Skip to content
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@

### Enhancements

* Add `--strict-baseline` command-line flag (and `strict_baseline` configuration
key) that fails linting when violations recorded in the baseline are no longer
detected, encouraging users to regenerate the baseline once baselined
violations have been fixed.
[swizzlr](https://github.com/swizzlr)
[#6511](https://github.com/realm/SwiftLint/issues/6511)

* Print fixed code read from stdin to stdout.
[SimplyDanny](https://github.com/SimplyDanny)
[#6501](https://github.com/realm/SwiftLint/issues/6501)
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,10 @@ baseline: Baseline.json
# The path to save detected violations to as a new baseline.
write_baseline: Baseline.json

# If true, SwiftLint will fail when violations recorded in the baseline are no
# longer detected, encouraging the baseline to be regenerated after fixes.
strict_baseline: false

# If true, SwiftLint will check for updates after linting or analyzing.
check_for_updates: true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ extension Configuration {
lenient: childConfiguration.lenient,
baseline: childConfiguration.baseline,
writeBaseline: childConfiguration.writeBaseline,
strictBaseline: childConfiguration.strictBaseline,
checkForUpdates: childConfiguration.checkForUpdates
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ extension Configuration {
case lenient = "lenient"
case baseline = "baseline"
case writeBaseline = "write_baseline"
case strictBaseline = "strict_baseline"
case checkForUpdates = "check_for_updates"
case childConfig = "child_config"
case parentConfig = "parent_config"
Expand Down Expand Up @@ -108,6 +109,7 @@ extension Configuration {
lenient: dict[Key.lenient.rawValue] as? Bool ?? false,
baseline: dict[Key.baseline.rawValue] as? String,
writeBaseline: dict[Key.writeBaseline.rawValue] as? String,
strictBaseline: dict[Key.strictBaseline.rawValue] as? Bool ?? false,
checkForUpdates: dict[Key.checkForUpdates.rawValue] as? Bool ?? false
)
}
Expand Down
10 changes: 10 additions & 0 deletions Source/SwiftLintFramework/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public struct Configuration {
/// The path to write a baseline to.
public let writeBaseline: String?

/// Fail linting when violations recorded in the baseline are no longer detected.
public let strictBaseline: Bool

/// Check for updates.
public let checkForUpdates: Bool

Expand Down Expand Up @@ -89,6 +92,7 @@ public struct Configuration {
lenient: Bool,
baseline: String?,
writeBaseline: String?,
strictBaseline: Bool,
checkForUpdates: Bool
) {
self.rulesWrapper = rulesWrapper
Expand All @@ -104,6 +108,7 @@ public struct Configuration {
self.lenient = lenient
self.baseline = baseline
self.writeBaseline = writeBaseline
self.strictBaseline = strictBaseline
self.checkForUpdates = checkForUpdates
}

Expand All @@ -125,6 +130,7 @@ public struct Configuration {
lenient = configuration.lenient
baseline = configuration.baseline
writeBaseline = configuration.writeBaseline
strictBaseline = configuration.strictBaseline
checkForUpdates = configuration.checkForUpdates
}

Expand Down Expand Up @@ -170,6 +176,7 @@ public struct Configuration {
lenient: Bool = false,
baseline: String? = nil,
writeBaseline: String? = nil,
strictBaseline: Bool = false,
checkForUpdates: Bool = false
) {
if let pinnedVersion, pinnedVersion != Version.current.value {
Expand Down Expand Up @@ -200,6 +207,7 @@ public struct Configuration {
lenient: lenient,
baseline: baseline,
writeBaseline: writeBaseline,
strictBaseline: strictBaseline,
checkForUpdates: checkForUpdates
)
}
Expand Down Expand Up @@ -316,6 +324,7 @@ extension Configuration: Hashable {
hasher.combine(lenient)
hasher.combine(baseline)
hasher.combine(writeBaseline)
hasher.combine(strictBaseline)
hasher.combine(checkForUpdates)
hasher.combine(basedOnCustomConfigurationFiles)
hasher.combine(cachePath)
Expand All @@ -338,6 +347,7 @@ extension Configuration: Hashable {
lhs.lenient == rhs.lenient &&
lhs.baseline == rhs.baseline &&
lhs.writeBaseline == rhs.writeBaseline &&
lhs.strictBaseline == rhs.strictBaseline &&
lhs.checkForUpdates == rhs.checkForUpdates &&
lhs.rulesMode == rhs.rulesMode
}
Expand Down
40 changes: 40 additions & 0 deletions Source/SwiftLintFramework/LintOrAnalyzeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ package struct LintOrAnalyzeOptions {
let reporter: String?
let baseline: String?
let writeBaseline: String?
let strictBaseline: Bool
let workingDirectory: String?
let quiet: Bool
let output: String?
Expand Down Expand Up @@ -73,6 +74,7 @@ package struct LintOrAnalyzeOptions {
reporter: String?,
baseline: String?,
writeBaseline: String?,
strictBaseline: Bool,
workingDirectory: String?,
quiet: Bool,
output: String?,
Expand Down Expand Up @@ -101,6 +103,7 @@ package struct LintOrAnalyzeOptions {
self.reporter = reporter
self.baseline = baseline
self.writeBaseline = writeBaseline
self.strictBaseline = strictBaseline
self.workingDirectory = workingDirectory
self.quiet = quiet
self.output = output
Expand Down Expand Up @@ -150,6 +153,7 @@ package struct LintOrAnalyzeCommand {
if let baselineOutputPath = options.writeBaseline ?? builder.configuration.writeBaseline {
try Baseline(violations: builder.unfilteredViolations).write(toPath: baselineOutputPath)
}
appendFixedBaselineViolations(builder: builder)
let numberOfSeriousViolations = try Signposts.record(name: "LintOrAnalyzeCommand.PostProcessViolations") {
try postProcessViolations(files: files, builder: builder)
}
Expand All @@ -165,6 +169,7 @@ package struct LintOrAnalyzeCommand {
let options = builder.options
let visitorMutationQueue = DispatchQueue(label: "io.realm.swiftlint.lintVisitorMutation")
let baseline = try baseline(options, builder.configuration)
builder.baseline = baseline
return try await builder.configuration.visitLintableFiles(options: options, cache: builder.cache,
storage: builder.storage) { linter in
let currentViolations: [StyleViolation]
Expand Down Expand Up @@ -234,6 +239,22 @@ package struct LintOrAnalyzeCommand {
return numberOfSeriousViolations
}

private static func appendFixedBaselineViolations(builder: LintOrAnalyzeResultBuilder) {
let options = builder.options
let configuration = builder.configuration
guard options.strictBaseline || configuration.strictBaseline,
let baseline = builder.baseline else {
return
}
let currentBaseline = Baseline(violations: builder.unfilteredViolations)
let fixedViolations = currentBaseline.compare(baseline).map(createFixedBaselineViolation)
guard fixedViolations.isNotEmpty else {
return
}
builder.violations += fixedViolations
builder.report(violations: fixedViolations, realtimeCondition: true)
}

private static func baseline(_ options: LintOrAnalyzeOptions, _ configuration: Configuration) throws -> Baseline? {
if let baselinePath = options.baseline ?? configuration.baseline {
do {
Expand Down Expand Up @@ -266,6 +287,24 @@ package struct LintOrAnalyzeCommand {
return numberOfWarningViolations >= warningThreshold
}

private static func createFixedBaselineViolation(fromBaseline violation: StyleViolation) -> StyleViolation {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So reporting these as violations is very convenient, but the fixed_baseline_violation "pseudo-rule" is not really a rule - for example, I can't // swiftlint:disable fixed_baseline_violation at the top of a file (and fixed_baseline_violation isn't easily matched up to strict_baseline).

I wonder if these should be reported as Issue's instead, something like

Issue.fixedBaselineViolation(violation).print()

let description = RuleDescription(
identifier: "fixed_baseline_violation",
name: "Fixed Baseline",
description: "A violation recorded in the baseline has been fixed or is no longer detected",
kind: .lint
)
return StyleViolation(
ruleDescription: description,
severity: .error,
location: violation.location,
reason: """
Violation previously recorded in the baseline for '\(violation.ruleIdentifier)' is no longer \
detected. Regenerate the baseline to acknowledge this fix
"""
)
Comment on lines +297 to +305
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimplyDanny is there a more idomatic way to say "baseline is no longer valid"?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseline is "stale" perhaps, but that still doesn't feel quite right.

}

private static func createThresholdViolation(threshold: Int) -> StyleViolation {
let description = RuleDescription(
identifier: "warning_threshold",
Expand Down Expand Up @@ -374,6 +413,7 @@ private class LintOrAnalyzeResultBuilder {
var unfilteredViolations = [StyleViolation]()
/// The violations to be reported, possibly filtered by a baseline, plus any threshold violations.
var violations = [StyleViolation]()
var baseline: Baseline?
let storage = RuleStorage()
let configuration: Configuration
let reporter: any Reporter.Type
Expand Down
1 change: 1 addition & 0 deletions Source/swiftlint/Commands/Analyze.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension SwiftLint {
reporter: common.reporter,
baseline: common.baseline,
writeBaseline: common.writeBaseline,
strictBaseline: common.strictBaseline,
workingDirectory: common.workingDirectory,
quiet: quiet,
output: common.output,
Expand Down
1 change: 1 addition & 0 deletions Source/swiftlint/Commands/Lint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ extension SwiftLint {
reporter: common.reporter,
baseline: common.baseline,
writeBaseline: common.writeBaseline,
strictBaseline: common.strictBaseline,
workingDirectory: common.workingDirectory,
quiet: quiet,
output: common.output,
Expand Down
6 changes: 6 additions & 0 deletions Source/swiftlint/Common/LintOrAnalyzeArguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ struct LintOrAnalyzeArguments: ParsableArguments {
var baseline: String?
@Option(help: "The path to save detected violations to as a new baseline.")
var writeBaseline: String?
@Flag(help: """
Fail linting if violations previously recorded in the baseline are no longer \
detected. Encourages keeping the baseline up-to-date by regenerating it \
once baselined violations have been fixed.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little verbose. Fail linting when violations recorded in the baseline are no longer detected. might enough here (lifted from the strictBaseline property doc comment).

""")
var strictBaseline = false
@Option(help: "The working directory to use when running SwiftLint.")
var workingDirectory: String?
@Option(help: "The file where violations should be saved. Prints to stdout by default.")
Expand Down
14 changes: 14 additions & 0 deletions Tests/FileSystemAccessTests/BaselineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ final class BaselineTests: XCTestCase {
}
}

func testCompareDetectsFixedViolations() throws {
try withExampleFileCreated { sourceFilePath in
let originalViolations = Self.violations(for: sourceFilePath)
let oldBaseline = Baseline(violations: originalViolations)
// Simulate some baselined violations being fixed.
let remainingViolations = Array(originalViolations.dropFirst(2))
let newBaseline = Baseline(violations: remainingViolations)
// `newBaseline.compare(oldBaseline)` yields violations in the old
// baseline that are no longer detected — i.e., fixed violations.
let fixed = newBaseline.compare(oldBaseline)
XCTAssertEqual(fixed.count, 2)
}
}

func testCompare() throws {
try withExampleFileCreated { sourceFilePath in
let ruleDescriptions = Self.ruleDescriptions + Self.ruleDescriptions
Expand Down
1 change: 1 addition & 0 deletions Tests/FrameworkTests/LintOrAnalyzeOptionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ private extension LintOrAnalyzeOptions {
reporter: nil,
baseline: nil,
writeBaseline: nil,
strictBaseline: false,
workingDirectory: nil,
quiet: false,
output: nil,
Expand Down