File: SourceReducer.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (714 lines) | stat: -rw-r--r-- 26,160 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
import LSPLogging
import SourceKitD
@_spi(FixItApplier) import SwiftIDEUtils
import SwiftParser
import SwiftSyntax

// MARK: - Entry point

extension RequestInfo {
  @_spi(Testing)
  @MainActor
  public func reduceInputFile(
    using executor: SourceKitRequestExecutor,
    progressUpdate: (_ progress: Double, _ message: String) -> Void
  ) async throws -> RequestInfo {
    try await withoutActuallyEscaping(progressUpdate) { progressUpdate in
      let reducer = SourceReducer(sourcekitdExecutor: executor, progressUpdate: progressUpdate)
      return try await reducer.run(initialRequestInfo: self)
    }
  }
}

// MARK: - SourceReducer

/// The return value of a source reducer, indicating whether edits were made or if the reducer has finished reducing
/// the source file.
fileprivate enum ReducerResult {
  /// The reduction step produced edits that should be applied to the source file.
  case edits([SourceEdit])

  ///  The reduction step was not able to produce any further modifications to the source file. Reduction is done.
  case done

  init(doneIfEmpty edits: [SourceEdit]) {
    if edits.isEmpty {
      self = .done
    } else {
      self = .edits(edits)
    }
  }
}

/// The return value of `runReductionStep`, indicating whether applying the edits from a reducer reduced the issue,
/// failed to reproduce the issue or if no changes were applied by the reducer.
fileprivate enum ReductionStepResult {
  case reduced(RequestInfo)
  case didNotReproduce
  case noChange
}

