mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
224 lines
8.6 KiB
Swift
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)
|
|
}
|
|
}
|