Skip to content

Commit 698525c

Browse files
authored
Merge pull request #1419 from hamishknight/string-overflow
2 parents 9fe192e + 10ca899 commit 698525c

File tree

5 files changed

+178
-33
lines changed

5 files changed

+178
-33
lines changed

Sources/SwiftParser/Lexer/Cursor.swift

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ extension Lexer.Cursor {
8282
/// `stringInterpolationStart` points to the first character inside the interpolation.
8383
case inStringInterpolation(stringLiteralKind: StringLiteralKind, parenCount: Int)
8484

85-
/// We have parsed a string interpolation segment and are now expecting the closing `)`.
86-
case afterStringInterpolation
87-
8885
/// The mode in which leading trivia should be lexed for this state or `nil`
8986
/// if no trivia should be lexed.
9087
func leadingTriviaLexingMode(cursor: Lexer.Cursor) -> TriviaLexingMode? {
@@ -102,7 +99,6 @@ extension Lexer.Cursor {
10299
case .singleLine, .singleQuote: return .noNewlines
103100
case .multiLine: return .normal
104101
}
105-
case .afterStringInterpolation: return .normal
106102
}
107103
}
108104

@@ -117,7 +113,6 @@ extension Lexer.Cursor {
117113
case .afterClosingStringQuote: return nil
118114
case .inStringInterpolationStart: return nil
119115
case .inStringInterpolation: return .noNewlines
120-
case .afterStringInterpolation: return .noNewlines
121116
}
122117
}
123118

@@ -134,7 +129,6 @@ extension Lexer.Cursor {
134129
case .afterClosingStringQuote: return false
135130
case .inStringInterpolationStart: return false
136131
case .inStringInterpolation: return false
137-
case .afterStringInterpolation: return false
138132
}
139133
}
140134
}
@@ -333,8 +327,6 @@ extension Lexer.Cursor {
333327
result = lexInStringInterpolationStart(stringLiteralKind: stringLiteralKind)
334328
case .inStringInterpolation(stringLiteralKind: let stringLiteralKind, parenCount: let parenCount):
335329
result = lexInStringInterpolation(stringLiteralKind: stringLiteralKind, parenCount: parenCount, sourceBufferStart: sourceBufferStart)
336-
case .afterStringInterpolation:
337-
result = lexAfterStringInterpolation()
338330
}
339331

340332
if let stateTransition = result.stateTransition {
@@ -973,18 +965,6 @@ extension Lexer.Cursor {
973965
return self.lexNormal(sourceBufferStart: sourceBufferStart)
974966
}
975967
}
976-
977-
private mutating func lexAfterStringInterpolation() -> Lexer.Result {
978-
switch self.peek() {
979-
case UInt8(ascii: ")"):
980-
_ = self.advance()
981-
return Lexer.Result(.rightParen, stateTransition: .pop)
982-
case nil:
983-
return Lexer.Result(.eof)
984-
default:
985-
preconditionFailure("state 'isAfterStringInterpolation' expects to be positoned at ')'")
986-
}
987-
}
988968
}
989969

990970
// MARK: - Trivia
@@ -1797,7 +1777,7 @@ extension Lexer.Cursor {
17971777
error: error,
17981778
stateTransition: .push(newState: .inStringInterpolationStart(stringLiteralKind: stringLiteralKind))
17991779
)
1800-
} else if self.isAtEscapedNewline(delimiterLength: delimiterLength) {
1780+
} else if stringLiteralKind == .multiLine && self.isAtEscapedNewline(delimiterLength: delimiterLength) {
18011781
return Lexer.Result(
18021782
.stringSegment,
18031783
trailingTriviaLexingMode: .escapedNewlineInMultiLineStringLiteral

Sources/SwiftParser/Parser.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,16 @@ extension Parser {
308308
return missingToken(spec)
309309
}
310310
}
311+
312+
/// Same as `expectWithoutRecovery`, but also enforces that the token does
313+
/// not have any leading trivia. Otherwise, a missing token is synthesized.
314+
@inline(__always)
315+
mutating func expectWithoutRecoveryOrLeadingTrivia(_ spec: TokenSpec) -> Token {
316+
guard self.at(spec), currentToken.leadingTriviaText.isEmpty else {
317+
return missingToken(spec)
318+
}
319+
return self.eat(spec)
320+
}
311321
}
312322

