mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
306 lines
11 KiB
Swift
306 lines
11 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
import SwiftDiagnostics
|
|
import SwiftIDEUtils
|
|
import SwiftOperators
|
|
import SwiftParser
|
|
import SwiftParserDiagnostics
|
|
import SwiftSyntax
|
|
|
|
// swiftlint:disable:next blanket_disable_command
|
|
// swiftlint:disable closure_end_indentation opening_brace
|
|
|
|
package typealias AssertHandler = () -> Void
|
|
|
|
// Re-enable once all parser diagnostics in tests have been addressed.
|
|
// https://github.com/realm/SwiftLint/issues/3348
|
|
@TaskLocal package var parserDiagnosticsDisabledForTests = false
|
|
|
|
private typealias SourceKitResponse = [String: any SourceKitRepresentable]
|
|
|
|
/// Wraps a cached value, distinguishing "not yet computed" from "computed (possibly nil)".
|
|
private enum Cached<T> {
|
|
case notComputed
|
|
case computing(DispatchGroup)
|
|
case computed(T)
|
|
}
|
|
|
|
private enum CacheLookup<T> {
|
|
case hit(T)
|
|
case miss
|
|
case wait(DispatchGroup)
|
|
case compute(DispatchGroup)
|
|
}
|
|
|
|
/// Per-file cache storing all derived artifacts. One instance lives on each `SwiftLintFile`.
|
|
///
|
|
/// **Locking strategy**: a concurrent `DispatchQueue` guards all slots.
|
|
/// Reads use a plain `sync` (concurrent); writes use `sync(flags: .barrier)` (exclusive).
|
|
/// Each accessor computes its value *outside* the lock so that factories may safely access
|
|
/// other cached properties without risk of deadlock. Cold slots are marked as computing
|
|
/// under a barrier so concurrent first readers wait for the same result.
|
|
final class FileCache: @unchecked Sendable {
|
|
fileprivate let queue = DispatchQueue(label: "io.realm.swiftlint.fileCache", attributes: .concurrent)
|
|
|
|
fileprivate var syntaxTree = Cached<SourceFileSyntax>.notComputed
|
|
fileprivate var locationConverter = Cached<SourceLocationConverter>.notComputed
|
|
fileprivate var commands = Cached<[Command]>.notComputed
|
|
fileprivate var syntaxClassifications = Cached<SyntaxClassifications>.notComputed
|
|
fileprivate var linesWithTokens = Cached<Set<Int>>.notComputed
|
|
fileprivate var commentLines = Cached<Set<Int>>.notComputed
|
|
fileprivate var emptyLines = Cached<Set<Int>>.notComputed
|
|
fileprivate var response = Cached<SourceKitResponse?>.notComputed
|
|
fileprivate var structureDictionary = Cached<SourceKittenDictionary?>.notComputed
|
|
fileprivate var foldedSyntaxTree = Cached<SourceFileSyntax?>.notComputed
|
|
fileprivate var syntaxMap = Cached<SwiftLintSyntaxMap?>.notComputed
|
|
fileprivate var swiftSyntaxTokens = Cached<[SwiftLintSyntaxToken]?>.notComputed
|
|
fileprivate var assertHandlerSlot = Cached<AssertHandler?>.notComputed
|
|
|
|
/// Returns the cached value for a slot, computing it via `factory` on a cache miss.
|
|
/// The factory runs *outside* the lock, so it may safely access other cached properties.
|
|
/// On a concurrent first access the winner's result is kept; the loser's is discarded.
|
|
///
|
|
/// TODO: [06/05/2028] We can convert the explicit getters and setters to a keypath-based subscript once the Swift
|
|
/// compiler bug https://github.com/swiftlang/swift/issues/69386 is resolved.
|
|
fileprivate func getOrCompute<T>(factory: () -> T, get: () -> Cached<T>, set: (Cached<T>) -> Void) -> T {
|
|
// swiftlint:disable:previous cyclomatic_complexity
|
|
|
|
let initialState = queue.sync { () -> CacheLookup<T> in
|
|
switch get() {
|
|
case .computed(let value):
|
|
return .hit(value)
|
|
case .computing(let group):
|
|
return .wait(group)
|
|
case .notComputed:
|
|
return .miss
|
|
}
|
|
}
|
|
|
|
switch initialState {
|
|
case .hit(let value):
|
|
return value
|
|
case .wait(let group):
|
|
group.wait()
|
|
return getOrCompute(factory: factory, get: get, set: set)
|
|
case .miss, .compute:
|
|
break
|
|
}
|
|
|
|
let lookup = queue.sync(flags: .barrier) { () -> CacheLookup<T> in
|
|
switch get() {
|
|
case .computed(let value):
|
|
return .hit(value)
|
|
case .computing(let group):
|
|
return .wait(group)
|
|
case .notComputed:
|
|
let group = DispatchGroup()
|
|
group.enter()
|
|
set(.computing(group))
|
|
return .compute(group)
|
|
}
|
|
}
|
|
|
|
switch lookup {
|
|
case .hit(let value):
|
|
return value
|
|
case .wait(let group):
|
|
group.wait()
|
|
return getOrCompute(factory: factory, get: get, set: set)
|
|
case .compute(let group):
|
|
let value = factory()
|
|
queue.sync(flags: .barrier) {
|
|
defer { group.leave() }
|
|
if case .computing(let currentGroup) = get(), currentGroup === group {
|
|
set(.computed(value))
|
|
}
|
|
}
|
|
return value
|
|
case .miss:
|
|
queuedFatalError("Impossible state: missed then compute")
|
|
}
|
|
}
|
|
|
|
/// Resets all slots to `.notComputed`, forcing recomputation on next access.
|
|
fileprivate func invalidateAll() {
|
|
queue.sync(flags: .barrier) {
|
|
syntaxTree = .notComputed
|
|
locationConverter = .notComputed
|
|
commands = .notComputed
|
|
syntaxClassifications = .notComputed
|
|
linesWithTokens = .notComputed
|
|
commentLines = .notComputed
|
|
emptyLines = .notComputed
|
|
response = .notComputed
|
|
structureDictionary = .notComputed
|
|
foldedSyntaxTree = .notComputed
|
|
syntaxMap = .notComputed
|
|
swiftSyntaxTokens = .notComputed
|
|
assertHandlerSlot = .notComputed
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SwiftLintFile {
|
|
public var sourcekitdFailed: Bool {
|
|
get { cachedResponse == nil }
|
|
set {
|
|
fileCache.queue.sync(flags: .barrier) {
|
|
fileCache.response = newValue ? .computed(nil) : .notComputed
|
|
}
|
|
}
|
|
}
|
|
|
|
package var assertHandler: AssertHandler? {
|
|
get {
|
|
fileCache.getOrCompute
|
|
{ nil }
|
|
get: { fileCache.assertHandlerSlot }
|
|
set: { fileCache.assertHandlerSlot = $0 }
|
|
}
|
|
set {
|
|
fileCache.queue.sync(flags: .barrier) { fileCache.assertHandlerSlot = .computed(newValue) }
|
|
}
|
|
}
|
|
|
|
public var parserDiagnostics: [String] {
|
|
if parserDiagnosticsDisabledForTests {
|
|
return []
|
|
}
|
|
return ParseDiagnosticsGenerator.diagnostics(for: syntaxTree)
|
|
.filter { $0.diagMessage.severity == .error }
|
|
.map(\.message)
|
|
}
|
|
|
|
public var linesWithTokens: Set<Int> {
|
|
fileCache.getOrCompute
|
|
{ computeLinesWithTokens() }
|
|
get: { fileCache.linesWithTokens }
|
|
set: { fileCache.linesWithTokens = $0 }
|
|
}
|
|
|
|
public var structureDictionary: SourceKittenDictionary {
|
|
let value = fileCache.getOrCompute
|
|
{ cachedResponse.map(Structure.init).map { SourceKittenDictionary($0.dictionary) } }
|
|
get: { fileCache.structureDictionary }
|
|
set: { fileCache.structureDictionary = $0 }
|
|
guard let value else {
|
|
if let handler = assertHandler {
|
|
handler()
|
|
return SourceKittenDictionary([:])
|
|
}
|
|
queuedFatalError("Never call this for file that sourcekitd fails.")
|
|
}
|
|
return value
|
|
}
|
|
|
|
public var syntaxClassifications: SyntaxClassifications {
|
|
fileCache.getOrCompute
|
|
{ syntaxTree.classifications }
|
|
get: { fileCache.syntaxClassifications }
|
|
set: { fileCache.syntaxClassifications = $0 }
|
|
}
|
|
|
|
public var syntaxMap: SwiftLintSyntaxMap {
|
|
let value = fileCache.getOrCompute
|
|
{ cachedResponse.map { SwiftLintSyntaxMap(value: SyntaxMap(sourceKitResponse: $0)) } }
|
|
get: { fileCache.syntaxMap }
|
|
set: { fileCache.syntaxMap = $0 }
|
|
guard let value else {
|
|
if let handler = assertHandler {
|
|
handler()
|
|
return SwiftLintSyntaxMap(value: SyntaxMap(data: []))
|
|
}
|
|
queuedFatalError("Never call this for file that sourcekitd fails.")
|
|
}
|
|
return value
|
|
}
|
|
|
|
public var syntaxTree: SourceFileSyntax {
|
|
fileCache.getOrCompute
|
|
{ Parser.parse(source: contents) }
|
|
get: { fileCache.syntaxTree }
|
|
set: { fileCache.syntaxTree = $0 }
|
|
}
|
|
|
|
public var foldedSyntaxTree: SourceFileSyntax? {
|
|
fileCache.getOrCompute
|
|
{
|
|
OperatorTable.standardOperators
|
|
.foldAll(syntaxTree) { _ in /* Don't handle errors. */ }
|
|
.as(SourceFileSyntax.self)
|
|
}
|
|
get: { fileCache.foldedSyntaxTree }
|
|
set: { fileCache.foldedSyntaxTree = $0 }
|
|
}
|
|
|
|
public var locationConverter: SourceLocationConverter {
|
|
fileCache.getOrCompute
|
|
{ SourceLocationConverter(fileName: path?.filepath ?? "<nopath>", tree: syntaxTree) }
|
|
get: { fileCache.locationConverter }
|
|
set: { fileCache.locationConverter = $0 }
|
|
}
|
|
|
|
public var commands: [Command] { cachedCommands.filter(\.isValid) }
|
|
|
|
public var invalidCommands: [Command] { cachedCommands.filter { !$0.isValid } }
|
|
|
|
public var swiftSyntaxDerivedSourceKittenTokens: [SwiftLintSyntaxToken]? {
|
|
fileCache.getOrCompute
|
|
{ SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: self) }
|
|
get: { fileCache.swiftSyntaxTokens }
|
|
set: { fileCache.swiftSyntaxTokens = $0 }
|
|
}
|
|
|
|
public var commentLines: Set<Int> {
|
|
fileCache.getOrCompute
|
|
{ CommentLinesVisitor.commentLines(in: self) }
|
|
get: { fileCache.commentLines }
|
|
set: { fileCache.commentLines = $0 }
|
|
}
|
|
|
|
public var emptyLines: Set<Int> {
|
|
fileCache.getOrCompute
|
|
{ EmptyLinesVisitor.emptyLines(in: self) }
|
|
get: { fileCache.emptyLines }
|
|
set: { fileCache.emptyLines = $0 }
|
|
}
|
|
|
|
/// Invalidates all cached data for this file.
|
|
public func invalidateCache() {
|
|
if !isVirtual {
|
|
file.clearCaches()
|
|
}
|
|
fileCache.invalidateAll()
|
|
}
|
|
|
|
// MARK: - Private helpers
|
|
|
|
/// The raw SourceKit response for this file (cached).
|
|
private var cachedResponse: SourceKitResponse? {
|
|
fileCache.getOrCompute
|
|
{
|
|
do {
|
|
return try Request.editorOpen(file: file).sendIfNotDisabled()
|
|
} catch let error as Request.Error {
|
|
queuedPrintError(error.description)
|
|
return nil
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
get: { fileCache.response }
|
|
set: { fileCache.response = $0 }
|
|
}
|
|
|
|
private var cachedCommands: [Command] {
|
|
fileCache.getOrCompute
|
|
{
|
|
contents.contains("swiftlint:")
|
|
? CommandVisitor(locationConverter: locationConverter).walk(file: self, handler: \.commands)
|
|
: []
|
|
}
|
|
get: { fileCache.commands }
|
|
set: { fileCache.commands = $0 }
|
|
}
|
|
}
|