Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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()

Expand Down Expand Up @@ -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();
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
}
"""),
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 */ }

]
)
}
Expand Down Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add Trivia objects directly.


// 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 {
Expand Down
10 changes: 10 additions & 0 deletions Tests/BuiltInRulesTests/ConditionalReturnsOnNewlineRuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class ConditionalReturnsOnNewlineRuleTests: SwiftLintTestCase {
Example("if textField.returnKeyType == .Next {"),
Example("if true { // return }"),
Example("/*if true { */ return }"),
// guard statements should not trigger or be corrected
Example("guard true else { return }"),
]
let triggeringExamples = [
Expand All @@ -20,10 +21,19 @@ final class ConditionalReturnsOnNewlineRuleTests: SwiftLintTestCase {
Example("↓if true { break } else { return }"),
Example("↓if true { return \"YES\" } else { return \"NO\" }"),
]
// Only include `if` corrections - guard corrections should not apply with if_only
let expectedCorrections = [
Example("↓if true { return }"): Example("if true {\n return\n}"),
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\" }"),
]

let description = ConditionalReturnsOnNewlineRule.description
.with(triggeringExamples: triggeringExamples)
.with(nonTriggeringExamples: nonTriggeringExamples)
.with(corrections: expectedCorrections)

verifyRule(description, ruleConfiguration: ["if_only": true])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ conditional_returns_on_newline:
if_only: false
meta:
opt-in: true
correctable: false
correctable: true
contains_over_filter_count:
severity: warning
meta:
Expand Down