Skip to content

Commit 3d59e70

Browse files
m-chojnackitalanov
authored andcommitted
Add explicit_return opt-in rule
Add `explicit_return` opt-in rule that warns against omitting the `return` keyword inside closures, functions and getters. This rule provides behavior opposite to `implicit_return` and complements it. It's disabled by default and provides configurable return kinds and severity. # Conflicts: # Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ExplicitReturnConfiguration.swift # Source/SwiftLintBuiltInRules/Rules/Style/ExplicitReturnRule.swift # Source/SwiftLintBuiltInRules/Rules/Style/ExplicitReturnRuleExamples.swift # Tests/FileSystemAccessTests/ExplicitReturnConfigurationTests.swift # Tests/FileSystemAccessTests/ExplicitReturnRuleTests.swift
1 parent c40896d commit 3d59e70

7 files changed

Lines changed: 628 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3033,6 +3033,10 @@ accordingly._
30333033
[JP Simard](https://github.com/jpsim)
30343034

30353035
### Bug Fixes
3036+
* Add `explicit_return` opt-in rule that warns against omitting the `return`
3037+
keyword inside closures, functions and getters.
3038+
3039+
#### Bug Fixes
30363040

30373041
* Fix false positive in `self_in_property_initialization` rule when using
30383042
closures inside `didSet` and other accessors.

Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public let builtInRules: [any Rule.Type] = [
6262
ExplicitACLRule.self,
6363
ExplicitEnumRawValueRule.self,
6464
ExplicitInitRule.self,
65+
ExplicitReturnRule.self,
6566
ExplicitSelfRule.self,
6667
ExplicitTopLevelACLRule.self,
6768
ExplicitTypeInterfaceRule.self,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
public struct ExplicitReturnConfiguration: RuleConfiguration, Equatable {
2+
public enum ReturnKind: String, CaseIterable {
3+
case closure
4+
case function
5+
case getter
6+
}
7+
8+
public static let defaultIncludedKinds = Set(ReturnKind.allCases)
9+
10+
private(set) var severityConfiguration = SeverityConfiguration(.warning)
11+
12+
private(set) var includedKinds = Self.defaultIncludedKinds
13+
14+
public var consoleDescription: String {
15+
let includedKinds = self.includedKinds.map { $0.rawValue }
16+
return severityConfiguration.consoleDescription +
17+
", included: [\(includedKinds.sorted().joined(separator: ", "))]"
18+
}
19+
20+
public init(includedKinds: Set<ReturnKind> = Self.defaultIncludedKinds) {
21+
self.includedKinds = includedKinds
22+
}
23+
24+
public mutating func apply(configuration: Any) throws {
25+
guard let configuration = configuration as? [String: Any] else {
26+
throw ConfigurationError.unknownConfiguration
27+
}
28+
29+
if let includedKinds = configuration["included"] as? [String] {
30+
self.includedKinds = try Set(includedKinds.map {
31+
guard let kind = ReturnKind(rawValue: $0) else {
32+
throw ConfigurationError.unknownConfiguration
33+
}
34+
35+
return kind
36+
})
37+
}
38+
39+
if let severityString = configuration["severity"] as? String {
40+
try severityConfiguration.apply(configuration: severityString)
41+
}
42+
}
43+
44+
public func isKindIncluded(_ kind: ReturnKind) -> Bool {
45+
return self.includedKinds.contains(kind)
46+
}
47+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import Foundation
2+
import SourceKittenFramework
3+
import SwiftSyntax
4+
5+
public struct ExplicitReturnRule: ConfigurationProviderRule, SubstitutionCorrectableRule, OptInRule {
6+
public var configuration = ExplicitReturnConfiguration()
7+
8+
public init() {}
9+
10+
public static let description = RuleDescription(
11+
identifier: "explicit_return",
12+
name: "Explicit Return",
13+
description: "Prefer explicit returns in closures, functions and getters.",
14+
kind: .style,
15+
nonTriggeringExamples: ExplicitReturnRuleExamples.nonTriggeringExamples,
16+
triggeringExamples: ExplicitReturnRuleExamples.triggeringExamples,
17+
corrections: ExplicitReturnRuleExamples.corrections
18+
)
19+
20+
public func validate(file: SwiftLintFile) -> [StyleViolation] {
21+
return violations(file: file).map {
22+
StyleViolation(
23+
ruleDescription: Self.description,
24+
severity: configuration.severityConfiguration.severity,
25+
location: Location(file: file, byteOffset: $0)
26+
)
27+
}
28+
}
29+
30+
public func violationRanges(in file: SwiftLintFile) -> [NSRange] {
31+
return violations(file: file).compactMap {
32+
file.stringView.byteRangeToNSRange(ByteRange(location: $0, length: 0))
33+
}
34+
}
35+
36+
public func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
37+
return (violationRange, "return ")
38+
}
39+
40+
private func violations(file: SwiftLintFile) -> [ByteCount] {
41+
guard let tree = file.syntaxTree else { return [] }
42+
43+
let visitor = ExplicitReturnVisitor(includedKinds: configuration.includedKinds)
44+
visitor.walk(tree)
45+
46+
return visitor.positions.map { ByteCount($0.utf8Offset) }
47+
}
48+
}
49+
50+
private final class ExplicitReturnVisitor: SyntaxVisitor {
51+
private let includedKinds: Set<ExplicitReturnConfiguration.ReturnKind>
52+
53+
private(set) var positions: [AbsolutePosition] = []
54+
55+
init(includedKinds: Set<ExplicitReturnConfiguration.ReturnKind>) {
56+
self.includedKinds = includedKinds
57+
}
58+
59+
override func visitPost(_ node: ClosureExprSyntax) {
60+
guard includedKinds.contains(.closure),
61+
let firstItem = node.statements.first?.item,
62+
node.statements.count == 1 else { return }
63+
64+
if firstItem.isImplicitlyReturnable {
65+
positions.append(firstItem.positionAfterSkippingLeadingTrivia)
66+
}
67+
}
68+
69+
override func visitPost(_ node: FunctionDeclSyntax) {
70+
guard includedKinds.contains(.function),
71+
node.signature.allowsImplicitReturns,
72+
let firstItem = node.body?.statements.first?.item,
73+
node.body?.statements.count == 1 else { return }
74+
75+
if firstItem.isImplicitlyReturnable {
76+
positions.append(firstItem.positionAfterSkippingLeadingTrivia)
77+
}
78+
}
79+
80+
override func visitPost(_ node: VariableDeclSyntax) {
81+
guard includedKinds.contains(.getter) else { return }
82+
83+
for binding in node.bindings {
84+
if let accessor = binding.accessor?.as(CodeBlockSyntax.self) {
85+
// Shorthand syntax for getters: `var foo: Int { 0 }`
86+
guard let firstItem = accessor.statements.first?.item,
87+
accessor.statements.count == 1 else { continue }
88+
89+
if firstItem.isImplicitlyReturnable {
90+
positions.append(firstItem.positionAfterSkippingLeadingTrivia)
91+
}
92+
} else if let accessorBlock = binding.accessor?.as(AccessorBlockSyntax.self) {
93+
// Full syntax for getters: `var foo: Int { get { 0 } }`
94+
guard let accessor = accessorBlock.accessors.first(where: { $0.accessorKind.text == "get" }),
95+
let firstItem = accessor.body?.statements.first?.item,
96+
accessor.body?.statements.count == 1 else { continue }
97+
98+
if firstItem.isImplicitlyReturnable {
99+
positions.append(firstItem.positionAfterSkippingLeadingTrivia)
100+
}
101+
}
102+
}
103+
}
104+
}
105+
106+
private extension Syntax {
107+
var isImplicitlyReturnable: Bool {
108+
isProtocol(ExprSyntaxProtocol.self)
109+
}
110+
}
111+
112+
private extension FunctionSignatureSyntax {
113+
var allowsImplicitReturns: Bool {
114+
if let simpleType = output?.returnType.as(SimpleTypeIdentifierSyntax.self) {
115+
return simpleType.name.text != "Void" && simpleType.name.text != "Never"
116+
} else if let tupleType = output?.returnType.as(TupleTypeSyntax.self) {
117+
return !tupleType.elements.isEmpty
118+
} else {
119+
return output != nil
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)