/// Reduces an input source file while continuing to reproduce the crash
@MainActor
fileprivate class SourceReducer {
  /// The executor that is used to run a sourcekitd request and check whether it
  /// still crashes.
  private let sourcekitdExecutor: SourceKitRequestExecutor

  /// A callback to call to report progress
  private let progressUpdate: (_ progress: Double, _ message: String) -> Void

  /// The number of import declarations that the file had when the source reducer was started.
  private var initialImportCount: Int = 0

  /// The byte size of the file when source reduction was started. This gets reset every time an import gets inlined.
  private var fileSizeAfterLastImportInline: Int = 0

  init(
    sourcekitdExecutor: SourceKitRequestExecutor,
    progressUpdate: @escaping (_ progress: Double, _ message: String) -> Void
  ) {
    self.sourcekitdExecutor = sourcekitdExecutor
    self.progressUpdate = progressUpdate
  }

  /// Reduce the file contents in `initialRequest` to a smaller file that still reproduces a crash.
  @MainActor
  func run(initialRequestInfo: RequestInfo) async throws -> RequestInfo {
    var requestInfo = initialRequestInfo
    self.initialImportCount = Parser.parse(source: requestInfo.fileContents).numberOfImports
    self.fileSizeAfterLastImportInline = initialRequestInfo.fileContents.utf8.count

    try await validateRequestInfoReproducesIssue(requestInfo: requestInfo)

    requestInfo = try await mergeDuplicateTopLevelItems(requestInfo)
    requestInfo = try await removeTopLevelItems(requestInfo)
    requestInfo = try await removeFunctionBodies(requestInfo)
    requestInfo = try await removeMembersAndCodeBlockItemsBodies(requestInfo)
    while let importInlined = try await inlineFirstImport(requestInfo) {
      requestInfo = importInlined
      requestInfo = try await removeTopLevelItems(requestInfo)
      requestInfo = try await removeFunctionBodies(requestInfo)
      requestInfo = try await removeMembersAndCodeBlockItemsBodies(requestInfo)
    }

    requestInfo = try await removeComments(requestInfo)

    return requestInfo
  }

  // MARK: Reduction steps

  private func validateRequestInfoReproducesIssue(requestInfo: RequestInfo) async throws {
    let reductionResult = try await runReductionStep(requestInfo: requestInfo) { tree in .edits([]) }
    switch reductionResult {
    case .reduced:
      break
    case .didNotReproduce:
      throw ReductionError("Initial request info did not reproduce the issue")
    case .noChange:
      preconditionFailure("The reduction step always returns empty edits and not `done` so we shouldn't hit this")
    }
  }

  /// Remove the bodies of functions by an empty body.
  private func removeFunctionBodies(_ requestInfo: RequestInfo) async throws -> RequestInfo {
    try await runStatefulReductionStep(
      requestInfo: requestInfo,
      reducer: RemoveFunctionBodies()
    )
  }

  /// Merge any top level items. These can happen if eg. the frontend reducer merged multiple files into one, resulting
  /// in duplicate import statements. Merging these is important because `RemoveTopLevelItems` keeps track of nodes
  /// that have already been visited by node contents and thus fails to remove multiple occurrences of the same import
  /// statement in one go.
  private func mergeDuplicateTopLevelItems(_ requestInfo: RequestInfo) async throws -> RequestInfo {
    let reductionResult = try await runReductionStep(requestInfo: requestInfo, reduce: mergeDuplicateTopLevelItems(in:))
    switch reductionResult {
    case .reduced(let reducedRequestInfo):
      return reducedRequestInfo
    case .didNotReproduce, .noChange:
      return requestInfo
    }
  }

  /// Removes top level items in the source file.
  private func removeTopLevelItems(_ requestInfo: RequestInfo) async throws -> RequestInfo {
    var requestInfo = requestInfo

    // Try removing multiple top-level items at once first. This is useful eg. after inlining a Swift interface or after
    // merging .swift files. Once that's done, go into a more fine-grained mode.
    for simultaneousRemove in [100, 10, 1] {
      let reducedRequestInfo = try await runStatefulReductionStep(
        requestInfo: requestInfo,
        reducer: RemoveTopLevelItems(simultaneousRemove: simultaneousRemove)
      )
      requestInfo = reducedRequestInfo
    }
    return requestInfo
  }

  /// Remove members and code block items.
  ///
  /// When `simultaneousRemove` is set, this automatically removes `simultaneousRemove` number of adjacent items.
  /// This can significantly speed up the reduction of large files with many top-level items.
  private func removeMembersAndCodeBlockItemsBodies(
    _ requestInfo: RequestInfo
  ) async throws -> RequestInfo {
    var requestInfo = requestInfo
    // Run removal of members and code block items in a loop. Sometimes the removal of a code block item further down in the
    // file can remove the last reference to a member which can then be removed as well.
    while true {
      let reducedRequestInfo = try await runStatefulReductionStep(
        requestInfo: requestInfo,
        reducer: RemoveMembersAndCodeBlockItems()
      )
      if reducedRequestInfo.fileContents == requestInfo.fileContents {
        // No changes were made during reduction. We are done.
        break
      }
      requestInfo = reducedRequestInfo
    }
    return requestInfo
  }

  /// Remove comments from the source file.
  private func removeComments(_ requestInfo: RequestInfo) async throws -> RequestInfo {
    let reductionResult = try await runReductionStep(requestInfo: requestInfo, reduce: removeComments(from:))
    switch reductionResult {
    case .reduced(let reducedRequestInfo):
      return reducedRequestInfo
    case .didNotReproduce, .noChange:
      return requestInfo
    }
  }

  /// Replace the first `import` declaration in the source file by the contents of the Swift interface.
  private func inlineFirstImport(_ requestInfo: RequestInfo) async throws -> RequestInfo? {
    // Don't report progress after inlining an import because it might increase the file size before we have a chance
    // to increase `fileSizeAfterLastImportInline`. Progress will get updated again on the next successful reduction
    // step.
    let reductionResult = try await runReductionStep(requestInfo: requestInfo, reportProgress: false) { tree in
      let edits = await Diagnose.inlineFirstImport(
        in: tree,
        executor: sourcekitdExecutor,
        compilerArgs: requestInfo.compilerArgs
      )
      return edits
    }
    switch reductionResult {
    case .reduced(let requestInfo):
      self.fileSizeAfterLastImportInline = requestInfo.fileContents.utf8.count
      return requestInfo
    case .didNotReproduce, .noChange:
      return nil
    }
  }

  // MARK: Primitives to run reduction steps

  private func logSuccessfulReduction(_ requestInfo: RequestInfo, tree: SourceFileSyntax) {
    // The number of imports can grow if inlining a single module adds more than 1 new import.
    // To keep progress between 0 and 1, clamp the number of imports to the initial import count.
    let numberOfImports = min(tree.numberOfImports, initialImportCount)
    let fileSize = requestInfo.fileContents.utf8.count

    let progressPerRemovedImport = Double(1) / Double(initialImportCount + 1)
    let removeImportProgress = Double(initialImportCount - numberOfImports) * progressPerRemovedImport
    let fileReductionProgress =
      (1 - Double(fileSize) / Double(fileSizeAfterLastImportInline)) * progressPerRemovedImport
    var progress = removeImportProgress + fileReductionProgress
    if progress < 0 || progress > 1 {
      logger.fault(
        "Trying to report progress \(progress) from remove import progress \(removeImportProgress) and file reduction progress \(fileReductionProgress)"
      )
      progress = max(min(progress, 1), 0)
    }
    progressUpdate(progress, "Reduced to \(numberOfImports) imports and \(fileSize) bytes")
  }

  /// Run a single reduction step.
  ///
  /// If the request still crashes after applying the edits computed by `reduce`, return the reduced request info.
  /// Otherwise, return `nil`
  @MainActor
  private func runReductionStep(
    requestInfo: RequestInfo,
    reportProgress: Bool = true,
    reduce: (_ tree: SourceFileSyntax) async throws -> ReducerResult
  ) async throws -> ReductionStepResult {
    let tree = Parser.parse(source: requestInfo.fileContents)
    let edits: [SourceEdit]
    switch try await reduce(tree) {
    case .edits(let edit): edits = edit
    case .done: return .noChange
    }
    let reducedSource = FixItApplier.apply(edits: edits, to: tree)

    var adjustedOffset = requestInfo.offset
    for edit in edits {
      if edit.range.upperBound < AbsolutePosition(utf8Offset: requestInfo.offset) {
        adjustedOffset -= (edit.range.upperBound.utf8Offset - edit.range.lowerBound.utf8Offset)
        adjustedOffset += edit.replacement.utf8.count
      }
    }

    let reducedRequestInfo = RequestInfo(
      requestTemplate: requestInfo.requestTemplate,
      offset: adjustedOffset,
      compilerArgs: requestInfo.compilerArgs,
      fileContents: reducedSource
    )

    logger.debug("Try reduction to the following input file:\n\(reducedSource)")
    let result = try await sourcekitdExecutor.run(request: reducedRequestInfo)
    if case .reproducesIssue = result {
      logger.debug("Reduction successful")
      if reportProgress {
        logSuccessfulReduction(reducedRequestInfo, tree: tree)
      }
      return .reduced(reducedRequestInfo)
    } else {
      logger.debug("Reduction did not reproduce the issue")
      return .didNotReproduce
    }
  }

  /// Run a reducer that can carry state between reduction steps.
  ///
  /// This invokes `reduce(tree:)` on `reducer` as long as the `reduce` method returns non-empty edits.
  /// When `reducer.reduce(tree:)` returns empty edits, it indicates that it can't reduce the file any further
  /// and this method return the reduced request info.
  func runStatefulReductionStep(
    requestInfo: RequestInfo,
    reducer: any StatefulReducer
  ) async throws -> RequestInfo {
    /// Error to indicate that `reducer.reduce(tree:)` did not produce any edits.
    /// Will always be caught within the function.
    struct StatefulReducerFinishedReducing: Error {}

    var reproducer = requestInfo
    while true {
      let reduced = try await runReductionStep(requestInfo: reproducer) { tree in
        return reducer.reduce(tree: tree)
      }
      switch reduced {
      case .reduced(let reduced):
        reproducer = reduced
      case .didNotReproduce:
        // Continue the loop and run the reducer again.
        break
      case .noChange:
        // The reducer finished reducing the source file. We are done
        return reproducer
      }
    }
  }
}

