-
Notifications
You must be signed in to change notification settings - Fork 490
Add RemoveRedundantParentheses refactoring #3232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
PhantomInTheWire
wants to merge
17
commits into
swiftlang:main
Choose a base branch
from
PhantomInTheWire:feat/redun-parantheses
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+518
−10
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
728eed5
Add RemoveRedundantParentheses refactoring
PhantomInTheWire d7727b3
Add tests for RemoveRedundantParentheses
PhantomInTheWire 4b3fb3b
Refactor expr in RemoveRedundantParentheses
PhantomInTheWire 9df932e
Cleanup RemoveRedundantParantheses.swift
PhantomInTheWire 9a0de17
Fix bazel build
PhantomInTheWire 3df01c0
Fix bugs and add regression tests
PhantomInTheWire 6daeca6
Refactor testing methodology
PhantomInTheWire c3ee2c4
Fix closure preservation in conditions
PhantomInTheWire ce8d524
Use ancestorOrSelf for RepeatStmtSyntax and add regression tests
PhantomInTheWire eb5de7f
make singleUnlabeledExpression package level
PhantomInTheWire 4d0189d
Cleanup redundant Parentheses
PhantomInTheWire 839c215
Make attributed type non-simple
PhantomInTheWire aac977a
Refine RemoveRedundantParentheses refactoring and tests
PhantomInTheWire ec29c14
Preserve parentheses around try/await when followed by postfix expres…
PhantomInTheWire 811bd16
Fix cmake and bazel build
PhantomInTheWire 0851433
Support removing redundant parentheses in control flow and return/thr…
PhantomInTheWire d585e82
Refactor RemoveRedundantParentheses for improved correctness
PhantomInTheWire File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift.org open source project | ||
| // | ||
| // Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors | ||
| // Licensed under Apache License v2.0 with Runtime Library Exception | ||
| // | ||
| // See https://swift.org/LICENSE.txt for license information | ||
| // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| #if compiler(>=6) | ||
| @_spi(RawSyntax) public import SwiftSyntax | ||
| #else | ||
| @_spi(RawSyntax) import SwiftSyntax | ||
| #endif | ||
|
|
||
| /// Removes redundant parentheses from expressions. | ||
| /// | ||
| /// Examples: | ||
| /// - `((x))` -> `x` | ||
| /// - `(x)` -> `x` (where x is a simple expression) | ||
| /// | ||
| public struct RemoveRedundantParentheses: SyntaxRefactoringProvider { | ||
| public static func refactor( | ||
| syntax: TupleExprSyntax, | ||
| in context: Void | ||
| ) -> ExprSyntax { | ||
| // If the syntax tree has errors, we should not attempt to refactor it. | ||
| guard !syntax.hasError else { | ||
| return ExprSyntax(syntax) | ||
| } | ||
|
|
||
| // Check if the tuple expression has exactly one element and no label. | ||
| guard let innerExpr = syntax.elements.singleUnlabeledExpression else { | ||
| return ExprSyntax(syntax) | ||
| } | ||
|
|
||
| // Case 1: Nested parentheses ((expression)) -> (expression) | ||
| // Recursively strip inner parentheses to handle cases like (((x))) -> x | ||
| if let innerTuple = innerExpr.as(TupleExprSyntax.self) { | ||
| return preserveTrivia(from: syntax, to: refactor(syntax: innerTuple, in: ())) | ||
| } | ||
|
|
||
| // Case 2: Parentheses around simple expressions | ||
| if canRemoveParentheses(tuple: syntax, around: innerExpr, in: syntax.parent) { | ||
| return preserveTrivia(from: syntax, to: innerExpr) | ||
| } | ||
|
|
||
| // Default: Return unchanged | ||
| return ExprSyntax(syntax) | ||
| } | ||
|
|
||
| private static func preserveTrivia(from outer: TupleExprSyntax, to inner: ExprSyntax) -> ExprSyntax { | ||
| let leadingTrivia = outer.leftParen.leadingTrivia | ||
| .merging(outer.leftParen.trailingTrivia) | ||
| .merging(inner.leadingTrivia) | ||
| let trailingTrivia = inner.trailingTrivia | ||
| .merging(outer.rightParen.leadingTrivia) | ||
| .merging(outer.rightParen.trailingTrivia) | ||
| return | ||
| inner | ||
| .with(\.leadingTrivia, leadingTrivia) | ||
| .with(\.trailingTrivia, trailingTrivia) | ||
| } | ||
|
|
||
| private static func canRemoveParentheses(tuple: TupleExprSyntax, around expr: ExprSyntax, in parent: Syntax?) -> Bool | ||
| { | ||
| // Safety Check: Ambiguous Closures | ||
| // Closures and trailing closures inside conditions need parentheses to avoid ambiguity. | ||
| // e.g. `if ({ true }) == ({ true }) {}` or `if (call { true }) == false {}` | ||
| // This applies to if/while/guard (ConditionElementSyntax) and repeat-while (RepeatStmtSyntax). | ||
| // It also applies to InitializerClauseSyntax if it is inside a condition (e.g. `if let x = ({...})`). | ||
| let isInCondition = | ||
| parent?.ancestorOrSelf(mapping: { | ||
| if $0.is(ConditionElementSyntax.self) || $0.is(RepeatStmtSyntax.self) { | ||
| return $0 | ||
| } | ||
| return nil | ||
| }) != nil | ||
|
|
||
| if isInCondition && (expr.is(ClosureExprSyntax.self) || hasTrailingClosure(expr)) { | ||
| return false | ||
| } | ||
|
|
||
| // Safety Check: Immediately-invoked closures | ||
| if let functionCall = parent?.as(FunctionCallExprSyntax.self), | ||
| functionCall.calledExpression.as(TupleExprSyntax.self) != nil, | ||
| expr.is(ClosureExprSyntax.self) | ||
| { | ||
| return false | ||
| } | ||
|
|
||
| // Allowlist: Check keyPathInParent to explicitly know that this expression | ||
| // occurs in a place where the parentheses are redundant. | ||
| if let keyPath = tuple.keyPathInParent { | ||
| switch keyPath { | ||
| case \InitializerClauseSyntax.value, | ||
| \ConditionElementSyntax.condition, | ||
| \ReturnStmtSyntax.expression, | ||
| \ThrowStmtSyntax.expression, | ||
| \SwitchExprSyntax.subject, | ||
| \RepeatStmtSyntax.condition: | ||
| return true | ||
| default: | ||
| break | ||
| } | ||
| } | ||
|
|
||
| // Fallback: Allow if the expression itself is "simple" | ||
| guard isSimpleExpression(expr) else { | ||
| return false | ||
| } | ||
|
|
||
| // Safety Check: Postfix and Binary Precedence | ||
| // Expressions like `try`, `await`, `consume`, and `copy` bind looser than postfix and infix expressions. | ||
| // e.g., `(try? f()).description` is different from `try? f().description`. | ||
| // The former accesses `.description` on the Optional result, the latter on the unwrapped value. | ||
| // Similarly, `(try? f()) + 1` is different from `try? f() + 1` (Int? + Int vs Int + Int). | ||
| if parentHasTighterBindingThanEffect(parent) { | ||
| switch expr.as(ExprSyntaxEnum.self) { | ||
| case .tryExpr, .awaitExpr, .unsafeExpr, .consumeExpr, .copyExpr: | ||
| return false | ||
| default: | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| /// Returns true if parent is an expression with higher precedence than effects (try/await/etc). | ||
| /// This includes postfix expressions (member access, subscript, call, force unwrap, optional chaining), | ||
| /// infix operators, type casting (as/is), and ternary expressions. | ||
| private static func parentHasTighterBindingThanEffect(_ parent: Syntax?) -> Bool { | ||
| switch parent?.as(SyntaxEnum.self) { | ||
| // Postfix expressions: member access, subscript, function call, force unwrap, and postfix operators | ||
| // These all bind tighter than effect expressions (try/await/etc). | ||
| // For member access, since we're a TupleExprSyntax, we are always the base. | ||
| case .memberAccessExpr, .subscriptCallExpr, .functionCallExpr, .forceUnwrapExpr, .postfixOperatorExpr: | ||
| return true | ||
PhantomInTheWire marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| case .optionalChainingExpr(let optionalChaining): | ||
| // Optional chaining (?.) binds tighter than effects | ||
| return optionalChaining.expression != nil | ||
|
|
||
| // Infix operators and sequence expressions bind tighter than effects. | ||
| // For sequence expressions (before SwiftOperators folding), the parent chain | ||
| // is: TupleExpr -> ExprList -> SequenceExpr, e.g., `(try? f()) + 1`. | ||
| case .infixOperatorExpr, .sequenceExpr, .exprList: | ||
| return true | ||
|
|
||
| // Type casting operators (as, is) bind tighter than effects. | ||
| // Ternary operator also binds tighter than effects. | ||
| case .asExpr, .isExpr, .ternaryExpr: | ||
| return true | ||
|
|
||
| default: | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| private static func hasTrailingClosure(_ expr: ExprSyntax) -> Bool { | ||
| switch expr.as(ExprSyntaxEnum.self) { | ||
| case .functionCallExpr(let functionCall): | ||
| return functionCall.trailingClosure != nil || !functionCall.additionalTrailingClosures.isEmpty | ||
| case .macroExpansionExpr(let macroExpansion): | ||
| return macroExpansion.trailingClosure != nil || !macroExpansion.additionalTrailingClosures.isEmpty | ||
| case .subscriptCallExpr(let subscriptCall): | ||
| return subscriptCall.trailingClosure != nil || !subscriptCall.additionalTrailingClosures.isEmpty | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| /// Checks if a type is simple enough to not require parentheses. | ||
| /// Complex types like `any Equatable`, `some P`, or `A & B` need parentheses, e.g. `(any Equatable).self`. | ||
| private static func isSimpleType(_ type: TypeSyntax) -> Bool { | ||
| switch type.as(TypeSyntaxEnum.self) { | ||
| case .arrayType, | ||
| .classRestrictionType, | ||
| .dictionaryType, | ||
| .identifierType, | ||
| .implicitlyUnwrappedOptionalType, | ||
PhantomInTheWire marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .inlineArrayType, | ||
| .memberType, | ||
| .metatypeType, | ||
| .missingType, | ||
| .optionalType, | ||
| .tupleType: | ||
| return true | ||
| case .attributedType, // @escaping, @Sendable, etc. | ||
| .compositionType, // A & B | ||
| .functionType, // (A) -> B | ||
| .namedOpaqueReturnType, | ||
| .packElementType, | ||
| .packExpansionType, | ||
| .someOrAnyType, // some P, any P | ||
| .suppressedType: // ~Copyable | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| private static func isSimpleExpression(_ expr: ExprSyntax) -> Bool { | ||
| // Allow-list of simple expressions that typically don't depend on precedence | ||
| // in a way that requires parentheses when used in most contexts, | ||
| // or are self-contained. | ||
| switch expr.as(ExprSyntaxEnum.self) { | ||
| case .arrayExpr, | ||
| .booleanLiteralExpr, | ||
| .closureExpr, | ||
| .declReferenceExpr, | ||
| .dictionaryExpr, | ||
| .floatLiteralExpr, | ||
| .forceUnwrapExpr, | ||
| .integerLiteralExpr, | ||
| .macroExpansionExpr, | ||
| .memberAccessExpr, | ||
| .nilLiteralExpr, | ||
| .optionalChainingExpr, | ||
| .regexLiteralExpr, | ||
| .simpleStringLiteralExpr, | ||
| .stringLiteralExpr, | ||
| .subscriptCallExpr, | ||
| .superExpr: | ||
| return true | ||
PhantomInTheWire marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| case .typeExpr(let typeExpr): | ||
| // Types like `any Equatable` need parentheses, e.g. `(any Equatable).self` | ||
| return isSimpleType(typeExpr.type) | ||
| case .awaitExpr(let awaitExpr): | ||
| // await is only simple if its expression is also simple | ||
| return isSimpleExpression(awaitExpr.expression) | ||
| case .unsafeExpr(let unsafeExpr): | ||
| // Similar to await, unsafe is only simple if its expression is simple | ||
| return isSimpleExpression(unsafeExpr.expression) | ||
| case .tryExpr(let tryExpr): | ||
| // Only try! and try? are simple; regular try is NOT simple | ||
PhantomInTheWire marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // because it affects precedence (e.g., try (try! foo()).bar() vs try try! foo().bar()) | ||
| guard tryExpr.questionOrExclamationMark != nil else { | ||
| return false | ||
| } | ||
| return isSimpleExpression(tryExpr.expression) | ||
| case .functionCallExpr(let functionCall): | ||
| // A function call is simple enough to remove parentheses around it. | ||
| // Immediately-invoked closures need parentheses for disambiguation. | ||
| // Without parentheses, `let x = { 1 }()` parses as `let x = { 1 }` followed by `()` as a separate | ||
| // statement, rather than calling the closure. With parentheses: `let x = ({ 1 })()` works correctly. | ||
| return !functionCall.calledExpression.is(ClosureExprSyntax.self) | ||
PhantomInTheWire marked this conversation as resolved.
Show resolved
Hide resolved
PhantomInTheWire marked this conversation as resolved.
Show resolved
Hide resolved
PhantomInTheWire marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| default: | ||
| return false | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.