Files
sourcekit-lsp/Tests/SourceKitLSPTests/SwiftPMIntegrationTests.swift

906 lines
32 KiB
Swift

//===----------------------------------------------------------------------===//
//
// 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 BuildServerIntegration
import Foundation
@_spi(SourceKitLSP) import LanguageServerProtocol
import SKLogging
import SKOptions
import SKTestSupport
import SourceKitLSP
import SwiftExtensions
import XCTest
final class SwiftPMIntegrationTests: SourceKitLSPTestCase {
func testSwiftPMIntegration() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let project = try await SwiftPMTestProject(
files: [
"Lib.swift": """
struct Lib {
func 1️⃣foo() {}
}
""",
"Other.swift": """
func test() {
Lib().2️⃣foo()
}
""",
],
enableBackgroundIndexing: true
)
let (otherUri, otherPositions) = try project.openDocument("Other.swift")
let callPosition = otherPositions["2️⃣"]
let refs = try await project.testClient.send(
ReferencesRequest(
textDocument: TextDocumentIdentifier(otherUri),
position: callPosition,
context: ReferencesContext(includeDeclaration: true)
)
)
XCTAssertEqual(
Set(refs),
[
Location(uri: otherUri, range: Range(callPosition)),
Location(uri: try project.uri(for: "Lib.swift"), range: Range(try project.position(of: "1️⃣", in: "Lib.swift"))),
]
)
let completions = try await project.testClient.send(
CompletionRequest(textDocument: TextDocumentIdentifier(otherUri), position: callPosition)
)
XCTAssertEqual(
completions.items.clearingUnstableValues,
[
CompletionItem(
label: "foo()",
kind: .method,
detail: "Void",
deprecated: false,
sortText: nil,
filterText: "foo()",
insertText: "foo()",
insertTextFormat: .plain,
textEdit: .textEdit(
TextEdit(range: Range(callPosition), newText: "foo()")
)
),
CompletionItem(
label: "self",
kind: .keyword,
detail: "Lib",
deprecated: false,
sortText: nil,
filterText: "self",
insertText: "self",
insertTextFormat: .plain,
textEdit: .textEdit(
TextEdit(range: Range(callPosition), newText: "self")
)
),
]
)
}
func testAddFile() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let project = try await SwiftPMTestProject(
files: [
"Lib.swift": """
struct Lib {
func foo() {
1️⃣
}
}
"""
],
enableBackgroundIndexing: true
)
// First, create a new in-memory file and verify that we get some basic functionality for it
let newFileUrl = project.scratchDirectory
.appending(components: "Sources", "MyLibrary", "Other.swift")
let newFileUri = DocumentURI(newFileUrl)
let newFileContents = """
func baz(l: Lib) {
l.2️⃣foo()
#warning("A manual warning")
}
"""
let newFilePositions = project.testClient.openDocument(newFileContents, uri: newFileUri)
try await extractMarkers(newFileContents).textWithoutMarkers.writeWithRetry(to: newFileUrl)
let completionsBeforeSave = try await project.testClient.send(
CompletionRequest(textDocument: TextDocumentIdentifier(newFileUri), position: newFilePositions["2️⃣"])
)
XCTAssertEqual(Set(completionsBeforeSave.items.map(\.label)), ["foo()", "self"])
// We shouldn't get diagnostics for the new file yet since we still consider the build settings inferred from a
// sibling file fallback settings.
let diagnosticsBeforeSave = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(newFileUri))
)
XCTAssertEqual(diagnosticsBeforeSave.fullReport?.items, [])
let (oldFileUri, oldFilePositions) = try project.openDocument("Lib.swift")
// Check that we don't get completions for `baz` (defined in the new file) in the old file yet because the new file
// is not part of the package manifest yet.
let oldFileCompletionsBeforeSave = try await project.testClient.send(
CompletionRequest(textDocument: TextDocumentIdentifier(oldFileUri), position: oldFilePositions["1️⃣"])
)
XCTAssert(!oldFileCompletionsBeforeSave.items.contains(where: { $0.label == "baz(l: Lib)" }))
// Now save the file to disk, which adds it to the package graph, which should enable more functionality.
try await extractMarkers(newFileContents).textWithoutMarkers.writeWithRetry(to: newFileUrl)
project.testClient.send(
DidChangeWatchedFilesNotification(changes: [
FileEvent(uri: newFileUri, type: .created)
])
)
// Ensure that the DidChangeWatchedFilesNotification is handled before we continue.
try await project.testClient.send(SynchronizeRequest(index: true))
// Check that we still get completions in the new file, now get diagnostics in the new file and also see functions
// from the new file in the old file
let completionsAfterSave = try await project.testClient.send(
CompletionRequest(textDocument: TextDocumentIdentifier(newFileUri), position: newFilePositions["2️⃣"])
)
XCTAssertEqual(Set(completionsAfterSave.items.map(\.label)), ["foo()", "self"])
let diagnosticsAfterSave = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(newFileUri))
)
XCTAssertEqual(diagnosticsAfterSave.fullReport?.items.map(\.message), ["A manual warning"])
let oldFileCompletionsAfterSave = try await project.testClient.send(
CompletionRequest(textDocument: TextDocumentIdentifier(oldFileUri), position: oldFilePositions["1️⃣"])
)
assertContains(oldFileCompletionsAfterSave.items.map(\.label), "baz(l: Lib)")
}
func testNestedPackage() async throws {
try await SkipUnless.sourcekitdSupportsPlugin()
let project = try await MultiFileTestProject(files: [
"pkg/Sources/lib/lib.swift": "",
"pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(name: "a", products: [], dependencies: [],
targets: [.target(name: "lib", dependencies: [])])
""",
"nested/pkg/Sources/lib/a.swift.swift": """
struct Foo {
func bar() {}
}
""",
"nested/pkg/Sources/lib/b.swift": """
func test(foo: Foo) {
foo.1️⃣
}
""",
"nested/pkg/Package.swift": """
// swift-tools-version:4.2
import PackageDescription
let package = Package(name: "a", products: [], dependencies: [],
targets: [.target(name: "lib", dependencies: [])])
""",
])
let (uri, positions) = try project.openDocument("b.swift")
try await project.testClient.send(SynchronizeRequest(index: true))
let result = try await project.testClient.send(
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
XCTAssertEqual(
result.items.clearingUnstableValues,
[
CompletionItem(
label: "bar()",
kind: .method,
detail: "Void",
deprecated: false,
filterText: "bar()",
insertText: "bar()",
insertTextFormat: .plain,
textEdit: .textEdit(TextEdit(range: positions["1️⃣"]..<positions["1️⃣"], newText: "bar()"))
),
CompletionItem(
label: "self",
kind: .keyword,
detail: "Foo",
deprecated: false,
filterText: "self",
insertText: "self",
insertTextFormat: .plain,
textEdit: .textEdit(TextEdit(range: positions["1️⃣"]..<positions["1️⃣"], newText: "self"))
),
]
)
}
func testWasm() async throws {
try await SkipUnless.canCompileForWasm()
let project = try await SwiftPMTestProject(
files: [
"/.sourcekit-lsp/config.json": """
{
"swiftPM": {
"triple": "wasm32-unknown-none-wasm"
}
}
""",
"Test.swift": """
#if arch(wasm32)
let _: UnsafeRawPointer = 1
#endif
""",
],
manifest: """
let package = Package(
name: "WasmTest",
targets: [
.executableTarget(
name: "wasmTest",
cSettings: [.unsafeFlags(["-fdeclspec"])],
swiftSettings: [
.enableExperimentalFeature("Embedded"),
.interoperabilityMode(.Cxx),
.unsafeFlags(["-wmo", "-disable-cmo", "-Xfrontend", "-gnone"]),
],
linkerSettings: [.unsafeFlags(["-Xclang-linker", "-nostdlib", "-Xlinker", "--no-entry"])]
)
]
)
"""
)
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 'UnsafeRawPointer'"]
)
}
func testProvideSyntacticFunctionalityWhilePackageIsLoading() async throws {
let receivedDocumentSymbolsReply = WrappedSemaphore(name: "Received document symbols reply")
let project = try await SwiftPMTestProject(
files: [
"Test.swift": ""
],
hooks: Hooks(
buildServerHooks: BuildServerHooks(
swiftPMTestHooks: SwiftPMTestHooks(reloadPackageDidStart: {
receivedDocumentSymbolsReply.waitOrXCTFail()
})
)
),
pollIndex: false
)
let (uri, _) = try project.openDocument("Test.swift")
_ = try await project.testClient.send(DocumentSymbolRequest(textDocument: TextDocumentIdentifier(uri)))
receivedDocumentSymbolsReply.signal()
}
func testDiagnosticsGetRefreshedAfterPackageLoadingFinishes() async throws {
let receivedInitialDiagnosticsReply = WrappedSemaphore(name: "Received initial diagnostics reply")
let project = try await SwiftPMTestProject(
files: [
"Test.swift": """
let x: String = 1
"""
],
capabilities: ClientCapabilities(
workspace: WorkspaceClientCapabilities(diagnostics: RefreshRegistrationCapability(refreshSupport: true))
),
hooks: Hooks(
buildServerHooks: BuildServerHooks(
swiftPMTestHooks: SwiftPMTestHooks(reloadPackageDidStart: {
receivedInitialDiagnosticsReply.waitOrXCTFail()
})
)
),
pollIndex: false
)
let diagnosticRefreshRequestReceived = self.expectation(description: "DiagnosticsRefreshRequest received")
project.testClient.handleSingleRequest { (request: DiagnosticsRefreshRequest) in
diagnosticRefreshRequestReceived.fulfill()
return VoidResponse()
}
let (uri, _) = try project.openDocument("Test.swift")
let diagnosticsBeforePackageLoading = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(diagnosticsBeforePackageLoading.fullReport?.items, [])
receivedInitialDiagnosticsReply.signal()
try await Task.sleep(for: .seconds(1))
try await fulfillmentOfOrThrow(diagnosticRefreshRequestReceived)
let diagnosticsAfterPackageLoading = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(
diagnosticsAfterPackageLoading.fullReport?.items.map(\.message),
["Cannot convert value of type 'Int' to specified type 'String'"]
)
}
func testToolPluginWithBuild() async throws {
let project = try await SwiftPMTestProject(
files: [
"Plugins/plugin.swift": #"""
import PackagePlugin
@main struct CodeGeneratorPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
let genSourcesDir = context.pluginWorkDirectoryURL.appending(path: "GeneratedSources")
guard let target = target as? SourceModuleTarget else { return [] }
let codeGenerator = try context.tool(named: "CodeGenerator").url
let generatedFile = genSourcesDir.appending(path: "\(target.name)-generated.swift")
return [.buildCommand(
displayName: "Generating code for \(target.name)",
executable: codeGenerator,
arguments: [
generatedFile.path
],
inputFiles: [],
outputFiles: [generatedFile]
)]
}
}
"""#,
"Sources/CodeGenerator/CodeGenerator.swift": #"""
import Foundation
try "let generated = 1".write(to: URL(fileURLWithPath: CommandLine.arguments[1]), atomically: true, encoding: String.Encoding.utf8)
"""#,
"Sources/TestLib/TestLib.swift": #"""
func useGenerated() {
_ = 1️⃣generated
}
"""#,
],
manifest: """
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "PluginTest",
targets: [
.executableTarget(name: "CodeGenerator"),
.target(
name: "TestLib",
plugins: [.plugin(name: "CodeGeneratorPlugin")]
),
.plugin(
name: "CodeGeneratorPlugin",
capability: .buildTool(),
dependencies: ["CodeGenerator"]
),
]
)
""",
enableBackgroundIndexing: false
)
let (uri, positions) = try project.openDocument("TestLib.swift")
let result = try await project.testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
// We cannot run plugins when not using background indexing, so we expect no result here.
XCTAssertNil(result)
}
func testToolPluginWithBackgroundIndexing() async throws {
try await SkipUnless.canLoadPluginsBuiltByToolchain()
let project = try await SwiftPMTestProject(
files: [
"Plugins/plugin.swift": #"""
import Foundation
import PackagePlugin
@main struct CodeGeneratorPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
let genSourcesDir = context.pluginWorkDirectoryURL.appending(path: "GeneratedSources")
guard let target = target as? SourceModuleTarget else { return [] }
let codeGenerator = try context.tool(named: "CodeGenerator").url
let generatedFile = genSourcesDir.appending(path: "\(target.name)-generated.swift")
return [.buildCommand(
displayName: "Generating code for \(target.name)",
executable: codeGenerator,
arguments: [
generatedFile.path
],
inputFiles: [
URL(fileURLWithPath: "$TEST_DIR_BACKSLASH_ESCAPED/topDep.txt"),
URL(fileURLWithPath: "$TEST_DIR_BACKSLASH_ESCAPED/Sources/TestLib/targetDep.txt")
],
outputFiles: [generatedFile]
)]
}
}
"""#,
"Sources/CodeGenerator/CodeGenerator.swift": #"""
import Foundation
let topGenerated = try String(contentsOf: URL(fileURLWithPath: "$TEST_DIR_BACKSLASH_ESCAPED/topDep.txt"))
let targetGenerated = try String(contentsOf: URL(fileURLWithPath: "$TEST_DIR_BACKSLASH_ESCAPED/Sources/TestLib/targetDep.txt"))
try "\(topGenerated)\n\(targetGenerated)".write(
to: URL(fileURLWithPath: CommandLine.arguments[1]),
atomically: true,
encoding: String.Encoding.utf8
)
"""#,
"Sources/TestLib/TestLib.swift": #"""
func useGenerated() {
_ = 1️⃣topGenerated
_ = 2️⃣targetGenerated
}
"""#,
"/topDep.txt": "let topGenerated = 1",
"Sources/TestLib/targetDep.txt": "let targetGenerated = 1",
],
manifest: """
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "PluginTest",
targets: [
.executableTarget(name: "CodeGenerator"),
.target(
name: "TestLib",
plugins: [.plugin(name: "CodeGeneratorPlugin")]
),
.plugin(
name: "CodeGeneratorPlugin",
capability: .buildTool(),
dependencies: ["CodeGenerator"]
),
]
)
""",
enableBackgroundIndexing: true
)
let (uri, positions) = try project.openDocument("TestLib.swift")
// We should have run plugins and thus created generated.swift
do {
let topResult = try await project.testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
let topLocation = try XCTUnwrap(topResult?.locations?.only)
XCTAssertTrue(topLocation.uri.pseudoPath.hasSuffix("generated.swift"))
XCTAssertEqual(topLocation.range.lowerBound, Position(line: 0, utf16index: 4))
XCTAssertEqual(topLocation.range.upperBound, Position(line: 0, utf16index: 4))
let targetResult = try await project.testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
)
let targetLocation = try XCTUnwrap(targetResult?.locations?.only)
XCTAssertTrue(targetLocation.uri.pseudoPath.hasSuffix("generated.swift"))
XCTAssertEqual(targetLocation.range.lowerBound, Position(line: 1, utf16index: 4))
XCTAssertEqual(targetLocation.range.upperBound, Position(line: 1, utf16index: 4))
}
// Make a change to the top level input file of the plugin command
try await project.changeFileOnDisk(
"topDep.txt",
newMarkedContents: """
// some change
let topGenerated = 2
"""
)
try await project.testClient.send(SynchronizeRequest(index: true))
// Expect that the position has been updated in the dependency
try await repeatUntilExpectedResult {
let result = try await project.testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
let location = try XCTUnwrap(result?.locations?.only)
return location.range.lowerBound == Position(line: 1, utf16index: 4)
}
// Make a change to the target level input file of the plugin command
try await project.changeFileOnDisk(
"targetDep.txt",
newMarkedContents: """
// some change
let targetGenerated = 2
"""
)
try await project.testClient.send(SynchronizeRequest(index: true))
// Expect that the position has been updated in the dependency
try await repeatUntilExpectedResult {
let result = try await project.testClient.send(
DefinitionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
)
let location = try XCTUnwrap(result?.locations?.only)
return location.range.lowerBound == Position(line: 3, utf16index: 4)
}
}
func testChangePackageManifestFile() async throws {
let project = try await SwiftPMTestProject(
files: [
"Lib.swift": """
#if MY_FLAG
#error("MY_FLAG set")
#else
#error("MY_FLAG not set")
#endif
"""
],
manifest: """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [.target(name: "MyLibrary")]
)
"""
)
let (uri, _) = try project.openDocument("Lib.swift")
let initialDiagnostics = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(initialDiagnostics.fullReport?.items.map(\.message), ["MY_FLAG not set"])
try await project.changeFileOnDisk(
"Package.swift",
newMarkedContents: """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [.target(name: "MyLibrary", swiftSettings: [.define("MY_FLAG")])]
)
"""
)
try await repeatUntilExpectedResult {
let diagnosticsAfterUpdate = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
return diagnosticsAfterUpdate.fullReport?.items.map(\.message) == ["MY_FLAG set"]
}
}
func testChangeVersionSpecificPackageManifestFile() async throws {
let project = try await SwiftPMTestProject(
files: [
"Lib.swift": """
#if MY_FLAG
#error("MY_FLAG set")
#elseif MY_OTHER_FLAG
#error("MY_OTHER_FLAG set")
#else
#error("no flag set")
#endif
""",
"/Package@swift-6.1.swift": """
// swift-tools-version: 6.1
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [.target(name: "MyLibrary", swiftSettings: [.define("MY_FLAG")])]
)
""",
],
manifest: """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [.target(name: "MyLibrary")]
)
"""
)
let (uri, _) = try project.openDocument("Lib.swift")
let initialDiagnostics = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(initialDiagnostics.fullReport?.items.map(\.message), ["MY_FLAG set"])
try await project.changeFileOnDisk(
"Package@swift-6.1.swift",
newMarkedContents: """
// swift-tools-version: 6.1
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [.target(name: "MyLibrary", swiftSettings: [.define("MY_OTHER_FLAG")])]
)
"""
)
try await repeatUntilExpectedResult {
let diagnosticsAfterUpdate = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
return diagnosticsAfterUpdate.fullReport?.items.map(\.message) == ["MY_OTHER_FLAG set"]
}
}
func testAddVersionSpecificPackageManifestFile() async throws {
let project = try await SwiftPMTestProject(
files: [
"Lib.swift": """
#if MY_FLAG
#error("MY_FLAG set")
#else
#error("MY_FLAG not set")
#endif
"""
],
manifest: """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [.target(name: "MyLibrary")]
)
"""
)
let (uri, _) = try project.openDocument("Lib.swift")
let initialDiagnostics = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
XCTAssertEqual(initialDiagnostics.fullReport?.items.map(\.message), ["MY_FLAG not set"])
let versionSpecificManifestUrl = project.scratchDirectory.appending(component: "Package@swift-6.1.swift")
try await """
// swift-tools-version: 6.1
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [.target(name: "MyLibrary", swiftSettings: [.define("MY_FLAG")])]
)
""".writeWithRetry(to: versionSpecificManifestUrl)
project.testClient.send(
DidChangeWatchedFilesNotification(changes: [
FileEvent(uri: DocumentURI(versionSpecificManifestUrl), type: .created)
])
)
try await repeatUntilExpectedResult {
let diagnosticsAfterUpdate = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
return diagnosticsAfterUpdate.fullReport?.items.map(\.message) == ["MY_FLAG set"]
}
}
func testClearPreparationStatusWhenPackageManifestIsModifiedAndPackageIsOpenedWithoutTrailingSlash() async throws {
let project = try await SwiftPMTestProject(
files: [
"MyLibrary/MyLibrary.swift": """
#if MY_FLAG
public func foo() -> String { "" }
#else
public func foo() -> Int { "" }
#endif
""",
"MyExecutable/MyExecutable.swift": """
import MyLibrary
let 1️⃣x = foo()
""",
],
manifest: """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [
.target(name: "MyLibrary"),
.executableTarget(name: "MyExecutable", dependencies: ["MyLibrary"])
]
)
""",
workspaces: {
[WorkspaceFolder(uri: DocumentURI(try URL(filePath: $0.filePath, directoryHint: .notDirectory)))]
},
enableBackgroundIndexing: true
)
let (uri, positions) = try project.openDocument("MyExecutable.swift")
let preEditHoverResponse = try await project.testClient.send(
HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
assertContains(try XCTUnwrap(preEditHoverResponse?.contents.markupContent?.value), "let x: Int")
try await project.changeFileOnDisk(
"Package.swift",
newMarkedContents: """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [
.target(name: "MyLibrary", swiftSettings: [.define("MY_FLAG")]),
.executableTarget(name: "MyExecutable", dependencies: ["MyLibrary"])
]
)
"""
)
try await repeatUntilExpectedResult {
let postEditHoverResponse = try await project.testClient.send(
HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
return try XCTUnwrap(postEditHoverResponse?.contents.markupContent?.value).contains("let x: String")
}
}
// Ensure that when background indexing is disabled, we infer the build system based
// on existing products.
func testBuildSystemInference(buildSystem: SwiftPMBuildSystem) async throws {
let project = try await SwiftPMTestProject(
files: [
"Lib/Lib.swift": """
public func libFunction() -> Int { 0 }
""",
"MyClient/MyClient.swift": """
import Lib
func test() {
let _: String = libFunction()
}
""",
],
manifest: """
let package = Package(
name: "MyPackage",
targets: [
.target(name: "Lib"),
.executableTarget(name: "MyClient", dependencies: ["Lib"]),
]
)
""",
workspaces: { scratchDirectory in
// This build is expected to fail, but it will produce the 'Lib' module needed when we fetch diagnostics.
try? await SwiftPMTestProject.build(at: scratchDirectory, buildSystem: buildSystem)
return [WorkspaceFolder(uri: DocumentURI(scratchDirectory))]
},
enableBackgroundIndexing: false,
)
let (uri, _) = try project.openDocument("MyClient.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'"]
)
}
@inline(never) // https://github.com/swiftlang/swift/issues/88199
func testBuildSystemInference_Native() async throws {
try await testBuildSystemInference(buildSystem: .native)
}
@inline(never) // https://github.com/swiftlang/swift/issues/88199
func testBuildSystemInference_SwiftBuild() async throws {
try await SkipUnless.swiftPMBuildServerSupportedWithoutBackgroundIndexing()
try await testBuildSystemInference(buildSystem: .swiftbuild)
}
func testExperimentalFeaturesPassedToSyntaxTreeManager() async throws {
let project = try await SwiftPMTestProject(
files: [
"Test.swift": """
struct Foo {
var x: Int
}
"""
],
manifest: """
let package = Package(
name: "MyLibrary",
targets: [
.target(
name: "MyLibrary",
swiftSettings: [.enableExperimentalFeature("_test_EverythingUnexpected")]
)
]
)
""",
// 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 (uri, _) = try project.openDocument("Test.swift")
// DocumentSymbolRequest uses SyntaxTreeManager directly (not sourcekitd).
// After package loading, build settings include -enable-experimental-feature _test_EverythingUnexpected,
// which gets extracted and passed to the parser via SyntaxTreeManager.
// With this feature, all content is parsed as unexpected nodes and statements are empty,
// so DocumentSymbolRequest should return empty symbols (no struct Foo, no var x).
let responseWithFeature = try await project.testClient.send(
DocumentSymbolRequest(textDocument: TextDocumentIdentifier(uri))
)
guard case .documentSymbols(let symbolsWithFeature) = responseWithFeature else {
XCTFail("Expected document symbols")
return
}
// With _test_EverythingUnexpected, all content is parsed as unexpected,
// so there should be no symbols found
XCTAssertTrue(
symbolsWithFeature.isEmpty,
"Expected no symbols with _test_EverythingUnexpected feature, but got '\(symbolsWithFeature)' (all content parsed as unexpected)"
)
// Now update Package.swift to remove the experimental feature flag
let newManifest = """
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyLibrary",
targets: [
.target(name: "MyLibrary")
]
)
"""
try await project.changeFileOnDisk(
"Package.swift",
newMarkedContents: newManifest
)
// Wait for the build server to update the build graph after Package.swift changes.
// SynchronizeRequest(index: true) calls buildServerManager.waitForUpToDateBuildGraph()
// which ensures SwiftPM has finished re-parsing Package.swift and updated build settings.
try await project.testClient.send(SynchronizeRequest(index: true))
// After removing the experimental feature from Package.swift, the parser should
// produce a proper syntax tree, and DocumentSymbolRequest should find the symbols.
let response = try await project.testClient.send(
DocumentSymbolRequest(textDocument: TextDocumentIdentifier(uri))
)
guard case .documentSymbols(let symbols) = response else {
XCTFail("Expected document symbols")
return
}
// Check that we now find the struct Foo and var x
let fooSymbol = try XCTUnwrap(symbols.first(where: { $0.name == "Foo" }))
XCTAssertEqual(fooSymbol.kind, .struct)
let xSymbol = try XCTUnwrap(fooSymbol.children?.first(where: { $0.name == "x" }))
XCTAssertEqual(xSymbol.kind, .property)
}
}