diff --git a/CHANGELOG.md b/CHANGELOG.md index 3348c4e3b1..ed31ff5963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,11 @@ ### Bug Fixes +* Fix false positive in `void_function_in_ternary` rule when a ternary expression + appears inside an `if` or `switch` expression used as an implicit return value. + [WZBbiao](https://github.com/WZBbiao) + [#5611](https://github.com/realm/SwiftLint/issues/5611) + * Ensure that disable commands work for `redundant_nil_coalescing` rule. [SimplyDanny](https://github.com/SimplyDanny) [#6465](https://github.com/realm/SwiftLint/issues/6465) diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift index 27f7c3ad57..5f794a0c11 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift @@ -67,6 +67,23 @@ struct VoidFunctionInTernaryConditionRule: Rule { a &<<= b ? c() : d() a &-= b ? c() : d() """), + Example(""" + func makeValue() -> MyStruct { + if condition { + flag ? MyStruct(value: 0) : MyStruct(value: 1) + } else { + MyStruct(value: 2) + } + } + """), + Example(""" + func computeSize(for section: Int) -> CGSize { + switch section { + case 0: isEditing ? CGSize(width: 150, height: 20) : CGSize(width: 100, height: 20) + default: .zero + } + } + """), ], triggeringExamples: [ Example("success ↓? askQuestion() : exit()"), @@ -182,6 +199,15 @@ private extension ExprListSyntax { private extension CodeBlockItemSyntax { var isImplicitReturn: Bool { + isClosureImplicitReturn || isFunctionImplicitReturn || + isVariableImplicitReturn || isSubscriptImplicitReturn || + isAccessorImplicitReturn || isIfExprOrSwitchExprImplicitReturn + } + + /// Like `isImplicitReturn` but without checking `isIfExprOrSwitchExprImplicitReturn`. + /// Used inside `isIfExprOrSwitchExprImplicitReturn` to avoid recursive calls that can + /// trigger thread-safety issues when multiple rules traverse the syntax tree concurrently. + var isImplicitReturnExcludingIfSwitchExpr: Bool { isClosureImplicitReturn || isFunctionImplicitReturn || isVariableImplicitReturn || isSubscriptImplicitReturn || isAccessorImplicitReturn @@ -231,6 +257,40 @@ private extension CodeBlockItemSyntax { return parent.children(viewMode: .sourceAccurate).count == 1 } + + /// Returns `true` if this code block item is the sole expression in a branch of an `if` or + /// `switch` expression that is itself used as an implicit return value. This prevents false + /// positives when a ternary that returns a value appears inside an `if`/`switch` expression + /// branch (Swift 5.9+) where the result of the branch is used as the enclosing expression's + /// value. + var isIfExprOrSwitchExprImplicitReturn: Bool { + guard let parent = parent?.as(CodeBlockItemListSyntax.self), + parent.children(viewMode: .sourceAccurate).count == 1 else { + return false + } + + // Check if inside an if expression branch (body or else body). + // Chain: CodeBlockItemListSyntax -> CodeBlockSyntax -> IfExprSyntax + // Note: IfExprSyntax used as a statement is wrapped in ExpressionStmtSyntax inside the CodeBlockItemSyntax. + if let ifExpr = parent.parent?.parent?.as(IfExprSyntax.self), + let ifCodeBlockItem = ifExpr.parent?.as(ExpressionStmtSyntax.self)?.parent?.as(CodeBlockItemSyntax.self) { + // Use isImplicitReturnExcludingIfSwitchExpr instead of isImplicitReturn to avoid + // recursive calls that traverse shared syntax nodes concurrently during parallel rule execution. + return ifCodeBlockItem.isImplicitReturnExcludingIfSwitchExpr + } + + // Check if inside a switch expression case body. + // Chain: CodeBlockItemListSyntax -> SwitchCaseSyntax -> SwitchCaseListSyntax -> SwitchExprSyntax + // Note: SwitchExprSyntax used as a statement is wrapped in ExpressionStmtSyntax inside the CodeBlockItemSyntax. + if let switchExpr = parent.parent?.parent?.parent?.as(SwitchExprSyntax.self), + let switchCodeBlockItem = switchExpr.parent?.as(ExpressionStmtSyntax.self)?.parent?.as(CodeBlockItemSyntax.self) { + // Use isImplicitReturnExcludingIfSwitchExpr instead of isImplicitReturn to avoid + // recursive calls that traverse shared syntax nodes concurrently during parallel rule execution. + return switchCodeBlockItem.isImplicitReturnExcludingIfSwitchExpr + } + + return false + } } private extension FunctionSignatureSyntax {