Files
2025-07-12 09:41:00 -04:00

224 lines
8.6 KiB
Swift

import SwiftLintCore
import SwiftSyntax
@SwiftSyntaxRule(foldExpressions: true, explicitRewriter: true, optIn: true)
struct EmptyCountRule: Rule {
var configuration = EmptyCountConfiguration()
static let description = RuleDescription(
identifier: "empty_count",
name: "Empty Count",
description: "Prefer checking `isEmpty` over comparing `count` to zero",
kind: .performance,
nonTriggeringExamples: [
Example("var count = 0"),
Example("[Int]().isEmpty"),
Example("[Int]().count > 1"),
Example("[Int]().count == 1"),
Example("[Int]().count == 0xff"),
Example("[Int]().count == 0b01"),
Example("[Int]().count == 0o07"),
Example("discount == 0"),
Example("order.discount == 0"),
Example("let rule = #Rule(Tips.Event(id: \"someTips\")) { $0.donations.count == 0 }"),
Example("#Rule(param1: \"param1\")", excludeFromDocumentation: true),
],
triggeringExamples: [
Example("[Int]().↓count == 0"),
Example("0 == [Int]().↓count"),
Example("[Int]().↓count==0"),
Example("[Int]().↓count > 0"),
Example("[Int]().↓count != 0"),
Example("[Int]().↓count == 0x0"),
Example("[Int]().↓count == 0x00_00"),
Example("[Int]().↓count == 0b00"),
Example("[Int]().↓count == 0o00"),
Example("↓count == 0"),
Example("#ExampleMacro { $0.list.↓count == 0 }"),
Example("#Rule { $0.donations.↓count == 0 }", excludeFromDocumentation: true),
Example(
"#Rule(param1: \"param1\", param2: \"param2\") { $0.donations.↓count == 0 }",
excludeFromDocumentation: true
),
Example(
"#Rule(param1: \"param1\") { $0.donations.↓count == 0 } closure2: { doSomething() }",
excludeFromDocumentation: true
),
Example("#Rule(param1: \"param1\") { return $0.donations.↓count == 0 }", excludeFromDocumentation: true),
Example("""
#Rule(param1: "param1") {
doSomething()
return $0.donations.↓count == 0
}
""", excludeFromDocumentation: true),
],
corrections: [
Example("[].↓count == 0"):
Example("[].isEmpty"),
Example("0 == [].↓count"):
Example("[].isEmpty"),
Example("[Int]().↓count == 0"):
Example("[Int]().isEmpty"),
Example("0 == [Int]().↓count"):
Example("[Int]().isEmpty"),
Example("[Int]().↓count==0"):
Example("[Int]().isEmpty"),
Example("[Int]().↓count > 0"):
Example("![Int]().isEmpty"),
Example("[Int]().↓count != 0"):
Example("![Int]().isEmpty"),
Example("[Int]().↓count == 0x0"):
Example("[Int]().isEmpty"),
Example("[Int]().↓count == 0x00_00"):
Example("[Int]().isEmpty"),
Example("[Int]().↓count == 0b00"):
Example("[Int]().isEmpty"),
Example("[Int]().↓count == 0o00"):
Example("[Int]().isEmpty"),
Example("↓count == 0"):
Example("isEmpty"),
Example("↓count == 0 && [Int]().↓count == 0o00"):
Example("isEmpty && [Int]().isEmpty"),
Example("[Int]().count != 3 && [Int]().↓count != 0 || ↓count == 0 && [Int]().count > 2"):
Example("[Int]().count != 3 && ![Int]().isEmpty || isEmpty && [Int]().count > 2"),
Example("#ExampleMacro { $0.list.↓count == 0 }"):
Example("#ExampleMacro { $0.list.isEmpty }"),
Example("#Rule(param1: \"param1\") { return $0.donations.↓count == 0 }"):
Example("#Rule(param1: \"param1\") { return $0.donations.isEmpty }"),
]
)
}
private extension EmptyCountRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: InfixOperatorExprSyntax) {
guard let binaryOperator = node.binaryOperator, binaryOperator.isComparison else {
return
}
if let (_, position) = node.countNodeAndPosition(onlyAfterDot: configuration.onlyAfterDot) {
violations.append(position)
}
}
override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
node.isTipsRuleMacro ? .skipChildren : .visitChildren
}
}
final class Rewriter: ViolationsSyntaxRewriter<ConfigurationType> {
override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax {
guard let binaryOperator = node.binaryOperator, binaryOperator.isComparison else {
return super.visit(node)
}
if let (count, _) = node.countNodeAndPosition(onlyAfterDot: configuration.onlyAfterDot) {
let newNode =
if let count = count.as(MemberAccessExprSyntax.self) {
ExprSyntax(count.with(\.declName.baseName, "isEmpty").trimmed)
} else {
ExprSyntax(count.as(DeclReferenceExprSyntax.self)?.with(\.baseName, "isEmpty").trimmed)
}
guard let newNode else {
return super.visit(node)
}
numberOfCorrections += 1
return
if ["!=", "<", ">"].contains(binaryOperator) {
newNode.negated
.withTrivia(from: node)
} else {
newNode
.withTrivia(from: node)
}
}
return super.visit(node)
}
override func visit(_ node: MacroExpansionExprSyntax) -> ExprSyntax {
if node.isTipsRuleMacro {
ExprSyntax(node)
} else {
super.visit(node)
}
}
}
}
private extension ExprSyntax {
func countCallPosition(onlyAfterDot: Bool) -> AbsolutePosition? {
if let expr = self.as(MemberAccessExprSyntax.self) {
if expr.declName.argumentNames == nil, expr.declName.baseName.tokenKind == .identifier("count") {
return expr.declName.baseName.positionAfterSkippingLeadingTrivia
}
return nil
}
if !onlyAfterDot, let expr = self.as(DeclReferenceExprSyntax.self) {
return expr.baseName.tokenKind == .identifier("count") ? expr.positionAfterSkippingLeadingTrivia : nil
}
return nil
}
}
private extension TokenSyntax {
var binaryOperator: String? {
switch tokenKind {
case .binaryOperator(let str):
return str
default:
return nil
}
}
}
private extension MacroExpansionExprSyntax {
var isTipsRuleMacro: Bool {
macroName.text == "Rule" &&
additionalTrailingClosures.isEmpty &&
arguments.count == 1 &&
trailingClosure.map { $0.statements.onlyElement?.item.is(ReturnStmtSyntax.self) == false } ?? false
}
}
private extension ExprSyntaxProtocol {
var negated: ExprSyntax {
ExprSyntax(PrefixOperatorExprSyntax(operator: .prefixOperator("!"), expression: self))
}
}
private extension SyntaxProtocol {
func withTrivia(from node: some SyntaxProtocol) -> Self {
self
.with(\.leadingTrivia, node.leadingTrivia)
.with(\.trailingTrivia, node.trailingTrivia)
}
}
private extension InfixOperatorExprSyntax {
func countNodeAndPosition(onlyAfterDot: Bool) -> (ExprSyntax, AbsolutePosition)? {
if let intExpr = rightOperand.as(IntegerLiteralExprSyntax.self), intExpr.isZero,
let position = leftOperand.countCallPosition(onlyAfterDot: onlyAfterDot) {
return (leftOperand, position)
}
if let intExpr = leftOperand.as(IntegerLiteralExprSyntax.self), intExpr.isZero,
let position = rightOperand.countCallPosition(onlyAfterDot: onlyAfterDot) {
return (rightOperand, position)
}
return nil
}
var binaryOperator: String? {
self.operator.as(BinaryOperatorExprSyntax.self)?.operator.binaryOperator
}
}
private extension String {
private static let operators: Set = ["==", "!=", ">", ">=", "<", "<="]
var isComparison: Bool {
String.operators.contains(self)
}
}