-
Notifications
You must be signed in to change notification settings - Fork 637
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0c6c0df
commit aaee5d8
Showing
5 changed files
with
241 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
+ }) | ||
``` | ||
""" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |