Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/SwiftIfConfig/IfConfigEvaluation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import SwiftDiagnostics
import SwiftOperators
import SwiftSyntax
@_spi(RawSyntax) import SwiftSyntax

/// Evaluate the condition of an `#if`.
/// - Parameters:
Expand Down
10 changes: 1 addition & 9 deletions Sources/SwiftIfConfig/SyntaxLiteralUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//
//===----------------------------------------------------------------------===//

import SwiftSyntax
@_spi(RawSyntax) import SwiftSyntax

extension BooleanLiteralExprSyntax {
var literalValue: Bool {
Expand All @@ -25,14 +25,6 @@ extension TupleExprSyntax {
}
}

extension LabeledExprListSyntax {
/// If this list is a single, unlabeled expression, return it.
var singleUnlabeledExpression: ExprSyntax? {
guard count == 1, let element = first, element.label == nil else { return nil }
return element.expression
}
}

extension ExprSyntax {
/// Whether this is a simple identifier expression and, if so, what the identifier string is.
var simpleIdentifierExpr: Identifier? {
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftRefactor/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ add_swift_syntax_library(SwiftRefactor
MigrateToNewIfLetSyntax.swift
OpaqueParameterToGeneric.swift
RefactoringProvider.swift
RemoveRedundantParentheses.swift
RemoveSeparatorsFromIntegerLiteral.swift
SyntaxUtils.swift

Expand Down
254 changes: 254 additions & 0 deletions Sources/SwiftRefactor/RemoveRedundantParentheses.swift
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

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,
.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
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
// 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)
default:
return false
}
}
}
8 changes: 8 additions & 0 deletions Sources/SwiftSyntax/Convenience.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ extension IntegerLiteralExprSyntax {
}
}

extension LabeledExprListSyntax {
/// If this list is a single, unlabeled expression, return it.
@_spi(RawSyntax) public var singleUnlabeledExpression: ExprSyntax? {
guard count == 1, let element = first, element.label == nil else { return nil }
return element.expression
}
}

extension MemberAccessExprSyntax {
/// Creates a new ``MemberAccessExprSyntax`` where the accessed member is represented by
/// an identifier without specifying argument labels.
Expand Down
Loading