-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Fix NSRange length calculations using String.count instead of UTF-16 count #25341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
2d1281d
a7f03af
53d074f
b7f0395
597f2b5
6e6a16c
c6bc238
bc26927
e2f586c
c365360
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,10 +48,10 @@ extension String { | |
| /// - Returns: the requested `Range<String.Index>` | ||
| /// | ||
| func range(from nsRange: NSRange) -> Range<String.Index> { | ||
| let lowerBound = index(startIndex, offsetBy: nsRange.location) | ||
| let upperBound = index(lowerBound, offsetBy: nsRange.length) | ||
|
|
||
| return lowerBound ..< upperBound | ||
| guard let range = Range(nsRange, in: self) else { | ||
| preconditionFailure("Invalid NSRange \(nsRange) for string of UTF-16 length \(utf16.count)") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might result in a crash in production. The ideal change would be to eliminate |
||
| } | ||
| return range | ||
| } | ||
|
|
||
| func range(fromUTF16NSRange utf16NSRange: NSRange) -> Range<String.Index> { | ||
|
|
@@ -160,11 +160,7 @@ extension String { | |
| /// - Returns: the requested `NSRange`. | ||
| /// | ||
| func nsRange(from range: Range<String.Index>) -> NSRange { | ||
|
|
||
| let location = distance(from: startIndex, to: range.lowerBound) | ||
| let length = distance(from: range.lowerBound, to: range.upperBound) | ||
|
|
||
| return NSRange(location: location, length: length) | ||
| NSRange(range, in: self) | ||
| } | ||
|
|
||
| /// Converts a `Range<String.Index>` into an UTF16 NSRange. | ||
|
|
@@ -190,7 +186,7 @@ extension String { | |
| /// Returns a NSRange with a starting location at the very end of the string | ||
| /// | ||
| func endOfStringNSRange() -> NSRange { | ||
| return NSRange(location: count, length: 0) | ||
| return NSRange(location: utf16.count, length: 0) | ||
| } | ||
|
|
||
| func indexFromLocation(_ location: Int) -> String.Index? { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,10 +5,31 @@ import UIKit | |
| // | ||
| extension NSMutableAttributedString { | ||
|
|
||
| /// Applies a single attribute to the entire string. | ||
| /// | ||
| /// Prefer this over manually constructing an `NSRange` with `NSMakeRange(0, …)`, | ||
| /// which is error-prone when mixing `String.count` (grapheme clusters) with | ||
| /// `NSRange` (UTF-16 code units). | ||
| /// | ||
| public func applyAttribute(_ key: NSAttributedString.Key, value: Any) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (nit) I'd suggest naming it |
||
| // swiftlint:disable:next full_range_attributed_string_attribute | ||
| addAttribute(key, value: value, range: NSRange(location: 0, length: length)) | ||
| } | ||
|
|
||
| /// Applies a collection of attributes to the entire string. | ||
| /// | ||
| /// Prefer this over manually constructing an `NSRange` with `NSMakeRange(0, …)`, | ||
| /// which is error-prone when mixing `String.count` (grapheme clusters) with | ||
| /// `NSRange` (UTF-16 code units). | ||
| /// | ||
| public func applyAttributes(_ attrs: [NSAttributedString.Key: Any]) { | ||
| // swiftlint:disable:next full_range_attributed_string_attribute | ||
| addAttributes(attrs, range: NSRange(location: 0, length: length)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (nit) It doesn't seem to be one of the documented rules and there don't seem to be any custom rules in the .yml` file. It might be the case SwiftLint is behind. I'm not familiar with this rule though.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, it's a new one – source is here |
||
| } | ||
|
|
||
| /// Applies the specified foreground color to the full length of the receiver. | ||
| /// | ||
| public func applyForegroundColor(_ color: UIColor) { | ||
| let range = NSRange(location: 0, length: length) | ||
| addAttribute(.foregroundColor, value: color, range: range) | ||
| applyAttribute(.foregroundColor, value: color) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes are repetitive, so I focused on this first scenario. I generated a set of unit tests for it, verified the original issue and the fact that the change fixes it.
(nit) An idiomatic way to create
NSRangefromStringis with a specialized init method that abstracts the underlying representation away:(nit) Since it's
Stringin andStringout, the line 55 just below requires another conversion fromNSRangeback toSwift.Range. It might be worth taking it a step further and replacing it with Regex that will also add some compile-time guarantees for the expression.