@@ -27,6 +27,32 @@ extension RequestInfo {
27
27
28
28
// MARK: - SourceReducer
29
29
30
+ /// The return value of a source reducer, indicating whether edits were made or if the reducer has finished reducing
31
+ /// the source file.
32
+ fileprivate enum ReducerResult {
33
+ /// The reduction step produced edits that should be applied to the source file.
34
+ case edits( [ SourceEdit ] )
35
+
36
+ /// The reduction step was not able to produce any further modifications to the source file. Reduction is done.
37
+ case done
38
+
39
+ init ( doneIfEmpty edits: [ SourceEdit ] ) {
40
+ if edits. isEmpty {
41
+ self = . done
42
+ } else {
43
+ self = . edits( edits)
44
+ }
45
+ }
46
+ }
47
+
48
+ /// The return value of `runReductionStep`, indicating whether applying the edits from a reducer reduced the issue,
49
+ /// failed to reproduce the issue or if no changes were applied by the reducer.
50
+ fileprivate enum ReductionStepResult {
51
+ case reduced( RequestInfo )
52
+ case didNotReproduce
53
+ case noChange
54
+ }
55
+
30
56
/// Reduces an input source file while continuing to reproduce the crash
31
57
fileprivate class SourceReducer {
32
58
/// The executor that is used to run a sourcekitd request and check whether it
@@ -66,9 +92,14 @@ fileprivate class SourceReducer {
66
92
// MARK: Reduction steps
67
93
68
94
private func validateRequestInfoReproucesIssue( requestInfo: RequestInfo ) async throws {
69
- let initialReproducer = try await runReductionStep ( requestInfo: requestInfo) { tree in [ ] }
70
- if initialReproducer == nil {
95
+ let reductionResult = try await runReductionStep ( requestInfo: requestInfo) { tree in . edits( [ ] ) }
96
+ switch reductionResult {
97
+ case . reduced:
98
+ break
99
+ case . didNotReproduce:
71
100
throw ReductionError ( " Initial request info did not reproduce the issue " )
101
+ case . noChange:
102
+ preconditionFailure ( " The reduction step always returns empty edits and not `done` so we shouldn't hit this " )
72
103
}
73
104
}
74
105
@@ -107,19 +138,31 @@ fileprivate class SourceReducer {
107
138
108
139
/// Remove comments from the source file.
109
140
private func removeComments( _ requestInfo: RequestInfo ) async throws -> RequestInfo {
110
- try await runReductionStep ( requestInfo: requestInfo, reduce: removeComments ( from: ) ) ?? requestInfo
141
+ let reductionResult = try await runReductionStep ( requestInfo: requestInfo, reduce: removeComments ( from: ) )
142
+ switch reductionResult {
143
+ case . reduced( let reducedRequestInfo) :
144
+ return reducedRequestInfo
145
+ case . didNotReproduce, . noChange:
146
+ return requestInfo
147
+ }
111
148
}
112
149
113
150
/// Replace the first `import` declaration in the source file by the contents of the Swift interface.
114
151
private func inlineFirstImport( _ requestInfo: RequestInfo ) async throws -> RequestInfo ? {
115
- try await runReductionStep ( requestInfo: requestInfo) { tree in
152
+ let reductionResult = try await runReductionStep ( requestInfo: requestInfo) { tree in
116
153
let edits = await Diagnose . inlineFirstImport (
117
154
in: tree,
118
155
executor: sourcekitdExecutor,
119
156
compilerArgs: requestInfo. compilerArgs
120
157
)
121
158
return edits
122
159
}
160
+ switch reductionResult {
161
+ case . reduced( let requestInfo) :
162
+ return requestInfo
163
+ case . didNotReproduce, . noChange:
164
+ return nil
165
+ }
123
166
}
124
167
125
168
// MARK: Primitives to run reduction steps
@@ -134,11 +177,13 @@ fileprivate class SourceReducer {
134
177
/// Otherwise, return `nil`
135
178
private func runReductionStep(
136
179
requestInfo: RequestInfo ,
137
- reduce: ( _ tree: SourceFileSyntax ) async throws -> [ SourceEdit ] ?
138
- ) async throws -> RequestInfo ? {
180
+ reduce: ( _ tree: SourceFileSyntax ) async throws -> ReducerResult
181
+ ) async throws -> ReductionStepResult {
139
182
let tree = Parser . parse ( source: requestInfo. fileContents)
140
- guard let edits = try await reduce ( tree) else {
141
- return nil
183
+ let edits : [ SourceEdit ]
184
+ switch try await reduce ( tree) {
185
+ case . edits ( let edit) : edits = edit
186
+ case . done: return . noChange
142
187
}
143
188
let reducedSource = FixItApplier . apply ( edits: edits, to: tree)
144
189
@@ -161,10 +206,9 @@ fileprivate class SourceReducer {
161
206
let result = try await sourcekitdExecutor. run ( request: reducedRequestInfo. request ( for: temporarySourceFile) )
162
207
if case . reproducesIssue = result {
163
208
logSuccessfulReduction ( reducedRequestInfo)
164
- return reducedRequestInfo
209
+ return . reduced ( reducedRequestInfo)
165
210
} else {
166
- // The reduced request did not crash. We did not find a reduced test case, so return `nil`.
167
- return nil
211
+ return . didNotReproduce
168
212
}
169
213
}
170
214
@@ -183,18 +227,17 @@ fileprivate class SourceReducer {
183
227
184
228
var reproducer = requestInfo
185
229
while true {
186
- do {
187
- let reduced = try await runReductionStep ( requestInfo: reproducer) { tree in
188
- let edits = reducer. reduce ( tree: tree)
189
- if edits. isEmpty {
190
- throw StatefulReducerFinishedReducing ( )
191
- }
192
- return edits
193
- }
194
- if let reduced {
195
- reproducer = reduced
196
- }
197
- } catch is StatefulReducerFinishedReducing {
230
+ let reduced = try await runReductionStep ( requestInfo: reproducer) { tree in
231
+ return reducer. reduce ( tree: tree)
232
+ }
233
+ switch reduced {
234
+ case . reduced( let reduced) :
235
+ reproducer = reduced
236
+ case . didNotReproduce:
237
+ // Continue the loop and run the reducer again.
238
+ break
239
+ case . noChange:
240
+ // The reducer finished reducing the source file. We are done
198
241
return reproducer
199
242
}
200
243
}
@@ -205,7 +248,7 @@ fileprivate class SourceReducer {
205
248
206
249
/// See `SourceReducer.runReductionStep`
207
250
fileprivate protocol StatefulReducer {
208
- func reduce( tree: SourceFileSyntax ) -> [ SourceEdit ]
251
+ func reduce( tree: SourceFileSyntax ) -> ReducerResult
209
252
}
210
253
211
254
// MARK: Replace function bodies
@@ -222,11 +265,11 @@ fileprivate class ReplaceFunctionBodiesByFatalError: StatefulReducer {
222
265
/// There's no point replacing a `fatalError()` function by `fatalError()` again.
223
266
var keepFunctionBodies : [ String ] = [ " fatalError() " ]
224
267
225
- func reduce( tree: SourceFileSyntax ) -> [ SourceEdit ] {
268
+ func reduce( tree: SourceFileSyntax ) -> ReducerResult {
226
269
let visitor = Visitor ( keepFunctionBodies: keepFunctionBodies)
227
270
visitor. walk ( tree)
228
271
keepFunctionBodies = visitor. keepFunctionBodies
229
- return visitor. edits
272
+ return ReducerResult ( doneIfEmpty : visitor. edits)
230
273
}
231
274
232
275
private class Visitor : SyntaxAnyVisitor {
@@ -280,11 +323,11 @@ fileprivate class RemoveMembersAndCodeBlockItems: StatefulReducer {
280
323
self . simultaneousRemove = simultaneousRemove
281
324
}
282
325
283
- func reduce( tree: SourceFileSyntax ) -> [ SourceEdit ] {
326
+ func reduce( tree: SourceFileSyntax ) -> ReducerResult {
284
327
let visitor = Visitor ( keepMembers: keepItems, maxEdits: simultaneousRemove)
285
328
visitor. walk ( tree)
286
329
keepItems = visitor. keepItems
287
- return visitor. edits
330
+ return ReducerResult ( doneIfEmpty : visitor. edits)
288
331
}
289
332
290
333
private class Visitor : SyntaxAnyVisitor {
@@ -334,7 +377,7 @@ fileprivate class RemoveMembersAndCodeBlockItems: StatefulReducer {
334
377
}
335
378
336
379
/// Removes all comments from the source file.
337
- fileprivate func removeComments( from tree: SourceFileSyntax ) -> [ SourceEdit ] {
380
+ fileprivate func removeComments( from tree: SourceFileSyntax ) -> ReducerResult {
338
381
class CommentRemover : SyntaxVisitor {
339
382
var edits : [ SourceEdit ] = [ ]
340
383
@@ -361,7 +404,7 @@ fileprivate func removeComments(from tree: SourceFileSyntax) -> [SourceEdit] {
361
404
362
405
let remover = CommentRemover ( viewMode: . sourceAccurate)
363
406
remover. walk ( tree)
364
- return remover. edits
407
+ return . edits ( remover. edits)
365
408
}
366
409
367
410
fileprivate extension TriviaPiece {
@@ -457,17 +500,17 @@ fileprivate func inlineFirstImport(
457
500
in tree: SourceFileSyntax ,
458
501
executor: SourceKitRequestExecutor ,
459
502
compilerArgs: [ String ]
460
- ) async -> [ SourceEdit ] ? {
503
+ ) async -> ReducerResult {
461
504
guard let firstImport = FirstImportFinder . findFirstImport ( in: tree) else {
462
- return nil
505
+ return . done
463
506
}
464
507
guard let moduleName = firstImport. path. only? . name else {
465
- return nil
508
+ return . done
466
509
}
467
510
guard let interface = try ? await getSwiftInterface ( moduleName. text, executor: executor, compilerArgs: compilerArgs)
468
511
else {
469
- return nil
512
+ return . done
470
513
}
471
514
let edit = SourceEdit ( range: firstImport. position..< firstImport. endPosition, replacement: interface)
472
- return [ edit]
515
+ return . edits ( [ edit] )
473
516
}
0 commit comments