Skip to content

Commit a6041ff

Browse files
committed
[Lint/Format] Add a rule to detect extraneous return statements in function bodies
1 parent a2055fa commit a6041ff

File tree

5 files changed

+86
-0
lines changed

5 files changed

+86
-0
lines changed

Sources/SwiftFormat/Core/Pipelines+Generated.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class LintPipeline: SyntaxVisitor {
146146
visitIfEnabled(AlwaysUseLowerCamelCase.visit, for: node)
147147
visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node)
148148
visitIfEnabled(NoLeadingUnderscores.visit, for: node)
149+
visitIfEnabled(OmitReturns.visit, for: node)
149150
visitIfEnabled(UseTripleSlashForDocumentationComments.visit, for: node)
150151
visitIfEnabled(ValidateDocumentationComments.visit, for: node)
151152
return .visitChildren
@@ -343,6 +344,7 @@ extension FormatPipeline {
343344
node = NoLabelsInCasePatterns(context: context).rewrite(node)
344345
node = NoParensAroundConditions(context: context).rewrite(node)
345346
node = NoVoidReturnOnFunctionSignature(context: context).rewrite(node)
347+
node = OmitReturns(context: context).rewrite(node)
346348
node = OneCasePerLine(context: context).rewrite(node)
347349
node = OneVariableDeclarationPerLine(context: context).rewrite(node)
348350
node = OrderedImports(context: context).rewrite(node)

Sources/SwiftFormat/Core/RuleNameCache+Generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public let ruleNameCache: [ObjectIdentifier: String] = [
3737
ObjectIdentifier(NoLeadingUnderscores.self): "NoLeadingUnderscores",
3838
ObjectIdentifier(NoParensAroundConditions.self): "NoParensAroundConditions",
3939
ObjectIdentifier(NoVoidReturnOnFunctionSignature.self): "NoVoidReturnOnFunctionSignature",
40+
ObjectIdentifier(OmitReturns.self): "OmitReturns",
4041
ObjectIdentifier(OneCasePerLine.self): "OneCasePerLine",
4142
ObjectIdentifier(OneVariableDeclarationPerLine.self): "OneVariableDeclarationPerLine",
4243
ObjectIdentifier(OnlyOneTrailingClosureArgument.self): "OnlyOneTrailingClosureArgument",
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
/// Single-expression functions, closures, subscripts can omit `return` statement.
16+
///
17+
/// Lint: `func <name>() { return ... }` and similar single expression constructs will yield a lint error.
18+
///
19+
/// Format: `func <name>() { return ... }` constructs will be replaced with
20+
/// equivalent `func <name>() { ... }` constructs.
21+
@_spi(Rules)
22+
public final class OmitReturns: SyntaxFormatRule {
23+
public override class var isOptIn: Bool { return true }
24+
25+
public override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
26+
let decl = super.visit(node)
27+
28+
// func <name>() -> <Type> { return ... }
29+
if var funcDecl = decl.as(FunctionDeclSyntax.self),
30+
let body = funcDecl.body,
31+
let `return` = containsSingleReturn(body.statements) {
32+
funcDecl.body?.statements = unwrapReturnStmt(`return`)
33+
diagnose(.omitReturnStatement, on: `return`, severity: .refactoring)
34+
return DeclSyntax(funcDecl)
35+
}
36+
37+
return decl
38+
}
39+
40+
private func containsSingleReturn(_ body: CodeBlockItemListSyntax) -> ReturnStmtSyntax? {
41+
if let element = body.firstAndOnly?.as(CodeBlockItemSyntax.self),
42+
let ret = element.item.as(ReturnStmtSyntax.self),
43+
!ret.children(viewMode: .all).isEmpty, ret.expression != nil {
44+
return ret
45+
}
46+
47+
return nil
48+
}
49+
50+
private func unwrapReturnStmt(_ `return`: ReturnStmtSyntax) -> CodeBlockItemListSyntax {
51+
CodeBlockItemListSyntax([
52+
CodeBlockItemSyntax(
53+
leadingTrivia: `return`.leadingTrivia,
54+
item: .expr(`return`.expression!),
55+
semicolon: nil,
56+
trailingTrivia: `return`.trailingTrivia)
57+
])
58+
}
59+
}
60+
61+
extension Finding.Message {
62+
public static let omitReturnStatement: Finding.Message =
63+
"`return` can be omitted because body consists of a single expression"
64+
}

Sources/SwiftFormatConfiguration/RuleRegistry+Generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ enum RuleRegistry {
3636
"NoLeadingUnderscores": false,
3737
"NoParensAroundConditions": true,
3838
"NoVoidReturnOnFunctionSignature": true,
39+
"OmitReturns": false,
3940
"OneCasePerLine": true,
4041
"OneVariableDeclarationPerLine": true,
4142
"OnlyOneTrailingClosureArgument": true,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@_spi(Rules) import SwiftFormat
2+
3+
final class OmitReturnsTests: LintOrFormatRuleTestCase {
4+
func testOmitReturnInFunction() {
5+
XCTAssertFormatting(
6+
OmitReturns.self,
7+
input: """
8+
func test() -> Bool {
9+
return false
10+
}
11+
""",
12+
expected: """
13+
func test() -> Bool {
14+
false
15+
}
16+
""")
17+
}
18+
}

0 commit comments

Comments
 (0)