154 lines
5.5 KiB
Swift
154 lines
5.5 KiB
Swift
import Foundation
|
|
|
|
private enum LinterCacheError: Error {
|
|
case noLocation
|
|
}
|
|
|
|
private struct FileCacheEntry: Codable {
|
|
let violations: [StyleViolation]
|
|
let lastModification: Date
|
|
let swiftVersion: SwiftVersion
|
|
}
|
|
|
|
private struct FileCache: Codable {
|
|
var entries: [String: FileCacheEntry]
|
|
|
|
static var empty: FileCache { return Self(entries: [:]) }
|
|
}
|
|
|
|
/// A persisted cache for storing and retrieving linter results.
|
|
public final class LinterCache {
|
|
private typealias Encoder = PropertyListEncoder
|
|
private typealias Decoder = PropertyListDecoder
|
|
private static let fileExtension = "plist"
|
|
|
|
private typealias Cache = [String: FileCache]
|
|
|
|
private var lazyReadCache: Cache
|
|
private let readCacheLock = NSLock()
|
|
private var writeCache = Cache()
|
|
private let writeCacheLock = 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.lazyReadCache = Cache()
|
|
self.swiftVersion = swiftVersion
|
|
}
|
|
|
|
/// Creates a `LinterCache` by specifying a SwiftLint configuration and a file manager.
|
|
///
|
|
/// - parameter configuration: The SwiftLint configuration for which this cache will be used.
|
|
/// - parameter fileManager: The file manager to use to read lintable file information.
|
|
public init(configuration: Configuration, fileManager: LintableFileManager = FileManager.default) {
|
|
location = configuration.cacheURL
|
|
lazyReadCache = Cache()
|
|
self.fileManager = fileManager
|
|
self.swiftVersion = .current
|
|
}
|
|
|
|
private init(cache: Cache, location: URL?, fileManager: LintableFileManager, swiftVersion: SwiftVersion) {
|
|
self.lazyReadCache = 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
|
|
|
|
writeCacheLock.lock()
|
|
var filesCache = writeCache[configurationDescription] ?? .empty
|
|
filesCache.entries[file] = FileCacheEntry(violations: violations, lastModification: lastModification,
|
|
swiftVersion: swiftVersion)
|
|
writeCache[configurationDescription] = filesCache
|
|
writeCacheLock.unlock()
|
|
}
|
|
|
|
internal func violations(forFile file: String, configuration: Configuration) -> [StyleViolation]? {
|
|
guard let lastModification = fileManager.modificationDate(forFileAtPath: file),
|
|
let entry = fileCache(cacheDescription: configuration.cacheDescription).entries[file],
|
|
entry.lastModification == lastModification,
|
|
entry.swiftVersion == swiftVersion
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
return entry.violations
|
|
}
|
|
|
|
/// Persists the cache to disk.
|
|
///
|
|
/// - throws: Throws if the linter cache doesn't have a `location` value, if the cache couldn't be serialized, or if
|
|
/// the contents couldn't be written to disk.
|
|
public func save() throws {
|
|
guard let url = location else {
|
|
throw LinterCacheError.noLocation
|
|
}
|
|
writeCacheLock.lock()
|
|
defer {
|
|
writeCacheLock.unlock()
|
|
}
|
|
guard writeCache.isNotEmpty else {
|
|
return
|
|
}
|
|
|
|
readCacheLock.lock()
|
|
let readCache = lazyReadCache
|
|
readCacheLock.unlock()
|
|
|
|
let encoder = Encoder()
|
|
for (description, writeFileCache) in writeCache where writeFileCache.entries.isNotEmpty {
|
|
let fileCacheEntries = readCache[description]?.entries.merging(writeFileCache.entries) { _, write in write }
|
|
let fileCache = fileCacheEntries.map(FileCache.init) ?? writeFileCache
|
|
let data = try encoder.encode(fileCache)
|
|
let file = url.appendingPathComponent(description).appendingPathExtension(Self.fileExtension)
|
|
try data.write(to: file, options: .atomic)
|
|
}
|
|
}
|
|
|
|
internal func flushed() -> LinterCache {
|
|
LinterCache(cache: mergeCaches(), location: location, fileManager: fileManager, swiftVersion: swiftVersion)
|
|
}
|
|
|
|
private func fileCache(cacheDescription: String) -> FileCache {
|
|
readCacheLock.lock()
|
|
defer {
|
|
readCacheLock.unlock()
|
|
}
|
|
|
|
if let fileCache = lazyReadCache[cacheDescription] {
|
|
return fileCache
|
|
}
|
|
|
|
guard let location else {
|
|
return .empty
|
|
}
|
|
|
|
let file = location.appendingPathComponent(cacheDescription).appendingPathExtension(Self.fileExtension)
|
|
let data = try? Data(contentsOf: file)
|
|
let fileCache = data.flatMap { try? Decoder().decode(FileCache.self, from: $0) } ?? .empty
|
|
lazyReadCache[cacheDescription] = fileCache
|
|
return fileCache
|
|
}
|
|
|
|
private func mergeCaches() -> Cache {
|
|
readCacheLock.lock()
|
|
writeCacheLock.lock()
|
|
defer {
|
|
readCacheLock.unlock()
|
|
writeCacheLock.unlock()
|
|
}
|
|
return lazyReadCache.merging(writeCache) { read, write in
|
|
FileCache(entries: read.entries.merging(write.entries) { _, write in write })
|
|
}
|
|
}
|
|
}
|