diff --git a/Source/SwiftLintFramework/Baseline/Baseline.swift b/Source/SwiftLintFramework/Baseline/Baseline.swift new file mode 100644 index 000000000..6b300b214 --- /dev/null +++ b/Source/SwiftLintFramework/Baseline/Baseline.swift @@ -0,0 +1,58 @@ +import Foundation + +public class Baseline { + private let baselinePath: String + private var baselineViolations = [BaselineViolation]() + + public init(baselinePath: String) { + self.baselinePath = baselinePath + } + + public func isInBaseline(violation: StyleViolation) -> Bool { + let baselineViolation = BaselineViolation( + ruleIdentifier: violation.ruleIdentifier, + location: violation.location.description, + reason: violation.reason + ) + let contains = baselineViolations.contains(baselineViolation) + return contains + } + + public func saveBaseline(violations: [StyleViolation]) { + let fileContent = violations.map(generateForSingleViolation).joined(separator: "\n") + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: baselinePath) { + fileManager.createFile(atPath: baselinePath, contents: fileContent.data(using: .utf8)) + } + } + + public func readBaseline() { + let fileManager = FileManager.default + guard let fileContent = fileManager.contents(atPath: baselinePath), + let stringContent = String(data: fileContent, encoding: .utf8) else { + return + } + stringContent.enumerateLines { [weak self] line, _ in + guard let self = self else { return } + let violation = self.parseLine(line: line) + self.baselineViolations.append(violation) + } + } + + private func parseLine(line: String) -> BaselineViolation { + let components = line.components(separatedBy: ";") + let location = components[0] + let reason = components[1] + let ruleIdentifier = components[2] + + return BaselineViolation( + ruleIdentifier: ruleIdentifier, + location: location, + reason: reason + ) + } + + private func generateForSingleViolation(_ violation: StyleViolation) -> String { + return "\(violation.location);\(violation.reason);\(violation.ruleIdentifier)" + } +} diff --git a/Source/SwiftLintFramework/Baseline/BaselineViolation.swift b/Source/SwiftLintFramework/Baseline/BaselineViolation.swift new file mode 100644 index 000000000..c6a65ec51 --- /dev/null +++ b/Source/SwiftLintFramework/Baseline/BaselineViolation.swift @@ -0,0 +1,13 @@ +import Foundation + +struct BaselineViolation: Equatable { + let ruleIdentifier: String + let location: String + let reason: String + + static func == (lhs: BaselineViolation, rhs: BaselineViolation) -> Bool { + return lhs.ruleIdentifier == rhs.ruleIdentifier && + lhs.location == rhs.location && + lhs.reason == rhs.reason + } +} diff --git a/Source/swiftlint/Commands/LintCommand.swift b/Source/swiftlint/Commands/LintCommand.swift index 98c4218e6..feb00abf4 100644 --- a/Source/swiftlint/Commands/LintCommand.swift +++ b/Source/swiftlint/Commands/LintCommand.swift @@ -24,19 +24,20 @@ struct LintOptions: OptionsProtocol { let cachePath: String let ignoreCache: Bool let enableAllRules: Bool + let useBaseline: Bool // swiftlint:disable line_length - static func create(_ path: String) -> (_ useSTDIN: Bool) -> (_ configurationFile: String) -> (_ strict: Bool) -> (_ lenient: Bool) -> (_ forceExclude: Bool) -> (_ excludeByPrefix: Bool) -> (_ useScriptInputFiles: Bool) -> (_ benchmark: Bool) -> (_ reporter: String) -> (_ quiet: Bool) -> (_ cachePath: String) -> (_ ignoreCache: Bool) -> (_ enableAllRules: Bool) -> (_ paths: [String]) -> LintOptions { - return { useSTDIN in { configurationFile in { strict in { lenient in { forceExclude in { excludeByPrefix in { useScriptInputFiles in { benchmark in { reporter in { quiet in { cachePath in { ignoreCache in { enableAllRules in { paths in + static func create(_ path: String) -> (_ useSTDIN: Bool) -> (_ configurationFile: String) -> (_ strict: Bool) -> (_ lenient: Bool) -> (_ forceExclude: Bool) -> (_ excludeByPrefix: Bool) -> (_ useScriptInputFiles: Bool) -> (_ benchmark: Bool) -> (_ reporter: String) -> (_ quiet: Bool) -> (_ cachePath: String) -> (_ ignoreCache: Bool) -> (_ enableAllRules: Bool) -> (_ useBaseline: Bool) -> (_ paths: [String]) -> LintOptions { + return { useSTDIN in { configurationFile in { strict in { lenient in { forceExclude in { excludeByPrefix in { useScriptInputFiles in { benchmark in { reporter in { quiet in { cachePath in { ignoreCache in { enableAllRules in { useBaseline in { paths in let allPaths: [String] if !path.isEmpty { allPaths = [path] } else { allPaths = paths } - return self.init(paths: allPaths, useSTDIN: useSTDIN, configurationFile: configurationFile, strict: strict, lenient: lenient, forceExclude: forceExclude, excludeByPrefix: excludeByPrefix, useScriptInputFiles: useScriptInputFiles, benchmark: benchmark, reporter: reporter, quiet: quiet, cachePath: cachePath, ignoreCache: ignoreCache, enableAllRules: enableAllRules) + return self.init(paths: allPaths, useSTDIN: useSTDIN, configurationFile: configurationFile, strict: strict, lenient: lenient, forceExclude: forceExclude, excludeByPrefix: excludeByPrefix, useScriptInputFiles: useScriptInputFiles, benchmark: benchmark, reporter: reporter, quiet: quiet, cachePath: cachePath, ignoreCache: ignoreCache, enableAllRules: enableAllRules, useBaseline: useBaseline) // swiftlint:enable line_length - }}}}}}}}}}}}}} + }}}}}}}}}}}}}}} } static func evaluate(_ mode: CommandMode) -> Result>> { @@ -65,6 +66,9 @@ struct LintOptions: OptionsProtocol { usage: "ignore cache when linting") <*> mode <| Option(key: "enable-all-rules", defaultValue: false, usage: "run all rules, even opt-in and disabled ones, ignoring `only_rules`") + <*> mode <| Option(key: "useBaseline", defaultValue: false, + usage: "save a list of violations if there is no baseline file, and then use the list " + + "as a baseline to ignore old issues and only report new ones") // This should go last to avoid eating other args <*> mode <| pathsArgument(action: "lint") } diff --git a/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift b/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift index 2907abb34..b93d22154 100644 --- a/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift +++ b/Source/swiftlint/Helpers/LintOrAnalyzeCommand.swift @@ -17,6 +17,7 @@ enum LintOrAnalyzeMode { } struct LintOrAnalyzeCommand { + // swiftlint:disable:next function_body_length static func run(_ options: LintOrAnalyzeOptions) -> Result<(), CommandantError<()>> { var fileBenchmark = Benchmark(name: "files") var ruleBenchmark = Benchmark(name: "rules") @@ -26,12 +27,20 @@ struct LintOrAnalyzeCommand { let reporter = reporterFrom(optionsReporter: options.reporter, configuration: configuration) let cache = options.ignoreCache ? nil : LinterCache(configuration: configuration) let visitorMutationQueue = DispatchQueue(label: "io.realm.swiftlint.lintVisitorMutation") + let rootPath = options.paths.first?.absolutePathStandardized() ?? "" + let baseline = Baseline(baselinePath: rootPath) + if options.useBaseline { + baseline.readBaseline() + } return configuration.visitLintableFiles(options: options, cache: cache, storage: storage) { linter in - let currentViolations: [StyleViolation] + var currentViolations: [StyleViolation] if options.benchmark { let start = Date() let (violationsBeforeLeniency, currentRuleTimes) = linter.styleViolationsAndRuleTimes(using: storage) currentViolations = applyLeniency(options: options, violations: violationsBeforeLeniency) + if options.useBaseline { + currentViolations = filteredViolations(baseline: baseline, currentViolations: currentViolations) + } visitorMutationQueue.sync { fileBenchmark.record(file: linter.file, from: start) currentRuleTimes.forEach { ruleBenchmark.record(id: $0, time: $1) } @@ -39,6 +48,9 @@ struct LintOrAnalyzeCommand { } } else { currentViolations = applyLeniency(options: options, violations: linter.styleViolations(using: storage)) + if options.useBaseline { + currentViolations = filteredViolations(baseline: baseline, currentViolations: currentViolations) + } visitorMutationQueue.sync { violations += currentViolations } @@ -46,6 +58,9 @@ struct LintOrAnalyzeCommand { linter.file.invalidateCache() reporter.report(violations: currentViolations, realtimeCondition: true) }.flatMap { files in + if options.useBaseline { + baseline.saveBaseline(violations: violations) + } if isWarningThresholdBroken(configuration: configuration, violations: violations) && !options.lenient { violations.append(createThresholdViolation(threshold: configuration.warningThreshold!)) @@ -67,6 +82,17 @@ struct LintOrAnalyzeCommand { } } + private static func filteredViolations(baseline: Baseline, + currentViolations: [StyleViolation]) -> [StyleViolation] { + var filteredViolations = [StyleViolation]() + for violation in currentViolations { + if !baseline.isInBaseline(violation: violation) { + filteredViolations.append(violation) + } + } + return filteredViolations + } + private static func printStatus(violations: [StyleViolation], files: [SwiftLintFile], serious: Int, verb: String) { let pluralSuffix = { (collection: [Any]) -> String in return collection.count != 1 ? "s" : "" @@ -143,6 +169,7 @@ struct LintOrAnalyzeOptions { let cachePath: String let ignoreCache: Bool let enableAllRules: Bool + let useBaseline: Bool let autocorrect: Bool let compilerLogPath: String let compileCommands: String @@ -163,6 +190,7 @@ struct LintOrAnalyzeOptions { cachePath = options.cachePath ignoreCache = options.ignoreCache enableAllRules = options.enableAllRules + useBaseline = options.useBaseline autocorrect = false compilerLogPath = "" compileCommands = "" @@ -184,6 +212,7 @@ struct LintOrAnalyzeOptions { cachePath = "" ignoreCache = true enableAllRules = options.enableAllRules + useBaseline = false autocorrect = options.autocorrect compilerLogPath = options.compilerLogPath compileCommands = options.compileCommands