Skip to content

Commit 8fb103d

Browse files
committed
Add new opt-in non_final_class rule
1 parent 0004642 commit 8fb103d

File tree

5 files changed

+96
-0
lines changed

5 files changed

+96
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
[kapitoshka438](https://github.com/kapitoshka438)
3131
[#3723](https://github.com/realm/SwiftLint/issues/3723)
3232

33+
* Add new `non_final_class` opt-in rule that triggers when a class isn't `final`.
34+
[JaviSoto](https://github.com/JaviSoto)
35+
3336
### Bug Fixes
3437

3538
* Fix issue referencing the Tests package from another Bazel workspace.

Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ public let builtInRules: [any Rule.Type] = [
133133
NoGroupingExtensionRule.self,
134134
NoMagicNumbersRule.self,
135135
NoSpaceInMethodCallRule.self,
136+
NonFinalClassRule.self,
136137
NonOptionalStringDataConversionRule.self,
137138
NonOverridableClassDeclarationRule.self,
138139
NotificationCenterDetachmentRule.self,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxBuilder
3+
4+
@SwiftSyntaxRule(correctable: true, optIn: true)
5+
struct NonFinalClassRule: Rule {
6+
var configuration = SeverityConfiguration<Self>(.warning)
7+
8+
static let description = RuleDescription(
9+
identifier: "non_final_class",
10+
name: "Non-Final Class",
11+
description: "Classes should be marked as `final` unless they are explicitly `open`.",
12+
kind: .idiomatic,
13+
nonTriggeringExamples: [
14+
Example("final class MyClass {}"),
15+
Example("open class MyClass {}"),
16+
Example("public final class MyClass {}"),
17+
],
18+
triggeringExamples: [
19+
Example("class MyClass {}"),
20+
Example("public class MyClass {}"),
21+
],
22+
corrections: [
23+
Example("class MyClass {}"): Example("final class MyClass {}"),
24+
Example("public class MyClass {}"): Example("public final class MyClass {}"),
25+
]
26+
)
27+
28+
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor<ConfigurationType>? {
29+
Visitor(configuration: configuration, file: file)
30+
}
31+
32+
private final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
33+
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
34+
let modifiers = node.modifiers
35+
if !modifiers.contains(where: { $0.name.text == "final" }) && !modifiers.contains(where: { $0.name.text == "open" }) {
36+
let classToken = node.classKeyword
37+
violations.append(.init(
38+
position: classToken.positionAfterSkippingLeadingTrivia,
39+
reason: "Classes should be marked as `final` unless they are explicitly `open`",
40+
correction: .init(
41+
start: classToken.positionAfterSkippingLeadingTrivia,
42+
end: classToken.positionAfterSkippingLeadingTrivia,
43+
replacement: "final "
44+
)
45+
))
46+
}
47+
return .visitChildren
48+
}
49+
}
50+
}

Tests/GeneratedTests/GeneratedTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,12 @@ final class NoSpaceInMethodCallRuleGeneratedTests: SwiftLintTestCase {
787787
}
788788
}
789789

790+
final class NonFinalClassRuleGeneratedTests: SwiftLintTestCase {
791+
func testWithDefaultConfiguration() {
792+
verifyRule(NonFinalClassRule.description)
793+
}
794+
}
795+
790796
final class NonOptionalStringDataConversionRuleGeneratedTests: SwiftLintTestCase {
791797
func testWithDefaultConfiguration() {
792798
verifyRule(NonOptionalStringDataConversionRule.description)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@testable import SwiftLintFramework
2+
import XCTest
3+
4+
final class NonFinalClassRuleTests: XCTestCase {
5+
func testNonTriggeringExamples() {
6+
let examples = [
7+
"final class MyClass {}",
8+
"open class MyClass {}",
9+
"public final class MyClass {}",
10+
]
11+
examples.forEach { example in
12+
verifyRule(NonFinalClassRule.description, string: example, expected: [])
13+
}
14+
}
15+
16+
func testTriggeringExamples() {
17+
let examples = [
18+
"class MyClass {}",
19+
"public class MyClass {}",
20+
]
21+
examples.forEach { example in
22+
let violations = violationsForRule(NonFinalClassRule.description, string: example)
23+
XCTAssertEqual(violations.count, 1, "Expected one violation in: \(example)")
24+
XCTAssertEqual(violations.first?.reason, "Classes should be marked as `final` unless they are explicitly `open`")
25+
}
26+
}
27+
28+
func testCorrections() {
29+
assertCorrection(NonFinalClassRule.description,
30+
input: "class MyClass {}",
31+
expected: "final class MyClass {}")
32+
assertCorrection(NonFinalClassRule.description,
33+
input: "public class MyClass {}",
34+
expected: "public final class MyClass {}")
35+
}
36+
}

0 commit comments

Comments
 (0)