Skip to content

Commit bda0cc4

Browse files
authored
Merge pull request #212 from DavidGoldman/dependencyhandling
Work on implementing dependenciesUpdated part of Build System integration
2 parents c363a39 + aafc5f3 commit bda0cc4

File tree

9 files changed

+182
-31
lines changed

9 files changed

+182
-31
lines changed

Sources/SKCore/BuildSystemDelegate.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@ public protocol BuildSystemDelegate: AnyObject {
2121
/// interest.
2222
func buildTargetsChanged(_ changes: [BuildTargetEvent])
2323

24-
/// Notify the delegate that the given files' build settings have changed.
24+
/// Notify the delegate that the given files' build settings have changed. If the given set is
25+
/// empty, assume that all open files are affected.
2526
///
2627
/// The callee should request new build settings for any of the given files
2728
/// that they are interested in.
2829
func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>)
30+
31+
/// Notify the delegate that the dependencies of the given files have changed and that ASTs
32+
/// may need to be refreshed. If the given set is empty, assume that all open files are affected.
33+
///
34+
/// The callee should refresh ASTs unless it is able to determine that a refresh is not necessary.
35+
func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>)
2936
}

Sources/SKTestSupport/SKTibsTestWorkspace.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,34 @@ extension XCTestCase {
121121

122122
return workspace
123123
}
124+
125+
/// Constructs a mutable SKTibsTestWorkspace for the given test from INPUTS.
126+
///
127+
/// The resulting workspace allow edits and is not persistent.
128+
public func mutableSourceKitTibsTestWorkspace(
129+
name: String,
130+
clientCapabilities: ClientCapabilities = .init(),
131+
tmpDir: URL? = nil,
132+
testFile: String = #file
133+
) throws -> SKTibsTestWorkspace? {
134+
let testDirName = testDirectoryName
135+
let workspace = try SKTibsTestWorkspace(
136+
projectDir: inputsDirectory(testFile: testFile)
137+
.appendingPathComponent(name, isDirectory: true),
138+
tmpDir: tmpDir ?? URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
139+
.appendingPathComponent("sk-test-data/\(testDirName)", isDirectory: true),
140+
toolchain: ToolchainRegistry.shared.default!,
141+
clientCapabilities: clientCapabilities)
142+
143+
if workspace.builder.targets.contains(where: { target in !target.clangTUs.isEmpty })
144+
&& !workspace.builder.toolchain.clangHasIndexSupport {
145+
fputs("warning: skipping test because '\(workspace.builder.toolchain.clang.path)' does not " +
146+
"have indexstore support; use swift-clang\n", stderr)
147+
return nil
148+
}
149+
150+
return workspace
151+
}
124152
}
125153

126154
extension TestLocation {

Sources/SourceKit/SourceKitServer.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -245,24 +245,44 @@ extension SourceKitServer: BuildSystemDelegate {
245245
// TODO: do something with these changes once build target support is in place
246246
}
247247

248+
private func affectedOpenDocumentsForChangeSet(
249+
_ changes: Set<DocumentURI>,
250+
_ documentManager: DocumentManager
251+
) -> Set<DocumentURI> {
252+
// An empty change set is treated as if all open files have been modified.
253+
guard !changes.isEmpty else {
254+
return documentManager.openDocuments
255+
}
256+
return documentManager.openDocuments.intersection(changes)
257+
}
258+
248259
public func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) {
249260
guard let workspace = self.workspace else {
250261
return
251262
}
252263
let documentManager = workspace.documentManager
253-
let openDocuments = documentManager.openDocuments
254-
for uri in changedFiles {
255-
guard openDocuments.contains(uri) else {
256-
continue
257-
}
258-
264+
for uri in self.affectedOpenDocumentsForChangeSet(changedFiles, documentManager) {
259265
log("Build settings changed for opened file \(uri)")
260266
if let snapshot = documentManager.latestSnapshot(uri),
261267
let service = languageService(for: uri, snapshot.document.language, in: workspace) {
262268
service.documentUpdatedBuildSettings(uri, language: snapshot.document.language)
263269
}
264270
}
265271
}
272+
273+
public func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) {
274+
guard let workspace = self.workspace else {
275+
return
276+
}
277+
let documentManager = workspace.documentManager
278+
for uri in self.affectedOpenDocumentsForChangeSet(changedFiles, documentManager) {
279+
log("Dependencies updated for opened file \(uri)")
280+
if let snapshot = documentManager.latestSnapshot(uri),
281+
let service = languageService(for: uri, snapshot.document.language, in: workspace) {
282+
service.documentDependenciesUpdated(uri, language: snapshot.document.language)
283+
}
284+
}
285+
}
266286
}
267287

