Skip to content

Commit f5c0823

Browse files
committed
An escaped backslash at the end of a line in a mulit-line string literal does not escape the newline
1 parent 1e28b74 commit f5c0823

File tree

10 files changed

+567
-25
lines changed

10 files changed

+567
-25
lines changed

CodeGeneration/Sources/SyntaxSupport/gyb_generated/Trivia.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ public let TRIVIAS: [Trivia] = [
142142
swiftCharacters: [
143143
Character("\\")
144144
]),
145+
Trivia(name: "Pound",
146+
comment: #"A '#' that is at the end of a line in a multi-line string literal to escape the newline."#,
147+
characters: [
148+
Character("#")
149+
],
150+
swiftCharacters: [
151+
Character("#")
152+
]),
145153
Trivia(name: "UnexpectedText",
146154
comment: #"Any skipped unexpected text."#),
147155
Trivia(name: "Shebang",

Sources/SwiftParser/StringLiterals.swift

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -136,24 +136,32 @@ extension Parser {
136136
/// not part of the represented string. If the last line has its newline
137137
/// escaped by a trailing `\`, mark that string segment as unexpected and
138138
/// generate a missing segment that doesn't have a trailing `\`.
139-
private func reclassifyNewlineOfLastSegmentAsTrivia(firstSegment: RawStringSegmentSyntax?, middleSegments: inout [RawStringLiteralSegmentsSyntax.Element]) -> Bool {
139+
private func reclassifyNewlineOfLastSegmentAsTrivia(rawStringDelimitersToken: RawTokenSyntax?, firstSegment: RawStringSegmentSyntax?, middleSegments: inout [RawStringLiteralSegmentsSyntax.Element]) -> Bool {
140140
switch middleSegments.last {
141141
case .stringSegment(let lastMiddleSegment):
142142
if let newlineSuffix = lastMiddleSegment.content.tokenText.newlineSuffix {
143143
// The newline at the end of the last line in the string literal is not part of the represented string.
144144
// Mark it as trivia.
145145
var content = lastMiddleSegment.content.reclassifyAsTrailingTrivia([newlineSuffix], arena: self.arena)
146146
var unexpectedBeforeContent: RawTokenSyntax?
147-
if content.tokenText.hasSuffix("\\") {
148-
// The newline on the last line must not be escaped
149-
unexpectedBeforeContent = content
150-
content = RawTokenSyntax(
151-
missing: .stringSegment,
152-
text: SyntaxText(rebasing: content.tokenText[0..<content.tokenText.count - 1]),
153-
leadingTriviaPieces: content.leadingTriviaPieces,
154-
trailingTriviaPieces: content.trailingTriviaPieces,
155-
arena: self.arena
156-
)
147+
if content.tokenText.hasNonEscapedBackslashSuffix(rawStringDelimiters: rawStringDelimitersToken?.tokenText ?? "", newline: "") {
148+
// The newline on the last line must not be escaped in non-raw string literals.
149+
150+
if let rawStringDelimitersToken = rawStringDelimitersToken {
151+
// ... except in raw string literals where the C++ parser accepts the
152+
// last line to be escaped. To match the C++ parser's behavior, we also
153+
// need to allow escaped newline in raw string literals.
154+
content = content.reclassifyAsTrailingTrivia([.backslashes(1), .pounds(rawStringDelimitersToken.tokenText.count)], arena: self.arena)
155+
} else {
156+
unexpectedBeforeContent = content
157+
content = RawTokenSyntax(
158+
missing: .stringSegment,
159+
text: SyntaxText(rebasing: content.tokenText[0..<content.tokenText.count - 1]),
160+
leadingTriviaPieces: content.leadingTriviaPieces,
161+
trailingTriviaPieces: content.trailingTriviaPieces,
162+
arena: self.arena
163+
)
164+
}
157165
}
158166

159167
middleSegments[middleSegments.count - 1] = .stringSegment(
@@ -185,6 +193,7 @@ extension Parser {
185193
/// `.insufficientIndentationInMultilineStringLiteral` lexer error will be
186194
/// attached to the string segment token.
187195
private func postProcessIndentationAndEscapedNewlineOfMiddleSegments(
196+
rawStringDelimitersToken: RawTokenSyntax?,
188197
middleSegments: inout [RawStringLiteralSegmentsSyntax.Element],
189198
isFirstSegmentOnNewLine: Bool,
190199
indentation: SyntaxText,
@@ -231,16 +240,19 @@ extension Parser {
231240

232241
isSegmentOnNewLine = segment.content.tokenText.newlineSuffix != nil
233242

243+
let rawDelimiters = rawStringDelimitersToken?.tokenText ?? ""
244+
assert(rawDelimiters.allSatisfy({ $0 == UInt8(ascii: "#") }))
245+
234246
// If the segment has a `\` in front of its trailing newline, that newline
235247
// is not part of the represented string and should be trivia.
236248

237249
let backslashNewlineSuffix: [RawTriviaPiece]?
238-
if segment.content.tokenText.hasSuffix("\\\r\n") {
239-
backslashNewlineSuffix = [.backslashes(1), .carriageReturnLineFeeds(1)]
240-
} else if segment.content.tokenText.hasSuffix("\\\n") {
241-
backslashNewlineSuffix = [.backslashes(1), .newlines(1)]
242-
} else if segment.content.tokenText.hasSuffix("\\\r") {
243-
backslashNewlineSuffix = [.backslashes(1), .carriageReturns(1)]
250+
if segment.content.tokenText.hasNonEscapedBackslashSuffix(rawStringDelimiters: rawDelimiters, newline: "\r\n") {
251+
backslashNewlineSuffix = [.backslashes(1), .pounds(rawDelimiters.count), .carriageReturnLineFeeds(1)].filter { $0.byteLength > 0 }
252+
} else if segment.content.tokenText.hasNonEscapedBackslashSuffix(rawStringDelimiters: rawDelimiters, newline: "\n") {
253+
backslashNewlineSuffix = [.backslashes(1), .pounds(rawDelimiters.count), .newlines(1)].filter { $0.byteLength > 0 }
254+
} else if segment.content.tokenText.hasNonEscapedBackslashSuffix(rawStringDelimiters: rawDelimiters, newline: "\r") {
255+
backslashNewlineSuffix = [.backslashes(1), .pounds(rawDelimiters.count), .carriageReturns(1)].filter { $0.byteLength > 0 }
244256
} else {
245257
backslashNewlineSuffix = nil
246258
}
@@ -281,6 +293,7 @@ extension Parser {
281293
/// escaped by a trailing `\`, mark that string segment as unexpected and
282294
/// generate a missing segment that doesn't have a trailing `\`.
283295
private func postProcessMultilineStringLiteral(
296+
rawStringDelimitersToken: RawTokenSyntax?,
284297
openQuote: RawTokenSyntax,
285298
segments allSegments: [RawStringLiteralSegmentsSyntax.Element],
286299
closeQuote: RawTokenSyntax
@@ -331,6 +344,7 @@ extension Parser {
331344
// Check that the close quote is on new line
332345

333346
let closeDelimiterOnNewLine = reclassifyNewlineOfLastSegmentAsTrivia(
347+
rawStringDelimitersToken: rawStringDelimitersToken,
334348
firstSegment: firstSegment,
335349
middleSegments: &middleSegments
336350
)
@@ -391,6 +405,7 @@ extension Parser {
391405
// Check indentation of segments and escaped newlines at end of segment
392406

393407
postProcessIndentationAndEscapedNewlineOfMiddleSegments(
408+
rawStringDelimitersToken: rawStringDelimitersToken,
394409
middleSegments: &middleSegments,
395410
isFirstSegmentOnNewLine: isFirstSegmentOnNewLine,
396411
indentation: indentation,
@@ -525,7 +540,7 @@ extension Parser {
525540
let (unexpectedBeforeCloseDelimiter, closeDelimiter) = self.parseStringDelimiter(openDelimiter: openDelimiter)
526541

527542
if openQuote.tokenKind == .multilineStringQuote, !openQuote.isMissing, !closeQuote.isMissing {
528-
let postProcessed = postProcessMultilineStringLiteral(openQuote: openQuote, segments: segments, closeQuote: closeQuote)
543+
let postProcessed = postProcessMultilineStringLiteral(rawStringDelimitersToken: openDelimiter, openQuote: openQuote, segments: segments, closeQuote: closeQuote)
529544
return RawStringLiteralExprSyntax(
530545
openDelimiter: openDelimiter,
531546
RawUnexpectedNodesSyntax(combining: unexpectedBeforeOpenQuote, postProcessed.unexpectedBeforeOpenQuote, arena: self.arena),
@@ -552,3 +567,20 @@ extension Parser {
552567
}
553568
}
554569
}
570+
571+
// MARK: - Utilities
572+
573+
fileprivate extension SyntaxText {
574+
private func hasSuffix(_ other: String) -> Bool {
575+
var other = other
576+
return other.withSyntaxText { self.hasSuffix($0) }
577+
}
578+
579+
/// Returns `true` if this string end with `\<rawStringDelimiters><newline>`
580+
/// but the backslash is not escaped, i.e. doesn't end with
581+
/// `\\<rawStringDelimiters><newline>`
582+
func hasNonEscapedBackslashSuffix(rawStringDelimiters: SyntaxText, newline: SyntaxText) -> Bool {
583+
return self.hasSuffix("\\\(rawStringDelimiters)\(newline)")
584+
&& !self.hasSuffix("\\\\\(rawStringDelimiters)\(newline)")
585+
}
586+
}

Sources/SwiftParser/TriviaParser.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ public struct TriviaParser {
8989
pieces.append(.shebang(start.text(upTo: cursor)))
9090
continue
9191
}
92+
cursor.advance(while: { $0 == "#" })
93+
pieces.append(.pounds(start.distance(to: cursor)))
94+
continue
9295

9396
case UInt8(ascii: "<"), UInt8(ascii: ">"):
9497
// SCM conflict markers.

Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ extension Trivia {
176176
return Array(repeating: TriviaPiece.newlines(1), count: count)
177177
case .backslashes(let count):
178178
return Array(repeating: TriviaPiece.backslashes(1), count: count)
179+
case .pounds(let count):
180+
return Array(repeating: TriviaPiece.pounds(1), count: count)
179181
case .carriageReturns(let count):
180182
return Array(repeating: TriviaPiece.carriageReturns(1), count: count)
181183
case .carriageReturnLineFeeds(let count):

Sources/SwiftSyntax/SourceLocation.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,8 @@ fileprivate extension RawTriviaPiece {
475475
let .tabs(count),
476476
let .verticalTabs(count),
477477
let .formfeeds(count),
478-
let .backslashes(count):
478+
let .backslashes(count),
479+
let .pounds(count):
479480
lineLength += SourceLength(utf8Length: count)
480481
case let .newlines(count),
481482
let .carriageReturns(count):

Sources/SwiftSyntax/generated/Trivia.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ public enum TriviaPiece {
6565
/// A backslash that is at the end of a line in a multi-line string literal to escape the newline.
6666
case backslashes(Int)
6767

68+
/// A '#' that is at the end of a line in a multi-line string literal to escape the newline.
69+
case pounds(Int)
70+
6871
/// Any skipped unexpected text.
6972
case unexpectedText(String)
7073

@@ -107,6 +110,8 @@ extension TriviaPiece: TextOutputStreamable {
107110
target.write(text)
108111
case let .backslashes(count):
109112
printRepeated(#"\"#, count: count)
113+
case let .pounds(count):
114+
printRepeated("#", count: count)
110115
case let .unexpectedText(text):
111116
target.write(text)
112117
case let .shebang(text):
@@ -143,6 +148,8 @@ extension TriviaPiece: CustomDebugStringConvertible {
143148
return "docBlockComment(\(name.debugDescription))"
144149
case .backslashes(let data):
145150
return "backslashes(\(data))"
151+
case .pounds(let data):
152+
return "pounds(\(data))"
146153
case .unexpectedText(let name):
147154
return "unexpectedText(\(name.debugDescription))"
148155
case .shebang(let name):
@@ -303,6 +310,16 @@ public struct Trivia {
303310
return .backslashes(1)
304311
}
305312

313+
/// Returns a piece of trivia for some number of "#" characters.
314+
public static func pounds(_ count: Int) -> Trivia {
315+
return [.pounds(count)]
316+
}
317+
318+
/// Gets a piece of trivia for "#" characters.
319+
public static var pound: Trivia {
320+
return .pounds(1)
321+
}
322+
306323
/// Returns a piece of trivia for UnexpectedText.
307324
public static func unexpectedText(_ text: String) -> Trivia {
308325
return [.unexpectedText(text)]
@@ -418,6 +435,8 @@ extension TriviaPiece {
418435
return SourceLength(of: text)
419436
case let .backslashes(count):
420437
return SourceLength(utf8Length: count)
438+
case let .pounds(count):
439+
return SourceLength(utf8Length: count)
421440
case let .unexpectedText(text):
422441
return SourceLength(of: text)
423442
case let .shebang(text):
@@ -456,6 +475,8 @@ public enum RawTriviaPiece: Equatable {
456475

457476
case backslashes(Int)
458477

478+
case pounds(Int)
479+
459480
case unexpectedText(SyntaxText)
460481

461482
case shebang(SyntaxText)
@@ -486,6 +507,8 @@ public enum RawTriviaPiece: Equatable {
486507
return .docBlockComment(arena.intern(text))
487508
case let .backslashes(count):
488509
return .backslashes(count)
510+
case let .pounds(count):
511+
return .pounds(count)
489512
case let .unexpectedText(text):
490513
return .unexpectedText(arena.intern(text))
491514
case let .shebang(text):
@@ -533,6 +556,8 @@ extension TriviaPiece {
533556
self = .docBlockComment(String(syntaxText: text))
534557
case let .backslashes(count):
535558
self = .backslashes(count)
559+
case let .pounds(count):
560+
self = .pounds(count)
536561
case let .unexpectedText(text):
537562
self = .unexpectedText(String(syntaxText: text))
538563
case let .shebang(text):
@@ -568,6 +593,8 @@ extension RawTriviaPiece {
568593
return text.count
569594
case let .backslashes(count):
570595
return count
596+
case let .pounds(count):
597+
return count
571598
case let .unexpectedText(text):
572599
return text.count
573600
case let .shebang(text):
@@ -601,6 +628,8 @@ extension RawTriviaPiece {
601628
return text
602629
case .backslashes(_):
603630
return nil
631+
case .pounds(_):
632+
return nil
604633
case .unexpectedText(let text):
605634
return text
606635
case .shebang(let text):

Tests/SwiftParserTest/TriviaParserTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ final class TriviaParserTests: XCTestCase {
6262
position: .trailing
6363
),
6464
[
65-
.unexpectedText("#!/bin/env"),
65+
.pounds(1),
66+
.unexpectedText("!/bin/env"),
6667
.spaces(1),
6768
.unexpectedText("swift"),
6869
]

0 commit comments

Comments
 (0)