Skip to content

Commit d8a8e3c

Browse files
committed
Merge remote-tracking branch 'swiftlang/main' into release/6.1
2 parents f57980a + 1cd010d commit d8a8e3c

File tree

16 files changed

+306
-74
lines changed

16 files changed

+306
-74
lines changed

Sources/BuildSystemIntegration/BuildSystemManager.swift

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,21 @@ package struct SourceFileInfo: Sendable {
5454
/// from non-test targets or files that don't actually contain any tests.
5555
package var mayContainTests: Bool
5656

57+
/// Source files returned here fall into two categories:
58+
/// - Buildable source files are files that can be built by the build system and that make sense to background index
59+
/// - Non-buildable source files include eg. the SwiftPM package manifest or header files. We have sufficient
60+
/// compiler arguments for these files to provide semantic editor functionality but we can't build them.
61+
package var isBuildable: Bool
62+
5763
fileprivate func merging(_ other: SourceFileInfo?) -> SourceFileInfo {
5864
guard let other else {
5965
return self
6066
}
6167
return SourceFileInfo(
6268
targets: targets.union(other.targets),
6369
isPartOfRootProject: other.isPartOfRootProject || isPartOfRootProject,
64-
mayContainTests: other.mayContainTests || mayContainTests
70+
mayContainTests: other.mayContainTests || mayContainTests,
71+
isBuildable: other.isBuildable || isBuildable
6572
)
6673
}
6774
}
@@ -327,11 +334,9 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
327334

328335
private var cachedTargetSources = RequestCache<BuildTargetSourcesRequest>()
329336

330-
/// The parameters with which `SourceFilesAndDirectories` can be cached in `cachedSourceFilesAndDirectories`.
331-
private struct SourceFilesAndDirectoriesKey: Hashable {
332-
let includeNonBuildableFiles: Bool
333-
let sourcesItems: [SourcesItem]
334-
}
337+
/// `SourceFilesAndDirectories` is a global property that only gets reset when the build targets change and thus
338+
/// has no real key.
339+
private struct SourceFilesAndDirectoriesKey: Hashable {}
335340

