File: PrettyPrint.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 (863 lines) | stat: -rw-r--r-- 36,544 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
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 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 SwiftSyntax
import Foundation

/// PrettyPrinter takes a Syntax node and outputs a well-formatted, re-indented reproduction of the
/// code as a String.
@_spi(Testing)
public class PrettyPrinter {

  /// Information about an open break that has not yet been closed during the printing stage.
  private struct ActiveOpenBreak {
    /// The index of the open break.
    let index: Int

    /// The kind of open break that created this scope.
    let kind: OpenBreakKind

    /// The line number where the open break occurred.
    let lineNumber: Int

    /// Indicates whether the open break contributed a continuation indent to its scope.
    ///
    /// This indent is applied independently of `contributesBlockIndent`, which means a given break
    /// may apply both a continuation indent and a block indent, either indent, or neither indent.
    var contributesContinuationIndent: Bool

    /// Indicates whether the open break contributed a block indent to its scope. Only one block
    /// indent is applied per line that contains open breaks.
    ///
    /// This indent is applied independently of `contributesContinuationIndent`, which means a given
    /// break may apply both a continuation indent and a block indent, either indent, or neither
    /// indent.
    var contributesBlockIndent: Bool
  }

  /// Records state of `contextualBreakingStart` tokens.
  private struct ActiveBreakingContext {
    /// The line number in the `outputBuffer` where a start token appeared.
    let lineNumber: Int

    enum BreakingBehavior {
      /// The behavior hasn't been determined. This is treated as `continuation`.
      case unset
      /// The break is created as a `continuation` break, setting `currentLineIsContinuation` when
      /// it fires.
      case continuation
      /// The break maintains the existing value of `currentLineIsContinuation` when it fires.
      case maintain
    }

    /// The behavior to use when a `contextual` break fires inside of this break context.
    var contextualBreakingBehavior = BreakingBehavior.unset
  }

  private let context: Context
  private var configuration: Configuration { return context.configuration }
  private let maxLineLength: Int
  private var tokens: [Token]
  private var source: String

  /// Keep track of where formatting was disabled in the original source
  ///
  /// To format a selection, we insert `enableFormatting`/`disableFormatting` tokens into the
  /// stream when entering/exiting a selection range. Those tokens include utf8 offsets into the
  /// original source. When enabling formatting, we copy the text between `disabledPosition` and the
  /// current position to `outputBuffer`. From then on, we continue to format until the next
  /// `disableFormatting` token.
  private var disabledPosition: AbsolutePosition? = nil

  private var outputBuffer: String = ""

  /// The number of spaces remaining on the current line.
  private var spaceRemaining: Int

  /// Keep track of the token lengths.
  private var lengths = [Int]()

  /// Did the previous token create a new line? This is used to determine if a group needs to
  /// consistently break.
  private var lastBreak = false

  /// Keep track of whether we are forcing breaks within a group (for consistent breaking).
  private var forceBreakStack = [false]

  /// If true, the token stream is printed to the console for debugging purposes.
  private var printTokenStream: Bool

  /// Whether the pretty printer should restrict its changes to whitespace. When true, only
  /// whitespace (e.g. spaces, newlines) are modified. Otherwise, text changes (e.g. add/remove
  /// trailing commas) are performed in addition to whitespace.
  private let whitespaceOnly: Bool

  /// Keeps track of the line numbers and indentation states of the open (and unclosed) breaks seen
  /// so far.
  private var activeOpenBreaks: [ActiveOpenBreak] = []

  /// Stack of the active breaking contexts.
  private var activeBreakingContexts: [ActiveBreakingContext] = []

  /// The most recently ended breaking context, used to force certain following `contextual` breaks.
  private var lastEndedBreakingContext: ActiveBreakingContext? = nil

  /// Keeps track of the current line number being printed.
  private var lineNumber: Int = 1

  /// Indicates whether or not the current line being printed is a continuation line.
  private var currentLineIsContinuation = false

  /// Keeps track of the continuation line state as you go into and out of open-close break groups.
  private var continuationStack: [Bool] = []

