Skip to content

Commit b6c05ed

Browse files
authored
Merge pull request swiftlang#139 from dylansturg/unbalanced_parenthesized_exprs
Group the closing paren of exprs into the enclosed open/close breaks.
2 parents 01914f8 + 9df5aad commit b6c05ed

File tree

6 files changed

+369
-7
lines changed

6 files changed

+369
-7
lines changed

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
4545
/// breaks and start/end contextual breaking tokens have been inserted.
4646
private var preVisitedExprs = [ExprSyntax]()
4747

48+
/// Lists the tokens that are the closing or right parens of a parenthesized expression (i.e. a
49+
/// tuple expression with 1 element).
50+
private var parenthesizedExprParens = Set<TokenSyntax>()
51+
4852
init(configuration: Configuration, operatorContext: OperatorContext) {
4953
self.config = configuration
5054
self.operatorContext = operatorContext
@@ -645,6 +649,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
645649
// `stackedIndentationBehavior`).
646650
after(node.leftParen, tokens: .open)
647651
before(node.rightParen, tokens: .close)
652+
parenthesizedExprParens.insert(node.rightParen)
648653

649654
// When there's a comment inside of a parenthesized expression, we want to allow the comment
650655
// to exist at the EOL with the left paren or on its own line. The contents are always
@@ -1348,9 +1353,16 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
13481353
node.colonMark,
13491354
tokens: .break(.close(mustBreak: false), size: 0), .break(.open(kind: .continuation)), .open)
13501355
after(node.colonMark, tokens: .space)
1351-
after(
1352-
node.secondChoice.lastToken,
1353-
tokens: .break(.close(mustBreak: false), size: 0), .close, .close)
1356+
1357+
// When the ternary is wrapped in parens, absorb the closing paren into the ternary's group so
1358+
// that it is glued to the last token of the ternary.
1359+
let closeScopeToken: TokenSyntax?
1360+
if let parenExpr = outerMostEnclosingParenthesizedExpr(from: Syntax(node.secondChoice)) {
1361+
closeScopeToken = parenExpr.lastToken
1362+
} else {
1363+
closeScopeToken = node.secondChoice.lastToken
1364+
}
1365+
after(closeScopeToken, tokens: .break(.close(mustBreak: false), size: 0), .close, .close)
13541366
return .visitChildren
13551367
}
13561368

@@ -2866,6 +2878,24 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
28662878
}
28672879
}
28682880

2881+
/// Returns the node for the outermost parenthesized expr (i.e. a single element tuple) that is
2882+
/// ended at the given node. When the given node is not the last component of a parenthesized
2883+
/// expression, this method returns nil.
2884+
private func outerMostEnclosingParenthesizedExpr(from node: Syntax) -> Syntax? {
2885+
guard let afterToken = node.lastToken?.nextToken, parenthesizedExprParens.contains(afterToken)
2886+
else {
2887+
return nil
2888+
}
2889+
var parenthesizedExpr = afterToken.parent
2890+
while let nextToken = parenthesizedExpr?.lastToken?.nextToken,
2891+
parenthesizedExprParens.contains(nextToken),
2892+
let nextExpr = nextToken.parent
2893+
{
2894+
parenthesizedExpr = nextExpr
2895+
}
2896+
return parenthesizedExpr
2897+
}
2898+
28692899
/// Determines if indentation should be stacked around a subexpression to the right of the given
28702900
/// operator, and, if so, returns the node after which indentation stacking should be closed and
28712901
/// whether or not the continuation state should be reset as well.
@@ -2877,7 +2907,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
28772907
private func stackedIndentationBehavior(
28782908
after operatorExpr: ExprSyntax? = nil,
28792909
rhs: ExprSyntax
2880-
) -> (unindentingNode: ExprSyntax, shouldReset: Bool)? {
2910+
) -> (unindentingNode: Syntax, shouldReset: Bool)? {
28812911
// Check for logical operators first, and if it's that kind of operator, stack indentation
28822912
// around the entire right-hand-side. We have to do this check before checking the RHS for
28832913
// parentheses because if the user writes something like `... && (foo) > bar || ...`, we don't
@@ -2892,21 +2922,35 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
28922922
precedence === operatorContext.precedenceGroup(named: .logicalConjunction)
28932923
|| precedence === operatorContext.precedenceGroup(named: .logicalDisjunction)
28942924
{
2895-
return (unindentingNode: rhs, shouldReset: true)
2925+
// When `rhs` side is the last sequence in an enclosing parenthesized expression, absorb the
2926+
// paren into the right hand side by unindenting after the final closing paren. This glues
2927+
// the paren to the last token of `rhs`.
2928+
if let unindentingParenExpr = outerMostEnclosingParenthesizedExpr(from: Syntax(rhs)) {
2929+
return (unindentingNode: unindentingParenExpr, shouldReset: true)
2930+
}
2931+
return (unindentingNode: Syntax(rhs), shouldReset: true)
28962932
}
28972933
}
28982934

