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 { case notComputed case computing(DispatchGroup) case computed(T) } private enum CacheLookup { 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.notComputed fileprivate var locationConverter = Cached.notComputed fileprivate var commands = Cached<[Command]>.notComputed fileprivate var syntaxClassifications = Cached.notComputed fileprivate var linesWithTokens = Cached>.notComputed fileprivate var commentLines = Cached>.notComputed fileprivate var emptyLines = Cached>.notComputed fileprivate var response = Cached.notComputed fileprivate var structureDictionary = Cached.notComputed fileprivate var foldedSyntaxTree = Cached.notComputed fileprivate var syntaxMap = Cached.notComputed fileprivate var swiftSyntaxTokens = Cached<[SwiftLintSyntaxToken]?>.notComputed fileprivate var assertHandlerSlot = Cached.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(factory: () -> T, get: () -> Cached, set: (Cached) -> Void) -> T { // swiftlint:disable:previous cyclomatic_complexity let initialState = queue.sync { () -> CacheLookup 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 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 { 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 ?? "", 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 { fileCache.getOrCompute { CommentLinesVisitor.commentLines(in: self) } get: { fileCache.commentLines } set: { fileCache.commentLines = $0 } } public var emptyLines: Set { 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 } } }