  /// Keeps track of the line number where comma regions started. Line numbers are removed as their
  /// corresponding end token are encountered.
  private var commaDelimitedRegionStack: [Int] = []

  /// Keeps track of the most recent number of consecutive newlines that have been printed.
  ///
  /// This value is reset to zero whenever non-newline content is printed.
  private var consecutiveNewlineCount = 0

  /// Keeps track of the most recent number of spaces that should be printed before the next text
  /// token.
  private var pendingSpaces = 0

  /// Indicates whether or not the printer is currently at the beginning of a line.
  private var isAtStartOfLine = true

  /// Tracks how many printer control tokens to suppress firing breaks are active.
  private var activeBreakSuppressionCount = 0

  /// Whether breaks are supressed from firing. When true, no breaks should fire and the only way to
  /// move to a new line is an explicit new line token. Discretionary breaks aren't suppressed
  /// if ``allowSuppressedDiscretionaryBreaks`` is true.
  private var isBreakingSuppressed: Bool {
    return activeBreakSuppressionCount > 0
  }

  /// Indicates whether discretionary breaks should still be included even if break suppression is
  /// enabled (see ``isBreakingSuppressed``).
  private var allowSuppressedDiscretionaryBreaks = false

  /// The computed indentation level, as a number of spaces, based on the state of any unclosed
  /// delimiters and whether or not the current line is a continuation line.
  private var currentIndentation: [Indent] {
    let indentation = configuration.indentation
    var totalIndentation: [Indent] = activeOpenBreaks.flatMap { (open) -> [Indent] in
      let count = (open.contributesBlockIndent ? 1 : 0)
        + (open.contributesContinuationIndent ? 1 : 0)
      return Array(repeating: indentation, count: count)
    }
    if currentLineIsContinuation {
      totalIndentation.append(configuration.indentation)
    }
    return totalIndentation
  }

  /// The current line number being printed, with adjustments made for open/close break
  /// calculations.
  ///
  /// Some of the open/close break logic is based on whether matching breaks are located on the same
  /// physical line. In some situations, newlines can be printed before breaks that would cause the
  /// line number to increase by one by the time we reach the break, when we really wish to consider
  /// the break as being located at the end of the previous line.
  private var openCloseBreakCompensatingLineNumber: Int {
    return isAtStartOfLine ? lineNumber - 1 : lineNumber
  }

  /// Creates a new PrettyPrinter with the provided formatting configuration.
  ///
  /// - Parameters:
  ///   - context: The formatter context.
  ///   - node: The node to be pretty printed.
  ///   - printTokenStream: Indicates whether debug information about the token stream should be
  ///     printed to standard output.
  ///   - whitespaceOnly: Whether only whitespace changes should be made.
  public init(context: Context, source: String, node: Syntax, printTokenStream: Bool, whitespaceOnly: Bool) {
    self.context = context
    self.source = source
    let configuration = context.configuration
    self.tokens = node.makeTokenStream(
      configuration: configuration,
      selection: context.selection,
      operatorTable: context.operatorTable)
    self.maxLineLength = configuration.lineLength
    self.spaceRemaining = self.maxLineLength
    self.printTokenStream = printTokenStream
    self.whitespaceOnly = whitespaceOnly
  }

  /// Append the given string to the output buffer.
  ///
  /// No further processing is performed on the string.
  private func writeRaw<S: StringProtocol>(_ str: S) {
    if disabledPosition == nil {
      outputBuffer.append(String(str))
    }
  }

