mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
123 lines
4.1 KiB
Swift
123 lines
4.1 KiB
Swift
import SwiftSyntax
|
|
|
|
@SwiftSyntaxRule(optIn: true)
|
|
struct NoGroupingExtensionRule: Rule {
|
|
var configuration = SeverityConfiguration<Self>(.warning)
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "no_grouping_extension",
|
|
name: "No Grouping Extension",
|
|
description: "Extensions shouldn't be used to group code within the same source file",
|
|
kind: .idiomatic,
|
|
nonTriggeringExamples: [
|
|
Example("protocol Food {}\nextension Food {}"),
|
|
Example("class Apples {}\nextension Oranges {}"),
|
|
Example("class Box<T> {}\nextension Box where T: Vegetable {}"),
|
|
],
|
|
triggeringExamples: [
|
|
Example("enum Fruit {}\n↓extension Fruit {}"),
|
|
Example("↓extension Tea: Error {}\nstruct Tea {}"),
|
|
Example("class Ham { class Spam {}}\n↓extension Ham.Spam {}"),
|
|
Example("extension External { struct Gotcha {}}\n↓extension External.Gotcha {}"),
|
|
]
|
|
)
|
|
|
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
|
Visitor(configuration: configuration, file: file)
|
|
.walk(tree: file.syntaxTree) { visitor in
|
|
visitor.extensionDeclarations.compactMap { decl in
|
|
guard visitor.typeDeclarations.contains(decl.name) else {
|
|
return nil
|
|
}
|
|
|
|
return ReasonedRuleViolation(position: decl.position)
|
|
}
|
|
}
|
|
.sorted()
|
|
.map { makeViolation(file: file, violation: $0) }
|
|
}
|
|
}
|
|
|
|
private extension NoGroupingExtensionRule {
|
|
struct ExtensionDeclaration: Hashable {
|
|
let name: String
|
|
let position: AbsolutePosition
|
|
}
|
|
|
|
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
|
|
private(set) var typeDeclarations = Set<String>()
|
|
private var typeScope: [String] = []
|
|
private(set) var extensionDeclarations = Set<ExtensionDeclaration>()
|
|
|
|
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] {
|
|
[
|
|
ProtocolDeclSyntax.self,
|
|
FunctionDeclSyntax.self,
|
|
VariableDeclSyntax.self,
|
|
InitializerDeclSyntax.self,
|
|
SubscriptDeclSyntax.self,
|
|
]
|
|
}
|
|
|
|
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
pushType(named: node.name.text)
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: ActorDeclSyntax) {
|
|
typeScope.removeLast()
|
|
}
|
|
|
|
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
pushType(named: node.name.text)
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: ClassDeclSyntax) {
|
|
typeScope.removeLast()
|
|
}
|
|
|
|
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
pushType(named: node.name.text)
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: EnumDeclSyntax) {
|
|
typeScope.removeLast()
|
|
}
|
|
|
|
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
pushType(named: node.name.text)
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: StructDeclSyntax) {
|
|
typeScope.removeLast()
|
|
}
|
|
|
|
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
typeScope.append(node.extendedType.trimmedDescription)
|
|
|
|
guard node.genericWhereClause == nil else {
|
|
return .skipChildren
|
|
}
|
|
|
|
let decl = ExtensionDeclaration(
|
|
name: node.extendedType.trimmedDescription,
|
|
position: node.extensionKeyword.positionAfterSkippingLeadingTrivia
|
|
)
|
|
extensionDeclarations.insert(decl)
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: ExtensionDeclSyntax) {
|
|
typeScope.removeLast()
|
|
}
|
|
|
|
private func pushType(named name: String) {
|
|
typeScope.append(name)
|
|
typeDeclarations.insert(typeScope.joined(separator: "."))
|
|
}
|
|
}
|
|
}
|