Skip to content

Commit

Permalink
Add option to disable redundant_discardable_let in SwiftUI view bod…
Browse files Browse the repository at this point in the history
…ies (#5929)
  • Loading branch information
SimplyDanny authored Jan 3, 2025
1 parent be25c1f commit ae8aeb3
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 24 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
[dk-talks](https://github.com/dk-talks)
[SimplyDanny](https://github.com/SimplyDanny)

* Add option to disable `redundant_discardable_let` rule in SwiftUI view bodies.
[SimplyDanny](https://github.com/SimplyDanny)
[#3855](https://github.com/realm/SwiftLint/issues/3855)

* Add new `redundant_sendable` rule that triggers on `Sendable` conformances of
types that are implicitly already `Sendable` due to being actor-isolated. It
is enabled by default.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SwiftLintCore

@AutoConfigParser
struct RedundantDiscardableLetConfiguration: SeverityBasedRuleConfiguration {
typealias Parent = RedundantDiscardableLetRule

@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
@ConfigurationElement(key: "ignore_swiftui_view_bodies")
private(set) var ignoreSwiftUIViewBodies = false
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import SwiftSyntax

@SwiftSyntaxRule(explicitRewriter: true)
@SwiftSyntaxRule(correctable: true)
struct RedundantDiscardableLetRule: Rule {
var configuration = SeverityConfiguration<Self>(.warning)
var configuration = RedundantDiscardableLetConfiguration()

static let description = RuleDescription(
identifier: "redundant_discardable_let",
Expand All @@ -16,10 +16,22 @@ struct RedundantDiscardableLetRule: Rule {
Example("let _: ExplicitType = foo()"),
Example("while let _ = SplashStyle(rawValue: maxValue) { maxValue += 1 }"),
Example("async let _ = await foo()"),
Example("""
var body: some View {
let _ = foo()
return Text("Hello, World!")
}
""", configuration: ["ignore_swiftui_view_bodies": true]),
],
triggeringExamples: [
Example("↓let _ = foo()"),
Example("if _ = foo() { ↓let _ = bar() }"),
Example("""
var body: some View {
↓let _ = foo()
Text("Hello, World!")
}
"""),
],
corrections: [
Example("↓let _ = foo()"): Example("_ = foo()"),
Expand All @@ -29,35 +41,63 @@ struct RedundantDiscardableLetRule: Rule {
}

private extension RedundantDiscardableLetRule {
private enum CodeBlockKind {
case normal
case view
}

final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: VariableDeclSyntax) {
if node.hasRedundantDiscardableLetViolation {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
private var codeBlockScopes = Stack<CodeBlockKind>()

override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind {
codeBlockScopes.push(node.isViewBody ? .view : .normal)
return .visitChildren
}
}

final class Rewriter: ViolationsSyntaxRewriter<ConfigurationType> {
override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
guard node.hasRedundantDiscardableLetViolation else {
return super.visit(node)
}
override func visitPost(_: AccessorBlockSyntax) {
codeBlockScopes.pop()
}

correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
let newNode = node
.with(\.bindingSpecifier, .keyword(.let, presence: .missing))
.with(\.bindings, node.bindings.with(\.leadingTrivia, node.bindingSpecifier.leadingTrivia))
return super.visit(newNode)
override func visit(_: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
codeBlockScopes.push(.normal)
return .visitChildren
}

override func visitPost(_: CodeBlockSyntax) {
codeBlockScopes.pop()
}

override func visitPost(_ node: VariableDeclSyntax) {
if codeBlockScopes.peek() != .view || !configuration.ignoreSwiftUIViewBodies,
node.bindingSpecifier.tokenKind == .keyword(.let),
let binding = node.bindings.onlyElement,
binding.pattern.is(WildcardPatternSyntax.self),
binding.typeAnnotation == nil,
!node.modifiers.contains(where: { $0.name.text == "async" }) {
violations.append(
ReasonedRuleViolation(
position: node.bindingSpecifier.positionAfterSkippingLeadingTrivia,
correction: .init(
start: node.bindingSpecifier.positionAfterSkippingLeadingTrivia,
end: binding.pattern.positionAfterSkippingLeadingTrivia,
replacement: ""
)
)
)
}
}
}
}

private extension VariableDeclSyntax {
var hasRedundantDiscardableLetViolation: Bool {
bindingSpecifier.tokenKind == .keyword(.let)
&& bindings.count == 1
&& bindings.first!.pattern.is(WildcardPatternSyntax.self)
&& bindings.first!.typeAnnotation == nil
&& modifiers.contains(where: { $0.name.text == "async" }) != true
private extension AccessorBlockSyntax {
var isViewBody: Bool {
if let binding = parent?.as(PatternBindingSyntax.self),
binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "body",
let type = binding.typeAnnotation?.type.as(SomeOrAnyTypeSyntax.self) {
return type.someOrAnySpecifier.text == "some"
&& type.constraint.as(IdentifierTypeSyntax.self)?.name.text == "View"
&& binding.parent?.parent?.is(VariableDeclSyntax.self) == true
}
return false
}
}
1 change: 1 addition & 0 deletions Tests/IntegrationTests/default_rule_configurations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,7 @@ reduce_into:
opt-in: true
redundant_discardable_let:
severity: warning
ignore_swiftui_view_bodies: false
meta:
opt-in: false
redundant_nil_coalescing:
Expand Down

0 comments on commit ae8aeb3

Please sign in to comment.