Skip to content

Commit 35f07f5

Browse files
authored
Merge pull request #1037 from ahoppen/ahoppen/diagnose
Add a subcommand to sourcekit-lsp that automatically reduces sourcekitd issues
2 parents a280ce3 + 9415d03 commit 35f07f5

16 files changed

+1269
-6
lines changed

Package.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageDescription
55

66
let package = Package(
77
name: "SourceKitLSP",
8-
platforms: [.macOS("12.0")],
8+
platforms: [.macOS(.v13)],
99
products: [
1010
.executable(name: "sourcekit-lsp", targets: ["sourcekit-lsp"]),
1111
.library(name: "_SourceKitLSP", targets: ["SourceKitLSP"]),
@@ -24,6 +24,7 @@ let package = Package(
2424
.executableTarget(
2525
name: "sourcekit-lsp",
2626
dependencies: [
27+
"Diagnose",
2728
"LanguageServerProtocol",
2829
"LanguageServerProtocolJSONRPC",
2930
"SKCore",
@@ -61,6 +62,19 @@ let package = Package(
6162
exclude: ["CMakeLists.txt"]
6263
),
6364

65+
.target(
66+
name: "Diagnose",
67+
dependencies: [
68+
"SourceKitD",
69+
"SKCore",
70+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
71+
.product(name: "SwiftSyntax", package: "swift-syntax"),
72+
.product(name: "SwiftParser", package: "swift-syntax"),
73+
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
74+
],
75+
exclude: ["CMakeLists.txt"]
76+
),
77+
6478
// MARK: LanguageServerProtocol
6579
// The core LSP types, suitable for any LSP implementation.
6680
.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 case .reproducesIssue = result {
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: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
#if canImport(Darwin)
47+
// Creating an NSPredicate from a string is not supported in corelibs-foundation.
48+
@Option(
49+
help: """
50+
If the sourcekitd response matches this predicate, consider it as reproducing the issue.
51+
sourcekitd crashes are always considered as reproducers.
52+
53+
The predicate is an NSPredicate and `self` is the sourcekitd response.
54+
"""
55+
)
56+
var predicate: String?
57+
#endif
58+
59+
var sourcekitd: String? {
60+
get async throws {
61+
if let sourcekitdOverride {
62+
return sourcekitdOverride
63+
}
64+
65+
let installPath = try AbsolutePath(validating: Bundle.main.bundlePath)
66+
let toolchainRegistry = ToolchainRegistry(installPath: installPath)
67+
return await toolchainRegistry.default?.sourcekitd?.pathString
68+
}
69+
}
70+
71+
/// Request infos of crashes that should be diagnosed.
72+
func requestInfos() throws -> [(name: String, info: RequestInfo)] {
73+
if let sourcekitdRequestPath {
74+
let request = try String(contentsOfFile: sourcekitdRequestPath)
75+
return [(sourcekitdRequestPath, try RequestInfo(request: request))]
76+
}
77+
#if canImport(OSLog)
78+
return try OSLogScraper(searchDuration: TimeInterval(osLogScrapeDuration * 60)).getCrashedRequests()
79+
#else
80+
throw ReductionError("--request-file must be specified on all platforms other than macOS")
81+
#endif
82+
}
83+
84+
public init() {}
85+
86+
public func run() async throws {
87+
guard let sourcekitd = try await sourcekitd else {
88+
throw ReductionError("Unable to find sourcekitd.framework")
89+
}
90+
91+
for (name, requestInfo) in try requestInfos() {
92+
print("-- Diagnosing \(name)")
93+
do {
94+
var requestInfo = requestInfo
95+
var nspredicate: NSPredicate? = nil
96+
#if canImport(Darwin)
97+
if let predicate {
98+
nspredicate = NSPredicate(format: predicate)
99+
}
100+
#endif
101+
let executor = SourceKitRequestExecutor(
102+
sourcekitd: URL(fileURLWithPath: sourcekitd),
103+
reproducerPredicate: nspredicate
104+
)
105+
let fileReducer = FileReducer(sourcekitdExecutor: executor)
106+
requestInfo = try await fileReducer.run(initialRequestInfo: requestInfo)
107+
108+
let commandLineReducer = CommandLineArgumentReducer(sourcekitdExecutor: executor)
109+
requestInfo = try await commandLineReducer.run(initialRequestInfo: requestInfo)
110+
111+
let reproducerBundle = try makeReproducerBundle(for: requestInfo)
112+
113+
print("----------------------------------------")
114+
print(
115+
"Reduced SourceKit crash and created a bundle that contains information to reproduce the issue at the following path."
116+
)
117+
print("Please file an issue at https://github.com/apple/sourcekit-lsp/issues/new and attach this bundle")
118+
print()
119+
print(reproducerBundle.path)
120+
121+
// We have found a reproducer. Stop. Looking further probably won't help because other crashes are likely the same cause.
122+
return
123+
} catch {
124+
// Reducing this request failed. Continue reducing the next one, maybe that one succeeds.
125+
print(error)
126+
}
127+
}
128+
129+
print("No reducible crashes found")
130+
throw ExitCode(1)
131+
}
132+
}

0 commit comments

Comments
 (0)