Skip to content

Commit 3b727c6

Browse files
authored
Merge pull request #1864 from kimdv/kimdv/add-diagnostics-for-string-interpolates-message-label
Add diagnostic for label with string segment
2 parents 69066f5 + 4226e7a commit 3b727c6

34 files changed

+1001
-61
lines changed

CodeGeneration/Sources/SyntaxSupport/AvailabilityNodes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public let AVAILABILITY_NODES: [Node] = [
8585
kind: .nodeChoices(choices: [
8686
Child(
8787
name: "String",
88-
kind: .node(kind: .stringLiteralExpr)
88+
kind: .node(kind: .simpleStringLiteralExpr)
8989
),
9090
Child(
9191
name: "Version",

CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1710,7 +1710,7 @@ public let DECL_NODES: [Node] = [
17101710
),
17111711
Child(
17121712
name: "FileName",
1713-
kind: .node(kind: .stringLiteralExpr),
1713+
kind: .node(kind: .simpleStringLiteralExpr),
17141714
nameForDiagnostics: "file name"
17151715
),
17161716
Child(

CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,38 @@ public let EXPR_NODES: [Node] = [
15031503
elementChoices: [.stringSegment, .expressionSegment]
15041504
),
15051505

1506+
Node(
1507+
kind: .simpleStringLiteralExpr,
1508+
base: .expr,
1509+
nameForDiagnostics: "simple string literal",
1510+
documentation: "A simple string that can’t contain string interpolation and cannot have raw string delimiters.",
1511+
children: [
1512+
Child(
1513+
name: "OpeningQuote",
1514+
kind: .token(choices: [.token(.stringQuote), .token(.multilineStringQuote)]),
1515+
documentation: "Open quote for the string literal"
1516+
),
1517+
Child(
1518+
name: "Segments",
1519+
kind: .collection(kind: .simpleStringLiteralSegmentList, collectionElementName: "Segment"),
1520+
documentation: "String content"
1521+
),
1522+
Child(
1523+
name: "ClosingQuote",
1524+
kind: .token(choices: [.token(.stringQuote), .token(.multilineStringQuote)]),
1525+
documentation: "Close quote for the string literal"
1526+
),
1527+
]
1528+
),
1529+
1530+
Node(
1531+
kind: .simpleStringLiteralSegmentList,
1532+
base: .syntaxCollection,
1533+
nameForDiagnostics: nil,
1534+
documentation: "String literal segments that only can contain non string interpolated or extended escaped strings",
1535+
elementChoices: [.stringSegment]
1536+
),
1537+
15061538
// string literal segment in a string interpolation expression.
15071539
Node(
15081540
kind: .stringSegment,

CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ public enum SyntaxNodeKind: String, CaseIterable {
250250
case specializeAvailabilityArgument
251251
case specializeTargetFunctionArgument
252252
case stmt
253+
case simpleStringLiteralExpr
254+
case simpleStringLiteralSegmentList
253255
case stringLiteralExpr
254256
case stringLiteralSegmentList
255257
case stringSegment

Sources/SwiftBasicFormat/BasicFormat.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ open class BasicFormat: SyntaxRewriter {
347347
case \ExpressionSegmentSyntax.backslash,
348348
\ExpressionSegmentSyntax.rightParen,
349349
\DeclNameArgumentSyntax.colon,
350+
\SimpleStringLiteralExprSyntax.openingQuote,
350351
\StringLiteralExprSyntax.openingQuote,
351352
\RegexLiteralExprSyntax.openingSlash:
352353
return false

Sources/SwiftParser/Availability.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,7 @@ extension Parser {
102102
(.renamed, let handle)?:
103103
let argumentLabel = self.eat(handle)
104104
let (unexpectedBeforeColon, colon) = self.expect(.colon)
105-
// FIXME: Make sure this is a string literal with no interpolation.
106-
let stringValue = self.parseStringLiteral()
105+
let stringValue = self.parseSimpleString()
107106

108107
entry = .availabilityLabeledArgument(
109108
RawAvailabilityLabeledArgumentSyntax(

Sources/SwiftParser/Directives.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ extension Parser {
196196
if !self.at(.rightParen) {
197197
let (unexpectedBeforeFile, file) = self.expect(.keyword(.file))
198198
let (unexpectedBeforeFileColon, fileColon) = self.expect(.colon)
199-
let fileName = self.parseStringLiteral()
199+
let fileName = self.parseSimpleString()
200200
let (unexpectedBeforeComma, comma) = self.expect(.comma)
201201

202202
let (unexpectedBeforeLine, line) = self.expect(.keyword(.line))

Sources/SwiftParser/StringLiterals.swift

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ extension Parser {
147147
return RawTokenSyntax(
148148
kind: token.tokenKind,
149149
text: SyntaxText(rebasing: token.tokenText.dropFirst(reclassifyLeading.count).dropLast(reclassifyTrailing.count)),
150-
leadingTriviaPieces: token.leadingTriviaPieces + TriviaParser.parseTrivia(reclassifyLeading, position: .trailing),
150+
leadingTriviaPieces: token.leadingTriviaPieces + TriviaParser.parseTrivia(reclassifyLeading, position: .leading),
151151
trailingTriviaPieces: TriviaParser.parseTrivia(reclassifyTrailing, position: .trailing) + token.trailingTriviaPieces,
152152
presence: token.presence,
153153
tokenDiagnostic: token.tokenView.tokenDiagnostic ?? tokenDiagnostic,
@@ -595,10 +595,110 @@ extension Parser {
595595
)
596596
}
597597
}
598+
599+
mutating func parseSimpleString() -> RawSimpleStringLiteralExprSyntax {
600+
let openDelimiter = self.consume(if: .rawStringPoundDelimiter)
601+
let (unexpectedBeforeOpenQuote, openQuote) = self.expect(anyIn: SimpleStringLiteralExprSyntax.OpeningQuoteOptions.self, default: .stringQuote)
602+
603+
/// Parse segments.
604+
var segments: [RawStringSegmentSyntax] = []
605+
var loopProgress = LoopProgressCondition()
606+
while hasProgressed(&loopProgress) {
607+
// If we encounter a token with leading trivia, we're no longer in the
608+
// string literal.
609+
guard currentToken.leadingTriviaText.isEmpty else { break }
610+
611+
if let stringSegment = self.consume(if: .stringSegment, TokenSpec(.identifier, remapping: .stringSegment)) {
612+
var unexpectedAfterContent: RawUnexpectedNodesSyntax?
613+
614+
if let (backslash, leftParen) = self.consume(if: .backslash, followedBy: .leftParen) {
615+
var unexpectedTokens: [RawSyntax] = [RawSyntax(backslash), RawSyntax(leftParen)]
616+
617+
let (unexpectedBeforeRightParen, rightParen) = self.expect(TokenSpec(.rightParen, allowAtStartOfLine: false))
618+
unexpectedTokens += unexpectedBeforeRightParen?.elements ?? []
619+
unexpectedTokens.append(RawSyntax(rightParen))
620+
621+
unexpectedAfterContent = RawUnexpectedNodesSyntax(
622+
unexpectedTokens,
623+
arena: self.arena
624+
)
625+
}
626+
627+
segments.append(RawStringSegmentSyntax(content: stringSegment, unexpectedAfterContent, arena: self.arena))
628+
} else {
629+
break
630+
}
631+
}
632+
633+
let (unexpectedBetweenSegmentAndCloseQuote, closeQuote) = self.expect(
634+
anyIn: SimpleStringLiteralExprSyntax.ClosingQuoteOptions.self,
635+
default: openQuote.closeTokenKind
636+
)
637+
let closeDelimiter = self.consume(if: .rawStringPoundDelimiter)
638+
639+
if openQuote.tokenKind == .multilineStringQuote, !openQuote.isMissing, !closeQuote.isMissing {
640+
let postProcessed = postProcessMultilineStringLiteral(
641+
rawStringDelimitersToken: openDelimiter,
642+
openQuote: openQuote,
643+
segments: segments.compactMap { RawStringLiteralSegmentListSyntax.Element.stringSegment($0) },
644+
closeQuote: closeQuote
645+
)
646+
647+
return RawSimpleStringLiteralExprSyntax(
648+
RawUnexpectedNodesSyntax(
649+
combining: openDelimiter,
650+
unexpectedBeforeOpenQuote,
651+
postProcessed.unexpectedBeforeOpeningQuote,
652+
arena: self.arena
653+
),
654+
openingQuote: postProcessed.openingQuote,
655+
segments: RawSimpleStringLiteralSegmentListSyntax(
656+
// `RawSimpleStringLiteralSegmentListSyntax` only accepts `RawStringSegmentSyntax`.
657+
// So we can safely cast.
658+
elements: postProcessed.segments.map { $0.cast(RawStringSegmentSyntax.self) },
659+
arena: self.arena
660+
),
661+
RawUnexpectedNodesSyntax(
662+
combining: unexpectedBetweenSegmentAndCloseQuote,
663+
postProcessed.unexpectedBeforeClosingQuote,
664+
arena: self.arena
665+
),
666+
closingQuote: postProcessed.closingQuote,
667+
RawUnexpectedNodesSyntax(
668+
[closeDelimiter],
669+
arena: self.arena
670+
),
671+
arena: self.arena
672+
)
673+
} else {
674+
return RawSimpleStringLiteralExprSyntax(
675+
RawUnexpectedNodesSyntax(combining: unexpectedBeforeOpenQuote, openDelimiter, arena: self.arena),
676+
openingQuote: openQuote,
677+
segments: RawSimpleStringLiteralSegmentListSyntax(elements: segments, arena: self.arena),
678+
unexpectedBetweenSegmentAndCloseQuote,
679+
closingQuote: closeQuote,
680+
RawUnexpectedNodesSyntax([closeDelimiter], arena: self.arena),
681+
arena: self.arena
682+
)
683+
}
684+
}
598685
}
599686

600687
// MARK: - Utilities
601688

689+
fileprivate extension RawTokenSyntax {
690+
var closeTokenKind: SimpleStringLiteralExprSyntax.ClosingQuoteOptions {
691+
switch self {
692+
case .multilineStringQuote:
693+
return .multilineStringQuote
694+
case .stringQuote:
695+
return .stringQuote
696+
default:
697+
return .stringQuote
698+
}
699+
}
700+
}
701+
602702
fileprivate extension SyntaxText {
603703
private func hasSuffix(_ other: String) -> Bool {
604704
var other = other

Sources/SwiftParser/generated/Parser+TokenSpecSet.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2585,6 +2585,88 @@ extension SameTypeRequirementSyntax {
25852585
}
25862586
}
25872587

2588+
extension SimpleStringLiteralExprSyntax {
2589+
@_spi(Diagnostics)
2590+
public enum OpeningQuoteOptions: TokenSpecSet {
2591+
case stringQuote
2592+
case multilineStringQuote
2593+
2594+
init?(lexeme: Lexer.Lexeme) {
2595+
switch PrepareForKeywordMatch(lexeme) {
2596+
case TokenSpec(.stringQuote):
2597+
self = .stringQuote
2598+
case TokenSpec(.multilineStringQuote):
2599+
self = .multilineStringQuote
2600+
default:
2601+
return nil
2602+
}
2603+
}
2604+
2605+
var spec: TokenSpec {
2606+
switch self {
2607+
case .stringQuote:
2608+
return .stringQuote
2609+
case .multilineStringQuote:
2610+
return .multilineStringQuote
2611+
}
2612+
}
2613+
2614+
/// Returns a token that satisfies the `TokenSpec` of this case.
2615+
///
2616+
/// If the token kind of this spec has variable text, e.g. for an identifier, this returns a token with empty text.
2617+
@_spi(Diagnostics)
2618+
public var tokenSyntax: TokenSyntax {
2619+
switch self {
2620+
case .stringQuote:
2621+
return .stringQuoteToken()
2622+
case .multilineStringQuote:
2623+
return .multilineStringQuoteToken()
2624+
}
2625+
}
2626+
}
2627+
}
2628+
2629+
extension SimpleStringLiteralExprSyntax {
2630+
@_spi(Diagnostics)
2631+
public enum ClosingQuoteOptions: TokenSpecSet {
2632+
case stringQuote
2633+
case multilineStringQuote
2634+
2635+
init?(lexeme: Lexer.Lexeme) {
2636+
switch PrepareForKeywordMatch(lexeme) {
2637+
case TokenSpec(.stringQuote):
2638+
self = .stringQuote
2639+
case TokenSpec(.multilineStringQuote):
2640+
self = .multilineStringQuote
2641+
default:
2642+
return nil
2643+
}
2644+
}
2645+
2646+
var spec: TokenSpec {
2647+
switch self {
2648+
case .stringQuote:
2649+
return .stringQuote
2650+
case .multilineStringQuote:
2651+
return .multilineStringQuote
2652+
}
2653+
}
2654+
2655+
/// Returns a token that satisfies the `TokenSpec` of this case.
2656+
///
2657+
/// If the token kind of this spec has variable text, e.g. for an identifier, this returns a token with empty text.
2658+
@_spi(Diagnostics)
2659+
public var tokenSyntax: TokenSyntax {
2660+
switch self {
2661+
case .stringQuote:
2662+
return .stringQuoteToken()
2663+
case .multilineStringQuote:
2664+
return .multilineStringQuoteToken()
2665+
}
2666+
}
2667+
}
2668+
}
2669+
25882670
extension SomeOrAnyTypeSyntax {
25892671
@_spi(Diagnostics)
25902672
public enum SomeOrAnySpecifierOptions: TokenSpecSet {

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,6 +1506,56 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
15061506
return .visitChildren
15071507
}
15081508

1509+
public override func visit(_ node: SimpleStringLiteralExprSyntax) -> SyntaxVisitorContinueKind {
1510+
if shouldSkip(node) {
1511+
return .skipChildren
1512+
}
1513+
1514+
var rawDelimiters: [TokenSyntax] = []
1515+
1516+
if let unexpectedBeforeOpenQuote = node.unexpectedBeforeOpeningQuote?.onlyPresentToken(where: { $0.tokenKind.isRawStringDelimiter }) {
1517+
rawDelimiters += [unexpectedBeforeOpenQuote]
1518+
}
1519+
1520+
if let unexpectedAfterCloseQuote = node.unexpectedAfterClosingQuote?.onlyPresentToken(where: { $0.tokenKind.isRawStringDelimiter }) {
1521+
rawDelimiters += [unexpectedAfterCloseQuote]
1522+
}
1523+
1524+
if !rawDelimiters.isEmpty {
1525+
addDiagnostic(
1526+
node,
1527+
.forbiddenExtendedEscapingString,
1528+
fixIts: [
1529+
FixIt(
1530+
message: RemoveNodesFixIt(rawDelimiters),
1531+
changes: rawDelimiters.map { .makeMissing($0) }
1532+
)
1533+
],
1534+
handledNodes: rawDelimiters.map { $0.id }
1535+
)
1536+
}
1537+
1538+
return .visitChildren
1539+
}
1540+
1541+
public override func visit(_ node: SimpleStringLiteralSegmentListSyntax) -> SyntaxVisitorContinueKind {
1542+
if shouldSkip(node) {
1543+
return .skipChildren
1544+
}
1545+
1546+
for segment in node {
1547+
if let unexpectedAfterContent = segment.unexpectedAfterContent {
1548+
addDiagnostic(
1549+
node,
1550+
.forbiddenInterpolatedString,
1551+
handledNodes: [unexpectedAfterContent.id]
1552+
)
1553+
}
1554+
}
1555+
1556+
return .visitChildren
1557+
}
1558+
15091559
public override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
15101560
if shouldSkip(node) {
15111561
return .skipChildren

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ extension DiagnosticMessage where Self == StaticParserError {
165165
public static var extraRightBracket: Self {
166166
.init("unexpected ']' in type; did you mean to write an array type?")
167167
}
168+
public static var forbiddenExtendedEscapingString: Self {
169+
.init("argument cannot be an extended escaping string literal")
170+
}
171+
public static var forbiddenInterpolatedString: Self {
172+
return .init("argument cannot be an interpolated string literal")
173+
}
168174
public static var initializerInPattern: Self {
169175
.init("unexpected initializer in pattern; did you mean to use '='?")
170176
}

Sources/SwiftParserDiagnostics/SyntaxExtensions.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@ extension TokenKind {
163163
return false
164164
}
165165
}
166+
167+
var isRawStringDelimiter: Bool {
168+
switch self {
169+
case .rawStringPoundDelimiter:
170+
return true
171+
default:
172+
return false
173+
}
174+
}
166175
}
167176

168177
public extension TriviaPiece {

Sources/SwiftParserDiagnostics/generated/SyntaxKindNameForDiagnostics.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ extension SyntaxKind {
331331
return "'return' statement"
332332
case .sameTypeRequirement:
333333
return "same type requirement"
334+
case .simpleStringLiteralExpr:
335+
return "simple string literal"
334336
case .someOrAnyType:
335337
return "type"
336338
case .sourceFile:

0 commit comments

Comments
 (0)