Skip to content

Commit

Permalink
Rewrite no_grouping_extension rule with SwiftSyntax (#5328)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelofabri authored Nov 6, 2023
1 parent 91c0240 commit 5987582
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 78 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
[u-abyss](https://github.com/u-abyss)
[#5259](https://github.com/realm/SwiftLint/issues/5259)

* Rewrite `no_grouping_extension` rule using SwiftSyntax.
[Marcelo Fabri](https://github.com/marcelofabri)

#### Bug Fixes

* Fix correction of `explicit_init` rule by keeping significant trivia.
Expand Down
53 changes: 0 additions & 53 deletions Source/SwiftLintBuiltInRules/Helpers/NamespaceCollector.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SourceKittenFramework
import SwiftSyntax

@SwiftSyntaxRule
struct NoGroupingExtensionRule: OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)

Expand All @@ -22,38 +23,100 @@ struct NoGroupingExtensionRule: OptInRule {
)

func validate(file: SwiftLintFile) -> [StyleViolation] {
let collector = NamespaceCollector(dictionary: file.structureDictionary)
let elements = collector.findAllElements(of: [.class, .enum, .struct, .extension])
return Visitor(configuration: configuration, file: file)
.walk(tree: file.syntaxTree) { visitor in
return visitor.extensionDeclarations.compactMap { decl in
guard visitor.typeDeclarations.contains(decl.name) else {
return nil
}

let susceptibleNames = Set(elements.compactMap { $0.kind != .extension ? $0.name : nil })

return elements.compactMap { element in
guard element.kind == .extension, susceptibleNames.contains(element.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(_ node: ActorDeclSyntax) {
typeScope.removeLast()
}

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
pushType(named: node.name.text)
return .visitChildren
}

guard !hasWhereClause(dictionary: element.dictionary, file: file) else {
return nil
override func visitPost(_ node: ClassDeclSyntax) {
typeScope.removeLast()
}

override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
pushType(named: node.name.text)
return .visitChildren
}

override func visitPost(_ node: EnumDeclSyntax) {
typeScope.removeLast()
}

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
pushType(named: node.name.text)
return .visitChildren
}

override func visitPost(_ node: StructDeclSyntax) {
typeScope.removeLast()
}

override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
typeScope.append(node.extendedType.trimmedDescription)

guard node.genericWhereClause == nil else {
return .skipChildren
}

return StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: element.offset))
let decl = ExtensionDeclaration(
name: node.extendedType.trimmedDescription,
position: node.extensionKeyword.positionAfterSkippingLeadingTrivia
)
extensionDeclarations.insert(decl)
return .visitChildren
}
}

private func hasWhereClause(dictionary: SourceKittenDictionary, file: SwiftLintFile) -> Bool {
guard let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength,
let bodyOffset = dictionary.bodyOffset,
case let contents = file.stringView,
case let rangeStart = nameOffset + nameLength,
case let rangeLength = bodyOffset - rangeStart,
let range = contents.byteRangeToNSRange(ByteRange(location: rangeStart, length: rangeLength))
else {
return false
override func visitPost(_ node: ExtensionDeclSyntax) {
typeScope.removeLast()
}

return file.match(pattern: "\\bwhere\\b", with: [.keyword], range: range).isNotEmpty
private func pushType(named name: String) {
typeScope.append(name)
typeDeclarations.insert(typeScope.joined(separator: "."))
}
}
}

0 comments on commit 5987582

Please sign in to comment.