Skip to content

Commit 40962a5

Browse files
authored
Merge pull request #973 from allevato/unsafe
Correctly format `unsafe` expressions.
2 parents 97598b5 + a743f5f commit 40962a5

File tree

6 files changed

+477
-352
lines changed

6 files changed

+477
-352
lines changed

Sources/SwiftFormat/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ add_library(SwiftFormat
4242
Core/SyntaxFormatRule.swift
4343
Core/SyntaxLintRule.swift
4444
Core/SyntaxProtocol+Convenience.swift
45+
Core/SyntaxTraits.swift
4546
Core/Trivia+Convenience.swift
4647
Core/WithAttributesSyntax+Convenience.swift
4748
Core/WithSemicolonSyntax.swift
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
/// Common protocol implemented by expression syntax types that support calling another expression.
16+
protocol CallingExprSyntaxProtocol: ExprSyntaxProtocol {
17+
var calledExpression: ExprSyntax { get }
18+
}
19+
20+
extension FunctionCallExprSyntax: CallingExprSyntaxProtocol {}
21+
extension SubscriptCallExprSyntax: CallingExprSyntaxProtocol {}
22+
23+
extension Syntax {
24+
func asProtocol(_: CallingExprSyntaxProtocol.Protocol) -> CallingExprSyntaxProtocol? {
25+
return self.asProtocol(SyntaxProtocol.self) as? CallingExprSyntaxProtocol
26+
}
27+
func isProtocol(_: CallingExprSyntaxProtocol.Protocol) -> Bool {
28+
return self.asProtocol(CallingExprSyntaxProtocol.self) != nil
29+
}
30+
}
31+
32+
extension ExprSyntax {
33+
func asProtocol(_: CallingExprSyntaxProtocol.Protocol) -> CallingExprSyntaxProtocol? {
34+
return Syntax(self).asProtocol(SyntaxProtocol.self) as? CallingExprSyntaxProtocol
35+
}
36+
func isProtocol(_: CallingExprSyntaxProtocol.Protocol) -> Bool {
37+
return self.asProtocol(CallingExprSyntaxProtocol.self) != nil
38+
}
39+
}
40+
41+
/// Common protocol implemented by expression syntax types that are expressed as a modified
42+
/// subexpression of the form `<keyword> <subexpr>`.
43+
protocol KeywordModifiedExprSyntaxProtocol: ExprSyntaxProtocol {
44+
var expression: ExprSyntax { get }
45+
}
46+
47+
extension AwaitExprSyntax: KeywordModifiedExprSyntaxProtocol {}
48+
extension TryExprSyntax: KeywordModifiedExprSyntaxProtocol {}
49+
extension UnsafeExprSyntax: KeywordModifiedExprSyntaxProtocol {}
50+
51+
extension Syntax {
52+
func asProtocol(_: KeywordModifiedExprSyntaxProtocol.Protocol) -> KeywordModifiedExprSyntaxProtocol? {
53+
return self.asProtocol(SyntaxProtocol.self) as? KeywordModifiedExprSyntaxProtocol
54+
}
55+
func isProtocol(_: KeywordModifiedExprSyntaxProtocol.Protocol) -> Bool {
56+
return self.asProtocol(KeywordModifiedExprSyntaxProtocol.self) != nil
57+
}
58+
}
59+
60+
extension ExprSyntax {
61+
func asProtocol(_: KeywordModifiedExprSyntaxProtocol.Protocol) -> KeywordModifiedExprSyntaxProtocol? {
62+
return Syntax(self).asProtocol(SyntaxProtocol.self) as? KeywordModifiedExprSyntaxProtocol
63+
}
64+
func isProtocol(_: KeywordModifiedExprSyntaxProtocol.Protocol) -> Bool {
65+
return self.asProtocol(KeywordModifiedExprSyntaxProtocol.self) != nil
66+
}
67+
}

Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift

Lines changed: 47 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,14 +1068,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
10681068

10691069
if let calledMemberAccessExpr = node.calledExpression.as(MemberAccessExprSyntax.self) {
10701070
if let base = calledMemberAccessExpr.base, base.is(DeclReferenceExprSyntax.self) {
1071-
// When this function call is wrapped by a try-expr or await-expr, the group applied when
1072-
// visiting that wrapping expression is sufficient. Adding another group here in that case
1073-
// can result in unnecessarily breaking after the try/await keyword.
1074-
if !(base.firstToken(viewMode: .sourceAccurate)?.previousToken(viewMode: .all)?.parent?.is(TryExprSyntax.self)
1075-
?? false
1076-
|| base.firstToken(viewMode: .sourceAccurate)?.previousToken(viewMode: .all)?.parent?.is(AwaitExprSyntax.self)
1077-
?? false)
1078-
{
1071+
// When this function call is wrapped by a keyword-modified expression, the group applied
1072+
// when visiting that wrapping expression is sufficient. Adding another group here in that
1073+
// case can result in unnecessarily breaking after the modifier keyword.
1074+
if !(base.firstToken(viewMode: .sourceAccurate)?.previousToken(viewMode: .all)?.parent?.isProtocol(
1075+
KeywordModifiedExprSyntaxProtocol.self
1076+
) ?? false) {
10791077
before(base.firstToken(viewMode: .sourceAccurate), tokens: .open)
10801078
after(calledMemberAccessExpr.declName.baseName.lastToken(viewMode: .sourceAccurate), tokens: .close)
10811079
}
@@ -1780,8 +1778,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17801778
tokens: .break(.continue, newlines: .elective(ignoresDiscretionary: true))
17811779
)
17821780

1783-
// Check for an anchor token inside of the expression to group with the try keyword.
1784-
if let anchorToken = findTryAwaitExprConnectingToken(inExpr: node.expression) {
1781+
// Check for an anchor token inside of the expression to end a group starting with the `try`
1782+
// keyword.
1783+
if !(node.parent?.isProtocol(KeywordModifiedExprSyntaxProtocol.self) ?? false),
1784+
let anchorToken = connectingTokenForKeywordModifiedExpr(inSubExpr: node.expression)
1785+
{
17851786
before(node.tryKeyword, tokens: .open)
17861787
after(anchorToken, tokens: .close)
17871788
}
@@ -1795,9 +1796,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
17951796
tokens: .break(.continue, newlines: .elective(ignoresDiscretionary: true))
17961797
)
17971798

1798-
// Check for an anchor token inside of the expression to group with the await keyword.
1799-
if !(node.parent?.is(TryExprSyntax.self) ?? false),
1800-
let anchorToken = findTryAwaitExprConnectingToken(inExpr: node.expression)
1799+
// Check for an anchor token inside of the expression to end a group starting with the `await`
1800+
// keyword.
1801+
if !(node.parent?.isProtocol(KeywordModifiedExprSyntaxProtocol.self) ?? false),
1802+
let anchorToken = connectingTokenForKeywordModifiedExpr(inSubExpr: node.expression)
18011803
{
18021804
before(node.awaitKeyword, tokens: .open)
18031805
after(anchorToken, tokens: .close)
@@ -1806,18 +1808,37 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
18061808
return .visitChildren
18071809
}
18081810

1809-
/// Searches the AST from `expr` to find a token that should be grouped with an enclosing
1810-
/// try-expr or await-expr. Returns that token, or nil when no such token is found.
1811+
override func visit(_ node: UnsafeExprSyntax) -> SyntaxVisitorContinueKind {
1812+
// Unlike `try` and `await`, `unsafe` is a contextual keyword that may not be separated from
1813+
// the following token by a line break. Keep them glued together with `.space`.
1814+
before(node.expression.firstToken(viewMode: .sourceAccurate), tokens: .space)
1815+
1816+
// Check for an anchor token inside of the expression to end a group starting with the `unsafe`
1817+
// keyword.
1818+
if !(node.parent?.isProtocol(KeywordModifiedExprSyntaxProtocol.self) ?? false),
1819+
let anchorToken = connectingTokenForKeywordModifiedExpr(inSubExpr: node.expression)
1820+
{
1821+
before(node.unsafeKeyword, tokens: .open)
1822+
after(anchorToken, tokens: .close)
1823+
}
1824+
1825+
return .visitChildren
1826+
}
1827+
1828+
/// Searches within a subexpression of a keyword-modified expression to find the last token in a
1829+
/// range that should be grouped with the leading keyword modifier.
18111830
///
1812-
/// - Parameter expr: An expression that is wrapped by a try-expr or await-expr.
1813-
/// - Returns: A token that should be grouped with the try-expr or await-expr, or nil.
1814-
func findTryAwaitExprConnectingToken(inExpr expr: ExprSyntax) -> TokenSyntax? {
1815-
if let awaitExpr = expr.as(AwaitExprSyntax.self) {
1816-
// If we were called from the `try` of a `try await <expr>`, drill into the child expression.
1817-
return findTryAwaitExprConnectingToken(inExpr: awaitExpr.expression)
1831+
/// - Parameter expr: An expression that is wrapped by a keyword-modified expression.
1832+
/// - Returns: The token that should end the group that is started by the modifier keyword, or
1833+
/// nil if there should be no group.
1834+
func connectingTokenForKeywordModifiedExpr(inSubExpr expr: ExprSyntax) -> TokenSyntax? {
1835+
if let modifiedExpr = expr.asProtocol(KeywordModifiedExprSyntaxProtocol.self) {
1836+
// If we were called from a keyword-modified expression like `try`, `await`, or `unsafe`,
1837+
// recursively drill into the child expression.
1838+
return connectingTokenForKeywordModifiedExpr(inSubExpr: modifiedExpr.expression)
18181839
}
18191840
if let callingExpr = expr.asProtocol(CallingExprSyntaxProtocol.self) {
1820-
return findTryAwaitExprConnectingToken(inExpr: callingExpr.calledExpression)
1841+
return connectingTokenForKeywordModifiedExpr(inSubExpr: callingExpr.calledExpression)
18211842
}
18221843
if let memberAccessExpr = expr.as(MemberAccessExprSyntax.self), let base = memberAccessExpr.base {
18231844
// When there's a simple base (i.e. identifier), group the entire `try/await <base>.<name>`
@@ -1826,7 +1847,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
18261847
if base.is(DeclReferenceExprSyntax.self) {
18271848
return memberAccessExpr.declName.baseName.lastToken(viewMode: .sourceAccurate)
18281849
}
1829-
return findTryAwaitExprConnectingToken(inExpr: base)
1850+
return connectingTokenForKeywordModifiedExpr(inSubExpr: base)
18301851
}
18311852
if expr.is(DeclReferenceExprSyntax.self) {
18321853
return expr.lastToken(viewMode: .sourceAccurate)
@@ -3820,13 +3841,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
38203841
/// that are known to wrap an expression, e.g. try expressions, are handled by checking the
38213842
/// expression that they contain.
38223843
private func isCompoundExpression(_ expr: ExprSyntax) -> Bool {
3844+
if let modifiedExpr = expr.asProtocol(KeywordModifiedExprSyntaxProtocol.self) {
3845+
return isCompoundExpression(modifiedExpr.expression)
3846+
}
38233847
switch Syntax(expr).as(SyntaxEnum.self) {
3824-
case .awaitExpr(let awaitExpr):
3825-
return isCompoundExpression(awaitExpr.expression)
38263848
case .infixOperatorExpr, .ternaryExpr, .isExpr, .asExpr:
38273849
return true
3828-
case .tryExpr(let tryExpr):
3829-
return isCompoundExpression(tryExpr.expression)
38303850
case .tupleExpr(let tupleExpr) where tupleExpr.elements.count == 1:
38313851
return isCompoundExpression(tupleExpr.elements.first!.expression)
38323852
default:
@@ -4500,29 +4520,3 @@ extension NewlineBehavior {
45004520
}
45014521
}
45024522
}
4503-
4504-
/// Common protocol implemented by expression syntax types that support calling another expression.
4505-
protocol CallingExprSyntaxProtocol: ExprSyntaxProtocol {
4506-
var calledExpression: ExprSyntax { get }
4507-
}
4508-
4509-
extension FunctionCallExprSyntax: CallingExprSyntaxProtocol {}
4510-
extension SubscriptCallExprSyntax: CallingExprSyntaxProtocol {}
4511-
4512-
extension Syntax {
4513-
func asProtocol(_: CallingExprSyntaxProtocol.Protocol) -> CallingExprSyntaxProtocol? {
4514-
return self.asProtocol(SyntaxProtocol.self) as? CallingExprSyntaxProtocol
4515-
}
4516-
func isProtocol(_: CallingExprSyntaxProtocol.Protocol) -> Bool {
4517-
return self.asProtocol(CallingExprSyntaxProtocol.self) != nil
4518-
}
4519-
}
4520-
4521-
extension ExprSyntax {
4522-
func asProtocol(_: CallingExprSyntaxProtocol.Protocol) -> CallingExprSyntaxProtocol? {
4523-
return Syntax(self).asProtocol(SyntaxProtocol.self) as? CallingExprSyntaxProtocol
4524-
}
4525-
func isProtocol(_: CallingExprSyntaxProtocol.Protocol) -> Bool {
4526-
return self.asProtocol(CallingExprSyntaxProtocol.self) != nil
4527-
}
4528-
}

0 commit comments

Comments
 (0)