| 
 | 1 | +//===----------------------------------------------------------------------===//  | 
 | 2 | +//  | 
 | 3 | +// This source file is part of the Swift.org open source project  | 
 | 4 | +//  | 
 | 5 | +// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors  | 
 | 6 | +// Licensed under Apache License v2.0 with Runtime Library Exception  | 
 | 7 | +//  | 
 | 8 | +// See https://swift.org/LICENSE.txt for license information  | 
 | 9 | +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors  | 
 | 10 | +//  | 
 | 11 | +//===----------------------------------------------------------------------===//  | 
 | 12 | + | 
 | 13 | +import LSPLogging  | 
 | 14 | +import LanguageServerProtocol  | 
 | 15 | +import SourceKitD  | 
 | 16 | +import SwiftParserDiagnostics  | 
 | 17 | + | 
 | 18 | +actor DiagnosticReportManager {  | 
 | 19 | +  /// A task to produce diagnostics, either from a diagnostics request to `sourcektid` or by using the built-in swift-syntax.  | 
 | 20 | +  private typealias ReportTask = Task<RelatedFullDocumentDiagnosticReport, Error>  | 
 | 21 | + | 
 | 22 | +  private let sourcekitd: SourceKitD  | 
 | 23 | +  private let syntaxTreeManager: SyntaxTreeManager  | 
 | 24 | +  private let documentManager: DocumentManager  | 
 | 25 | +  private let clientHasDiagnosticsCodeDescriptionSupport: Bool  | 
 | 26 | + | 
 | 27 | +  private nonisolated var keys: sourcekitd_keys { return sourcekitd.keys }  | 
 | 28 | +  private nonisolated var requests: sourcekitd_requests { return sourcekitd.requests }  | 
 | 29 | + | 
 | 30 | +  /// The cache that stores reportTasks for snapshot id and buildSettings  | 
 | 31 | +  ///  | 
 | 32 | +  /// Conceptually, this is a dictionary. To prevent excessive memory usage we  | 
 | 33 | +  /// only keep `cacheSize` entries within the array. Older entries are at the  | 
 | 34 | +  /// end of the list, newer entries at the front.  | 
 | 35 | +  private var reportTaskCache:  | 
 | 36 | +    [(  | 
 | 37 | +      snapshotID: DocumentSnapshot.ID,  | 
 | 38 | +      buildSettings: SwiftCompileCommand?,  | 
 | 39 | +      reportTask: ReportTask  | 
 | 40 | +    )] = []  | 
 | 41 | + | 
 | 42 | +  /// The number of reportTasks to keep  | 
 | 43 | +  ///  | 
 | 44 | +  /// - Note: This has been chosen without scientific measurements.  | 
 | 45 | +  private let cacheSize = 5  | 
 | 46 | + | 
 | 47 | +  init(  | 
 | 48 | +    sourcekitd: SourceKitD,  | 
 | 49 | +    syntaxTreeManager: SyntaxTreeManager,  | 
 | 50 | +    documentManager: DocumentManager,  | 
 | 51 | +    clientHasDiagnosticsCodeDescriptionSupport: Bool  | 
 | 52 | +  ) {  | 
 | 53 | +    self.sourcekitd = sourcekitd  | 
 | 54 | +    self.syntaxTreeManager = syntaxTreeManager  | 
 | 55 | +    self.documentManager = documentManager  | 
 | 56 | +    self.clientHasDiagnosticsCodeDescriptionSupport = clientHasDiagnosticsCodeDescriptionSupport  | 
 | 57 | +  }  | 
 | 58 | + | 
 | 59 | +  func diagnosticReport(  | 
 | 60 | +    for snapshot: DocumentSnapshot,  | 
 | 61 | +    buildSettings: SwiftCompileCommand?  | 
 | 62 | +  ) async throws -> RelatedFullDocumentDiagnosticReport {  | 
 | 63 | +    if let reportTask = reportTask(for: snapshot.id, buildSettings: buildSettings) {  | 
 | 64 | +      return try await reportTask.value  | 
 | 65 | +    }  | 
 | 66 | +    let reportTask: Task<RelatedFullDocumentDiagnosticReport, Error>  | 
 | 67 | +    if let buildSettings, !buildSettings.isFallback {  | 
 | 68 | +      reportTask = Task {  | 
 | 69 | +        return try await requestReport(with: snapshot, compilerArgs: buildSettings.compilerArgs)  | 
 | 70 | +      }  | 
 | 71 | +    } else {  | 
 | 72 | +      logger.log(  | 
 | 73 | +        "Producing syntactic diagnostics from the built-in swift-syntax because we \(buildSettings != nil ? "have fallback build settings" : "don't have build settings", privacy: .public))"  | 
 | 74 | +      )  | 
 | 75 | +      // If we don't have build settings or we only have fallback build settings,  | 
 | 76 | +      // sourcekitd won't be able to give us accurate semantic diagnostics.  | 
 | 77 | +      // Fall back to providing syntactic diagnostics from the built-in  | 
 | 78 | +      // swift-syntax. That's the best we can do for now.  | 
 | 79 | +      reportTask = Task {  | 
 | 80 | +        return try await requestFallbackReport(with: snapshot)  | 
 | 81 | +      }  | 
 | 82 | +    }  | 
 | 83 | +    setReportTask(for: snapshot.id, buildSettings: buildSettings, reportTask: reportTask)  | 
 | 84 | +    return try await reportTask.value  | 
 | 85 | +  }  | 
 | 86 | + | 
 | 87 | +  func removeItemsFromCache(with uri: DocumentURI) async {  | 
 | 88 | +    for item in reportTaskCache {  | 
 | 89 | +      if item.snapshotID.uri == uri {  | 
 | 90 | +        item.reportTask.cancel()  | 
 | 91 | +      }  | 
 | 92 | +    }  | 
 | 93 | +    reportTaskCache.removeAll(where: { $0.snapshotID.uri == uri })  | 
 | 94 | +  }  | 
 | 95 | + | 
 | 96 | +  private func requestReport(  | 
 | 97 | +    with snapshot: DocumentSnapshot,  | 
 | 98 | +    compilerArgs: [String]  | 
 | 99 | +  ) async throws -> LanguageServerProtocol.RelatedFullDocumentDiagnosticReport {  | 
 | 100 | +    try Task.checkCancellation()  | 
 | 101 | + | 
 | 102 | +    let keys = self.keys  | 
 | 103 | + | 
 | 104 | +    let skreq = sourcekitd.dictionary([  | 
 | 105 | +      keys.request: requests.diagnostics,  | 
 | 106 | +      keys.sourcefile: snapshot.uri.pseudoPath,  | 
 | 107 | +      keys.compilerargs: compilerArgs as [SKDValue],  | 
 | 108 | +    ])  | 
 | 109 | + | 
 | 110 | +    let dict = try await self.sourcekitd.send(skreq, fileContents: snapshot.text)  | 
 | 111 | + | 
 | 112 | +    try Task.checkCancellation()  | 
 | 113 | +    guard (try? documentManager.latestSnapshot(snapshot.uri).id) == snapshot.id else {  | 
 | 114 | +      // Check that the document wasn't modified while we were getting diagnostics. This could happen because we are  | 
 | 115 | +      // calling `fullDocumentDiagnosticReport` from `publishDiagnosticsIfNeeded` outside of `messageHandlingQueue`  | 
 | 116 | +      // and thus a concurrent edit is possible while we are waiting for the sourcekitd request to return a result.  | 
 | 117 | +      throw ResponseError.unknown("Document was modified while loading diagnostics")  | 
 | 118 | +    }  | 
 | 119 | + | 
 | 120 | +    let diagnostics: [Diagnostic] =  | 
 | 121 | +      dict[keys.diagnostics]?.compactMap({ diag in  | 
 | 122 | +        Diagnostic(  | 
 | 123 | +          diag,  | 
 | 124 | +          in: snapshot,  | 
 | 125 | +          useEducationalNoteAsCode: self.clientHasDiagnosticsCodeDescriptionSupport  | 
 | 126 | +        )  | 
 | 127 | +      }) ?? []  | 
 | 128 | + | 
 | 129 | +    return RelatedFullDocumentDiagnosticReport(items: diagnostics)  | 
 | 130 | +  }  | 
 | 131 | + | 
 | 132 | +  private func requestFallbackReport(  | 
 | 133 | +    with snapshot: DocumentSnapshot  | 
 | 134 | +  ) async throws -> LanguageServerProtocol.RelatedFullDocumentDiagnosticReport {  | 
 | 135 | +    // If we don't have build settings or we only have fallback build settings,  | 
 | 136 | +    // sourcekitd won't be able to give us accurate semantic diagnostics.  | 
 | 137 | +    // Fall back to providing syntactic diagnostics from the built-in  | 
 | 138 | +    // swift-syntax. That's the best we can do for now.  | 
 | 139 | +    let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)  | 
 | 140 | +    let swiftSyntaxDiagnostics = ParseDiagnosticsGenerator.diagnostics(for: syntaxTree)  | 
 | 141 | +    let diagnostics = swiftSyntaxDiagnostics.compactMap { (diag) -> Diagnostic? in  | 
 | 142 | +      if diag.diagnosticID == StaticTokenError.editorPlaceholder.diagnosticID {  | 
 | 143 | +        // Ignore errors about editor placeholders in the source file, similar to how sourcekitd ignores them.  | 
 | 144 | +        return nil  | 
 | 145 | +      }  | 
 | 146 | +      return Diagnostic(diag, in: snapshot)  | 
 | 147 | +    }  | 
 | 148 | +    return RelatedFullDocumentDiagnosticReport(items: diagnostics)  | 
 | 149 | +  }  | 
 | 150 | + | 
 | 151 | +  /// The reportTask for the given document snapshot and buildSettings.  | 
 | 152 | +  private func reportTask(  | 
 | 153 | +    for snapshotID: DocumentSnapshot.ID,  | 
 | 154 | +    buildSettings: SwiftCompileCommand?  | 
 | 155 | +  ) -> ReportTask? {  | 
 | 156 | +    return reportTaskCache.first(where: { $0.snapshotID == snapshotID && $0.buildSettings == buildSettings })?  | 
 | 157 | +      .reportTask  | 
 | 158 | +  }  | 
 | 159 | + | 
 | 160 | +  /// Set the reportTask for the given document snapshot and buildSettings.  | 
 | 161 | +  ///  | 
 | 162 | +  /// If we are already storing `cacheSize` many reports, the oldest one  | 
 | 163 | +  /// will get discarded.  | 
 | 164 | +  private func setReportTask(  | 
 | 165 | +    for snapshotID: DocumentSnapshot.ID,  | 
 | 166 | +    buildSettings: SwiftCompileCommand?,  | 
 | 167 | +    reportTask: ReportTask  | 
 | 168 | +  ) {  | 
 | 169 | +    reportTaskCache.insert((snapshotID, buildSettings, reportTask), at: 0)  | 
 | 170 | + | 
 | 171 | +    // Remove any reportTasks for old versions of this document.  | 
 | 172 | +    reportTaskCache.removeAll(where: { $0.snapshotID < snapshotID })  | 
 | 173 | + | 
 | 174 | +    // If we still have more than `cacheSize` reportTasks, delete the ones that  | 
 | 175 | +    // were produced last. We can always re-request them on-demand.  | 
 | 176 | +    while reportTaskCache.count > cacheSize {  | 
 | 177 | +      reportTaskCache.removeLast()  | 
 | 178 | +    }  | 
 | 179 | +  }  | 
 | 180 | +}  | 
0 commit comments