Summary
On Linux, NSRegularExpression.rangeOfFirstMatch(in:options:range:) can return {0, 0} for an anchored search whose range starts after the beginning of the string, even when the pattern should not match.
This also affects String.range(of:options:range:) / NSString.range(of:options:range:locale:) when using .regularExpression and .anchored, because those APIs route through NSRegularExpression.
The same examples return "not found" on macOS.
Reproducer
import Foundation
let source = "abcdefg"
let regex = try! NSRegularExpression(pattern: "-?[0-9]+")
let found = regex.rangeOfFirstMatch(
in: source,
options: .anchored,
range: NSRange(location: 3, length: 2)
)
print(found.location, found.length, found.location == NSNotFound)
Expected Result
The pattern -?[0-9]+ requires at least one digit. The searched range is "de", so there should be no match.
Expected output should indicate NSNotFound:
Actual Result on Linux
This is surprising because:
- The pattern requires at least one digit via
[0-9]+.
- The search is anchored to
NSRange(location: 3, length: 2).
- The returned range is
{0, 0}, outside the requested search range.
- The returned location is not
NSNotFound.
String.range Reproducer
The same behavior is observable through String.range(of:options:range:):
import Foundation
let source = "abcdefg"
let start = source.index(source.startIndex, offsetBy: 3)
let range = source.range(
of: "-?[0-9]+",
options: [.regularExpression, .anchored],
range: start..<source.endIndex
)
if let range {
print(
"matched",
range.lowerBound == range.upperBound ? "empty" : String(source[range]),
source.distance(from: source.startIndex, to: range.lowerBound),
source.distance(from: source.startIndex, to: range.upperBound)
)
} else {
print("nil")
}
Expected output:
Actual Linux output:
Tested Versions
Reproduced on Linux:
Swift version 6.1.3
Target: aarch64-unknown-linux-gnu
Swift version 6.3.1 (swift-6.3.1-RELEASE)
Target: aarch64-unknown-linux-gnu
Not reproduced on macOS:
Apple Swift version 6.3.1
Target: arm64-apple-macosx26.0
Likely Implementation Path
In swift-corelibs-foundation, the String.range(...) path appears to route through:
Sources/Foundation/NSStringAPI.swift: StringProtocol.range(of:options:range:locale:)
Sources/Foundation/NSString.swift: _rangeOfRegularExpressionPattern(regex:options:range:locale:)
Sources/Foundation/NSRegularExpression.swift: rangeOfFirstMatch(in:options:range:)
The direct NSRegularExpression.rangeOfFirstMatch(...) reproducer above returns {0, 0}, so the issue appears to be at or below the NSRegularExpression layer rather than only in the String overlay.
Workaround
Callers can defensively reject anchored matches whose returned location is outside the requested search range:
let searchRange = NSRange(location: 3, length: 2)
let found = regex.rangeOfFirstMatch(in: source, options: .anchored, range: searchRange)
if found.location == searchRange.location {
// accept match
} else {
// treat as no match
}
For String.Index-based code:
if let found = source.range(
of: pattern,
options: [.regularExpression, .anchored],
range: startIndex..<endIndex
), found.lowerBound == startIndex {
// accept match
} else {
// treat as no match
}
Summary
On Linux,
NSRegularExpression.rangeOfFirstMatch(in:options:range:)can return{0, 0}for an anchored search whoserangestarts after the beginning of the string, even when the pattern should not match.This also affects
String.range(of:options:range:)/NSString.range(of:options:range:locale:)when using.regularExpressionand.anchored, because those APIs route throughNSRegularExpression.The same examples return "not found" on macOS.
Reproducer
Expected Result
The pattern
-?[0-9]+requires at least one digit. The searched range is"de", so there should be no match.Expected output should indicate
NSNotFound:Actual Result on Linux
This is surprising because:
[0-9]+.NSRange(location: 3, length: 2).{0, 0}, outside the requested search range.NSNotFound.String.rangeReproducerThe same behavior is observable through
String.range(of:options:range:):Expected output:
Actual Linux output:
Tested Versions
Reproduced on Linux:
Not reproduced on macOS:
Likely Implementation Path
In
swift-corelibs-foundation, theString.range(...)path appears to route through:Sources/Foundation/NSStringAPI.swift:StringProtocol.range(of:options:range:locale:)Sources/Foundation/NSString.swift:_rangeOfRegularExpressionPattern(regex:options:range:locale:)Sources/Foundation/NSRegularExpression.swift:rangeOfFirstMatch(in:options:range:)The direct
NSRegularExpression.rangeOfFirstMatch(...)reproducer above returns{0, 0}, so the issue appears to be at or below theNSRegularExpressionlayer rather than only in theStringoverlay.Workaround
Callers can defensively reject anchored matches whose returned location is outside the requested search range:
For
String.Index-based code: