Repository: apple/swift-syntax (swift)

Issue:
Parser crashes on invalid multi-line literal
The following test case crashes the parser:

```swift
"a""" a a"""
```

```
SwiftParser/StringLiterals.swift:181: Fatal error: The lexer should only add trailing trivia to a string segment if the newline is escaped by a backslash
Please submit a bug report (https://swift.org/contributing/#reporting-bugs) and include the crash backtrace.
Stack dump:
0.      Program arguments: /Users/hamish/src/swift-dev/build/Release/swift-macosx-arm64/bin/swift-frontend -typecheck /Users/hamish/src/swift-test-arena/main.swift -sdk /Users/hamish/src/MacOSX.sdk -debug-diagnostic-names -diagnostic-style=swift
1.      Swift version 6.2-dev (LLVM 4197ac1672a278c, Swift 17cbe28e9d85a32)
2.      Compiling with effective version 5.10
3.      While evaluating request ParseTopLevelDeclsRequest(source_file "/Users/hamish/src/swift-test-arena/main.swift")
4.      While evaluating request ParseSourceFileRequest(source_file "/Users/hamish/src/swift-test-arena/main.swift")
5.      With parser at source location: /Users/hamish/src/swift-test-arena/main.swift:1288:4
Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
0  swift-frontend                0x0000000109eec46c llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) + 56
1  swift-frontend                0x0000000109eeabbc llvm::sys::RunSignalHandlers() + 112
2  swift-frontend                0x0000000109eecab4 SignalHandler(int, __siginfo*, void*) + 296
3  libsystem_platform.dylib      0x000000019c836704 _sigtramp + 56
4  libswiftCore.dylib            0x00000001afef795c $ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF + 176
5  lib_CompilerSwiftParser.dylib 0x0000000116234d88 $s20_CompilerSwiftParser0C0V38reclassifyNewlineOfLastSegmentAsTrivia33_78149DB072C20084E7D780D86E26C3AFLL24rawStringDelimitersToken020openQuoteHasTrailingE014middleSegmentsSb01_aB6Syntax03RawV6SyntaxVSg_SbSayAI03Rawt7LiteralH10ListSyntaxV7ElementOGztF + 220
6  lib_CompilerSwiftParser.dylib 0x0000000116235120 $s20_CompilerSwiftParser0C0V33postProcessMultilineStringLiteral33_78149DB072C20084E7D780D86E26C3AFLL03rawG15DelimitersToken9openQuote8segments05closeU0Say01_aB6Syntax03RawsX0VG023unexpectedBeforeOpeningU0_AL07openingU0SayAJ0ygh11SegmentListX0V7ElementOGAhM0z13BeforeClosingU0AL07closingU0tALSg_AltLtF + 252
7  lib_CompilerSwiftParser.dylib 0x000000011623324c $s20_CompilerSwiftParser0C0V18parseStringLiteral01_aB6Syntax03Rawef4ExprG0VyF + 3548
8  lib_CompilerSwiftParser.dylib 0x00000001161e12b0 $s20_CompilerSwiftParser0C0V20parseUnaryExpression6flavor7pattern01_aB6Syntax07RawExprI0VAC0K6FlavorO_AC14PatternContextOtF + 592
9  lib_CompilerSwiftParser.dylib 0x00000001161de590 $s20_CompilerSwiftParser0C0V30parseSequenceExpressionElement6flavor7pattern01_aB6Syntax07RawExprJ0VAC0L6FlavorO_AC14PatternContextOtF + 1500
10 lib_CompilerSwiftParser.dylib 0x00000001161dd444 $s20_CompilerSwiftParser0C0V23parseSequenceExpression6flavor7pattern01_aB6Syntax07RawExprI0VAC0K6FlavorO_AC14PatternContextOtF + 72
11 lib_CompilerSwiftParser.dylib 0x00000001162397cc $s20_CompilerSwiftParser0C0V9parseItem33_008C1B0E0A90478841CED0DF9A7053A1LL12isAtTopLevel13allowInitDecl01_aB6Syntax012RawCodeBlockeU0V0E0OSb_SbtF + 1240
12 lib_CompilerSwiftParser.dylib 0x0000000116238e58 $s20_CompilerSwiftParser0C0V18parseCodeBlockItem12isAtTopLevel13allowInitDecl01_aB6Syntax03RawefgO0VSgSb_SbtF + 492
13 lib_CompilerSwiftParser.dylib 0x0000000116237314 $s20_CompilerSwiftParser0C0V22parseCodeBlockItemList12isAtTopLevel13allowInitDecl5until01_aB6Syntax03RawefghQ0VSb_S2bACzXEtF05$s20_ab9Syntax017efgh9C0V01_aB6c35E5parse4fromAcD0H0Vz_tFZAA03RawdefgU19VAHzXEfU_SbAHzXEfU_Tf1nncn_nTm + 144
14 lib_CompilerSwiftParser.dylib 0x000000011626d91c $s20_CompilerSwiftSyntax010SourceFileC0V01_aB6ParserE5parse4fromAcD0F0Vz_tFZ + 68
15 swift-frontend                0x0000000105ce7a3c $s11swiftASTGen15parseSourceFile6buffer10moduleName8filename14declContextPtr4kindSVSo16BridgedStringRefV_A2ISvSgSo0n9GenerateddE4KindVtF + 220
```

