mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-06-16 10:34:34 +00:00
Add preferSwiftStringAPI rule (#2292) Replaces the Objective-C bridged `replacingOccurrences(of:with:)` with Swift-native `replacing(_:with:)`. The ObjC method has known Unicode bugs (e.g. corrupting emoji flag sequences); the Swift equivalent is safer, shorter, and available from Swift 5.7+. Only the two-argument form is transformed — calls with `options:` or `range:` are left unchanged as they have no direct Swift equivalent.
55 lines
2.1 KiB
Swift
55 lines
2.1 KiB
Swift
//
|
|
// PreferSwiftStringAPI.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Sutheesh Sukumaran on 05/04/2026.
|
|
// Copyright © 2026 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public extension FormatRule {
|
|
/// Replace Objective-C bridged String methods with their Swift equivalents
|
|
static let preferSwiftStringAPI = FormatRule(
|
|
help: "Replace Objective-C bridged String methods with Swift equivalents."
|
|
) { formatter in
|
|
// replacing(_:with:) was introduced in Swift 5.7
|
|
guard formatter.options.swiftVersion >= "5.7" else { return }
|
|
|
|
formatter.forEach(.identifier("replacingOccurrences")) { i, _ in
|
|
// Must be a method call (preceded by a dot)
|
|
guard let prevIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: i),
|
|
formatter.tokens[prevIndex] == .operator(".", .infix)
|
|
else { return }
|
|
|
|
// Must be followed immediately by a `(` argument list
|
|
guard let openParenIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: i),
|
|
formatter.tokens[openParenIndex] == .startOfScope("(")
|
|
else { return }
|
|
|
|
// Only transform the two-argument form: `replacingOccurrences(of:with:)`
|
|
let args = formatter.parseFunctionCallArguments(startOfScope: openParenIndex)
|
|
guard args.count == 2,
|
|
args[0].label == "of",
|
|
args[1].label == "with",
|
|
let ofLabelIndex = args[0].labelIndex
|
|
else { return }
|
|
|
|
// Remove the `of:` label and any whitespace up to the value.
|
|
// Since ofLabelIndex > i, this does not invalidate index i.
|
|
let valueStart = args[0].valueRange.lowerBound
|
|
formatter.removeTokens(in: ofLabelIndex ..< valueStart)
|
|
|
|
// Rename `replacingOccurrences` → `replacing`
|
|
formatter.replaceToken(at: i, with: .identifier("replacing"))
|
|
}
|
|
} examples: {
|
|
"""
|
|
```diff
|
|
- str.replacingOccurrences(of: "foo", with: "bar")
|
|
+ str.replacing("foo", with: "bar")
|
|
```
|
|
"""
|
|
}
|
|
}
|