import SwiftSyntax @SwiftSyntaxRule(optIn: true) struct StaticOperatorRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( identifier: "static_operator", name: "Static Operator", description: "Operators should be declared as static functions, not free functions", kind: .idiomatic, nonTriggeringExamples: [ Example(""" class A: Equatable { static func == (lhs: A, rhs: A) -> Bool { return false } """), Example(""" class A: Equatable { static func == (lhs: A, rhs: A) -> Bool { return false } """), Example(""" public extension Array where Element == Rule { static func == (lhs: Array, rhs: Array) -> Bool { if lhs.count != rhs.count { return false } return !zip(lhs, rhs).contains { !$0.0.isEqualTo($0.1) } } } """), Example(""" private extension Optional where Wrapped: Comparable { static func < (lhs: Optional, rhs: Optional) -> Bool { switch (lhs, rhs) { case let (lhs?, rhs?): return lhs < rhs case (nil, _?): return true default: return false } } } """), ], triggeringExamples: [ Example(""" ↓func == (lhs: A, rhs: A) -> Bool { return false } """), Example(""" ↓func == (lhs: A, rhs: A) -> Bool { return false } """), Example(""" ↓func == (lhs: [Rule], rhs: [Rule]) -> Bool { if lhs.count != rhs.count { return false } return !zip(lhs, rhs).contains { !$0.0.isEqualTo($0.1) } } """), Example(""" private ↓func < (lhs: T?, rhs: T?) -> Bool { switch (lhs, rhs) { case let (lhs?, rhs?): return lhs < rhs case (nil, _?): return true default: return false } } """), ] ) } private extension StaticOperatorRule { final class Visitor: ViolationsSyntaxVisitor { override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } override func visitPost(_ node: FunctionDeclSyntax) { if node.isFreeFunction, node.isOperator { violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia) } } } } private extension FunctionDeclSyntax { var isFreeFunction: Bool { parent?.is(CodeBlockItemSyntax.self) ?? false } var isOperator: Bool { switch name.tokenKind { case .binaryOperator: return true default: return false } } }