Conversation
ahoppen
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Could you run swift-format on your changes. Instructions should be in CONTRIBUTING.md
There was a problem hiding this comment.
I've already run swift format -i -r . to format the code according to the project's style guidelines.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
|
|
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
We should only apply this transformation if there’s only a single condition. You can use .only for that.
| if expr.as(TernaryExprSyntax.self) != nil { return true } | ||
| if expr.as(ClosureExprSyntax.self) != nil { return true } |
There was a problem hiding this comment.
Why are these too complex for a ternary?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
We have .trimmed, which does exactly this.
|
|
||
| func testNamedTupleAssignment() throws { | ||
| let baseline: CodeBlockItemListSyntax = """ | ||
| let coordinates: (x: Int, y: Int) |
There was a problem hiding this comment.
Should we keep the type annotation here?
There was a problem hiding this comment.
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
ahoppen
left a comment
There was a problem hiding this comment.
I have a few more comments inline.
| } | ||
|
|
||
| // MARK: - Models | ||
| /// ConvertibleIfElse |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
| varDecl.bindings.count == 1, | ||
| let binding = varDecl.bindings.first, |
There was a problem hiding this comment.
Can be .only. Same in a couple more places.
| try assertRefactorConvert(baseline, expected: expected) | ||
| } | ||
|
|
||
| func testBasicIfElseWithVarDeclaration() throws { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Does this also work if x < 0 is not wrapped in parentheses?
|
|
||
| // MARK: - Complex Expression Tests | ||
|
|
||
| func testFunctionCallInBranches() throws { |
There was a problem hiding this comment.
Since we just copy the expressions verbatim, I don’t think this provides any additional coverage over testBasicIfElseWithLetDeclaration. Similar for testDictionaryLiteralInBranches.
Refactor ConvertToTernaryExpr
Implements a new Swift refactoring that converts if-else statements with assignments into ternary expressions.
What it does
Converts this:
Into this:
Supported patterns
(a, b) = condition ? (1, 2) : (3, 4)x = condition ? value1 : value2Contributes to #2424