// MARK: - Reduce functions

/// See `SourceReducer.runReductionStep`
fileprivate protocol StatefulReducer {
  func reduce(tree: SourceFileSyntax) -> ReducerResult
}

// MARK: Remove function bodies

/// Tries removing the contents of function bodies one at a time.
fileprivate class RemoveFunctionBodies: StatefulReducer {
  /// The function bodies that should not be removed.
  ///
  /// When we tried removing a function, it gets added to this list.
  /// That way, if replacing it did not result in a reduced reproducer, we won't try replacing it again
  /// on the next invocation of `reduce(tree:)`.
  var keepFunctionBodies: [String] = []

  func reduce(tree: SourceFileSyntax) -> ReducerResult {
    let visitor = Visitor(keepFunctionBodies: keepFunctionBodies)
    visitor.walk(tree)
    keepFunctionBodies = visitor.keepFunctionBodies
    return ReducerResult(doneIfEmpty: visitor.edits)
  }

  private class Visitor: SyntaxAnyVisitor {
    var keepFunctionBodies: [String]
    var edits: [SourceEdit] = []

    init(keepFunctionBodies: [String]) {
      self.keepFunctionBodies = keepFunctionBodies
      super.init(viewMode: .sourceAccurate)
    }

    override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
      if !edits.isEmpty {
        // We already produced an edit. We only want to replace one function at a time.
        return .skipChildren
      }
      return .visitChildren
    }

    override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
      if !edits.isEmpty {
        return .skipChildren
      }
      guard node.statements.count > 0 else {
        return .skipChildren
      }
      if keepFunctionBodies.contains(node.statements.description.trimmingCharacters(in: .whitespacesAndNewlines)) {
        return .visitChildren
      }
      keepFunctionBodies.append(node.statements.description.trimmingCharacters(in: .whitespacesAndNewlines))
      edits.append(
        SourceEdit(
          range: node.statements.position..<node.statements.endPosition,
          replacement: ""
        )
      )
      return .skipChildren
    }
  }
}

