Skip to content

Commit 22dd17e

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 713f08f commit 22dd17e

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
@@ -51,7 +51,7 @@ extension Lexer.Cursor {
5151
/// - A string interpolation inside is entered
5252
/// - A regex literal is being lexed
5353
/// - A narrow case for 'try?' and 'try!' to ensure correct regex lexing
54-
enum State {
54+
enum State: Equatable {
5555
/// Normal top-level lexing mode
5656
case normal
5757

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

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

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

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 @@ public 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)