-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
LinterCache.swift
153 lines (129 loc) · 5.47 KB
/
LinterCache.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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 })
}
}
}