mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
206 lines
7.7 KiB
Swift
206 lines
7.7 KiB
Swift
import Foundation
|
|
|
|
private typealias BaselineViolations = [BaselineViolation]
|
|
private typealias ViolationsPerFile = [String: BaselineViolations]
|
|
private typealias ViolationsPerRule = [String: BaselineViolations]
|
|
|
|
private struct BaselineViolation: Codable, Hashable, Comparable {
|
|
let violation: StyleViolation
|
|
let text: String
|
|
var key: String { text + violation.reason }
|
|
|
|
init(violation: StyleViolation, text: String) {
|
|
let location = violation.location
|
|
self.violation = violation.with(location: Location(
|
|
// Within the baseline, we use relative paths, so that
|
|
// comparisons are independent of the absolute path
|
|
file: location.relativeFile,
|
|
line: location.line,
|
|
character: location.character)
|
|
)
|
|
self.text = text
|
|
}
|
|
|
|
static func == (lhs: Self, rhs: Self) -> Bool {
|
|
lhs.violation == rhs.violation && lhs.text == rhs.text
|
|
}
|
|
|
|
static func < (lhs: Self, rhs: Self) -> Bool {
|
|
lhs.violation.location == rhs.violation.location
|
|
? lhs.violation.ruleIdentifier < rhs.violation.ruleIdentifier
|
|
: lhs.violation.location < rhs.violation.location
|
|
}
|
|
}
|
|
|
|
/// A set of violations that can be used to filter newly detected violations.
|
|
public struct Baseline: Equatable {
|
|
private let baseline: ViolationsPerFile
|
|
private var sortedBaselineViolations: BaselineViolations {
|
|
baseline.flatMap(\.value).sorted()
|
|
}
|
|
|
|
/// The stored violations.
|
|
public var violations: [StyleViolation] {
|
|
sortedBaselineViolations.violationsWithAbsolutePaths
|
|
}
|
|
|
|
/// Creates a `Baseline` from a saved file.
|
|
///
|
|
/// - parameter fromPath: The path to read from.
|
|
public init(fromPath path: String) throws {
|
|
let data = try Data(contentsOf: URL(fileURLWithPath: path))
|
|
baseline = try JSONDecoder().decode(BaselineViolations.self, from: data).groupedByFile()
|
|
}
|
|
|
|
/// Creates a `Baseline` from a list of violations.
|
|
///
|
|
/// - parameter violations: The violations for the baseline.
|
|
public init(violations: [StyleViolation]) {
|
|
baseline = BaselineViolations(violations).groupedByFile()
|
|
}
|
|
|
|
/// 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)
|
|
try data.write(to: URL(fileURLWithPath: path))
|
|
}
|
|
|
|
/// Filters out violations that are present in the `Baseline`.
|
|
///
|
|
/// Assumes that all violations are from the same file.
|
|
///
|
|
/// - parameter violations: The violations to filter.
|
|
/// - Returns: The new violations.
|
|
public func filter(_ violations: [StyleViolation]) -> [StyleViolation] {
|
|
guard let firstViolation = violations.first,
|
|
let baselineViolations = baseline[firstViolation.location.relativeFile ?? ""],
|
|
baselineViolations.isNotEmpty else {
|
|
return violations
|
|
}
|
|
|
|
let relativePathViolations = BaselineViolations(violations)
|
|
let violationsWithAbsolutePaths = filter(
|
|
relativePathViolations: relativePathViolations,
|
|
baselineViolations: baselineViolations
|
|
)
|
|
return violations.filter { violationsWithAbsolutePaths.contains($0) }
|
|
}
|
|
|
|
private func filter(
|
|
relativePathViolations: BaselineViolations, baselineViolations: BaselineViolations
|
|
) -> Set<StyleViolation> {
|
|
if relativePathViolations == baselineViolations {
|
|
return []
|
|
}
|
|
|
|
let violationsByRuleIdentifier = relativePathViolations.groupedByRuleIdentifier(
|
|
filteredBy: baselineViolations
|
|
)
|
|
let baselineViolationsByRuleIdentifier = baselineViolations.groupedByRuleIdentifier(
|
|
filteredBy: relativePathViolations
|
|
)
|
|
|
|
var filteredViolations: Set<BaselineViolation> = []
|
|
|
|
for (ruleIdentifier, ruleViolations) in violationsByRuleIdentifier {
|
|
guard let baselineViolations = baselineViolationsByRuleIdentifier[ruleIdentifier],
|
|
baselineViolations.isNotEmpty else {
|
|
filteredViolations.formUnion(ruleViolations)
|
|
continue
|
|
}
|
|
|
|
let groupedRuleViolations = Dictionary(grouping: ruleViolations, by: \.key)
|
|
let groupedBaselineViolations = Dictionary(grouping: baselineViolations, by: \.key)
|
|
|
|
for (key, ruleViolations) in groupedRuleViolations {
|
|
guard let baselineViolations = groupedBaselineViolations[key] else {
|
|
filteredViolations.formUnion(ruleViolations)
|
|
continue
|
|
}
|
|
if ruleViolations.count > baselineViolations.count {
|
|
filteredViolations.formUnion(ruleViolations)
|
|
}
|
|
}
|
|
}
|
|
|
|
return Set(filteredViolations.violationsWithAbsolutePaths)
|
|
}
|
|
|
|
/// Returns the violations that are present in another `Baseline`, but not in this one.
|
|
///
|
|
/// The violations are filtered using the same algorithm as the `filter` method above.
|
|
///
|
|
/// - parameter otherBaseline: The other `Baseline`.
|
|
public func compare(_ otherBaseline: Self) -> [StyleViolation] {
|
|
otherBaseline.baseline.flatMap { relativePath, otherBaselineViolations -> Set<StyleViolation> in
|
|
if let baselineViolations = baseline[relativePath] {
|
|
return filter(relativePathViolations: otherBaselineViolations, baselineViolations: baselineViolations)
|
|
}
|
|
return Set(otherBaselineViolations.violationsWithAbsolutePaths)
|
|
}.sorted {
|
|
$0.location == $1.location ? $0.ruleIdentifier < $1.ruleIdentifier : $0.location < $1.location
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct LineCache {
|
|
private var lines: [String: [String]] = [:]
|
|
|
|
mutating func text(at location: Location) -> String {
|
|
let line = (location.line ?? 0) - 1
|
|
if line > 0, let file = location.file, let content = cached(file: file), line < content.count {
|
|
return content[line]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
private mutating func cached(file: String) -> [String]? {
|
|
if let fileLines = lines[file] {
|
|
return fileLines
|
|
}
|
|
if let fileLines = SwiftLintFile(path: file)?.lines.map(\.content) {
|
|
lines[file] = fileLines
|
|
return fileLines
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private extension Sequence where Element == BaselineViolation {
|
|
init(_ violations: [StyleViolation]) where Self == BaselineViolations {
|
|
var lineCache = LineCache()
|
|
self = violations.map { $0.baselineViolation(text: lineCache.text(at: $0.location)) }
|
|
}
|
|
|
|
var violationsWithAbsolutePaths: [StyleViolation] {
|
|
map(\.violation.withAbsolutePath)
|
|
}
|
|
|
|
func groupedByFile() -> ViolationsPerFile {
|
|
Dictionary(grouping: self) { $0.violation.location.relativeFile ?? "" }
|
|
}
|
|
|
|
func groupedByRuleIdentifier(filteredBy existingViolations: [BaselineViolation] = []) -> ViolationsPerRule {
|
|
Dictionary(grouping: Set(self).subtracting(existingViolations), by: \.violation.ruleIdentifier)
|
|
}
|
|
}
|
|
|
|
private extension StyleViolation {
|
|
var withAbsolutePath: StyleViolation {
|
|
let absolutePath: String? =
|
|
if let relativePath = location.file {
|
|
FileManager.default.currentDirectoryPath + "/" + relativePath
|
|
} else {
|
|
nil
|
|
}
|
|
let newLocation = Location(file: absolutePath, line: location.line, character: location.character)
|
|
return with(location: newLocation)
|
|
}
|
|
|
|
func baselineViolation(text: String = "") -> BaselineViolation {
|
|
BaselineViolation(violation: self, text: text)
|
|
}
|
|
}
|