Files
sourcekit-lsp/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift
Alex Hoppen a26b0b7b3a Resolve swiftly when referenced in compile commands
When the compiler in `compile_commands.json` references a `swift` executable that’s a symlink to `swiftly`, SourceKit-LSP got confused because the `swift` executable doesn’t reside in a real toolchain, causing us to not provide any semantic functionality.

When we discover that the `swift` executable reference in compile commands references a `swiftly` executable, use `swiftly use -p` to resolve the binary in the real toolchain and continue operating based on that.

Fixes #2128
rdar://150301344
2025-05-09 09:12:21 +02:00

368 lines
14 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 BuildSystemIntegration
import LanguageServerProtocol
import SKTestSupport
import SwiftExtensions
import TSCBasic
import TSCExtensions
import ToolchainRegistry
import XCTest
final class CompilationDatabaseTests: XCTestCase {
func testModifyCompilationDatabase() async throws {
let project = try await MultiFileTestProject(files: [
"main.cpp": """
#if FOO
void 1️⃣foo2️⃣() {}
#else
void 3️⃣foo4️⃣() {}
#endif
int main() {
5️⃣foo6️⃣();
}
""",
"compile_flags.txt": """
-DFOO
""",
])
let (mainUri, positions) = try project.openDocument("main.cpp")
// Verify that we get the expected result from a hover response before modifying the compile commands.
let highlightRequest = DocumentHighlightRequest(
textDocument: TextDocumentIdentifier(mainUri),
position: positions["5️⃣"]
)
let preChangeHighlightResponse = try await project.testClient.send(highlightRequest)
XCTAssertEqual(
preChangeHighlightResponse,
[
DocumentHighlight(range: positions["1️⃣"]..<positions["2️⃣"], kind: .text),
DocumentHighlight(range: positions["5️⃣"]..<positions["6️⃣"], kind: .text),
]
)
// Remove -DFOO from the compile commands.
try await project.changeFileOnDisk(FixedCompilationDatabaseBuildSystem.dbName, newMarkedContents: "")
// DocumentHighlight should now point to the definition in the `#else` block.
let expectedPostEditHighlight = [
DocumentHighlight(range: positions["3️⃣"]..<positions["4️⃣"], kind: .text),
DocumentHighlight(range: positions["5️⃣"]..<positions["6️⃣"], kind: .text),
]
// Updating the build settings takes a few seconds.
// Send highlight requests every second until we receive correct results.
try await repeatUntilExpectedResult {
let postChangeHighlightResponse = try await project.testClient.send(highlightRequest)
return postChangeHighlightResponse == expectedPostEditHighlight
}
}
func testJSONCompilationDatabaseWithDifferentToolchainsForSwift() async throws {
let dummyToolchain = Toolchain(
identifier: "dummy",
displayName: "dummy",
path: URL(fileURLWithPath: "/dummy"),
clang: nil,
swift: URL(fileURLWithPath: "/dummy/usr/bin/swift"),
swiftc: URL(fileURLWithPath: "/dummy/usr/bin/swiftc"),
swiftFormat: nil,
clangd: nil,
sourcekitd: URL(fileURLWithPath: "/dummy/usr/lib/sourcekitd.framework/sourcekitd"),
libIndexStore: nil
)
let toolchainRegistry = ToolchainRegistry(toolchains: [
try await unwrap(ToolchainRegistry.forTesting.default), dummyToolchain,
])
let project = try await MultiFileTestProject(
files: [
"testFromDefaultToolchain.swift": """
#warning("Test warning")
""",
"testFromDummyToolchain.swift": """
#warning("Test warning")
""",
"compile_commands.json": """
[
{
"directory": "$TEST_DIR_BACKSLASH_ESCAPED",
"arguments": [
"swiftc",
"$TEST_DIR_BACKSLASH_ESCAPED/testFromDefaultToolchain.swift",
\(defaultSDKArgs)
],
"file": "testFromDefaultToolchain.swift",
"output": "$TEST_DIR_BACKSLASH_ESCAPED/testFromDefaultToolchain.swift.o"
},
{
"directory": "$TEST_DIR_BACKSLASH_ESCAPED",
"arguments": [
"/dummy/usr/bin/swiftc",
"$TEST_DIR_BACKSLASH_ESCAPED/testFromDummyToolchain.swift",
\(defaultSDKArgs)
],
"file": "testFromDummyToolchain.swift",
"output": "$TEST_DIR_BACKSLASH_ESCAPED/testFromDummyToolchain.swift.o"
}
]
""",
],
toolchainRegistry: toolchainRegistry
)
// We should be able to provide semantic functionality for `testFromDefaultToolchain` because we open it using the
// default toolchain.
let (forDefaultToolchainUri, _) = try project.openDocument("testFromDefaultToolchain.swift")
let diagnostics = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(forDefaultToolchainUri))
)
XCTAssertEqual(
diagnostics.fullReport?.items.map(\.message),
["Test warning"]
)
// But for `testFromDummyToolchain.swift`, we can't launch sourcekitd (because it doesn't exist, we just provided a
// dummy), so we should receive an error. The exact error here is not super relevant, the important part is that we
// apparently tried to launch a different sourcekitd.
let (forDummyToolchainUri, _) = try project.openDocument("testFromDummyToolchain.swift")
await assertThrowsError(
try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(forDummyToolchainUri))
)
) { error in
guard let error = error as? ResponseError else {
XCTFail("Expected ResponseError, got \(error)")
return
}
assertContains(error.message, "No language service")
}
}
func testJSONCompilationDatabaseWithDifferentToolchainsForClang() async throws {
let dummyToolchain = Toolchain(
identifier: "dummy",
displayName: "dummy",
path: URL(fileURLWithPath: "/dummy"),
clang: URL(fileURLWithPath: "/dummy/usr/bin/clang"),
swift: nil,
swiftc: nil,
swiftFormat: nil,
clangd: URL(fileURLWithPath: "/dummy/usr/bin/clangd"),
sourcekitd: nil,
libIndexStore: nil
)
let toolchainRegistry = ToolchainRegistry(toolchains: [
try await unwrap(ToolchainRegistry.forTesting.default), dummyToolchain,
])
let project = try await MultiFileTestProject(
files: [
"testFromDefaultToolchain.c": """
void 1️⃣main() {}
""",
"testFromDummyToolchain.c": """
void 2️⃣main() {}
""",
"compile_commands.json": """
[
{
"directory": "$TEST_DIR_BACKSLASH_ESCAPED",
"arguments": [
"clang",
"$TEST_DIR_BACKSLASH_ESCAPED/testFromDefaultToolchain.c"
],
"file": "testFromDefaultToolchain.c",
"output": "$TEST_DIR_BACKSLASH_ESCAPED/testFromDefaultToolchain.o"
},
{
"directory": "$TEST_DIR_BACKSLASH_ESCAPED",
"arguments": [
"/dummy/usr/bin/clang",
"$TEST_DIR_BACKSLASH_ESCAPED/testFromDummyToolchain.c"
],
"file": "testFromDummyToolchain.c",
"output": "$TEST_DIR_BACKSLASH_ESCAPED/testFromDummyToolchain.o"
}
]
""",
],
toolchainRegistry: toolchainRegistry
)
// We should be able to provide semantic functionality for `testFromDefaultToolchain` because we open it using the
// default toolchain.
let (forDefaultToolchainUri, forDefaultToolchainPositions) = try project.openDocument("testFromDefaultToolchain.c")
let hover = try await project.testClient.send(
HoverRequest(
textDocument: TextDocumentIdentifier(forDefaultToolchainUri),
position: forDefaultToolchainPositions["1️⃣"]
)
)
let hoverContent = try XCTUnwrap(hover?.contents.markupContent?.value)
assertContains(hoverContent, "void main()")
// But for `testFromDummyToolchain.swift`, we can't launch sourcekitd (because it doesn't exist, we just provided a
// dummy), so we should receive an error. The exact error here is not super relevant, the important part is that we
// apparently tried to launch a different sourcekitd.
let (forDummyToolchainUri, forDummyToolchainPositions) = try project.openDocument("testFromDummyToolchain.c")
await assertThrowsError(
try await project.testClient.send(
HoverRequest(
textDocument: TextDocumentIdentifier(forDummyToolchainUri),
position: forDummyToolchainPositions["2️⃣"]
)
)
) { error in
guard let error = error as? ResponseError else {
XCTFail("Expected ResponseError, got \(error)")
return
}
assertContains(error.message, "No language service")
}
}
func testLookThroughSwiftly() async throws {
try await withTestScratchDir { scratchDirectory in
let defaultToolchain = try await unwrap(ToolchainRegistry.forTesting.default)
// We create a toolchain registry with the default toolchain, which is able to provide semantic functionality and
// a dummy toolchain that can't provide semantic functionality.
let fakeToolchainURL = scratchDirectory.appending(components: "fakeToolchain")
let fakeToolchain = Toolchain(
identifier: "fake",
displayName: "fake",
path: fakeToolchainURL,
clang: nil,
swift: fakeToolchainURL.appending(components: "usr", "bin", "swift"),
swiftc: fakeToolchainURL.appending(components: "usr", "bin", "swiftc"),
swiftFormat: nil,
clangd: nil,
sourcekitd: fakeToolchainURL.appending(components: "usr", "lib", "sourcekitd.framework", "sourcekitd"),
libIndexStore: nil
)
let toolchainRegistry = ToolchainRegistry(toolchains: [
try await unwrap(ToolchainRegistry.forTesting.default), fakeToolchain,
])
// We need to create a file for the swift executable because `SwiftlyResolver` checks for its presence.
try FileManager.default.createDirectory(
at: XCTUnwrap(fakeToolchain.swift).deletingLastPathComponent(),
withIntermediateDirectories: true
)
try await "".writeWithRetry(to: XCTUnwrap(fakeToolchain.swift))
// Create a dummy swiftly executable that picks the default toolchain for all file unless `fakeToolchain` is in
// the source file's path.
let dummySwiftlyExecutableUrl = scratchDirectory.appendingPathComponent("swiftly")
let dummySwiftExecutableUrl = scratchDirectory.appendingPathComponent("swift")
try FileManager.default.createSymbolicLink(
at: dummySwiftExecutableUrl,
withDestinationURL: dummySwiftlyExecutableUrl
)
try await createBinary(
"""
import Foundation
if FileManager.default.currentDirectoryPath.contains("fakeToolchain") {
print(#"\(fakeToolchain.path.filePath)"#)
} else {
print(#"\(defaultToolchain.path.filePath)"#)
}
""",
at: dummySwiftlyExecutableUrl
)
// Now create a project in which we have one file in a `realToolchain` directory for which our fake swiftly will
// pick the default toolchain and one in `fakeToolchain` for which swiftly will pick the fake toolchain. We should
// be able to get semantic functionality for the file in `realToolchain` but not for `fakeToolchain` because
// sourcekitd can't be launched for that toolchain (since it doesn't exist).
let dummySwiftExecutablePathForJSON = try dummySwiftExecutableUrl.filePath.replacing(#"\"#, with: #"\\"#)
let project = try await MultiFileTestProject(
files: [
"realToolchain/realToolchain.swift": """
#warning("Test warning")
""",
"fakeToolchain/fakeToolchain.swift": """
#warning("Test warning")
""",
"compile_commands.json": """
[
{
"directory": "$TEST_DIR_BACKSLASH_ESCAPED/realToolchain",
"arguments": [
"\(dummySwiftExecutablePathForJSON)",
"$TEST_DIR_BACKSLASH_ESCAPED/realToolchain/realToolchain.swift",
\(defaultSDKArgs)
],
"file": "realToolchain.swift",
"output": "$TEST_DIR_BACKSLASH_ESCAPED/realToolchain/test.swift.o"
},
{
"directory": "$TEST_DIR_BACKSLASH_ESCAPED/fakeToolchain",
"arguments": [
"\(dummySwiftExecutablePathForJSON)",
"$TEST_DIR_BACKSLASH_ESCAPED/fakeToolchain/fakeToolchain.swift",
\(defaultSDKArgs)
],
"file": "fakeToolchain.swift",
"output": "$TEST_DIR_BACKSLASH_ESCAPED/fakeToolchain/test.swift.o"
}
]
""",
],
toolchainRegistry: toolchainRegistry
)
let (forRealToolchainUri, _) = try project.openDocument("realToolchain.swift")
let diagnostics = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(forRealToolchainUri))
)
XCTAssertEqual(diagnostics.fullReport?.items.map(\.message), ["Test warning"])
let (forDummyToolchainUri, _) = try project.openDocument("fakeToolchain.swift")
await assertThrowsError(
try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(forDummyToolchainUri))
)
) { error in
guard let error = error as? ResponseError else {
XCTFail("Expected ResponseError, got \(error)")
return
}
// The actual error message here doesn't matter too much, we just need to check that we don't get diagnostics.
assertContains(error.message, "No language service")
}
}
}
}
fileprivate let defaultSDKArgs: String = {
if let defaultSDKPath {
let escapedPath = defaultSDKPath.replacing(#"\"#, with: #"\\"#)
return """
"-sdk", "\(escapedPath)"
"""
}
return ""
}()