Skip to content

Commit c0cd2ac

Browse files
committed
Add contextual request support to sourcekit-lsp diagnose
Teach `sourcekit-lsp diagnose` how to extract contextual requests from the system log and use them to reduce sourcekitd crashes.
1 parent 4516114 commit c0cd2ac

14 files changed

+313
-95
lines changed

Sources/Diagnose/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_library(Diagnose STATIC
1919
StderrStreamConcurrencySafe.swift
2020
SwiftFrontendCrashScraper.swift
2121
Toolchain+SwiftFrontend.swift
22+
Toolchain+PluginPaths.swift
2223
TraceFromSignpostsCommand.swift)
2324

2425
set_target_properties(Diagnose PROPERTIES

Sources/Diagnose/DiagnoseCommand.swift

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package import ArgumentParser
1414
import Foundation
1515
import LanguageServerProtocolExtensions
16+
import SKLogging
1617
import SwiftExtensions
1718
import TSCExtensions
1819
import ToolchainRegistry
@@ -136,6 +137,7 @@ package struct DiagnoseCommand: AsyncParsableCommand {
136137
break
137138
} catch {
138139
// Reducing this request failed. Continue reducing the next one, maybe that one succeeds.
140+
logger.info("Reducing sourcekitd crash failed: \(error.forLogging)")
139141
}
140142
}
141143
}
@@ -173,6 +175,7 @@ package struct DiagnoseCommand: AsyncParsableCommand {
173175

174176
let executor = OutOfProcessSourceKitRequestExecutor(
175177
sourcekitd: sourcekitd,
178+
pluginPaths: toolchain.pluginPaths,
176179
swiftFrontend: crashInfo.swiftFrontend,
177180
reproducerPredicate: nil
178181
)
@@ -457,6 +460,7 @@ package struct DiagnoseCommand: AsyncParsableCommand {
457460
let requestInfo = requestInfo
458461
let executor = OutOfProcessSourceKitRequestExecutor(
459462
sourcekitd: sourcekitd,
463+
pluginPaths: toolchain.pluginPaths,
460464
swiftFrontend: swiftFrontend,
461465
reproducerPredicate: nil
462466
)

Sources/Diagnose/MergeSwiftFiles.swift

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ extension RequestInfo {
3131
let compilerArgs = compilerArgs.filter { $0 != "-primary-file" && !$0.hasSuffix(".swift") } + ["$FILE"]
3232
let mergedRequestInfo = RequestInfo(
3333
requestTemplate: requestTemplate,
34+
contextualRequestTemplates: contextualRequestTemplates,
3435
offset: offset,
3536
compilerArgs: compilerArgs,
3637
fileContents: mergedFile

Sources/Diagnose/OSLogScraper.swift

+64-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
#if canImport(OSLog)
1414
import OSLog
15+
import SKLogging
16+
import RegexBuilder
1517

1618
/// Reads oslog messages to find recent sourcekitd crashes.
1719
struct OSLogScraper {
@@ -45,34 +47,90 @@ struct OSLogScraper {
4547
#"subsystem CONTAINS "sourcekit-lsp" AND composedMessage CONTAINS "sourcekitd crashed" AND category = %@"#,
4648
logCategory
4749
)
48-
var isInFileContentSection = false
50+
enum LogSection {
51+
case request
52+
case fileContents
53+
case contextualRequest
54+
}
55+
var section = LogSection.request
4956
var request = ""
5057
var fileContents = ""
58+
var contextualRequests: [String] = []
59+
let sourcekitdCrashedRegex = Regex {
60+
"sourcekitd crashed ("
61+
OneOrMore(.digit)
62+
"/"
63+
OneOrMore(.digit)
64+
")"
65+
}
66+
let contextualRequestRegex = Regex {
67+
"Contextual request "
68+
OneOrMore(.digit)
69+
" / "
70+
OneOrMore(.digit)
71+
":"
72+
}
73+
5174
for entry in try getLogEntries(matching: predicate) {
5275
for line in entry.composedMessage.components(separatedBy: "\n") {
53-
if line.starts(with: "sourcekitd crashed (") {
76+
if try sourcekitdCrashedRegex.wholeMatch(in: line) != nil {
5477
continue
5578
}
5679
if line == "Request:" {
5780
continue
5881
}
5982
if line == "File contents:" {
60-
isInFileContentSection = true
83+
section = .fileContents
84+
continue
85+
}
86+
if line == "File contents:" {
87+
section = .fileContents
88+
continue
89+
}
90+
if try contextualRequestRegex.wholeMatch(in: line) != nil {
91+
section = .contextualRequest
92+
contextualRequests.append("")
6193
continue
6294
}
6395
if line == "--- End Chunk" {
6496
continue
6597
}
66-
if isInFileContentSection {
67-
fileContents += line + "\n"
68-
} else {
98+
switch section {
99+
case .request:
69100
request += line + "\n"
101+
case .fileContents:
102+
fileContents += line + "\n"
103+
case .contextualRequest:
104+
if !contextualRequests.isEmpty {
105+
contextualRequests[contextualRequests.count - 1] += line + "\n"
106+
} else {
107+
// Should never happen because we have appended at least one element to `contextualRequests` when switching
108+
// to the `contextualRequest` section.
109+
logger.fault("Dropping contextual request line: \(line)")
110+
}
70111
}
71112
}
72113
}
73114

74115
var requestInfo = try RequestInfo(request: request)
116+
117+
let contextualRequestInfos = contextualRequests.compactMap { contextualRequest in
118+
orLog("Processsing contextual request") {
119+
try RequestInfo(request: contextualRequest)
120+
}
121+
}.filter { contextualRequest in
122+
if contextualRequest.fileContents != requestInfo.fileContents {
123+
logger.error("Contextual request concerns a different file than the crashed request. Ignoring it")
124+
return false
125+
}
126+
return true
127+
}
128+
requestInfo.contextualRequestTemplates = contextualRequestInfos.map(\.requestTemplate)
129+
if requestInfo.compilerArgs.isEmpty {
130+
requestInfo.compilerArgs = contextualRequestInfos.last(where: { !$0.compilerArgs.isEmpty })?.compilerArgs ?? []
131+
}
75132
requestInfo.fileContents = fileContents
133+
76134
return requestInfo
77135
}
78136

Sources/Diagnose/ReduceCommand.swift

+9-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
package import ArgumentParser
1414
import Foundation
15+
import SourceKitD
1516
import ToolchainRegistry
1617

1718
import struct TSCBasic.AbsolutePath
@@ -68,12 +69,16 @@ package struct ReduceCommand: AsyncParsableCommand {
6869

6970
@MainActor
7071
package func run() async throws {
71-
guard let sourcekitd = try await toolchain?.sourcekitd else {
72+
guard let toolchain = try await toolchain else {
73+
throw GenericError("Unable to find toolchain")
74+
}
75+
guard let sourcekitd = toolchain.sourcekitd else {
7276
throw GenericError("Unable to find sourcekitd.framework")
7377
}
74-
guard let swiftFrontend = try await toolchain?.swiftFrontend else {
78+
guard let swiftFrontend = toolchain.swiftFrontend else {
7579
throw GenericError("Unable to find sourcekitd.framework")
7680
}
81+
let pluginPaths = toolchain.pluginPaths
7782

7883
let progressBar = PercentProgressAnimation(stream: stderrStreamConcurrencySafe, header: "Reducing sourcekitd issue")
7984

@@ -82,6 +87,7 @@ package struct ReduceCommand: AsyncParsableCommand {
8287

8388
let executor = OutOfProcessSourceKitRequestExecutor(
8489
sourcekitd: sourcekitd,
90+
pluginPaths: pluginPaths,
8591
swiftFrontend: swiftFrontend,
8692
reproducerPredicate: nsPredicate
8793
)
@@ -96,6 +102,6 @@ package struct ReduceCommand: AsyncParsableCommand {
96102
try reduceRequestInfo.fileContents.write(to: reducedSourceFile, atomically: true, encoding: .utf8)
97103

98104
print("Reduced Request:")
99-
print(try reduceRequestInfo.request(for: reducedSourceFile))
105+
print(try reduceRequestInfo.requests(for: reducedSourceFile).joined(separator: "\n\n\n\n"))
100106
}
101107
}

Sources/Diagnose/ReduceFrontendCommand.swift

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ package struct ReduceFrontendCommand: AsyncParsableCommand {
9090

9191
let executor = OutOfProcessSourceKitRequestExecutor(
9292
sourcekitd: sourcekitd,
93+
pluginPaths: nil,
9394
swiftFrontend: swiftFrontend,
9495
reproducerPredicate: nsPredicate
9596
)

Sources/Diagnose/ReproducerBundle.swift

+8-6
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ func makeReproducerBundle(for requestInfo: RequestInfo, toolchain: Toolchain, bu
4040
+ requestInfo.compilerArgs.replacing(["$FILE"], with: ["./input.swift"]).joined(separator: " \\\n")
4141
try command.write(to: bundlePath.appendingPathComponent("command.sh"), atomically: true, encoding: .utf8)
4242
} else {
43-
let request = try requestInfo.request(for: URL(fileURLWithPath: "/input.swift"))
44-
try request.write(
45-
to: bundlePath.appendingPathComponent("request.yml"),
46-
atomically: true,
47-
encoding: .utf8
48-
)
43+
let requests = try requestInfo.requests(for: bundlePath.appendingPathComponent("input.swift"))
44+
for (index, request) in requests.enumerated() {
45+
try request.write(
46+
to: bundlePath.appendingPathComponent("request-\(index).yml"),
47+
atomically: true,
48+
encoding: .utf8
49+
)
50+
}
4951
}
5052
for compilerArg in requestInfo.compilerArgs {
5153
// Find the first slash so we are also able to copy files from eg.

0 commit comments

Comments
 (0)