Skip to content

Commit bfa6e9b

Browse files
committed
Fix false positives in redundant_discardable_let rule
- Fix false positive for @ViewBuilder functions when ignore_swiftui_view_bodies is set to true. - Fix false positive for #Preview when ignore_swiftui_view_bodies is set to true. - Fix false positive for PreviewProvider when ignore_swiftui_view_bodies is set to true.
1 parent e1ac6f8 commit bfa6e9b

File tree

2 files changed

+104
-4
lines changed

2 files changed

+104
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818

1919
### Bug Fixes
2020

21-
* None.
21+
* Fix false positives in `redundant_discardable_let` when `ignore_swiftui_view_bodies` is enabled.
22+
[kaseken](https://github.com/kaseken)
2223

2324
## 0.59.1: Crisp Spring Clean
2425

Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ struct RedundantDiscardableLetRule: Rule {
2222
return Text("Hello, World!")
2323
}
2424
""", configuration: ["ignore_swiftui_view_bodies": true]),
25+
Example("""
26+
@ViewBuilder
27+
func bar() -> some View {
28+
let _ = foo()
29+
Text("Hello, World!")
30+
}
31+
""", configuration: ["ignore_swiftui_view_bodies": true]),
32+
Example("""
33+
#Preview {
34+
let _ = foo()
35+
Text("Hello, World!")
36+
}
37+
""", configuration: ["ignore_swiftui_view_bodies": true]),
38+
Example("""
39+
static var previews: some View {
40+
let _ = foo()
41+
Text("Hello, World!")
42+
}
43+
""", configuration: ["ignore_swiftui_view_bodies": true]),
2544
],
2645
triggeringExamples: [
2746
Example("↓let _ = foo()"),
@@ -32,6 +51,25 @@ struct RedundantDiscardableLetRule: Rule {
3251
Text("Hello, World!")
3352
}
3453
"""),
54+
Example("""
55+
@ViewBuilder
56+
func bar() -> some View {
57+
let _ = foo()
58+
return Text("Hello, World!")
59+
}
60+
"""),
61+
Example("""
62+
#Preview {
63+
let _ = foo()
64+
return Text("Hello, World!")
65+
}
66+
"""),
67+
Example("""
68+
static var previews: some View {
69+
let _ = foo()
70+
Text("Hello, World!")
71+
}
72+
"""),
3573
],
3674
corrections: [
3775
Example("↓let _ = foo()"): Example("_ = foo()"),
@@ -50,23 +88,32 @@ private extension RedundantDiscardableLetRule {
5088
private var codeBlockScopes = Stack<CodeBlockKind>()
5189

5290
override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind {
53-
codeBlockScopes.push(node.isViewBody ? .view : .normal)
91+
codeBlockScopes.push(node.isViewBody || node.isPreviewProviderBody ? .view : .normal)
5492
return .visitChildren
5593
}
5694

5795
override func visitPost(_: AccessorBlockSyntax) {
5896
codeBlockScopes.pop()
5997
}
6098

61-
override func visit(_: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
62-
codeBlockScopes.push(.normal)
99+
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
100+
codeBlockScopes.push(node.isViewBuilderFunctionBody ? .view : .normal)
63101
return .visitChildren
64102
}
65103

66104
override func visitPost(_: CodeBlockSyntax) {
67105
codeBlockScopes.pop()
68106
}
69107

108+
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
109+
codeBlockScopes.push(node.isPreviewMacroBody ? .view : .normal)
110+
return .visitChildren
111+
}
112+
113+
override func visitPost(_: ClosureExprSyntax) {
114+
codeBlockScopes.pop()
115+
}
116+
70117
override func visitPost(_ node: VariableDeclSyntax) {
71118
if codeBlockScopes.peek() != .view || !configuration.ignoreSwiftUIViewBodies,
72119
node.bindingSpecifier.tokenKind == .keyword(.let),
@@ -100,4 +147,56 @@ private extension AccessorBlockSyntax {
100147
}
101148
return false
102149
}
150+
151+
var isPreviewProviderBody: Bool {
152+
guard let binding = parent?.as(PatternBindingSyntax.self),
153+
binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "previews",
154+
let bindingList = binding.parent?.as(PatternBindingListSyntax.self),
155+
let variableDecl = bindingList.parent?.as(VariableDeclSyntax.self) else {
156+
return false
157+
}
158+
159+
guard variableDecl.modifiers.contains(keyword: .static) &&
160+
variableDecl.bindingSpecifier.tokenKind == .keyword(.var) else {
161+
return false
162+
}
163+
164+
if let type = binding.typeAnnotation?.type.as(SomeOrAnyTypeSyntax.self) {
165+
return type.someOrAnySpecifier.text == "some" &&
166+
type.constraint.as(IdentifierTypeSyntax.self)?.name.text == "View"
167+
}
168+
169+
return false
170+
}
171+
}
172+
173+
private extension CodeBlockSyntax {
174+
var isViewBuilderFunctionBody: Bool {
175+
parent?.as(FunctionDeclSyntax.self)?.isViewBuilderFunction == true
176+
}
177+
}
178+
179+
private extension FunctionDeclSyntax {
180+
var isViewBuilderFunction: Bool {
181+
guard attributes.contains(attributeNamed: "ViewBuilder") else {
182+
return false
183+
}
184+
185+
guard let returnType = signature.returnClause?.type.as(SomeOrAnyTypeSyntax.self) else {
186+
return false
187+
}
188+
189+
return returnType.someOrAnySpecifier.text == "some" &&
190+
returnType.constraint.as(IdentifierTypeSyntax.self)?.name.text == "View"
191+
}
192+
}
193+
194+
private extension ClosureExprSyntax {
195+
var isPreviewMacroBody: Bool {
196+
if let macroExpansionExpr = parent?.as(MacroExpansionExprSyntax.self),
197+
macroExpansionExpr.macroName.text == "Preview" {
198+
return true
199+
}
200+
return false
201+
}
103202
}

0 commit comments

Comments
 (0)