diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index d3d2b82214..4064621760 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -176,6 +176,7 @@ public let builtInRules: [any Rule.Type] = [ ReduceBooleanRule.self, ReduceIntoRule.self, RedundantDiscardableLetRule.self, + RedundantFinalActorRule.self, RedundantNilCoalescingRule.self, RedundantObjcAttributeRule.self, RedundantSelfRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantFinalActorRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantFinalActorRule.swift new file mode 100644 index 0000000000..6d1424177d --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantFinalActorRule.swift @@ -0,0 +1,125 @@ +import SwiftSyntax + +@SwiftSyntaxRule(correctable: true, optIn: true) +struct RedundantFinalActorRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "redundant_final_actor", + name: "Redundant Final on Actor", + description: "`final` is redundant on an actor declaration and its members because actors cannot be subclassed", + rationale: """ + Actors in Swift currently do not support inheritance, making `final` redundant \ + on both actor declarations and their members. Note that this may change in future \ + Swift versions if actor inheritance is introduced. + """, + kind: .idiomatic, + nonTriggeringExamples: [ + Example("actor MyActor {}"), + Example("final class MyClass {}"), + Example(""" + @globalActor + actor MyGlobalActor {} + """), + Example(""" + actor MyActor { + func doWork() {} + var value: Int { 0 } + } + """), + Example(""" + class MyClass { + final func doWork() {} + } + """), + ], + triggeringExamples: [ + Example("↓final actor MyActor {}"), + Example("public ↓final actor DataStore {}"), + Example(""" + @globalActor + ↓final actor MyGlobalActor {} + """), + Example(""" + actor MyActor { + ↓final func doWork() {} + } + """), + Example(""" + actor MyActor { + ↓final var value: Int { 0 } + } + """), + ], + corrections: [ + Example("final actor MyActor {}"): + Example("actor MyActor {}"), + Example("public final actor DataStore {}"): + Example("public actor DataStore {}"), + Example("actor MyActor {\n final func doWork() {}\n}"): + Example("actor MyActor {\n func doWork() {}\n}"), + ] + ) +} + +private extension RedundantFinalActorRule { + final class Visitor: ViolationsSyntaxVisitor { + private var insideActor = false + + override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind { + insideActor = true + return .visitChildren + } + + override func visitPost(_ node: ActorDeclSyntax) { + if let finalModifier = node.modifiers.first(where: { $0.name.text == "final" }) { + addViolation(for: finalModifier) + } + insideActor = false + } + + override func visitPost(_ node: FunctionDeclSyntax) { + guard insideActor, + let finalModifier = node.modifiers.first(where: { $0.name.text == "final" }) else { + return + } + addViolation(for: finalModifier) + } + + override func visitPost(_ node: VariableDeclSyntax) { + guard insideActor, + let finalModifier = node.modifiers.first(where: { $0.name.text == "final" }) else { + return + } + addViolation(for: finalModifier) + } + + override func visitPost(_ node: SubscriptDeclSyntax) { + guard insideActor, + let finalModifier = node.modifiers.first(where: { $0.name.text == "final" }) else { + return + } + addViolation(for: finalModifier) + } + + // Don't descend into nested classes where `final` is meaningful + override func visit(_: ClassDeclSyntax) -> SyntaxVisitorContinueKind { + .skipChildren + } + + private func addViolation(for finalModifier: DeclModifierSyntax) { + let start = finalModifier.positionAfterSkippingLeadingTrivia + let end = finalModifier.endPosition + violations.append( + ReasonedRuleViolation( + position: start, + correction: .init( + start: start, + end: end, + replacement: "" + ) + ) + ) + } + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_07.swift b/Tests/GeneratedTests/GeneratedTests_07.swift index 257050f83f..c160cbc226 100644 --- a/Tests/GeneratedTests/GeneratedTests_07.swift +++ b/Tests/GeneratedTests/GeneratedTests_07.swift @@ -151,8 +151,8 @@ final class RedundantDiscardableLetRuleGeneratedTests: SwiftLintTestCase { } } -final class RedundantNilCoalescingRuleGeneratedTests: SwiftLintTestCase { +final class RedundantFinalActorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { - verifyRule(RedundantNilCoalescingRule.description) + verifyRule(RedundantFinalActorRule.description) } } diff --git a/Tests/GeneratedTests/GeneratedTests_08.swift b/Tests/GeneratedTests/GeneratedTests_08.swift index bb3481ea86..a18c6bdb96 100644 --- a/Tests/GeneratedTests/GeneratedTests_08.swift +++ b/Tests/GeneratedTests/GeneratedTests_08.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class RedundantNilCoalescingRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantNilCoalescingRule.description) + } +} + final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantObjcAttributeRule.description) @@ -150,9 +156,3 @@ final class StrictFilePrivateRuleGeneratedTests: SwiftLintTestCase { verifyRule(StrictFilePrivateRule.description) } } - -final class StrongIBOutletRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(StrongIBOutletRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_09.swift b/Tests/GeneratedTests/GeneratedTests_09.swift index 73827e5186..8195d79e64 100644 --- a/Tests/GeneratedTests/GeneratedTests_09.swift +++ b/Tests/GeneratedTests/GeneratedTests_09.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class StrongIBOutletRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(StrongIBOutletRule.description) + } +} + final class SuperfluousElseRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(SuperfluousElseRule.description) @@ -150,9 +156,3 @@ final class UnneededSynthesizedInitializerRuleGeneratedTests: SwiftLintTestCase verifyRule(UnneededSynthesizedInitializerRule.description) } } - -final class UnneededThrowsRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnneededThrowsRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_10.swift b/Tests/GeneratedTests/GeneratedTests_10.swift index 002ad994e7..b647b6a303 100644 --- a/Tests/GeneratedTests/GeneratedTests_10.swift +++ b/Tests/GeneratedTests/GeneratedTests_10.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class UnneededThrowsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnneededThrowsRule.description) + } +} + final class UnownedVariableCaptureRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnownedVariableCaptureRule.description) diff --git a/Tests/IntegrationTests/Resources/default_rule_configurations.yml b/Tests/IntegrationTests/Resources/default_rule_configurations.yml index 058ad6299b..7987b0c554 100644 --- a/Tests/IntegrationTests/Resources/default_rule_configurations.yml +++ b/Tests/IntegrationTests/Resources/default_rule_configurations.yml @@ -1006,6 +1006,11 @@ redundant_discardable_let: meta: opt-in: false correctable: true +redundant_final_actor: + severity: warning + meta: + opt-in: true + correctable: true redundant_nil_coalescing: severity: warning meta: