Skip to content

Commit 18cd7f9

Browse files
authored
Merge pull request #2543 from ahoppen/ahoppen/loop-progress-condition-false-negative
Fix a false negative assertion failure in `LoopProgressCondition` for unterminated nested string interpolation
2 parents d9bf34e + 22dd17e commit 18cd7f9

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)