Skip to content

Commit 3c45c79

Browse files
committed
Disable breaks inside of #if conditionals.
Adding line breaks inside of #if conditionals is dangerous because line breaks are only valid if the entire condition is wrapped in parens. It's difficult to conditionally add parens only if a break is necessary because it implies backtracking into the printer's output buffer to insert the opening paren. Our options for fixing this problem were: 1. Implement paren wrapping the #if condition if a break fires (very complex). 2. Always paren wrap the #if condition (looks bad in the most common case where there's no breaking). 3. Disable breaking in the pretty print but allow users to explicitly break in the condition using discretionary newlines. I selected option swiftlang#3 and implemented by adding new kind of token known as "printer control" to disable/enable suppressing of break tokens. These printer control tokens can be used in other scenarios, such as suppressing breaks in string interpolation expressions. Fixes SR-11815
1 parent 4a07aa0 commit 3c45c79

File tree

5 files changed

+140
-1
lines changed

5 files changed

+140
-1
lines changed

Sources/SwiftFormatPrettyPrint/PrettyPrint.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ public class PrettyPrinter {
9696
/// `currentLineIsContinuation` to become true).
9797
private var wasLastBreakKindContinue = false
9898

99+
/// Tracks how many printer control tokens to suppress firing breaks are active.
100+
private var activeBreakSuppressionCount = 0
101+
102+
/// Whether breaks are supressed from firing. When true, no breaks should fire and the only way to
103+
/// move to a new line is an explicit new line token.
104+
private var isBreakingSupressed: Bool {
105+
return activeBreakSuppressionCount > 0
106+
}
107+
99108
/// The computed indentation level, as a number of spaces, based on the state of any unclosed
100109
/// delimiters and whether or not the current line is a continuation line.
101110
private var currentIndentation: [Indent] {
@@ -283,6 +292,7 @@ public class PrettyPrinter {
283292
// scope. Additionally, continuation open breaks must indent when the break fires.
284293
let continuationBreakWillFire = openKind == .continuation
285294
&& (isAtStartOfLine || length > spaceRemaining || mustBreak)
295+
&& !isBreakingSupressed
286296
let contributesContinuationIndent = currentLineIsContinuation || continuationBreakWillFire
287297

288298
activeOpenBreaks.append(
@@ -387,7 +397,7 @@ public class PrettyPrinter {
387397
mustBreak = currentLineIsContinuation
388398
}
389399

390-
if (!isAtStartOfLine && length > spaceRemaining) || mustBreak {
400+
if !isBreakingSupressed && ((!isAtStartOfLine && length > spaceRemaining) || mustBreak) {
391401
currentLineIsContinuation = isContinuationIfBreakFires
392402
writeNewlines(1, kind: .flexible)
393403
lastBreak = true
@@ -451,6 +461,14 @@ public class PrettyPrinter {
451461
pendingSpaces = 0
452462
lastBreak = false
453463
spaceRemaining -= length
464+
465+
case .printerControl(let kind):
466+
switch kind {
467+
case .disableBreaking:
468+
activeBreakSuppressionCount += 1
469+
case .enableBreaking:
470+
activeBreakSuppressionCount -= 1
471+
}
454472
}
455473
}
456474

@@ -554,6 +572,10 @@ public class PrettyPrinter {
554572
}
555573
lengths.append(length)
556574
total += length
575+
576+
case .printerControl:
577+
// Control tokens have no length. They aren't printed.
578+
lengths.append(0)
557579
}
558580
}
559581

@@ -642,6 +664,9 @@ public class PrettyPrinter {
642664
printDebugIndent()
643665
print("[VERBATIM Length: \(length) Idx: \(idx)]")
644666
print(verbatim.print(indent: debugIndent))
667+
668+
case .printerControl(let kind):
669+
print("[PRINTER CONTROL Kind: \(kind)]")
645670
}
646671
}
647672

Sources/SwiftFormatPrettyPrint/Token.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,21 @@ enum NewlineKind {
141141
case mandatory
142142
}
143143

144+
/// Kinds of printer control tokens that can be used to customize the pretty printer's behavior in
145+
/// a subsection of a token stream.
146+
enum PrinterControlKind {
147+
/// A signal that break tokens shouldn't be allowed to fire until a corresponding enable breaking
148+
/// control token is encountered.
149+
///
150+
/// It's valid to nest `disableBreaking` and `enableBreaking` tokens. Breaks will be suppressed
151+
/// long as there is at least 1 unmatched disable token.
152+
case disableBreaking
153+
154+
/// A signal that break tokens should be allowed to fire following this token, as long as there
155+
/// are no other unmatched disable tokens.
156+
case enableBreaking
157+
}
158+
144159
enum Token {
145160
case syntax(String)
146161
case open(GroupBreakStyle)
@@ -150,6 +165,7 @@ enum Token {
150165
case newlines(Int, kind: NewlineKind)
151166
case comment(Comment, wasEndOfLine: Bool)
152167
case verbatim(Verbatim)
168+
case printerControl(kind: PrinterControlKind)
153169

154170
// Convenience overloads for the enum types
155171
static let open = Token.open(.inconsistent, 0)

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,12 @@ private final class TokenStreamCreator: SyntaxVisitor {
956956
} else {
957957
before(tokenToOpenWith.nextToken, tokens: .break(breakKindClose), .newline, .close)
958958
}
959+
960+
if let condition = node.condition {
961+
before(condition.firstToken, tokens: .printerControl(kind: .disableBreaking))
962+
after(condition.lastToken, tokens: .printerControl(kind: .enableBreaking), .break(.reset, size: 0))
963+
}
964+
959965
return .visitChildren
960966
}
961967

Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,93 @@ public class IfConfigTests: PrettyPrintTestCase {
141141

142142
assertPrettyPrintEqual(input: input, expected: expected, linelength: 45)
143143
}
144+
145+
public func testPrettyPrintLineBreaksDisabled() {
146+
let input =
147+
"""
148+
#if canImport(SwiftUI) && !(os(iOS)&&arch( arm ) )&&( (canImport(AppKit) || canImport(UIKit)) && !os(watchOS))
149+
conditionalFunc(foo, bar, baz)
150+
#endif
151+
"""
152+
153+
let expected =
154+
"""
155+
#if canImport(SwiftUI) && !(os(iOS) && arch(arm)) && ((canImport(AppKit) || canImport(UIKit)) && !os(watchOS))
156+
conditionalFunc(foo, bar, baz)
157+
#endif
158+
159+
"""
160+
161+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 40)
162+
}
163+
164+
public func testInvalidDiscretionaryLineBreaksRemoved() {
165+
let input =
166+
"""
167+
#if (canImport(SwiftUI) &&
168+
!(os(iOS) &&
169+
arch(arm)) &&
170+
((canImport(AppKit) ||
171+
canImport(UIKit)) && !os(watchOS)))
172+
conditionalFunc(foo, bar, baz)
173+
#endif
174+
"""
175+
176+
let expected =
177+
"""
178+
#if (canImport(SwiftUI) && !(os(iOS) && arch(arm)) && ((canImport(AppKit) || canImport(UIKit)) && !os(watchOS)))
179+
conditionalFunc(foo, bar, baz)
180+
#endif
181+
182+
"""
183+
184+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 40)
185+
}
186+
187+
public func testValidDiscretionaryLineBreaksRetained() {
188+
let input =
189+
"""
190+
#if (canImport(SwiftUI)
191+
&& !(os(iOS)
192+
&& arch(arm))
193+
&& ((canImport(AppKit)
194+
|| canImport(UIKit)) && !os(watchOS))
195+
&& canImport(Foundation))
196+
conditionalFunc(foo, bar, baz)
197+
#endif
198+
199+
#if (canImport(SwiftUI)
200+
&& os(iOS)
201+
&& arch(arm)
202+
&& canImport(AppKit)
203+
|| canImport(UIKit) && !os(watchOS)
204+
&& canImport(Foundation))
205+
conditionalFunc(foo, bar, baz)
206+
#endif
207+
"""
208+
209+
let expected =
210+
"""
211+
#if (canImport(SwiftUI)
212+
&& !(os(iOS)
213+
&& arch(arm))
214+
&& ((canImport(AppKit)
215+
|| canImport(UIKit)) && !os(watchOS))
216+
&& canImport(Foundation))
217+
conditionalFunc(foo, bar, baz)
218+
#endif
219+
220+
#if (canImport(SwiftUI)
221+
&& os(iOS)
222+
&& arch(arm)
223+
&& canImport(AppKit)
224+
|| canImport(UIKit) && !os(watchOS)
225+
&& canImport(Foundation))
226+
conditionalFunc(foo, bar, baz)
227+
#endif
228+
229+
"""
230+
231+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 40)
232+
}
144233
}

Tests/SwiftFormatPrettyPrintTests/XCTestManifests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,10 @@ extension IfConfigTests {
313313
static let __allTests__IfConfigTests = [
314314
("testBasicIfConfig", testBasicIfConfig),
315315
("testIfConfigNoIndentation", testIfConfigNoIndentation),
316+
("testInvalidDiscretionaryLineBreaksRemoved", testInvalidDiscretionaryLineBreaksRemoved),
316317
("testPoundIfAroundMembers", testPoundIfAroundMembers),
318+
("testPrettyPrintLineBreaksDisabled", testPrettyPrintLineBreaksDisabled),
319+
("testValidDiscretionaryLineBreaksRetained", testValidDiscretionaryLineBreaksRetained),
317320
]
318321
}
319322

0 commit comments

Comments
 (0)