Pull request description:
Previously we could incorrectly try to eat `"""` as the closing delimiter for e.g `"a"""`. Make sure we only eat a single `"`.

Resolves #3113

Patch:
diff --git a/Sources/SwiftParser/Lexer/Cursor.swift b/Sources/SwiftParser/Lexer/Cursor.swift
index 13d2af8c289..24262fbbbab 100644
--- a/Sources/SwiftParser/Lexer/Cursor.swift
+++ b/Sources/SwiftParser/Lexer/Cursor.swift
@@ -79,7 +79,7 @@ extension Lexer.Cursor {
 
     /// The lexer has finished lexing the contents of a string literal and is now
     /// looking for the closing quote.
-    case afterStringLiteral(isRawString: Bool)
+    case afterStringLiteral(kind: StringLiteralKind, isRawString: Bool)
 
     /// The lexer has lexed the closing quote of a string literal that had raw
     /// string delimiters and is now looking for the closing raw string delimiters.
@@ -453,8 +453,8 @@ extension Lexer.Cursor {
       result = lexAfterRawStringDelimiter(delimiterLength: delimiterLength)
     case .inStringLiteral(kind: let stringLiteralKind, delimiterLength: let delimiterLength):
       result = lexInStringLiteral(stringLiteralKind: stringLiteralKind, delimiterLength: delimiterLength)
-    case .afterStringLiteral(isRawString: _):
-      result = lexAfterStringLiteral()
+    case .afterStringLiteral(kind: let stringLiteralKind, isRawString: _):
+      result = lexAfterStringLiteral(stringLiteralKind: stringLiteralKind)
     case .afterClosingStringQuote:
       result = lexAfterClosingStringQuote()
     case .inStringInterpolationStart(stringLiteralKind: let stringLiteralKind):
@@ -998,7 +998,7 @@ extension Lexer.Cursor {
     case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
       return self.lexNumber()
     case #"'"#, #"""#:
-      return self.lexStringQuote(isOpening: true, leadingDelimiterLength: 0)
+      return self.lexStringQuote(matchingOpening: nil, leadingDelimiterLength: 0)
 
     case "`":
       return self.lexEscapedIdentifier()
@@ -1029,7 +1029,7 @@ extension Lexer.Cursor {
   private mutating func lexAfterRawStringDelimiter(delimiterLength: Int) -> Lexer.Result {
     switch self.peek() {
     case #"'"#, #"""#:
-      return self.lexStringQuote(isOpening: true, leadingDelimiterLength: delimiterLength)
+      return self.lexStringQuote(matchingOpening: nil, leadingDelimiterLength: delimiterLength)
     case nil:
       return Lexer.Result(.endOfFile)
     default:
@@ -1037,10 +1037,10 @@ extension Lexer.Cursor {
     }
   }
 
-  private mutating func lexAfterStringLiteral() -> Lexer.Result {
+  private mutating func lexAfterStringLiteral(stringLiteralKind: StringLiteralKind) -> Lexer.Result {
     switch self.peek() {
     case #"'"#, #"""#:
-      return self.lexStringQuote(isOpening: false, leadingDelimiterLength: 0)
+      return self.lexStringQuote(matchingOpening: stringLiteralKind, leadingDelimiterLength: 0)
     case nil:
       return Lexer.Result(.endOfFile)
     default:
@@ -1796,9 +1796,9 @@ extension Lexer.Cursor {
 extension Lexer.Cursor {
   private func stateTransitionAfterLexingStringQuote(kind: StringLiteralKind) -> Lexer.StateTransition {
     switch currentState {
-    case .afterStringLiteral(isRawString: true):
+    case .afterStringLiteral(kind: _, isRawString: true):
       return .replace(newState: .afterClosingStringQuote)
-    case .afterStringLiteral(isRawString: false):
+    case .afterStringLiteral(kind: _, isRawString: false):
       return .pop
     case .afterRawStringDelimiter(delimiterLength: let delimiterLength):
       return .replace(newState: .inStringLiteral(kind: kind, delimiterLength: delimiterLength))
@@ -1809,9 +1809,12 @@ extension Lexer.Cursor {
     }
   }
 
-  /// `isOpening` is `true` if this string quote is the opening quote of a string
-  /// literal and `false` if we are lexing the closing quote of a string literal.
-  mutating func lexStringQuote(isOpening: Bool, leadingDelimiterLength: Int) -> Lexer.Result {
+  /// `matchingOpening` is the opening literal kind if this string quote is the
+  /// closing quote of a string literal, `nil` if it's the opening quote.
+  mutating func lexStringQuote(
+    matchingOpening: StringLiteralKind?,
+    leadingDelimiterLength: Int
+  ) -> Lexer.Result {
     if self.advance(matching: "'") {
       return Lexer.Result(.singleQuote, stateTransition: stateTransitionAfterLexingStringQuote(kind: .singleQuote))
     }
@@ -1819,52 +1822,57 @@ extension Lexer.Cursor {
     let firstQuoteConsumed = self.advance(matching: #"""#)
     precondition(firstQuoteConsumed)
 
+    // Check to see if we have a multi-line delimiter. If we're matching an
+    // opening '"' then we want to bail since e.g `"a"""` shouldn't try to eat
+    // the '"""' as its closing delimiter.
     var lookingForMultilineString = self
-    if lookingForMultilineString.advance(matching: #"""#), lookingForMultilineString.advance(matching: #"""#) {
-      if leadingDelimiterLength > 0 {
-        // If this is a string literal, check if we have the closing delimiter on the same line to correctly parse things like `#"""#` as a single line string containing a quote.
-        var isSingleLineString = lookingForMultilineString
-
-        if isSingleLineString.advanceIfStringDelimiter(delimiterLength: leadingDelimiterLength) {
-          // If we have the correct number of delimiters now, we have something like `#"""#`.
-          // This is a single-line string.
-          return Lexer.Result(.stringQuote, stateTransition: stateTransitionAfterLexingStringQuote(kind: .singleLine))
-        }
+    if matchingOpening == .singleLine
+      || !(lookingForMultilineString.advance(matching: #"""#) && lookingForMultilineString.advance(matching: #"""#))
+    {
+      return Lexer.Result(.stringQuote, stateTransition: stateTransitionAfterLexingStringQuote(kind: .singleLine))
+    }
 
-        // Scan ahead until the end of the line. Every time we see a closing
-        // quote, check if it is followed by the correct number of closing delimiters.
-        while isSingleLineString.is(notAt: "\r", "\n") {
-          if isSingleLineString.advance(if: { $0 == #"""# }) {
-            if isSingleLineString.advanceIfStringDelimiter(delimiterLength: leadingDelimiterLength) {
-              return Lexer.Result(
-                .stringQuote,
-                stateTransition: stateTransitionAfterLexingStringQuote(kind: .singleLine)
-              )
-            }
-            continue
-          }
-          _ = isSingleLineString.advance()
-        }
+    if leadingDelimiterLength > 0 {
+      // If this is a string literal, check if we have the closing delimiter on the same line to correctly parse things like `#"""#` as a single line string containing a quote.
+      var isSingleLineString = lookingForMultilineString
+
+      if isSingleLineString.advanceIfStringDelimiter(delimiterLength: leadingDelimiterLength) {
+        // If we have the correct number of delimiters now, we have something like `#"""#`.
+        // This is a single-line string.
+        return Lexer.Result(.stringQuote, stateTransition: stateTransitionAfterLexingStringQuote(kind: .singleLine))
       }
 
-      self = lookingForMultilineString
-      let trailingTriviaLexingMode: TriviaLexingMode?
-      if isOpening && self.is(at: "\n", "\r") {
-        // The opening quote of a multi-line string literal must be followed by
-        // a newline that's not part of the represented string.
-        trailingTriviaLexingMode = .escapedNewlineInMultiLineStringLiteral
-      } else {
-        trailingTriviaLexingMode = nil
+      // Scan ahead until the end of the line. Every time we see a closing
+      // quote, check if it is followed by the correct number of closing delimiters.
+      while isSingleLineString.is(notAt: "\r", "\n") {
+        if isSingleLineString.advance(if: { $0 == #"""# }) {
+          if isSingleLineString.advanceIfStringDelimiter(delimiterLength: leadingDelimiterLength) {
+            return Lexer.Result(
+              .stringQuote,
+              stateTransition: stateTransitionAfterLexingStringQuote(kind: .singleLine)
+            )
+          }
+          continue
+        }
+        _ = isSingleLineString.advance()
       }
+    }
 
-      return Lexer.Result(
-        .multilineStringQuote,
-        stateTransition: stateTransitionAfterLexingStringQuote(kind: .multiLine),
-        trailingTriviaLexingMode: trailingTriviaLexingMode
-      )
+    self = lookingForMultilineString
+    let trailingTriviaLexingMode: TriviaLexingMode?
+    if matchingOpening == nil && self.is(at: "\n", "\r") {
+      // The opening quote of a multi-line string literal must be followed by
+      // a newline that's not part of the represented string.
+      trailingTriviaLexingMode = .escapedNewlineInMultiLineStringLiteral
     } else {
-      return Lexer.Result(.stringQuote, stateTransition: stateTransitionAfterLexingStringQuote(kind: .singleLine))
+      trailingTriviaLexingMode = nil
     }
+
+    return Lexer.Result(
+      .multilineStringQuote,
+      stateTransition: stateTransitionAfterLexingStringQuote(kind: .multiLine),
+      trailingTriviaLexingMode: trailingTriviaLexingMode
+    )
   }
 
   /// Returns `true` if the cursor is positioned at `\##(` with `delimiterLength`
@@ -1935,7 +1943,9 @@ extension Lexer.Cursor {
         return Lexer.Result(
           .stringSegment,
           error: error,
-          stateTransition: .replace(newState: .afterStringLiteral(isRawString: delimiterLength > 0))
+          stateTransition: .replace(
+            newState: .afterStringLiteral(kind: stringLiteralKind, isRawString: delimiterLength > 0)
+          )
         )
       default:
         break
@@ -1967,7 +1977,9 @@ extension Lexer.Cursor {
         return Lexer.Result(
           .stringSegment,
           error: error,
-          stateTransition: .replace(newState: .afterStringLiteral(isRawString: delimiterLength > 0))
+          stateTransition: .replace(
+            newState: .afterStringLiteral(kind: stringLiteralKind, isRawString: delimiterLength > 0)
+          )
         )
       }
     }


Test patch:
diff --git a/Tests/SwiftParserTest/ExpressionTests.swift b/Tests/SwiftParserTest/ExpressionTests.swift
index 40e31d34fc7..e47abffd938 100644
--- a/Tests/SwiftParserTest/ExpressionTests.swift
+++ b/Tests/SwiftParserTest/ExpressionTests.swift
@@ -1882,6 +1882,62 @@ final class ExpressionTests: ParserTestCase {
     )
   }
 
+  func testInvalidMultiLineClosingDelimiter() {
+    assertParse(
+      #"""
+      "a"1️⃣""2️⃣ a3️⃣ a4️⃣ℹ️"""5️⃣
+      """#,
+      diagnostics: [
+        DiagnosticSpec(
+          locationMarker: "1️⃣",
+          message: "consecutive statements on a line must be separated by newline or ';'",
+          fixIts: [
+            "insert newline", "insert ';'",
+          ]
+        ),
+        DiagnosticSpec(
+          locationMarker: "2️⃣",
+          message: "consecutive statements on a line must be separated by newline or ';'",
+          fixIts: [
+            "insert newline", "insert ';'",
+          ]
+        ),
+        DiagnosticSpec(
+          locationMarker: "3️⃣",
+          message: "consecutive statements on a line must be separated by newline or ';'",
+          fixIts: [
+            "insert newline", "insert ';'",
+          ]
+        ),
+        DiagnosticSpec(
+          locationMarker: "4️⃣",
+          message: "consecutive statements on a line must be separated by newline or ';'",
+          fixIts: [
+            "insert newline", "insert ';'",
+          ]
+        ),
+        DiagnosticSpec(
+          locationMarker: "5️⃣",
+          message: #"expected '"""' to end string literal"#,
+          notes: [
+            NoteSpec(message: #"to match this opening '"""'"#)
+          ],
+          fixIts: [
+            #"insert '"""'"#
+          ]
+        ),
+      ],
+      fixedSource: #"""
+        "a"
+        ""
+        a
+        a
+        """
+        """
+        """#
+    )
+  }
+
   func testEmptyLineInMultilineStringLiteral() {
     assertParse(
       #"""