diff --git a/Sources/LanguageServerProtocol/WorkspaceSettings.swift b/Sources/LanguageServerProtocol/WorkspaceSettings.swift index 77d2d3f2..86cd7c46 100644 --- a/Sources/LanguageServerProtocol/WorkspaceSettings.swift +++ b/Sources/LanguageServerProtocol/WorkspaceSettings.swift @@ -16,6 +16,7 @@ public enum WorkspaceSettingsChange: Codable, Hashable { case clangd(ClangWorkspaceSettings) + case documentUpdated(DocumentUpdatedBuildSettings) case unknown public init(from decoder: Decoder) throws { @@ -24,6 +25,8 @@ public enum WorkspaceSettingsChange: Codable, Hashable { // it will rectify this issue. if let settings = try? ClangWorkspaceSettings(from: decoder) { self = .clangd(settings) + } else if let settings = try? DocumentUpdatedBuildSettings(from: decoder) { + self = .documentUpdated(settings) } else { self = .unknown } @@ -33,6 +36,8 @@ public enum WorkspaceSettingsChange: Codable, Hashable { switch self { case .clangd(let settings): try settings.encode(to: encoder) + case .documentUpdated(let settings): + try settings.encode(to: encoder) case .unknown: break // Nothing to do. } @@ -74,3 +79,17 @@ public struct ClangCompileCommand: Codable, Hashable { self.workingDirectory = workingDirectory } } + +/// Workspace settings for a document that has updated build settings. +public struct DocumentUpdatedBuildSettings: Codable, Hashable { + /// The url of the document that has updated. + public var url: URL + + /// The language of the document that has updated. + public var language: Language + + public init(url: URL, language: Language) { + self.url = url + self.language = language + } +} diff --git a/Sources/SKCore/BuildServerBuildSystem.swift b/Sources/SKCore/BuildServerBuildSystem.swift index 693023ff..cdb141c1 100644 --- a/Sources/SKCore/BuildServerBuildSystem.swift +++ b/Sources/SKCore/BuildServerBuildSystem.swift @@ -30,6 +30,9 @@ public final class BuildServerBuildSystem { var buildServer: Connection? public private(set) var indexStorePath: AbsolutePath? + /// Delegate to handle any build system events. + public weak var delegate: BuildSystemDelegate? = nil + public init(projectRoot: AbsolutePath, buildFolder: AbsolutePath?, fileSystem: FileSystem = localFileSystem) throws { let configPath = projectRoot.appending(component: "buildServer.json") let config = try loadBuildServerConfig(path: configPath, fileSystem: fileSystem) @@ -116,6 +119,19 @@ final class BuildServerHandler: LanguageServerEndpoint { extension BuildServerBuildSystem: BuildSystem { + /// Register the given file for build-system level change notifications, such as command + /// line flag changes, dependency changes, etc. + public func registerForChangeNotifications(for url: LanguageServerProtocol.URL) { + // TODO: Implement via BSP extensions. + } + + /// Unregister the given file for build-system level change notifications, such as command + /// line flag changes, dependency changes, etc. + public func unregisterForChangeNotifications(for url: LanguageServerProtocol.URL) { + // TODO: Implement via BSP extensions. + } + + public var indexDatabasePath: AbsolutePath? { return buildFolder?.appending(components: "index", "db") } diff --git a/Sources/SKCore/BuildSystem.swift b/Sources/SKCore/BuildSystem.swift index 0e99ed52..a60cfd6c 100644 --- a/Sources/SKCore/BuildSystem.swift +++ b/Sources/SKCore/BuildSystem.swift @@ -15,13 +15,13 @@ import TSCBasic /// Provider of FileBuildSettings and other build-related information. /// -/// The primary role of the build system is to answer queries for FileBuildSettings and (TODO) to -/// notify clients when they change. The BuildSystem is also the source of related informatino, -/// such as where the index datastore is located. +/// The primary role of the build system is to answer queries for FileBuildSettings and to notify +/// its delegate when they change. The BuildSystem is also the source of related information, such +/// as where the index datastore is located. /// /// For example, a SwiftPMWorkspace provides compiler arguments for the files contained in a /// SwiftPM package root directory. -public protocol BuildSystem { +public protocol BuildSystem: AnyObject { /// The path to the raw index store data, if any. var indexStorePath: AbsolutePath? { get } @@ -35,5 +35,14 @@ public protocol BuildSystem { /// Returns the toolchain to use to compile this file func toolchain(for: URL, _ language: Language) -> Toolchain? - // TODO: notifications when settings change. + /// Delegate to handle any build system events such as file build settings changing. + var delegate: BuildSystemDelegate? { get set } + + /// Register the given file for build-system level change notifications, such as command + /// line flag changes, dependency changes, etc. + func registerForChangeNotifications(for: URL) + + /// Unregister the given file for build-system level change notifications, such as command + /// line flag changes, dependency changes, etc. + func unregisterForChangeNotifications(for: URL) } diff --git a/Sources/SKCore/BuildSystemDelegate.swift b/Sources/SKCore/BuildSystemDelegate.swift new file mode 100644 index 00000000..0407fbce --- /dev/null +++ b/Sources/SKCore/BuildSystemDelegate.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import LanguageServerProtocol +import TSCBasic + +/// Handles build system events, such as file build settings changes. +public protocol BuildSystemDelegate: AnyObject { + + /// Notify the delegate that the given files' build settings have changed. + /// + /// The callee should request new build settings for any of the given files that they are interested in. + func fileBuildSettingsChanged(_ changedFiles: Set) +} diff --git a/Sources/SKCore/BuildSystemList.swift b/Sources/SKCore/BuildSystemList.swift index 085d150c..1f69eb7f 100644 --- a/Sources/SKCore/BuildSystemList.swift +++ b/Sources/SKCore/BuildSystemList.swift @@ -16,6 +16,12 @@ import LanguageServerProtocol /// Provides build settings from a list of build systems in priority order. public final class BuildSystemList { + /// Delegate to handle any build system events. + public var delegate: BuildSystemDelegate? { + get { return providers.first?.delegate } + set { providers.first?.delegate = newValue } + } + /// The build systems to try (in order). public var providers: [BuildSystem] = [ FallbackBuildSystem() @@ -25,7 +31,6 @@ public final class BuildSystemList { } extension BuildSystemList: BuildSystem { - public var indexStorePath: AbsolutePath? { return providers.first?.indexStorePath } public var indexDatabasePath: AbsolutePath? { return providers.first?.indexDatabasePath } @@ -39,6 +44,20 @@ extension BuildSystemList: BuildSystem { return nil } + /// Register the given file for build-system level change notifications, such as command + /// line flag changes, dependency changes, etc. + public func registerForChangeNotifications(for url: URL) { + // Only register with the primary build system, since we only use its delegate. + providers.first?.registerForChangeNotifications(for: url) + } + + /// Unregister the given file for build-system level change notifications, such as command + /// line flag changes, dependency changes, etc. + public func unregisterForChangeNotifications(for url: URL) { + // Only unregister with the primary build system, since we only use its delegate. + providers.first?.unregisterForChangeNotifications(for: url) + } + public func toolchain(for url: URL, _ language: Language) -> Toolchain? { return providers.first?.toolchain(for: url, language) } diff --git a/Sources/SKCore/CompilationDatabaseBuildSystem.swift b/Sources/SKCore/CompilationDatabaseBuildSystem.swift index 0ec9ea59..9b232ad0 100644 --- a/Sources/SKCore/CompilationDatabaseBuildSystem.swift +++ b/Sources/SKCore/CompilationDatabaseBuildSystem.swift @@ -23,6 +23,9 @@ public final class CompilationDatabaseBuildSystem { /// The compilation database. var compdb: CompilationDatabase? = nil + /// Delegate to handle any build system events. + public weak var delegate: BuildSystemDelegate? = nil + let fileSystem: FileSystem public init(projectRoot: AbsolutePath? = nil, fileSystem: FileSystem = localFileSystem) { @@ -50,6 +53,12 @@ extension CompilationDatabaseBuildSystem: BuildSystem { public func toolchain(for: URL, _ language: Language) -> Toolchain? { return nil } + /// We don't support change watching. + public func registerForChangeNotifications(for: URL) {} + + /// We don't support change watching. + public func unregisterForChangeNotifications(for: URL) {} + func database(for url: URL) -> CompilationDatabase? { if let path = try? AbsolutePath(validating: url.path) { return database(for: path) diff --git a/Sources/SKCore/FallbackBuildSystem.swift b/Sources/SKCore/FallbackBuildSystem.swift index b4672e99..8bee5b41 100644 --- a/Sources/SKCore/FallbackBuildSystem.swift +++ b/Sources/SKCore/FallbackBuildSystem.swift @@ -31,6 +31,9 @@ public final class FallbackBuildSystem: BuildSystem { return nil }() + /// Delegate to handle any build system events. + public weak var delegate: BuildSystemDelegate? = nil + public var indexStorePath: AbsolutePath? { return nil } public var indexDatabasePath: AbsolutePath? { return nil } @@ -50,6 +53,12 @@ public final class FallbackBuildSystem: BuildSystem { } } + /// We don't support change watching. + public func registerForChangeNotifications(for: URL) {} + + /// We don't support change watching. + public func unregisterForChangeNotifications(for: URL) {} + public func toolchain(for: URL, _ language: Language) -> Toolchain? { return nil } func settingsSwift(_ path: AbsolutePath) -> FileBuildSettings { diff --git a/Sources/SKCore/FileBuildSettings.swift b/Sources/SKCore/FileBuildSettings.swift index 6c266e7f..1d505227 100644 --- a/Sources/SKCore/FileBuildSettings.swift +++ b/Sources/SKCore/FileBuildSettings.swift @@ -14,7 +14,7 @@ /// /// Encapsulates all the settings needed to compile a single file, including the compiler arguments /// and working directory. FileBuildSettings are typically the result of a BuildSystem query. -public struct FileBuildSettings { +public struct FileBuildSettings: Equatable { /// The compiler arguments to use for this file. public var compilerArguments: [String] diff --git a/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift b/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift index a8d18a20..ab14202f 100644 --- a/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift +++ b/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift @@ -37,6 +37,9 @@ public final class SwiftPMWorkspace { case cannotDetermineHostToolchain } + /// Delegate to handle any build system events. + public weak var delegate: BuildSystemDelegate? = nil + let workspacePath: AbsolutePath let packageRoot: AbsolutePath var packageGraph: PackageGraph @@ -218,6 +221,18 @@ extension SwiftPMWorkspace: BuildSystem { return nil } + /// Register the given file for build-system level change notifications, such as command + /// line flag changes, dependency changes, etc. + public func registerForChangeNotifications(for url: LanguageServerProtocol.URL) { + // TODO: Support for change detection (via file watching) + } + + /// Unregister the given file for build-system level change notifications, such as command + /// line flag changes, dependency changes, etc. + public func unregisterForChangeNotifications(for url: LanguageServerProtocol.URL) { + // TODO: Support for change detection (via file watching) + } + /// Returns the resolved target description for the given file, if one is known. func targetDescription(for file: AbsolutePath) -> TargetBuildDescription? { if let td = fileToTarget[file] { diff --git a/Sources/SourceKit/DocumentManager.swift b/Sources/SourceKit/DocumentManager.swift index 166a48d3..ce3a3759 100644 --- a/Sources/SourceKit/DocumentManager.swift +++ b/Sources/SourceKit/DocumentManager.swift @@ -61,6 +61,13 @@ public final class DocumentManager { var documents: [URL: Document] = [:] + /// All currently opened documents. + public var openDocuments: Set { + return queue.sync { + return Set(documents.keys) + } + } + /// Opens a new document with the given content and metadata. /// /// - returns: The initial contents of the file. diff --git a/Sources/SourceKit/SourceKitServer.swift b/Sources/SourceKit/SourceKitServer.swift index 52ed5260..c3554aff 100644 --- a/Sources/SourceKit/SourceKitServer.swift +++ b/Sources/SourceKit/SourceKitServer.swift @@ -216,6 +216,32 @@ public final class SourceKitServer: LanguageServer { } } +// MARK: - Build System Delegate + +extension SourceKitServer: BuildSystemDelegate { + public func fileBuildSettingsChanged(_ changedFiles: Set) { + guard let workspace = self.workspace else { + return + } + let documentManager = workspace.documentManager + let openDocuments = documentManager.openDocuments + for url in changedFiles { + guard openDocuments.contains(url) else { + continue + } + + log("Build settings changed for opened file \(url)") + if let snapshot = documentManager.latestSnapshot(url), + let service = languageService(for: url, snapshot.document.language, in: workspace) { + service.send( + DidChangeConfiguration(settings: + .documentUpdated( + DocumentUpdatedBuildSettings(url: url, language: snapshot.document.language)))) + } + } + } +} + // MARK: - Request and notification handling extension SourceKitServer { @@ -259,6 +285,9 @@ extension SourceKitServer { ) } + assert(self.workspace != nil) + self.workspace?.buildSettings.delegate = self + req.reply(InitializeResult(capabilities: ServerCapabilities( textDocumentSync: TextDocumentSyncOptions( openClose: true, @@ -311,7 +340,10 @@ extension SourceKitServer { func openDocument(_ note: Notification, workspace: Workspace) { workspace.documentManager.open(note) - if let service = languageService(for: note.params.textDocument.url, note.params.textDocument.language, in: workspace) { + let textDocument = note.params.textDocument + workspace.buildSettings.registerForChangeNotifications(for: textDocument.url) + + if let service = languageService(for: textDocument.url, textDocument.language, in: workspace) { service.send(note.params) } } @@ -319,7 +351,10 @@ extension SourceKitServer { func closeDocument(_ note: Notification, workspace: Workspace) { workspace.documentManager.close(note) - if let service = workspace.documentService[note.params.textDocument.url] { + let url = note.params.textDocument.url + workspace.buildSettings.unregisterForChangeNotifications(for: url) + + if let service = workspace.documentService[url] { service.send(note.params) } } diff --git a/Sources/SourceKit/clangd/ClangLanguageServer.swift b/Sources/SourceKit/clangd/ClangLanguageServer.swift index 05279645..b0b71476 100644 --- a/Sources/SourceKit/clangd/ClangLanguageServer.swift +++ b/Sources/SourceKit/clangd/ClangLanguageServer.swift @@ -29,8 +29,8 @@ final class ClangLanguageServerShim: LanguageServer { let clang: AbsolutePath? /// Creates a language server for the given client using the sourcekitd dylib at the specified path. - public init(client: Connection, clangd: Connection, buildSystem: BuildSystem, clang: AbsolutePath?) throws { - + public init(client: Connection, clangd: Connection, buildSystem: BuildSystem, + clang: AbsolutePath?) throws { self.clangd = clangd self.buildSystem = buildSystem self.clang = clang @@ -39,6 +39,7 @@ final class ClangLanguageServerShim: LanguageServer { public override func _registerBuiltinHandlers() { _register(ClangLanguageServerShim.initialize) + _register(ClangLanguageServerShim.didChangeConfiguration) _register(ClangLanguageServerShim.openDocument) _register(ClangLanguageServerShim.foldingRange) } @@ -97,9 +98,29 @@ extension ClangLanguageServerShim { } } + // MARK: - Workspace + + func didChangeConfiguration(_ note: Notification) { + switch note.params.settings { + case .clangd: + break + case .documentUpdated(let settings): + updateDocumentSettings(url: settings.url, language: settings.language) + case .unknown: + break + } + } + + // MARK: - Text synchronization + func openDocument(_ note: Notification) { - let url = note.params.textDocument.url - let settings = buildSystem.settings(for: url, note.params.textDocument.language) + let textDocument = note.params.textDocument + updateDocumentSettings(url: textDocument.url, language: textDocument.language) + clangd.send(note.params) + } + + private func updateDocumentSettings(url: URL, language: Language) { + let settings = buildSystem.settings(for: url, language) logAsync(level: settings == nil ? .warning : .debug) { _ in let settingsStr = settings == nil ? "nil" : settings!.compilerArguments.description @@ -111,8 +132,6 @@ extension ClangLanguageServerShim { ClangWorkspaceSettings( compilationDatabaseChanges: [url.path: ClangCompileCommand(settings, clang: clang)])))) } - - clangd.send(note.params) } func foldingRange(_ req: Request) { diff --git a/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift b/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift index eb2aba2b..2eee02cf 100644 --- a/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift +++ b/Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift @@ -31,6 +31,8 @@ public final class SwiftLanguageServer: LanguageServer { var currentDiagnostics: [CachedDiagnostic] = [] + var buildSettingsByFile: [URL: FileBuildSettings] = [:] + let onExit: () -> Void var api: sourcekitd_functions_t { return sourcekitd.api } @@ -56,6 +58,7 @@ public final class SwiftLanguageServer: LanguageServer { _register(SwiftLanguageServer.cancelRequest) _register(SwiftLanguageServer.shutdown) _register(SwiftLanguageServer.exit) + _register(SwiftLanguageServer.didChangeConfiguration) _register(SwiftLanguageServer.openDocument) _register(SwiftLanguageServer.closeDocument) _register(SwiftLanguageServer.changeDocument) @@ -97,7 +100,6 @@ public final class SwiftLanguageServer: LanguageServer { } func handleDocumentUpdate(url: URL) { - guard let snapshot = documentManager.latestSnapshot(url) else { return } @@ -187,6 +189,54 @@ extension SwiftLanguageServer { onExit() } + // MARK: - Workspace + + func didChangeConfiguration(notification: Notification) { + switch notification.params.settings { + case .clangd: + break + case .documentUpdated(let settings): + documentBuildSettingsUpdated(settings.url, language: settings.language) + case .unknown: + break + } + } + + private func documentBuildSettingsUpdated(_ url: URL, language: Language) { + guard let snapshot = documentManager.latestSnapshot(url) else { + return + } + + // Confirm that the build settings actually changed, otherwise we don't + // need to do anything. + let newSettings = buildSystem.settings(for: url, language) + guard buildSettingsByFile[url] != newSettings else { + return + } + buildSettingsByFile[url] = newSettings + + // Close and re-open the document internally to inform sourcekitd to + // update the settings. At the moment there's no better way to do this. + let closeReq = SKRequestDictionary(sourcekitd: sourcekitd) + closeReq[keys.request] = requests.editor_close + closeReq[keys.name] = url.path + _ = self.sourcekitd.sendSync(closeReq) + + let openReq = SKRequestDictionary(sourcekitd: sourcekitd) + openReq[keys.request] = requests.editor_open + openReq[keys.name] = url.path + openReq[keys.sourcetext] = snapshot.text + if let settings = newSettings { + openReq[keys.compilerargs] = settings.compilerArguments + } + + guard let dict = self.sourcekitd.sendSync(openReq).success else { + // Already logged failure. + return + } + publishDiagnostics(response: dict, for: snapshot) + } + // MARK: - Text synchronization func openDocument(_ note: Notification) { @@ -200,8 +250,13 @@ extension SwiftLanguageServer { req[keys.name] = note.params.textDocument.url.path req[keys.sourcetext] = snapshot.text - if let settings = buildSystem.settings(for: snapshot.document.url, snapshot.document.language) { + // If the BuildSystem has settings, cache them internally. + let url = snapshot.document.url + if let settings = buildSystem.settings(for: url, snapshot.document.language) { req[keys.compilerargs] = settings.compilerArguments + buildSettingsByFile[url] = settings + } else { + buildSettingsByFile[url] = nil } guard let dict = self.sourcekitd.sendSync(req).success else { @@ -215,9 +270,15 @@ extension SwiftLanguageServer { func closeDocument(_ note: Notification) { documentManager.close(note) + let url = note.params.textDocument.url + + // Clear the build settings since there's no point in caching + // them for a closed file. + buildSettingsByFile[url] = nil + let req = SKRequestDictionary(sourcekitd: sourcekitd) req[keys.request] = requests.editor_close - req[keys.name] = note.params.textDocument.url.path + req[keys.name] = url.path _ = self.sourcekitd.sendSync(req) } @@ -292,7 +353,8 @@ extension SwiftLanguageServer { skreq[keys.sourcefile] = snapshot.document.url.path skreq[keys.sourcetext] = snapshot.text - if let settings = buildSystem.settings(for: snapshot.document.url, snapshot.document.language) { + // FIXME: SourceKit should probably cache this for us. + if let settings = self.buildSettingsByFile[snapshot.document.url] { skreq[keys.compilerargs] = settings.compilerArguments } @@ -659,8 +721,8 @@ extension SwiftLanguageServer { skreq[keys.offset] = offset skreq[keys.sourcefile] = snapshot.document.url.path - // FIXME: should come from the internal document - if let settings = buildSystem.settings(for: snapshot.document.url, snapshot.document.language) { + // FIXME: SourceKit should probably cache this for us. + if let settings = self.buildSettingsByFile[snapshot.document.url] { skreq[keys.compilerargs] = settings.compilerArguments } diff --git a/Tests/SourceKitTests/BuildSystemTests.swift b/Tests/SourceKitTests/BuildSystemTests.swift new file mode 100644 index 00000000..f33e4416 --- /dev/null +++ b/Tests/SourceKitTests/BuildSystemTests.swift @@ -0,0 +1,235 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SourceKit +import TSCBasic +import LanguageServerProtocol +import SKCore +import SKSupport +import SKTestSupport +import XCTest + +// Workaround ambiguity with Foundation. +typealias LSPNotification = LanguageServerProtocol.Notification + +/// Build system to be used for testing BuildSystem and BuildSystemDelegate functionality with SourceKitServer +/// and other components. +final class TestBuildSystem: BuildSystem { + var indexStorePath: AbsolutePath? = nil + var indexDatabasePath: AbsolutePath? = nil + + weak var delegate: BuildSystemDelegate? + + /// Build settings by file. + var buildSettingsByFile: [URL: FileBuildSettings] = [:] + + /// Toolchains by file. + var toolchainsByFile: [URL: Toolchain] = [:] + + /// Files currently being watched by our delegate. + var watchedFiles: Set = [] + + func settings(for url: URL, _ language: Language) -> FileBuildSettings? { + return buildSettingsByFile[url] + } + + func toolchain(for url: URL, _ language: Language) -> Toolchain? { + return toolchainsByFile[url] + } + + func registerForChangeNotifications(for url: URL) { + watchedFiles.insert(url) + } + + func unregisterForChangeNotifications(for url: URL) { + watchedFiles.remove(url) + } +} + +final class BuildSystemTests: XCTestCase { + + /// Connection and lifetime management for the service. + var testServer: TestSourceKitServer! = nil + + /// The primary interface to make requests to the SourceKitServer. + var sk: TestClient! = nil + + /// The server's workspace data. Accessing this is unsafe if the server does so concurrently. + var workspace: Workspace! = nil + + /// The build system that we use to verify SourceKitServer behavior. + var buildSystem: TestBuildSystem! = nil + + /// Whether clangd exists in the toolchain. + var haveClangd: Bool = false + + override func setUp() { + haveClangd = ToolchainRegistry.shared.toolchains.contains { $0.clangd != nil } + testServer = TestSourceKitServer() + buildSystem = TestBuildSystem() + + self.workspace = Workspace( + rootPath: nil, + clientCapabilities: ClientCapabilities(), + buildSettings: buildSystem, + index: nil, + buildSetup: TestSourceKitServer.buildSetup) + testServer.server!.workspace = workspace + + sk = testServer.client + _ = try! sk.sendSync(InitializeRequest( + processId: nil, + rootPath: nil, + rootURL: nil, + initializationOptions: nil, + capabilities: ClientCapabilities(workspace: nil, textDocument: nil), + trace: .off, + workspaceFolders: nil)) + } + + override func tearDown() { + buildSystem = nil + workspace = nil + sk = nil + testServer = nil + } + + func testClangdDocumentUpdatedBuildSettings() { + guard haveClangd else { return } + + let url = URL(fileURLWithPath: "/file.m") + let args = [url.path, "-DDEBUG"] + let text = """ + #ifdef FOO + static void foo() {} + #endif + + int main() { + foo(); + return 0; + } + """ + + buildSystem.buildSettingsByFile[url] = FileBuildSettings(compilerArguments: args) + + sk.sendNoteSync(DidOpenTextDocument(textDocument: TextDocumentItem( + url: url, + language: .objective_c, + version: 12, + text: text + )), { (note: Notification) in + XCTAssertEqual(note.params.diagnostics.count, 1) + XCTAssertEqual(text, self.workspace.documentManager.latestSnapshot(url)!.text) + }) + + // Modify the build settings and inform the delegate. + // This should trigger a new publish diagnostics and we should no longer have errors. + buildSystem.buildSettingsByFile[url] = FileBuildSettings(compilerArguments: args + ["-DFOO"]) + testServer.server?.fileBuildSettingsChanged([url]) + + let expectation = XCTestExpectation(description: "refresh") + sk.handleNextNotification { (note: Notification) in + XCTAssertEqual(note.params.diagnostics.count, 0) + XCTAssertEqual(text, self.workspace.documentManager.latestSnapshot(url)!.text) + expectation.fulfill() + } + + let result = XCTWaiter.wait(for: [expectation], timeout: 5) + if result != .completed { + fatalError("error \(result) waiting for diagnostics notification") + } + } + + func testSwiftDocumentUpdatedBuildSettings() { + let url = URL(fileURLWithPath: "/a.swift") + let args = FallbackBuildSystem().settings(for: url, .swift)!.compilerArguments + + buildSystem.buildSettingsByFile[url] = FileBuildSettings(compilerArguments: args) + + let text = """ + #if FOO + func foo() {} + #endif + + foo() + """ + sk.sendNoteSync(DidOpenTextDocument(textDocument: TextDocumentItem( + url: url, + language: .swift, + version: 12, + text: text + )), { (note: Notification) in + // Syntactic analysis - no expected errors here. + XCTAssertEqual(note.params.diagnostics.count, 0) + XCTAssertEqual(text, self.workspace.documentManager.latestSnapshot(url)!.text) + }, { (note: Notification) in + // Semantic analysis - expect one error here. + XCTAssertEqual(note.params.diagnostics.count, 1) + }) + + // Modify the build settings and inform the delegate. + // This should trigger a new publish diagnostics and we should no longer have errors. + buildSystem.buildSettingsByFile[url] = FileBuildSettings(compilerArguments: args + ["-DFOO"]) + + let expectation = XCTestExpectation(description: "refresh") + expectation.expectedFulfillmentCount = 2 + sk.handleNextNotification { (note: Notification) in + // Semantic analysis - SourceKit currently caches diagnostics so we still see an error. + XCTAssertEqual(note.params.diagnostics.count, 1) + expectation.fulfill() + } + sk.appendOneShotNotificationHandler { (note: Notification) in + // Semantic analysis - no expected errors here because we fixed the settings. + XCTAssertEqual(note.params.diagnostics.count, 0) + expectation.fulfill() + } + testServer.server?.fileBuildSettingsChanged([url]) + + let result = XCTWaiter.wait(for: [expectation], timeout: 5) + if result != .completed { + fatalError("error \(result) waiting for diagnostics notification") + } + } + + func testSwiftDocumentBuildSettingsChangedFalseAlarm() { + let url = URL(fileURLWithPath: "/a.swift") + + sk.sendNoteSync(DidOpenTextDocument(textDocument: TextDocumentItem( + url: url, + language: .swift, + version: 12, + text: """ + func + """ + )), { (note: Notification) in + XCTAssertEqual(note.params.diagnostics.count, 1) + XCTAssertEqual("func", self.workspace.documentManager.latestSnapshot(url)!.text) + }) + + // Modify the build settings and inform the SourceKitServer. + // This shouldn't trigger new diagnostics since nothing actually changed (false alarm). + testServer.server?.fileBuildSettingsChanged([url]) + + let expectation = XCTestExpectation(description: "refresh doesn't occur") + expectation.isInverted = true + sk.handleNextNotification { (note: Notification) in + XCTAssertEqual(note.params.diagnostics.count, 1) + XCTAssertEqual("func", self.workspace.documentManager.latestSnapshot(url)!.text) + expectation.fulfill() + } + + let result = XCTWaiter.wait(for: [expectation], timeout: 5) + if result != .completed { + fatalError("error \(result) waiting for diagnostics notification") + } + } +} diff --git a/Tests/SourceKitTests/XCTestManifests.swift b/Tests/SourceKitTests/XCTestManifests.swift index 898ae6eb..610d3d10 100644 --- a/Tests/SourceKitTests/XCTestManifests.swift +++ b/Tests/SourceKitTests/XCTestManifests.swift @@ -1,6 +1,17 @@ #if !canImport(ObjectiveC) import XCTest +extension BuildSystemTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__BuildSystemTests = [ + ("testClangdDocumentUpdatedBuildSettings", testClangdDocumentUpdatedBuildSettings), + ("testSwiftDocumentBuildSettingsChangedFalseAlarm", testSwiftDocumentBuildSettingsChangedFalseAlarm), + ("testSwiftDocumentUpdatedBuildSettings", testSwiftDocumentUpdatedBuildSettings), + ] +} + extension CodeActionTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -119,6 +130,7 @@ extension SwiftPMIntegrationTests { public func __allTests() -> [XCTestCaseEntry] { return [ + testCase(BuildSystemTests.__allTests__BuildSystemTests), testCase(CodeActionTests.__allTests__CodeActionTests), testCase(DocumentColorTests.__allTests__DocumentColorTests), testCase(DocumentSymbolTest.__allTests__DocumentSymbolTest),