Repository: apple/swift-syntax (swift)

Issue:
Parser should emit a single diagnostic if both open and close quote are missing from string literal
### Issue Kind

Bad Diagnostic Produced

### Source Code

```swift
@_expose(Cxx, baz) func foo() {}
```


### Description

From https://github.com/apple/swift-syntax/pull/1688/files#r1201105564

In the above example the parser produces two diagnostics: One for the missing opening `"` before `baz` and one for the missing closing `"` after `baz`. It should emit a single diagnostic instead that offers to add both.

Pull request description:
Resolve #1691 

When a string segment is located between missing quotes on both sides, looking ahead is interrupted, and each quote is diagnosed separately. 
I have modified the logic to continue looking ahead even if a standalone string segment is encountered during the search.
Furthermore, I added a condition to the `parentContextClause` in order to modify the diagnostic, but I am uncertain if it is the appropriate approach.
If there is a better solution available, please let me know. Thank you.

Patch:
diff --git a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift
index 70d2df47bf1..61228720d9d 100644
--- a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift
+++ b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift
@@ -1257,7 +1257,31 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
           node.closeQuote.id,
         ].compactMap { $0 }
       )
+    } else if node.openQuote.presence == .missing,
+      node.unexpectedBetweenOpenDelimiterAndOpenQuote == nil,
+      node.closeQuote.presence == .missing,
+      node.unexpectedBetweenCloseQuoteAndCloseDelimiter == nil,
+      !node.segments.isMissingAllTokens
+    {
+      addDiagnostic(
+        node,
+        MissingBothStringQuotesOfStringSegments(stringSegments: node.segments),
+        fixIts: [
+          FixIt(
+            message: InsertTokenFixIt(missingNodes: [Syntax(node.openQuote), Syntax(node.closeQuote)]),
+            changes: [
+              .makePresent(node.openQuote),
+              .makePresent(node.closeQuote),
+            ]
+          )
+        ],
+        handledNodes: [
+          node.openQuote.id,
+          node.closeQuote.id,
+        ]
+      )
     }
+
     for (diagnostic, handledNodes) in MultiLineStringLiteralIndentatinDiagnosticsGenerator.diagnose(node) {
       addDiagnostic(diagnostic, handledNodes: handledNodes)
     }
diff --git a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift
index 442cfde55da..80f5e7622da 100644
--- a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift
+++ b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift
@@ -386,6 +386,14 @@ public struct MissingAttributeArgument: ParserError {
   }
 }
 
+public struct MissingBothStringQuotesOfStringSegments: ParserError {
+  public let stringSegments: StringLiteralSegmentsSyntax
+
+  public var message: String {
+    return #"expected \#(stringSegments.shortSingleLineContentDescription) to be surrounded by '"'"#
+  }
+}
+
 public struct MissingConditionInStatement: ParserError {
   let node: SyntaxProtocol
 


Test patch:
diff --git a/Tests/SwiftParserTest/AttributeTests.swift b/Tests/SwiftParserTest/AttributeTests.swift
index 46fc15627ea..8901150cd76 100644
--- a/Tests/SwiftParserTest/AttributeTests.swift
+++ b/Tests/SwiftParserTest/AttributeTests.swift
@@ -454,19 +454,14 @@ final class AttributeTests: XCTestCase {
 
     assertParse(
       """
-      @_expose(Cxx, 1️⃣baz2️⃣) func foo() {}
+      @_expose(Cxx, 1️⃣baz) func foo() {}
       """,
       diagnostics: [
         DiagnosticSpec(
           locationMarker: "1️⃣",
-          message: #"expected '"' in string literal"#,
-          fixIts: [#"insert '"'"#]
-        ),
-        DiagnosticSpec(
-          locationMarker: "2️⃣",
-          message: #"expected '"' to end string literal"#,
-          fixIts: [#"insert '"'"#]
-        ),
+          message: #"expected code 'baz' to be surrounded by '"'"#,
+          fixIts: [#"insert '"' and '"'"#]
+        )
       ],
       fixedSource: """
         @_expose(Cxx, "baz") func foo() {}
@@ -543,20 +538,15 @@ final class AttributeTests: XCTestCase {
 
     assertParse(
       """
-      @_unavailableFromAsync(message: 1️⃣abc2️⃣)
+      @_unavailableFromAsync(message: 1️⃣abc)
       func foo() {}
       """,
       diagnostics: [
         DiagnosticSpec(
           locationMarker: "1️⃣",
-          message: #"expected '"' in string literal"#,
-          fixIts: [#"insert '"'"#]
-        ),
-        DiagnosticSpec(
-          locationMarker: "2️⃣",
-          message: #"expected '"' to end string literal"#,
-          fixIts: [#"insert '"'"#]
-        ),
+          message: #"expected code 'abc' to be surrounded by '"'"#,
+          fixIts: [#"insert '"' and '"'"#]
+        )
       ],
       fixedSource: """
         @_unavailableFromAsync(message: "abc")
diff --git a/Tests/SwiftParserTest/translated/OriginalDefinedInAttrTests.swift b/Tests/SwiftParserTest/translated/OriginalDefinedInAttrTests.swift
index 2d471a6b8d4..5af8c3e2e3e 100644
--- a/Tests/SwiftParserTest/translated/OriginalDefinedInAttrTests.swift
+++ b/Tests/SwiftParserTest/translated/OriginalDefinedInAttrTests.swift
@@ -86,13 +86,8 @@ final class OriginalDefinedInAttrTests: XCTestCase {
         ),
         DiagnosticSpec(
           locationMarker: "1️⃣",
-          message: #"expected '"' in string literal"#,
-          fixIts: [#"insert '"'"#]
-        ),
-        DiagnosticSpec(
-          locationMarker: "2️⃣",
-          message: #"expected '"' to end string literal"#,
-          fixIts: [#"insert '"'"#]
+          message: #"expected code 'OSX' to be surrounded by '"'"#,
+          fixIts: [#"insert '"' and '"'"#]
         ),
         DiagnosticSpec(
           locationMarker: "2️⃣",