@@ -34,53 +34,100 @@ class FileReducer {
34
34
var requestInfo = initialRequestInfo
35
35
try await validateRequestInfoCrashes ( requestInfo: requestInfo)
36
36
37
- requestInfo = try await runReductionStep ( requestInfo: requestInfo, reduce: removeComments) ?? requestInfo
37
+ requestInfo = try await fatalErrorFunctionBodies ( requestInfo)
38
+ requestInfo = try await removeMembersAndCodeBlockItemsBodies ( requestInfo)
39
+ while let importInlined = try await inlineFirstImport ( requestInfo) {
40
+ requestInfo = importInlined
41
+ requestInfo = try await fatalErrorFunctionBodies ( requestInfo)
42
+ // Generated interfaces are huge. Try removing multiple consecutive declarations at once
43
+ // before going into fine-grained mode
44
+ requestInfo = try await removeMembersAndCodeBlockItemsBodies ( requestInfo, simultaneousRemove: 100 )
45
+ requestInfo = try await removeMembersAndCodeBlockItemsBodies ( requestInfo, simultaneousRemove: 10 )
46
+ requestInfo = try await removeMembersAndCodeBlockItemsBodies ( requestInfo)
47
+ }
48
+
49
+ requestInfo = try await removeComments ( requestInfo)
50
+
51
+ return requestInfo
52
+ }
53
+
54
+ // MARK: - Reduction steps
55
+
56
+ private func validateRequestInfoCrashes( requestInfo: RequestInfo) async throws {
57
+ let initialReproducer = try await runReductionStep ( requestInfo: requestInfo) { tree in [ ] }
58
+ if initialReproducer == nil {
59
+ throw ReductionError ( " Initial request info did not crash " )
60
+ }
61
+ }
38
62
39
- requestInfo = try await runStatefulReductionStep (
63
+ /// Replace function bodies by `fatalError()`
64
+ private func fatalErrorFunctionBodies( _ requestInfo: RequestInfo) async throws -> RequestInfo {
65
+ try await runStatefulReductionStep (
40
66
requestInfo: requestInfo,
41
67
reducer: ReplaceFunctionBodiesByFatalError ( )
42
68
)
69
+ }
43
70
71
+ /// Remove members and code block items.
72
+ ///
73
+ /// When `simultaneousRemove` is set, this automatically removes `simultaneousRemove` number of adjacent items.
74
+ /// This can significantly speed up the reduction of large files with many top-level items.
75
+ private func removeMembersAndCodeBlockItemsBodies(
76
+ _ requestInfo: RequestInfo,
77
+ simultaneousRemove: Int = 1
78
+ ) async throws -> RequestInfo {
79
+ var requestInfo = requestInfo
44
80
// Run removal of members and code block items in a loop. Sometimes the removal of a code block item further down in the
45
81
// file can remove the last reference to a member which can then be removed as well.
46
82
while true {
47
83
let reducedRequestInfo = try await runStatefulReductionStep (
48
84
requestInfo: requestInfo,
49
- reducer: RemoveMembersAndCodeBlockItems ( )
85
+ reducer: RemoveMembersAndCodeBlockItems ( simultaneousRemove : simultaneousRemove )
50
86
)
51
87
if reducedRequestInfo. fileContents == requestInfo. fileContents {
52
88
// No changes were made during reduction. We are done.
53
89
break
54
90
}
55
91
requestInfo = reducedRequestInfo
56
92
}
57
-
58
93
return requestInfo
59
94
}
60
95
61
- func logSuccessfulReduction( _ requestInfo: RequestInfo ) {
62
- print ( " Reduced source file to \( requestInfo. fileContents. utf8. count) bytes " )
96
+ /// Remove comments from the source file.
97
+ private func removeComments( _ requestInfo: RequestInfo) async throws -> RequestInfo {
98
+ try await runReductionStep ( requestInfo: requestInfo, reduce: removeComments ( from: ) ) ?? requestInfo
63
99
}
64
100
65
- // MARK: - Running reduction steps
66
-
67
- private func validateRequestInfoCrashes( requestInfo: RequestInfo ) async throws {
68
- let initialReproducer = try await runReductionStep ( requestInfo: requestInfo) { tree in [ ] }
69
- if initialReproducer == nil {
70
- throw ReductionError ( " Initial request info did not crash " )
101
+ /// Replace the first `import` declaration in the source file by the contents of the Swift interface.
102
+ private func inlineFirstImport( _ requestInfo: RequestInfo) async throws -> RequestInfo? {
103
+ try await runReductionStep ( requestInfo: requestInfo) { tree in
104
+ let edits = await Diagnose . inlineFirstImport (
105
+ in: tree,
106
+ executor: sourcekitdExecutor,
107
+ compilerArgs: requestInfo. compilerArgs
108
+ )
109
+ return edits
71
110
}
72
111
}
73
112
113
+ // MARK: - Primitives to run reduction steps
114
+
115
+ func logSuccessfulReduction( _ requestInfo: RequestInfo) {
116
+ print ( " Reduced source file to \( requestInfo. fileContents. utf8. count) bytes " )
117
+ }
118
+
74
119
/// Run a single reduction step.
75
120
///
76
121
/// If the request still crashes after applying the edits computed by `reduce`, return the reduced request info.
77
122
/// Otherwise, return `nil`
78
123
private func runReductionStep(
79
124
requestInfo: RequestInfo,
80
- reduce: ( _ tree: SourceFileSyntax ) throws -> [ SourceEdit ]
125
+ reduce: ( _ tree: SourceFileSyntax) async throws -> [ SourceEdit] ?
81
126
) async throws -> RequestInfo? {
82
127
let tree = Parser . parse ( source: requestInfo. fileContents)
83
- let edits = try reduce ( tree)
128
+ guard let edits = try await reduce ( tree) else {
129
+ return nil
130
+ }
84
131
let reducedSource = FixItApplier . apply ( edits: edits, to: tree)
85
132
86
133
var adjustedOffset = requestInfo. offset
@@ -100,7 +147,7 @@ class FileReducer {
100
147
101
148
try reducedSource. write ( to: temporarySourceFile, atomically: false , encoding: . utf8)
102
149
let result = try await sourcekitdExecutor. run ( request: reducedRequestInfo. request ( for: temporarySourceFile) )
103
- if result == . crashed {
150
+ if case . reproducesIssue = result {
104
151
logSuccessfulReduction ( reducedRequestInfo)
105
152
return reducedRequestInfo
106
153
} else {
@@ -149,6 +196,8 @@ protocol StatefulReducer {
149
196
func reduce( tree: SourceFileSyntax ) -> [ SourceEdit ]
150
197
}
151
198
199
+ // MARK: Replace function bodies
200
+
152
201
/// Tries replacing one function body by `fatalError()` at a time.
153
202
class ReplaceFunctionBodiesByFatalError: StatefulReducer {
154
203
/// The function bodies that should not be replaced by `fatalError()`.
@@ -205,15 +254,23 @@ class ReplaceFunctionBodiesByFatalError: StatefulReducer {
205
254
}
206
255
}
207
256
257
+ // MARK: Remove members and code block items
258
+
208
259
/// Tries removing `MemberBlockItemSyntax` and `CodeBlockItemSyntax` one at a time.
209
260
class RemoveMembersAndCodeBlockItems : StatefulReducer {
210
261
/// The code block items / members that shouldn't be removed.
211
262
///
212
263
/// See `ReplaceFunctionBodiesByFatalError.keepFunctionBodies`.
213
264
var keepItems : [ String ] = [ ]
214
265
266
+ let simultaneousRemove : Int
267
+
268
+ init ( simultaneousRemove: Int ) {
269
+ self . simultaneousRemove = simultaneousRemove
270
+ }
271
+
215
272
func reduce( tree: SourceFileSyntax ) -> [ SourceEdit ] {
216
- let visitor = Visitor ( keepMembers: keepItems)
273
+ let visitor = Visitor ( keepMembers: keepItems, maxEdits : simultaneousRemove )
217
274
visitor. walk ( tree)
218
275
keepItems = visitor. keepItems
219
276
return visitor. edits
@@ -222,21 +279,23 @@ class RemoveMembersAndCodeBlockItems: StatefulReducer {
222
279
private class Visitor : SyntaxAnyVisitor {
223
280
var keepItems : [ String ]
224
281
var edits : [ SourceEdit ] = [ ]
282
+ let maxEdits : Int
225
283
226
- init ( keepMembers: [ String ] ) {
284
+ init ( keepMembers: [ String ] , maxEdits : Int ) {
227
285
self . keepItems = keepMembers
286
+ self . maxEdits = maxEdits
228
287
super. init ( viewMode: . sourceAccurate)
229
288
}
230
289
231
290
override func visitAny( _ node: Syntax ) -> SyntaxVisitorContinueKind {
232
- if ! edits. isEmpty {
291
+ if edits. count >= maxEdits {
233
292
return . skipChildren
234
293
}
235
294
return . visitChildren
236
295
}
237
296
238
297
override func visit( _ node: MemberBlockItemSyntax ) -> SyntaxVisitorContinueKind {
239
- if ! edits. isEmpty {
298
+ if edits. count >= maxEdits {
240
299
return . skipChildren
241
300
}
242
301
if keepItems. contains ( node. description. trimmingCharacters ( in: . whitespacesAndNewlines) ) {
@@ -249,7 +308,7 @@ class RemoveMembersAndCodeBlockItems: StatefulReducer {
249
308
}
250
309
251
310
override func visit( _ node: CodeBlockItemSyntax ) -> SyntaxVisitorContinueKind {
252
- if ! edits. isEmpty {
311
+ if edits. count >= maxEdits {
253
312
return . skipChildren
254
313
}
255
314
if keepItems. contains ( node. description. trimmingCharacters ( in: . whitespacesAndNewlines) ) {
@@ -304,3 +363,106 @@ fileprivate extension TriviaPiece {
304
363
}
305
364
}
306
365
}
366
+
367
+ // MARK: Inline first include
368
+
369
+ class FirstImportFinder : SyntaxAnyVisitor {
370
+ var firstImport : ImportDeclSyntax ?
371
+
372
+ override func visitAny( _ node: Syntax ) -> SyntaxVisitorContinueKind {
373
+ if firstImport == nil {
374
+ return . visitChildren
375
+ } else {
376
+ return . skipChildren
377
+ }
378
+ }
379
+
380
+ override func visit( _ node: ImportDeclSyntax ) -> SyntaxVisitorContinueKind {
381
+ if firstImport == nil {
382
+ firstImport = node
383
+ }
384
+ return . skipChildren
385
+ }
386
+
387
+ static func findFirstImport( in tree: some SyntaxProtocol ) -> ImportDeclSyntax ? {
388
+ let visitor = FirstImportFinder ( viewMode: . sourceAccurate)
389
+ visitor. walk ( tree)
390
+ return visitor. firstImport
391
+ }
392
+ }
393
+
394
+ private func getSwiftInterface( _ moduleName: String , executor: SourceKitRequestExecutor , compilerArgs: [ String ] )
395
+ async throws -> String
396
+ {
397
+ // FIXME: Use the sourcekitd specified on the command line once rdar://121676425 is fixed
398
+ let sourcekitdPath =
399
+ " /Applications/Geode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/sourcekitdInProc.framework/sourcekitdInProc "
400
+ let executor = SourceKitRequestExecutor (
401
+ sourcekitd: URL ( fileURLWithPath: sourcekitdPath) ,
402
+ reproducerPredicate: nil
403
+ )
404
+
405
+ // We use `RequestInfo` and its template to add the compiler arguments to the request.
406
+ let requestTemplate = """
407
+ {
408
+ key.request: source.request.editor.open.interface,
409
+ key.name: " fake " ,
410
+ key.compilerargs: [
411
+ $COMPILERARGS
412
+ ],
413
+ key.modulename: " \( moduleName) "
414
+ }
415
+ """
416
+ let requestInfo = RequestInfo (
417
+ requestTemplate: requestTemplate,
418
+ offset: 0 ,
419
+ compilerArgs: compilerArgs,
420
+ fileContents: " "
421
+ )
422
+ let request = try requestInfo. request ( for: URL ( fileURLWithPath: " / " ) )
423
+
424
+ guard case . success( let result) = try await executor. run ( request: request) else {
425
+ throw ReductionError ( " Failed to get Swift Interface for \( moduleName) " )
426
+ }
427
+
428
+ // Extract the line containing the source text and parse that using JSON decoder.
429
+ // We can't parse the entire response using `JSONEncoder` because the sourcekitd response isn't actually valid JSON
430
+ // (it doesn't quote keys, for example). So, extract the string, which is actually correctly JSON encoded.
431
+ let quotedSourceText = result. components ( separatedBy: " \n " ) . compactMap { ( line) -> Substring ? in
432
+ let prefix = " key.sourcetext: "
433
+ guard line. hasPrefix ( prefix) else {
434
+ return nil
435
+ }
436
+ var line : Substring = line [ ... ]
437
+ line = line. dropFirst ( prefix. count)
438
+ if line. hasSuffix ( " , " ) {
439
+ line = line. dropLast ( )
440
+ }
441
+ return line
442
+ } . only
443
+ guard let quotedSourceText else {
444
+ throw ReductionError ( " Failed to decode Swift interface response for \( moduleName) " )
445
+ }
446
+ // Filter control characters. JSONDecoder really doensn't like them and they are likely not important if they occur eg. in a comment.
447
+ let sanitizedData = Data ( quotedSourceText. utf8. filter { $0 >= 32 } )
448
+ return try JSONDecoder ( ) . decode ( String . self, from: sanitizedData)
449
+ }
450
+
451
+ func inlineFirstImport(
452
+ in tree: SourceFileSyntax ,
453
+ executor: SourceKitRequestExecutor ,
454
+ compilerArgs: [ String ]
455
+ ) async -> [ SourceEdit ] ? {
456
+ guard let firstImport = FirstImportFinder . findFirstImport ( in: tree) else {
457
+ return nil
458
+ }
459
+ guard let moduleName = firstImport. path. only? . name else {
460
+ return nil
461
+ }
462
+ guard let interface = try ? await getSwiftInterface ( moduleName. text, executor: executor, compilerArgs: compilerArgs)
463
+ else {
464
+ return nil
465
+ }
466
+ let edit = SourceEdit ( range: firstImport. position..< firstImport. endPosition, replacement: interface)
467
+ return [ edit]
468
+ }
0 commit comments