  /// Writes newlines into the output stream, taking into account any preexisting consecutive
  /// newlines and the maximum allowed number of blank lines.
  ///
  /// This function does some implicit collapsing of consecutive newlines to ensure that the
  /// results are consistent when breaks and explicit newlines coincide. For example, imagine a
  /// break token that fires (thus creating a single non-discretionary newline) because it is
  /// followed by a group that contains 2 discretionary newlines that were found in the user's
  /// source code at that location. In that case, the break "overlaps" with the discretionary
  /// newlines and it will write a newline before we get to the discretionaries. Thus, we have to
  /// subtract the previously written newlines during the second call so that we end up with the
  /// correct number overall.
  ///
  /// - Parameter newlines: The number and type of newlines to write.
  private func writeNewlines(_ newlines: NewlineBehavior) {
    let numberToPrint: Int
    switch newlines {
    case .elective:
      numberToPrint = consecutiveNewlineCount == 0 ? 1 : 0
    case .soft(let count, _):
      // We add 1 to the max blank lines because it takes 2 newlines to create the first blank line.
      numberToPrint = min(count, configuration.maximumBlankLines + 1) - consecutiveNewlineCount
    case .hard(let count):
      numberToPrint = count
    }

    guard numberToPrint > 0 else { return }
    writeRaw(String(repeating: "\n", count: numberToPrint))
    lineNumber += numberToPrint
    isAtStartOfLine = true
    consecutiveNewlineCount += numberToPrint
    pendingSpaces = 0
  }

  /// Request that the given number of spaces be printed out before the next text token.
  ///
  /// Spaces are printed only when the next text token is printed in order to prevent us from
  /// printing lines that are only whitespace or have trailing whitespace.
  private func enqueueSpaces(_ count: Int) {
    pendingSpaces += count
    spaceRemaining -= count
  }

  /// Writes the given text to the output stream.
  ///
  /// Before printing the text, this function will print any line-leading indentation or interior
  /// leading spaces that are required before the text itself.
  private func write(_ text: String) {
    if isAtStartOfLine {
      writeRaw(currentIndentation.indentation())
      spaceRemaining = maxLineLength - currentIndentation.length(in: configuration)
      isAtStartOfLine = false
    } else if pendingSpaces > 0  {
      writeRaw(String(repeating: " ", count: pendingSpaces))
    }
    writeRaw(text)
    consecutiveNewlineCount = 0
    pendingSpaces = 0
  }

