From 6b5a8f861bcdbd6959b4c9e452939c1393d71202 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 26 Oct 2026 12:24:38 -0500 Subject: [PATCH] Match literal at end of sub-character substring (#910) Matching was failing for substrings that have a sub-character end index. This change fixes the boundary check for those cases, so that the end of the subject bounds is always a boundary. rdar://190999851 --- .../_StringProcessing/Engine/Processor.swift | 4 ++-- Tests/RegexTests/MatchTests.swift | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Sources/_StringProcessing/Engine/Processor.swift b/Sources/_StringProcessing/Engine/Processor.swift index 0bf19b829..0faf01332 100644 --- a/Sources/_StringProcessing/Engine/Processor.swift +++ b/Sources/_StringProcessing/Engine/Processor.swift @@ -767,7 +767,7 @@ extension String { let idx = unicodeScalars.index(after: pos) assert(idx <= end, "Input is a substring with a sub-scalar endIndex.") - if boundaryCheck && !isOnGraphemeClusterBoundary(idx) { + if boundaryCheck && idx != end && !isOnGraphemeClusterBoundary(idx) { return nil } @@ -788,7 +788,7 @@ extension String { guard cur <= end else { return nil } - if boundaryCheck && !isOnGraphemeClusterBoundary(cur) { + if boundaryCheck && cur != end && !isOnGraphemeClusterBoundary(cur) { return nil } diff --git a/Tests/RegexTests/MatchTests.swift b/Tests/RegexTests/MatchTests.swift index acf156d76..d9508ddbf 100644 --- a/Tests/RegexTests/MatchTests.swift +++ b/Tests/RegexTests/MatchTests.swift @@ -3038,4 +3038,27 @@ extension RegexTests { XCTAssertNotNil("testS".wholeMatch(of: regex)) XCTAssertNotNil("tesTs".wholeMatch(of: regex)) } + + func testLiteralAtSubCharacterEnd() throws { + let cafe = "cafe\u{301}" + let sub = cafe[..(verbatim: "afe"))) + } + + func testAutoPossessificationSkippedNodes() throws { + firstMatchTest(#"(\w* {0})a"#, input: "a", match: "a") + firstMatchTest(#"((\w* {0})){1}()a"#, input: "a", match: "a") + firstMatchTest(#"(?:\w{1,}? *?)b"#, input: "bb", match: "bb") + firstMatchTest(#"(?i)]()*|A{,1}a{1}"#, input: "a", match: "a") + firstMatchTest(#"(b?W?)(?i)([B])"#, input: "b", match: "b") + + // Digits aren't a subset of word characters when only \w is ASCII-only. + firstMatchTest(#"(?W)\d{,1}\W"#, input: "\u{661}", match: "\u{661}") + } }