313323
// MARK: Expecting Tokens with Recovery

Sources/SwiftParser/StringLiterals.swift

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,15 @@ extension Parser {
475475
var segments: [RawStringLiteralSegmentsSyntax.Element] = []
476476
var loopProgress = LoopProgressCondition()
477477
while loopProgress.evaluate(self.currentToken) {
478+
// If we encounter a token with leading trivia, we're no longer in the
479+
// string literal.
480+
guard currentToken.leadingTriviaText.isEmpty else { break }
481+
478482
if let stringSegment = self.consume(if: .stringSegment) {
479483
segments.append(.stringSegment(RawStringSegmentSyntax(content: stringSegment, arena: self.arena)))
480484
} else if let backslash = self.consume(if: .backslash) {
481485
let (unexpectedBeforeDelimiter, delimiter) = self.parseStringDelimiter(openDelimiter: openDelimiter)
482-
let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen)
486+
let leftParen = self.expectWithoutRecoveryOrLeadingTrivia(.leftParen)
483487
let expressions = RawTupleExprElementListSyntax(elements: self.parseArgumentListElements(pattern: .none), arena: self.arena)
484488

485489
// For recovery, eat anything up to the next token that either starts a new string segment or terminates the string.
@@ -489,8 +493,16 @@ extension Parser {
489493
while !self.at(.rightParen, .stringSegment, .backslash) && !self.at(TokenSpec(openQuoteKind), .eof) && unexpectedProgress.evaluate(self.currentToken) {
490494
unexpectedBeforeRightParen.append(self.consumeAnyToken())
491495
}
492-
let rightParen = self.expectWithoutRecovery(.rightParen)
493-
if rightParen.isMissing, case .inStringInterpolation = self.currentToken.cursor.currentState {
496+
// Consume the right paren if present, ensuring that it's on the same
497+
// line if this is a single-line literal. Leading trivia is fine as
498+
// we allow e.g "\(foo )".
499+
let rightParen: Token
500+
if self.at(.rightParen) && self.currentToken.isAtStartOfLine && openQuote.tokenKind != .multilineStringQuote {
501+
rightParen = missingToken(.rightParen)
502+
} else {
503+
rightParen = self.expectWithoutRecovery(.rightParen)
504+
}
505+
if case .inStringInterpolation = self.currentToken.cursor.currentState {
494506
// The parser has more knowledge that we have reached the end of the
495507
// string interpolation now, even if we haven't seen the closing ')'.
496508
// For example, consider the following code
@@ -509,7 +521,6 @@ extension Parser {
509521
backslash: backslash,
510522
unexpectedBeforeDelimiter,
511523
delimiter: delimiter,
512-
unexpectedBeforeLeftParen,
513524
leftParen: leftParen,
514525
expressions: expressions,
515526
RawUnexpectedNodesSyntax(unexpectedBeforeRightParen, arena: self.arena),
@@ -527,12 +538,12 @@ extension Parser {
527538
let unexpectedBeforeCloseQuote: RawUnexpectedNodesSyntax?
528539
let closeQuote: RawTokenSyntax
529540
if openQuoteKind == .singleQuote {
530-
let singleQuote = self.expectWithoutRecovery(.singleQuote)
541+
let singleQuote = self.expectWithoutRecoveryOrLeadingTrivia(.singleQuote)
531542
unexpectedBeforeCloseQuote = RawUnexpectedNodesSyntax([singleQuote], arena: self.arena)
532543
closeQuote = missingToken(.stringQuote)
533544
} else {
534545
unexpectedBeforeCloseQuote = nil
535-
closeQuote = self.expectWithoutRecovery(TokenSpec(openQuote.tokenKind))
546+
closeQuote = self.expectWithoutRecoveryOrLeadingTrivia(TokenSpec(openQuote.tokenKind))
536547
}
537548

538549
let (unexpectedBeforeCloseDelimiter, closeDelimiter) = self.parseStringDelimiter(openDelimiter: openDelimiter)

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,17 +1656,161 @@ final class StatementExpressionTests: XCTestCase {
16561656
func testUnterminatedInterpolationAtEndOfMultilineStringLiteral() {
16571657
AssertParse(
16581658
#"""
1659-
"""\({(1️⃣})
1660-
2️⃣"""3️⃣
1659+
"""1️⃣\({(2️⃣})
1660+
"""
1661+
"""#,
1662+
diagnostics: [
1663+
DiagnosticSpec(locationMarker: "1️⃣", message: "multi-line string literal content must begin on a new line"),
1664+
DiagnosticSpec(locationMarker: "2️⃣", message: "expected value and ')' to end tuple"),
1665+
]
1666+
)
1667+
}
1668+
1669+
func testUnterminatedString1() {
1670+
AssertParse(
1671+
#"""
1672+
"abc1️⃣
1673+
"2️⃣
1674+
"""#,
1675+
diagnostics: [
1676+
DiagnosticSpec(locationMarker: "1️⃣", message: #"expected '"' to end string literal"#),
1677+
DiagnosticSpec(locationMarker: "2️⃣", message: #"expected '"' to end string literal"#),
1678+
]
1679+
)
1680+
}
1681+
1682+
func testUnterminatedString2() {
1683+
AssertParse(
1684+
#"""
1685+
"1️⃣
1686+
"2️⃣
1687+
"""#,
1688+
diagnostics: [
1689+
DiagnosticSpec(locationMarker: "1️⃣", message: #"expected '"' to end string literal"#),
1690+
DiagnosticSpec(locationMarker: "2️⃣", message: #"expected '"' to end string literal"#),
1691+
]
1692+
)
1693+
}
1694+
1695+
func testUnterminatedString3() {
1696+
AssertParse(
1697+
#"""
1698+
"abc1️⃣
1699+
\(def)2️⃣"3️⃣
1700+
"""#,
1701+
diagnostics: [
1702+
DiagnosticSpec(locationMarker: "1️⃣", message: #"expected '"' to end string literal"#),
1703+
DiagnosticSpec(locationMarker: "2️⃣", message: "consecutive statements on a line must be separated by ';'"),
1704+
DiagnosticSpec(locationMarker: "3️⃣", message: #"expected '"' to end string literal"#),
1705+
]
1706+
)
1707+
}
1708+
1709+
func testUnterminatedString4() {
1710+
AssertParse(
1711+
#"""
1712+
"abc\(def1️⃣2️⃣
1713+
3️⃣)"
1714+
"""#,
1715+
diagnostics: [
1716+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected ')' in string literal"),
1717+
DiagnosticSpec(locationMarker: "2️⃣", message: #"expected '"' to end string literal"#),
1718+
DiagnosticSpec(locationMarker: "3️⃣", message: #"extraneous code ')"' at top level"#),
1719+
]
1720+
)
1721+
}
1722+
1723+
func testUnterminatedString5() {
1724+
AssertParse(
1725+
#"""
1726+
"abc\(1️⃣2️⃣
1727+
def3️⃣)"
1728+
"""#,
1729+
diagnostics: [
1730+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected value and ')' in string literal"),
1731+
DiagnosticSpec(locationMarker: "2️⃣", message: #"expected '"' to end string literal"#),
1732+
DiagnosticSpec(locationMarker: "3️⃣", message: #"extraneous code ')"' at top level"#),
1733+
]
1734+
)
1735+
}
1736+
1737+
func testUnterminatedString6() {
1738+
AssertParse(
1739+
#"""
1740+
"abc1️⃣\2️⃣
1741+
(def)3️⃣"4️⃣
1742+
"""#,
1743+
diagnostics: [
1744+
DiagnosticSpec(locationMarker: "1️⃣", message: "invalid escape sequence in literal"),
1745+
DiagnosticSpec(locationMarker: "2️⃣", message: #"expected '"' to end string literal"#),
1746+
DiagnosticSpec(locationMarker: "3️⃣", message: "consecutive statements on a line must be separated by ';'"),
1747+
DiagnosticSpec(locationMarker: "4️⃣", message: #"expected '"' to end string literal"#),
1748+
]
1749+
)
1750+
}
1751+
1752+
func testUnterminatedString7() {
1753+
AssertParse(
1754+
#"""
1755+
#1️⃣
1756+
"abc"2️⃣#3️⃣
1757+
"""#,
1758+
diagnostics: [
1759+
DiagnosticSpec(locationMarker: "1️⃣", message: "expected identifier in macro expansion"),
1760+
DiagnosticSpec(locationMarker: "2️⃣", message: "consecutive statements on a line must be separated by ';'"),
1761+
DiagnosticSpec(locationMarker: "3️⃣", message: "expected identifier in macro expansion"),
1762+
]
1763+
)
1764+
}
1765+
1766+
func testUnterminatedString8() {
1767+
AssertParse(
1768+
#"""
1769+
#"1️⃣
1770+
abc2️⃣"#3️⃣
16611771
"""#,
16621772
diagnostics: [
1663-
DiagnosticSpec(locationMarker: "1️⃣", message: "expected value and ')' to end tuple"),
1664-
DiagnosticSpec(locationMarker: "2️⃣", message: #"unexpected code '"""' in string literal"#),
1665-
DiagnosticSpec(locationMarker: "3️⃣", message: #"expected '"""' to end string literal"#),
1773+
DiagnosticSpec(locationMarker: "1️⃣", message: ##"expected '"#' to end string literal"##),
1774+
DiagnosticSpec(locationMarker: "2️⃣", message: "consecutive statements on a line must be separated by ';'"),
1775+
DiagnosticSpec(locationMarker: "3️⃣", message: #"expected '"' to end string literal"#),
16661776
]
16671777
)
16681778
}
16691779
1780+
func testUnterminatedString9() {
1781+
AssertParse(
1782+
#"""
1783+
#"abc1️⃣
1784+
"#2️⃣
1785+
"""#,
1786+
diagnostics: [
1787+
DiagnosticSpec(locationMarker: "1️⃣", message: ##"expected '"#' to end string literal"##),
1788+
DiagnosticSpec(locationMarker: "2️⃣", message: #"expected '"' to end string literal"#),
1789+
]
1790+
)
1791+
}
1792+
1793+
func testUnterminatedString10() {
1794+
AssertParse(
1795+
#"""
1796+
#"abc"1️⃣
1797+
#2️⃣
1798+
"""#,
1799+
diagnostics: [
1800+
DiagnosticSpec(locationMarker: "1️⃣", message: ##"expected '"#' to end string literal"##),
1801+
DiagnosticSpec(locationMarker: "2️⃣", message: "expected identifier in macro expansion"),
1802+
]
1803+
)
1804+
}
1805+
1806+
func testTriviaEndingInterpolation() {
1807+
AssertParse(
1808+
#"""
1809+
"abc\(def )"
1810+
"""#
1811+
)
1812+
}
1813+
16701814
func testStringLiteralAfterKeyPath() {
16711815
AssertParse(
16721816
#"""

Tests/SwiftParserTest/translated/UnclosedStringInterpolationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ final class UnclosedStringInterpolationTests: XCTestCase {
111111
func testSkipUnexpectedOpeningParensInStringLiteral() {
112112
AssertParse(
113113
#"""
114-
"\(e 1️⃣H()2️⃣r
114+
"\(e 1️⃣H()r2️⃣
115115
"""#,
116116
diagnostics: [
117117
DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code 'H(' in string literal"),

0 commit comments

Comments
 (0)