-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Make conditional_returns_on_newline rule autocorrectable #6489
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
base: main
Are you sure you want to change the base?
Changes from all commits
1c48cd1
2591ad2
678ca00
975967e
546f771
48d2a64
5140533
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import SwiftBasicFormat | ||
| import SwiftSyntax | ||
|
|
||
| @SwiftSyntaxRule(optIn: true) | ||
| @SwiftSyntaxRule(explicitRewriter: true, optIn: true) | ||
| struct ConditionalReturnsOnNewlineRule: Rule { | ||
| var configuration = ConditionalReturnsOnNewlineConfiguration() | ||
|
|
||
|
|
@@ -30,6 +31,33 @@ struct ConditionalReturnsOnNewlineRule: Rule { | |
| Example(""" | ||
| ↓guard condition else { XCTFail(); return } | ||
| """), | ||
| ], | ||
| corrections: [ | ||
| Example("↓if true { return }"): Example("if true {\n return\n}"), | ||
| Example(""" | ||
| func f() { | ||
| ↓if true { return } | ||
| } | ||
| """): Example(""" | ||
| func f() { | ||
| if true { | ||
| return | ||
| } | ||
| } | ||
| """), | ||
| Example("↓if true { break } else { return }"): | ||
| Example("if true { break } else {\n return\n}"), | ||
| Example("↓if true { return \"YES\" } else { return \"NO\" }"): | ||
| Example("if true {\n return \"YES\"\n} else { return \"NO\" }"), | ||
| Example("↓guard true else { return }"): | ||
| Example("guard true else {\n return\n}"), | ||
| Example(""" | ||
| ↓guard condition else { XCTFail(); return } | ||
| """): Example(""" | ||
| guard condition else { XCTFail(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure this is the expected fix here ... and not sure if there is any reasonable fix for that. We should probably restrict fixing to single return statements. That would also simplify the implementation a little. Alternatively, we could move both statements onto the next line but keeping them together (there is another rule to violate on multiple statements on one line). |
||
| return | ||
| } | ||
| """), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sure to keep leading an trailing comments as well, e.g. guard true else { /* leading comment */ return /* trailing comment */ } |
||
| ] | ||
| ) | ||
| } | ||
|
|
@@ -67,6 +95,95 @@ private extension ConditionalReturnsOnNewlineRule { | |
| locationConverter.location(for: token.positionAfterSkippingLeadingTrivia).line | ||
| } | ||
| } | ||
|
|
||
| final class Rewriter: ViolationsSyntaxRewriter<ConfigurationType> { | ||
| override func visit(_ node: IfExprSyntax) -> ExprSyntax { | ||
| // Match the visitor's logic: if body has violation, only fix body (and return early) | ||
| // If body is fine, check else body | ||
| if isReturn(node.body.statements.lastReturn, onTheSameLineAs: node.ifKeyword) { | ||
| numberOfCorrections += 1 | ||
| let modifiedNode = node.with(\.body, fixCodeBlock(node.body, baseToken: node.ifKeyword)) | ||
| return super.visit(modifiedNode) | ||
| } | ||
|
|
||
| // Check if else body's return is on the same line as the else keyword | ||
| if let elseBody = node.elseBody?.as(CodeBlockSyntax.self), let elseKeyword = node.elseKeyword, | ||
| isReturn(elseBody.statements.lastReturn, onTheSameLineAs: elseKeyword) { | ||
| numberOfCorrections += 1 | ||
| let fixedElseBody = fixCodeBlock(elseBody, baseToken: node.ifKeyword) | ||
| let modifiedNode = node.with(\.elseBody, .codeBlock(fixedElseBody)) | ||
| return super.visit(modifiedNode) | ||
| } | ||
|
|
||
| return super.visit(node) | ||
| } | ||
|
|
||
| override func visit(_ node: GuardStmtSyntax) -> StmtSyntax { | ||
| if configuration.ifOnly { | ||
| return super.visit(node) | ||
| } | ||
|
|
||
| guard isReturn(node.body.statements.lastReturn, onTheSameLineAs: node.guardKeyword) else { | ||
| return super.visit(node) | ||
| } | ||
|
|
||
| numberOfCorrections += 1 | ||
| let fixedNode = node.with(\.body, fixCodeBlock(node.body, baseToken: node.guardKeyword)) | ||
| return super.visit(fixedNode) | ||
| } | ||
|
|
||
| private func isReturn(_ returnStmt: ReturnStmtSyntax?, onTheSameLineAs token: TokenSyntax) -> Bool { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don't duplicate this method. |
||
| guard let returnStmt else { | ||
| return false | ||
| } | ||
|
|
||
| return locationConverter.location(for: returnStmt.returnKeyword.positionAfterSkippingLeadingTrivia).line == | ||
| locationConverter.location(for: token.positionAfterSkippingLeadingTrivia).line | ||
| } | ||
|
|
||
| private func fixCodeBlock(_ block: CodeBlockSyntax, baseToken: TokenSyntax) -> CodeBlockSyntax { | ||
| // Get the indentation of the base token (e.g., `if` or `guard`) | ||
| let baseIndentation = baseToken.leadingTrivia.indentation(isOnNewline: true) ?? Trivia() | ||
| let innerIndentation = Trivia(pieces: baseIndentation.pieces + [.spaces(4)]) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can add |
||
|
|
||
| // Check if the first statement is the return (i.e., return is the only statement) | ||
| let returnIsFirst = block.statements.first?.item.is(ReturnStmtSyntax.self) == true | ||
|
|
||
| // Fix the left brace: only remove trailing whitespace if return is the first statement | ||
| let fixedLeftBrace = returnIsFirst | ||
| ? block.leftBrace.with(\.trailingTrivia, Trivia()) | ||
| : block.leftBrace | ||
|
|
||
| // Fix the statements: add newline + inner indentation before the return, | ||
| // and remove trailing whitespace from the return value and the preceding statement | ||
| var statements = Array(block.statements) | ||
| for index in statements.indices where statements[index].item.is(ReturnStmtSyntax.self) { | ||
| // Remove trailing whitespace from the previous statement if there is one | ||
| if index > 0 { | ||
| statements[index - 1] = statements[index - 1].with(\.trailingTrivia, Trivia()) | ||
| } | ||
| // Add newline + indentation before return and remove its trailing whitespace | ||
| statements[index] = statements[index] | ||
| .with( | ||
| \.leadingTrivia, | ||
| Trivia(pieces: [.newlines(1)] + innerIndentation.pieces) | ||
| ) | ||
| .with(\.trailingTrivia, Trivia()) | ||
| } | ||
| let fixedStatements = CodeBlockItemListSyntax(statements) | ||
|
|
||
| // Fix the closing brace: add newline + base indentation before it | ||
| let fixedRightBrace = block.rightBrace.with( | ||
| \.leadingTrivia, | ||
| Trivia(pieces: [.newlines(1)] + baseIndentation.pieces) | ||
| ) | ||
|
|
||
| return block | ||
| .with(\.leftBrace, fixedLeftBrace) | ||
| .with(\.statements, fixedStatements) | ||
| .with(\.rightBrace, fixedRightBrace) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private extension CodeBlockItemListSyntax { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.