// MARK: Remove top level items

/// Tries removing top level items in the source file.
///
/// If `simultaneousRemove` is set, it tries to remove that many adjacent top-level items at a time to quickly reduce
/// the source file.
fileprivate class RemoveTopLevelItems: StatefulReducer {
  /// The code block items that shouldn't be removed.
  ///
  /// See `ReplaceFunctionBodiesByFatalError.keepFunctionBodies`.
  var keepItems: [String] = []

  let simultaneousRemove: Int

  init(simultaneousRemove: Int) {
    self.simultaneousRemove = simultaneousRemove
  }

  func reduce(tree: SourceFileSyntax) -> ReducerResult {
    if tree.statements.count <= simultaneousRemove {
      // There are fewer top-level items in the source file than we want to remove. Since removing everything in the
      // source file is very unlikely to be successful, we are done.
      return .done
    }
    let visitor = Visitor(keepMembers: keepItems, maxEdits: simultaneousRemove)
    visitor.walk(tree)
    keepItems = visitor.keepItems
    return ReducerResult(doneIfEmpty: visitor.edits)
  }

  private class Visitor: SyntaxAnyVisitor {
    var keepItems: [String]
    var edits: [SourceEdit] = []
    let maxEdits: Int

    init(keepMembers: [String], maxEdits: Int) {
      self.keepItems = keepMembers
      self.maxEdits = maxEdits
      super.init(viewMode: .sourceAccurate)
    }

    override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
      guard edits.count < maxEdits else {
        return .skipChildren
      }
      return .visitChildren
    }

    override func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind {
      guard node.parent?.parent?.is(SourceFileSyntax.self) ?? false else {
        return .skipChildren
      }
      guard edits.count < maxEdits else {
        return .skipChildren
      }
      if keepItems.contains(node.description.trimmingCharacters(in: .whitespacesAndNewlines)) {
        return .visitChildren
      } else {
        keepItems.append(node.description.trimmingCharacters(in: .whitespacesAndNewlines))
        edits.append(SourceEdit(range: node.position..<node.endPosition, replacement: ""))
        return .skipChildren
      }
    }
  }
}

// MARK: Remove members and code block items

