Skip to content

Commit b7d7cd4

Browse files
authored
Merge pull request #1278 from ahoppen/ahoppen/single-quote-literals
Diagnose singe-line strings
2 parents 00c6476 + 4918b4b commit b7d7cd4

File tree

5 files changed

+74
-6
lines changed

5 files changed

+74
-6
lines changed

Sources/SwiftParser/StringLiterals.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ extension Parser {
5151
let openDelimiter = self.consume(if: .rawStringDelimiter)
5252

5353
/// Parse open quote.
54-
let (unexpectedBeforeOpenQuote, openQuote) = self.expectAny([.stringQuote, .multilineStringQuote, .singleQuote], default: .stringQuote)
54+
var (unexpectedBeforeOpenQuote, openQuote) = self.expectAny([.stringQuote, .multilineStringQuote], default: .stringQuote)
55+
var openQuoteKind: RawTokenKind = openQuote.tokenKind
56+
if openQuote.isMissing, let singleQuote = self.consume(if: .singleQuote) {
57+
unexpectedBeforeOpenQuote = RawUnexpectedNodesSyntax(combining: unexpectedBeforeOpenQuote, singleQuote, arena: self.arena)
58+
openQuoteKind = .singleQuote
59+
}
5560

5661
/// Parse segments.
5762
var segments: [RawStringLiteralSegmentsSyntax.Element] = []
@@ -68,7 +73,7 @@ extension Parser {
6873
// This allows us to skip over extraneous identifiers etc. in an unterminated string interpolation.
6974
var unexpectedBeforeRightParen: [RawTokenSyntax] = []
7075
var unexpectedProgress = LoopProgressCondition()
71-
while !self.at(any: [.rightParen, .stringSegment, .backslash, openQuote.tokenKind, .eof]) && unexpectedProgress.evaluate(self.currentToken) {
76+
while !self.at(any: [.rightParen, .stringSegment, .backslash, openQuoteKind, .eof]) && unexpectedProgress.evaluate(self.currentToken) {
7277
unexpectedBeforeRightParen.append(self.consumeAnyToken())
7378
}
7479
let rightParen = self.expectWithoutRecovery(.rightParen)
@@ -106,7 +111,15 @@ extension Parser {
106111
}
107112

108113
/// Parse close quote.
109-
let (unexpectedBeforeCloseQuote, closeQuote) = self.expect(openQuote.tokenKind)
114+
let unexpectedBeforeCloseQuote: RawUnexpectedNodesSyntax?
115+
let closeQuote: RawTokenSyntax
116+
if openQuoteKind == .singleQuote {
117+
let (unexpectedBeforeSingleQuote, singleQuote) = self.expect(.singleQuote)
118+
unexpectedBeforeCloseQuote = RawUnexpectedNodesSyntax(combining: unexpectedBeforeSingleQuote, singleQuote, arena: self.arena)
119+
closeQuote = missingToken(.stringQuote)
120+
} else {
121+
(unexpectedBeforeCloseQuote, closeQuote) = self.expect(openQuote.tokenKind)
122+
}
110123

111124
let (unexpectedBeforeCloseDelimiter, closeDelimiter) = self.parseStringDelimiter(openDelimiter: openDelimiter)
112125

Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ extension FixIt.Changes {
6060

6161
/// If `transferTrivia` is `true`, the leading and trailing trivia of the
6262
/// removed node will be transferred to the trailing trivia of the previous token.
63-
static func makeMissing<SyntaxType: SyntaxProtocol>(_ node: SyntaxType, transferTrivia: Bool = true) -> Self {
63+
static func makeMissing<SyntaxType: SyntaxProtocol>(_ node: SyntaxType?, transferTrivia: Bool = true) -> Self {
64+
guard let node = node else {
65+
return FixIt.Changes(changes: [])
66+
}
6467
var changes = [FixIt.Change.replace(oldNode: Syntax(node), newNode: MissingMaker().visit(Syntax(node)))]
6568
if transferTrivia {
6669
changes += FixIt.Changes.transferTriviaAtSides(from: [node]).changes

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,37 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
817817
return .visitChildren
818818
}
819819

820+
public override func visit(_ node: StringLiteralExprSyntax) -> SyntaxVisitorContinueKind {
821+
if shouldSkip(node) {
822+
return .skipChildren
823+
}
824+
825+
if let singleQuote = node.unexpectedBetweenOpenDelimiterAndOpenQuote?.onlyToken(where: { $0.tokenKind == .singleQuote }) {
826+
let fixIt = FixIt(
827+
message: ReplaceTokensFixIt(replaceTokens: [singleQuote], replacement: node.openQuote),
828+
changes: [
829+
.makeMissing(singleQuote, transferTrivia: false),
830+
.makePresent(node.openQuote, leadingTrivia: singleQuote.leadingTrivia ?? []),
831+
.makeMissing(node.unexpectedBetweenSegmentsAndCloseQuote, transferTrivia: false),
832+
.makePresent(node.closeQuote, trailingTrivia: node.unexpectedBetweenSegmentsAndCloseQuote?.trailingTrivia ?? []),
833+
]
834+
)
835+
addDiagnostic(
836+
singleQuote,
837+
.singleQuoteStringLiteral,
838+
fixIts: [fixIt],
839+
handledNodes: [
840+
node.unexpectedBetweenOpenDelimiterAndOpenQuote?.id,
841+
node.openQuote.id,
842+
node.unexpectedBetweenSegmentsAndCloseQuote?.id,
843+
node.closeQuote.id,
844+
].compactMap { $0 }
845+
)
846+
}
847+
848+
return .visitChildren
849+
}
850+
820851
public override func visit(_ node: SwitchCaseSyntax) -> SyntaxVisitorContinueKind {
821852
if shouldSkip(node) {
822853
return .skipChildren

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ extension DiagnosticMessage where Self == StaticParserError {
158158
public static var operatorShouldBeDeclaredWithoutBody: Self {
159159
.init("operator should not be declared with body")
160160
}
161+
public static var singleQuoteStringLiteral: Self {
162+
.init(#"Single-quoted string literal found, use '"'"#)
163+
}
161164
public static var standaloneSemicolonStatement: Self {
162165
.init("standalone ';' statements are not allowed")
163166
}

Tests/SwiftParserTest/ExpressionTests.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,8 +632,26 @@ final class ExpressionTests: XCTestCase {
632632
func testSingleQuoteStringLiteral() {
633633
AssertParse(
634634
#"""
635-
'red'
636-
"""#
635+
1️⃣'red'
636+
"""#,
637+
diagnostics: [
638+
DiagnosticSpec(message: #"Single-quoted string literal found, use '"'"#, fixIts: [#"replace ''' by '"'"#])
639+
],
640+
fixedSource: """
641+
"red"
642+
"""
643+
)
644+
645+
AssertParse(
646+
#"""
647+
1️⃣' red ' + 1
648+
"""#,
649+
diagnostics: [
650+
DiagnosticSpec(message: #"Single-quoted string literal found, use '"'"#, fixIts: [#"replace ''' by '"'"#])
651+
],
652+
fixedSource: """
653+
" red " + 1
654+
"""
637655
)
638656
}
639657

0 commit comments

Comments
 (0)