mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-10-18 08:47:38 +02:00
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
368 lines
14 KiB
Swift
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 ""
|
|
}()
|