Skip to content

NSRegularExpression.rangeOfFirstMatch returns incorrect {0, 0} anchored match for non-zero search range on Linux #5467

Description

@g-mark

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:

<NSNotFound> 0 true

Actual Result on Linux

0 0 false

This is surprising because:

  1. The pattern requires at least one digit via [0-9]+.
  2. The search is anchored to NSRange(location: 3, length: 2).
  3. The returned range is {0, 0}, outside the requested search range.
  4. 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:

nil

Actual Linux output:

matched empty 0 0

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
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions