From 40721ffce3ef5604a9e18a3a67efb0c2233df65d Mon Sep 17 00:00:00 2001 From: Andrew Elliott Date: Fri, 9 Oct 2026 09:58:21 -0500 Subject: [PATCH 1/2] Add `check_if_and_switch_expressions` option to `implicit_return` When enabled, the rule also reports `return` statements in all branches of an `if` or `switch` that is the only statement of a body, as long as removing them turns it into a valid `if`/`switch` expression. Nested `if`/`switch` statements in branches are handled recursively. The option defaults to `false`. Resolves #6167. Co-authored-by: Nandhini Subramani <42835327+nandhinisubbu@users.noreply.github.com> --- CHANGELOG.md | 8 + .../ImplicitReturnConfiguration.swift | 5 +- .../Rules/Style/ImplicitReturnRule.swift | 77 +++++- ...nRuleExamples+IfAndSwitchExpressions.swift | 228 ++++++++++++++++++ .../ImplicitReturnConfigurationTests.swift | 2 + .../Resources/default_rule_configurations.yml | 1 + 6 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples+IfAndSwitchExpressions.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 50835855b4..4d018fd950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ ### Enhancements +* Add `check_if_and_switch_expressions` option to the `implicit_return` rule. + When enabled, the rule also triggers on `return` statements in all branches + of an `if` or `switch` (including nested ones) that could be turned into an + `if` or `switch` expression. Defaults to `false`. + [Andrew Elliott](https://github.com/andrewse02) + [nandhinisubbu](https://github.com/nandhinisubbu) + [#6167](https://github.com/realm/SwiftLint/issues/6167) + * Add autocorrection to the `multiline_call_arguments` rule, expanding single-line and multi-line calls to one-argument-per-line, including nested calls whose closing `)` would otherwise be stranded with the last argument. diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitReturnConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitReturnConfiguration.swift index 5c0b607607..c773323790 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitReturnConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ImplicitReturnConfiguration.swift @@ -21,9 +21,12 @@ struct ImplicitReturnConfiguration: SeverityBasedRuleConfiguration { private(set) var severityConfiguration = SeverityConfiguration(.warning) @ConfigurationElement(key: "included") private(set) var includedKinds = Self.defaultIncludedKinds + @ConfigurationElement(key: "check_if_and_switch_expressions") + private(set) var checkIfAndSwitchExpressions = false - init(includedKinds: Set = Self.defaultIncludedKinds) { + init(includedKinds: Set = Self.defaultIncludedKinds, checkIfAndSwitchExpressions: Bool = false) { self.includedKinds = includedKinds + self.checkIfAndSwitchExpressions = checkIfAndSwitchExpressions } func isKindIncluded(_ kind: ReturnKind) -> Bool { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift index 92aacced6d..0b30969b35 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift @@ -9,9 +9,14 @@ struct ImplicitReturnRule: Rule { name: "Implicit Return", description: "Prefer implicit returns in closures, functions and getters", kind: .style, - nonTriggeringExamples: ImplicitReturnRuleExamples.nonTriggeringExamples, - triggeringExamples: ImplicitReturnRuleExamples.triggeringExamples, + nonTriggeringExamples: ImplicitReturnRuleExamples.nonTriggeringExamples + + ImplicitReturnRuleExamples.IfAndSwitchExpressionExamples.nonTriggeringExamples, + triggeringExamples: ImplicitReturnRuleExamples.triggeringExamples + + ImplicitReturnRuleExamples.IfAndSwitchExpressionExamples.triggeringExamples, corrections: ImplicitReturnRuleExamples.corrections + .merging(ImplicitReturnRuleExamples.IfAndSwitchExpressionExamples.corrections) { _, _ in + preconditionFailure("Duplicate correction in implicit return rule examples.") + } ) } @@ -62,9 +67,15 @@ private extension ImplicitReturnRule { } private func collectViolation(in itemList: CodeBlockItemListSyntax) { - guard let returnStmt = itemList.onlyElement?.item.as(ReturnStmtSyntax.self) else { - return + if let returnStmt = itemList.onlyReturnStatement { + collectViolation(for: returnStmt) + } else if configuration.checkIfAndSwitchExpressions, + let returnStmts = itemList.ifOrSwitchExpressionReturnStatements { + returnStmts.forEach(collectViolation(for:)) } + } + + private func collectViolation(for returnStmt: ReturnStmtSyntax) { let returnKeyword = returnStmt.returnKeyword violations.append( at: returnKeyword.positionAfterSkippingLeadingTrivia, @@ -78,3 +89,61 @@ private extension ImplicitReturnRule { } } } + +private extension CodeBlockItemListSyntax { + var onlyReturnStatement: ReturnStmtSyntax? { + onlyElement?.item.as(ReturnStmtSyntax.self) + } + + /// The `return` statements in all branches of the only statement in this list if it is an `if` or `switch` + /// that could be used as an expression after removing them. `nil` otherwise. + var ifOrSwitchExpressionReturnStatements: [ReturnStmtSyntax]? { + guard let expression = onlyElement?.item.as(ExpressionStmtSyntax.self)?.expression else { + return nil + } + if let ifExpr = expression.as(IfExprSyntax.self) { + return ifExpr.branchReturnStatements + } + if let switchExpr = expression.as(SwitchExprSyntax.self) { + return switchExpr.branchReturnStatements + } + return nil + } + + /// The `return` statements that make this branch of an `if` or `switch` produce a value. Either the branch's + /// only statement returning a value or the returns of a nested `if` or `switch` qualifying itself. + var branchReturnStatements: [ReturnStmtSyntax]? { + if let returnStmt = onlyReturnStatement { + return returnStmt.expression == nil ? nil : [returnStmt] + } + return ifOrSwitchExpressionReturnStatements + } +} + +private extension IfExprSyntax { + var branchReturnStatements: [ReturnStmtSyntax]? { + guard let thenReturns = body.statements.branchReturnStatements else { + return nil + } + let elseReturns: [ReturnStmtSyntax]? = switch elseBody { + case let .codeBlock(block): block.statements.branchReturnStatements + case let .ifExpr(ifExpr): ifExpr.branchReturnStatements + case nil: nil + } + return elseReturns.map { thenReturns + $0 } + } +} + +private extension SwitchExprSyntax { + var branchReturnStatements: [ReturnStmtSyntax]? { + var returns = [ReturnStmtSyntax]() + for element in cases { + guard case let .switchCase(switchCase) = element, + let caseReturns = switchCase.statements.branchReturnStatements else { + return nil + } + returns += caseReturns + } + return returns.isEmpty ? nil : returns + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples+IfAndSwitchExpressions.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples+IfAndSwitchExpressions.swift new file mode 100644 index 0000000000..98ef2a79c5 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples+IfAndSwitchExpressions.swift @@ -0,0 +1,228 @@ +import SwiftLintCore + +extension ImplicitReturnRuleExamples { + struct IfAndSwitchExpressionExamples { + private static let configuration: [String: any Sendable] = ["check_if_and_switch_expressions": true] + + static let nonTriggeringExamples = #examples([ + """ + func f() -> Int { + if condition { + return 1 + } else { + return 2 + } + } + """.asExample(), + """ + func f() -> Int { + if condition { 1 } else { 2 } + } + """.asExample(configuration: configuration), + """ + func f() -> Int? { + if condition { + return 1 + } + return nil + } + """.asExample(configuration: configuration), + """ + func f() { + if condition { + return + } + } + """.asExample(configuration: configuration), + """ + func f() { + if condition { + return + } else { + return + } + } + """.asExample(configuration: configuration), + """ + func f() -> Int { + if condition { + let x = 1 + return x + } else { + return 2 + } + } + """.asExample(configuration: configuration), + """ + func f() -> Int { + if condition { + return 1 + } else { + throw MyError() + } + } + """.asExample(configuration: configuration), + """ + func f() -> Int { + if a { + return 1 + } else if b { + return 2 + } + return 3 + } + """.asExample(configuration: configuration), + """ + func f() -> String { + switch value { + case 0: + return "zero" + default: + print("other") + return "other" + } + } + """.asExample(configuration: configuration), + """ + func f() -> String { + switch value { + case 0: + return "zero" + #if DEBUG + default: + return "debug" + #else + default: + return "release" + #endif + } + } + """.asExample(configuration: configuration), + """ + func f() -> Int { + label: switch value { + case 0: return 1 + default: return 2 + } + } + """.asExample(configuration: configuration), + ]) + + static let triggeringExamples = #examples([ + """ + func f() -> Int { + if condition { + ↓return 1 + } else { + ↓return 2 + } + } + """.asExample(configuration: configuration), + """ + func f() -> Int { + if a { + ↓return 1 + } else if b { + ↓return 2 + } else { + ↓return 3 + } + } + """.asExample(configuration: configuration), + """ + func f() -> String { + switch value { + case 0: + ↓return "zero" + case 1: + ↓return "one" + @unknown default: + ↓return "other" + } + } + """.asExample(configuration: configuration), + """ + func f() -> String { + switch value { + case 0: + if condition { + ↓return "zero" + } else { + ↓return "nil" + } + default: + ↓return "other" + } + } + """.asExample(configuration: configuration), + """ + var foo: Int { + if condition { + switch value { + case 0: ↓return 0 + default: ↓return 1 + } + } else { + ↓return 2 + } + } + """.asExample(configuration: configuration), + """ + foo.map { value in + if value { + ↓return 1 + } else { + ↓return 2 + } + } + """.asExample(configuration: configuration), + ]) + + static let corrections = #corrections([ + """ + func f() -> Int { + if condition { + // Comment + return 1 + } else { + return 2 + } + } + """.asExample(configuration: configuration): """ + func f() -> Int { + if condition { + // Comment + 1 + } else { + 2 + } + } + """, + """ + func f() -> String { + switch value { + case 0: return "zero" + default: + if condition { + return "one" + } else { + return "other" + } + } + } + """.asExample(configuration: configuration): """ + func f() -> String { + switch value { + case 0: "zero" + default: + if condition { + "one" + } else { + "other" + } + } + } + """, + ]) + } +} diff --git a/Tests/BuiltInRulesTests/ImplicitReturnConfigurationTests.swift b/Tests/BuiltInRulesTests/ImplicitReturnConfigurationTests.swift index 07f63ddcf2..e2a466296a 100644 --- a/Tests/BuiltInRulesTests/ImplicitReturnConfigurationTests.swift +++ b/Tests/BuiltInRulesTests/ImplicitReturnConfigurationTests.swift @@ -16,6 +16,7 @@ struct ImplicitReturnConfigurationTests { "initializer", "subscript", ], + "check_if_and_switch_expressions": true, ] try configuration.apply(configuration: config) @@ -28,6 +29,7 @@ struct ImplicitReturnConfigurationTests { ]) #expect(configuration.severityConfiguration.severity == .error) #expect(configuration.includedKinds == expectedKinds) + #expect(configuration.checkIfAndSwitchExpressions) } @Test diff --git a/Tests/IntegrationTests/Resources/default_rule_configurations.yml b/Tests/IntegrationTests/Resources/default_rule_configurations.yml index cc2f59c8b2..701f796f63 100644 --- a/Tests/IntegrationTests/Resources/default_rule_configurations.yml +++ b/Tests/IntegrationTests/Resources/default_rule_configurations.yml @@ -520,6 +520,7 @@ implicit_optional_initialization: implicit_return: severity: warning included: [closure, function, getter, initializer, subscript] + check_if_and_switch_expressions: false meta: opt-in: true correctable: true From 0669c0061f502c22f93c08187a5255a7d5f68357 Mon Sep 17 00:00:00 2001 From: Andrew Elliott Date: Fri, 9 Oct 2026 14:42:24 -0500 Subject: [PATCH 2/2] Only check if/switch in closures with explicit return type Without a contextual type, the branches of an `if` or `switch` expression are type-checked independently. Removing the `return` statements can therefore break code that compiled before, e.g. branches returning `Optional(1)` and `nil` in an untyped closure. --- CHANGELOG.md | 3 +- .../Rules/Style/ImplicitReturnRule.swift | 11 ++++-- ...nRuleExamples+IfAndSwitchExpressions.swift | 34 ++++++++++++++++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d018fd950..d95bb1b4cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ * Add `check_if_and_switch_expressions` option to the `implicit_return` rule. When enabled, the rule also triggers on `return` statements in all branches of an `if` or `switch` (including nested ones) that could be turned into an - `if` or `switch` expression. Defaults to `false`. + `if` or `switch` expression. Closures are only checked when they declare + an explicit return type. Defaults to `false`. [Andrew Elliott](https://github.com/andrewse02) [nandhinisubbu](https://github.com/nandhinisubbu) [#6167](https://github.com/realm/SwiftLint/issues/6167) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift index 0b30969b35..052cb1d666 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRule.swift @@ -34,7 +34,12 @@ private extension ImplicitReturnRule { override func visitPost(_ node: ClosureExprSyntax) { if configuration.isKindIncluded(.closure) { - collectViolation(in: node.statements) + // Without an explicit return type, the branches of an `if` or `switch` expression are type-checked + // independently, which may fail where the `return` statements compiled fine (e.g. `1` and `nil`). + collectViolation( + in: node.statements, + checkIfAndSwitch: node.signature?.returnClause != nil + ) } } @@ -66,10 +71,10 @@ private extension ImplicitReturnRule { } } - private func collectViolation(in itemList: CodeBlockItemListSyntax) { + private func collectViolation(in itemList: CodeBlockItemListSyntax, checkIfAndSwitch: Bool = true) { if let returnStmt = itemList.onlyReturnStatement { collectViolation(for: returnStmt) - } else if configuration.checkIfAndSwitchExpressions, + } else if checkIfAndSwitch, configuration.checkIfAndSwitchExpressions, let returnStmts = itemList.ifOrSwitchExpressionReturnStatements { returnStmts.forEach(collectViolation(for:)) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples+IfAndSwitchExpressions.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples+IfAndSwitchExpressions.swift index 98ef2a79c5..e378094391 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples+IfAndSwitchExpressions.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ImplicitReturnRuleExamples+IfAndSwitchExpressions.swift @@ -99,6 +99,23 @@ extension ImplicitReturnRuleExamples { } """.asExample(configuration: configuration), """ + foo.map { value in + if value { + return 1 + } else { + return 2 + } + } + """.asExample(configuration: configuration), + """ + let f = { (x: Int) in + switch x { + case 0: return Optional(1) + default: return nil + } + } + """.asExample(configuration: configuration), + """ func f() -> Int { label: switch value { case 0: return 1 @@ -168,7 +185,7 @@ extension ImplicitReturnRuleExamples { } """.asExample(configuration: configuration), """ - foo.map { value in + foo.map { value -> Int in if value { ↓return 1 } else { @@ -223,6 +240,21 @@ extension ImplicitReturnRuleExamples { } } """, + """ + let f = { (x: Int) -> Int? in + switch x { + case 0: return 1 + default: return nil + } + } + """.asExample(configuration: configuration): """ + let f = { (x: Int) -> Int? in + switch x { + case 0: 1 + default: nil + } + } + """, ]) } }