Skip to content

Commit 10ca899

Browse files
committed
Don't allow single-line string literals to span multiple lines
There were a few cases where we could parse tokens that had leading trivia, causing us to incorrectly accept some single-line string literals that spanned multiple lines. Fix up these cases to ensure the tokens we parse don't have leading trivia.
1 parent 0686dc7 commit 10ca899

File tree

3 files changed

+156
-5
lines changed

3 files changed

+156
-5
lines changed

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: 16 additions & 5 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,7 +493,15 @@ 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)
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+
}
493505
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 ')'.
@@ -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: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,6 +1666,73 @@ final class StatementExpressionTests: XCTestCase {
16661666
)
16671667
}
16681668

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+
}
16691736

16701737
func testUnterminatedString6() {
16711738
AssertParse(
@@ -1681,6 +1748,69 @@ final class StatementExpressionTests: XCTestCase {
16811748
]
16821749
)
16831750
}
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️⃣
1771+
"""#,
1772+
diagnostics: [
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"#),
1776+
]
1777+
)
1778+
}
1779+
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+
16841814
func testStringLiteralAfterKeyPath() {
16851815
AssertParse(
16861816
#"""

0 commit comments

Comments
 (0)