Skip to content

Commit f718ad7

Browse files
committed
Diagnose if the last newline of a multi-line string literal is escaped
1 parent 651cff4 commit f718ad7

File tree

9 files changed

+99
-34
lines changed

9 files changed

+99
-34
lines changed

Sources/SwiftParser/StringLiterals.swift

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ fileprivate class StringLiteralExpressionIndentationChecker {
8686
}
8787
}
8888

89-
9089
extension Parser {
9190
/// Consumes a raw string delimiter that has the same number of `#` as `openDelimiter`.
9291
private mutating func parseStringDelimiter(openDelimiter: RawTokenSyntax?) -> (unexpectedBeforeCheckedDelimiter: RawUnexpectedNodesSyntax?, checkedDelimiter: RawTokenSyntax?) {
@@ -185,9 +184,25 @@ extension Parser {
185184
if lastMiddleSegment.content.tokenText.hasSuffix("\n") {
186185
// The newline at the end of the last line in the string literal is not part of the represented string.
187186
// Mark it as trivia.
187+
var content = lastMiddleSegment.content.reclassifyAsTrailingTrivia([.newlines(1)], arena: self.arena)
188+
var unexpectedBeforeContent: RawTokenSyntax?
189+
if content.tokenText.hasSuffix("\\") {
190+
// The newline on the last line must not be escaped
191+
unexpectedBeforeContent = content
192+
content = RawTokenSyntax(
193+
missing: .stringSegment,
194+
text: SyntaxText(rebasing: content.tokenText[0..<content.tokenText.count - 1]),
195+
leadingTriviaPieces: content.leadingTriviaPieces,
196+
trailingTriviaPieces: content.trailingTriviaPieces,
197+
arena: self.arena
198+
)
199+
}
200+
188201
middleSegments[middleSegments.count - 1] = .stringSegment(
189202
RawStringSegmentSyntax(
190-
content: lastMiddleSegment.content.reclassifyAsTrailingTrivia([.newlines(1)], arena: self.arena),
203+
RawUnexpectedNodesSyntax(combining: lastMiddleSegment.unexpectedBeforeContent, unexpectedBeforeContent, arena: self.arena),
204+
content: content,
205+
lastMiddleSegment.unexpectedAfterContent,
191206
arena: self.arena
192207
)
193208
)
@@ -261,14 +276,10 @@ extension Parser {
261276
for (index, segment) in middleSegments.enumerated() {
262277
switch segment {
263278
case .stringSegment(var segment):
264-
if segment.content.isMissing {
265-
// Don't diagnose incorrect indentation for segments that we synthesized
266-
break
267-
}
268-
// We are not considering unexpected and leading trivia for indentation
269-
// computation. If these assertions are violated, we can probably lift
270-
// them but we would need to check the produce the expected results.
271-
assert(segment.unexpectedBeforeContent == nil && segment.content.leadingTriviaByteLength == 0)
279+
// We are not considering leading trivia for indentation computation.
280+
// If these assertions are violated, we can probably lift them but we
281+
// would need to check the produce the expected results.
282+
assert(segment.content.leadingTriviaByteLength == 0)
272283

273284
// Re-classify indentation as leading trivia
274285
if isSegmentOnNewLine {

Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ extension FixIt.Changes {
9898
!firstToken.tokenKind.isPunctuation,
9999
!previousToken.tokenKind.isPunctuation,
100100
firstToken.leadingTrivia.isEmpty,
101-
(previousToken.presence == .missing ? BasicFormat().visit(previousToken).trailingTrivia : previousToken.trailingTrivia).isEmpty
101+
(previousToken.presence == .missing ? BasicFormat().visit(previousToken).trailingTrivia : previousToken.trailingTrivia).isEmpty,
102+
leadingTrivia == nil
102103
{
103104
/// If neither this nor the previous token are punctionation make sure they
104105
/// are separated by a space.

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,26 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
712712
for (diagnostic, handledNodes) in MultiLineStringLiteralIndentatinDiagnosticsGenerator.diagnose(node) {
713713
addDiagnostic(diagnostic, handledNodes: handledNodes)
714714
}
715+
if case .stringSegment(let segment) = node.segments.last {
716+
if let invalidContent = segment.unexpectedBeforeContent?.onlyToken(where: { $0.text.hasSuffix("\\") }) {
717+
let leadingTrivia = segment.content.leadingTrivia
718+
let trailingTrivia = segment.content.trailingTrivia
719+
let fixIt = FixIt(
720+
message: .removeBackslash,
721+
changes: [
722+
.makePresent(segment.content, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia),
723+
.makeMissing(invalidContent, transferTrivia: false),
724+
]
725+
)
726+
addDiagnostic(
727+
invalidContent,
728+
position: invalidContent.endPositionBeforeTrailingTrivia.advanced(by: -1),
729+
.escapedNewlineAtLatlineOfMultiLineStringLiteralNotAllowed,
730+
fixIts: [fixIt],
731+
handledNodes: [segment.id]
732+
)
733+
}
734+
}
715735
return .visitChildren
716736
}
717737

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ extension DiagnosticMessage where Self == StaticParserError {
122122
public static var editorPlaceholderInSourceFile: Self {
123123
.init("editor placeholder in source file")
124124
}
125+
public static var escapedNewlineAtLatlineOfMultiLineStringLiteralNotAllowed: Self {
126+
.init("escaped newline at the last line of a multi-line string literal is not allowed")
127+
}
125128
public static var expectedExpressionAfterTry: Self {
126129
.init("expected expression after 'try'")
127130
}
@@ -415,6 +418,9 @@ extension FixItMessage where Self == StaticParserFixIt {
415418
public static var joinIdentifiersWithCamelCase: Self {
416419
.init("join the identifiers together with camel-case")
417420
}
421+
public static var removeBackslash: Self {
422+
.init("remove '\'")
423+
}
418424
public static var removeExtraneousDelimiters: Self {
419425
.init("remove extraneous delimiters")
420426
}

Sources/SwiftParserDiagnostics/PresenceUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class PresentMaker: SyntaxRewriter {
4949
if token.presence == .missing {
5050
let presentToken: TokenSyntax
5151
let (rawKind, text) = token.tokenKind.decomposeToRaw()
52-
if let text = text, !text.isEmpty {
52+
if let text = text, (!text.isEmpty || rawKind == .stringSegment) { // string segments can have empty text
5353
presentToken = TokenSyntax(token.tokenKind, presence: .present)
5454
} else {
5555
let newKind = TokenKind.fromRaw(kind: rawKind, text: rawKind.defaultText.map(String.init) ?? "<#\(rawKind.nameForDiagnostics)#>")

Sources/SwiftSyntax/Raw/RawSyntax.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ extension RawSyntax: CustomDebugStringConvertible {
785785
case .parsedToken(let dat):
786786
target.write(".parsedToken(")
787787
target.write(String(describing: dat.tokenKind))
788-
target.write(" wholeText=\(dat.tokenText.debugDescription)")
788+
target.write(" wholeText=\(dat.wholeText.debugDescription)")
789789
target.write(" textRange=\(dat.textRange.description)")
790790
case .materializedToken(let dat):
791791
target.write(".materializedToken(")

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -918,8 +918,8 @@ final class ExpressionTests: XCTestCase {
918918
StringLiteralExprSyntax(
919919
openQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2), trailingTrivia: .newline),
920920
segments: StringLiteralSegmentsSyntax([
921-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 1\n"))),
922-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 2"), trailingTrivia: .newline)),
921+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 1\n", leadingTrivia: .spaces(2)))),
922+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 2", leadingTrivia: .spaces(2), trailingTrivia: .newline))),
923923
]),
924924
closeQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2))
925925
)
@@ -938,8 +938,8 @@ final class ExpressionTests: XCTestCase {
938938
StringLiteralExprSyntax(
939939
openQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2), trailingTrivia: .newline),
940940
segments: StringLiteralSegmentsSyntax([
941-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 1 "), trailingTrivia: [.unexpectedText("\\"), .newlines(1)])),
942-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 2"), trailingTrivia: .newline)),
941+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 1 ", leadingTrivia: .spaces(2), trailingTrivia: [.unexpectedText("\\"), .newlines(1)]))),
942+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 2", leadingTrivia: .spaces(2), trailingTrivia: .newline))),
943943
]),
944944
closeQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2))
945945
)
@@ -951,20 +951,28 @@ final class ExpressionTests: XCTestCase {
951951
#"""
952952
"""
953953
line 1
954-
line 2 \
954+
line 2 1️⃣\
955955
"""
956956
"""#,
957957
substructure: Syntax(
958958
StringLiteralExprSyntax(
959959
openQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2), trailingTrivia: .newline),
960960
segments: StringLiteralSegmentsSyntax([
961-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 1\n"))),
962-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 2 \\"), trailingTrivia: .newline)),
961+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 1\n", leadingTrivia: .spaces(2)))),
962+
.stringSegment(
963+
StringSegmentSyntax(
964+
UnexpectedNodesSyntax([Syntax(TokenSyntax.stringSegment(" line 2 \\", trailingTrivia: .newline))]),
965+
content: .stringSegment("line 2 ", leadingTrivia: .spaces(2), trailingTrivia: .newline, presence: .missing)
966+
)
967+
),
963968
]),
964969
closeQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2))
965970
)
966971
),
967-
substructureCheckTrivia: true
972+
substructureCheckTrivia: true,
973+
diagnostics: [
974+
DiagnosticSpec(message: "escaped newline at the last line of a multi-line string literal is not allowed")
975+
]
968976
)
969977
}
970978
}

Tests/SwiftParserTest/translated/MultilineErrorsTests.swift

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -468,12 +468,18 @@ final class MultilineErrorsTests: XCTestCase {
468468
#"""
469469
_ = """
470470
line one
471-
line two\
471+
line two1️⃣\
472472
"""
473473
"""#,
474474
diagnostics: [
475-
// TODO: Old parser expected error on line 3: escaped newline at the last line is not allowed, Fix-It replacements: 11 - 12 = ''
476-
]
475+
DiagnosticSpec(message: "escaped newline at the last line of a multi-line string literal is not allowed", fixIts: ["remove '\'"])
476+
],
477+
fixedSource: #"""
478+
_ = """
479+
line one
480+
line two
481+
"""
482+
"""#
477483
)
478484
}
479485

@@ -507,25 +513,35 @@ final class MultilineErrorsTests: XCTestCase {
507513
AssertParse(
508514
#"""
509515
_ = """
510-
foo\
516+
foo1️⃣\
511517
"""
512518
"""#,
513519
diagnostics: [
514-
// TODO: Old parser expected error on line 2: escaped newline at the last line is not allowed, Fix-It replacements: 6 - 7 = ''
515-
]
520+
DiagnosticSpec(message: "escaped newline at the last line of a multi-line string literal is not allowed")
521+
],
522+
fixedSource: #"""
523+
_ = """
524+
foo
525+
"""
526+
"""#
516527
)
517528
}
518529

519530
func testMultilineErrors25() {
520531
AssertParse(
521532
#"""
522533
_ = """
523-
foo\
534+
foo1️⃣\
524535
"""
525536
"""#,
526537
diagnostics: [
527-
// TODO: Old parser expected error on line 3: escaped newline at the last line is not allowed, Fix-It replacements: 6 - 7 = ''
528-
]
538+
DiagnosticSpec(message: "escaped newline at the last line of a multi-line string literal is not allowed")
539+
],
540+
fixedSource: #"""
541+
_ = """
542+
foo
543+
"""
544+
"""#
529545
)
530546
}
531547

@@ -549,12 +565,12 @@ final class MultilineErrorsTests: XCTestCase {
549565
"""
550566
"""#,
551567
diagnostics: [
552-
DiagnosticSpec(message: "insufficient indentation of line in multi-line string literal")
568+
DiagnosticSpec(message: "escaped newline at the last line of a multi-line string literal is not allowed")
553569
// TODO: Old parser expected error on line 2: escaped newline at the last line is not allowed, Fix-It replacements: 1 - 2 = ''
554570
],
555571
fixedSource: #"""
556572
_ = """
557-
\
573+
558574
"""
559575
"""#
560576
)

Tests/SwiftParserTest/translated/RawStringTests.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,12 @@ final class RawStringTests: XCTestCase {
7474
##"""
7575
_ = #"""
7676
Three\r
77-
Gamma\
77+
Gamma1️⃣\
7878
"""#
79-
"""##
79+
"""##,
80+
diagnostics: [
81+
DiagnosticSpec(message: "escaped newline at the last line of a multi-line string literal is not allowed")
82+
]
8083
)
8184
}
8285

0 commit comments

Comments
 (0)