import SwiftSyntax @SwiftSyntaxRule struct SortedEnumCasesRule: OptInRule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( identifier: "sorted_enum_cases", name: "Sorted Enum Cases", description: "Enum cases should be sorted", kind: .style, nonTriggeringExamples: [ Example(""" enum foo { case a case b case c } """), Example(""" enum foo { case example case exBoyfriend } """), Example(""" enum foo { case a case B case c } """), Example(""" enum foo { case a, b, c } """), Example(""" enum foo { case a case b, c } """), Example(""" enum foo { case a(foo: Foo) case b(String), c } """), Example(""" enum foo { case a case b, C, d } """), Example(""" @frozen enum foo { case b case a case c, f, d } """) ], triggeringExamples: [ Example(""" enum foo { ↓case b ↓case a case c } """), Example(""" enum foo { ↓case B ↓case a case c } """), Example(""" enum foo { case ↓b, ↓a, c } """), Example(""" enum foo { case ↓B, ↓a, c } """), Example(""" enum foo { ↓case b, c ↓case a } """), Example(""" enum foo { case a case b, ↓d, ↓c } """), Example(""" enum foo { case a(foo: Foo) case ↓c, ↓b(String) } """) ] ) } private extension SortedEnumCasesRule { final class Visitor: ViolationsSyntaxVisitor { override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .allExcept(EnumDeclSyntax.self) } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { guard !node.attributes.contains(attributeNamed: "frozen") else { return .skipChildren } let cases = node.memberBlock.members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) } let sortedCases = cases .sorted(by: { let lhs = $0.elements.first!.name.text let rhs = $1.elements.first!.name.text return lhs.caseInsensitiveCompare(rhs) == .orderedAscending }) zip(sortedCases, cases).forEach { sortedCase, currentCase in if sortedCase.elements.first?.name.text != currentCase.elements.first?.name.text { violations.append(currentCase.positionAfterSkippingLeadingTrivia) } } return .visitChildren } override func visitPost(_ node: EnumCaseDeclSyntax) { let sortedElements = node.elements.sorted(by: { $0.name.text.caseInsensitiveCompare($1.name.text) == .orderedAscending }) zip(sortedElements, node.elements).forEach { sortedElement, currentElement in if sortedElement.name.text != currentElement.name.text { violations.append(currentElement.positionAfterSkippingLeadingTrivia) } } } } }