mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-06-24 12:21:58 +02:00
f9cbba2c2c
Migrate remaining AtomicBool / AtomicUInt32 usages in test cases to ThreadSafeBox / Atomic. - Closure-captured `AtomicBool` flags become `ThreadSafeBox<Bool>` for now. We will revisit this when eliminating ThreadSafeBox. - One `AtomicUInt32` counter (SourceKitDRegistryTests.swift's `nextToken`) to `Atomic<UInt32>`.
1577 lines
50 KiB
Swift
1577 lines
50 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2022 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 BuildServerIntegration
|
|
@_spi(SourceKitLSP) import BuildServerProtocol
|
|
import Foundation
|
|
@_spi(SourceKitLSP) import LanguageServerProtocol
|
|
@_spi(SourceKitLSP) import SKLogging
|
|
import SKOptions
|
|
import SKTestSupport
|
|
import SemanticIndex
|
|
import SourceKitLSP
|
|
import SwiftExtensions
|
|
import TSCBasic
|
|
import ToolchainRegistry
|
|
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
|
|
import XCTest
|
|
|
|
final class WorkspaceTests: SourceKitLSPTestCase {
|
|
func testMultipleSwiftPMWorkspaces() async throws {
|
|
try await SkipUnless.sourcekitdSupportsPlugin()
|
|
|
|
// The package manifest is the same for both packages we open.
|
|
let packageManifest = """
|
|
// swift-tools-version: 5.7
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "MyLibrary",
|
|
targets: [
|
|
.target(name: "MyLibrary"),
|
|
.executableTarget(name: "MyExec", dependencies: ["MyLibrary"])
|
|
]
|
|
)
|
|
"""
|
|
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
// PackageA
|
|
"PackageA/Sources/MyLibrary/libA.swift": """
|
|
public struct FancyLib {
|
|
public init() {}
|
|
public func sayHello() {}
|
|
}
|
|
""",
|
|
|
|
"PackageA/Sources/MyExec/execA.swift": """
|
|
import MyLibrary
|
|
|
|
FancyLib().1️⃣sayHello()
|
|
""",
|
|
|
|
"PackageA/Package.swift": packageManifest,
|
|
|
|
// PackageB
|
|
"PackageB/Sources/MyLibrary/libB.swift": """
|
|
public struct Lib {
|
|
public init() {}
|
|
public func foo() {}
|
|
}
|
|
""",
|
|
"PackageB/Sources/MyExec/execB.swift": """
|
|
import MyLibrary
|
|
Lib().2️⃣foo()
|
|
""",
|
|
"PackageB/Package.swift": packageManifest,
|
|
],
|
|
workspaces: { scratchDir in
|
|
return [
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "PackageA"))),
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "PackageB"))),
|
|
]
|
|
},
|
|
enableBackgroundIndexing: true
|
|
)
|
|
try await project.testClient.send(SynchronizeRequest(index: true))
|
|
|
|
let (bUri, bPositions) = try project.openDocument("execB.swift")
|
|
|
|
let completions = try await project.testClient.send(
|
|
CompletionRequest(textDocument: TextDocumentIdentifier(bUri), position: bPositions["2️⃣"])
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
completions.items.clearingUnstableValues,
|
|
[
|
|
CompletionItem(
|
|
label: "foo()",
|
|
kind: .method,
|
|
detail: "Void",
|
|
deprecated: false,
|
|
filterText: "foo()",
|
|
insertText: "foo()",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(bPositions["2️⃣"]), newText: "foo()")
|
|
)
|
|
),
|
|
CompletionItem(
|
|
label: "self",
|
|
kind: .keyword,
|
|
detail: "Lib",
|
|
deprecated: false,
|
|
filterText: "self",
|
|
insertText: "self",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(bPositions["2️⃣"]), newText: "self")
|
|
)
|
|
),
|
|
]
|
|
)
|
|
|
|
let (aUri, aPositions) = try project.openDocument("execA.swift")
|
|
|
|
let otherCompletions = try await project.testClient.send(
|
|
CompletionRequest(textDocument: TextDocumentIdentifier(aUri), position: aPositions["1️⃣"])
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
otherCompletions.items.clearingUnstableValues,
|
|
[
|
|
CompletionItem(
|
|
label: "sayHello()",
|
|
kind: .method,
|
|
detail: "Void",
|
|
documentation: nil,
|
|
deprecated: false,
|
|
filterText: "sayHello()",
|
|
insertText: "sayHello()",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(aPositions["1️⃣"]), newText: "sayHello()")
|
|
)
|
|
),
|
|
CompletionItem(
|
|
label: "self",
|
|
kind: LanguageServerProtocol.CompletionItemKind(rawValue: 14),
|
|
detail: "FancyLib",
|
|
documentation: nil,
|
|
deprecated: false,
|
|
filterText: "self",
|
|
insertText: "self",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(aPositions["1️⃣"]), newText: "self")
|
|
)
|
|
),
|
|
]
|
|
)
|
|
}
|
|
|
|
func testOpenPackageManifestInMultiSwiftPMWorkspaceSetup() async throws {
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
// PackageA
|
|
"PackageA/Sources/MyLibrary/libA.swift": "",
|
|
"PackageA/Package.swift": SwiftPMTestProject.defaultPackageManifest,
|
|
|
|
// PackageB
|
|
"PackageB/Sources/MyLibrary/libB.swift": "",
|
|
"PackageB/Package.swift": SwiftPMTestProject.defaultPackageManifest,
|
|
],
|
|
workspaces: { scratchDir in
|
|
return [
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir)),
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "PackageA"))),
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "PackageB"))),
|
|
]
|
|
}
|
|
)
|
|
|
|
let bPackageManifestUri = DocumentURI(
|
|
project.scratchDirectory.appending(components: "PackageB", "Package.swift")
|
|
)
|
|
|
|
project.testClient.openDocument(SwiftPMTestProject.defaultPackageManifest, uri: bPackageManifestUri)
|
|
|
|
// Ensure that we get proper build settings for Package.swift and no error about `No such module: PackageDescription`
|
|
let diags = try await project.testClient.send(
|
|
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(bPackageManifestUri))
|
|
)
|
|
XCTAssertEqual(diags.fullReport?.items, [])
|
|
}
|
|
|
|
func testCorrectWorkspaceForPackageSwiftInMultiSwiftPMWorkspaceSetup() async throws {
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
// PackageA
|
|
"PackageA/Sources/MyLibrary/libA.swift": "",
|
|
"PackageA/Package.swift": SwiftPMTestProject.defaultPackageManifest,
|
|
|
|
// PackageB
|
|
"PackageB/Sources/MyLibrary/libB.swift": "",
|
|
"PackageB/Package.swift": SwiftPMTestProject.defaultPackageManifest,
|
|
],
|
|
workspaces: { scratchDir in
|
|
return [
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir)),
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "PackageA"))),
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "PackageB"))),
|
|
]
|
|
}
|
|
)
|
|
|
|
let pkgA = DocumentURI(
|
|
project.scratchDirectory
|
|
.appending(components: "PackageA", "Package.swift")
|
|
)
|
|
|
|
let pkgB = DocumentURI(
|
|
project.scratchDirectory
|
|
.appending(components: "PackageB", "Package.swift")
|
|
)
|
|
|
|
assertEqual(
|
|
await project.testClient.server.workspaceForDocument(uri: pkgA)?.rootUri,
|
|
DocumentURI(project.scratchDirectory.appending(component: "PackageA"))
|
|
)
|
|
|
|
assertEqual(
|
|
await project.testClient.server.workspaceForDocument(uri: pkgB)?.rootUri,
|
|
DocumentURI(project.scratchDirectory.appending(component: "PackageB"))
|
|
)
|
|
}
|
|
|
|
func testSwiftPMPackageInSubfolder() async throws {
|
|
try await SkipUnless.sourcekitdSupportsPlugin()
|
|
|
|
let packageManifest = """
|
|
// swift-tools-version: 5.7
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "MyLibrary",
|
|
targets: [
|
|
.target(name: "MyLibrary"),
|
|
.executableTarget(name: "MyExec", dependencies: ["MyLibrary"])
|
|
]
|
|
)
|
|
"""
|
|
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
// PackageA
|
|
"PackageA/Sources/MyLibrary/libA.swift": """
|
|
public struct FancyLib {
|
|
public init() {}
|
|
public func sayHello() {}
|
|
}
|
|
""",
|
|
|
|
"PackageA/Sources/MyExec/execA.swift": """
|
|
import MyLibrary
|
|
|
|
FancyLib().1️⃣sayHello()
|
|
""",
|
|
|
|
"PackageA/Package.swift": packageManifest,
|
|
],
|
|
enableBackgroundIndexing: true
|
|
)
|
|
|
|
let (uri, positions) = try project.openDocument("execA.swift")
|
|
|
|
try await project.testClient.send(SynchronizeRequest(index: true))
|
|
|
|
let otherCompletions = try await project.testClient.send(
|
|
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
otherCompletions.items.clearingUnstableValues,
|
|
[
|
|
CompletionItem(
|
|
label: "sayHello()",
|
|
kind: .method,
|
|
detail: "Void",
|
|
documentation: nil,
|
|
deprecated: false,
|
|
filterText: "sayHello()",
|
|
insertText: "sayHello()",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(positions["1️⃣"]), newText: "sayHello()")
|
|
)
|
|
),
|
|
CompletionItem(
|
|
label: "self",
|
|
kind: LanguageServerProtocol.CompletionItemKind(rawValue: 14),
|
|
detail: "FancyLib",
|
|
documentation: nil,
|
|
deprecated: false,
|
|
filterText: "self",
|
|
insertText: "self",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(positions["1️⃣"]), newText: "self")
|
|
)
|
|
),
|
|
]
|
|
)
|
|
}
|
|
|
|
func testNestedSwiftPMWorkspacesWithoutDedicatedWorkspaceFolder() async throws {
|
|
try await SkipUnless.sourcekitdSupportsPlugin()
|
|
|
|
// The package manifest is the same for both packages we open.
|
|
let packageManifest = """
|
|
// swift-tools-version: 5.7
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "MyLibrary",
|
|
targets: [
|
|
.target(name: "MyLibrary"),
|
|
.executableTarget(name: "MyExec", dependencies: ["MyLibrary"])
|
|
]
|
|
)
|
|
"""
|
|
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
// PackageA
|
|
"PackageA/Sources/MyLibrary/libA.swift": """
|
|
public struct FancyLib {
|
|
public init() {}
|
|
public func sayHello() {}
|
|
}
|
|
""",
|
|
|
|
"PackageA/Sources/MyExec/execA.swift": """
|
|
import MyLibrary
|
|
|
|
FancyLib().1️⃣sayHello()
|
|
""",
|
|
|
|
"PackageA/Package.swift": packageManifest,
|
|
|
|
// PackageB
|
|
"Sources/MyLibrary/libB.swift": """
|
|
public struct Lib {
|
|
public init() {}
|
|
public func foo() {}
|
|
}
|
|
""",
|
|
"Sources/MyExec/execB.swift": """
|
|
import MyLibrary
|
|
Lib().2️⃣foo()
|
|
""",
|
|
"Package.swift": packageManifest,
|
|
],
|
|
enableBackgroundIndexing: true
|
|
)
|
|
|
|
try await project.testClient.send(SynchronizeRequest(index: true))
|
|
|
|
let (bUri, bPositions) = try project.openDocument("execB.swift")
|
|
|
|
let completions = try await project.testClient.send(
|
|
CompletionRequest(textDocument: TextDocumentIdentifier(bUri), position: bPositions["2️⃣"])
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
completions.items.clearingUnstableValues,
|
|
[
|
|
CompletionItem(
|
|
label: "foo()",
|
|
kind: .method,
|
|
detail: "Void",
|
|
deprecated: false,
|
|
filterText: "foo()",
|
|
insertText: "foo()",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(bPositions["2️⃣"]), newText: "foo()")
|
|
)
|
|
),
|
|
CompletionItem(
|
|
label: "self",
|
|
kind: .keyword,
|
|
detail: "Lib",
|
|
deprecated: false,
|
|
filterText: "self",
|
|
insertText: "self",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(bPositions["2️⃣"]), newText: "self")
|
|
)
|
|
),
|
|
]
|
|
)
|
|
|
|
let (aUri, aPositions) = try project.openDocument("execA.swift")
|
|
|
|
try await project.testClient.send(SynchronizeRequest(index: true))
|
|
|
|
let otherCompletions = try await project.testClient.send(
|
|
CompletionRequest(textDocument: TextDocumentIdentifier(aUri), position: aPositions["1️⃣"])
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
otherCompletions.items.clearingUnstableValues,
|
|
[
|
|
CompletionItem(
|
|
label: "sayHello()",
|
|
kind: .method,
|
|
detail: "Void",
|
|
documentation: nil,
|
|
deprecated: false,
|
|
filterText: "sayHello()",
|
|
insertText: "sayHello()",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(aPositions["1️⃣"]), newText: "sayHello()")
|
|
)
|
|
),
|
|
CompletionItem(
|
|
label: "self",
|
|
kind: LanguageServerProtocol.CompletionItemKind(rawValue: 14),
|
|
detail: "FancyLib",
|
|
documentation: nil,
|
|
deprecated: false,
|
|
filterText: "self",
|
|
insertText: "self",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(range: Range(aPositions["1️⃣"]), newText: "self")
|
|
)
|
|
),
|
|
]
|
|
)
|
|
}
|
|
|
|
func testMultipleClangdWorkspaces() async throws {
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
"WorkspaceA/main.cpp": """
|
|
#if FOO
|
|
void 1️⃣foo2️⃣() {}
|
|
#else
|
|
void foo() {}
|
|
#endif
|
|
|
|
int main() {
|
|
3️⃣foo4️⃣();
|
|
}
|
|
""",
|
|
"WorkspaceA/compile_flags.txt": """
|
|
-DFOO
|
|
""",
|
|
"WorkspaceB/test.m": "",
|
|
],
|
|
workspaces: { scratchDir in
|
|
return [
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "WorkspaceA"))),
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "WorkspaceB"))),
|
|
]
|
|
},
|
|
usePullDiagnostics: false
|
|
)
|
|
|
|
_ = try project.openDocument("test.m")
|
|
|
|
let diags = try await project.testClient.nextDiagnosticsNotification()
|
|
XCTAssertEqual(diags.diagnostics.count, 0)
|
|
|
|
let (mainUri, positions) = try project.openDocument("main.cpp")
|
|
|
|
let highlightRequest = DocumentHighlightRequest(
|
|
textDocument: TextDocumentIdentifier(mainUri),
|
|
position: positions["3️⃣"]
|
|
)
|
|
let highlightResponse = try await project.testClient.send(highlightRequest)
|
|
XCTAssertEqual(
|
|
highlightResponse,
|
|
[
|
|
DocumentHighlight(range: positions["1️⃣"]..<positions["2️⃣"], kind: .text),
|
|
DocumentHighlight(range: positions["3️⃣"]..<positions["4️⃣"], kind: .text),
|
|
]
|
|
)
|
|
}
|
|
|
|
func testRecomputeFileWorkspaceMembershipOnPackageSwiftChange() async throws {
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
"PackageA/Sources/MyLibrary/libA.swift": "",
|
|
"PackageA/Package.swift": SwiftPMTestProject.defaultPackageManifest,
|
|
|
|
"PackageB/Sources/MyLibrary/libB.swift": """
|
|
public struct Lib {
|
|
public func foo() {}
|
|
public init() {}
|
|
}
|
|
""",
|
|
"PackageB/Sources/MyExec/main.swift": """
|
|
import MyLibrary
|
|
|
|
Lib().1️⃣
|
|
""",
|
|
"PackageB/Package.swift": SwiftPMTestProject.defaultPackageManifest,
|
|
],
|
|
workspaces: { scratchDir in
|
|
return [
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "PackageA", directoryHint: .isDirectory))),
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "PackageB", directoryHint: .isDirectory))),
|
|
]
|
|
}
|
|
)
|
|
|
|
let (mainUri, _) = try project.openDocument("main.swift")
|
|
|
|
// We open PackageA first. Thus, MyExec/main (which is a file in PackageB that hasn't been added to Package.swift
|
|
// yet) will belong to PackageA by default (because it provides fallback build settings for it).
|
|
assertEqual(
|
|
await project.testClient.server.workspaceForDocument(uri: mainUri)?.rootUri,
|
|
DocumentURI(project.scratchDirectory.appending(component: "PackageA", directoryHint: .isDirectory))
|
|
)
|
|
|
|
// Add the MyExec target to PackageB/Package.swift
|
|
let newPackageManifest = """
|
|
// swift-tools-version: 5.7
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "MyLibrary",
|
|
targets: [
|
|
.target(name: "MyLibrary"),
|
|
.executableTarget(name: "MyExec", dependencies: ["MyLibrary"])
|
|
]
|
|
)
|
|
"""
|
|
|
|
let packageBManifestPath = project.scratchDirectory
|
|
.appending(components: "PackageB", "Package.swift")
|
|
|
|
try await newPackageManifest.writeWithRetry(to: packageBManifestPath)
|
|
project.testClient.send(
|
|
DidChangeWatchedFilesNotification(changes: [
|
|
FileEvent(uri: DocumentURI(packageBManifestPath), type: .changed)
|
|
])
|
|
)
|
|
// Ensure that the DidChangeWatchedFilesNotification is handled before we continue.
|
|
_ = try await project.testClient.send(SynchronizeRequest())
|
|
|
|
// After updating PackageB/Package.swift, PackageB can provide proper build settings for MyExec/main.swift and
|
|
// thus workspace membership should switch to PackageB.
|
|
|
|
// Updating the build settings takes a few seconds. Send code completion requests every second until we receive correct results.
|
|
let packageBRootUri = DocumentURI(
|
|
project.scratchDirectory.appending(component: "PackageB", directoryHint: .isDirectory)
|
|
)
|
|
try await repeatUntilExpectedResult {
|
|
await project.testClient.server.workspaceForDocument(uri: mainUri)?.rootUri == packageBRootUri
|
|
}
|
|
}
|
|
|
|
func testMixedPackage() async throws {
|
|
try await SkipUnless.sourcekitdSupportsPlugin()
|
|
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"clib/include/clib.h": """
|
|
#ifndef CLIB_H
|
|
#define CLIB_H
|
|
|
|
void clib_func(void);
|
|
void clib_other(void);
|
|
|
|
#endif // CLIB_H
|
|
""",
|
|
"clib/clib.c": """
|
|
#include "clib.h"
|
|
|
|
void clib_func(void) {1️⃣}
|
|
""",
|
|
"lib/lib.swift": """
|
|
public struct Lib {
|
|
public func foo() {}
|
|
public init() {}
|
|
}
|
|
2️⃣
|
|
""",
|
|
],
|
|
manifest: """
|
|
let package = Package(
|
|
name: "MyLibrary",
|
|
targets: [
|
|
.target(name: "lib", dependencies: []),
|
|
.target(name: "clib", dependencies: []),
|
|
]
|
|
)
|
|
"""
|
|
)
|
|
|
|
let (swiftUri, swiftPositions) = try project.openDocument("lib.swift")
|
|
let (cUri, cPositions) = try project.openDocument("clib.c")
|
|
|
|
let cCompletions = try await project.testClient.send(
|
|
CompletionRequest(textDocument: TextDocumentIdentifier(cUri), position: cPositions["1️⃣"])
|
|
)
|
|
XCTAssertGreaterThanOrEqual(cCompletions.items.count, 0)
|
|
|
|
let swiftCompletions = try await project.testClient.send(
|
|
CompletionRequest(textDocument: TextDocumentIdentifier(swiftUri), position: swiftPositions["2️⃣"])
|
|
)
|
|
XCTAssertGreaterThanOrEqual(swiftCompletions.items.count, 0)
|
|
}
|
|
|
|
func testChangeWorkspaceFolders() async throws {
|
|
try await SkipUnless.sourcekitdSupportsPlugin()
|
|
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
"subdir/Sources/otherPackage/otherPackage.swift": """
|
|
import package
|
|
|
|
func test() {
|
|
Package().1️⃣helloWorld()
|
|
}
|
|
""",
|
|
"subdir/Sources/package/package.swift": """
|
|
public struct Package {
|
|
public init() {}
|
|
|
|
public func helloWorld() {
|
|
print("Hello world!")
|
|
}
|
|
}
|
|
""",
|
|
"subdir/Package.swift": """
|
|
// swift-tools-version: 5.5
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "package",
|
|
products: [
|
|
.library(name: "package", targets: ["package"]),
|
|
.library(name: "otherPackage", targets: ["otherPackage"]),
|
|
],
|
|
targets: [
|
|
.target(
|
|
name: "package",
|
|
dependencies: []
|
|
),
|
|
.target(
|
|
name: "otherPackage",
|
|
dependencies: ["package"]
|
|
),
|
|
]
|
|
)
|
|
""",
|
|
],
|
|
workspaces: { scratchDir in
|
|
return [
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "fake")))
|
|
]
|
|
},
|
|
// We don't want to test behavior based on fallback settings. Increase the buildSettingsTimeout to ensure we always get proper build settings.
|
|
options: .testDefault(buildSettingsTimeout: defaultTimeoutDuration)
|
|
)
|
|
|
|
let packageDir = try project.uri(for: "Package.swift").fileURL!.deletingLastPathComponent()
|
|
|
|
try await SwiftPMTestProject.build(at: packageDir)
|
|
|
|
let (otherPackageUri, positions) = try project.openDocument("otherPackage.swift")
|
|
let testPosition = positions["1️⃣"]
|
|
|
|
let preChangeWorkspaceResponse = try await project.testClient.send(
|
|
CompletionRequest(
|
|
textDocument: TextDocumentIdentifier(otherPackageUri),
|
|
position: testPosition
|
|
)
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
preChangeWorkspaceResponse.items,
|
|
[],
|
|
"Should not receive cross-module code completion results when opening an unrelated directory as workspace root"
|
|
)
|
|
|
|
project.testClient.send(
|
|
DidChangeWorkspaceFoldersNotification(
|
|
event: WorkspaceFoldersChangeEvent(added: [
|
|
WorkspaceFolder(uri: DocumentURI(packageDir))
|
|
])
|
|
)
|
|
)
|
|
|
|
try await project.testClient.send(SynchronizeRequest(index: true))
|
|
|
|
let postChangeWorkspaceResponse = try await project.testClient.send(
|
|
CompletionRequest(
|
|
textDocument: TextDocumentIdentifier(otherPackageUri),
|
|
position: testPosition
|
|
)
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
postChangeWorkspaceResponse.items.clearingUnstableValues,
|
|
[
|
|
CompletionItem(
|
|
label: "helloWorld()",
|
|
kind: .method,
|
|
detail: "Void",
|
|
documentation: nil,
|
|
deprecated: false,
|
|
filterText: "helloWorld()",
|
|
insertText: "helloWorld()",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(
|
|
range: Range(testPosition),
|
|
newText: "helloWorld()"
|
|
)
|
|
)
|
|
),
|
|
CompletionItem(
|
|
label: "self",
|
|
kind: .keyword,
|
|
detail: "Package",
|
|
documentation: nil,
|
|
deprecated: false,
|
|
filterText: "self",
|
|
insertText: "self",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(
|
|
TextEdit(
|
|
range: Range(testPosition),
|
|
newText: "self"
|
|
)
|
|
)
|
|
),
|
|
]
|
|
)
|
|
}
|
|
func testIntegrationTest() async throws {
|
|
try await SkipUnless.sourcekitdSupportsPlugin()
|
|
|
|
// This test is doing the same as `test-sourcekit-lsp` in the `swift-integration-tests` repo.
|
|
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"Sources/clib/include/clib.h": """
|
|
#ifndef CLIB_H
|
|
#define CLIB_H
|
|
|
|
void clib_func(void);
|
|
void clib_other(void);
|
|
|
|
#endif // CLIB_H
|
|
""",
|
|
"Sources/clib/clib.c": """
|
|
#include "clib.h"
|
|
|
|
void 1️⃣clib_func(void) {2️⃣}
|
|
""",
|
|
"Sources/exec/main.swift": """
|
|
import lib
|
|
import clib
|
|
|
|
Lib().3️⃣foo()
|
|
4️⃣clib_func()
|
|
""",
|
|
"Sources/lib/lib.swift": """
|
|
public struct Lib {
|
|
public func 5️⃣foo6️⃣() {}
|
|
public init() {}
|
|
}
|
|
""",
|
|
],
|
|
manifest: """
|
|
// swift-tools-version:5.5
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "pkg",
|
|
targets: [
|
|
.target(name: "exec", dependencies: ["lib", "clib"]),
|
|
.target(name: "lib", dependencies: []),
|
|
.target(name: "clib", dependencies: []),
|
|
]
|
|
)
|
|
""",
|
|
enableBackgroundIndexing: true
|
|
)
|
|
let (mainUri, mainPositions) = try project.openDocument("main.swift")
|
|
|
|
let fooDefinitionResponse = try await project.testClient.send(
|
|
DefinitionRequest(textDocument: TextDocumentIdentifier(mainUri), position: mainPositions["3️⃣"])
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
fooDefinitionResponse,
|
|
.locations([
|
|
Location(
|
|
uri: try project.uri(for: "lib.swift"),
|
|
range: try project.range(from: "5️⃣", to: "6️⃣", in: "lib.swift")
|
|
)
|
|
])
|
|
)
|
|
|
|
let clibFuncDefinitionResponse = try await project.testClient.send(
|
|
DefinitionRequest(textDocument: TextDocumentIdentifier(mainUri), position: mainPositions["4️⃣"])
|
|
)
|
|
XCTAssertEqual(
|
|
clibFuncDefinitionResponse,
|
|
.locations([
|
|
Location(uri: try project.uri(for: "clib.c"), range: try Range(project.position(of: "1️⃣", in: "clib.c")))
|
|
])
|
|
)
|
|
|
|
let swiftCompletionResponse = try await project.testClient.send(
|
|
CompletionRequest(textDocument: TextDocumentIdentifier(mainUri), position: mainPositions["3️⃣"])
|
|
)
|
|
XCTAssertEqual(
|
|
swiftCompletionResponse.items.clearingUnstableValues,
|
|
[
|
|
CompletionItem(
|
|
label: "foo()",
|
|
kind: .method,
|
|
detail: "Void",
|
|
deprecated: false,
|
|
filterText: "foo()",
|
|
insertText: "foo()",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(TextEdit(range: Range(mainPositions["3️⃣"]), newText: "foo()"))
|
|
),
|
|
CompletionItem(
|
|
label: "self",
|
|
kind: .keyword,
|
|
detail: "Lib",
|
|
deprecated: false,
|
|
filterText: "self",
|
|
insertText: "self",
|
|
insertTextFormat: .plain,
|
|
textEdit: .textEdit(TextEdit(range: Range(mainPositions["3️⃣"]), newText: "self"))
|
|
),
|
|
]
|
|
)
|
|
|
|
let (clibcUri, clibcPositions) = try project.openDocument("clib.c")
|
|
|
|
let cCompletionResponse = try await project.testClient.send(
|
|
CompletionRequest(textDocument: TextDocumentIdentifier(clibcUri), position: clibcPositions["2️⃣"])
|
|
)
|
|
// rdar://73762053: This should also suggest clib_other
|
|
XCTAssert(cCompletionResponse.items.contains(where: { $0.insertText == "clib_func" }))
|
|
}
|
|
|
|
func testWorkspaceOptions() async throws {
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"/.sourcekit-lsp/config.json": """
|
|
{
|
|
"swiftPM": {
|
|
"swiftCompilerFlags": ["-D", "TEST"]
|
|
}
|
|
}
|
|
""",
|
|
"Test.swift": """
|
|
func test() {
|
|
#if TEST
|
|
let x: String = 1
|
|
#endif
|
|
}
|
|
""",
|
|
]
|
|
)
|
|
|
|
let (uri, _) = try project.openDocument("Test.swift")
|
|
let diagnostics = try await project.testClient.send(
|
|
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
|
|
)
|
|
XCTAssertEqual(
|
|
diagnostics.fullReport?.items.map(\.message),
|
|
["Cannot convert value of type 'Int' to specified type 'String'"]
|
|
)
|
|
}
|
|
|
|
func testOptionsInInitializeRequest() async throws {
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"Test.swift": """
|
|
func test() {
|
|
#if TEST
|
|
let x: String = 1
|
|
#endif
|
|
}
|
|
"""
|
|
],
|
|
initializationOptions: SourceKitLSPOptions(
|
|
swiftPM: SourceKitLSPOptions.SwiftPMOptions(swiftCompilerFlags: ["-D", "TEST"])
|
|
).encodeToLSPAny()
|
|
)
|
|
|
|
let (uri, _) = try project.openDocument("Test.swift")
|
|
let diagnostics = try await project.testClient.send(
|
|
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
|
|
)
|
|
XCTAssertEqual(
|
|
diagnostics.fullReport?.items.map(\.message),
|
|
["Cannot convert value of type 'Int' to specified type 'String'"]
|
|
)
|
|
}
|
|
|
|
func testWorkspaceOptionsOverrideGlobalOptions() async throws {
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"/.sourcekit-lsp/config.json": """
|
|
{
|
|
"swiftPM": {
|
|
"swiftCompilerFlags": ["-D", "TEST"]
|
|
}
|
|
}
|
|
""",
|
|
"Test.swift": """
|
|
func test() {
|
|
#if TEST
|
|
let x: String = 1
|
|
#endif
|
|
#if OTHER
|
|
let x: String = 1.0
|
|
#endif
|
|
}
|
|
""",
|
|
],
|
|
initializationOptions: SourceKitLSPOptions(
|
|
swiftPM: SourceKitLSPOptions.SwiftPMOptions(swiftCompilerFlags: ["-D", "OTHER"])
|
|
).encodeToLSPAny()
|
|
)
|
|
|
|
let (uri, _) = try project.openDocument("Test.swift")
|
|
let diagnostics = try await project.testClient.send(
|
|
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
|
|
)
|
|
XCTAssertEqual(
|
|
diagnostics.fullReport?.items.map(\.message),
|
|
["Cannot convert value of type 'Int' to specified type 'String'"]
|
|
)
|
|
}
|
|
|
|
func testWorkspaceOptionsOverrideBuildServer() async throws {
|
|
let project = try await MultiFileTestProject(files: [
|
|
".sourcekit-lsp/config.json": """
|
|
{
|
|
"defaultWorkspaceType": "compilationDatabase"
|
|
}
|
|
""",
|
|
"src/Foo.swift": """
|
|
#if HAVE_SETTINGS
|
|
#error("Have settings")
|
|
#endif
|
|
""",
|
|
"Sources/MyLib/Bar.swift": "",
|
|
"build/compile_commands.json": """
|
|
[
|
|
{
|
|
"directory": "$TEST_DIR_BACKSLASH_ESCAPED",
|
|
"arguments": [
|
|
"swiftc",
|
|
"$TEST_DIR_BACKSLASH_ESCAPED/src/Foo.swift",
|
|
\(defaultSDKArgs)
|
|
"-DHAVE_SETTINGS"
|
|
],
|
|
"file": "src/Foo.swift",
|
|
"output": "$TEST_DIR_BACKSLASH_ESCAPED/build/Foo.swift.o"
|
|
}
|
|
]
|
|
""",
|
|
"Package.swift": """
|
|
// swift-tools-version: 5.7
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "MyLib",
|
|
targets: [
|
|
.target(name: "MyLib"),
|
|
]
|
|
)
|
|
""",
|
|
])
|
|
let (uri, _) = try project.openDocument("Foo.swift")
|
|
let diagnostics = try await project.testClient.send(
|
|
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
|
|
)
|
|
XCTAssertEqual(
|
|
diagnostics.fullReport?.items.map(\.message),
|
|
["Have settings"]
|
|
)
|
|
}
|
|
|
|
func testImplicitWorkspaceOptionsOverrideBuildServer() async throws {
|
|
let project = try await MultiFileTestProject(files: [
|
|
"projA/.sourcekit-lsp/config.json": """
|
|
{
|
|
"defaultWorkspaceType": "compilationDatabase"
|
|
}
|
|
""",
|
|
"projA/src/Foo.swift": """
|
|
#if HAVE_SETTINGS
|
|
#error("Have settings")
|
|
#endif
|
|
""",
|
|
"projA/Sources/MyLib/Bar.swift": "",
|
|
"projA/build/compile_commands.json": """
|
|
[
|
|
{
|
|
"directory": "$TEST_DIR_BACKSLASH_ESCAPED/projA",
|
|
"arguments": [
|
|
"swiftc",
|
|
"$TEST_DIR_BACKSLASH_ESCAPED/projA/src/Foo.swift",
|
|
\(defaultSDKArgs)
|
|
"-DHAVE_SETTINGS"
|
|
],
|
|
"file": "src/Foo.swift",
|
|
"output": "$TEST_DIR_BACKSLASH_ESCAPED/projA/build/Foo.swift.o"
|
|
}
|
|
]
|
|
""",
|
|
"projA/Package.swift": """
|
|
// swift-tools-version: 5.7
|
|
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "MyLib",
|
|
targets: [
|
|
.target(name: "MyLib"),
|
|
]
|
|
)
|
|
""",
|
|
])
|
|
let (uri, _) = try project.openDocument("Foo.swift")
|
|
let diagnostics = try await project.testClient.send(
|
|
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
|
|
)
|
|
XCTAssertEqual(
|
|
diagnostics.fullReport?.items.map(\.message),
|
|
["Have settings"]
|
|
)
|
|
}
|
|
|
|
func testWorkspaceOptionsCanAddSearchPaths() async throws {
|
|
let project = try await MultiFileTestProject(files: [
|
|
".sourcekit-lsp/config.json": """
|
|
{
|
|
"compilationDatabase": {
|
|
"searchPaths": ["otherbuild"]
|
|
}
|
|
}
|
|
""",
|
|
"src/Foo.swift": """
|
|
#if HAVE_SETTINGS
|
|
#error("Have settings")
|
|
#endif
|
|
""",
|
|
"otherbuild/compile_commands.json": """
|
|
[
|
|
{
|
|
"directory": "$TEST_DIR_BACKSLASH_ESCAPED",
|
|
"arguments": [
|
|
"swiftc",
|
|
"$TEST_DIR_BACKSLASH_ESCAPED/src/Foo.swift",
|
|
\(defaultSDKArgs)
|
|
"-DHAVE_SETTINGS"
|
|
],
|
|
"file": "src/Foo.swift",
|
|
"output": "$TEST_DIR_BACKSLASH_ESCAPED/otherbuild/Foo.swift.o"
|
|
}
|
|
]
|
|
""",
|
|
])
|
|
let (uri, _) = try project.openDocument("Foo.swift")
|
|
let diagnostics = try await project.testClient.send(
|
|
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
|
|
)
|
|
XCTAssertEqual(
|
|
diagnostics.fullReport?.items.map(\.message),
|
|
["Have settings"]
|
|
)
|
|
}
|
|
|
|
func testUnknownFileInProjectRootUsesWorkspaceAtRoot() async throws {
|
|
let project = try await MultiFileTestProject(files: [
|
|
"build/test.h": "",
|
|
"build/compile_commands.json": "[]",
|
|
])
|
|
|
|
let uri = try project.uri(for: "test.h")
|
|
assertEqual(
|
|
await project.testClient.server.workspaceForDocument(uri: uri)?.rootUri?.fileURL,
|
|
project.scratchDirectory
|
|
)
|
|
}
|
|
|
|
func testDidChangeActiveEditorDocument() async throws {
|
|
let didChangeBaseLib = ThreadSafeBox<Bool>(initialValue: false)
|
|
let didPrepareLibBAfterChangingBaseLib = self.expectation(description: "Did prepare LibB after changing base lib")
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"BaseLib/BaseLib.swift": "",
|
|
"LibA/LibA.swift": "",
|
|
"LibB/LibB.swift": "",
|
|
],
|
|
manifest: """
|
|
let package = Package(
|
|
name: "MyLib",
|
|
targets: [
|
|
.target(name: "BaseLib"),
|
|
.target(name: "LibA", dependencies: ["BaseLib"]),
|
|
.target(name: "LibB", dependencies: ["BaseLib"]),
|
|
]
|
|
)
|
|
""",
|
|
capabilities: ClientCapabilities(experimental: [
|
|
DidChangeActiveDocumentNotification.method: ["supported": true]
|
|
]),
|
|
hooks: Hooks(
|
|
indexHooks: IndexHooks(preparationTaskDidStart: { task in
|
|
guard didChangeBaseLib.value else {
|
|
return
|
|
}
|
|
do {
|
|
XCTAssert(
|
|
task.targetsToPrepare.contains(try BuildTargetIdentifier(target: "LibB", destination: .target)),
|
|
"Prepared unexpected targets: \(task.targetsToPrepare)"
|
|
)
|
|
try await repeatUntilExpectedResult {
|
|
Task.currentPriority > .low
|
|
}
|
|
didPrepareLibBAfterChangingBaseLib.fulfill()
|
|
} catch {
|
|
XCTFail("Received unexpected error: \(error)")
|
|
}
|
|
})
|
|
),
|
|
enableBackgroundIndexing: true
|
|
)
|
|
|
|
_ = try project.openDocument("LibA.swift")
|
|
let (libBUri, _) = try project.openDocument("LibB.swift")
|
|
let baseLibUri = try XCTUnwrap(project.uri(for: "BaseLib.swift"))
|
|
|
|
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: baseLibUri, type: .changed)]))
|
|
// Ensure that we handle the `DidChangeWatchedFilesNotification`.
|
|
try await project.testClient.send(SynchronizeRequest())
|
|
didChangeBaseLib.value = true
|
|
|
|
project.testClient.send(
|
|
DidChangeActiveDocumentNotification(textDocument: TextDocumentIdentifier(libBUri))
|
|
)
|
|
try await fulfillmentOfOrThrow(didPrepareLibBAfterChangingBaseLib)
|
|
|
|
withExtendedLifetime(project) {}
|
|
}
|
|
|
|
func testSourceKitOptions() async throws {
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"Test.swift": ""
|
|
],
|
|
options: .testDefault(experimentalFeatures: [.sourceKitOptionsRequest])
|
|
)
|
|
let optionsOptional = try await project.testClient.send(
|
|
SourceKitOptionsRequest(
|
|
textDocument: TextDocumentIdentifier(unwrap(project.uri(for: "Test.swift"))),
|
|
prepareTarget: false,
|
|
allowFallbackSettings: false
|
|
)
|
|
)
|
|
let options = try XCTUnwrap(optionsOptional)
|
|
assertContains(options.compilerArguments, "-module-name")
|
|
XCTAssertEqual(options.kind, .normal)
|
|
XCTAssertNil(options.didPrepareTarget)
|
|
}
|
|
|
|
func testSourceKitOptionsAllowingFallback() async throws {
|
|
let hooks = Hooks(
|
|
buildServerHooks: BuildServerHooks(
|
|
swiftPMTestHooks: SwiftPMTestHooks(
|
|
reloadPackageDidStart: {
|
|
// Essentially make sure that the package never loads, so we are forced to return fallback arguments.
|
|
try? await Task.sleep(for: .seconds(defaultTimeout * 2))
|
|
}
|
|
)
|
|
)
|
|
)
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"Test.swift": ""
|
|
],
|
|
options: .testDefault(experimentalFeatures: [.sourceKitOptionsRequest]),
|
|
hooks: hooks,
|
|
pollIndex: false
|
|
)
|
|
let optionsOptional = try await project.testClient.send(
|
|
SourceKitOptionsRequest(
|
|
textDocument: TextDocumentIdentifier(unwrap(project.uri(for: "Test.swift"))),
|
|
prepareTarget: false,
|
|
allowFallbackSettings: true
|
|
)
|
|
)
|
|
let options = try XCTUnwrap(optionsOptional)
|
|
// Fallback arguments can't know the module name
|
|
XCTAssert(!options.compilerArguments.contains("-module-name"))
|
|
XCTAssertEqual(options.kind, .fallback)
|
|
XCTAssertNil(options.didPrepareTarget)
|
|
}
|
|
|
|
func testSourceKitOptionsTriggersPrepare() async throws {
|
|
let didChangeBaseLib = ThreadSafeBox<Bool>(initialValue: false)
|
|
let didPrepareAfterChangingBaseLib = self.expectation(description: "Did prepare after changing base lib")
|
|
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"BaseLib/BaseLib.swift": "",
|
|
"LibA/LibA.swift": "",
|
|
],
|
|
manifest: """
|
|
let package = Package(
|
|
name: "MyLib",
|
|
targets: [
|
|
.target(name: "BaseLib"),
|
|
.target(name: "LibA", dependencies: ["BaseLib"])
|
|
]
|
|
)
|
|
""",
|
|
options: .testDefault(experimentalFeatures: [.sourceKitOptionsRequest]),
|
|
hooks: Hooks(
|
|
indexHooks: IndexHooks(
|
|
preparationTaskDidStart: { _ in
|
|
guard didChangeBaseLib.value else {
|
|
return
|
|
}
|
|
didPrepareAfterChangingBaseLib.fulfill()
|
|
}
|
|
)
|
|
),
|
|
enableBackgroundIndexing: true
|
|
)
|
|
|
|
let baseLibUri = try XCTUnwrap(project.uri(for: "BaseLib.swift"))
|
|
let uri = try XCTUnwrap(project.uri(for: "LibA.swift"))
|
|
|
|
let noPrepare = try await project.testClient.send(
|
|
SourceKitOptionsRequest(
|
|
textDocument: TextDocumentIdentifier(uri),
|
|
prepareTarget: false,
|
|
allowFallbackSettings: false
|
|
)
|
|
)
|
|
try XCTAssertEqual(XCTUnwrap(noPrepare).didPrepareTarget, nil)
|
|
|
|
let prepareUpToDate = try await project.testClient.send(
|
|
SourceKitOptionsRequest(
|
|
textDocument: TextDocumentIdentifier(uri),
|
|
prepareTarget: true,
|
|
allowFallbackSettings: false
|
|
)
|
|
)
|
|
try XCTAssertEqual(XCTUnwrap(prepareUpToDate).didPrepareTarget, false)
|
|
|
|
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: baseLibUri, type: .changed)]))
|
|
// Ensure that we handle the `DidChangeWatchedFilesNotification`.
|
|
try await project.testClient.send(SynchronizeRequest())
|
|
didChangeBaseLib.value = true
|
|
|
|
let triggerPrepare = try await project.testClient.send(
|
|
SourceKitOptionsRequest(
|
|
textDocument: TextDocumentIdentifier(uri),
|
|
prepareTarget: true,
|
|
allowFallbackSettings: false
|
|
)
|
|
)
|
|
try XCTAssertEqual(XCTUnwrap(triggerPrepare).didPrepareTarget, true)
|
|
|
|
// Check that we did actually run a preparation
|
|
try await fulfillmentOfOrThrow(didPrepareAfterChangingBaseLib)
|
|
|
|
let prepareUpToDateAgain = try await project.testClient.send(
|
|
SourceKitOptionsRequest(
|
|
textDocument: TextDocumentIdentifier(uri),
|
|
prepareTarget: true,
|
|
allowFallbackSettings: false
|
|
)
|
|
)
|
|
try XCTAssertEqual(XCTUnwrap(prepareUpToDateAgain).didPrepareTarget, false)
|
|
}
|
|
|
|
func testBuildServerUsesStandardizedFileUrlsInsteadOfRealpath() async throws {
|
|
try SkipUnless.platformIsDarwin("The realpath vs standardized path difference only exists on macOS")
|
|
|
|
// Explicitly create a directory at /tmp (which is a standardized path but whose realpath is /private/tmp)
|
|
let scratchDirectory = URL(fileURLWithPath: "/tmp")
|
|
.appending(component: testScratchName())
|
|
try FileManager.default.createDirectory(at: scratchDirectory, withIntermediateDirectories: true)
|
|
|
|
defer {
|
|
if cleanScratchDirectories {
|
|
try? FileManager.default.removeItem(at: scratchDirectory)
|
|
}
|
|
}
|
|
|
|
_ = try MultiFileTestProject.writeFilesToDisk(
|
|
files: [
|
|
"test.h": "",
|
|
"test.c": """
|
|
#include "test.h"
|
|
""",
|
|
"compile_commands.json": """
|
|
[
|
|
{
|
|
"directory": "$TEST_DIR_BACKSLASH_ESCAPED",
|
|
"arguments": [
|
|
"clang",
|
|
"$TEST_DIR_BACKSLASH_ESCAPED/test.c",
|
|
"-DHAVE_SETTINGS",
|
|
"-index-store-path",
|
|
"$TEST_DIR_BACKSLASH_ESCAPED/index"
|
|
],
|
|
"file": "test.c",
|
|
"output": "$TEST_DIR_BACKSLASH_ESCAPED/build/test.o"
|
|
}
|
|
]
|
|
""",
|
|
],
|
|
scratchDirectory: scratchDirectory
|
|
)
|
|
|
|
let clang = try unwrap(await ToolchainRegistry.forTesting.default?.clang)
|
|
let clangOutput = try await withTimeout(defaultTimeoutDuration) {
|
|
try await Process.checkNonZeroExit(
|
|
arguments: [
|
|
clang.filePath, "-index-store-path", scratchDirectory.appending(component: "index").filePath,
|
|
scratchDirectory.appending(component: "test.c").filePath,
|
|
"-fsyntax-only",
|
|
]
|
|
)
|
|
}
|
|
logger.debug("Clang output:\n\(clangOutput)")
|
|
|
|
let testClient = try await TestSourceKitLSPClient(
|
|
options: .testDefault(experimentalFeatures: [.sourceKitOptionsRequest]),
|
|
workspaceFolders: [WorkspaceFolder(uri: DocumentURI(scratchDirectory), name: nil)]
|
|
)
|
|
try await testClient.send(SynchronizeRequest(index: true))
|
|
|
|
// Check that we can infer build settings for the header from its main file. indexstore-db stores this main file
|
|
// path as `/private/tmp` while the build server only knows about it as `/tmp`.
|
|
let options = try await testClient.send(
|
|
SourceKitOptionsRequest(
|
|
textDocument: TextDocumentIdentifier(scratchDirectory.appending(component: "test.h")),
|
|
prepareTarget: false,
|
|
allowFallbackSettings: false
|
|
)
|
|
)
|
|
assertContains(options.compilerArguments, "-DHAVE_SETTINGS")
|
|
}
|
|
|
|
func testOutputPaths() async throws {
|
|
let project = try await SwiftPMTestProject(
|
|
files: [
|
|
"FileA.swift": "",
|
|
"FileB.swift": "",
|
|
],
|
|
options: .testDefault(experimentalFeatures: [.outputPathsRequest]),
|
|
enableBackgroundIndexing: true
|
|
)
|
|
|
|
let outputPaths = try await project.testClient.send(
|
|
OutputPathsRequest(
|
|
target: BuildTargetIdentifier(target: "MyLibrary", destination: .target).uri,
|
|
workspace: DocumentURI(project.scratchDirectory)
|
|
)
|
|
)
|
|
XCTAssertEqual(outputPaths.outputPaths.map { $0.suffix(13) }.sorted(), ["FileA.swift.o", "FileB.swift.o"])
|
|
}
|
|
|
|
func testOrphanedClangLanguageServiceShutdown() async throws {
|
|
// test that when we remove a workspace, the ClangLanguageService for that workspace is shut down.
|
|
// verify this by checking that clangd receives a ShutdownRequest.
|
|
|
|
let clangdReceivedShutdown = self.expectation(description: "clangd received shutdown request")
|
|
clangdReceivedShutdown.assertForOverFulfill = false
|
|
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
"WorkspaceA/compile_flags.txt": "",
|
|
"WorkspaceA/dummy.c": "",
|
|
"WorkspaceB/main.c": """
|
|
int main() { return 0; }
|
|
""",
|
|
"WorkspaceB/compile_flags.txt": "",
|
|
],
|
|
workspaces: { scratchDir in
|
|
return [
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "WorkspaceA"))),
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "WorkspaceB"))),
|
|
]
|
|
},
|
|
hooks: Hooks(preForwardRequestToClangd: { request in
|
|
if request is ShutdownRequest {
|
|
clangdReceivedShutdown.fulfill()
|
|
}
|
|
})
|
|
)
|
|
|
|
// open a .c file in WorkspaceB to launch clangd
|
|
let (mainUri, _) = try project.openDocument("main.c")
|
|
|
|
// send a request to ensure clangd is up and running
|
|
_ = try await project.testClient.send(
|
|
DocumentSymbolRequest(textDocument: TextDocumentIdentifier(mainUri))
|
|
)
|
|
|
|
// Get the language service for WorkspaceB before closing
|
|
let clangLanguageServiceBeforeClose = try await unwrap(project.testClient.primaryLanguageService(for: mainUri))
|
|
|
|
// close the document
|
|
project.testClient.send(DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(mainUri)))
|
|
|
|
// remove WorkspaceB
|
|
let workspaceBUri = DocumentURI(project.scratchDirectory.appending(component: "WorkspaceB"))
|
|
project.testClient.send(
|
|
DidChangeWorkspaceFoldersNotification(
|
|
event: WorkspaceFoldersChangeEvent(removed: [WorkspaceFolder(uri: workspaceBUri)])
|
|
)
|
|
)
|
|
_ = try await project.testClient.send(SynchronizeRequest())
|
|
// wait for clangd to receive the shutdown request
|
|
try await fulfillmentOfOrThrow(clangdReceivedShutdown)
|
|
|
|
let workspaceAfterRemoval = await project.testClient.server.workspaceForDocument(uri: mainUri)
|
|
XCTAssertNotEqual(
|
|
try XCTUnwrap(workspaceAfterRemoval?.rootUri?.fileURL?.lastPathComponent),
|
|
"WorkspaceB",
|
|
"WorkspaceB should have been removed"
|
|
)
|
|
|
|
// verify the language service is orphaned - opening a file in WorkspaceA should get a different language service
|
|
let (dummyUri, _) = try project.openDocument("dummy.c")
|
|
_ = try await project.testClient.send(
|
|
DocumentSymbolRequest(textDocument: TextDocumentIdentifier(dummyUri))
|
|
)
|
|
|
|
let clangLanguageServiceForWorkspaceA = try await unwrap(project.testClient.primaryLanguageService(for: dummyUri))
|
|
|
|
XCTAssertFalse(
|
|
clangLanguageServiceBeforeClose === clangLanguageServiceForWorkspaceA,
|
|
"WorkspaceB's ClangLanguageService should have been shut down and a new one created for WorkspaceA"
|
|
)
|
|
}
|
|
|
|
func testOrphanedSwiftLanguageServiceIsShutDown() async throws {
|
|
// Test that when a workspace is removed, its SwiftLanguageService is shut down
|
|
// and WorkspaceA gets an independent service instance — mirroring the Clang behavior.
|
|
|
|
let project = try await MultiFileTestProject(
|
|
files: [
|
|
"WorkspaceA/Sources/LibA/LibA.swift": """
|
|
public struct LibA {
|
|
public func 1️⃣foo() {}
|
|
public init() {}
|
|
}
|
|
""",
|
|
"WorkspaceA/Package.swift": """
|
|
// swift-tools-version: 5.7
|
|
import PackageDescription
|
|
let package = Package(
|
|
name: "LibA",
|
|
targets: [.target(name: "LibA")]
|
|
)
|
|
""",
|
|
"WorkspaceB/Sources/LibB/LibB.swift": """
|
|
public struct LibB {
|
|
public func 2️⃣bar() {}
|
|
public init() {}
|
|
}
|
|
""",
|
|
"WorkspaceB/Package.swift": """
|
|
// swift-tools-version: 5.7
|
|
import PackageDescription
|
|
let package = Package(
|
|
name: "LibB",
|
|
targets: [.target(name: "LibB")]
|
|
)
|
|
""",
|
|
],
|
|
workspaces: { scratchDir in
|
|
try await SwiftPMTestProject.resolvePackageDependencies(at: scratchDir.appending(component: "WorkspaceA"))
|
|
try await SwiftPMTestProject.resolvePackageDependencies(at: scratchDir.appending(component: "WorkspaceB"))
|
|
return [
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "WorkspaceA"))),
|
|
WorkspaceFolder(uri: DocumentURI(scratchDir.appending(component: "WorkspaceB"))),
|
|
]
|
|
}
|
|
)
|
|
|
|
// Wait for SwiftPM to finish loading the package graph before opening documents.
|
|
_ = try await project.testClient.send(SynchronizeRequest(index: true))
|
|
|
|
let (libBUri, libBPositions) = try project.openDocument("LibB.swift")
|
|
|
|
// Send a request to ensure the SwiftLanguageService for WorkspaceB is up.
|
|
_ = try await project.testClient.send(
|
|
HoverRequest(textDocument: TextDocumentIdentifier(libBUri), position: libBPositions["2️⃣"])
|
|
)
|
|
|
|
// Capture the identity of WorkspaceB's SwiftLanguageService before removal.
|
|
let libBServiceID = try await ObjectIdentifier(unwrap(project.testClient.primaryLanguageService(for: libBUri)))
|
|
|
|
// Open a file in WorkspaceA before removing WorkspaceB, to verify the service survives the removal.
|
|
let (libAUri, libAPositions) = try project.openDocument("LibA.swift")
|
|
|
|
// Capture the identity of WorkspaceA's service before the removal.
|
|
let libAServiceIDBefore = try await ObjectIdentifier(
|
|
unwrap(project.testClient.primaryLanguageService(for: libAUri))
|
|
)
|
|
|
|
// Close the document and remove WorkspaceB.
|
|
project.testClient.send(DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(libBUri)))
|
|
let workspaceBUri = DocumentURI(project.scratchDirectory.appending(component: "WorkspaceB"))
|
|
project.testClient.send(
|
|
DidChangeWorkspaceFoldersNotification(
|
|
event: WorkspaceFoldersChangeEvent(removed: [WorkspaceFolder(uri: workspaceBUri)])
|
|
)
|
|
)
|
|
_ = try await project.testClient.send(SynchronizeRequest())
|
|
|
|
// WorkspaceA's service should still work after WorkspaceB is removed.
|
|
let hover = try await project.testClient.send(
|
|
HoverRequest(textDocument: TextDocumentIdentifier(libAUri), position: libAPositions["1️⃣"])
|
|
)
|
|
XCTAssertNotNil(hover, "Should still get hover response after removing WorkspaceB")
|
|
assertContains(hover?.contents.markupContent?.value ?? "", "foo")
|
|
|
|
let libAService = try await unwrap(project.testClient.primaryLanguageService(for: libAUri))
|
|
XCTAssertEqual(
|
|
libAServiceIDBefore,
|
|
ObjectIdentifier(libAService),
|
|
"WorkspaceA's service should be the same instance"
|
|
)
|
|
XCTAssertNotEqual(
|
|
libBServiceID,
|
|
ObjectIdentifier(libAService),
|
|
"WorkspaceA and WorkspaceB must use independent service instances"
|
|
)
|
|
}
|
|
}
|
|
|
|
private let defaultSDKArgs: String = {
|
|
if let defaultSDKPath {
|
|
let escapedPath = defaultSDKPath.replacing(#"\"#, with: #"\\"#)
|
|
return """
|
|
"-sdk", "\(escapedPath)",
|
|
"""
|
|
}
|
|
return ""
|
|
}()
|