Skip to content

Refactor ConvertToTernaryExpr#3252

Open
a7maad-ayman wants to merge 2 commits intoswiftlang:mainfrom
a7maad-ayman:convert-to-ternary-expression
Open

Refactor ConvertToTernaryExpr#3252
a7maad-ayman wants to merge 2 commits intoswiftlang:mainfrom
a7maad-ayman:convert-to-ternary-expression

Conversation

@a7maad-ayman
Copy link

@a7maad-ayman a7maad-ayman commented Jan 21, 2026

Refactor ConvertToTernaryExpr

Implements a new Swift refactoring that converts if-else statements with assignments into ternary expressions.

What it does

Converts this:

let result: Type
if condition {
  result = trueValue
} else {
  result = falseValue
}

Into this:

let result: Type = condition ? trueValue : falseValue

Supported patterns

  • Variable declaration + if-else assignment
  • Standalone if-else assignment
  • Tuple assignments: (a, b) = condition ? (1, 2) : (3, 4)
  • Simple assignments: x = condition ? value1 : value2

Contributes to #2424

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for starting this, @a7maad-ayman. I left some initial comments from my first scan through the PR. Once those are resolved, I’ll have another more detailed look at it.

/// - Optionally, the variable is declared immediately before the if statement
public struct ConvertToTernaryExpression: SyntaxRefactoringProvider {

public static func refactor(syntax: CodeBlockItemListSyntax, in context: Void) throws -> CodeBlockItemListSyntax {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you run swift-format on your changes. Instructions should be in CONTRIBUTING.md

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already run swift format -i -r . to format the code according to the project's style guidelines.

Copy link
Member

@ahoppen ahoppen Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry. I meant whether you could remove this superfluous newline. Same for all the other functions as well

let items = Array(codeBlock)
guard !items.isEmpty else { return nil }

/// Variable declaration followed by if statement.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be simpler to find the if expression to extract first and then just look up if the previous statement is a variable declaration of that name? I feel like that could unify this branch and the one below a little.

Comment on lines +235 to +246
private static func extractCondition(from ifExpr: IfExprSyntax) -> ExprSyntax? {
guard let firstCondition = ifExpr.conditions.first else {
return nil
}

guard case .expression(let condition) = firstCondition.condition else {
return nil
}

return condition
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the entire code in analyzePattern would read easier if these checks would just be inline. This one, for example could just be a single

guard case .expression(let condition) = ifExpr.conditions.first.condition else {
  return nil
}

Similar for most of the other checks

}

private static func extractCondition(from ifExpr: IfExprSyntax) -> ExprSyntax? {
guard let firstCondition = ifExpr.conditions.first else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should only apply this transformation if there’s only a single condition. You can use .only for that.

Comment on lines +339 to +340
if expr.as(TernaryExprSyntax.self) != nil { return true }
if expr.as(ClosureExprSyntax.self) != nil { return true }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these too complex for a ternary?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think nested ternaries hurt readability x = a ? b : (c ? d : e) is harder to follow than an if-else. Closures with captures are also clearer in if-else form since they benefit from proper line breaks and indentation rather than being collapsed into a single line. Both cases are better left as-is.

Happy to hear your thoughts — do you have a preference on which cases should be excluded?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if a user requests a conversion to a ternary expression, we should offer it to them. Users should be able to decide for themself whether applying the code action has a benefit for their code.

// MARK: - Alternative API for single if statement refactoring
extension ConvertToTernaryExpression {

public static func refactor(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT this doesn’t satisfy any requirement in SyntaxRefactoringProvider. Since most clients will enter the refactoring action through that protocol, this method is practically unreachable. We should be able to perform this refactoring using the main refactor entry point.

}

// MARK: - Builders
private static func withoutTrivia<T: SyntaxProtocol>(_ node: T) -> T {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have .trimmed, which does exactly this.


func testNamedTupleAssignment() throws {
let baseline: CodeBlockItemListSyntax = """
let coordinates: (x: Int, y: Int)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep the type annotation here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type annotations are now handled differently based on assignment type:

  • Tuples: Type annotation is preserved (e.g., (x: Int, y: Int))
  • Simple types: Type annotation is removed and inferred (e.g., Int)

- Add `Collection.only` to `SyntaxUtils.swift` (mirrors the pattern in
    `SwiftParserDiagnostics` and `CodeGeneration`)
- Preserve the type annotation in the output declaration when the declared
    type is a named tuple (e.g. `(x: Int, y: Int)`)
- Delete `extractCondition`, `extractElseBlock` & `validateIfExpr` and
    inline their checks directly into `analyzePattern`.
- Add inline comments to `isExpressionTooComplexForTernary` explaining
    why nested ternaries and closures are excluded
@a7maad-ayman a7maad-ayman requested a review from ahoppen February 26, 2026 21:13
Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few more comments inline.

}

// MARK: - Models
/// ConvertibleIfElse
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment doesn’t provide any value.

I think an example that shows an if expression and highlights which member represents which part of the if expression would be helpful.

let isTupleAssignment: Bool
}

/// AssignmentInfo
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar here, this comment doesn’t provide any value but documenting the members would be valuable.

/// - Optionally, the variable is declared immediately before the if statement
public struct ConvertToTernaryExpression: SyntaxRefactoringProvider {

public static func refactor(syntax: CodeBlockItemListSyntax, in context: Void) throws -> CodeBlockItemListSyntax {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the node that this refactoring action is invoked on be an IfExprSyntax. If we invoke it on CodeBlockItemListSyntax, it’s not clear what you want to convert if you have multiple if expressions within the code block.

}
}

extension Collection {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s move this to its own file `Collection+only.swift because it’s more of a general utility and not really related to syntax nodes.

Comment on lines +136 to +137
varDecl.bindings.count == 1,
let binding = varDecl.bindings.first,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be .only. Same in a couple more places.

try assertRefactorConvert(baseline, expected: expected)
}

func testBasicIfElseWithVarDeclaration() throws {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parses exactly the same way as testBasicIfElseWithLetDeclaration, so I don’t think there’s much value in this test.

try assertRefactorConvert(baseline, expected: expected)
}

func testBasicIfElseWithVarDeclaration() throws {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parses exactly the same way as testBasicIfElseWithLetDeclaration, so I don’t think there’s much value in this test.va

}

let keyword = decl.bindingSpecifier.tokenKind
return keyword == .keyword(.let) || keyword == .keyword(.var)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly are you trying to guard for here? Variable declarations will always have var or let and should we introduce a new keyword here, we’d likely want to also support it.

func testParenthesizedCondition() throws {
let baseline: CodeBlockItemListSyntax = """
let output: Int
if (x > 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this also work if x < 0 is not wrapped in parentheses?


// MARK: - Complex Expression Tests

func testFunctionCallInBranches() throws {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we just copy the expressions verbatim, I don’t think this provides any additional coverage over testBasicIfElseWithLetDeclaration. Similar for testDictionaryLiteralInBranches.

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.

2 participants