  /// Print out the provided token, and apply line-wrapping and indentation as needed.
  ///
  /// This method takes a Token and it's length, and it keeps track of how much space is left on the
  /// current line it is printing on. If a token exceeds the remaining space, we break to a new line,
  /// and apply the appropriate level of indentation.
  ///
  /// - Parameters:
  ///   - idx: The index of the token/length pair to be printed.
  private func printToken(idx: Int) {
    let token = tokens[idx]
    let length = lengths[idx]

    if self.printTokenStream {
      printDebugToken(token: token, length: length, idx: idx)
    }
    assert(length >= 0, "Token lengths must be positive")

    switch token {
    case .contextualBreakingStart:
      activeBreakingContexts.append(ActiveBreakingContext(lineNumber: lineNumber))

      // Discard the last finished breaking context to keep it from effecting breaks inside of the
      // new context. The discarded context has already either had an impact on the contextual break
      // after it or there was no relevant contextual break, so it's safe to discard.
      lastEndedBreakingContext = nil

    case .contextualBreakingEnd:
      guard let closedContext = activeBreakingContexts.popLast() else {
        fatalError("Encountered unmatched contextualBreakingEnd token.")
      }

      // Break contexts create scopes, and a breaking context should never be carried between
      // scopes. When there's no active break context, discard the popped one to prevent carrying it
      // into a new scope.
      lastEndedBreakingContext = activeBreakingContexts.isEmpty ? nil : closedContext

    // Check if we need to force breaks in this group, and calculate the indentation to be used in
    // the group.
    case .open(let breaktype):
      // Determine if the break tokens in this group need to be forced.
      if (length > spaceRemaining || lastBreak), case .consistent = breaktype {
        forceBreakStack.append(true)
      } else {
        forceBreakStack.append(false)
      }

    case .close:
      forceBreakStack.removeLast()

    // Create a line break if needed. Calculate the indentation required and adjust spaceRemaining
    // accordingly.
    case .break(let kind, let size, let newline):
      var mustBreak = forceBreakStack.last ?? false

      // Tracks whether the current line should be considered a continuation line, *if and only if
      // the break fires* (note that this is assigned to `currentLineIsContinuation` only in that
      // case).
      var isContinuationIfBreakFires = false

      switch kind {
      case .open(let openKind):
        let lastOpenBreak = activeOpenBreaks.last
        let currentLineNumber = openCloseBreakCompensatingLineNumber

        // Only increase the indentation if there wasn't an open break already encountered on this
        // line (i.e., the previous open break didn't fire), to prevent the indentation of the next
        // line from being more than one level deeper than this line.
        let lastOpenBreakWasSameLine = currentLineNumber == (lastOpenBreak?.lineNumber ?? 0)
        if lastOpenBreakWasSameLine && openKind == .block {
          // If the last open break was on the same line, then we mark it as *not* contributing to
          // the indentation of the subsequent lines. When the breaks are closed, this ensures that
          // indentation is popped evenly (and also popped in an order that causes everything to
          // line up properly).
          activeOpenBreaks[activeOpenBreaks.count - 1].contributesBlockIndent = false
        }

        // If an open break occurs on a continuation line, we must push that continuation
        // indentation onto the stack. The open break will reset the continuation state for the
        // lines within it (unless they are themselves continuations within that particular
        // scope), so we need the continuation indentation to persist across all the lines in that
        // scope. Additionally, continuation open breaks must indent when the break fires.
        let continuationBreakWillFire = openKind == .continuation
          && (isAtStartOfLine || length > spaceRemaining || mustBreak)
        let contributesContinuationIndent = currentLineIsContinuation || continuationBreakWillFire

        activeOpenBreaks.append(
          ActiveOpenBreak(
            index: idx,
            kind: openKind,
            lineNumber: currentLineNumber,
            contributesContinuationIndent: contributesContinuationIndent,
            contributesBlockIndent: openKind == .block))

        continuationStack.append(currentLineIsContinuation)

        // Once we've reached an open break and preserved the continuation state, the "scope" we now
        // enter is *not* a continuation scope. If it was one, we'll re-enter it when we reach the
        // corresponding close.
        currentLineIsContinuation = false

      case .close(let closeMustBreak):
        guard let matchingOpenBreak = activeOpenBreaks.popLast() else {
          fatalError("Unmatched closing break")
        }

        let openedOnDifferentLine
          = openCloseBreakCompensatingLineNumber != matchingOpenBreak.lineNumber

        if matchingOpenBreak.contributesBlockIndent {
          // The actual line number is used, instead of the compensating line number. When the close
          // break is at the start of a new line, the block indentation isn't carried to the new line.
          let currentLine = lineNumber
          // When two or more open breaks are encountered on the same line, only the final open
          // break is allowed to increase the block indent, avoiding multiple block indents. As the
          // open breaks on that line are closed, the new final open break must be enabled again to
          // add a block indent.
          if matchingOpenBreak.lineNumber == currentLine,
            let lastActiveOpenBreak = activeOpenBreaks.last,
            lastActiveOpenBreak.kind == .block,
            !lastActiveOpenBreak.contributesBlockIndent
          {
            activeOpenBreaks[activeOpenBreaks.count - 1].contributesBlockIndent = true
          }
        }

        if closeMustBreak {
          // If it's a mandatory breaking close, then we must break (regardless of line length) if
          // the break is on a different line than its corresponding open break.
          mustBreak = openedOnDifferentLine
        } else if spaceRemaining == 0 {
          // If there is no room left on the line, then we must force this break to fire so that the
          // next token that comes along (typically a closing bracket of some kind) ends up on the
          // next line.
          mustBreak = true
        } else {
          // Otherwise, if we're not force-breaking and we're on a different line than the
          // corresponding open, then the current line must effectively become a continuation line.
          // This ensures that any reset breaks that might follow on the same line are honored. For
          // example, the reset break before the open curly brace below must be made to fire so that
          // the brace can distinguish the argument lines from the block body.
          //
          //    if let someLongVariableName = someLongFunctionName(
          //      firstArgument: argumentValue)
          //    {
          //      ...
          //    }
          //
          // In this case, the preferred style would be to break before the parenthesis and place it
          // on the same line as the curly brace, but that requires quite a bit more contextual
          // information than is easily available. The user can, however, do so with discretionary
          // breaks (if they are enabled).
          //
          // Note that in this case, the transformation of the current line into a continuation line
          // must happen regardless of whether this break fires.
          //
          // Likewise, we need to do this if we popped an old continuation state off the stack,
          // even if the break *doesn't* fire.
          let matchingOpenBreakIndented = matchingOpenBreak.contributesContinuationIndent
            || matchingOpenBreak.contributesBlockIndent
          currentLineIsContinuation = matchingOpenBreakIndented && openedOnDifferentLine
        }

        let wasContinuationWhenOpened = (continuationStack.popLast() ?? false)
          || matchingOpenBreak.contributesContinuationIndent
          // This ensures a continuation indent is propagated to following scope when an initial
          // scope would've indented if the leading break wasn't at the start of a line.
          || (matchingOpenBreak.kind == .continuation && openedOnDifferentLine)

        // Restore the continuation state of the scope we were in before the open break occurred.
        currentLineIsContinuation = currentLineIsContinuation || wasContinuationWhenOpened
        isContinuationIfBreakFires = wasContinuationWhenOpened

      case .continue:
        isContinuationIfBreakFires = true

      case .same:
        break

      case .reset:
        mustBreak = currentLineIsContinuation

      case .contextual:
        // When the last context spanned multiple lines, move the next context (in the same parent
        // break context scope) onto its own line. For example, this is used when the previous
        // context includes a multiline trailing closure or multiline function argument list.
        if let lastBreakingContext = lastEndedBreakingContext {
          if configuration.lineBreakAroundMultilineExpressionChainComponents {
            mustBreak = lastBreakingContext.lineNumber != lineNumber
          }
        }

        // Wait for a contextual break to fire and then update the breaking behavior for the rest of
        // the contextual breaks in this scope to match the behavior of the one that fired.
        let willFire = (!isAtStartOfLine && length > spaceRemaining) || mustBreak
        if willFire {
          // Update the active breaking context according to the most recently finished breaking
          // context so all following contextual breaks in this scope to have matching behavior.
          if let closedContext = lastEndedBreakingContext,
            let activeContext = activeBreakingContexts.last,
            case .unset = activeContext.contextualBreakingBehavior
          {
            activeBreakingContexts[activeBreakingContexts.count - 1].contextualBreakingBehavior =
              (closedContext.lineNumber == lineNumber) ? .continuation : .maintain
          }
        }

        if let activeBreakingContext = activeBreakingContexts.last {
          switch activeBreakingContext.contextualBreakingBehavior {
          case .unset, .continuation:
            isContinuationIfBreakFires = true
          case .maintain:
            isContinuationIfBreakFires = currentLineIsContinuation
          }
        }

        lastEndedBreakingContext = nil
      }

      var overrideBreakingSuppressed = false
      switch newline {
      case .elective: break
      case .soft(_, let discretionary):
        // A discretionary newline (i.e. from the source) should create a line break even if the
        // rules for breaking are disabled.
        overrideBreakingSuppressed = discretionary && allowSuppressedDiscretionaryBreaks
        mustBreak = true
      case .hard:
        // A hard newline must always create a line break, regardless of the context.
        overrideBreakingSuppressed = true
        mustBreak = true
      }

      let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed
      if !suppressBreaking && ((!isAtStartOfLine && length > spaceRemaining) || mustBreak) {
        currentLineIsContinuation = isContinuationIfBreakFires
        writeNewlines(newline)
        lastBreak = true
      } else {
        if isAtStartOfLine {
          // Make sure that the continuation status is correct even at the beginning of a line
          // (for example, after a newline token). This is necessary because a discretionary newline
          // might be inserted into the token stream before a continuation break, and the length of
          // that break might not be enough to satisfy the conditions above but we still need to
          // treat the line as a continuation.
          currentLineIsContinuation = isContinuationIfBreakFires
        }
        enqueueSpaces(size)
        lastBreak = false
      }

    // Print out the number of spaces according to the size, and adjust spaceRemaining.
    case .space(let size, _):
      enqueueSpaces(size)

    // Print any indentation required, followed by the text content of the syntax token.
    case .syntax(let text):
      guard !text.isEmpty else { break }
      lastBreak = false
      write(text)
      spaceRemaining -= text.count

    case .comment(let comment, let wasEndOfLine):
      lastBreak = false

      write(comment.print(indent: currentIndentation))
      if wasEndOfLine {
        if comment.length > spaceRemaining && !isBreakingSuppressed {
          diagnose(.moveEndOfLineComment, category: .endOfLineComment)
        }
      } else {
        spaceRemaining -= comment.length
      }

    case .verbatim(let verbatim):
      writeRaw(verbatim.print(indent: currentIndentation))
      consecutiveNewlineCount = 0
      pendingSpaces = 0
      lastBreak = false
      spaceRemaining -= length

    case .printerControl(let kind):
      switch kind {
      case .disableBreaking(let allowDiscretionary):
        activeBreakSuppressionCount += 1
        // Override the supression of discretionary breaks if we're at the top level or
        // discretionary breaks are currently allowed (false should override true, but not the other
        // way around).
        if activeBreakSuppressionCount == 1 || allowSuppressedDiscretionaryBreaks {
          allowSuppressedDiscretionaryBreaks = allowDiscretionary
        }
      case .enableBreaking:
        activeBreakSuppressionCount -= 1
      }

    case .commaDelimitedRegionStart:
      commaDelimitedRegionStack.append(openCloseBreakCompensatingLineNumber)

    case .commaDelimitedRegionEnd(let hasTrailingComma, let isSingleElement):
      guard let startLineNumber = commaDelimitedRegionStack.popLast() else {
        fatalError("Found trailing comma end with no corresponding start.")
      }

      // We need to specifically disable trailing commas on elements of single item collections.
      // The syntax library can't distinguish a collection's initializer (where the elements are
      // types) from a literal (where the elements are the contents of a collection instance).
      // We never want to add a trailing comma in an initializer so we disable trailing commas on
      // single element collections.
      let shouldHaveTrailingComma =
        startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement && configuration.multiElementCollectionTrailingCommas
      if shouldHaveTrailingComma && !hasTrailingComma {
        diagnose(.addTrailingComma, category: .trailingComma)
      } else if !shouldHaveTrailingComma && hasTrailingComma {
        diagnose(.removeTrailingComma, category: .trailingComma)
      }

      let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma
      if shouldWriteComma {
        write(",")
        spaceRemaining -= 1
      }

    case .enableFormatting(let enabledPosition):
      guard let disabledPosition else {
        // if we're not disabled, we ignore the token
        break
      }
      let start = source.utf8.index(source.utf8.startIndex, offsetBy: disabledPosition.utf8Offset)
      let end: String.Index
      if let enabledPosition {
        end = source.utf8.index(source.utf8.startIndex, offsetBy: enabledPosition.utf8Offset)
      } else {
        end = source.endIndex
      }
      var text = String(source[start..<end])
      // strip trailing whitespace so that the next formatting can add the right amount
      if let nonWhitespace = text.rangeOfCharacter(
        from: CharacterSet.whitespaces.inverted, options: .backwards) {
        text = String(text[..<nonWhitespace.upperBound])
      }

      self.disabledPosition = nil
      writeRaw(text)
      if text.hasSuffix("\n") {
        isAtStartOfLine = true
        consecutiveNewlineCount = 1
      } else {
        isAtStartOfLine = false
        consecutiveNewlineCount = 0
      }

    case .disableFormatting(let newPosition):
      assert(disabledPosition == nil)
      disabledPosition = newPosition
    }
  }

