Skip to content

Commit 319f8c5

Browse files
committed
Fix multi-line string literal errors with line endings other than \n
1 parent 67f0564 commit 319f8c5

File tree

6 files changed

+154
-60
lines changed

6 files changed

+154
-60
lines changed

Sources/SwiftParser/Lexer/Cursor.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1773,7 +1773,10 @@ extension Lexer.Cursor {
17731773
if stringLiteralKind == .multiLine {
17741774
// Make sure each line starts a new string segment so the parser can
17751775
// validate the multi-line string literal's indentation.
1776-
_ = self.advance()
1776+
let charcter = self.advance()
1777+
if charcter == UInt8(ascii: "\r") {
1778+
_ = self.advance(matching: "\n")
1779+
}
17771780
return Lexer.Result(.stringSegment)
17781781
} else {
17791782
// Single line literals cannot span multiple lines.

Sources/SwiftParser/StringLiterals.swift

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fileprivate class StringLiteralExpressionIndentationChecker {
4747
return nil
4848
}
4949
let hasSufficientIndentation = token.tokenView.leadingTrivia { leadingTrivia in
50-
let indentationStartIndex = leadingTrivia.lastIndex(where: { $0 == UInt8(ascii: "\n") || $0 == UInt8(ascii: "\n") })?.advanced(by: 1) ?? leadingTrivia.startIndex
50+
let indentationStartIndex = leadingTrivia.lastIndex(where: { $0 == UInt8(ascii: "\n") || $0 == UInt8(ascii: "\r") })?.advanced(by: 1) ?? leadingTrivia.startIndex
5151
return SyntaxText(rebasing: leadingTrivia[indentationStartIndex...]).hasPrefix(expectedIndentation)
5252
}
5353
if hasSufficientIndentation {
@@ -86,6 +86,36 @@ fileprivate class StringLiteralExpressionIndentationChecker {
8686
}
8787
}
8888

89+
fileprivate extension SyntaxText {
90+
/// If the text ends with any newline character, return the trivia for that
91+
/// newline, otherwise `nil`.
92+
var newlineSuffix: RawTriviaPiece? {
93+
if hasSuffix("\r\n") {
94+
return .carriageReturnLineFeeds(1)
95+
} else if hasSuffix("\n") {
96+
return .newlines(1)
97+
} else if hasSuffix("\r") {
98+
return .carriageReturns(1)
99+
} else {
100+
return nil
101+
}
102+
}
103+
104+
/// If the text is a single newline character, return the trivia piece that
105+
/// represents it, otherwise `nil`.
106+
var triviaPieceIfNewline: RawTriviaPiece? {
107+
if self == "\r\n" {
108+
return .carriageReturnLineFeeds(1)
109+
} else if self == "\n" {
110+
return .newlines(1)
111+
} else if self == "\r" {
112+
return .carriageReturns(1)
113+
} else {
114+
return nil
115+
}
116+
}
117+
}
118+
89119
extension Parser {
90120
/// Consumes a raw string delimiter that has the same number of `#` as `openDelimiter`.
91121
private mutating func parseStringDelimiter(openDelimiter: RawTokenSyntax?) -> (unexpectedBeforeCheckedDelimiter: RawUnexpectedNodesSyntax?, checkedDelimiter: RawTokenSyntax?) {
@@ -129,7 +159,6 @@ extension Parser {
129159
}
130160
}
131161

132-
// FIXME: Handle \r and \r\n if needed in here
133162
private func postProcessMultilineStringLiteral(
134163
openQuote: RawTokenSyntax,
135164
segments allSegments: [RawStringLiteralSegmentsSyntax.Element],
@@ -181,10 +210,10 @@ extension Parser {
181210
let closeDelimiterOnNewLine: Bool
182211
switch middleSegments.last {
183212
case .stringSegment(let lastMiddleSegment):
184-
if lastMiddleSegment.content.tokenText.hasSuffix("\n") {
213+
if let newlineSuffix = lastMiddleSegment.content.tokenText.newlineSuffix {
185214
// The newline at the end of the last line in the string literal is not part of the represented string.
186215
// Mark it as trivia.
187-
var content = lastMiddleSegment.content.reclassifyAsTrailingTrivia([.newlines(1)], arena: self.arena)
216+
var content = lastMiddleSegment.content.reclassifyAsTrailingTrivia([newlineSuffix], arena: self.arena)
188217
var unexpectedBeforeContent: RawTokenSyntax?
189218
if content.tokenText.hasSuffix("\\") {
190219
// The newline on the last line must not be escaped
@@ -213,7 +242,11 @@ extension Parser {
213242
case .expressionSegment:
214243
closeDelimiterOnNewLine = false
215244
case nil:
216-
closeDelimiterOnNewLine = firstSegment?.content.tokenText.hasSuffix("\n") ?? false
245+
if let firstSegment = firstSegment {
246+
closeDelimiterOnNewLine = firstSegment.content.tokenText.newlineSuffix != nil
247+
} else {
248+
closeDelimiterOnNewLine = false
249+
}
217250
}
218251

219252
if !closeDelimiterOnNewLine {
@@ -256,8 +289,8 @@ extension Parser {
256289
// iterating over is on a new line.
257290
var isSegmentOnNewLine: Bool
258291

259-
if firstSegment?.content.tokenText == "\n" {
260-
openQuote = openQuote.extendingTrailingTrivia(by: [.newlines(1)], arena: self.arena)
292+
if let newlineTrivia = firstSegment?.content.tokenText.triviaPieceIfNewline {
293+
openQuote = openQuote.extendingTrailingTrivia(by: [newlineTrivia], arena: self.arena)
261294
isSegmentOnNewLine = true
262295
} else {
263296
if let firstSegment = firstSegment {
@@ -307,15 +340,25 @@ extension Parser {
307340
}
308341
}
309342

310-
isSegmentOnNewLine = segment.content.tokenText.hasSuffix("\n")
343+
isSegmentOnNewLine = segment.content.tokenText.newlineSuffix != nil
311344

312345
// If the segment has a `\` in front of its trailing newline, that newline
313346
// is not part of the reprsented string and should be trivia.
314347

315-
if segment.content.tokenText.hasSuffix("\\\n") {
348+
let backslashNewlineSuffix: [RawTriviaPiece]?
349+
if segment.content.tokenText.hasSuffix("\\\r\n") {
350+
backslashNewlineSuffix = [.backslashs(1), .carriageReturnLineFeeds(1)]
351+
} else if segment.content.tokenText.hasSuffix("\\\n") {
352+
backslashNewlineSuffix = [.backslashs(1), .newlines(1)]
353+
} else if segment.content.tokenText.hasSuffix("\\\r") {
354+
backslashNewlineSuffix = [.backslashs(1), .carriageReturns(1)]
355+
} else {
356+
backslashNewlineSuffix = nil
357+
}
358+
if let backslashNewlineSuffix = backslashNewlineSuffix {
316359
segment = RawStringSegmentSyntax(
317360
segment.unexpectedBeforeContent,
318-
content: segment.content.reclassifyAsTrailingTrivia([.backslashs(1), .newlines(1)], arena: self.arena),
361+
content: segment.content.reclassifyAsTrailingTrivia(backslashNewlineSuffix, arena: self.arena),
319362
segment.unexpectedAfterContent,
320363
arena: self.arena
321364
)

Tests/SwiftParserTest/Assertions.swift

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -440,16 +440,32 @@ func AssertDiagnostic<T: SyntaxProtocol>(
440440
}
441441
}
442442

443+
public struct AssertParseOptions: OptionSet {
444+
public var rawValue: UInt8
445+
446+
public init(rawValue: UInt8) {
447+
self.rawValue = rawValue
448+
}
449+
450+
/// Trivia mismatches in the substructure should cause failures
451+
public static let substructureCheckTrivia = AssertParseOptions(rawValue: 1 << 0)
452+
453+
/// Replace all `\r` and `\r\n` in the fixed source by `\n`.
454+
/// Useful to match source code that contains other line endings to expected
455+
/// fixedfixed source that has `\n` line endings.
456+
public static let normalizeNewlinesInFixedSource = AssertParseOptions(rawValue: 1 << 1)
457+
}
458+
443459
/// Same as `AssertParse` overload with a `(String) -> S` `parse`,
444460
/// parsing the resulting `String` as a `SourceFileSyntax`.
445461
func AssertParse(
446462
_ markedSource: String,
447463
substructure expectedSubstructure: Syntax? = nil,
448464
substructureAfterMarker: String = "START",
449-
substructureCheckTrivia: Bool = false,
450465
diagnostics expectedDiagnostics: [DiagnosticSpec] = [],
451466
applyFixIts: [String]? = nil,
452467
fixedSource expectedFixedSource: String? = nil,
468+
options: AssertParseOptions = [],
453469
file: StaticString = #file,
454470
line: UInt = #line
455471
) {
@@ -458,10 +474,10 @@ func AssertParse(
458474
{ SourceFileSyntax.parse(from: &$0) },
459475
substructure: expectedSubstructure,
460476
substructureAfterMarker: substructureAfterMarker,
461-
substructureCheckTrivia: substructureCheckTrivia,
462477
diagnostics: expectedDiagnostics,
463478
applyFixIts: applyFixIts,
464479
fixedSource: expectedFixedSource,
480+
options: options,
465481
file: file,
466482
line: line
467483
)
@@ -475,10 +491,10 @@ func AssertParse<S: SyntaxProtocol>(
475491
_ parse: (inout Parser) -> S,
476492
substructure expectedSubstructure: Syntax? = nil,
477493
substructureAfterMarker: String = "START",
478-
substructureCheckTrivia: Bool = false,
479494
diagnostics expectedDiagnostics: [DiagnosticSpec] = [],
480495
applyFixIts: [String]? = nil,
481496
fixedSource expectedFixedSource: String? = nil,
497+
options: AssertParseOptions = [],
482498
file: StaticString = #file,
483499
line: UInt = #line
484500
) {
@@ -490,10 +506,10 @@ func AssertParse<S: SyntaxProtocol>(
490506
},
491507
substructure: expectedSubstructure,
492508
substructureAfterMarker: substructureAfterMarker,
493-
substructureCheckTrivia: substructureCheckTrivia,
494509
diagnostics: expectedDiagnostics,
495510
applyFixIts: applyFixIts,
496511
fixedSource: expectedFixedSource,
512+
options: options,
497513
file: file,
498514
line: line
499515
)
@@ -522,10 +538,10 @@ func AssertParse<S: SyntaxProtocol>(
522538
_ parse: (String) -> S,
523539
substructure expectedSubstructure: Syntax? = nil,
524540
substructureAfterMarker: String = "START",
525-
substructureCheckTrivia: Bool = false,
526541
diagnostics expectedDiagnostics: [DiagnosticSpec] = [],
527542
applyFixIts: [String]? = nil,
528543
fixedSource expectedFixedSource: String? = nil,
544+
options: AssertParseOptions = [],
529545
file: StaticString = #file,
530546
line: UInt = #line
531547
) {
@@ -553,7 +569,7 @@ func AssertParse<S: SyntaxProtocol>(
553569
if let expectedSubstructure = expectedSubstructure {
554570
let subtreeMatcher = SubtreeMatcher(Syntax(tree), markers: markerLocations)
555571
do {
556-
try subtreeMatcher.assertSameStructure(afterMarker: substructureAfterMarker, Syntax(expectedSubstructure), includeTrivia: substructureCheckTrivia, file: file, line: line)
572+
try subtreeMatcher.assertSameStructure(afterMarker: substructureAfterMarker, Syntax(expectedSubstructure), includeTrivia: options.contains(.substructureCheckTrivia), file: file, line: line)
557573
} catch {
558574
XCTFail("Matching for a subtree failed with error: \(error)", file: file, line: line)
559575
}
@@ -579,8 +595,15 @@ func AssertParse<S: SyntaxProtocol>(
579595
// Applying Fix-Its
580596
if let expectedFixedSource = expectedFixedSource {
581597
let fixedTree = FixItApplier.applyFixes(in: diags, withMessages: applyFixIts, to: tree)
598+
var fixedTreeDescription = fixedTree.description
599+
if options.contains(.normalizeNewlinesInFixedSource) {
600+
fixedTreeDescription =
601+
fixedTreeDescription
602+
.replacingOccurrences(of: "\r\n", with: "\n")
603+
.replacingOccurrences(of: "\r", with: "\n")
604+
}
582605
AssertStringsEqualWithDiff(
583-
fixedTree.description.trimmingTrailingWhitespace(),
606+
fixedTreeDescription.trimmingTrailingWhitespace(),
584607
expectedFixedSource.trimmingTrailingWhitespace(),
585608
file: file,
586609
line: line

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ final class ExpressionTests: XCTestCase {
924924
closeQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2))
925925
)
926926
),
927-
substructureCheckTrivia: true
927+
options: [.substructureCheckTrivia]
928928
)
929929

930930
AssertParse(
@@ -944,7 +944,7 @@ final class ExpressionTests: XCTestCase {
944944
closeQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2))
945945
)
946946
),
947-
substructureCheckTrivia: true
947+
options: [.substructureCheckTrivia]
948948
)
949949

950950
AssertParse(
@@ -969,10 +969,10 @@ final class ExpressionTests: XCTestCase {
969969
closeQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2))
970970
)
971971
),
972-
substructureCheckTrivia: true,
973972
diagnostics: [
974973
DiagnosticSpec(message: "escaped newline at the last line of a multi-line string literal is not allowed")
975-
]
974+
],
975+
options: [.substructureCheckTrivia]
976976
)
977977
}
978978
}

Tests/SwiftParserTest/translated/HashbangLibraryTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ final class HashbangLibraryTests: XCTestCase {
5252
eofToken: .eof()
5353
)
5454
),
55-
substructureCheckTrivia: true
55+
options: [.substructureCheckTrivia]
5656
)
5757
}
5858
}

0 commit comments

Comments
 (0)