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) + """ + } }