diff --git a/CHANGELOG.md b/CHANGELOG.md index b575048de4..6538b218b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantDiscardableLetConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantDiscardableLetConfiguration.swift new file mode 100644 index 0000000000..70a588991a --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/RedundantDiscardableLetConfiguration.swift @@ -0,0 +1,11 @@ +import SwiftLintCore + +@AutoConfigParser +struct RedundantDiscardableLetConfiguration: SeverityBasedRuleConfiguration { + typealias Parent = RedundantDiscardableLetRule + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + @ConfigurationElement(key: "ignore_swiftui_view_bodies") + private(set) var ignoreSwiftUIViewBodies = false +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift index 76904825a0..d7d6f2a1f6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift @@ -1,8 +1,8 @@ import SwiftSyntax -@SwiftSyntaxRule(explicitRewriter: true) +@SwiftSyntaxRule(correctable: true) struct RedundantDiscardableLetRule: Rule { - var configuration = SeverityConfiguration(.warning) + var configuration = RedundantDiscardableLetConfiguration() static let description = RuleDescription( identifier: "redundant_discardable_let", @@ -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()"), @@ -29,35 +41,63 @@ struct RedundantDiscardableLetRule: Rule { } private extension RedundantDiscardableLetRule { + private enum CodeBlockKind { + case normal + case view + } + final class Visitor: ViolationsSyntaxVisitor { - override func visitPost(_ node: VariableDeclSyntax) { - if node.hasRedundantDiscardableLetViolation { - violations.append(node.positionAfterSkippingLeadingTrivia) - } + private var codeBlockScopes = Stack() + + override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind { + codeBlockScopes.push(node.isViewBody ? .view : .normal) + return .visitChildren } - } - final class Rewriter: ViolationsSyntaxRewriter { - 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 } } diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 717d6aec93..76cefae7c2 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -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: