Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift
2025-10-08 08:12:05 +02:00

127 lines
4.3 KiB
Swift

import SwiftSyntax
import SwiftSyntaxBuilder
@SwiftSyntaxRule(explicitRewriter: true, optIn: true)
struct PreferZeroOverExplicitInitRule: Rule {
var configuration = SeverityConfiguration<Self>(.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<ConfigurationType> {
override func visitPost(_ node: FunctionCallExprSyntax) {
if node.hasViolation {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
final class Rewriter: ViolationsSyntaxRewriter<ConfigurationType> {
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
}
}
}