28992935
// If the right-hand-side is a ternary expression, stack indentation around the condition so
29002936
// that it is indented relative to the `?` and `:` tokens.
29012937
if let ternaryExpr = rhs.as(TernaryExprSyntax.self) {
2902-
return (unindentingNode: ternaryExpr.conditionExpression, shouldReset: false)
2938+
// We don't try to absorb any parens in this case, because the condition of a ternary cannot
2939+
// be grouped with any exprs outside of the condition.
2940+
return (unindentingNode: Syntax(ternaryExpr.conditionExpression), shouldReset: false)
29032941
}
29042942

29052943
// If the right-hand-side of the operator is or starts with a parenthesized expression, stack
29062944
// indentation around the operator and those parentheses. We don't need to reset here because
29072945
// the parentheses are sufficient to provide a visual indication of the nesting relationship.
29082946
if let parenthesizedExpr = parenthesizedLeftmostExpr(of: rhs) {
2909-
return (unindentingNode: ExprSyntax(parenthesizedExpr), shouldReset: false)
2947+
// When `rhs` side is the last sequence in an enclosing parenthesized expression, absorb the
2948+
// paren into the right hand side by unindenting after the final closing paren. This glues the
2949+
// paren to the last token of `rhs`.
2950+
if let unindentingParenExpr = outerMostEnclosingParenthesizedExpr(from: Syntax(rhs)) {
2951+
return (unindentingNode: unindentingParenExpr, shouldReset: true)
2952+
}
2953+
return (unindentingNode: Syntax(parenthesizedExpr), shouldReset: false)
29102954
}
29112955

29122956
// Otherwise, don't stack--use regular continuation breaks instead.

Tests/SwiftFormatPrettyPrintTests/GuardStmtTests.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,66 @@ final class GuardStmtTests: PrettyPrintTestCase {
197197

198198
assertPrettyPrintEqual(input: input, expected: expected, linelength: 40)
199199
}
200+
201+
func testParenthesizedClauses() {
202+
let input =
203+
"""
204+
guard foo && (
205+
bar < 1 || bar > 1
206+
) && baz else {
207+
// do something
208+
}
209+
guard muchLongerFoo && (
210+
muchLongerBar < 1 || muchLongerBar > 1
211+
) && baz else {
212+
// do something
213+
}
214+
guard muchLongerFoo && (
215+
muchLongerBar < 1 || muchLongerBar > 100000000
216+
) && baz else {
217+
// do something
218+
}
219+
guard muchLongerFoo && (
220+
muchLongerBar < 1 || muchLongerBar > 100000000 || (
221+
extraTerm1 + extraTerm2 + extraTerm3
222+
)
223+
) && baz else {
224+
// do something
225+
}
226+
"""
227+
228+
let expected =
229+
"""
230+
guard foo && (bar < 1 || bar > 1) && baz else {
231+
// do something
232+
}
233+
guard
234+
muchLongerFoo
235+
&& (muchLongerBar < 1 || muchLongerBar > 1)
236+
&& baz
237+
else {
238+
// do something
239+
}
240+
guard
241+
muchLongerFoo
242+
&& (muchLongerBar < 1
243+
|| muchLongerBar > 100000000)
244+
&& baz
245+
else {
246+
// do something
247+
}
248+
guard
249+
muchLongerFoo
250+
&& (muchLongerBar < 1
251+
|| muchLongerBar > 100000000
252+
|| (extraTerm1 + extraTerm2 + extraTerm3))
253+
&& baz
254+
else {
255+
// do something
256+
}
257+
258+
"""
259+
260+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 50)
261+
}
200262
}

Tests/SwiftFormatPrettyPrintTests/IfStmtTests.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,63 @@ final class IfStmtTests: PrettyPrintTestCase {
387387

388388
assertPrettyPrintEqual(input: input, expected: expected, linelength: 40)
389389
}
390+
391+
func testParenthesizedClauses() {
392+
let input =
393+
"""
394+
if foo && (
395+
bar < 1 || bar > 1
396+
) && baz {
397+
// do something
398+
}
399+
if muchLongerFoo && (
400+
muchLongerBar < 1 || muchLongerBar > 1
401+
) && baz {
402+
// do something
403+
}
404+
if muchLongerFoo && (
405+
muchLongerBar < 1 || muchLongerBar > 100000000
406+
) && baz {
407+
// do something
408+
}
409+
if muchLongerFoo && (
410+
muchLongerBar < 1 || muchLongerBar > 100000000 || (
411+
extraTerm1 + extraTerm2 + extraTerm3
412+
)
413+
) && baz {
414+
// do something
415+
}
416+
"""
417+
418+
let expected =
419+
"""
420+
if foo && (bar < 1 || bar > 1) && baz {
421+
// do something
422+
}
423+
if muchLongerFoo
424+
&& (muchLongerBar < 1 || muchLongerBar > 1)
425+
&& baz
426+
{
427+
// do something
428+
}
429+
if muchLongerFoo
430+
&& (muchLongerBar < 1
431+
|| muchLongerBar > 100000000)
432+
&& baz
433+
{
434+
// do something
435+
}
436+
if muchLongerFoo
437+
&& (muchLongerBar < 1
438+
|| muchLongerBar > 100000000
439+
|| (extraTerm1 + extraTerm2 + extraTerm3))
440+
&& baz
441+
{
442+
// do something
443+
}
444+
445+
"""
446+
447+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 50)
448+
}
390449
}

0 commit comments

Comments
 (0)