Files
Danny Mösch a6c4fd98bc Move files from SwiftLintCore to SwiftLintFramework
Ideally, SwiftLintCore would some day only contain components
that are needed to define rules. Consequently, it would be the
only bundle required to import for (external) rule development.
2024-12-23 12:51:43 +01:00

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: Self { Self(entries: [:]) }
}
/// A persisted cache for storing and retrieving linter results.
public final class LinterCache {
private typealias Encoder = PropertyListEncoder
private typealias Decoder = PropertyListDecoder
private typealias Cache = [String: FileCache]
private static let fileExtension = "plist"
private var lazyReadCache: Cache
private let readCacheLock = NSLock()
private var writeCache = Cache()
private let writeCacheLock = NSLock()
internal let fileManager: any LintableFileManager
private let location: URL?
private let swiftVersion: SwiftVersion
internal init(fileManager: some 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: some LintableFileManager = FileManager.default) {
location = configuration.cacheURL
lazyReadCache = Cache()
self.fileManager = fileManager
self.swiftVersion = .current
}
private init(cache: Cache, location: URL?, fileManager: some 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 {
Self(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 })
}
}
}