From 7d6f6cfa17ee484bc3cb288ddcba0a4cfcf64a80 Mon Sep 17 00:00:00 2001 From: Muhammad Zeeshan Date: Sat, 9 Dec 2023 15:11:25 +0500 Subject: [PATCH] Add new `one_declaration_per_file` rule (#5377) --- .swiftlint.yml | 1 + CHANGELOG.md | 6 ++ .../Models/BuiltInRules.swift | 1 + .../Idiomatic/OneDelarationPerFileRule.swift | 75 +++++++++++++++++++ Tests/GeneratedTests/GeneratedTests.swift | 6 ++ 5 files changed, 89 insertions(+) create mode 100644 Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDelarationPerFileRule.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 965ef4afd..d58e73904 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -38,6 +38,7 @@ disabled_rules: - no_fallthrough_only - no_grouping_extension - no_magic_numbers + - one_declaration_per_file - prefer_nimble - prefer_self_in_static_references - prefixed_toplevel_constant diff --git a/CHANGELOG.md b/CHANGELOG.md index 599240bfb..6502094eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ #### Enhancements +* Add new `one_declaration_per_file` rule that allows only a + single class/struct/enum/protocol declaration per file. + Extensions are an exception; more than one is allowed. + [Muhammad Zeeshan](https://github.com/mzeeshanid) + [#2802](https://github.com/realm/SwiftLint/issues/2802) + * Rewrite the following rules with SwiftSyntax: * `identifier_name` * `let_var_whitespace` diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 63749cbd0..4615c17ca 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -135,6 +135,7 @@ public let builtInRules: [any Rule.Type] = [ NotificationCenterDetachmentRule.self, NumberSeparatorRule.self, ObjectLiteralRule.self, + OneDelarationPerFileRule.self, OpeningBraceRule.self, OperatorFunctionWhitespaceRule.self, OperatorUsageWhitespaceRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDelarationPerFileRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDelarationPerFileRule.swift new file mode 100644 index 000000000..82b918e00 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDelarationPerFileRule.swift @@ -0,0 +1,75 @@ +import SwiftSyntax + +@SwiftSyntaxRule +struct OneDelarationPerFileRule: OptInRule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "one_declaration_per_file", + name: "One Declaration per File", + description: "Only a single declaration is allowed in a file", + kind: .idiomatic, + nonTriggeringExamples: [ + Example(""" + class Foo {} + """), + Example(""" + class Foo {} + extension Foo {} + """), + Example(""" + struct S { + struct N {} + } + """) + ], + triggeringExamples: [ + Example(""" + class Foo {} + ↓class Bar {} + """), + Example(""" + protocol Foo {} + ↓enum Bar {} + """), + Example(""" + struct Foo {} + ↓struct Bar {} + """) + ] + ) +} + +private extension OneDelarationPerFileRule { + final class Visitor: ViolationsSyntaxVisitor { + private var declarationVisited = false + override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } + + override func visitPost(_ node: ActorDeclSyntax) { + appendViolationIfNeeded(node: node.actorKeyword) + } + + override func visitPost(_ node: ClassDeclSyntax) { + appendViolationIfNeeded(node: node.classKeyword) + } + + override func visitPost(_ node: StructDeclSyntax) { + appendViolationIfNeeded(node: node.structKeyword) + } + + override func visitPost(_ node: EnumDeclSyntax) { + appendViolationIfNeeded(node: node.enumKeyword) + } + + override func visitPost(_ node: ProtocolDeclSyntax) { + appendViolationIfNeeded(node: node.protocolKeyword) + } + + func appendViolationIfNeeded(node: TokenSyntax) { + if declarationVisited { + violations.append(node.positionAfterSkippingLeadingTrivia) + } + declarationVisited = true + } + } +} diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index 919fd63dd..078c6f614 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -800,6 +800,12 @@ class ObjectLiteralRuleGeneratedTests: SwiftLintTestCase { } } +class OneDelarationPerFileRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OneDelarationPerFileRule.description) + } +} + class OpeningBraceRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(OpeningBraceRule.description)