diff --git a/Rules.md b/Rules.md
index b549f46e5..2b15515c1 100644
--- a/Rules.md
+++ b/Rules.md
@@ -80,6 +80,7 @@
* [wrap](#wrap)
* [wrapArguments](#wrapArguments)
* [wrapAttributes](#wrapAttributes)
+* [wrapMultilineConditionalAssignment](#wrapMultilineConditionalAssignment)
* [wrapMultilineStatementBraces](#wrapMultilineStatementBraces)
* [wrapSingleLineComments](#wrapSingleLineComments)
* [yodaConditions](#yodaConditions)
@@ -2691,6 +2692,28 @@ Option | Description
+## wrapMultilineConditionalAssignment
+
+Wraps multiline conditional assignment expressions after the assignment operator.
+
+
+Examples
+
+- let planetLocation = if let star = planet.star {
+- "The \(star.name) system"
+- } else {
+- "Rogue planet"
+- }
++ let planetLocation =
++ if let star = planet.star {
++ "The \(star.name) system"
++ } else {
++ "Rogue planet"
++ }
+
+
+
+
## wrapMultilineStatementBraces
Wrap the opening brace of multiline statements.
diff --git a/Sources/Examples.swift b/Sources/Examples.swift
index 3b9b13daa..54afaa6c1 100644
--- a/Sources/Examples.swift
+++ b/Sources/Examples.swift
@@ -1737,4 +1737,18 @@ private struct Examples {
+ func foo(_ bar: Bar) { ... }
```
"""
+
+ let wrapMultilineConditionalAssignment = #"""
+ - let planetLocation = if let star = planet.star {
+ - "The \(star.name) system"
+ - } else {
+ - "Rogue planet"
+ - }
+ + let planetLocation =
+ + if let star = planet.star {
+ + "The \(star.name) system"
+ + } else {
+ + "Rogue planet"
+ + }
+ """#
}
diff --git a/Sources/Rules.swift b/Sources/Rules.swift
index af836c90b..3606ad284 100644
--- a/Sources/Rules.swift
+++ b/Sources/Rules.swift
@@ -1614,7 +1614,50 @@ public struct _FormatRules {
{
indentStack[indentStack.count - 1] += formatter.options.indent
}
+ case .operator("=", .infix):
+ // If/switch expressions on their own line following an `=` assignment should always be indented
+ guard let nextKeyword = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: i),
+ ["if", "switch"].contains(formatter.tokens[nextKeyword].string),
+ !formatter.onSameLine(i, nextKeyword)
+ else { fallthrough }
+
+ let indent = (indentStack.last ?? "") + formatter.options.indent
+ indentStack.append(indent)
+ stringBodyIndentStack.append("")
+ indentCounts.append(1)
+ scopeStartLineIndexes.append(lineIndex)
+ linewrapStack.append(false)
+ scopeStack.append(.operator("=", .infix))
+ scopeStartLineIndexes.append(lineIndex)
+
default:
+ /// If this is the final `endOfScope` in a conditional assignment,
+ /// we have to end the scope introduced by that assignment operator.
+ defer {
+ if token == .endOfScope("}"), let startOfScope = formatter.startOfScope(at: i) {
+ // Find the `=` before this start of scope, which isn't itself part of the conditional statement
+ var previousAssignmentIndex = formatter.index(of: .operator("=", .infix), before: startOfScope)
+ while let currentPreviousAssignmentIndex = previousAssignmentIndex,
+ formatter.isConditionalStatement(at: currentPreviousAssignmentIndex)
+ {
+ previousAssignmentIndex = formatter.index(of: .operator("=", .infix), before: currentPreviousAssignmentIndex)
+ }
+
+ // Make sure the `=` actually created a new scope
+ if scopeStack.last == .operator("=", .infix),
+ // Parse the conditional branches following the `=` assignment operator
+ let previousAssignmentIndex = previousAssignmentIndex,
+ let nextTokenAfterAssignment = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: previousAssignmentIndex),
+ let conditionalBranches = formatter.conditionalBranches(at: nextTokenAfterAssignment),
+ // If this is the very end of the conditional assignment following the `=`,
+ // then we can end the scope.
+ conditionalBranches.last?.endOfBranch == i
+ {
+ popScope()
+ }
+ }
+ }
+
// Handle end of scope
if let scope = scopeStack.last, token.isEndOfScope(scope) {
let indentCount = indentCounts.last! - 1
@@ -1631,6 +1674,7 @@ public struct _FormatRules {
stringBodyIndentStack.append(stringBodyIndentStack.last ?? "")
}
}
+
// Don't reduce indent if line doesn't start with end of scope
let start = formatter.startOfLine(at: i)
guard let firstIndex = formatter.index(of: .nonSpaceOrComment, after: start - 1) else {
@@ -1960,6 +2004,7 @@ public struct _FormatRules {
} else if !formatter.options.xcodeIndentation || !isWrappedDeclaration() {
indent += formatter.linewrapIndent(at: i)
}
+
linewrapStack[linewrapStack.count - 1] = true
indentStack.append(indent)
stringBodyIndentStack.append("")
@@ -7587,4 +7632,74 @@ public struct _FormatRules {
}
}
}
+
+ public let wrapMultilineConditionalAssignment = FormatRule(
+ help: "Wraps multiline conditional assignment expressions after the assignment operator.",
+ orderAfter: ["conditionalAssignment"],
+ sharedOptions: ["linebreaks"]
+ ) { formatter in
+ formatter.forEach(.keyword) { introducerIndex, introducerToken in
+ guard ["let", "var"].contains(introducerToken.string),
+ let identifierIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: introducerIndex),
+ let identifier = formatter.token(at: identifierIndex),
+ identifier.isIdentifier
+ else { return }
+
+ // Find the `=` index for this variable, if present
+ let assignmentIndex: Int
+ if let colonIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: identifierIndex),
+ formatter.tokens[colonIndex] == .delimiter(":"),
+ let startOfTypeIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: colonIndex),
+ let typeRange = formatter.parseType(at: startOfTypeIndex)?.range,
+ let tokenAfterType = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: typeRange.upperBound),
+ formatter.tokens[tokenAfterType] == .operator("=", .infix)
+ {
+ assignmentIndex = tokenAfterType
+ }
+
+ else if let tokenAfterIdentifier = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: identifierIndex),
+ formatter.tokens[tokenAfterIdentifier] == .operator("=", .infix)
+ {
+ assignmentIndex = tokenAfterIdentifier
+ }
+
+ else {
+ return
+ }
+
+ // Verify the RHS of the assignment is an if/switch expression
+ guard let startOfConditionalExpression = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: assignmentIndex),
+ ["if", "switch"].contains(formatter.tokens[startOfConditionalExpression].string),
+ let conditionalBranches = formatter.conditionalBranches(at: startOfConditionalExpression),
+ let lastBranch = conditionalBranches.last
+ else { return }
+
+ // If the entire expression is on a single line, we leave the formatting as-is
+ guard !formatter.onSameLine(startOfConditionalExpression, lastBranch.endOfBranch) else {
+ return
+ }
+
+ // The `=` should be on the same line as the `let`/`var` introducer
+ if !formatter.onSameLine(introducerIndex, assignmentIndex),
+ formatter.last(.nonSpaceOrComment, before: assignmentIndex)?.isLinebreak == true,
+ let previousToken = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: assignmentIndex),
+ formatter.onSameLine(introducerIndex, previousToken)
+ {
+ // Move the assignment operator to follow the previous token.
+ // Also remove any trailing space after the previous position
+ // of the assignment operator.
+ if formatter.tokens[assignmentIndex + 1].isSpaceOrLinebreak {
+ formatter.removeToken(at: assignmentIndex + 1)
+ }
+
+ formatter.removeToken(at: assignmentIndex)
+ formatter.insert([.space(" "), .operator("=", .infix)], at: previousToken + 1)
+ }
+
+ // And there should be a line break between the `=` and the `if` / `switch` keyword
+ else if !formatter.tokens[(assignmentIndex + 1) ..< startOfConditionalExpression].contains(where: \.isLinebreak) {
+ formatter.insertLinebreak(at: startOfConditionalExpression - 1)
+ }
+ }
+ }
}
diff --git a/Tests/RulesTests+Indentation.swift b/Tests/RulesTests+Indentation.swift
index 4f7525293..26211c1c2 100644
--- a/Tests/RulesTests+Indentation.swift
+++ b/Tests/RulesTests+Indentation.swift
@@ -3759,4 +3759,304 @@ class IndentTests: RulesTests {
let options = FormatOptions(closingParenOnSameLine: true)
testFormatting(for: input, rule: FormatRules.indent, options: options)
}
+
+ func testIndentIfExpressionAssignmentOnNextLine() {
+ let input = """
+ let foo =
+ if let bar = someBar {
+ bar
+ } else if let baaz = someBaaz {
+ baaz
+ } else if let quux = someQuux {
+ if let foo = someFoo {
+ foo
+ } else {
+ quux
+ }
+ } else {
+ foo2
+ }
+
+ print(foo)
+ """
+
+ let output = """
+ let foo =
+ if let bar = someBar {
+ bar
+ } else if let baaz = someBaaz {
+ baaz
+ } else if let quux = someQuux {
+ if let foo = someFoo {
+ foo
+ } else {
+ quux
+ }
+ } else {
+ foo2
+ }
+
+ print(foo)
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.indent, exclude: ["wrapMultilineStatementBraces"])
+ }
+
+ func testIndentIfExpressionAssignmentOnSameLine() {
+ let input = """
+ let foo = if let bar {
+ bar
+ } else if let baaz {
+ baaz
+ } else if let quux {
+ if let foo {
+ foo
+ } else {
+ quux
+ }
+ }
+ """
+
+ testFormatting(for: input, rule: FormatRules.indent, exclude: ["wrapMultilineConditionalAssignment"])
+ }
+
+ func testIndentSwitchExpressionAssignment() {
+ let input = """
+ let foo =
+ switch bar {
+ case true:
+ bar
+ case baaz:
+ baaz
+ }
+ """
+
+ let output = """
+ let foo =
+ switch bar {
+ case true:
+ bar
+ case baaz:
+ baaz
+ }
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.indent)
+ }
+
+ func testIndentSwitchExpressionAssignmentInNestedScope() {
+ let input = """
+ class Foo {
+ func foo() -> Foo {
+ let foo =
+ switch bar {
+ case true:
+ bar
+ case baaz:
+ baaz
+ }
+
+ return foo
+ }
+ }
+ """
+
+ let output = """
+ class Foo {
+ func foo() -> Foo {
+ let foo =
+ switch bar {
+ case true:
+ bar
+ case baaz:
+ baaz
+ }
+
+ return foo
+ }
+ }
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.indent)
+ }
+
+ func testIndentNestedSwitchExpressionAssignment() {
+ let input = """
+ let foo =
+ switch bar {
+ case true:
+ bar
+ case baaz:
+ switch bar {
+ case true:
+ bar
+ case baaz:
+ baaz
+ }
+ }
+ """
+
+ let output = """
+ let foo =
+ switch bar {
+ case true:
+ bar
+ case baaz:
+ switch bar {
+ case true:
+ bar
+ case baaz:
+ baaz
+ }
+ }
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.indent)
+ }
+
+ func testIndentSwitchExpressionAssignmentWithComments() {
+ let input = """
+ let foo =
+ // There is a comment before the switch statement
+ switch bar {
+ // Plus a comment before each case
+ case true:
+ bar
+ // Plus a comment before each case
+ case baaz:
+ baaz
+ }
+
+ print(foo)
+ """
+
+ let output = """
+ let foo =
+ // There is a comment before the switch statement
+ switch bar {
+ // Plus a comment before each case
+ case true:
+ bar
+ // Plus a comment before each case
+ case baaz:
+ baaz
+ }
+
+ print(foo)
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.indent)
+ }
+
+ func testIndentIfExpressionWithSingleComment() {
+ let input = """
+ let foo =
+ // There is a comment before the first branch
+ if let foo {
+ foo
+ } else {
+ bar
+ }
+
+ print(foo)
+ """
+
+ testFormatting(for: input, rule: FormatRules.indent)
+ }
+
+ func testIndentIfExpressionWithComments() {
+ let input = """
+ let foo =
+ // There is a comment before the first branch
+ if let foo {
+ foo
+ }
+ // There is a comment before the second branch
+ else {
+ bar
+ }
+
+ print(foo)
+ """
+
+ testFormatting(for: input, rule: FormatRules.indent, exclude: ["wrapMultilineStatementBraces"])
+ }
+
+ func testIndentMultilineIfExpression() {
+ let input = """
+ let foo =
+ if
+ let foo,
+ foo != disallowedFoo
+ {
+ foo
+ }
+ // There is a comment before the second branch
+ else {
+ bar
+ }
+
+ print(foo)
+ print(foo)
+ """
+
+ testFormatting(for: input, rule: FormatRules.indent, exclude: ["braces"])
+ }
+
+ func testIndentNestedIfExpressionWithComments() {
+ let input = """
+ let foo =
+ // There is a comment before the first branch
+ if let foo {
+ foo
+ }
+ // There is a comment before the second branch
+ else {
+ // And a comment before each of these nested branches
+ if let bar {
+ bar
+ }
+ // And a comment before each of these nested branches
+ else {
+ baaz
+ }
+ }
+
+ print(foo)
+ """
+
+ testFormatting(for: input, rule: FormatRules.indent, exclude: ["wrapMultilineStatementBraces"])
+ }
+
+ func testIndentIfExpressionWithMultilineComments() {
+ let input = """
+ let foo =
+ // There is a comment before the first branch
+ // which spans across multiple lines
+ if let foo {
+ foo
+ }
+ // And also a comment before the second branch
+ // which spans across multiple lines
+ else {
+ bar
+ }
+ """
+
+ testFormatting(for: input, rule: FormatRules.indent)
+ }
+
+ func testSE0380Example() {
+ let input = """
+ let bullet =
+ if isRoot && (count == 0 || !willExpand) { "" }
+ else if count == 0 { "- " }
+ else if maxDepth <= 0 { "▹ " }
+ else { "▿ " }
+
+ print(bullet)
+ """
+ let options = FormatOptions()
+ testFormatting(for: input, rule: FormatRules.indent, options: options, exclude: ["wrapConditionalBodies", "andOperator", "redundantParens"])
+ }
}
diff --git a/Tests/RulesTests+Redundancy.swift b/Tests/RulesTests+Redundancy.swift
index c3b002170..2a4cf4b0d 100644
--- a/Tests/RulesTests+Redundancy.swift
+++ b/Tests/RulesTests+Redundancy.swift
@@ -1273,7 +1273,7 @@ class RedundancyTests: RulesTests {
}
"""
let options = FormatOptions(redundantType: .inferred, swiftVersion: "5.9")
- testFormatting(for: input, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, rule: FormatRules.redundantType, options: options, exclude: ["wrapMultilineConditionalAssignment"])
}
func testRedundantTypeWithIfExpression_inferred() {
@@ -1292,7 +1292,7 @@ class RedundancyTests: RulesTests {
}
"""
let options = FormatOptions(redundantType: .inferred, swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType, options: options, exclude: ["wrapMultilineConditionalAssignment"])
}
func testRedundantTypeWithIfExpression_explicit() {
@@ -1311,7 +1311,7 @@ class RedundancyTests: RulesTests {
}
"""
let options = FormatOptions(redundantType: .explicit, swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType, options: options, exclude: ["wrapMultilineConditionalAssignment"])
}
func testRedundantTypeWithNestedIfExpression_inferred() {
@@ -1348,7 +1348,7 @@ class RedundancyTests: RulesTests {
}
"""
let options = FormatOptions(redundantType: .inferred, swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType, options: options, exclude: ["wrapMultilineConditionalAssignment"])
}
func testRedundantTypeWithNestedIfExpression_explicit() {
@@ -1385,7 +1385,7 @@ class RedundancyTests: RulesTests {
}
"""
let options = FormatOptions(redundantType: .explicit, swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType, options: options, exclude: ["wrapMultilineConditionalAssignment"])
}
func testRedundantTypeWithLiteralsInIfExpression() {
@@ -1404,7 +1404,7 @@ class RedundancyTests: RulesTests {
}
"""
let options = FormatOptions(redundantType: .inferred, swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.redundantType, options: options)
+ testFormatting(for: input, output, rule: FormatRules.redundantType, options: options, exclude: ["wrapMultilineConditionalAssignment"])
}
// --redundanttype explicit
@@ -2694,7 +2694,7 @@ class RedundancyTests: RulesTests {
}
"""
let options = FormatOptions(swiftVersion: "5.9")
- testFormatting(for: input, [output], rules: [FormatRules.redundantReturn, FormatRules.redundantClosure, FormatRules.indent], options: options)
+ testFormatting(for: input, [output], rules: [FormatRules.redundantReturn, FormatRules.redundantClosure, FormatRules.indent], options: options, exclude: ["wrapMultilineConditionalAssignment"])
}
func testRedundantSwitchStatementReturnInFunction() {
@@ -7850,7 +7850,7 @@ class RedundancyTests: RulesTests {
let options = FormatOptions(swiftVersion: "5.9")
testFormatting(for: input, [output], rules: [FormatRules.redundantReturn, FormatRules.redundantClosure],
- options: options, exclude: ["indent"])
+ options: options, exclude: ["indent", "wrapMultilineConditionalAssignment"])
}
func testRedundantClosureWithExplicitReturn2() {
@@ -8247,7 +8247,7 @@ class RedundancyTests: RulesTests {
"""
let options = FormatOptions(swiftVersion: "5.9")
testFormatting(for: input, [output], rules: [FormatRules.redundantReturn, FormatRules.redundantClosure],
- options: options, exclude: ["indent"])
+ options: options, exclude: ["indent", "wrapMultilineConditionalAssignment"])
}
func testRedundantClosureDoesntLeaveStrayTryAwait() {
@@ -8269,7 +8269,7 @@ class RedundancyTests: RulesTests {
"""
let options = FormatOptions(swiftVersion: "5.9")
testFormatting(for: input, [output], rules: [FormatRules.redundantReturn, FormatRules.redundantClosure],
- options: options, exclude: ["indent"])
+ options: options, exclude: ["indent", "wrapMultilineConditionalAssignment"])
}
func testRedundantClosureDoesntLeaveInvalidSwitchExpressionInOperatorChain() {
@@ -8387,7 +8387,7 @@ class RedundancyTests: RulesTests {
"""
let options = FormatOptions(swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.redundantClosure, options: options, exclude: ["indent"])
+ testFormatting(for: input, output, rule: FormatRules.redundantClosure, options: options, exclude: ["indent", "wrapMultilineConditionalAssignment"])
}
func testRedundantClosureDoesntLeaveInvalidSwitchExpressionInArray() {
@@ -8722,7 +8722,7 @@ class RedundancyTests: RulesTests {
"""
let options = FormatOptions(swiftVersion: "5.10")
- testFormatting(for: input, output, rule: FormatRules.redundantClosure, options: options, exclude: ["indent"])
+ testFormatting(for: input, output, rule: FormatRules.redundantClosure, options: options, exclude: ["indent", "wrapMultilineConditionalAssignment"])
}
func testRedundantClosureDoesntBreakBuildWithRedundantReturnRuleDisabled() {
@@ -8785,7 +8785,7 @@ class RedundancyTests: RulesTests {
let options = FormatOptions(swiftVersion: "5.9")
testFormatting(for: input, [output], rules: [FormatRules.redundantReturn, FormatRules.redundantClosure],
- options: options, exclude: ["indent", "blankLinesBetweenScopes"])
+ options: options, exclude: ["indent", "blankLinesBetweenScopes", "wrapMultilineConditionalAssignment"])
}
func testRedundantSwitchStatementReturnInFunctionWithMultipleWhereClauses() {
diff --git a/Tests/RulesTests+Syntax.swift b/Tests/RulesTests+Syntax.swift
index d4b4e1a2c..c2e26d746 100644
--- a/Tests/RulesTests+Syntax.swift
+++ b/Tests/RulesTests+Syntax.swift
@@ -3437,7 +3437,7 @@ class SyntaxTests: RulesTests {
}
"""
let options = FormatOptions(swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["redundantType"])
+ testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["redundantType", "wrapMultilineConditionalAssignment"])
}
func testConvertsSimpleSwitchStatementAssignment() {
@@ -3459,7 +3459,7 @@ class SyntaxTests: RulesTests {
}
"""
let options = FormatOptions(swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["redundantType"])
+ testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["redundantType", "wrapMultilineConditionalAssignment"])
}
func testConvertsTrivialSwitchStatementAssignment() {
@@ -3477,7 +3477,7 @@ class SyntaxTests: RulesTests {
}
"""
let options = FormatOptions(swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options)
+ testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["wrapMultilineConditionalAssignment"])
}
func testConvertsNestedIfAndStatementAssignments() {
@@ -3525,7 +3525,7 @@ class SyntaxTests: RulesTests {
}
"""
let options = FormatOptions(swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["redundantType"])
+ testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["redundantType", "wrapMultilineConditionalAssignment"])
}
func testConvertsIfStatementAssignmentPreservingComment() {
@@ -3548,7 +3548,7 @@ class SyntaxTests: RulesTests {
}
"""
let options = FormatOptions(swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["indent", "redundantType"])
+ testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["indent", "redundantType", "wrapMultilineConditionalAssignment"])
}
func testDoesntConvertsIfStatementAssigningMultipleProperties() {
@@ -3802,7 +3802,7 @@ class SyntaxTests: RulesTests {
"""
let options = FormatOptions(swiftVersion: "5.9")
- testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options)
+ testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["wrapMultilineConditionalAssignment"])
}
// TODO: update branches parser to handle this case properly
@@ -3881,7 +3881,273 @@ class SyntaxTests: RulesTests {
"""
let options = FormatOptions(swiftVersion: "5.10")
- testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options)
+ testFormatting(for: input, output, rule: FormatRules.conditionalAssignment, options: options, exclude: ["wrapMultilineConditionalAssignment"])
+ }
+
+ // MARK: - forLoop
+
+ func testConvertSimpleForEachToForLoop() {
+ let input = """
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ placeholderStrings.forEach { string in
+ print(string)
+ }
+
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ placeholderStrings.forEach { (string: String) in
+ print(string)
+ }
+ """
+
+ let output = """
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ for string in placeholderStrings {
+ print(string)
+ }
+
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ for string in placeholderStrings {
+ print(string)
+ }
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testConvertAnonymousForEachToForLoop() {
+ let input = """
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ placeholderStrings.forEach {
+ print($0)
+ }
+
+ potatoes.forEach({ $0.bake() })
+ """
+
+ let output = """
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ for placeholderString in placeholderStrings {
+ print(placeholderString)
+ }
+
+ for potato in potatoes { potato.bake() }
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testConvertNestedForEach() {
+ let input = """
+ let nestedArrays = [[1, 2], [3, 4]]
+ nestedArrays.forEach {
+ $0.forEach {
+ print($0)
+ }
+ }
+ """
+
+ let output = """
+ let nestedArrays = [[1, 2], [3, 4]]
+ for nestedArray in nestedArrays {
+ for nestedArrayItem in nestedArray {
+ print(nestedArrayItem)
+ }
+ }
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testDefaultNameAlreadyUsedInLoopBody() {
+ let input = """
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ placeholderStrings.forEach {
+ let placeholderString = $0.uppercased()
+ print(placeholderString, $0)
+ }
+ """
+
+ let output = """
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ for placeholderStringItem in placeholderStrings {
+ let placeholderString = placeholderStringItem.uppercased()
+ print(placeholderString, placeholderStringItem)
+ }
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testIgnoreLoopsWithCaptureListForNow() {
+ let input = """
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ placeholderStrings.forEach { [someCapturedValue = fooBar] in
+ print($0, someCapturedValue)
+ }
+ """
+ testFormatting(for: input, rule: FormatRules.preferForLoop)
+ }
+
+ func testConvertsReturnToContinue() {
+ let input = """
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ placeholderStrings.forEach {
+ func capitalize(_ value: String) -> String {
+ return value.uppercased()
+ }
+
+ if $0 == "foo" {
+ return
+ } else {
+ print(capitalize($0))
+ }
+ }
+ """
+
+ let output = """
+ let placeholderStrings = ["foo", "bar", "baaz"]
+ for placeholderString in placeholderStrings {
+ func capitalize(_ value: String) -> String {
+ return value.uppercased()
+ }
+
+ if placeholderString == "foo" {
+ continue
+ } else {
+ print(capitalize(placeholderString))
+ }
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testHandlesForEachOnChainedProperties() {
+ let input = """
+ let bar = foo.bar
+ bar.baaz.quux.strings.forEach {
+ print($0)
+ }
+ """
+
+ let output = """
+ let bar = foo.bar
+ for string in bar.baaz.quux.strings {
+ print(string)
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testHandlesForEachOnFunctionCallResult() {
+ let input = """
+ let bar = foo.bar
+ foo.item().bar[2].baazValues(option: true).forEach {
+ print($0)
+ }
+ """
+
+ let output = """
+ let bar = foo.bar
+ for baazValue in foo.item().bar[2].baazValues(option: true) {
+ print(baazValue)
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testHandlesForEachOnSubscriptResult() {
+ let input = """
+ let bar = foo.bar
+ foo.item().bar[2].dictionary["myValue"].forEach {
+ print($0)
+ }
+ """
+
+ let output = """
+ let bar = foo.bar
+ for dictionaryItem in foo.item().bar[2].dictionary["myValue"] {
+ print(dictionaryItem)
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testHandlesForEachOnArrayLiteral() {
+ let input = """
+ let quux = foo.bar.baaz.quux
+ ["foo", "bar", "baaz", quux].forEach {
+ print($0)
+ }
+ """
+
+ let output = """
+ let quux = foo.bar.baaz.quux
+ for item in ["foo", "bar", "baaz", quux] {
+ print(item)
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testHandlesForEachOnCurriedFunctionWithSubscript() {
+ let input = """
+ let quux = foo.bar.baaz.quux
+ foo(bar)(baaz)["item"].forEach {
+ print($0)
+ }
+ """
+
+ let output = """
+ let quux = foo.bar.baaz.quux
+ for fooItem in foo(bar)(baaz)["item"] {
+ print(fooItem)
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop)
+ }
+
+ func testHandlesForEachOnArrayLiteralInParens() {
+ let input = """
+ let quux = foo.bar.baaz.quux
+ (["foo", "bar", "baaz", quux]).forEach {
+ print($0)
+ }
+ """
+
+ let output = """
+ let quux = foo.bar.baaz.quux
+ for item in (["foo", "bar", "baaz", quux]) {
+ print(item)
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.preferForLoop, exclude: ["redundantParens"])
+ }
+
+ func testPreservesForEachAfterMultilineChain() {
+ let input = """
+ placeholderStrings
+ .filter { $0.style == .fooBar }
+ .map { $0.uppercased() }
+ .forEach { print($0) }
+
+ placeholderStrings
+ .filter({ $0.style == .fooBar })
+ .map({ $0.uppercased() })
+ .forEach({ print($0) })
+ """
+ testFormatting(for: input, rule: FormatRules.preferForLoop, exclude: ["trailingClosures"])
+ }
+
+ func testPreservesChainWithClosure() {
+ let input = """
+ // Converting this to a for loop would result in unusual looking syntax like
+ // `for string in strings.map { $0.uppercased() } { print($0) }`
+ // which causes a warning to be emitted: "trailing closure in this context is
+ // confusable with the body of the statement; pass as a parenthesized argument
+ // to silence this warning".
+ strings.map { $0.uppercased() }.forEach { print($0) }
+ """
+ testFormatting(for: input, rule: FormatRules.preferForLoop)
}
func testDoesntConvertIfStatementWithForLoopInBranch() {
diff --git a/Tests/RulesTests+Wrapping.swift b/Tests/RulesTests+Wrapping.swift
index cace62cd8..4932b663b 100644
--- a/Tests/RulesTests+Wrapping.swift
+++ b/Tests/RulesTests+Wrapping.swift
@@ -4935,4 +4935,82 @@ class WrappingTests: RulesTests {
testFormatting(for: input, output, rule: FormatRules.wrapSingleLineComments,
options: FormatOptions(maxWidth: 40), exclude: ["docComments"])
}
+
+ // MARK: - wrapMultilineConditionalAssignment
+
+ func testWrapIfExpressionAssignment() {
+ let input = """
+ let foo = if let bar {
+ bar
+ } else {
+ baaz
+ }
+ """
+
+ let output = """
+ let foo =
+ if let bar {
+ bar
+ } else {
+ baaz
+ }
+ """
+
+ testFormatting(for: input, [output], rules: [FormatRules.wrapMultilineConditionalAssignment, FormatRules.indent])
+ }
+
+ func testUnwrapsAssignmentOperatorInIfExpressionAssignment() {
+ let input = """
+ let foo
+ = if let bar {
+ bar
+ } else {
+ baaz
+ }
+ """
+
+ let output = """
+ let foo =
+ if let bar {
+ bar
+ } else {
+ baaz
+ }
+ """
+
+ testFormatting(for: input, [output], rules: [FormatRules.wrapMultilineConditionalAssignment, FormatRules.indent])
+ }
+
+ func testUnwrapsAssignmentOperatorInIfExpressionFollowingComment() {
+ let input = """
+ let foo
+ // In order to unwrap the `=` here it has to move it to
+ // before the comment, rather than simply unwrapping it.
+ = if let bar {
+ bar
+ } else {
+ baaz
+ }
+ """
+
+ let output = """
+ let foo =
+ // In order to unwrap the `=` here it has to move it to
+ // before the comment, rather than simply unwrapping it.
+ if let bar {
+ bar
+ } else {
+ baaz
+ }
+ """
+
+ testFormatting(for: input, [output], rules: [FormatRules.wrapMultilineConditionalAssignment, FormatRules.indent])
+ }
+
+ func testPreservesSingleLineConditionalAssignment() {
+ let input = """
+ let foo = if let bar { bar } else { baaz }
+ print(foo)
+ """
+ }
}