Skip to content

Commit 3852bac

Browse files
authored
Merge pull request #535 from allevato/xctnothrow-assignment-fixes
Allow exceptions to `NoAssignmentInExpressions`.
2 parents 799c88b + bc44599 commit 3852bac

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

Sources/SwiftFormatConfiguration/Configuration.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public struct Configuration: Codable, Equatable {
3636
case indentSwitchCaseLabels
3737
case rules
3838
case spacesAroundRangeFormationOperators
39+
case noAssignmentInExpressions
3940
}
4041

4142
/// The version of this configuration.
@@ -147,6 +148,9 @@ public struct Configuration: Codable, Equatable {
147148
/// `...` and `..<`.
148149
public var spacesAroundRangeFormationOperators = false
149150

151+
/// Contains exceptions for the `NoAssignmentInExpressions` rule.
152+
public var noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration()
153+
150154
/// Constructs a Configuration with all default values.
151155
public init() {
152156
self.version = highestSupportedConfigurationVersion
@@ -208,6 +212,10 @@ public struct Configuration: Codable, Equatable {
208212
?? FileScopedDeclarationPrivacyConfiguration()
209213
self.indentSwitchCaseLabels
210214
= try container.decodeIfPresent(Bool.self, forKey: .indentSwitchCaseLabels) ?? false
215+
self.noAssignmentInExpressions =
216+
try container.decodeIfPresent(
217+
NoAssignmentInExpressionsConfiguration.self, forKey: .noAssignmentInExpressions)
218+
?? NoAssignmentInExpressionsConfiguration()
211219

212220
// If the `rules` key is not present at all, default it to the built-in set
213221
// so that the behavior is the same as if the configuration had been
@@ -238,6 +246,7 @@ public struct Configuration: Codable, Equatable {
238246
spacesAroundRangeFormationOperators, forKey: .spacesAroundRangeFormationOperators)
239247
try container.encode(fileScopedDeclarationPrivacy, forKey: .fileScopedDeclarationPrivacy)
240248
try container.encode(indentSwitchCaseLabels, forKey: .indentSwitchCaseLabels)
249+
try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions)
241250
try container.encode(rules, forKey: .rules)
242251
}
243252

@@ -287,3 +296,15 @@ public struct FileScopedDeclarationPrivacyConfiguration: Codable, Equatable {
287296
/// private access.
288297
public var accessLevel: AccessLevel = .private
289298
}
299+
300+
/// Configuration for the `NoAssignmentInExpressions` rule.
301+
public struct NoAssignmentInExpressionsConfiguration: Codable, Equatable {
302+
/// A list of function names where assignments are allowed to be embedded in expressions that are
303+
/// passed as parameters to that function.
304+
public var allowedFunctions: [String] = [
305+
// Allow `XCTAssertNoThrow` because `XCTAssertNoThrow(x = try ...)` is clearer about intent than
306+
// `x = try XCTUnwrap(try? ...)` or force-unwrapped if you need to use the value `x` later on
307+
// in the test.
308+
"XCTAssertNoThrow"
309+
]
310+
}

Sources/SwiftFormatRules/NoAssignmentInExpressions.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import SwiftFormatConfiguration
1314
import SwiftFormatCore
1415
import SwiftSyntax
1516

@@ -27,7 +28,10 @@ public final class NoAssignmentInExpressions: SyntaxFormatRule {
2728
public override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax {
2829
// Diagnose any assignment that isn't directly a child of a `CodeBlockItem` (which would be the
2930
// case if it was its own statement).
30-
if isAssignmentExpression(node) && !isStandaloneAssignmentStatement(node) {
31+
if isAssignmentExpression(node)
32+
&& !isStandaloneAssignmentStatement(node)
33+
&& !isInAllowedFunction(node)
34+
{
3135
diagnose(.moveAssignmentToOwnStatement, on: node)
3236
}
3337
return ExprSyntax(node)
@@ -131,6 +135,30 @@ public final class NoAssignmentInExpressions: SyntaxFormatRule {
131135
}
132136
return parent.is(CodeBlockItemSyntax.self)
133137
}
138+
139+
/// Returns true if the infix operator expression is in the (non-closure) parameters of an allowed
140+
/// function call.
141+
private func isInAllowedFunction(_ node: InfixOperatorExprSyntax) -> Bool {
142+
let allowedFunctions = context.configuration.noAssignmentInExpressions.allowedFunctions
143+
// Walk up the tree until we find a FunctionCallExprSyntax, and if the name matches, return
144+
// true. However, stop early if we hit a CodeBlockItemSyntax first; this would represent a
145+
// closure context where we *don't* want the exception to apply (for example, in
146+
// `someAllowedFunction(a, b) { return c = d }`, the `c = d` is a descendent of a function call
147+
// but we want it to be evaluated in its own context.
148+
var node = Syntax(node)
149+
while let parent = node.parent {
150+
node = parent
151+
if node.is(CodeBlockItemSyntax.self) {
152+
break
153+
}
154+
if let functionCallExpr = node.as(FunctionCallExprSyntax.self),
155+
allowedFunctions.contains(functionCallExpr.calledExpression.trimmedDescription)
156+
{
157+
return true
158+
}
159+
}
160+
return false
161+
}
134162
}
135163

136164
extension Finding.Message {

Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,35 @@ final class NoAssignmentInExpressionsTests: LintOrFormatRuleTestCase {
180180
)
181181
XCTAssertNotDiagnosed(.moveAssignmentToOwnStatement)
182182
}
183+
184+
func testAssignmentExpressionsInAllowedFunctions() {
185+
XCTAssertFormatting(
186+
NoAssignmentInExpressions.self,
187+
input: """
188+
// These should not diagnose.
189+
XCTAssertNoThrow(a = try b())
190+
XCTAssertNoThrow { a = try b() }
191+
XCTAssertNoThrow({ a = try b() })
192+
someRegularFunction({ a = b })
193+
someRegularFunction { a = b }
194+
195+
// This should be diagnosed.
196+
someRegularFunction(a = b)
197+
""",
198+
expected: """
199+
// These should not diagnose.
200+
XCTAssertNoThrow(a = try b())
201+
XCTAssertNoThrow { a = try b() }
202+
XCTAssertNoThrow({ a = try b() })
203+
someRegularFunction({ a = b })
204+
someRegularFunction { a = b }
205+
206+
// This should be diagnosed.
207+
someRegularFunction(a = b)
208+
"""
209+
)
210+
XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 9, column: 21)
211+
// Make sure no other expressions were diagnosed.
212+
XCTAssertNotDiagnosed(.moveAssignmentToOwnStatement)
213+
}
183214
}

0 commit comments

Comments
 (0)