mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
Add preferCountWhere rule (#1939)
This commit is contained in:
committed by
Cal Stephens
parent
e683fb14ed
commit
4aa152bc3e
@@ -37,6 +37,7 @@
|
||||
* [modifierOrder](#modifierOrder)
|
||||
* [numberFormatting](#numberFormatting)
|
||||
* [opaqueGenericParameters](#opaqueGenericParameters)
|
||||
* [preferCountWhere](#preferCountWhere)
|
||||
* [preferForLoop](#preferForLoop)
|
||||
* [preferKeyPath](#preferKeyPath)
|
||||
* [redundantBackticks](#redundantBackticks)
|
||||
@@ -1655,6 +1656,32 @@ Without this declaration, only functions will be reordered, while properties wil
|
||||
</details>
|
||||
<br/>
|
||||
|
||||
## preferCountWhere
|
||||
|
||||
Prefer `count(where:)` over `filter(_:).count`.
|
||||
|
||||
<details>
|
||||
<summary>Examples</summary>
|
||||
|
||||
```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
|
||||
+ })
|
||||
```
|
||||
|
||||
</details>
|
||||
<br/>
|
||||
|
||||
## preferForLoop
|
||||
|
||||
Convert functional `forEach` calls to for loops.
|
||||
|
||||
@@ -56,6 +56,7 @@ let ruleRegistry: [String: FormatRule] = [
|
||||
"numberFormatting": .numberFormatting,
|
||||
"opaqueGenericParameters": .opaqueGenericParameters,
|
||||
"organizeDeclarations": .organizeDeclarations,
|
||||
"preferCountWhere": .preferCountWhere,
|
||||
"preferForLoop": .preferForLoop,
|
||||
"preferKeyPath": .preferKeyPath,
|
||||
"privateStateVariables": .privateStateVariables,
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// 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 = openParen, let closeParen = 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
|
||||
+ })
|
||||
```
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -631,6 +631,11 @@
|
||||
2E9DE5082C95F9A1000FEDF8 /* FileMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9DE5052C95F9A1000FEDF8 /* FileMacro.swift */; };
|
||||
2E9DE5092C95F9A1000FEDF8 /* FileMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9DE5052C95F9A1000FEDF8 /* FileMacro.swift */; };
|
||||
2E9DE50F2C95FBB6000FEDF8 /* FileMacroTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9DE50A2C95FB4F000FEDF8 /* FileMacroTests.swift */; };
|
||||
2EA8C3702D04F51A00843B42 /* PreferCountWhere.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA8C36F2D04F51A00843B42 /* PreferCountWhere.swift */; };
|
||||
2EA8C3712D04F51A00843B42 /* PreferCountWhere.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA8C36F2D04F51A00843B42 /* PreferCountWhere.swift */; };
|
||||
2EA8C3722D04F51A00843B42 /* PreferCountWhere.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA8C36F2D04F51A00843B42 /* PreferCountWhere.swift */; };
|
||||
2EA8C3732D04F51A00843B42 /* PreferCountWhere.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA8C36F2D04F51A00843B42 /* PreferCountWhere.swift */; };
|
||||
2EA8C3752D05282600843B42 /* PreferCountWhereTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA8C3742D05282600843B42 /* PreferCountWhereTests.swift */; };
|
||||
2EDC9DDA2CCE9A7C0085DBE2 /* DeclarationV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDC9DD92CCE9A7C0085DBE2 /* DeclarationV2.swift */; };
|
||||
2EDC9DDB2CCE9A7C0085DBE2 /* DeclarationV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDC9DD92CCE9A7C0085DBE2 /* DeclarationV2.swift */; };
|
||||
2EDC9DDC2CCE9A7C0085DBE2 /* DeclarationV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDC9DD92CCE9A7C0085DBE2 /* DeclarationV2.swift */; };
|
||||
@@ -1022,6 +1027,8 @@
|
||||
2E8DE6F72C57FEB30032BF25 /* WrapSingleLineCommentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WrapSingleLineCommentsTests.swift; sourceTree = "<group>"; };
|
||||
2E9DE5052C95F9A1000FEDF8 /* FileMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMacro.swift; sourceTree = "<group>"; };
|
||||
2E9DE50A2C95FB4F000FEDF8 /* FileMacroTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMacroTests.swift; sourceTree = "<group>"; };
|
||||
2EA8C36F2D04F51A00843B42 /* PreferCountWhere.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferCountWhere.swift; sourceTree = "<group>"; };
|
||||
2EA8C3742D05282600843B42 /* PreferCountWhereTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferCountWhereTests.swift; sourceTree = "<group>"; };
|
||||
2EDC9DD92CCE9A7C0085DBE2 /* DeclarationV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclarationV2.swift; sourceTree = "<group>"; };
|
||||
2EDC9DDE2CCE9BC70085DBE2 /* DeclarationV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclarationV2Tests.swift; sourceTree = "<group>"; };
|
||||
2EF737512C5E881C00128F91 /* CodeOrganizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeOrganizationTests.swift; sourceTree = "<group>"; };
|
||||
@@ -1267,6 +1274,7 @@
|
||||
2E2BABAE2C57F6DD00590239 /* EmptyBraces.swift */,
|
||||
580496D42C584E8F004B7DBF /* EmptyExtensions.swift */,
|
||||
2E2BABD92C57F6DD00590239 /* EnumNamespaces.swift */,
|
||||
ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */,
|
||||
2E2BABC72C57F6DD00590239 /* ExtensionAccessControl.swift */,
|
||||
2E2BABC02C57F6DD00590239 /* FileHeader.swift */,
|
||||
2E9DE5052C95F9A1000FEDF8 /* FileMacro.swift */,
|
||||
@@ -1287,6 +1295,7 @@
|
||||
2E2BABF62C57F6DD00590239 /* NumberFormatting.swift */,
|
||||
2E2BABED2C57F6DD00590239 /* OpaqueGenericParameters.swift */,
|
||||
2E2BABBF2C57F6DD00590239 /* OrganizeDeclarations.swift */,
|
||||
2EA8C36F2D04F51A00843B42 /* PreferCountWhere.swift */,
|
||||
2E2BABCE2C57F6DD00590239 /* PreferForLoop.swift */,
|
||||
2E2BABDB2C57F6DD00590239 /* PreferKeyPath.swift */,
|
||||
9BDB4F1C2C94773600C93995 /* PrivateStateVariables.swift */,
|
||||
@@ -1356,7 +1365,6 @@
|
||||
2E2BABBB2C57F6DD00590239 /* WrapSingleLineComments.swift */,
|
||||
2E2BABDF2C57F6DD00590239 /* WrapSwitchCases.swift */,
|
||||
2E2BABF92C57F6DD00590239 /* YodaConditions.swift */,
|
||||
ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */,
|
||||
);
|
||||
path = Rules;
|
||||
sourceTree = "<group>";
|
||||
@@ -1390,6 +1398,7 @@
|
||||
2E8DE6B62C57FEB30032BF25 /* EmptyBracesTests.swift */,
|
||||
012242D92CD355B000B96EF8 /* EmptyExtensionsTests.swift */,
|
||||
2E8DE6972C57FEB30032BF25 /* EnumNamespacesTests.swift */,
|
||||
ABC11AF72CC082D300556471 /* EnvironmentEntryTests.swift */,
|
||||
2E8DE6992C57FEB30032BF25 /* ExtensionAccessControlTests.swift */,
|
||||
2E8DE6962C57FEB30032BF25 /* FileHeaderTests.swift */,
|
||||
2E9DE50A2C95FB4F000FEDF8 /* FileMacroTests.swift */,
|
||||
@@ -1410,6 +1419,7 @@
|
||||
2E8DE6B02C57FEB30032BF25 /* NumberFormattingTests.swift */,
|
||||
2E8DE69E2C57FEB30032BF25 /* OpaqueGenericParametersTests.swift */,
|
||||
2E8DE6E22C57FEB30032BF25 /* OrganizeDeclarationsTests.swift */,
|
||||
2EA8C3742D05282600843B42 /* PreferCountWhereTests.swift */,
|
||||
2E8DE6982C57FEB30032BF25 /* PreferForLoopTests.swift */,
|
||||
2E8DE6BF2C57FEB30032BF25 /* PreferKeyPathTests.swift */,
|
||||
9BDB4F1A2C94760000C93995 /* PrivateStateVariablesTests.swift */,
|
||||
@@ -1476,7 +1486,6 @@
|
||||
2E8DE6F12C57FEB30032BF25 /* WrapSwitchCasesTests.swift */,
|
||||
2E8DE6A42C57FEB30032BF25 /* WrapTests.swift */,
|
||||
2E8DE6AC2C57FEB30032BF25 /* YodaConditionsTests.swift */,
|
||||
ABC11AF72CC082D300556471 /* EnvironmentEntryTests.swift */,
|
||||
);
|
||||
path = Rules;
|
||||
sourceTree = "<group>";
|
||||
@@ -1882,6 +1891,7 @@
|
||||
2E2BADA32C57F6DD00590239 /* RedundantTypedThrows.swift in Sources */,
|
||||
2E2BAD4B2C57F6DD00590239 /* RedundantVoidReturnType.swift in Sources */,
|
||||
2E2BAC332C57F6DD00590239 /* Semicolons.swift in Sources */,
|
||||
2EA8C3702D04F51A00843B42 /* PreferCountWhere.swift in Sources */,
|
||||
2E2BAC0B2C57F6DD00590239 /* Indent.swift in Sources */,
|
||||
2E2BACC32C57F6DD00590239 /* SpaceInsideBrackets.swift in Sources */,
|
||||
2EDC9DDA2CCE9A7C0085DBE2 /* DeclarationV2.swift in Sources */,
|
||||
@@ -2104,6 +2114,7 @@
|
||||
2E8DE7252C57FEB30032BF25 /* BracesTests.swift in Sources */,
|
||||
2E8DE74B2C57FEB30032BF25 /* LeadingDelimitersTests.swift in Sources */,
|
||||
2E8DE7552C57FEB30032BF25 /* RedundantTypedThrowsTests.swift in Sources */,
|
||||
2EA8C3752D05282600843B42 /* PreferCountWhereTests.swift in Sources */,
|
||||
015CE8B12B448CCE00924504 /* SingularizeTests.swift in Sources */,
|
||||
2E8DE7042C57FEB30032BF25 /* ExtensionAccessControlTests.swift in Sources */,
|
||||
015F83FB2BF1448D0060A07E /* ReporterTests.swift in Sources */,
|
||||
@@ -2233,6 +2244,7 @@
|
||||
2E2BAC682C57F6DD00590239 /* LinebreakAtEndOfFile.swift in Sources */,
|
||||
2E2BAC8C2C57F6DD00590239 /* SpaceInsideComments.swift in Sources */,
|
||||
2E2BAC102C57F6DD00590239 /* WrapMultilineConditionalAssignment.swift in Sources */,
|
||||
2EA8C3712D04F51A00843B42 /* PreferCountWhere.swift in Sources */,
|
||||
2E2BACC02C57F6DD00590239 /* TypeSugar.swift in Sources */,
|
||||
2E2BACEC2C57F6DD00590239 /* RedundantObjc.swift in Sources */,
|
||||
2E2BACE02C57F6DD00590239 /* RedundantReturn.swift in Sources */,
|
||||
@@ -2373,6 +2385,7 @@
|
||||
01BBD85B21DAA2A700457380 /* Globs.swift in Sources */,
|
||||
2E2BAD112C57F6DD00590239 /* RedundantProperty.swift in Sources */,
|
||||
01A8320824EC7F7700A9D0EB /* FormattingHelpers.swift in Sources */,
|
||||
2EA8C3722D04F51A00843B42 /* PreferCountWhere.swift in Sources */,
|
||||
082D644B2CA4719E0072DA14 /* RedundantEquatable.swift in Sources */,
|
||||
2E2BACFD2C57F6DD00590239 /* BlankLinesBetweenImports.swift in Sources */,
|
||||
2E2BACF12C57F6DD00590239 /* ConditionalAssignment.swift in Sources */,
|
||||
@@ -2522,6 +2535,7 @@
|
||||
2E2BADB22C57F6DD00590239 /* RedundantType.swift in Sources */,
|
||||
9028F7851DA4B435009FE5B4 /* Formatter.swift in Sources */,
|
||||
2E2BAD122C57F6DD00590239 /* RedundantProperty.swift in Sources */,
|
||||
2EA8C3732D04F51A00843B42 /* PreferCountWhere.swift in Sources */,
|
||||
082D644D2CA4719E0072DA14 /* RedundantEquatable.swift in Sources */,
|
||||
E487212A201E3DD50014845E /* RulesStore.swift in Sources */,
|
||||
2E2BACFE2C57F6DD00590239 /* BlankLinesBetweenImports.swift in Sources */,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// PreferCountWhereTests.swift
|
||||
// SwiftFormatTests
|
||||
//
|
||||
// Created by Cal Stephens on 12/7/24.
|
||||
// Copyright © 2024 Nick Lockwood. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import SwiftFormat
|
||||
|
||||
final class PreferCountWhereTests: XCTestCase {
|
||||
func testConvertFilterToCountWhere() {
|
||||
let input = """
|
||||
planets.filter({ !$0.moons.isEmpty }).count
|
||||
"""
|
||||
|
||||
let output = """
|
||||
planets.count(where: { !$0.moons.isEmpty })
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "6.0")
|
||||
testFormatting(for: input, output, rule: .preferCountWhere, options: options)
|
||||
}
|
||||
|
||||
func testConvertFilterTrailingClosureToCountWhere() {
|
||||
let input = """
|
||||
planets.filter { !$0.moons.isEmpty }.count
|
||||
"""
|
||||
|
||||
let output = """
|
||||
planets.count(where: { !$0.moons.isEmpty })
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "6.0")
|
||||
testFormatting(for: input, output, rule: .preferCountWhere, options: options)
|
||||
}
|
||||
|
||||
func testConvertNestedFilter() {
|
||||
let input = """
|
||||
planets.filter { planet in
|
||||
planet.moons.filter { moon in
|
||||
moon.hasAtmosphere
|
||||
}.count > 1
|
||||
}.count
|
||||
"""
|
||||
|
||||
let output = """
|
||||
planets.count(where: { planet in
|
||||
planet.moons.count(where: { moon in
|
||||
moon.hasAtmosphere
|
||||
}) > 1
|
||||
})
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "6.0")
|
||||
testFormatting(for: input, output, rule: .preferCountWhere, options: options)
|
||||
}
|
||||
|
||||
func testPreservesFilterBeforeSwift6() {
|
||||
let input = """
|
||||
planets.filter { !$0.moons.isEmpty }.count
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.10")
|
||||
testFormatting(for: input, rule: .preferCountWhere, options: options)
|
||||
}
|
||||
|
||||
func testPreservesCountMethod() {
|
||||
let input = """
|
||||
planets.filter { !$0.moons.isEmpty }.count(of: earth)
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "6.0")
|
||||
testFormatting(for: input, rule: .preferCountWhere, options: options)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user