268288
// MARK: - Request and notification handling

Sources/SourceKit/ToolchainLanguageServer.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ public protocol ToolchainLanguageServer: AnyObject {
2828
func changeDocument(_ note: DidChangeTextDocumentNotification)
2929
func willSaveDocument(_ note: WillSaveTextDocumentNotification)
3030
func didSaveDocument(_ note: DidSaveTextDocumentNotification)
31+
32+
// MARK: - Build System Integration
33+
3134
func documentUpdatedBuildSettings(_ uri: DocumentURI, language: Language)
35+
func documentDependenciesUpdated(_ uri: DocumentURI, language: Language)
3236

3337
// MARK: - Text Document
3438

Sources/SourceKit/clangd/ClangLanguageServer.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ extension ClangLanguageServerShim {
106106

107107
}
108108

109+
// MARK: - Build System Integration
110+
109111
public func documentUpdatedBuildSettings(_ uri: DocumentURI, language: Language) {
110112
guard let url = uri.fileURL else {
111113
// FIXME: The clang workspace can probably be reworked to support non-file URIs.
@@ -126,6 +128,15 @@ extension ClangLanguageServerShim {
126128
}
127129
}
128130

131+
public func documentDependenciesUpdated(_ uri: DocumentURI, language: Language) {
132+
// In order to tell clangd to reload an AST, we send it an empty `didChangeTextDocument`. This
133+
// works well for us as the moment since clangd ignores the document version.
134+
let note = DidChangeTextDocumentNotification(
135+
textDocument: VersionedTextDocumentIdentifier(uri, version: nil),
136+
contentChanges: [])
137+
clangd.send(note)
138+
}
139+
129140
// MARK: - Text Document
130141

131142
func completion(_ req: Request<CompletionRequest>) {

Sources/SourceKit/sourcekitd/SwiftLanguageServer.swift

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,32 @@ extension SwiftLanguageServer {
177177
onExit()
178178
}
179179

180-
// MARK: - Workspace
180+
// MARK: - Build System Integration
181+
182+
/// Should be called on self.queue.
183+
private func reopenDocument(_ snapshot: DocumentSnapshot, _ settings: FileBuildSettings?) {
184+
let keys = self.keys
185+
let path = snapshot.document.uri.pseudoPath
186+
187+
let closeReq = SKRequestDictionary(sourcekitd: self.sourcekitd)
188+
closeReq[keys.request] = self.requests.editor_close
189+
closeReq[keys.name] = path
190+
_ = self.sourcekitd.sendSync(closeReq)
191+
192+
let openReq = SKRequestDictionary(sourcekitd: self.sourcekitd)
193+
openReq[keys.request] = self.requests.editor_open
194+
openReq[keys.name] = path
195+
openReq[keys.sourcetext] = snapshot.text
196+
if let settings = settings {
197+
openReq[keys.compilerargs] = settings.compilerArguments
198+
}
199+
200+
guard let dict = self.sourcekitd.sendSync(openReq).success else {
201+
// Already logged failure.
202+
return
203+
}
204+
self.publishDiagnostics(response: dict, for: snapshot)
205+
}
181206

182207
public func documentUpdatedBuildSettings(_ uri: DocumentURI, language: Language) {
183208
self.queue.async {
@@ -193,28 +218,21 @@ extension SwiftLanguageServer {
193218
}
194219
self.buildSettingsByFile[uri] = newSettings
195220

196-
let keys = self.keys
197-
198221
// Close and re-open the document internally to inform sourcekitd to
199222
// update the settings. At the moment there's no better way to do this.
200-
let closeReq = SKRequestDictionary(sourcekitd: self.sourcekitd)
201-
closeReq[keys.request] = self.requests.editor_close
202-
closeReq[keys.name] = uri.pseudoPath
203-
_ = self.sourcekitd.sendSync(closeReq)
204-
205-
let openReq = SKRequestDictionary(sourcekitd: self.sourcekitd)
206-
openReq[keys.request] = self.requests.editor_open
207-
openReq[keys.name] = uri.pseudoPath
208-
openReq[keys.sourcetext] = snapshot.text
209-
if let settings = newSettings {
210-
openReq[keys.compilerargs] = settings.compilerArguments
211-
}
223+
self.reopenDocument(snapshot, newSettings)
224+
}
225+
}
212226

213-
guard let dict = self.sourcekitd.sendSync(openReq).success else {
214-
// Already logged failure.
227+
public func documentDependenciesUpdated(_ uri: DocumentURI, language: Language) {
228+
self.queue.async {
229+
guard let snapshot = self.documentManager.latestSnapshot(uri) else {
215230
return
216231
}
217-
self.publishDiagnostics(response: dict, for: snapshot)
232+
233+
// Forcefully reopen the document since the `BuildSystem` has informed us
234+
// that the dependencies have changed and the AST needs to be reloaded.
235+
self.reopenDocument(snapshot, self.buildSettingsByFile[uri])
218236
}
219237
}
220238

Tests/SKCoreTests/BuildServerBuildSystemTests.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ final class BuildServerBuildSystemTests: XCTestCase {
5757

5858
let fileUrl = URL(fileURLWithPath: "/some/file/path")
5959
let expectation = XCTestExpectation(description: "\(fileUrl) settings updated")
60-
let buildSystemDelegate = TestDelegate(fileExpectations: [DocumentURI(fileUrl): expectation])
60+
let buildSystemDelegate = TestDelegate(settingsExpectations: [DocumentURI(fileUrl): expectation])
6161
buildSystem.delegate = buildSystemDelegate
6262
buildSystem.registerForChangeNotifications(for: DocumentURI(fileUrl))
6363

@@ -185,13 +185,16 @@ final class BuildServerBuildSystemTests: XCTestCase {
185185

186186
final class TestDelegate: BuildSystemDelegate {
187187

188-
let fileExpectations: [DocumentURI:XCTestExpectation]
188+
let settingsExpectations: [DocumentURI:XCTestExpectation]
189189
let targetExpectations: [BuildTargetEvent:XCTestExpectation]
190+
let dependenciesUpdatedExpectations: [DocumentURI:XCTestExpectation]
190191

191-
public init(fileExpectations: [DocumentURI:XCTestExpectation] = [:],
192-
targetExpectations: [BuildTargetEvent:XCTestExpectation] = [:]) {
193-
self.fileExpectations = fileExpectations
192+
public init(settingsExpectations: [DocumentURI:XCTestExpectation] = [:],
193+
targetExpectations: [BuildTargetEvent:XCTestExpectation] = [:],
194+
dependenciesUpdatedExpectations: [DocumentURI:XCTestExpectation] = [:]) {
195+
self.settingsExpectations = settingsExpectations
194196
self.targetExpectations = targetExpectations
197+
self.dependenciesUpdatedExpectations = dependenciesUpdatedExpectations
195198
}
196199

197200
func buildTargetsChanged(_ changes: [BuildTargetEvent]) {
@@ -202,7 +205,13 @@ final class TestDelegate: BuildSystemDelegate {
202205

203206
func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) {
204207
for uri in changedFiles {
205-
fileExpectations[uri]?.fulfill()
208+
settingsExpectations[uri]?.fulfill()
209+
}
210+
}
211+
212+
public func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) {
213+
for uri in changedFiles {
214+
dependenciesUpdatedExpectations[uri]?.fulfill()
206215
}
207216
}
208217
}

Tests/SourceKitTests/SourceKitTests.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,57 @@ final class SKTests: XCTestCase {
182182
deprecated: false),
183183
]))
184184
}
185+
186+
func testDependenciesUpdatedSwiftTibs() throws {
187+
guard let ws = try mutableSourceKitTibsTestWorkspace(name: "SwiftModules") else { return }
188+
guard let server = ws.testServer.server else {
189+
XCTFail("Unable to fetch SourceKitServer to notify for build system events.")
190+
return
191+
}
192+
193+
let moduleRef = ws.testLoc("aaa:call:c")
194+
let startExpectation = XCTestExpectation(description: "initial diagnostics")
195+
startExpectation.expectedFulfillmentCount = 2
196+
ws.sk.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
197+
// Semantic analysis: no errors expected here.
198+
XCTAssertEqual(note.params.diagnostics.count, 0)
199+
startExpectation.fulfill()
200+
}
201+
ws.sk.appendOneShotNotificationHandler { (note: Notification<PublishDiagnosticsNotification>) in
202+
// Semantic analysis: expect module import error.
203+
XCTAssertEqual(note.params.diagnostics.count, 1)
204+
if let diagnostic = note.params.diagnostics.first {
205+
XCTAssert(diagnostic.message.contains("no such module"),
206+
"expected module import error but found \"\(diagnostic.message)\"")
207+
}
208+
startExpectation.fulfill()
209+
}
210+
211+
try ws.openDocument(moduleRef.url, language: .swift)
212+
let started = XCTWaiter.wait(for: [startExpectation], timeout: 3)
213+
if started != .completed {
214+
fatalError("error \(started) waiting for initial diagnostics notification")
215+
}
216+
217+
try ws.buildAndIndex()
218+
219+
let finishExpectation = XCTestExpectation(description: "post-build diagnostics")
220+
finishExpectation.expectedFulfillmentCount = 2
221+
ws.sk.handleNextNotification { (note: Notification<PublishDiagnosticsNotification>) in
222+
// Semantic analysis - SourceKit currently caches diagnostics so we still see an error.
223+
XCTAssertEqual(note.params.diagnostics.count, 1)
224+
finishExpectation.fulfill()
225+
}
226+
ws.sk.appendOneShotNotificationHandler { (note: Notification<PublishDiagnosticsNotification>) in
227+
// Semantic analysis: no more errors expected, import should resolve since we built.
228+
XCTAssertEqual(note.params.diagnostics.count, 0)
229+
finishExpectation.fulfill()
230+
}
231+
server.filesDependenciesUpdated([DocumentURI(moduleRef.url)])
232+
233+
let finished = XCTWaiter.wait(for: [finishExpectation], timeout: 3)
234+
if finished != .completed {
235+
fatalError("error \(finished) waiting for post-build diagnostics notification")
236+
}
237+
}
185238
}

Tests/SourceKitTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ extension SKTests {
122122
// to regenerate.
123123
static let __allTests__SKTests = [
124124
("testCodeCompleteSwiftTibs", testCodeCompleteSwiftTibs),
125+
("testDependenciesUpdatedSwiftTibs", testDependenciesUpdatedSwiftTibs),
125126
("testIndexShutdown", testIndexShutdown),
126127
("testIndexSwiftModules", testIndexSwiftModules),
127128
("testInitJSON", testInitJSON),

0 commit comments

Comments
 (0)