Skip to content

Commit

Permalink
Make empty_count rule correctable (realm#5409)
Browse files Browse the repository at this point in the history
  • Loading branch information
KS1019 authored Jan 29, 2024
1 parent 62a3dda commit f0c1780
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@
that contain one of the patterns.
[kasrababaei](https://github.com/kasrababaei)

* Make `empty_count` auto-correctable.
[KS1019](https://github.com/KS1019/)

#### Bug Fixes

* Silence `discarded_notification_center_observer` rule in closures. Furthermore,
Expand Down
128 changes: 116 additions & 12 deletions Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import SwiftLintCore
import SwiftSyntax

@SwiftSyntaxRule(foldExpressions: true)
struct EmptyCountRule: OptInRule {
struct EmptyCountRule: SwiftSyntaxCorrectableRule, OptInRule {
var configuration = EmptyCountConfiguration()

static let description = RuleDescription(
Expand Down Expand Up @@ -31,31 +32,95 @@ struct EmptyCountRule: OptInRule {
Example("[Int]().↓count == 0b00"),
Example("[Int]().↓count == 0o00"),
Example("↓count == 0")
],
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")
]
)

func makeRewriter(file: SwiftLintFile) -> (some ViolationsSyntaxRewriter)? {
Rewriter(
configuration: configuration,
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}

private extension EmptyCountRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
private let operators: Set = ["==", "!=", ">", ">=", "<", "<="]

override func visitPost(_ node: InfixOperatorExprSyntax) {
guard let operatorNode = node.operator.as(BinaryOperatorExprSyntax.self),
let binaryOperator = operatorNode.operator.binaryOperator,
operators.contains(binaryOperator) else {
guard let binaryOperator = node.binaryOperator, binaryOperator.isComparison else {
return
}

if let intExpr = node.rightOperand.as(IntegerLiteralExprSyntax.self), intExpr.isZero,
let position = node.leftOperand.countCallPosition(onlyAfterDot: configuration.onlyAfterDot) {
if let (_, position) = node.countNodeAndPosition(onlyAfterDot: configuration.onlyAfterDot) {
violations.append(position)
return
}
}
}

if let intExpr = node.leftOperand.as(IntegerLiteralExprSyntax.self), intExpr.isZero,
let position = node.rightOperand.countCallPosition(onlyAfterDot: configuration.onlyAfterDot) {
violations.append(position)
final class Rewriter: ViolationsSyntaxRewriter {
private let configuration: EmptyCountConfiguration

init(configuration: EmptyCountConfiguration,
locationConverter: SourceLocationConverter,
disabledRegions: [SourceRange]) {
self.configuration = configuration
super.init(locationConverter: locationConverter, disabledRegions: disabledRegions)
}

override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax {
guard let binaryOperator = node.binaryOperator, binaryOperator.isComparison else {
return super.visit(node)
}

if let (count, position) = node.countNodeAndPosition(onlyAfterDot: configuration.onlyAfterDot) {
let newNode =
if let count = count.as(MemberAccessExprSyntax.self) {
count.with(\.declName.baseName, "isEmpty").trimmed.as(ExprSyntax.self)
} else {
count.as(DeclReferenceExprSyntax.self)?.with(\.baseName, "isEmpty").trimmed.as(ExprSyntax.self)
}
guard let newNode else { return super.visit(node) }
correctionPositions.append(position)
return
if ["!=", "<", ">"].contains(binaryOperator) {
newNode.negated
.withTrivia(from: node)
} else {
newNode
.withTrivia(from: node)
}
} else {
return super.visit(node)
}
}
}
Expand Down Expand Up @@ -89,3 +154,42 @@ private extension TokenSyntax {
}
}
}

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)
} else if let intExpr = leftOperand.as(IntegerLiteralExprSyntax.self), intExpr.isZero,
let position = rightOperand.countCallPosition(onlyAfterDot: onlyAfterDot) {
return (rightOperand, position)
} else {
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)
}
}
32 changes: 32 additions & 0 deletions Tests/SwiftLintFrameworkTests/EmptyCountRuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,41 @@ class EmptyCountRuleTests: SwiftLintTestCase {
Example("[Int]().↓count == 0o00\n")
]

let 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("count == 0"),
Example("count == 0 && [Int]().↓count == 0o00"):
Example("count == 0 && [Int]().isEmpty"),
Example("[Int]().count != 3 && [Int]().↓count != 0 || count == 0 && [Int]().count > 2"):
Example("[Int]().count != 3 && ![Int]().isEmpty || count == 0 && [Int]().count > 2")
]

let description = EmptyCountRule.description
.with(triggeringExamples: triggeringExamples)
.with(nonTriggeringExamples: nonTriggeringExamples)
.with(corrections: corrections)

verifyRule(description, ruleConfiguration: ["only_after_dot": true])
}
Expand Down

0 comments on commit f0c1780

Please sign in to comment.