Skip to content
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

[SR-10312] Trap if using an Index from another String #52712

Open
swift-ci opened this issue Apr 4, 2019 · 9 comments
Open

[SR-10312] Trap if using an Index from another String #52712

swift-ci opened this issue Apr 4, 2019 · 9 comments
Assignees
Labels
new feature standard library Area: Standard library umbrella

Comments

@swift-ci
Copy link
Collaborator

swift-ci commented Apr 4, 2019

Previous ID SR-10312
Radar None
Original Reporter Raphael (JIRA User)
Type New Feature
Additional Detail from JIRA
Votes 0
Component/s Standard Library
Labels New Feature
Assignee @milseman
Priority Medium

md5: eea8c7490c7ace945740627cc99a3cce

Issue Description:

Resolution: We should trap (perhaps debug only) when using an index from a different String than it was created from.

Original bug:

Generating a NSRange from a Range<String.Index> is giving inconsistent results if the string was bridged from NSString. I believe this is because Swift 5 is using UTF-8 whereas NSString uses UTF-16.

The following code demonstrates the issue:

import Foundation
 
let text = "Début: 04:54AM"
let nsString = NSString(string: text)
let range = text.range(of: "04:54AM")!
 
let nsRange1 = NSRange(range, in: nsString as String) // This will be {8, 7}
let nsRange2 = NSRange(range, in: text) // This will be {7, 7}

The above code runs fine on Swift 4.2 but generates inconsistent results on Swift 5. On a real world scenario, the first NSRange could be generated by using the text property of a UILabel crashing the app when the range is used as it's upper bound will be out of the string ranges.

@milseman
Copy link
Mannequin

milseman mannequin commented Apr 5, 2019

Here, text and nsString are distinct strings and you cannot use indices across different strings. You'll need to use (nsString as String).range(of: "04:54AM")!.

Let me throw together a test file to make sure we have the right behavior, though.

@swift-ci
Copy link
Collaborator Author

swift-ci commented Apr 5, 2019

Comment by Raphael Cruzeiro (JIRA)

@milseman I figured it would be something like that and it's possible that this behaviour won't be an issue for most people. The problem, in my opinion, arises when you throw in UIKit into the mix. I first encountered this issue in a helper extension to add specific styling to ranges of text on a UILabel. Since (as far as the developer is concerned), the `text` attribute of a `UILabel` is `String`, it is to expect that if both strings have the same set of characters as a value, they are equivalent.

Here is the extension that broke under Swift 5 with the workaround already in place

extension UILabel {
    func add(style: TextStyle, to range: Range<String.Index>) {
        let string: String = "\(self.text ?? "")"
        let nsRange = NSRange(range, in: string)
        let attributedString = NSMutableAttributedString(attributedString: self.attributedText!)
        attributedString.addAttributes([
            .foregroundColor: style.foregroundColor,
            .font: style.font
        ], range: nsRange)
        if style.strikeThrough {
            attributedString.addAttributes([
                .strikethroughStyle: NSNumber(value: NSUnderlineStyle.single.rawValue),
                .strikethroughColor: style.foregroundColor
                ],
                range: nsRange
            )
        }
        self.attributedText = attributedString
    }
}

@milseman
Copy link
Mannequin

milseman mannequin commented Apr 5, 2019

I understand that expectation, but it also sort of depends on what you mean by "same set of characters". By characters, if you mean Character, then no, as

"é" == "e\u{301}"

either as String or Character. If you mean Unicode scalar values, then it does depend on whether representation is relevant to whether they are the "same set".

Would it be useful to have a String API that's something to the effect of

myString.isIndexInterchangeable(with: otherString)

Or maybe

myString.isValidIndex(idx)

As for NSAttributedString usage, CC @itaiferber. Itai, do you have any ideas how to write this better?

@swift-ci
Copy link
Collaborator Author

swift-ci commented Apr 5, 2019

Comment by Raphael Cruzeiro (JIRA)

Another point of confusion here is that:

text == NSString as String

Which I guess brings the question of what should define the equivalence of two strings (not arguing that they would be !=). For developers that started coding with Swift and have never touched Obj-C, an error like this would be really confusing as they won't be aware that UIKit deals with NSStrings internally and that what you get back might not necessarily be what you passed to it.

@swift-ci
Copy link
Collaborator Author

swift-ci commented Apr 5, 2019

Comment by Raphael Cruzeiro (JIRA)

Perhaps the only real solution for this is to make this kind of behaviour extra clear in the docs (and hope UIKit follows suit).

@milseman
Copy link
Mannequin

milseman mannequin commented Apr 5, 2019

This falls under the category of not being able to use indices with collections they didn't originally come from. For example, Dictionary will actually trap if you use an index from a different dictionary, and we've had long-standing requests to do the same for String. We have some bits reserved for this purpose and might add it in the future.

Do you want to move this to a documentation request bug instead? CC @natecook1000 for ideas regarding documentation.

@swift-ci
Copy link
Collaborator Author

swift-ci commented Apr 9, 2019

Comment by Raphael Cruzeiro (JIRA)

@milseman, having the same trap for String would actually make sense. It would make this kind of mistake really obvious. With Swift 4.2 you could use the same indices with different strings and it would work 100% of the time (even if that was not intended). Now with Swift 5, this will still work on quite a lot of cases (it only breaks if you bridge from NSString and you have characters that might end up getting encoded differently). I believe that making the string trap would be the most efficient way to make it clear that it's mot supposed to be used that way.

@milseman
Copy link
Mannequin

milseman mannequin commented Apr 9, 2019

Yes, I've been thinking of what time to introduce this trap. We'd probably want to roll it out initially as a debug-only precondition failure (preserve runtime behavior of shipping code), which would also allow us to keep it's representation opaque at the ABI level. I was considering whether it's better to wait for more ease-of-use functionality in String, since currently many users have to drop down to the indexing level for common operations. Do you find yourself manipulating String.Index in your own code, or do you mostly use it (and ranges) as arguments and return values?

edit: grammar

@milseman
Copy link
Mannequin

milseman mannequin commented Apr 10, 2019

I'm converting this bug to track the trapping feature

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new feature standard library Area: Standard library umbrella
Projects
None yet
Development

No branches or pull requests

1 participant