Skip to content

fix: void_function_in_ternary false positive inside if/switch expressions#6510

Open
WZBbiao wants to merge 3 commits intorealm:mainfrom
WZBbiao:fix/issue-5611-void-function-in-ternary-if-switch-expr
Open

fix: void_function_in_ternary false positive inside if/switch expressions#6510
WZBbiao wants to merge 3 commits intorealm:mainfrom
WZBbiao:fix/issue-5611-void-function-in-ternary-if-switch-expr

Conversation

@WZBbiao
Copy link

@WZBbiao WZBbiao commented Feb 24, 2026

Problem

The void_function_in_ternary rule incorrectly flags ternary expressions that are used as the return value of an if or switch expression (Swift 5.9+). When a ternary is the sole expression in a branch of an if/switch expression that itself serves as an implicit return value, its result is being used — so the rule should not flag it.

Minimal reproducer:

// False positive: ternary inside if expression used as function's implicit return
func makeValue() -> MyStruct {
    if condition {
        flag ? MyStruct(value: 0) : MyStruct(value: 1)  // void_function_in_ternary warning — incorrect
    } else {
        MyStruct(value: 2)
    }
}

// False positive: ternary inside switch expression used as function's implicit return
func computeSize(for section: Int) -> CGSize {
    switch section {
    case 0: isEditing ? CGSize(width: 150, height: 20) : CGSize(width: 100, height: 20)  // warning — incorrect
    default: .zero
    }
}

Related reports: #5611

Root Cause

The rule determines whether a ternary is a "void call" by checking whether its containing CodeBlockItemSyntax is in an implicit return position (via isImplicitReturn). However, isImplicitReturn only checked for:

  • Single-expression closures
  • Single-expression functions
  • Single-expression variable computed bodies
  • Single-expression subscripts / accessors

It did not account for a ternary being the sole expression inside a branch of an if or switch expression that itself occupies one of those implicit-return positions.

Solution

Added a new computed property isIfExprOrSwitchExprImplicitReturn to the CodeBlockItemSyntax extension. It walks up the syntax tree:

  • if expression branchCodeBlockItemListSyntax → CodeBlockSyntax → IfExprSyntax — and then checks whether the IfExprSyntax's own containing CodeBlockItemSyntax satisfies isImplicitReturn (recursively, to handle nesting).
  • switch expression caseCodeBlockItemListSyntax → SwitchCaseSyntax → SwitchCaseListSyntax → SwitchExprSyntax — and applies the same recursive check on the SwitchExprSyntax's container.

This correctly handles arbitrarily nested if/switch expressions inside any implicit-return context (functions, closures, computed properties, subscripts, accessors).

Testing

Two new non-triggering examples were added to VoidFunctionInTernaryConditionRule.description:

  1. A ternary inside an if expression branch that is the function's implicit return.
  2. A ternary inside a switch expression case that is the function's implicit return.

The existing triggering examples remain unchanged, ensuring that legitimate void-function ternaries (e.g. ternaries with additional statements following them in the same block) are still caught.

Fixes #5611

@SwiftLintBot
Copy link

SwiftLintBot commented Feb 24, 2026

1 Warning
⚠️ This PR may need tests.
19 Messages
📖 Building this branch resulted in a binary size of 27276.6 KiB vs 27276.45 KiB when built on main (0% larger).
📖 Linting Aerial with this PR took 0.2 s vs 0.17 s on main (17% slower).
📖 Linting Alamofire with this PR took 0.22 s vs 0.18 s on main (22% slower).
📖 Linting Brave with this PR took 0.83 s vs 0.81 s on main (2% slower).
📖 Linting DuckDuckGo with this PR took 3.24 s vs 3.25 s on main (0% faster).
📖 Linting Firefox with this PR took 1.52 s vs 1.52 s on main (0% slower).
📖 Linting Kickstarter with this PR took 0.93 s vs 0.92 s on main (1% slower).
📖 Linting Moya with this PR took 0.15 s vs 0.15 s on main (0% slower).
📖 Linting NetNewsWire with this PR took 0.34 s vs 0.36 s on main (5% faster).
📖 Linting Nimble with this PR took 0.18 s vs 0.16 s on main (12% slower).
📖 Linting PocketCasts with this PR took 0.91 s vs 0.91 s on main (0% slower).
📖 Linting Quick with this PR took 0.13 s vs 0.14 s on main (7% faster).
📖 Linting Realm with this PR took 0.43 s vs 0.42 s on main (2% slower).
📖 Linting Sourcery with this PR took 0.3 s vs 0.3 s on main (0% slower).
📖 Linting Swift with this PR took 0.49 s vs 0.49 s on main (0% slower).
📖 Linting SwiftLintPerformanceTests with this PR took 3.82 s vs 3.86 s on main (1% faster).
📖 Linting VLC with this PR took 0.25 s vs 0.25 s on main (0% slower).
📖 Linting Wire with this PR took 1.97 s vs 1.99 s on main (1% faster).
📖 Linting WordPress with this PR took 1.38 s vs 1.37 s on main (0% slower).

Generated by 🚫 Danger

WZBbiao added 2 commits February 24, 2026 18:08
…hExprImplicitReturn

IfExprSyntax and SwitchExprSyntax used as statements are wrapped in
ExpressionStmtSyntax inside CodeBlockItemSyntax. The previous code
incorrectly tried to cast the parent directly to CodeBlockItemSyntax,
which always returned nil and caused the non-triggering examples to
be incorrectly flagged as violations.

Also adds CHANGELOG entry for the void_function_in_ternary fix.
…ssues

Replace recursive call to isImplicitReturn inside isIfExprOrSwitchExprImplicitReturn
with a new non-recursive isImplicitReturnExcludingIfSwitchExpr property. This avoids
potential data races when multiple rules traverse the same syntax tree concurrently
during parallel rule execution in SwiftLint's linter.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

void_function_in_ternary incorrectly triggered with if and switch expressions

2 participants