  /// Scan over the array of Tokens and calculate their lengths.
  ///
  /// This method is based on the `scan` function described in Derek Oppen's "Pretty Printing" paper
  /// (1979).
  ///
  /// - Returns: A String containing the formatted source code.
  public func prettyPrint() -> String {
    // Keep track of the indices of the .open and .break token locations.
    var delimIndexStack = [Int]()
    // Keep a running total of the token lengths.
    var total = 0

    // Calculate token lengths
    for (i, token) in tokens.enumerated() {
      switch token {
      case .contextualBreakingStart:
        lengths.append(0)

      case .contextualBreakingEnd:
        lengths.append(0)

      // Open tokens have lengths equal to the total of the contents of its group. The value is
      // calculated when close tokens are encountered.
      case .open:
        lengths.append(-total)
        delimIndexStack.append(i)

      // Close tokens have a length of 0. Calculate the length of the corresponding open token, and
      // the previous break token (if any).
      case .close:
        lengths.append(0)

        // TODO(dabelknap): Handle the unwrapping more gracefully
        guard let index = delimIndexStack.popLast() else {
          print("Bad index 1")
          return ""
        }
        lengths[index] += total

        // TODO(dabelknap): Handle the unwrapping more gracefully
        if case .break = tokens[index] {
          guard let index = delimIndexStack.popLast() else {
            print("Bad index 2")
            return ""
          }
          lengths[index] += total
        }

      // Break lengths are equal to its size plus the token or group following it. Calculate the
      // length of any prior break tokens.
      case .break(_, let size, let newline):
        if let index = delimIndexStack.last, case .break = tokens[index] {
          lengths[index] += total
          delimIndexStack.removeLast()
        }
        lengths.append(-total)
        delimIndexStack.append(i)

        if case .elective = newline {
          total += size
        } else {
          // `size` is never used in this case, because the break always fires. Use `maxLineLength`
          // to ensure enclosing groups are large enough to force preceding breaks to fire.
          total += maxLineLength
        }

      // Space tokens have a length equal to its size.
      case .space(let size, _):
        lengths.append(size)
        total += size

      // Syntax tokens have a length equal to the number of columns needed to print its contents.
      case .syntax(let text):
        lengths.append(text.count)
        total += text.count

      case .comment(let comment, let wasEndOfLine):
        lengths.append(comment.length)
        total += wasEndOfLine ? 0 : comment.length

      case .verbatim(let verbatim):
        let length = verbatim.prettyPrintingLength(maximum: maxLineLength)
        lengths.append(length)
        total += length

      case .printerControl:
        // Control tokens have no length. They aren't printed.
        lengths.append(0)

      case .commaDelimitedRegionStart:
        lengths.append(0)

      case .commaDelimitedRegionEnd(_, let isSingleElement):
        // The token's length is only necessary when a comma will be printed, but it's impossible to
        // know at this point whether the region-start token will be on the same line as this token.
        // Without adding this length to the total, it would be possible for this comma to be
        // printed in column `maxLineLength`. Unfortunately, this can cause breaks to fire
        // unnecessarily when the enclosed tokens comma would fit within `maxLineLength`.
        let length = isSingleElement ? 0 : 1
        total += length
        lengths.append(length)

      case .enableFormatting, .disableFormatting:
        // no effect on length calculations
        lengths.append(0)
      }
    }

    // There may be an extra break token that needs to have its length calculated.
    assert(delimIndexStack.count < 2, "Too many unresolved delimiter token lengths.")
    if let index = delimIndexStack.popLast() {
      if case .open = tokens[index] {
        assert(false, "Open tokens must be closed.")
      }
      lengths[index] += total
    }

    // Print out the token stream, wrapping according to line-length limitations.
    for i in 0..<tokens.count {
      printToken(idx: i)
    }

    guard activeOpenBreaks.isEmpty else {
      fatalError("At least one .break(.open) was not matched by a .break(.close)")
    }

    return outputBuffer
  }