336341
private struct SourceFilesAndDirectories {
337342
/// The source files in the workspace, ie. all `SourceItem`s that have `kind == .file`.
@@ -678,7 +683,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
678683
package func targets(for document: DocumentURI) async -> Set<BuildTargetIdentifier> {
679684
return await orLog("Getting targets for source file") {
680685
var result: Set<BuildTargetIdentifier> = []
681-
let filesAndDirectories = try await sourceFilesAndDirectories(includeNonBuildableFiles: true)
686+
let filesAndDirectories = try await sourceFilesAndDirectories()
682687
if let targets = filesAndDirectories.files[document]?.targets {
683688
result.formUnion(targets)
684689
}
@@ -1033,50 +1038,44 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
10331038
return response.items
10341039
}
10351040

1036-
/// Returns all source files in the project that can be built.
1041+
/// Returns all source files in the project.
10371042
///
10381043
/// - SeeAlso: Comment in `sourceFilesAndDirectories` for a definition of what `buildable` means.
1039-
package func buildableSourceFiles() async throws -> [DocumentURI: SourceFileInfo] {
1040-
return try await sourceFilesAndDirectories(includeNonBuildableFiles: false).files
1044+
package func sourceFiles(includeNonBuildableFiles: Bool) async throws -> [DocumentURI: SourceFileInfo] {
1045+
let files = try await sourceFilesAndDirectories().files
1046+
if includeNonBuildableFiles {
1047+
return files
1048+
} else {
1049+
return files.filter(\.value.isBuildable)
1050+
}
10411051
}
10421052

10431053
/// Get all files and directories that are known to the build system, ie. that are returned by a `buildTarget/sources`
10441054
/// request for any target in the project.
10451055
///
1046-
/// Source files returned here fall into two categories:
1047-
/// - Buildable source files are files that can be built by the build system and that make sense to background index
1048-
/// - Non-buildable source files include eg. the SwiftPM package manifest or header files. We have sufficient
1049-
/// compiler arguments for these files to provide semantic editor functionality but we can't build them.
1050-
///
1051-
/// `includeNonBuildableFiles` determines whether non-buildable files should be included.
1052-
private func sourceFilesAndDirectories(includeNonBuildableFiles: Bool) async throws -> SourceFilesAndDirectories {
1053-
let targets = try await self.buildTargets()
1054-
let sourcesItems = try await self.sourceFiles(in: Set(targets.keys))
1055-
1056-
let key = SourceFilesAndDirectoriesKey(
1057-
includeNonBuildableFiles: includeNonBuildableFiles,
1058-
sourcesItems: sourcesItems
1059-
)
1056+
/// - Important: This method returns both buildable and non-buildable source files. Callers need to check
1057+
/// `SourceFileInfo.isBuildable` if they are only interested in buildable source files.
1058+
private func sourceFilesAndDirectories() async throws -> SourceFilesAndDirectories {
1059+
return try await cachedSourceFilesAndDirectories.get(
1060+
SourceFilesAndDirectoriesKey(),
1061+
isolation: self
1062+
) { key in
1063+
let targets = try await self.buildTargets()
1064+
let sourcesItems = try await self.sourceFiles(in: Set(targets.keys))
10601065

1061-
return try await cachedSourceFilesAndDirectories.get(key, isolation: self) { key in
10621066
var files: [DocumentURI: SourceFileInfo] = [:]
10631067
var directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)] = [:]
1064-
for sourcesItem in key.sourcesItems {
1068+
for sourcesItem in sourcesItems {
10651069
let target = targets[sourcesItem.target]?.target
10661070
let isPartOfRootProject = !(target?.tags.contains(.dependency) ?? false)
10671071
let mayContainTests = target?.tags.contains(.test) ?? true
1068-
if !key.includeNonBuildableFiles && (target?.tags.contains(.notBuildable) ?? false) {
1069-
continue
1070-
}
1071-
10721072
for sourceItem in sourcesItem.sources {
1073-
if !key.includeNonBuildableFiles && sourceItem.sourceKitData?.isHeader ?? false {
1074-
continue
1075-
}
10761073
let info = SourceFileInfo(
10771074
targets: [sourcesItem.target],
10781075
isPartOfRootProject: isPartOfRootProject,
1079-
mayContainTests: mayContainTests
1076+
mayContainTests: mayContainTests,
1077+
isBuildable: !(target?.tags.contains(.notBuildable) ?? false)
1078+
&& !(sourceItem.sourceKitData?.isHeader ?? false)
10801079
)
10811080
switch sourceItem.kind {
10821081
case .file:
@@ -1093,7 +1092,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
10931092
}
10941093

10951094
package func testFiles() async throws -> [DocumentURI] {
1096-
return try await buildableSourceFiles().compactMap { (uri, info) -> DocumentURI? in
1095+
return try await sourceFiles(includeNonBuildableFiles: false).compactMap { (uri, info) -> DocumentURI? in
10971096
guard info.isPartOfRootProject, info.mayContainTests else {
10981097
return nil
10991098
}

Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ package actor CompilationDatabaseBuildSystem: BuiltInBuildSystem {
9999
let args = command.commandLine
100100
for i in args.indices.reversed() {
101101
if args[i] == "-index-store-path" && i + 1 < args.count {
102-
return URL(fileURLWithPath: args[i + 1])
102+
return URL(
103+
fileURLWithPath: args[i + 1],
104+
relativeTo: URL(fileURLWithPath: command.directory, isDirectory: true)
105+
)
103106
}
104107
}
105108
}

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
587587
SourceItem(
588588
uri: DocumentURI($0),
589589
kind: $0.isDirectory ? .directory : .file,
590-
generated: false,
590+
generated: false
591591
)
592592
}
593593
result.append(SourcesItem(target: target, sources: sources))

Sources/LanguageServerProtocol/SupportTypes/DocumentURI.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,21 @@ public struct DocumentURI: Codable, Hashable, Sendable {
5656
/// fallback mode that drops semantic functionality.
5757
public var pseudoPath: String {
5858
if storage.isFileURL {
59-
return storage.withUnsafeFileSystemRepresentation { filePath in
60-
if let filePath {
61-
String(cString: filePath)
62-
} else {
63-
""
59+
return storage.withUnsafeFileSystemRepresentation { filePathPtr in
60+
guard let filePathPtr else {
61+
return ""
6462
}
63+
let filePath = String(cString: filePathPtr)
64+
#if os(Windows)
65+
// VS Code spells file paths with a lowercase drive letter, while the rest of Windows APIs use an uppercase
66+
// drive letter. Normalize the drive letter spelling to be uppercase.
67+
if filePath.first?.isASCII ?? false, filePath.first?.isLetter ?? false, filePath.first?.isLowercase ?? false,
68+
filePath.count > 1, filePath[filePath.index(filePath.startIndex, offsetBy: 1)] == ":"
69+
{
70+
return filePath.first!.uppercased() + filePath.dropFirst()
71+
}
72+
#endif
73+
return filePath
6574
}
6675
} else {
6776
return storage.absoluteString

Sources/SKTestSupport/MultiFileTestProject.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ package class MultiFileTestProject {
8888
/// File contents can also contain `$TEST_DIR`, which gets replaced by the temporary directory.
8989
package init(
9090
files: [RelativeFileLocation: String],
91-
workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
91+
workspaces: (_ scratchDirectory: URL) async throws -> [WorkspaceFolder] = {
92+
[WorkspaceFolder(uri: DocumentURI($0))]
93+
},
9294
initializationOptions: LSPAny? = nil,
9395
capabilities: ClientCapabilities = ClientCapabilities(),
9496
options: SourceKitLSPOptions = .testDefault(),

Sources/SKTestSupport/SkipUnless.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,10 @@ package actor SkipUnless {
418418
try XCTSkipUnless(Platform.current == .darwin, message)
419419
}
420420

421+
package static func platformIsWindows(_ message: String) throws {
422+
try XCTSkipUnless(Platform.current == .windows, message)
423+
}
424+
421425
package static func platformSupportsTaskPriorityElevation() throws {
422426
#if os(macOS)
423427
guard #available(macOS 14.0, *) else {

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ package class SwiftPMTestProject: MultiFileTestProject {
163163
package init(
164164
files: [RelativeFileLocation: String],
165165
manifest: String = SwiftPMTestProject.defaultPackageManifest,
166-
workspaces: (URL) async throws -> [WorkspaceFolder] = { [WorkspaceFolder(uri: DocumentURI($0))] },
166+
workspaces: (_ scratchDirectory: URL) async throws -> [WorkspaceFolder] = {
167+
[WorkspaceFolder(uri: DocumentURI($0))]
168+
},
167169
initializationOptions: LSPAny? = nil,
168170
capabilities: ClientCapabilities = ClientCapabilities(),
169171
options: SourceKitLSPOptions = .testDefault(),

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ package final actor SemanticIndexManager {
291291
filesToIndex
292292
} else {
293293
await orLog("Getting files to index") {
294-
try await self.buildSystemManager.buildableSourceFiles().keys.sorted { $0.stringValue < $1.stringValue }
294+
try await self.buildSystemManager.sourceFiles(includeNonBuildableFiles: false).keys
295+
.sorted { $0.stringValue < $1.stringValue }
295296
} ?? []
296297
}
297298
if !indexFilesWithUpToDateUnit {
@@ -408,7 +409,7 @@ package final actor SemanticIndexManager {
408409
toCover files: some Collection<DocumentURI> & Sendable
409410
) async -> [FileToIndex] {
410411
let sourceFiles = await orLog("Getting source files in project") {
411-
Set(try await buildSystemManager.buildableSourceFiles().keys)
412+
Set(try await buildSystemManager.sourceFiles(includeNonBuildableFiles: false).keys)
412413
}
413414
guard let sourceFiles else {
414415
return []

Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,27 @@ actor DiagnosticReportManager {
110110
keys.compilerArgs: compilerArgs as [SKDRequestValue],
111111
])
112112

113-
let dict = try await self.sourcekitd.send(
114-
skreq,
115-
timeout: options.sourcekitdRequestTimeoutOrDefault,
116-
fileContents: snapshot.text
117-
)
113+
let dict: SKDResponseDictionary
114+
do {
115+
dict = try await self.sourcekitd.send(
116+
skreq,
117+
timeout: options.sourcekitdRequestTimeoutOrDefault,
118+
fileContents: snapshot.text
119+
)
120+
} catch SKDError.requestFailed(let sourcekitdError) {
121+
var errorMessage = sourcekitdError
122+
if errorMessage.hasPrefix("error response (Request Failed): error: ") {
123+
errorMessage = String(errorMessage.dropFirst(40))
124+
}
125+
return RelatedFullDocumentDiagnosticReport(items: [
126+
Diagnostic(
127+
range: Position(line: 0, utf16index: 0)..<Position(line: 0, utf16index: 0),
128+
severity: .error,
129+
source: "SourceKit",
130+
message: "Internal SourceKit error: \(errorMessage)"
131+
)
132+
])
133+
}
118134

119135
try Task.checkCancellation()
120136

Sources/SourceKitLSP/Workspace.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,51 @@ fileprivate func firstNonNil<T>(
6262
return try await defaultValue()
6363
}
6464

65+
/// Actor that caches realpaths for `sourceFilesWithSameRealpath`.
66+
fileprivate actor SourceFilesWithSameRealpathInferrer {
67+
private let buildSystemManager: BuildSystemManager
68+
private var realpathCache: [DocumentURI: DocumentURI] = [:]
69+
70+
init(buildSystemManager: BuildSystemManager) {
71+
self.buildSystemManager = buildSystemManager
72+
}
73+
74+
private func realpath(of uri: DocumentURI) -> DocumentURI {
75+
if let cached = realpathCache[uri] {
76+
return cached
77+
}
78+
let value = uri.symlinkTarget ?? uri
79+
realpathCache[uri] = value
80+
return value
81+
}
82+
83+
/// Returns the URIs of all source files in the project that have the same realpath as a document in `documents` but
84+
/// are not in `documents`.
85+
///
86+
/// This is useful in the following scenario: A project has target A containing A.swift an target B containing B.swift
87+
/// B.swift is a symlink to A.swift. When A.swift is modified, both the dependencies of A and B need to be marked as
88+
/// having an out-of-date preparation status, not just A.
89+
package func sourceFilesWithSameRealpath(as documents: [DocumentURI]) async -> [DocumentURI] {
90+
let realPaths = Set(documents.map { realpath(of: $0) })
91+
return await orLog("Determining source files with same realpath") {
92+
var result: [DocumentURI] = []
93+
let filesAndDirectories = try await buildSystemManager.sourceFiles(includeNonBuildableFiles: true)
94+
for file in filesAndDirectories.keys {
95+
if realPaths.contains(realpath(of: file)) && !documents.contains(file) {
96+
result.append(file)
97+
}
98+
}
99+
return result
100+
} ?? []
101+
}
102+
103+
func filesDidChange(_ events: [FileEvent]) {
104+
for event in events {
105+
realpathCache[event.uri] = nil
106+
}
107+
}
108+
}
109+
65110
/// Represents the configuration and state of a project or combination of projects being worked on
66111
/// together.
67112
///
@@ -86,6 +131,8 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
86131
/// The build system manager to use for documents in this workspace.
87132
package let buildSystemManager: BuildSystemManager
88133

134+
private let sourceFilesWithSameRealpathInferrer: SourceFilesWithSameRealpathInferrer
135+
89136
let options: SourceKitLSPOptions
90137

91138
/// The source code index, if available.
@@ -126,6 +173,9 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
126173
self.options = options
127174
self._uncheckedIndex = ThreadSafeBox(initialValue: uncheckedIndex)
128175
self.buildSystemManager = buildSystemManager
176+
self.sourceFilesWithSameRealpathInferrer = SourceFilesWithSameRealpathInferrer(
177+
buildSystemManager: buildSystemManager
178+
)
129179
if options.backgroundIndexingOrDefault, let uncheckedIndex,
130180
await buildSystemManager.initializationData?.prepareProvider ?? false
131181
{
@@ -316,6 +366,17 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
316366
}
317367

318368
package func filesDidChange(_ events: [FileEvent]) async {
369+
// First clear any cached realpaths in `sourceFilesWithSameRealpathInferrer`.
370+
await sourceFilesWithSameRealpathInferrer.filesDidChange(events)
371+
372+
// Now infer any edits for source files that share the same realpath as one of the modified files.
373+
var events = events
374+
events +=
375+
await sourceFilesWithSameRealpathInferrer
376+
.sourceFilesWithSameRealpath(as: events.filter { $0.type == .changed }.map(\.uri))
377+
.map { FileEvent(uri: $0, type: .changed) }
378+
379+
// Notify all clients about the reported and inferred edits.
319380
await buildSystemManager.filesDidChange(events)
320381
await syntacticTestIndex.filesDidChange(events)
321382
await semanticIndexManager?.filesDidChange(events)

Sources/SwiftExtensions/URLExtensions.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,21 @@ extension URL {
6767
guard self.isFileURL else {
6868
throw FilePathError.noFileURL(self)
6969
}
70-
return try self.withUnsafeFileSystemRepresentation { buffer in
71-
guard let buffer else {
70+
return try self.withUnsafeFileSystemRepresentation { filePathPtr in
71+
guard let filePathPtr else {
7272
throw FilePathError.noFileSystemRepresentation(self)
7373
}
74-
return String(cString: buffer)
74+
let filePath = String(cString: filePathPtr)
75+
#if os(Windows)
76+
// VS Code spells file paths with a lowercase drive letter, while the rest of Windows APIs use an uppercase
77+
// drive letter. Normalize the drive letter spelling to be uppercase.
78+
if filePath.first?.isASCII ?? false, filePath.first?.isLetter ?? false, filePath.first?.isLowercase ?? false,
79+
filePath.count > 1, filePath[filePath.index(filePath.startIndex, offsetBy: 1)] == ":"
80+
{
81+
return filePath.first!.uppercased() + filePath.dropFirst()
82+
}
83+
#endif
84+
return filePath
7585
}
7686
}
7787
}

0 commit comments

Comments
 (0)