Skip to content

Commit 2a6868a

Browse files
authored
Merge pull request #627 from allevato/no-discard-conditional-assignment
Add the `UseExplicitNilCheckInConditions` rule.
2 parents e2accd4 + 9718f58 commit 2a6868a

File tree

7 files changed

+181
-0
lines changed

7 files changed

+181
-0
lines changed

Documentation/RuleDocumentation.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ automatically.
1212
Here's the list of available rules:
1313

1414
- [AllPublicDeclarationsHaveDocumentation](#AllPublicDeclarationsHaveDocumentation)
15+
- [AlwaysUseLiteralForEmptyCollectionInit](#AlwaysUseLiteralForEmptyCollectionInit)
1516
- [AlwaysUseLowerCamelCase](#AlwaysUseLowerCamelCase)
1617
- [AmbiguousTrailingClosureOverload](#AmbiguousTrailingClosureOverload)
1718
- [BeginDocumentationCommentWithOneLineSummary](#BeginDocumentationCommentWithOneLineSummary)
@@ -43,6 +44,7 @@ Here's the list of available rules:
4344
- [ReturnVoidInsteadOfEmptyTuple](#ReturnVoidInsteadOfEmptyTuple)
4445
- [TypeNamesShouldBeCapitalized](#TypeNamesShouldBeCapitalized)
4546
- [UseEarlyExits](#UseEarlyExits)
47+
- [UseExplicitNilCheckInConditions](#UseExplicitNilCheckInConditions)
4648
- [UseLetInEveryBoundCaseVariable](#UseLetInEveryBoundCaseVariable)
4749
- [UseShorthandTypeNames](#UseShorthandTypeNames)
4850
- [UseSingleLinePropertyGetter](#UseSingleLinePropertyGetter)
@@ -59,6 +61,17 @@ Lint: If a public declaration is missing a documentation comment, a lint error i
5961

6062
`AllPublicDeclarationsHaveDocumentation` is a linter-only rule.
6163

64+
### AlwaysUseLiteralForEmptyCollectionInit
65+
66+
Never use `[<Type>]()` syntax. In call sites that should be replaced with `[]`,
67+
for initializations use explicit type combined with empty array literal `let _: [<Type>] = []`
68+
Static properties of a type that return that type should not include a reference to their type.
69+
70+
Lint: Non-literal empty array initialization will yield a lint error.
71+
Format: All invalid use sites would be related with empty literal (with or without explicit type annotation).
72+
73+
`AlwaysUseLiteralForEmptyCollectionInit` rule can format your code automatically.
74+
6275
### AlwaysUseLowerCamelCase
6376

6477
All values should be written in lower camel-case (`lowerCamelCase`).
@@ -446,6 +459,20 @@ Format: `if ... else { return/throw/break/continue }` constructs will be replace
446459

447460
`UseEarlyExits` rule can format your code automatically.
448461

462+
### UseExplicitNilCheckInConditions
463+
464+
When checking an optional value for `nil`-ness, prefer writing an explicit `nil` check rather
465+
than binding and immediately discarding the value.
466+
467+
For example, `if let _ = someValue { ... }` is forbidden. Use `if someValue != nil { ... }`
468+
instead.
469+
470+
Lint: `let _ = expr` inside a condition list will yield a lint error.
471+
472+
Format: `let _ = expr` inside a condition list will be replaced by `expr != nil`.
473+
474+
`UseExplicitNilCheckInConditions` rule can format your code automatically.
475+
449476
### UseLetInEveryBoundCaseVariable
450477

451478
Every variable bound in a `case` pattern must have its own `let/var`.

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ let package = Package(
5252
dependencies: [
5353
.product(name: "Markdown", package: "swift-markdown"),
5454
.product(name: "SwiftSyntax", package: "swift-syntax"),
55+
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
5556
.product(name: "SwiftOperators", package: "swift-syntax"),
5657
.product(name: "SwiftParser", package: "swift-syntax"),
5758
.product(name: "SwiftParserDiagnostics", package: "swift-syntax"),

Sources/SwiftFormat/Core/Pipelines+Generated.swift

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

9292
override func visit(_ node: ConditionElementSyntax) -> SyntaxVisitorContinueKind {
9393
visitIfEnabled(NoParensAroundConditions.visit, for: node)
94+
visitIfEnabled(UseExplicitNilCheckInConditions.visit, for: node)
9495
return .visitChildren
9596
}
9697

@@ -376,6 +377,7 @@ extension FormatPipeline {
376377
node = OrderedImports(context: context).rewrite(node)
377378
node = ReturnVoidInsteadOfEmptyTuple(context: context).rewrite(node)
378379
node = UseEarlyExits(context: context).rewrite(node)
380+
node = UseExplicitNilCheckInConditions(context: context).rewrite(node)
379381
node = UseShorthandTypeNames(context: context).rewrite(node)
380382
node = UseSingleLinePropertyGetter(context: context).rewrite(node)
381383
node = UseTripleSlashForDocumentationComments(context: context).rewrite(node)

Sources/SwiftFormat/Core/RuleNameCache+Generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public let ruleNameCache: [ObjectIdentifier: String] = [
4848
ObjectIdentifier(ReturnVoidInsteadOfEmptyTuple.self): "ReturnVoidInsteadOfEmptyTuple",
4949
ObjectIdentifier(TypeNamesShouldBeCapitalized.self): "TypeNamesShouldBeCapitalized",
5050
ObjectIdentifier(UseEarlyExits.self): "UseEarlyExits",
51+
ObjectIdentifier(UseExplicitNilCheckInConditions.self): "UseExplicitNilCheckInConditions",
5152
ObjectIdentifier(UseLetInEveryBoundCaseVariable.self): "UseLetInEveryBoundCaseVariable",
5253
ObjectIdentifier(UseShorthandTypeNames.self): "UseShorthandTypeNames",
5354
ObjectIdentifier(UseSingleLinePropertyGetter.self): "UseSingleLinePropertyGetter",

Sources/SwiftFormat/Core/RuleRegistry+Generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"ReturnVoidInsteadOfEmptyTuple": true,
4848
"TypeNamesShouldBeCapitalized": true,
4949
"UseEarlyExits": false,
50+
"UseExplicitNilCheckInConditions": true,
5051
"UseLetInEveryBoundCaseVariable": true,
5152
"UseShorthandTypeNames": true,
5253
"UseSingleLinePropertyGetter": true,
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 - 2023 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+
import SwiftSyntaxBuilder
15+
16+
/// When checking an optional value for `nil`-ness, prefer writing an explicit `nil` check rather
17+
/// than binding and immediately discarding the value.
18+
///
19+
/// For example, `if let _ = someValue { ... }` is forbidden. Use `if someValue != nil { ... }`
20+
/// instead.
21+
///
22+
/// Lint: `let _ = expr` inside a condition list will yield a lint error.
23+
///
24+
/// Format: `let _ = expr` inside a condition list will be replaced by `expr != nil`.
25+
@_spi(Rules)
26+
public final class UseExplicitNilCheckInConditions: SyntaxFormatRule {
27+
public override func visit(_ node: ConditionElementSyntax) -> ConditionElementSyntax {
28+
switch node.condition {
29+
case .optionalBinding(let optionalBindingCondition):
30+
guard
31+
let initializerClause = optionalBindingCondition.initializer,
32+
isDiscardedAssignmentPattern(optionalBindingCondition.pattern)
33+
else {
34+
return node
35+
}
36+
37+
diagnose(.useExplicitNilComparison, on: optionalBindingCondition)
38+
39+
// Since we're moving the initializer value from the RHS to the LHS of an expression/pattern,
40+
// preserve the relative position of the trailing trivia. Similarly, preserve the leading
41+
// trivia of the original node, since that token is being removed entirely.
42+
var value = initializerClause.value
43+
let trailingTrivia = value.trailingTrivia
44+
value.trailingTrivia = []
45+
46+
return ConditionElementSyntax(
47+
condition: .expression("\(node.leadingTrivia)\(value) != nil\(trailingTrivia)"))
48+
default:
49+
return node
50+
}
51+
}
52+
53+
/// Returns true if the given pattern is a discarding assignment expression (for example, the `_`
54+
/// in `let _ = x`).
55+
private func isDiscardedAssignmentPattern(_ pattern: PatternSyntax) -> Bool {
56+
guard let exprPattern = pattern.as(ExpressionPatternSyntax.self) else {
57+
return false
58+
}
59+
return exprPattern.expression.is(DiscardAssignmentExprSyntax.self)
60+
}
61+
}
62+
63+
extension Finding.Message {
64+
@_spi(Rules)
65+
public static let useExplicitNilComparison: Finding.Message =
66+
"compare this value using `!= nil` instead of binding and discarding it"
67+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import _SwiftFormatTestSupport
2+
3+
@_spi(Rules) import SwiftFormat
4+
5+
final class UseExplicitNilCheckInConditionsTests: LintOrFormatRuleTestCase {
6+
func testIfExpressions() {
7+
assertFormatting(
8+
UseExplicitNilCheckInConditions.self,
9+
input: """
10+
if 1️⃣let _ = x {}
11+
if let x = y, 2️⃣let _ = x.m {}
12+
if let x = y {} else if 3️⃣let _ = z {}
13+
""",
14+
expected: """
15+
if x != nil {}
16+
if let x = y, x.m != nil {}
17+
if let x = y {} else if z != nil {}
18+
""",
19+
findings: [
20+
FindingSpec("1️⃣", message: "compare this value using `!= nil` instead of binding and discarding it"),
21+
FindingSpec("2️⃣", message: "compare this value using `!= nil` instead of binding and discarding it"),
22+
FindingSpec("3️⃣", message: "compare this value using `!= nil` instead of binding and discarding it"),
23+
]
24+
)
25+
}
26+
27+
func testGuardStatements() {
28+
assertFormatting(
29+
UseExplicitNilCheckInConditions.self,
30+
input: """
31+
guard 1️⃣let _ = x else {}
32+
guard let x = y, 2️⃣let _ = x.m else {}
33+
""",
34+
expected: """
35+
guard x != nil else {}
36+
guard let x = y, x.m != nil else {}
37+
""",
38+
findings: [
39+
FindingSpec("1️⃣", message: "compare this value using `!= nil` instead of binding and discarding it"),
40+
FindingSpec("2️⃣", message: "compare this value using `!= nil` instead of binding and discarding it"),
41+
]
42+
)
43+
}
44+
45+
func testWhileStatements() {
46+
assertFormatting(
47+
UseExplicitNilCheckInConditions.self,
48+
input: """
49+
while 1️⃣let _ = x {}
50+
while let x = y, 2️⃣let _ = x.m {}
51+
""",
52+
expected: """
53+
while x != nil {}
54+
while let x = y, x.m != nil {}
55+
""",
56+
findings: [
57+
FindingSpec("1️⃣", message: "compare this value using `!= nil` instead of binding and discarding it"),
58+
FindingSpec("2️⃣", message: "compare this value using `!= nil` instead of binding and discarding it"),
59+
]
60+
)
61+
}
62+
63+
func testTriviaPreservation() {
64+
assertFormatting(
65+
UseExplicitNilCheckInConditions.self,
66+
input: """
67+
if /*comment*/ 1️⃣let _ = x /*comment*/ {}
68+
if 2️⃣let _ = x // comment
69+
{}
70+
""",
71+
expected: """
72+
if /*comment*/ x != nil /*comment*/ {}
73+
if x != nil // comment
74+
{}
75+
""",
76+
findings: [
77+
FindingSpec("1️⃣", message: "compare this value using `!= nil` instead of binding and discarding it"),
78+
FindingSpec("2️⃣", message: "compare this value using `!= nil` instead of binding and discarding it"),
79+
]
80+
)
81+
}
82+
}

0 commit comments

Comments
 (0)