Skip to content

Commit 11d16d0

Browse files
authored
Merge pull request swiftlang#442 from allevato/no-assignment-expr-rule
Add a rule that prevents assignment from appearing in expression contexts.
2 parents dc7edc3 + 08be967 commit 11d16d0

File tree

6 files changed

+299
-0
lines changed

6 files changed

+299
-0
lines changed

Sources/SwiftFormat/Pipelines+Generated.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class LintPipeline: SyntaxVisitor {
6363

6464
override func visit(_ node: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind {
6565
visitIfEnabled(DoNotUseSemicolons.visit, for: node)
66+
visitIfEnabled(NoAssignmentInExpressions.visit, for: node)
6667
visitIfEnabled(OneVariableDeclarationPerLine.visit, for: node)
6768
visitIfEnabled(UseEarlyExits.visit, for: node)
6869
return .visitChildren
@@ -165,6 +166,11 @@ class LintPipeline: SyntaxVisitor {
165166
return .visitChildren
166167
}
167168

169+
override func visit(_ node: InfixOperatorExprSyntax) -> SyntaxVisitorContinueKind {
170+
visitIfEnabled(NoAssignmentInExpressions.visit, for: node)
171+
return .visitChildren
172+
}
173+
168174
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
169175
visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node)
170176
visitIfEnabled(BeginDocumentationCommentWithOneLineSummary.visit, for: node)
@@ -312,6 +318,7 @@ extension FormatPipeline {
312318
node = FullyIndirectEnum(context: context).visit(node)
313319
node = GroupNumericLiterals(context: context).visit(node)
314320
node = NoAccessLevelOnExtensionDeclaration(context: context).visit(node)
321+
node = NoAssignmentInExpressions(context: context).visit(node)
315322
node = NoCasesWithOnlyFallthrough(context: context).visit(node)
316323
node = NoEmptyTrailingClosureParentheses(context: context).visit(node)
317324
node = NoLabelsInCasePatterns(context: context).visit(node)

Sources/SwiftFormatConfiguration/RuleRegistry+Generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ enum RuleRegistry {
2828
"NeverUseForceTry": false,
2929
"NeverUseImplicitlyUnwrappedOptionals": false,
3030
"NoAccessLevelOnExtensionDeclaration": true,
31+
"NoAssignmentInExpressions": true,
3132
"NoBlockComments": true,
3233
"NoCasesWithOnlyFallthrough": true,
3334
"NoEmptyTrailingClosureParentheses": true,

Sources/SwiftFormatCore/Trivia+Convenience.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ extension Trivia {
4444
})
4545
}
4646

47+
/// Returns this set of trivia, without any leading spaces.
48+
public func withoutLeadingSpaces() -> Trivia {
49+
return Trivia(
50+
pieces: Array(drop {
51+
if case .spaces = $0 { return false }
52+
if case .tabs = $0 { return false }
53+
return true
54+
}))
55+
}
56+
4757
/// Returns this set of trivia, without any newlines.
4858
public func withoutNewlines() -> Trivia {
4959
return Trivia(
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 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 SwiftFormatCore
14+
import SwiftSyntax
15+
16+
/// Assignment expressions must be their own statements.
17+
///
18+
/// Assignment should not be used in an expression context that expects a `Void` value. For example,
19+
/// assigning a variable within a `return` statement existing a `Void` function is prohibited.
20+
///
21+
/// Lint: If an assignment expression is found in a position other than a standalone statement, a
22+
/// lint finding is emitted.
23+
///
24+
/// Format: A `return` statement containing an assignment expression is expanded into two separate
25+
/// statements.
26+
public final class NoAssignmentInExpressions: SyntaxFormatRule {
27+
public override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax {
28+
// Diagnose any assignment that isn't directly a child of a `CodeBlockItem` (which would be the
29+
// case if it was its own statement).
30+
if isAssignmentExpression(node) && node.parent?.is(CodeBlockItemSyntax.self) == false {
31+
diagnose(.moveAssignmentToOwnStatement, on: node)
32+
}
33+
return ExprSyntax(node)
34+
}
35+
36+
public override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
37+
var newItems = [CodeBlockItemSyntax]()
38+
newItems.reserveCapacity(node.count)
39+
40+
for item in node {
41+
// Make sure to visit recursively so that any nested decls get processed first.
42+
let newItem = visit(item)
43+
44+
// Rewrite any `return <assignment>` expressions as `<assignment><newline>return`.
45+
switch newItem.item {
46+
case .stmt(let stmt):
47+
guard
48+
let returnStmt = stmt.as(ReturnStmtSyntax.self),
49+
let assignmentExpr = assignmentExpression(from: returnStmt)
50+
else {
51+
// Head to the default case where we just keep the original item.
52+
fallthrough
53+
}
54+
55+
// Move the leading trivia from the `return` statement to the new assignment statement,
56+
// since that's a more sensible place than between the two.
57+
newItems.append(
58+
CodeBlockItemSyntax(
59+
item: .expr(ExprSyntax(assignmentExpr)),
60+
semicolon: nil,
61+
errorTokens: nil
62+
)
63+
.withLeadingTrivia(
64+
(returnStmt.leadingTrivia ?? []) + (assignmentExpr.leadingTrivia ?? []))
65+
.withTrailingTrivia([]))
66+
newItems.append(
67+
CodeBlockItemSyntax(
68+
item: .stmt(StmtSyntax(returnStmt.withExpression(nil))),
69+
semicolon: nil,
70+
errorTokens: nil
71+
)
72+
.withLeadingTrivia([.newlines(1)])
73+
.withTrailingTrivia(returnStmt.trailingTrivia?.withoutLeadingSpaces() ?? []))
74+
75+
default:
76+
newItems.append(newItem)
77+
}
78+
}
79+
80+
return CodeBlockItemListSyntax(newItems)
81+
}
82+
83+
/// Extracts and returns the assignment expression in the given `return` statement, if there was
84+
/// one.
85+
///
86+
/// If the `return` statement did not have an expression or if its expression was not an
87+
/// assignment expression, nil is returned.
88+
private func assignmentExpression(from returnStmt: ReturnStmtSyntax) -> InfixOperatorExprSyntax? {
89+
guard
90+
let returnExpr = returnStmt.expression,
91+
let infixOperatorExpr = returnExpr.as(InfixOperatorExprSyntax.self)
92+
else {
93+
return nil
94+
}
95+
return isAssignmentExpression(infixOperatorExpr) ? infixOperatorExpr : nil
96+
}
97+
98+
/// Returns a value indicating whether the given infix operator expression is an assignment
99+
/// expression (either simple assignment with `=` or compound assignment with an operator like
100+
/// `+=`).
101+
private func isAssignmentExpression(_ expr: InfixOperatorExprSyntax) -> Bool {
102+
if expr.operatorOperand.is(AssignmentExprSyntax.self) {
103+
return true
104+
}
105+
guard let binaryOp = expr.operatorOperand.as(BinaryOperatorExprSyntax.self) else {
106+
return false
107+
}
108+
return context.operatorTable.infixOperator(named: binaryOp.operatorToken.text)?.precedenceGroup
109+
== "AssignmentPrecedence"
110+
}
111+
}
112+
113+
extension Finding.Message {
114+
public static let moveAssignmentToOwnStatement: Finding.Message =
115+
"move assignment expression into its own statement"
116+
}

Sources/SwiftFormatRules/RuleNameCache+Generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public let ruleNameCache: [ObjectIdentifier: String] = [
2828
ObjectIdentifier(NeverUseForceTry.self): "NeverUseForceTry",
2929
ObjectIdentifier(NeverUseImplicitlyUnwrappedOptionals.self): "NeverUseImplicitlyUnwrappedOptionals",
3030
ObjectIdentifier(NoAccessLevelOnExtensionDeclaration.self): "NoAccessLevelOnExtensionDeclaration",
31+
ObjectIdentifier(NoAssignmentInExpressions.self): "NoAssignmentInExpressions",
3132
ObjectIdentifier(NoBlockComments.self): "NoBlockComments",
3233
ObjectIdentifier(NoCasesWithOnlyFallthrough.self): "NoCasesWithOnlyFallthrough",
3334
ObjectIdentifier(NoEmptyTrailingClosureParentheses.self): "NoEmptyTrailingClosureParentheses",
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import SwiftFormatRules
2+
3+
final class NoAssignmentInExpressionsTests: LintOrFormatRuleTestCase {
4+
func testAssignmentInExpressionContextIsDiagnosed() {
5+
XCTAssertFormatting(
6+
NoAssignmentInExpressions.self,
7+
input: """
8+
foo(bar, baz = quux, a + b)
9+
""",
10+
expected: """
11+
foo(bar, baz = quux, a + b)
12+
"""
13+
)
14+
XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 1, column: 10)
15+
// Make sure no other expressions were diagnosed.
16+
XCTAssertNotDiagnosed(.moveAssignmentToOwnStatement)
17+
}
18+
19+
func testReturnStatementWithoutExpressionIsUnchanged() {
20+
XCTAssertFormatting(
21+
NoAssignmentInExpressions.self,
22+
input: """
23+
func foo() {
24+
return
25+
}
26+
""",
27+
expected: """
28+
func foo() {
29+
return
30+
}
31+
"""
32+
)
33+
XCTAssertNotDiagnosed(.moveAssignmentToOwnStatement)
34+
}
35+
36+
func testReturnStatementWithNonAssignmentExpressionIsUnchanged() {
37+
XCTAssertFormatting(
38+
NoAssignmentInExpressions.self,
39+
input: """
40+
func foo() {
41+
return a + b
42+
}
43+
""",
44+
expected: """
45+
func foo() {
46+
return a + b
47+
}
48+
"""
49+
)
50+
XCTAssertNotDiagnosed(.moveAssignmentToOwnStatement)
51+
}
52+
53+
func testReturnStatementWithSimpleAssignmentExpressionIsExpanded() {
54+
// For this and similar tests below, we don't try to match the leading indentation in the new
55+
// `return` statement; the pretty-printer will fix it up.
56+
XCTAssertFormatting(
57+
NoAssignmentInExpressions.self,
58+
input: """
59+
func foo() {
60+
return a = b
61+
}
62+
""",
63+
expected: """
64+
func foo() {
65+
a = b
66+
return
67+
}
68+
"""
69+
)
70+
XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 2, column: 10)
71+
}
72+
73+
func testReturnStatementWithCompoundAssignmentExpressionIsExpanded() {
74+
XCTAssertFormatting(
75+
NoAssignmentInExpressions.self,
76+
input: """
77+
func foo() {
78+
return a += b
79+
}
80+
""",
81+
expected: """
82+
func foo() {
83+
a += b
84+
return
85+
}
86+
"""
87+
)
88+
XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 2, column: 10)
89+
}
90+
91+
func testReturnStatementWithAssignmentDealsWithLeadingLineCommentSensibly() {
92+
XCTAssertFormatting(
93+
NoAssignmentInExpressions.self,
94+
input: """
95+
func foo() {
96+
// some comment
97+
return a = b
98+
}
99+
""",
100+
expected: """
101+
func foo() {
102+
// some comment
103+
a = b
104+
return
105+
}
106+
"""
107+
)
108+
XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 3, column: 10)
109+
}
110+
111+
func testReturnStatementWithAssignmentDealsWithTrailingLineCommentSensibly() {
112+
XCTAssertFormatting(
113+
NoAssignmentInExpressions.self,
114+
input: """
115+
func foo() {
116+
return a = b // some comment
117+
}
118+
""",
119+
expected: """
120+
func foo() {
121+
a = b
122+
return // some comment
123+
}
124+
"""
125+
)
126+
XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 2, column: 10)
127+
}
128+
129+
func testReturnStatementWithAssignmentDealsWithTrailingBlockCommentSensibly() {
130+
XCTAssertFormatting(
131+
NoAssignmentInExpressions.self,
132+
input: """
133+
func foo() {
134+
return a = b /* some comment */
135+
}
136+
""",
137+
expected: """
138+
func foo() {
139+
a = b
140+
return /* some comment */
141+
}
142+
"""
143+
)
144+
XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 2, column: 10)
145+
}
146+
147+
func testReturnStatementWithAssignmentDealsWithNestedBlockCommentSensibly() {
148+
XCTAssertFormatting(
149+
NoAssignmentInExpressions.self,
150+
input: """
151+
func foo() {
152+
return /* some comment */ a = b
153+
}
154+
""",
155+
expected: """
156+
func foo() {
157+
/* some comment */ a = b
158+
return
159+
}
160+
"""
161+
)
162+
XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 2, column: 29)
163+
}
164+
}

0 commit comments

Comments
 (0)