Skip to content

Commit c22de52

Browse files
kasekenSimplyDanny
andauthored
Respect ignore_swiftui_view_bodies option in view builders and preview macros/providers (#6075)
Co-authored-by: Danny Mösch <[email protected]>
1 parent 3a922d4 commit c22de52

File tree

2 files changed

+139
-6
lines changed

2 files changed

+139
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
[JP Simard](https://github.com/jpsim)
3434
[Matt Pennig](https://github.com/pennig)
3535

36+
* Fix false positives of `redundant_discardable_let` rule in `@ViewBuilder` functions,
37+
`#Preview` macro bodies and preview providers when `ignore_swiftui_view_bodies` is
38+
enabled.
39+
[kaseken](https://github.com/kaseken)
40+
[#6063](https://github.com/realm/SwiftLint/issues/6063)
41+
3642
### Bug Fixes
3743

3844
* Improved error reporting when SwiftLint exits, because of an invalid configuration file

Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift

Lines changed: 133 additions & 6 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,10 +51,74 @@ 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+
"""),
73+
Example("""
74+
var notBody: some View {
75+
↓let _ = foo()
76+
Text("Hello, World!")
77+
}
78+
""", configuration: ["ignore_swiftui_view_bodies": true], excludeFromDocumentation: true),
79+
Example("""
80+
var body: some NotView {
81+
↓let _ = foo()
82+
Text("Hello, World!")
83+
}
84+
""", configuration: ["ignore_swiftui_view_bodies": true], excludeFromDocumentation: true),
3585
],
3686
corrections: [
3787
Example("↓let _ = foo()"): Example("_ = foo()"),
3888
Example("if _ = foo() { ↓let _ = bar() }"): Example("if _ = foo() { _ = bar() }"),
89+
Example("""
90+
var body: some View {
91+
↓let _ = foo()
92+
Text("Hello, World!")
93+
}
94+
"""): Example("""
95+
var body: some View {
96+
_ = foo()
97+
Text("Hello, World!")
98+
}
99+
"""),
100+
Example("""
101+
#Preview {
102+
↓let _ = foo()
103+
return Text("Hello, World!")
104+
}
105+
"""): Example("""
106+
#Preview {
107+
_ = foo()
108+
return Text("Hello, World!")
109+
}
110+
"""),
111+
Example("""
112+
var body: some View {
113+
let _ = foo()
114+
return Text("Hello, World!")
115+
}
116+
""", configuration: ["ignore_swiftui_view_bodies": true]): Example("""
117+
var body: some View {
118+
let _ = foo()
119+
return Text("Hello, World!")
120+
}
121+
"""),
39122
]
40123
)
41124
}
@@ -50,23 +133,32 @@ private extension RedundantDiscardableLetRule {
50133
private var codeBlockScopes = Stack<CodeBlockKind>()
51134

52135
override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind {
53-
codeBlockScopes.push(node.isViewBody ? .view : .normal)
136+
codeBlockScopes.push(node.isViewBody || node.isPreviewProviderBody ? .view : .normal)
54137
return .visitChildren
55138
}
56139

57140
override func visitPost(_: AccessorBlockSyntax) {
58141
codeBlockScopes.pop()
59142
}
60143

61-
override func visit(_: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
62-
codeBlockScopes.push(.normal)
144+
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
145+
codeBlockScopes.push(node.isViewBuilderFunctionBody ? .view : .normal)
63146
return .visitChildren
64147
}
65148

66149
override func visitPost(_: CodeBlockSyntax) {
67150
codeBlockScopes.pop()
68151
}
69152

153+
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
154+
codeBlockScopes.push(node.isPreviewMacroBody ? .view : .normal)
155+
return .visitChildren
156+
}
157+
158+
override func visitPost(_: ClosureExprSyntax) {
159+
codeBlockScopes.pop()
160+
}
161+
70162
override func visitPost(_ node: VariableDeclSyntax) {
71163
if codeBlockScopes.peek() != .view || !configuration.ignoreSwiftUIViewBodies,
72164
node.bindingSpecifier.tokenKind == .keyword(.let),
@@ -94,10 +186,45 @@ private extension AccessorBlockSyntax {
94186
if let binding = parent?.as(PatternBindingSyntax.self),
95187
binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "body",
96188
let type = binding.typeAnnotation?.type.as(SomeOrAnyTypeSyntax.self) {
97-
return type.someOrAnySpecifier.text == "some"
98-
&& type.constraint.as(IdentifierTypeSyntax.self)?.name.text == "View"
99-
&& binding.parent?.parent?.is(VariableDeclSyntax.self) == true
189+
return type.isView && binding.parent?.parent?.is(VariableDeclSyntax.self) == true
100190
}
101191
return false
102192
}
193+
194+
var isPreviewProviderBody: Bool {
195+
guard let binding = parent?.as(PatternBindingSyntax.self),
196+
binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "previews",
197+
let bindingList = binding.parent?.as(PatternBindingListSyntax.self),
198+
let variableDecl = bindingList.parent?.as(VariableDeclSyntax.self),
199+
variableDecl.modifiers.contains(keyword: .static),
200+
variableDecl.bindingSpecifier.tokenKind == .keyword(.var),
201+
let type = binding.typeAnnotation?.type.as(SomeOrAnyTypeSyntax.self) else {
202+
return false
203+
}
204+
205+
return type.isView
206+
}
207+
}
208+
209+
private extension CodeBlockSyntax {
210+
var isViewBuilderFunctionBody: Bool {
211+
guard let functionDecl = parent?.as(FunctionDeclSyntax.self),
212+
functionDecl.attributes.contains(attributeNamed: "ViewBuilder") else {
213+
return false
214+
}
215+
return functionDecl.signature.returnClause?.type.as(SomeOrAnyTypeSyntax.self)?.isView ?? false
216+
}
217+
}
218+
219+
private extension ClosureExprSyntax {
220+
var isPreviewMacroBody: Bool {
221+
parent?.as(MacroExpansionExprSyntax.self)?.macroName.text == "Preview"
222+
}
223+
}
224+
225+
private extension SomeOrAnyTypeSyntax {
226+
var isView: Bool {
227+
someOrAnySpecifier.text == "some" &&
228+
constraint.as(IdentifierTypeSyntax.self)?.name.text == "View"
229+
}
103230
}

0 commit comments

Comments
 (0)