Files
SwiftFormat/Sources/Rules/PreferSwiftStringAPI.swift
T
SUTHEESH SUKUMARAN 78836b9959 Replace objc string methods with swift equivalents #2292 (#2491)
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.
2026-04-11 12:26:55 -07:00

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")
```
"""
}
}