diff --git a/CHANGELOG.md b/CHANGELOG.md index 876ea03743..6ce69b66db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Source/SwiftLintBuiltInRules/Helpers/NamespaceCollector.swift b/Source/SwiftLintBuiltInRules/Helpers/NamespaceCollector.swift deleted file mode 100644 index 654ff1837a..0000000000 --- a/Source/SwiftLintBuiltInRules/Helpers/NamespaceCollector.swift +++ /dev/null @@ -1,53 +0,0 @@ -import SourceKittenFramework - -struct NamespaceCollector { - struct Element { - let name: String - let kind: SwiftDeclarationKind - let offset: ByteCount - let dictionary: SourceKittenDictionary - - init?(dictionary: SourceKittenDictionary, namespace: [String]) { - guard let name = dictionary.name, - let kind = dictionary.declarationKind, - let offset = dictionary.offset else { - return nil - } - - self.name = (namespace + [name]).joined(separator: ".") - self.kind = kind - self.offset = offset - self.dictionary = dictionary - } - } - - private let dictionary: SourceKittenDictionary - - init(dictionary: SourceKittenDictionary) { - self.dictionary = dictionary - } - - func findAllElements(of types: Set, - namespace: [String] = []) -> [Element] { - return findAllElements(in: dictionary, of: types, namespace: namespace) - } - - private func findAllElements(in dictionary: SourceKittenDictionary, - of types: Set, - namespace: [String] = []) -> [Element] { - return dictionary.substructure.flatMap { subDict -> [Element] in - var elements: [Element] = [] - guard let element = Element(dictionary: subDict, namespace: namespace) else { - return elements - } - - if types.contains(element.kind) { - elements.append(element) - } - - elements += findAllElements(in: subDict, of: types, namespace: [element.name]) - - return elements - } - } -} diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoGroupingExtensionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoGroupingExtensionRule.swift index e539896b07..9b0b45ac03 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoGroupingExtensionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/NoGroupingExtensionRule.swift @@ -1,5 +1,6 @@ -import SourceKittenFramework +import SwiftSyntax +@SwiftSyntaxRule struct NoGroupingExtensionRule: OptInRule { var configuration = SeverityConfiguration(.warning) @@ -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 { + private(set) var typeDeclarations = Set() + private var typeScope: [String] = [] + private(set) var extensionDeclarations = Set() + + 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: ".")) + } } }