-
Notifications
You must be signed in to change notification settings - Fork 244
/
Copy pathPrettyPrint.swift
791 lines (673 loc) · 32.3 KB
/
PrettyPrint.swift
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
//===----------------------------------------------------------------------===//
//
// 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 SwiftFormatConfiguration
import SwiftFormatCore
import SwiftSyntax
/// PrettyPrinter takes a Syntax node and outputs a well-formatted, re-indented reproduction of the
/// code as a String.
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 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.
private var isBreakingSupressed: Bool {
return activeBreakSuppressionCount > 0
}
/// 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.
/// - operatorContext: The operator context that defines the infix operators and precedence
/// groups that should be used to make operator-sensitive formatting decisions.
/// - 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, operatorContext: OperatorContext, node: Syntax, printTokenStream: Bool,
whitespaceOnly: Bool
) {
self.context = context
let configuration = context.configuration
self.tokens =
node.makeTokenStream(configuration: configuration, operatorContext: operatorContext)
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) {
outputBuffer.append(String(str))
}
/// Writes newlines into the output stream, taking into account any pre-existing 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 remaning 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
mustBreak = true
case .hard:
// A hard newline must always create a line break, regardless of the context.
overrideBreakingSuppressed = true
mustBreak = true
}
let suppressBreaking = isBreakingSupressed && !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 {
diagnose(.moveEndOfLineComment)
}
} 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:
activeBreakSuppressionCount += 1
case .enableBreaking:
activeBreakSuppressionCount -= 1
}
case .commaDelimitedRegionStart:
commaDelimitedRegionStack.append(openCloseBreakCompensatingLineNumber)
case .commaDelimitedRegionEnd(let hasTrailingComma):
guard let startLineNumber = commaDelimitedRegionStack.popLast() else {
fatalError("Found trailing comma end with no corresponding start.")
}
let shouldWriteComma = whitespaceOnly ? hasTrailingComma :
startLineNumber != openCloseBreakCompensatingLineNumber
if shouldWriteComma && !hasTrailingComma {
diagnose(.addTrailingComma)
} else if !shouldWriteComma && hasTrailingComma {
diagnose(.removeTrailingComma)
}
if shouldWriteComma {
write(",")
spaceRemaining -= 1
}
}
}
/// 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 indicies 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
// calcualted 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:
// 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`.
total += 1
lengths.append(1)
}
}
// There may be an extra break token that needs to have its length calculated.
assert(delimIndexStack.count < 2, "Too many unresolved delmiter 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)]")
}
}
/// Diagnoses the given message at the current location in `outputBuffer`.
private func diagnose(_ message: Diagnostic.Message) {
// Add 1 since columns uses 1-based indices.
let column = maxLineLength - spaceRemaining + 1
let offset = outputBuffer.utf8.count
let location =
SourceLocation(line: lineNumber, column: column, offset: offset, file: context.fileURL.path)
context.diagnosticEngine?.diagnose(message, location: location)
}
}
extension Diagnostic.Message {
public static let moveEndOfLineComment = Diagnostic.Message(
.warning, "move end-of-line comment that exceeds the line length")
public static let addTrailingComma = Diagnostic.Message(
.warning, "add trailing comma to the last element in multiline collection literal")
public static let removeTrailingComma = Diagnostic.Message(
.warning, "remove trailing comma from the last element in single line collection literal")
}