Skip to content

Commit 2507941

Browse files
committed
Fix a false negative assertion failure in LoopProgressCondition for unterminated nested string interpolation
If we have two nested, unterminated string interpolation segments, the lexer generates two empty `stringLiteral` tokens (one after each interpolation segment). When consuming the first empty string segment, we did actually make progress in the lexer by popping one nested string interpolation off the state stack. However, `LoopProgressCondition` did not consider this progress because it only looked at the top state in the state stack. To fix this, consider the state stack size in `LoopProgressCondition` as well. Fixes #2533 rdar://124168557
1 parent d9bf34e commit 2507941

File tree

4 files changed

+73
-9
lines changed

4 files changed

+73
-9
lines changed

Sources/SwiftParser/Lexer/Cursor.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ extension Lexer.Cursor {
5252
/// - A string interpolation inside is entered
5353
/// - A regex literal is being lexed
5454
/// - A narrow case for 'try?' and 'try!' to ensure correct regex lexing
55-
enum State {
55+
enum State: Equatable {
5656
/// Normal top-level lexing mode
5757
case normal
5858

@@ -203,6 +203,11 @@ extension Lexer.Cursor {
203203
}
204204
}
205205
}
206+
207+
/// See `Lexer.Cursor.hasProgressed(comparedTo:)`.
208+
fileprivate func hasProgressed(comparedTo other: StateStack) -> Bool {
209+
return currentState != other.currentState || stateStack?.count != other.stateStack?.count
210+
}
206211
}
207212

208213
/// An error that was discovered in a lexeme while lexing it.
@@ -257,6 +262,16 @@ extension Lexer {
257262
self.position = Position(input: input, previous: previous)
258263
}
259264

265+
/// Returns `true` if this cursor is sufficiently different to `other` in a way that indicates that the lexer has
266+
/// made progress.
267+
///
268+
/// This is the case if the lexer advanced its position in the source file or if it has performed a state
269+
/// transition.
270+
func hasProgressed(comparedTo other: Cursor) -> Bool {
271+
return position.input.baseAddress != other.position.input.baseAddress
272+
|| stateStack.hasProgressed(comparedTo: other.stateStack)
273+
}
274+
260275
var input: UnsafeBufferPointer<UInt8> { position.input }
261276
var previous: UInt8 { position.previous }
262277

Sources/SwiftParser/LoopProgressCondition.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,7 @@ struct LoopProgressCondition {
2929
guard let previousToken = self.currentToken else {
3030
return true
3131
}
32-
// The loop has made progress if either
33-
// - the parser is now pointing at a different location in the source file
34-
// - the parser is still pointing at the same position in the source file
35-
// but now has a different token kind (and thus consumed a zero-length
36-
// token like an empty string interpolation
37-
let hasMadeProgress =
38-
previousToken.tokenText.baseAddress != currentToken.tokenText.baseAddress
39-
|| (previousToken.byteLength == 0 && previousToken.rawTokenKind != currentToken.rawTokenKind)
32+
let hasMadeProgress = currentToken.cursor.hasProgressed(comparedTo: previousToken.cursor)
4033
assert(hasMadeProgress, "Loop should always make progress")
4134
return hasMadeProgress
4235
}

Tests/SwiftParserTest/LexerTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,4 +1561,26 @@ class LexerTests: ParserTestCase {
15611561
]
15621562
)
15631563
}
1564+
1565+
func testNestedUnterminatedStringInterpolations() {
1566+
assertLexemes(
1567+
#"""
1568+
"\("\(
1569+
1570+
"""#,
1571+
lexemes: [
1572+
LexemeSpec(.stringQuote, text: #"""#),
1573+
LexemeSpec(.stringSegment, text: ""),
1574+
LexemeSpec(.backslash, text: #"\"#),
1575+
LexemeSpec(.leftParen, text: "("),
1576+
LexemeSpec(.stringQuote, text: #"""#),
1577+
LexemeSpec(.stringSegment, text: ""),
1578+
LexemeSpec(.backslash, text: #"\"#),
1579+
LexemeSpec(.leftParen, text: "("),
1580+
LexemeSpec(.stringSegment, text: ""),
1581+
LexemeSpec(.stringSegment, text: ""),
1582+
LexemeSpec(.endOfFile, leading: "\n", text: "", flags: [.isAtStartOfLine]),
1583+
]
1584+
)
1585+
}
15641586
}

Tests/SwiftParserTest/translated/UnclosedStringInterpolationTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,38 @@ final class UnclosedStringInterpolationTests: ParserTestCase {
247247
"""#
248248
)
249249
}
250+
251+
func testNestedUnterminatedStringInterpolation() {
252+
assertParse(
253+
#"""
254+
1️⃣"\2️⃣(3️⃣"\(4️⃣
255+
256+
"""#,
257+
diagnostics: [
258+
DiagnosticSpec(locationMarker: "4️⃣", message: "expected value and ')' in string literal", fixIts: ["insert value and ')'"]),
259+
DiagnosticSpec(
260+
locationMarker: "4️⃣",
261+
message: #"expected '"' to end string literal"#,
262+
notes: [NoteSpec(locationMarker: "3️⃣", message: #"to match this opening '"'"#)],
263+
fixIts: [#"insert '"'"#]
264+
),
265+
DiagnosticSpec(
266+
locationMarker: "4️⃣",
267+
message: "expected ')' in string literal",
268+
notes: [NoteSpec(locationMarker: "2️⃣", message: "to match this opening '('")],
269+
fixIts: ["insert ')'"]
270+
),
271+
DiagnosticSpec(
272+
locationMarker: "4️⃣",
273+
message: #"expected '"' to end string literal"#,
274+
notes: [NoteSpec(locationMarker: "1️⃣", message: #"to match this opening '"'"#)],
275+
fixIts: [#"insert '"'"#]
276+
),
277+
],
278+
fixedSource: #"""
279+
"\("\(<#expression#>)")"
280+
281+
"""#
282+
)
283+
}
250284
}

0 commit comments

Comments
 (0)