/// Tries removing `MemberBlockItemSyntax` and `CodeBlockItemSyntax` one at a time.
fileprivate class RemoveMembersAndCodeBlockItems: StatefulReducer {
  /// The code block items / members that shouldn't be removed.
  ///
  /// See `ReplaceFunctionBodiesByFatalError.keepFunctionBodies`.
  var keepItems: [String] = []

  func reduce(tree: SourceFileSyntax) -> ReducerResult {
    let visitor = Visitor(keepMembers: keepItems)
    visitor.walk(tree)
    keepItems = visitor.keepItems
    return ReducerResult(doneIfEmpty: visitor.edits)
  }

  private class Visitor: SyntaxAnyVisitor {
    var keepItems: [String]
    var edits: [SourceEdit] = []

    init(keepMembers: [String]) {
      self.keepItems = keepMembers
      super.init(viewMode: .sourceAccurate)
    }

    override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
      guard edits.isEmpty else {
        return .skipChildren
      }
      return .visitChildren
    }

    override func visit(_ node: MemberBlockItemSyntax) -> SyntaxVisitorContinueKind {
      guard edits.isEmpty else {
        return .skipChildren
      }
      if keepItems.contains(node.description.trimmingCharacters(in: .whitespacesAndNewlines)) {
        return .visitChildren
      } else {
        keepItems.append(node.description.trimmingCharacters(in: .whitespacesAndNewlines))
        edits.append(SourceEdit(range: node.position..<node.endPosition, replacement: ""))
        return .skipChildren
      }
    }

    override func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind {
      guard edits.isEmpty else {
        return .skipChildren
      }
      if keepItems.contains(node.description.trimmingCharacters(in: .whitespacesAndNewlines)) {
        return .visitChildren
      } else {
        keepItems.append(node.description.trimmingCharacters(in: .whitespacesAndNewlines))
        edits.append(SourceEdit(range: node.position..<node.endPosition, replacement: ""))
        return .skipChildren
      }
    }
  }
}

/// For any top-level items in the source file that occur multiple times, only keep the first occurrence.
fileprivate func mergeDuplicateTopLevelItems(in tree: SourceFileSyntax) -> ReducerResult {
  class DuplicateTopLevelItemMerger: SyntaxVisitor {
    var seenItems: Set<String> = []
    var edits: [SourceEdit] = []

    override func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind {
      guard node.parent?.parent?.is(SourceFileSyntax.self) ?? false else {
        return .skipChildren
      }
      if !seenItems.insert(node.trimmedDescription).inserted {
        edits.append(
          SourceEdit(
            range: node.positionAfterSkippingLeadingTrivia..<node.endPositionBeforeTrailingTrivia,
            replacement: ""
          )
        )
      }
      return .skipChildren
    }
  }

  let remover = DuplicateTopLevelItemMerger(viewMode: .sourceAccurate)
  remover.walk(tree)
  return .edits(remover.edits)
}

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

    private func removeComments(from trivia: Trivia, startPosition: AbsolutePosition) {
      var position = startPosition
      var previousTriviaPiece: TriviaPiece?
      for triviaPiece in trivia {
        defer {
          previousTriviaPiece = triviaPiece
          position += triviaPiece.sourceLength
        }
        if triviaPiece.isComment || (triviaPiece.isNewline && previousTriviaPiece?.isComment ?? false) {
          edits.append(SourceEdit(range: position..<(position + triviaPiece.sourceLength), replacement: ""))
        }
      }
    }

    override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind {
      removeComments(from: node.leadingTrivia, startPosition: node.position)
      removeComments(from: node.trailingTrivia, startPosition: node.endPositionBeforeTrailingTrivia)
      return .skipChildren
    }
  }

  let remover = CommentRemover(viewMode: .sourceAccurate)
  remover.walk(tree)
  return .edits(remover.edits)
}

fileprivate extension TriviaPiece {
  var isComment: Bool {
    switch self {
    case .blockComment, .docBlockComment, .docLineComment, .lineComment:
      return true
    default:
      return false
    }
  }
}

// MARK: Inline first include

