Repository: swiftlang/swift-syntax (swift)

Issue:
Parser crash with skipSingle and string literals
### Issue Kind

Parser Crash

### Source Code

```swift
if p{""
```


### Description

_No response_

Pull request description:
The loop has made progress if either
 - The parser is now pointing at a different location in the source file
 - The parser is still pointing at the same position in the source file but now has a different token kind (and thus consumed a zero-length token like an empty string interpolation

The second condition is new.

Fixes #1247
rdar://104413423

Patch:
diff --git a/Sources/SwiftParser/Lookahead.swift b/Sources/SwiftParser/Lookahead.swift
index 4b089a44336..4388ee685bd 100644
--- a/Sources/SwiftParser/Lookahead.swift
+++ b/Sources/SwiftParser/Lookahead.swift
@@ -307,6 +307,13 @@ extension Parser.Lookahead {
     return skip(initialState: .skipSingle)
   }
 
+  // Note: We don't need to treat string quotes as bracketed tokens because:
+  //  - If we skip over the opening quote, we also automatically skip over
+  //    closing quote since it has the same token kind
+  //  - It is very unlikely that we look for string segments, so we
+  //    automatically skip over those as individual tokens
+  //  - String interpolation contains parentheses, so it automatically skips
+  //    until the closing parenthesis.
   private enum BracketedTokens: RawTokenKindSubset {
     case leftParen
     case leftBrace
diff --git a/Sources/SwiftParser/LoopProgressCondition.swift b/Sources/SwiftParser/LoopProgressCondition.swift
index f1910797a35..a15f0cc9e92 100644
--- a/Sources/SwiftParser/LoopProgressCondition.swift
+++ b/Sources/SwiftParser/LoopProgressCondition.swift
@@ -25,8 +25,19 @@ struct LoopProgressCondition {
   /// ie. it checks if the the parser's `currentToken` has changed in between two calls to `evaluate`.
   /// In non-assert builds, `evaluate()` returns `false` if the loop has not made progress, thus aborting the loop.
   mutating func evaluate(_ currentToken: Lexer.Lexeme) -> Bool {
-    let hasMadeProgress = self.currentToken?.tokenText.baseAddress != currentToken.tokenText.baseAddress
-    self.currentToken = currentToken
+    defer {
+      self.currentToken = currentToken
+    }
+
+    guard let previousToken = self.currentToken else {
+      return true
+    }
+    // The loop has made progress if either
+    //  - the parser is now pointing at a different location in the source file
+    //  - the parser is still pointing at the same position in the source file
+    //     but now has a different token kind (and thus consumed a zero-length
+    //     token like an empty string interpolation
+    let hasMadeProgress = previousToken.tokenText.baseAddress != currentToken.tokenText.baseAddress || (previousToken.byteLength == 0 && previousToken.rawTokenKind != currentToken.rawTokenKind)
     assert(hasMadeProgress, "Loop should always make progress")
     return hasMadeProgress
   }


Test patch:
diff --git a/Tests/SwiftParserTest/StatementTests.swift b/Tests/SwiftParserTest/StatementTests.swift
index bf2fcad7fb1..109411565f7 100644
--- a/Tests/SwiftParserTest/StatementTests.swift
+++ b/Tests/SwiftParserTest/StatementTests.swift
@@ -603,4 +603,16 @@ final class StatementTests: XCTestCase {
       )
     )
   }
+
+  func testSkippingOverEmptyStringLiteral() {
+    // https://github.com/apple/swift-syntax/issues/1247
+    AssertParse(
+      """
+      if p{""1️⃣
+      """,
+      diagnostics: [
+        DiagnosticSpec(message: "expected '}' to end 'if' statement")
+      ]
+    )
+  }
 }