Skip to content

Commit 9d79cdc

Browse files
committed
Diagnose if the last newline of a multi-line string literal is escaped
1 parent f148d7b commit 9d79cdc

File tree

9 files changed

+100
-35
lines changed

9 files changed

+100
-35
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
@@ -101,7 +101,8 @@ extension FixIt.Changes {
101101
!firstToken.tokenKind.isPunctuation,
102102
!previousToken.tokenKind.isPunctuation,
103103
firstToken.leadingTrivia.isEmpty,
104-
(previousToken.presence == .missing ? BasicFormat().visit(previousToken).trailingTrivia : previousToken.trailingTrivia).isEmpty
104+
(previousToken.presence == .missing ? BasicFormat().visit(previousToken).trailingTrivia : previousToken.trailingTrivia).isEmpty,
105+
leadingTrivia == nil
105106
{
106107
/// If neither this nor the previous token are punctionation make sure they
107108
/// are separated by a space.

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,26 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
816816
for (diagnostic, handledNodes) in MultiLineStringLiteralIndentatinDiagnosticsGenerator.diagnose(node) {
817817
addDiagnostic(diagnostic, handledNodes: handledNodes)
818818
}
819+
if case .stringSegment(let segment) = node.segments.last {
820+
if let invalidContent = segment.unexpectedBeforeContent?.onlyToken(where: { $0.text.hasSuffix("\\") }) {
821+
let leadingTrivia = segment.content.leadingTrivia
822+
let trailingTrivia = segment.content.trailingTrivia
823+
let fixIt = FixIt(
824+
message: .removeBackslash,
825+
changes: [
826+
.makePresent(segment.content, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia),
827+
.makeMissing(invalidContent, transferTrivia: false),
828+
]
829+
)
830+
addDiagnostic(
831+
invalidContent,
832+
position: invalidContent.endPositionBeforeTrailingTrivia.advanced(by: -1),
833+
.escapedNewlineAtLatlineOfMultiLineStringLiteralNotAllowed,
834+
fixIts: [fixIt],
835+
handledNodes: [segment.id]
836+
)
837+
}
838+
}
819839
return .visitChildren
820840
}
821841

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
}
@@ -456,6 +459,9 @@ extension FixItMessage where Self == StaticParserFixIt {
456459
public static var joinIdentifiersWithCamelCase: Self {
457460
.init("join the identifiers together with camel-case")
458461
}
462+
public static var removeBackslash: Self {
463+
.init("remove '\'")
464+
}
459465
public static var removeExtraneousDelimiters: Self {
460466
.init("remove extraneous delimiters")
461467
}

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
@@ -819,7 +819,7 @@ extension RawSyntax: CustomDebugStringConvertible {
819819
case .parsedToken(let dat):
820820
target.write(".parsedToken(")
821821
target.write(String(describing: dat.tokenKind))
822-
target.write(" wholeText=\(dat.tokenText.debugDescription)")
822+
target.write(" wholeText=\(dat.wholeText.debugDescription)")
823823
target.write(" textRange=\(dat.textRange.description)")
824824
case .materializedToken(let dat):
825825
target.write(".materializedToken(")

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ final class ExpressionTests: XCTestCase {
575575
"""1️⃣"""
576576
"""##,
577577
diagnostics: [
578-
DiagnosticSpec(message: "multi-line string literal content must begin on a new line")
578+
DiagnosticSpec(message: "multi-line string literal closing delimiter must begin on a new line")
579579
],
580580
fixedSource: ##"""
581581
"""
@@ -957,8 +957,8 @@ final class ExpressionTests: XCTestCase {
957957
StringLiteralExprSyntax(
958958
openQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2), trailingTrivia: .newline),
959959
segments: StringLiteralSegmentsSyntax([
960-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 1\n"))),
961-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 2"), trailingTrivia: .newline)),
960+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 1\n", leadingTrivia: .spaces(2)))),
961+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 2", leadingTrivia: .spaces(2), trailingTrivia: .newline))),
962962
]),
963963
closeQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2))
964964
)
@@ -977,8 +977,8 @@ final class ExpressionTests: XCTestCase {
977977
StringLiteralExprSyntax(
978978
openQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2), trailingTrivia: .newline),
979979
segments: StringLiteralSegmentsSyntax([
980-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 1 "), trailingTrivia: [.unexpectedText("\\"), .newlines(1)])),
981-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 2"), trailingTrivia: .newline)),
980+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 1 ", leadingTrivia: .spaces(2), trailingTrivia: [.unexpectedText("\\"), .newlines(1)]))),
981+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 2", leadingTrivia: .spaces(2), trailingTrivia: .newline))),
982982
]),
983983
closeQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2))
984984
)
@@ -990,20 +990,28 @@ final class ExpressionTests: XCTestCase {
990990
#"""
991991
"""
992992
line 1
993-
line 2 \
993+
line 2 1️⃣\
994994
"""
995995
"""#,
996996
substructure: Syntax(
997997
StringLiteralExprSyntax(
998998
openQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2), trailingTrivia: .newline),
999999
segments: StringLiteralSegmentsSyntax([
1000-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 1\n"))),
1001-
.stringSegment(StringSegmentSyntax(leadingTrivia: .spaces(2), content: .stringSegment("line 2 \\"), trailingTrivia: .newline)),
1000+
.stringSegment(StringSegmentSyntax(content: .stringSegment("line 1\n", leadingTrivia: .spaces(2)))),
1001+
.stringSegment(
1002+
StringSegmentSyntax(
1003+
UnexpectedNodesSyntax([Syntax(TokenSyntax.stringSegment(" line 2 \\", trailingTrivia: .newline))]),
1004+
content: .stringSegment("line 2 ", leadingTrivia: .spaces(2), trailingTrivia: .newline, presence: .missing)
1005+
)
1006+
),
10021007
]),
10031008
closeQuote: .multilineStringQuoteToken(leadingTrivia: .spaces(2))
10041009
)
10051010
),
1006-
substructureCheckTrivia: true
1011+
substructureCheckTrivia: true,
1012+
diagnostics: [
1013+
DiagnosticSpec(message: "escaped newline at the last line of a multi-line string literal is not allowed")
1014+
]
10071015
)
10081016
}
10091017
}

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)