import SwiftSyntax @SwiftSyntaxRule(explicitRewriter: true, optIn: true) struct PreferZeroOverExplicitInitRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( identifier: "prefer_zero_over_explicit_init", name: "Prefer Zero Over Explicit Init", description: "Prefer `.zero` over explicit init with zero parameters (e.g. `CGPoint(x: 0, y: 0)`)", kind: .idiomatic, nonTriggeringExamples: [ Example("CGRect(x: 0, y: 0, width: 0, height: 1)"), Example("CGPoint(x: 0, y: -1)"), Example("CGSize(width: 2, height: 4)"), Example("CGVector(dx: -5, dy: 0)"), Example("UIEdgeInsets(top: 0, left: 1, bottom: 0, right: 1)"), ], triggeringExamples: [ Example("↓CGPoint(x: 0, y: 0)"), Example("↓CGPoint(x: 0.000000, y: 0)"), Example("↓CGPoint(x: 0.000000, y: 0.000)"), Example("↓CGRect(x: 0, y: 0, width: 0, height: 0)"), Example("↓CGSize(width: 0, height: 0)"), Example("↓CGVector(dx: 0, dy: 0)"), Example("↓UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)"), ], corrections: [ Example("↓CGPoint(x: 0, y: 0)"): Example("CGPoint.zero"), Example("(↓CGPoint(x: 0, y: 0))"): Example("(CGPoint.zero)"), Example("↓CGRect(x: 0, y: 0, width: 0, height: 0)"): Example("CGRect.zero"), Example("↓CGSize(width: 0, height: 0.000)"): Example("CGSize.zero"), Example("↓CGVector(dx: 0, dy: 0)"): Example("CGVector.zero"), Example("↓UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)"): Example("UIEdgeInsets.zero"), ] ) } private extension PreferZeroOverExplicitInitRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: FunctionCallExprSyntax) { if node.hasViolation { violations.append(node.positionAfterSkippingLeadingTrivia) } } } final class Rewriter: ViolationsSyntaxRewriter { override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax { guard node.hasViolation, let name = node.name else { return super.visit(node) } numberOfCorrections += 1 let newNode = MemberAccessExprSyntax(name: "zero") .with(\.base, "\(raw: name)") .with(\.leadingTrivia, node.leadingTrivia) .with(\.trailingTrivia, node.trailingTrivia) return super.visit(newNode) } } } private extension FunctionCallExprSyntax { var hasViolation: Bool { isCGPointZeroCall || isCGSizeCall || isCGRectCall || isCGVectorCall || isUIEdgeInsetsCall } var isCGPointZeroCall: Bool { name == "CGPoint" && argumentNames == ["x", "y"] && argumentsAreAllZero } var isCGSizeCall: Bool { name == "CGSize" && argumentNames == ["width", "height"] && argumentsAreAllZero } var isCGRectCall: Bool { name == "CGRect" && argumentNames == ["x", "y", "width", "height"] && argumentsAreAllZero } var isCGVectorCall: Bool { name == "CGVector" && argumentNames == ["dx", "dy"] && argumentsAreAllZero } var isUIEdgeInsetsCall: Bool { name == "UIEdgeInsets" && argumentNames == ["top", "left", "bottom", "right"] && argumentsAreAllZero } var name: String? { guard let expr = calledExpression.as(DeclReferenceExprSyntax.self) else { return nil } return expr.baseName.text } var argumentNames: [String?] { arguments.map(\.label?.text) } var argumentsAreAllZero: Bool { arguments.allSatisfy { arg in if let intExpr = arg.expression.as(IntegerLiteralExprSyntax.self) { return intExpr.isZero } if let floatExpr = arg.expression.as(FloatLiteralExprSyntax.self) { return floatExpr.isZero } return false } } }