mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
120 lines
4.5 KiB
Swift
120 lines
4.5 KiB
Swift
//
|
|
// PreferCountWhere.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Cal Stephens on 12/7/24.
|
|
// Copyright © 2024 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public extension FormatRule {
|
|
static let preferCountWhere = FormatRule(
|
|
help: "Prefer `count(where:)` over `filter(_:).count`."
|
|
) { formatter in
|
|
// count(where:) was added in Swift 6.0
|
|
guard formatter.options.swiftVersion >= "6.0" else { return }
|
|
|
|
formatter.forEach(.identifier("filter")) { filterIndex, _ in
|
|
guard let nextIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: filterIndex) else { return }
|
|
|
|
// Parse the `filter` call, which takes exactly one closure
|
|
// and is either `filter { ... }` or `filter({ ... })`
|
|
let openParen: Int?
|
|
let startOfClosure: Int
|
|
let endOfClosure: Int
|
|
let closeParen: Int?
|
|
|
|
if formatter.tokens[nextIndex] == .startOfScope("("),
|
|
let startOfClosureIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: nextIndex),
|
|
formatter.tokens[startOfClosureIndex] == .startOfScope("{"),
|
|
let endOfClosureIndex = formatter.endOfScope(at: startOfClosureIndex),
|
|
let tokenAfterClosure = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: endOfClosureIndex),
|
|
formatter.tokens[tokenAfterClosure] == .endOfScope(")")
|
|
{
|
|
openParen = nextIndex
|
|
startOfClosure = startOfClosureIndex
|
|
endOfClosure = endOfClosureIndex
|
|
closeParen = tokenAfterClosure
|
|
}
|
|
|
|
else if formatter.tokens[nextIndex] == .startOfScope("{"),
|
|
let endOfClosureIndex = formatter.endOfScope(at: nextIndex)
|
|
{
|
|
openParen = nil
|
|
startOfClosure = nextIndex
|
|
endOfClosure = endOfClosureIndex
|
|
closeParen = nil
|
|
}
|
|
|
|
else {
|
|
return
|
|
}
|
|
|
|
// Check if there's a `.count` property access after the filter call
|
|
guard let dotIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: closeParen ?? endOfClosure),
|
|
formatter.tokens[dotIndex] == .operator(".", .infix),
|
|
let countIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: dotIndex),
|
|
formatter.tokens[countIndex] == .identifier("count")
|
|
else { return }
|
|
|
|
// Ensure the `.count` is a property access, not a method call.
|
|
if let tokenAfterCount = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: countIndex),
|
|
formatter.tokens[tokenAfterCount].isStartOfScope
|
|
{ return }
|
|
|
|
// Remove the `.count` property access.
|
|
formatter.removeToken(at: countIndex)
|
|
formatter.removeToken(at: dotIndex)
|
|
|
|
// Replace the `filter(_:)` call with `count(where:)`.
|
|
// Since the `where` label provides semantic value,
|
|
// convert to the non-trailing-closure form.
|
|
|
|
// Replace `filter({ ... })` with `count(where: { ... })`.
|
|
if let openParen, let closeParen {
|
|
formatter.replaceToken(at: filterIndex, with: .identifier("count"))
|
|
|
|
formatter.insert(
|
|
[.identifier("where"), .delimiter(":"), .space(" ")],
|
|
at: openParen + 1
|
|
)
|
|
}
|
|
|
|
// Replace `filter { ... }` with `count(where: { ... })`.
|
|
else {
|
|
formatter.insert(.endOfScope(")"), at: endOfClosure + 1)
|
|
|
|
formatter.insert(
|
|
[.startOfScope("("), .identifier("where"), .delimiter(":"), .space(" ")],
|
|
at: startOfClosure
|
|
)
|
|
|
|
if formatter.tokens[filterIndex + 1].isSpace {
|
|
formatter.removeToken(at: filterIndex + 1)
|
|
}
|
|
|
|
formatter.replaceToken(at: filterIndex, with: .identifier("count"))
|
|
}
|
|
}
|
|
} examples: {
|
|
"""
|
|
```diff
|
|
- planets.filter { !$0.moons.isEmpty }.count
|
|
+ planets.count(where: { !$0.moons.isEmpty })
|
|
|
|
- planets.filter { planet in
|
|
- planet.moons.filter { moon in
|
|
- moon.hasAtmosphere
|
|
- }.count > 1
|
|
- }.count
|
|
+ planets.count(where: { planet in
|
|
+ planet.moons.count(where: { moon in
|
|
+ moon.hasAtmosphere
|
|
+ }) > 1
|
|
+ })
|
|
```
|
|
"""
|
|
}
|
|
}
|