Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wrapMultilineConditionalAssignment rule to wrap if / switch expressions to new line after assignment operator #1574

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a721974
Add rule to convert `forEach { ... }` calls to for loops (#1490)
calda Aug 1, 2023
c162045
Fix edge cases in `forLoop` rule (#1494)
calda Aug 6, 2023
21cee30
Rename `forLoop` to `preferForLoop`
nicklockwood Aug 12, 2023
1890ae7
`--conditionswrap` option to format condition in Xcode 12 style, in c…
Jan 27, 2021
b9b85d2
Add options for spacing around delimiter (#1335)
facumenzella Dec 27, 2022
8aad4f0
Extend `initCoderUnavailable ` rule (#1442)
facumenzella May 21, 2023
7a38fe6
Add `created.name` and `created.email` file header placeholders
hampustagerud Aug 8, 2023
82fd81d
Fix header replacements when placeholder is used multiple times
hampustagerud Aug 8, 2023
bd979dd
Add option to configure how dates are printed in the file header
hampustagerud Aug 9, 2023
8428d57
Add option to configure which timezone dates should be formatted to
hampustagerud Aug 14, 2023
bfccda0
Add documentation about new file header templating
hampustagerud Aug 14, 2023
00d0729
Integrate date formatting options in file header rule
hampustagerud Aug 15, 2023
80d1de9
Fix crash in the shell helper function
hampustagerud Aug 14, 2023
529fb99
Add test for GitFileInfo
hampustagerud Aug 15, 2023
3e04525
Add support for following the file across renames in git
hampustagerud Aug 15, 2023
a6b5bf5
Reduce number of shell commands to get git info and improve reliability
hampustagerud Aug 16, 2023
b46d172
Update workflow to support running git commands from tests
hampustagerud Aug 17, 2023
2c1bebe
Add noExplicitOwnership rule
calda Aug 27, 2023
7725ebc
Add option for forcing closing paren on same line of function calls
gering Oct 12, 2023
11eb5c5
Extract helper isFunctionCall(at:)
gering Oct 12, 2023
ec205e4
Support inference of options.forceClosingParenOnSameLineForFunctionCalls
gering Oct 13, 2023
4a69e73
Rename `closingparenfcall` to `callsiteparen`
nicklockwood Oct 14, 2023
a208d66
Flip `--shortoptionals` default to `except-properties`
nicklockwood Oct 29, 2023
0a56702
Implement wrapMultilineConditionalAssignment rule with WIP fix for in…
calda Nov 9, 2023
e6b98ed
Update indent rule to support wrapped conditional assignment formatting
calda Nov 16, 2023
e7190f3
Patch build for Swift 5.6 and earlier
calda Nov 16, 2023
727b791
Add test case for single-line conditional assignment
calda Nov 17, 2023
521698a
Merge branch 'develop' into cal--multilineConditionalAssignment
nicklockwood Nov 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
* [wrap](#wrap)
* [wrapArguments](#wrapArguments)
* [wrapAttributes](#wrapAttributes)
* [wrapMultilineConditionalAssignment](#wrapMultilineConditionalAssignment)
* [wrapMultilineStatementBraces](#wrapMultilineStatementBraces)
* [wrapSingleLineComments](#wrapSingleLineComments)
* [yodaConditions](#yodaConditions)
Expand Down Expand Up @@ -2691,6 +2692,28 @@ Option | Description
</details>
<br/>

## wrapMultilineConditionalAssignment

Wraps multiline conditional assignment expressions after the assignment operator.

<details>
<summary>Examples</summary>

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

</details>
<br/>

## wrapMultilineStatementBraces

Wrap the opening brace of multiline statements.
Expand Down
14 changes: 14 additions & 0 deletions Sources/Examples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
+ }
"""#
}
115 changes: 115 additions & 0 deletions Sources/Rules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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("")
Expand Down Expand Up @@ -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)
}
}
}
}
Loading