import Foundation internal enum LinterCacheError: Error { case invalidFormat case noLocation } public final class LinterCache { private typealias Cache = [String: [String: [String: Any]]] private let readCache: Cache private var writeCache = Cache() private let lock = NSLock() internal let fileManager: LintableFileManager private let location: URL? private let swiftVersion: SwiftVersion internal init(fileManager: LintableFileManager = FileManager.default, swiftVersion: SwiftVersion = .current) { location = nil self.fileManager = fileManager self.readCache = [:] self.swiftVersion = swiftVersion } internal init(cache: Any, fileManager: LintableFileManager = FileManager.default, swiftVersion: SwiftVersion = .current) throws { guard let dictionary = cache as? Cache else { throw LinterCacheError.invalidFormat } self.readCache = dictionary location = nil self.fileManager = fileManager self.swiftVersion = swiftVersion } public init(configuration: Configuration, fileManager: LintableFileManager = FileManager.default) { location = configuration.cacheURL if let data = try? Data(contentsOf: location!), let json = try? JSONSerialization.jsonObject(with: data), let cache = json as? Cache { readCache = cache } else { readCache = [:] } self.fileManager = fileManager self.swiftVersion = .current } private init(cache: Cache, location: URL?, fileManager: LintableFileManager, swiftVersion: SwiftVersion) { self.readCache = cache self.location = location self.fileManager = fileManager self.swiftVersion = swiftVersion } internal func cache(violations: [StyleViolation], forFile file: String, configuration: Configuration) { guard let lastModification = fileManager.modificationDate(forFileAtPath: file) else { return } let configurationDescription = configuration.cacheDescription lock.lock() var filesCache = writeCache[configurationDescription] ?? [:] filesCache[file] = [ Key.violations.rawValue: violations.map(dictionary(for:)), Key.lastModification.rawValue: lastModification.timeIntervalSinceReferenceDate, Key.swiftVersion.rawValue: swiftVersion.rawValue ] writeCache[configurationDescription] = filesCache lock.unlock() } internal func violations(forFile file: String, configuration: Configuration) -> [StyleViolation]? { guard let lastModification = fileManager.modificationDate(forFileAtPath: file) else { return nil } let configurationDescription = configuration.cacheDescription func getCacheLastModification(dict: [String: Any]) -> TimeInterval? { let value = dict[.lastModification] #if os(Linux) if let cacheLastModificationInt = value as? Int { return TimeInterval(cacheLastModificationInt) } #endif return value as? TimeInterval } guard let filesCache = readCache[configurationDescription], let entry = filesCache[file], let cacheLastModification = getCacheLastModification(dict: entry), cacheLastModification == lastModification.timeIntervalSinceReferenceDate, let swiftVersion = (entry[.swiftVersion] as? String).flatMap(SwiftVersion.init(rawValue:)), swiftVersion == self.swiftVersion, let violations = entry[.violations] as? [[String: Any]] else { return nil } return violations.compactMap { StyleViolation.from(cache: $0, file: file) } } public func save() throws { guard let url = location else { throw LinterCacheError.noLocation } guard !writeCache.isEmpty else { return } let cache = mergeCaches() let json = try cache.toJSON() try json.write(to: url, atomically: true, encoding: .utf8) } internal func flushed() -> LinterCache { return LinterCache(cache: mergeCaches(), location: location, fileManager: fileManager, swiftVersion: swiftVersion) } private func mergeCaches() -> Cache { var cache = readCache lock.lock() for (key, value) in writeCache { var filesCache = cache[key] ?? [:] for (file, fileCache) in value { filesCache[file] = fileCache } cache[key] = filesCache } lock.unlock() return cache } private func dictionary(for violation: StyleViolation) -> [String: Any] { return [ Key.line.rawValue: violation.location.line ?? NSNull() as Any, Key.character.rawValue: violation.location.character ?? NSNull() as Any, Key.severity.rawValue: violation.severity.rawValue, Key.type.rawValue: violation.ruleDescription.name, Key.ruleID.rawValue: violation.ruleDescription.identifier, Key.reason.rawValue: violation.reason, Key.ruleKind.rawValue: violation.ruleDescription.kind.rawValue ] } } private extension LinterCache { enum Key: String { case character case lastModification = "last_modification" case line case reason case ruleID = "rule_id" case severity case type case violations case ruleKind = "rule_kind" case swiftVersion = "swift_version" } } private extension Dictionary where Key == String { subscript(_ key: LinterCache.Key) -> Value? { return self[key.rawValue] } } private extension StyleViolation { static func from(cache: [String: Any], file: String) -> StyleViolation? { guard let severityString = cache[.severity] as? String, let severity = ViolationSeverity(rawValue: severityString), let name = cache[.type] as? String, let ruleID = cache[.ruleID] as? String, let reason = cache[.reason] as? String, let ruleKind = (cache[.ruleKind] as? String).flatMap(RuleKind.init(rawValue:)) else { return nil } let line = cache[.line] as? Int let character = cache[.character] as? Int let description = RuleDescription(identifier: ruleID, name: name, description: reason, kind: ruleKind) return StyleViolation(ruleDescription: description, severity: severity, location: Location(file: file, line: line, character: character), reason: reason) } } internal extension Dictionary where Key == String { func toJSON() throws -> String { // not using .sortedKeys to avoid crash let prettyJSONData = try JSONSerialization.data(withJSONObject: self, options: .prettyPrinted) if let jsonString = String(data: prettyJSONData, encoding: .utf8) { return jsonString } else { throw LinterCacheError.invalidFormat } } }