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 `--pretty-baseline` flag (and matching `pretty_baseline`
configuration key) that writes baseline files as pretty-printed JSON
with sorted keys, producing stable, reviewable diffs when the baseline
is regenerated. Default behaviour is unchanged so existing baselines
remain byte-identical.
[swizzlr](https://github.com/swizzlr)

* 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, the baseline produced by write_baseline is pretty-printed with
# sorted keys, producing stable diffs when regenerated. Defaults to false.
pretty_baseline: false

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

Expand Down
13 changes: 11 additions & 2 deletions Source/SwiftLintCore/Models/Baseline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,17 @@ public struct Baseline: Equatable {
/// Writes a `Baseline` to disk in JSON format.
///
/// - parameter toPath: The path to write to.
public func write(toPath path: String) throws {
let data = try JSONEncoder().encode(sortedBaselineViolations)
/// - parameter pretty: If `true`, output is pretty-printed with sorted keys
/// and unescaped slashes, producing stable, reviewable
/// diffs. Defaults to `false` for backwards
/// compatibility with baselines written by older
/// versions of SwiftLint.
public func write(toPath path: String, pretty: Bool = false) throws {
let encoder = JSONEncoder()
if pretty {
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
}
let data = try encoder.encode(sortedBaselineViolations)
try data.write(to: URL(fileURLWithPath: path))
}

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,
prettyBaseline: childConfiguration.prettyBaseline,
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 prettyBaseline = "pretty_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,
prettyBaseline: dict[Key.prettyBaseline.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?

/// Pretty-print the baseline output with sorted keys.
public let prettyBaseline: 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?,
prettyBaseline: 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.prettyBaseline = prettyBaseline
self.checkForUpdates = checkForUpdates
}

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

Expand Down Expand Up @@ -170,6 +176,7 @@ public struct Configuration {
lenient: Bool = false,
baseline: String? = nil,
writeBaseline: String? = nil,
prettyBaseline: 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,
prettyBaseline: prettyBaseline,
checkForUpdates: checkForUpdates
)
}
Expand Down Expand Up @@ -316,6 +324,7 @@ extension Configuration: Hashable {
hasher.combine(lenient)
hasher.combine(baseline)
hasher.combine(writeBaseline)
hasher.combine(prettyBaseline)
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.prettyBaseline == rhs.prettyBaseline &&
lhs.checkForUpdates == rhs.checkForUpdates &&
lhs.rulesMode == rhs.rulesMode
}
Expand Down
6 changes: 5 additions & 1 deletion 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 prettyBaseline: 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?,
prettyBaseline: 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.prettyBaseline = prettyBaseline
self.workingDirectory = workingDirectory
self.quiet = quiet
self.output = output
Expand Down Expand Up @@ -148,7 +151,8 @@ package struct LintOrAnalyzeCommand {
let builder = LintOrAnalyzeResultBuilder(options)
let files = try await collectViolations(builder: builder)
if let baselineOutputPath = options.writeBaseline ?? builder.configuration.writeBaseline {
try Baseline(violations: builder.unfilteredViolations).write(toPath: baselineOutputPath)
let pretty = options.prettyBaseline || builder.configuration.prettyBaseline
try Baseline(violations: builder.unfilteredViolations).write(toPath: baselineOutputPath, pretty: pretty)
}
let numberOfSeriousViolations = try Signposts.record(name: "LintOrAnalyzeCommand.PostProcessViolations") {
try postProcessViolations(files: files, builder: builder)
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,
prettyBaseline: common.prettyBaseline,
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,
prettyBaseline: common.prettyBaseline,
workingDirectory: common.workingDirectory,
quiet: quiet,
output: common.output,
Expand Down
11 changes: 11 additions & 0 deletions Source/swiftlint/Common/LintOrAnalyzeArguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ struct LintOrAnalyzeArguments: ParsableArguments {
var baseline: String?
@Option(help: "The path to save detected violations to as a new baseline.")
var writeBaseline: String?
@Flag(help: """
Pretty-print the baseline output with sorted keys and unescaped slashes, \
producing stable, reviewable diffs. Only applies when writing a baseline.
""")
var prettyBaseline = 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 All @@ -62,6 +67,12 @@ struct LintOrAnalyzeArguments: ParsableArguments {
"""
)
var onlyRule: [String] = []

func validate() throws {
if prettyBaseline, writeBaseline == nil {
throw ValidationError("'--pretty-baseline' requires '--write-baseline <path>'.")
}
}
}

// MARK: - Common Argument Help
Expand Down
95 changes: 95 additions & 0 deletions Tests/FileSystemAccessTests/BaselineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ final class BaselineTests: XCTestCase {
ruleDescriptions.violations(for: filePath)
}

/// Violations across two synthetic files, intentionally returned in
/// reverse order to prove the baseline serializer sorts by file and
/// location before writing.
private static func twoFileViolations(for filePath: String) -> [StyleViolation] {
let other = "other" + filePath.bridge().lastPathComponent
return [
StyleViolation(
ruleDescription: BlockBasedKVORule.description,
location: Location(file: other, line: 4, character: 1)
),
StyleViolation(
ruleDescription: ArrayInitRule.description,
location: Location(file: filePath, line: 2, character: 1)
),
]
}

private static func baseline(for filePath: String) -> Baseline {
Baseline(violations: ruleDescriptions.violations(for: filePath))
}
Expand Down Expand Up @@ -83,6 +100,84 @@ final class BaselineTests: XCTestCase {
}
}

func testBaselineFileIsCompactByDefault() throws {
// The default output must stay backwards-compatible: a single minified
// line (no newlines, no indentation, no blank lines). A byte-level
// snapshot isn't viable because `JSONEncoder`'s compact output has
// non-deterministic key order — that's one of the reasons the opt-in
// pretty mode uses `.sortedKeys`. This test asserts the format
// properties; `testBaselineFileIsPrettyPrintedWhenRequested` asserts
// the exact serialized output for the opt-in mode.
try withExampleFileCreated { sourceFilePath in
let baselinePath = temporaryDirectoryPath.stringByAppendingPathComponent(UUID().uuidString)
try Baseline(violations: Self.twoFileViolations(for: sourceFilePath))
.write(toPath: baselinePath)
defer { try? FileManager.default.removeItem(atPath: baselinePath) }
let contents = try String(contentsOf: URL(fileURLWithPath: baselinePath), encoding: .utf8)
let fileName = sourceFilePath.bridge().lastPathComponent

XCTAssertFalse(contents.contains("\n"), "Default baseline output must be a single line")
XCTAssertFalse(contents.contains(" "), "Default baseline output must not be indented")

// Structurally equivalent to the pretty output, minus formatting.
let decoded = try JSONSerialization.jsonObject(with: Data(contents.utf8)) as? [[String: Any]]
let files = decoded?.compactMap { ($0["violation"] as? [String: Any])?["location"] as? [String: Any] }
.compactMap { $0["file"] as? String }
XCTAssertEqual(files, [fileName, "other\(fileName)"])
}
}

func testBaselineFileIsPrettyPrintedWhenRequested() throws {
try withExampleFileCreated { sourceFilePath in
let baselinePath = temporaryDirectoryPath.stringByAppendingPathComponent(UUID().uuidString)
try Baseline(violations: Self.twoFileViolations(for: sourceFilePath))
.write(toPath: baselinePath, pretty: true)
defer { try? FileManager.default.removeItem(atPath: baselinePath) }
let contents = try String(contentsOf: URL(fileURLWithPath: baselinePath), encoding: .utf8)
let fileName = sourceFilePath.bridge().lastPathComponent

// Pretty output: sorted keys, two-space indent, sorted by file and
// then by location, unescaped slashes.
// swiftlint:disable line_length
let expected = """
[
{
"text" : "import SwiftLintFramework",
"violation" : {
"location" : {
"character" : 1,
"file" : "\(fileName)",
"line" : 2
},
"reason" : "Prefer using `Array(seq)` over `seq.map { $0 }` to convert a sequence into an Array",
"ruleDescription" : "Prefer using `Array(seq)` over `seq.map { $0 }` to convert a sequence into an Array",
"ruleIdentifier" : "array_init",
"ruleName" : "Array Init",
"severity" : "warning"
}
},
{
"text" : "",
"violation" : {
"location" : {
"character" : 1,
"file" : "other\(fileName)",
"line" : 4
},
"reason" : "Prefer the new block based KVO API with keypaths when using Swift 3.2 or later",
"ruleDescription" : "Prefer the new block based KVO API with keypaths when using Swift 3.2 or later",
"ruleIdentifier" : "block_based_kvo",
"ruleName" : "Block Based KVO",
"severity" : "warning"
}
}
]
"""
// swiftlint:enable line_length
XCTAssertEqual(contents, expected)
}
}

func testUnchangedViolations() throws {
try withExampleFileCreated { sourceFilePath in
XCTAssertEqual(Self.baseline(for: sourceFilePath).filter(Self.violations(for: sourceFilePath)), [])
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,
prettyBaseline: false,
workingDirectory: nil,
quiet: false,
output: nil,
Expand Down