Skip to content

Commit 5929fb5

Browse files
committed
Simplify SourceReducer by returning descriptive enums instead of optionals
1 parent 81cef83 commit 5929fb5

File tree

1 file changed

+78
-35
lines changed

1 file changed

+78
-35
lines changed

Sources/Diagnose/SourceReducer.swift

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,32 @@ extension RequestInfo {
2727

2828
// MARK: - SourceReducer
2929

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+
3056
/// Reduces an input source file while continuing to reproduce the crash
3157
fileprivate class SourceReducer {
3258
/// The executor that is used to run a sourcekitd request and check whether it
@@ -66,9 +92,14 @@ fileprivate class SourceReducer {
6692
// MARK: Reduction steps
6793

6894
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:
71100
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")
72103
}
73104
}
74105

@@ -107,19 +138,31 @@ fileprivate class SourceReducer {
107138

108139
/// Remove comments from the source file.
109140
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+
}
111148
}
112149

113150
/// Replace the first `import` declaration in the source file by the contents of the Swift interface.
114151
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
116153
let edits = await Diagnose.inlineFirstImport(
117154
in: tree,
118155
executor: sourcekitdExecutor,
119156
compilerArgs: requestInfo.compilerArgs
120157
)
121158
return edits
122159
}
160+
switch reductionResult {
161+
case .reduced(let requestInfo):
162+
return requestInfo
163+
case .didNotReproduce, .noChange:
164+
return nil
165+
}
123166
}
124167

125168
// MARK: Primitives to run reduction steps
@@ -134,11 +177,13 @@ fileprivate class SourceReducer {
134177
/// Otherwise, return `nil`
135178
private func runReductionStep(
136179
requestInfo: RequestInfo,
137-
reduce: (_ tree: SourceFileSyntax) async throws -> [SourceEdit]?
138-
) async throws -> RequestInfo? {
180+
reduce: (_ tree: SourceFileSyntax) async throws -> ReducerResult
181+
) async throws -> ReductionStepResult {
139182
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
142187
}
143188
let reducedSource = FixItApplier.apply(edits: edits, to: tree)
144189

@@ -161,10 +206,9 @@ fileprivate class SourceReducer {
161206
let result = try await sourcekitdExecutor.run(request: reducedRequestInfo.request(for: temporarySourceFile))
162207
if case .reproducesIssue = result {
163208
logSuccessfulReduction(reducedRequestInfo)
164-
return reducedRequestInfo
209+
return .reduced(reducedRequestInfo)
165210
} 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
168212
}
169213
}
170214

@@ -183,18 +227,17 @@ fileprivate class SourceReducer {
183227

184228
var reproducer = requestInfo
185229
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
198241
return reproducer
199242
}
200243
}
@@ -205,7 +248,7 @@ fileprivate class SourceReducer {
205248

206249
/// See `SourceReducer.runReductionStep`
207250
fileprivate protocol StatefulReducer {
208-
func reduce(tree: SourceFileSyntax) -> [SourceEdit]
251+
func reduce(tree: SourceFileSyntax) -> ReducerResult
209252
}
210253

211254
// MARK: Replace function bodies
@@ -222,11 +265,11 @@ fileprivate class ReplaceFunctionBodiesByFatalError: StatefulReducer {
222265
/// There's no point replacing a `fatalError()` function by `fatalError()` again.
223266
var keepFunctionBodies: [String] = ["fatalError()"]
224267

225-
func reduce(tree: SourceFileSyntax) -> [SourceEdit] {
268+
func reduce(tree: SourceFileSyntax) -> ReducerResult {
226269
let visitor = Visitor(keepFunctionBodies: keepFunctionBodies)
227270
visitor.walk(tree)
228271
keepFunctionBodies = visitor.keepFunctionBodies
229-
return visitor.edits
272+
return ReducerResult(doneIfEmpty: visitor.edits)
230273
}
231274

232275
private class Visitor: SyntaxAnyVisitor {
@@ -280,11 +323,11 @@ fileprivate class RemoveMembersAndCodeBlockItems: StatefulReducer {
280323
self.simultaneousRemove = simultaneousRemove
281324
}
282325

283-
func reduce(tree: SourceFileSyntax) -> [SourceEdit] {
326+
func reduce(tree: SourceFileSyntax) -> ReducerResult {
284327
let visitor = Visitor(keepMembers: keepItems, maxEdits: simultaneousRemove)
285328
visitor.walk(tree)
286329
keepItems = visitor.keepItems
287-
return visitor.edits
330+
return ReducerResult(doneIfEmpty: visitor.edits)
288331
}
289332

290333
private class Visitor: SyntaxAnyVisitor {
@@ -334,7 +377,7 @@ fileprivate class RemoveMembersAndCodeBlockItems: StatefulReducer {
334377
}
335378

336379
/// Removes all comments from the source file.
337-
fileprivate func removeComments(from tree: SourceFileSyntax) -> [SourceEdit] {
380+
fileprivate func removeComments(from tree: SourceFileSyntax) -> ReducerResult {
338381
class CommentRemover: SyntaxVisitor {
339382
var edits: [SourceEdit] = []
340383

@@ -361,7 +404,7 @@ fileprivate func removeComments(from tree: SourceFileSyntax) -> [SourceEdit] {
361404

362405
let remover = CommentRemover(viewMode: .sourceAccurate)
363406
remover.walk(tree)
364-
return remover.edits
407+
return .edits(remover.edits)
365408
}
366409

367410
fileprivate extension TriviaPiece {
@@ -457,17 +500,17 @@ fileprivate func inlineFirstImport(
457500
in tree: SourceFileSyntax,
458501
executor: SourceKitRequestExecutor,
459502
compilerArgs: [String]
460-
) async -> [SourceEdit]? {
503+
) async -> ReducerResult {
461504
guard let firstImport = FirstImportFinder.findFirstImport(in: tree) else {
462-
return nil
505+
return .done
463506
}
464507
guard let moduleName = firstImport.path.only?.name else {
465-
return nil
508+
return .done
466509
}
467510
guard let interface = try? await getSwiftInterface(moduleName.text, executor: executor, compilerArgs: compilerArgs)
468511
else {
469-
return nil
512+
return .done
470513
}
471514
let edit = SourceEdit(range: firstImport.position..<firstImport.endPosition, replacement: interface)
472-
return [edit]
515+
return .edits([edit])
473516
}

0 commit comments

Comments
 (0)