  /// Used to track the indentation level for the debug token stream output.
  var debugIndent: [Indent] = []

  /// Print out the token stream to the console for debugging.
  ///
  /// Indentation is applied to make identification of groups easier.
  private func printDebugToken(token: Token, length: Int, idx: Int) {
    func printDebugIndent() {
      print(debugIndent.indentation(), terminator: "")
    }

    switch token {
    case .syntax(let syntax):
      printDebugIndent()
      print("[SYNTAX \"\(syntax)\" Length: \(length) Idx: \(idx)]")

    case .break(let kind, let size, let newline):
      printDebugIndent()
      print("[BREAK Kind: \(kind) Size: \(size) Length: \(length) NL: \(newline) Idx: \(idx)]")

    case .open(let breakstyle):
      printDebugIndent()
      switch breakstyle {
      case .consistent:
        print("[OPEN Consistent Length: \(length) Idx: \(idx)]")
      case .inconsistent:
        print("[OPEN Inconsistent Length: \(length) Idx: \(idx)]")
      }
      debugIndent.append(.spaces(2))

    case .close:
      debugIndent.removeLast()
      printDebugIndent()
      print("[CLOSE Idx: \(idx)]")

    case .space(let size, let flexible):
      printDebugIndent()
      print("[SPACE Size: \(size) Flexible: \(flexible) Length: \(length) Idx: \(idx)]")

    case .comment(let comment, let wasEndOfLine):
      printDebugIndent()
      switch comment.kind {
      case .line:
        print("[COMMENT Line Length: \(length) EOL: \(wasEndOfLine) Idx: \(idx)]")
      case .docLine:
        print("[COMMENT DocLine Length: \(length) EOL: \(wasEndOfLine) Idx: \(idx)]")
      case .block:
        print("[COMMENT Block Length: \(length) EOL: \(wasEndOfLine) Idx: \(idx)]")
      case .docBlock:
        print("[COMMENT DocBlock Length: \(length) EOL: \(wasEndOfLine) Idx: \(idx)]")
      }
      printDebugIndent()
      print(comment.print(indent: debugIndent))

    case .verbatim(let verbatim):
      printDebugIndent()
      print("[VERBATIM Length: \(length) Idx: \(idx)]")
      print(verbatim.print(indent: debugIndent))

    case .printerControl(let kind):
      printDebugIndent()
      print("[PRINTER CONTROL Kind: \(kind) Idx: \(idx)]")

    case .commaDelimitedRegionStart:
      printDebugIndent()
      print("[COMMA DELIMITED START Idx: \(idx)]")

    case .commaDelimitedRegionEnd:
      printDebugIndent()
      print("[COMMA DELIMITED END Idx: \(idx)]")

    case .contextualBreakingStart:
      printDebugIndent()
      print("[START BREAKING CONTEXT Idx: \(idx)]")

    case .contextualBreakingEnd:
      printDebugIndent()
      print("[END BREAKING CONTEXT Idx: \(idx)]")

    case .enableFormatting(let pos):
      printDebugIndent()
      print("[ENABLE FORMATTING utf8 offset: \(String(describing: pos))]")

    case .disableFormatting(let pos):
      printDebugIndent()
      print("[DISABLE FORMATTING utf8 offset: \(pos)]")
    }
  }

  /// Emits a finding with the given message and category at the current location in `outputBuffer`.
  private func diagnose(_ message: Finding.Message, category: PrettyPrintFindingCategory) {
    // Add 1 since columns uses 1-based indices.
    let column = maxLineLength - spaceRemaining + 1
    context.findingEmitter.emit(
      message,
      category: category,
      location: Finding.Location(file: context.fileURL.path, line: lineNumber, column: column))
  }
}

extension Finding.Message {
  fileprivate static let moveEndOfLineComment: Finding.Message =
    "move end-of-line comment that exceeds the line length"

  fileprivate static let addTrailingComma: Finding.Message =
    "add trailing comma to the last element in multiline collection literal"

  fileprivate static let removeTrailingComma: Finding.Message =
    "remove trailing comma from the last element in single line collection literal"
}