Skip to content

Commit 194cde2

Browse files
committed
Add a subcommand to sourcekit-lsp that automatically reduces sourcekitd crashes
Essentially, this is adding three pieces of functionality: 1. `sourcekit-lsp run-sourcekitd-request` takes a JSON sourcekitd request and a path to `sourcekitd.framework` and executes that request using that sourcekitd. This is basically equivalent to `sourcekitd-test -json-request-path` but it works even if the toolchain with `sourcekitd.framework` doesn’t have a `sourcekitd-test`. 2. `sourcekit-lsp diagnose --request-file` takes a path to a JSON sourcekitd request, gets the source file’s contents from disk and reduces the source file + compiler arguments to create a reduced reproducer. It then copies all files referenced from the compiler arguments to a temporary folder and asks the user to file an issue with that information. 3. `sourcekit-lsp diagnose` without arguments is similar to (2) but looks in OSLog for the last sourcekitd crash.
1 parent a9242f8 commit 194cde2

16 files changed

+1076
-6
lines changed

Package.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ if forceNonDarwinLogger {
3939

4040
let package = Package(
4141
name: "SourceKitLSP",
42-
platforms: [.macOS("12.0")],
42+
platforms: [.macOS(.v13)],
4343
products: [
4444
.executable(name: "sourcekit-lsp", targets: ["sourcekit-lsp"]),
4545
.library(name: "_SourceKitLSP", targets: ["SourceKitLSP"]),
@@ -60,6 +60,7 @@ let package = Package(
6060
.executableTarget(
6161
name: "sourcekit-lsp",
6262
dependencies: [
63+
"Diagnose",
6364
"LanguageServerProtocol",
6465
"LanguageServerProtocolJSONRPC",
6566
"SKCore",
@@ -97,6 +98,19 @@ let package = Package(
9798
exclude: ["CMakeLists.txt"]
9899
),
99100

101+
.target(
102+
name: "Diagnose",
103+
dependencies: [
104+
"SourceKitD",
105+
"SKCore",
106+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
107+
.product(name: "SwiftSyntax", package: "swift-syntax"),
108+
.product(name: "SwiftParser", package: "swift-syntax"),
109+
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
110+
],
111+
exclude: ["CMakeLists.txt"]
112+
),
113+
100114
// MARK: LanguageServerProtocol
101115
// The core LSP types, suitable for any LSP implementation.
102116
.target(

Sources/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
add_subdirectory(BuildServerProtocol)
22
add_subdirectory(Csourcekitd)
3+
add_subdirectory(Diagnose)
34
add_subdirectory(LanguageServerProtocol)
45
add_subdirectory(LanguageServerProtocolJSONRPC)
56
add_subdirectory(LSPLogging)

Sources/Diagnose/CMakeLists.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
add_library(Diagnose STATIC
2+
CommandLineArgumentsReducer.swift
3+
DiagnoseCommand.swift
4+
FileReducer.swift
5+
FixItApplier.swift
6+
OSLogScraper.swift
7+
ReductionError.swift
8+
ReproducerBundle.swift
9+
RequestInfo.swift
10+
SourceKitDRequestExecutor.swift
11+
SourcekitdRequestCommand.swift)
12+
13+
set_target_properties(Diagnose PROPERTIES
14+
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
15+
16+
target_link_libraries(Diagnose PUBLIC
17+
SKCore
18+
SourceKitD
19+
ArgumentParser
20+
SwiftSyntax::SwiftSyntax
21+
SwiftSyntax::SwiftParser
22+
TSCBasic
23+
)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
/// Reduces the compiler arguments needed to reproduce a sourcekitd crash.
16+
class CommandLineArgumentReducer {
17+
/// The executor that is used to run a sourcekitd request and check whether it
18+
/// still crashes.
19+
private let sourcekitdExecutor: SourceKitRequestExecutor
20+
21+
/// The file to which we write the reduced source code.
22+
private let temporarySourceFile: URL
23+
24+
init(sourcekitdExecutor: SourceKitRequestExecutor) {
25+
self.sourcekitdExecutor = sourcekitdExecutor
26+
temporarySourceFile = FileManager.default.temporaryDirectory.appendingPathComponent("reduce.swift")
27+
}
28+
29+
func logSuccessfulReduction(_ requestInfo: RequestInfo) {
30+
print("Reduced compiler arguments to \(requestInfo.compilerArgs.count)")
31+
}
32+
33+
func run(initialRequestInfo: RequestInfo) async throws -> RequestInfo {
34+
try initialRequestInfo.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8)
35+
var requestInfo = initialRequestInfo
36+
37+
var argumentIndexToRemove = requestInfo.compilerArgs.count - 1
38+
while argumentIndexToRemove >= 0 {
39+
var numberOfArgumentsToRemove = 1
40+
// If the argument is preceded by -Xswiftc or -Xcxx, we need to remove the `-X` flag as well.
41+
if argumentIndexToRemove - numberOfArgumentsToRemove >= 0
42+
&& requestInfo.compilerArgs[argumentIndexToRemove - numberOfArgumentsToRemove].hasPrefix("-X")
43+
{
44+
numberOfArgumentsToRemove += 1
45+
}
46+
47+
if let reduced = try await tryRemoving(
48+
(argumentIndexToRemove - numberOfArgumentsToRemove + 1)...argumentIndexToRemove,
49+
from: requestInfo
50+
) {
51+
requestInfo = reduced
52+
argumentIndexToRemove -= numberOfArgumentsToRemove
53+
continue
54+
}
55+
56+
// If removing the argument failed and the argument is preceded by an argument starting with `-`, try removing that as well.
57+
// E.g. removing `-F` followed by a search path.
58+
if argumentIndexToRemove - numberOfArgumentsToRemove >= 0
59+
&& requestInfo.compilerArgs[argumentIndexToRemove - numberOfArgumentsToRemove].hasPrefix("-")
60+
{
61+
numberOfArgumentsToRemove += 1
62+
}
63+
64+
// If the argument is preceded by -Xswiftc or -Xcxx, we need to remove the `-X` flag as well.
65+
if argumentIndexToRemove - numberOfArgumentsToRemove >= 0
66+
&& requestInfo.compilerArgs[argumentIndexToRemove - numberOfArgumentsToRemove].hasPrefix("-X")
67+
{
68+
numberOfArgumentsToRemove += 1
69+
}
70+
71+
if let reduced = try await tryRemoving(
72+
(argumentIndexToRemove - numberOfArgumentsToRemove + 1)...argumentIndexToRemove,
73+
from: requestInfo
74+
) {
75+
requestInfo = reduced
76+
argumentIndexToRemove -= numberOfArgumentsToRemove
77+
continue
78+
}
79+
argumentIndexToRemove -= 1
80+
}
81+
82+
return requestInfo
83+
}
84+
85+
private func tryRemoving(
86+
_ argumentsToRemove: ClosedRange<Int>,
87+
from requestInfo: RequestInfo
88+
) async throws -> RequestInfo? {
89+
var reducedRequestInfo = requestInfo
90+
reducedRequestInfo.compilerArgs.removeSubrange(argumentsToRemove)
91+
92+
let result = try await sourcekitdExecutor.run(request: reducedRequestInfo.request(for: temporarySourceFile))
93+
if result == .crashed {
94+
logSuccessfulReduction(reducedRequestInfo)
95+
return reducedRequestInfo
96+
} else {
97+
// The reduced request did not crash. We did not find a reduced test case, so return `nil`.
98+
return nil
99+
}
100+
}
101+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
import Foundation
15+
import SKCore
16+
17+
import struct TSCBasic.AbsolutePath
18+
19+
public struct DiagnoseCommand: AsyncParsableCommand {
20+
public static var configuration: CommandConfiguration = CommandConfiguration(
21+
commandName: "diagnose",
22+
abstract: "Reduce sourcekitd crashes",
23+
shouldDisplay: false
24+
)
25+
26+
@Option(
27+
name: .customLong("request-file"),
28+
help:
29+
"Path to a sourcekitd request. If not specified, the command will look for crashed sourcekitd requests and have been logged to OSLog"
30+
)
31+
var sourcekitdRequestPath: String?
32+
33+
@Option(
34+
name: .customLong("os-log-history"),
35+
help: "If now request file is passed, how many minutes of OS Log history should be scraped for a crash."
36+
)
37+
var osLogScrapeDuration: Int = 60
38+
39+
@Option(
40+
name: .customLong("sourcekitd"),
41+
help:
42+
"Path to sourcekitd.framework/sourcekitd. If not specified, the toolchain is found in the same way that sourcekit-lsp finds it"
43+
)
44+
var sourcekitdOverride: String?
45+
46+
var sourcekitd: String? {
47+
get async throws {
48+
if let sourcekitdOverride {
49+
return sourcekitdOverride
50+
}
51+
52+
let installPath = try AbsolutePath(validating: Bundle.main.bundlePath)
53+
let toolchainRegistry = ToolchainRegistry(installPath: installPath)
54+
return await toolchainRegistry.default?.sourcekitd?.pathString
55+
}
56+
}
57+
58+
/// Request infos of crashes that should be diagnosed.
59+
func requestInfos() throws -> [(name: String, info: RequestInfo)] {
60+
if let sourcekitdRequestPath {
61+
let request = try String(contentsOfFile: sourcekitdRequestPath)
62+
return [(sourcekitdRequestPath, try RequestInfo(request: request))]
63+
}
64+
#if canImport(OSLog)
65+
return try OSLogScraper(searchDuration: TimeInterval(osLogScrapeDuration * 60)).getCrashedRequests()
66+
#else
67+
throw ReductionError("--request-file must be specified on all platforms other than macOS")
68+
#endif
69+
}
70+
71+
public init() {}
72+
73+
public func run() async throws {
74+
guard let sourcekitd = try await sourcekitd else {
75+
throw ReductionError("Unable to find sourcekitd.framework")
76+
}
77+
78+
for (name, requestInfo) in try requestInfos() {
79+
print("-- Diagnosing \(name)")
80+
do {
81+
var requestInfo = requestInfo
82+
let executor = SourceKitRequestExecutor(sourcekitd: URL(fileURLWithPath: sourcekitd))
83+
let fileReducer = FileReducer(sourcekitdExecutor: executor)
84+
requestInfo = try await fileReducer.run(initialRequestInfo: requestInfo)
85+
86+
let commandLineReducer = CommandLineArgumentReducer(sourcekitdExecutor: executor)
87+
requestInfo = try await commandLineReducer.run(initialRequestInfo: requestInfo)
88+
89+
let reproducerBundle = try makeReproducerBundle(for: requestInfo)
90+
91+
print("----------------------------------------")
92+
print(
93+
"Reduced SourceKit crash and created a bundle that contains information to reproduce the issue at the following path."
94+
)
95+
print("Please file an issue at https://github.com/apple/sourcekit-lsp/issues/new and attach this bundle")
96+
print()
97+
print(reproducerBundle.path)
98+
99+
// We have found a reproducer. Stop. Looking further probably won't help because other crashes are likely the same cause.
100+
return
101+
} catch {
102+
// Reducing this request failed. Continue reducing the next one, maybe that one succeeds.
103+
print(error)
104+
}
105+
}
106+
107+
print("No reducible crashes found")
108+
throw ExitCode(1)
109+
}
110+
}

0 commit comments

Comments
 (0)