From 6803436d873e53c45a84a9ae2b135ca8f9b0e94b Mon Sep 17 00:00:00 2001 From: inju2403 <56947879+inju2403@users.noreply.github.com> Date: Fri, 29 May 2026 15:46:12 +0900 Subject: [PATCH] [SwiftIDEUtils] Fix infinite loop in NameMatcher for whitespace positions in string segments The `while let baseNamePosition = positionsToResolve.first(where:)` loop in the `.stringSegment` branch of `visit(_:TokenSyntax)` only mutated `positionsToResolve` via `addResolvedLocIfRequested`. When `getFirstTokenLength` returned `nil` (the position landed on whitespace inside the string segment, so the parser saw leading trivia) the loop hit `continue` without removing the position, and `first(where:)` found the same position again on every subsequent iteration -- an infinite loop. Iterate a snapshot of the matching positions instead, so a position for which `getFirstTokenLength` returns `nil` is simply skipped to the next one. This is the same class of bug as the one fixed in 528257fe4 for the trivia loop earlier in the same function. Adds a regression test (`testStringLiteralWithPositionOnWhitespace`) that hangs against `main` and passes in <5ms with this change. --- Sources/SwiftIDEUtils/NameMatcher.swift | 7 ++++++- Tests/SwiftIDEUtilsTest/NameMatcherTests.swift | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftIDEUtils/NameMatcher.swift b/Sources/SwiftIDEUtils/NameMatcher.swift index 5ba9ab4fba3..284762be39c 100644 --- a/Sources/SwiftIDEUtils/NameMatcher.swift +++ b/Sources/SwiftIDEUtils/NameMatcher.swift @@ -252,7 +252,12 @@ public class NameMatcher: SyntaxAnyVisitor { } if case .stringSegment = token.tokenKind { - while let baseNamePosition = positionsToResolve.first(where: { token.rangeWithoutTrivia.contains($0) }) { + // Iterate a snapshot: `addResolvedLocIfRequested` is the only path that + // removes a position from `positionsToResolve`, and it is skipped when + // `getFirstTokenLength` returns `nil`. A `while let .first(where:)` loop + // would re-pick any such position forever. + let positionsInToken = positionsToResolve.filter { token.rangeWithoutTrivia.contains($0) } + for baseNamePosition in positionsInToken { let positionOffsetInStringSegment = baseNamePosition.utf8Offset - token.position.utf8Offset guard let tokenLength = getFirstTokenLength(in: token.syntaxTextBytes[positionOffsetInStringSegment...]) else { continue diff --git a/Tests/SwiftIDEUtilsTest/NameMatcherTests.swift b/Tests/SwiftIDEUtilsTest/NameMatcherTests.swift index e5856339b18..9d49cab768b 100644 --- a/Tests/SwiftIDEUtilsTest/NameMatcherTests.swift +++ b/Tests/SwiftIDEUtilsTest/NameMatcherTests.swift @@ -152,6 +152,15 @@ class NameMatcherTests: XCTestCase { ) } + func testStringLiteralWithPositionOnWhitespace() { + assertNameMatcherResult( + """ + "hello1️⃣ world" + """, + expected: [] + ) + } + func testWildcardParameter() { assertNameMatcherResult( "func 0️⃣foo1(_ x: Int) {}",