fileprivate class FirstImportFinder: SyntaxAnyVisitor {
  var firstImport: ImportDeclSyntax?

  override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
    if firstImport == nil {
      return .visitChildren
    } else {
      return .skipChildren
    }
  }

  override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind {
    if firstImport == nil {
      firstImport = node
    }
    return .skipChildren
  }

  static func findFirstImport(in tree: some SyntaxProtocol) -> ImportDeclSyntax? {
    let visitor = FirstImportFinder(viewMode: .sourceAccurate)
    visitor.walk(tree)
    return visitor.firstImport
  }
}

/// Return the generated interface of the given module.
///
/// `compilerArgs` are the compiler args used to generate the interface. Initially these are the compiler arguments of
/// the file that imports the module. If `areFallbackArgs` is set, we have synthesized fallback arguments that only
/// contain a target and SDK. This is useful when reducing a swift-frontend crash because sourcekitd requires driver
/// arguments but the swift-frontend crash has frontend args.
@MainActor
fileprivate func getSwiftInterface(
  _ moduleName: String,
  executor: SourceKitRequestExecutor,
  compilerArgs: [String],
  areFallbackArgs: Bool = false
) async throws -> String {
  // We use `RequestInfo` and its template to add the compiler arguments to the request.
  let requestTemplate = """
    {
      key.request: source.request.editor.open.interface,
      key.name: "fake",
      key.compilerargs: [
        $COMPILER_ARGS
      ],
      key.modulename: "\(moduleName)"
    }
    """
  let requestInfo = RequestInfo(
    requestTemplate: requestTemplate,
    offset: 0,
    compilerArgs: compilerArgs,
    fileContents: ""
  )

  let result: String
  switch try await executor.run(request: requestInfo) {
  case .success(response: let response):
    result = response
  case .error where !areFallbackArgs:
    var fallbackArgs: [String] = []
    var argsIterator = compilerArgs.makeIterator()
    while let arg = argsIterator.next() {
      if arg == "-target" || arg == "-sdk" {
        fallbackArgs.append(arg)
        if let value = argsIterator.next() {
          fallbackArgs.append(value)
        }
      }
    }
    return try await getSwiftInterface(
      moduleName,
      executor: executor,
      compilerArgs: fallbackArgs,
      areFallbackArgs: true
    )
  default:
    throw ReductionError("Failed to get Swift Interface for \(moduleName)")
  }

  // Extract the line containing the source text and parse that using JSON decoder.
  // We can't parse the entire response using `JSONEncoder` because the sourcekitd response isn't actually valid JSON
  // (it doesn't quote keys, for example). So, extract the string, which is actually correctly JSON encoded.
  let quotedSourceText = result.components(separatedBy: "\n").compactMap { (line) -> Substring? in
    let prefix = "  key.sourcetext: "
    guard line.hasPrefix(prefix) else {
      return nil
    }
    var line: Substring = line[...]
    line = line.dropFirst(prefix.count)
    if line.hasSuffix(",") {
      line = line.dropLast()
    }
    return line
  }.only
  guard let quotedSourceText else {
    throw ReductionError("Failed to decode Swift interface response for \(moduleName)")
  }
  // Filter control characters. JSONDecoder really doensn't like them and they are likely not important if they occur eg. in a comment.
  let sanitizedData = Data(quotedSourceText.utf8.filter { $0 >= 32 })
  return try JSONDecoder().decode(String.self, from: sanitizedData)
}

@MainActor
fileprivate func inlineFirstImport(
  in tree: SourceFileSyntax,
  executor: SourceKitRequestExecutor,
  compilerArgs: [String]
) async -> ReducerResult {
  guard let firstImport = FirstImportFinder.findFirstImport(in: tree) else {
    return .done
  }
  guard let moduleName = firstImport.path.only?.name else {
    return .done
  }
  guard let interface = try? await getSwiftInterface(moduleName.text, executor: executor, compilerArgs: compilerArgs)
  else {
    return .done
  }
  let edit = SourceEdit(range: firstImport.position..<firstImport.endPosition, replacement: interface)
  return .edits([edit])
}

fileprivate extension SourceFileSyntax {
  var numberOfImports: Int {
    // If a module is imported multiple times (eg. because we merged .swift files), only count it once.
    let importedModules = self.statements.compactMap { $0.item.as(ImportDeclSyntax.self)?.path.trimmedDescription }
    return Set(importedModules).count
  }
}