Skip to content

Commit e830959

Browse files
committed
Add diagnostic for label with string segment
1 parent d5c8158 commit e830959

32 files changed

+895
-42
lines changed

CodeGeneration/Sources/SyntaxSupport/AvailabilityNodes.swift

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

CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1723,7 +1723,7 @@ public let DECL_NODES: [Node] = [
17231723
),
17241724
Child(
17251725
name: "FileName",
1726-
kind: .node(kind: .stringLiteralExpr),
1726+
kind: .node(kind: .simpleStringLiteralExpr),
17271727
nameForDiagnostics: "file name"
17281728
),
17291729
Child(

CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,38 @@ public let EXPR_NODES: [Node] = [
14941494
elementChoices: [.stringSegment, .expressionSegment]
14951495
),
14961496

1497+
Node(
1498+
kind: .simpleStringLiteralExpr,
1499+
base: .expr,
1500+
nameForDiagnostics: "simple string literal",
1501+
documentation: "A simple string that can’t contain string interpolation and cannot have raw string delimiters.",
1502+
children: [
1503+
Child(
1504+
name: "OpenQuote",
1505+
kind: .token(choices: [.token(tokenKind: "StringQuoteToken"), .token(tokenKind: "MultilineStringQuoteToken")]),
1506+
documentation: "Open quote for the string literal"
1507+
),
1508+
Child(
1509+
name: "Segments",
1510+
kind: .collection(kind: .simpleStringLiteralSegments, collectionElementName: "Segment"),
1511+
documentation: "String content"
1512+
),
1513+
Child(
1514+
name: "CloseQuote",
1515+
kind: .token(choices: [.token(tokenKind: "StringQuoteToken"), .token(tokenKind: "MultilineStringQuoteToken")]),
1516+
documentation: "Close quote for the string literal"
1517+
),
1518+
]
1519+
),
1520+
1521+
Node(
1522+
kind: .simpleStringLiteralSegments,
1523+
base: .syntaxCollection,
1524+
nameForDiagnostics: nil,
1525+
documentation: "String literal segments that only can contain non string interpolated or extended escaped strings",
1526+
elementChoices: [.stringSegment]
1527+
),
1528+
14971529
// string literal segment in a string interpolation expression.
14981530
Node(
14991531
kind: .stringSegment,

CodeGeneration/Sources/SyntaxSupport/SyntaxNodeKind.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ public enum SyntaxNodeKind: String, CaseIterable {
251251
case specializeAttributeSpecList
252252
case specializeExpr
253253
case stmt
254+
case simpleStringLiteralExpr
255+
case simpleStringLiteralSegments
254256
case stringLiteralExpr
255257
case stringLiteralSegments
256258
case stringSegment

Sources/SwiftBasicFormat/BasicFormat.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ open class BasicFormat: SyntaxRewriter {
330330
case \ExpressionSegmentSyntax.backslash,
331331
\ExpressionSegmentSyntax.rightParen,
332332
\DeclNameArgumentSyntax.colon,
333+
\SimpleStringLiteralExprSyntax.openQuote,
333334
\StringLiteralExprSyntax.openQuote,
334335
\RegexLiteralExprSyntax.openSlash:
335336
return false

Sources/SwiftParser/Availability.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@ extension Parser {
107107
(.renamed, let handle)?:
108108
let argumentLabel = self.eat(handle)
109109
let (unexpectedBeforeColon, colon) = self.expect(.colon)
110-
// FIXME: Make sure this is a string literal with no interpolation.
111-
let stringValue = self.parseStringLiteral()
110+
let stringValue = self.parseSimpleString()
112111

113112
entry = .availabilityLabeledArgument(
114113
RawAvailabilityLabeledArgumentSyntax(

Sources/SwiftParser/Directives.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ extension Parser {
239239
if !self.at(.rightParen) {
240240
let (unexpectedBeforeFile, file) = self.expect(.keyword(.file))
241241
let (unexpectedBeforeFileColon, fileColon) = self.expect(.colon)
242-
let fileName = self.parseStringLiteral()
242+
let fileName = self.parseSimpleString()
243243
let (unexpectedBeforeComma, comma) = self.expect(.comma)
244244

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

Sources/SwiftParser/StringLiterals.swift

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,10 +595,118 @@ extension Parser {
595595
)
596596
}
597597
}
598+
599+
mutating func parseSimpleString() -> RawSimpleStringLiteralExprSyntax {
600+
let openDelimiter = self.consume(if: .rawStringDelimiter)
601+
let (unexpectedBeforeOpenQuote, openQuote) = self.expect(anyIn: SimpleStringLiteralExprSyntax.OpenQuoteOptions.self, default: .stringQuote)
602+
603+
/// Parse segments.
604+
var segments: [RawStringSegmentSyntax] = []
605+
var loopProgress = LoopProgressCondition()
606+
while loopProgress.evaluate(self.currentToken) {
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 = self.consume(if: .backslash) {
615+
let (unexpectedBeforeDelimiter, delimiter) = self.parsePoundDelimiter(.rawStringDelimiter, matching: openDelimiter)
616+
let leftParen = self.expectWithoutRecoveryOrLeadingTrivia(.leftParen)
617+
let expressions = RawTupleExprElementListSyntax(elements: self.parseArgumentListElements(pattern: .none), arena: self.arena)
618+
619+
// For recovery, eat anything up to the next token that either starts a new string segment or terminates the string.
620+
// This allows us to skip over extraneous identifiers etc. in an unterminated string interpolation.
621+
var unexpectedBeforeRightParen: [RawTokenSyntax] = []
622+
var unexpectedProgress = LoopProgressCondition()
623+
while !self.at(.rightParen, .stringSegment, .backslash) && !self.at(TokenSpec(openQuote.tokenKind), .endOfFile)
624+
&& unexpectedProgress.evaluate(self.currentToken)
625+
{
626+
unexpectedBeforeRightParen.append(self.consumeAnyToken())
627+
}
628+
// Consume the right paren if present, ensuring that it's on the same
629+
// line if this is a single-line literal. Leading trivia is fine as
630+
// we allow e.g "\(foo )".
631+
let rightParen: Token
632+
if self.at(.rightParen) && self.currentToken.isAtStartOfLine && openQuote.tokenKind != .multilineStringQuote {
633+
rightParen = missingToken(.rightParen)
634+
} else {
635+
rightParen = self.expectWithoutRecovery(.rightParen)
636+
}
637+
if case .inStringInterpolation = self.currentToken.cursor.currentState {
638+
// The parser has more knowledge that we have reached the end of the
639+
// string interpolation now, even if we haven't seen the closing ')'.
640+
// For example, consider the following code
641+
// "\(abc "
642+
// Since the lexer doesn't know anything about the expression structure,
643+
// it assumes that the `"` starts a new string literal. But since we
644+
// know in the parser that an identifier cannot be followed by a string
645+
// literal without a connecting binary operator and can thus consider
646+
// it as the surrounding string literal end, which thus also terminates
647+
// the string interpolation.
648+
self.lexemes.perform(stateTransition: .pop, currentToken: &self.currentToken)
649+
}
650+
unexpectedAfterContent = RawUnexpectedNodesSyntax([
651+
backslash,
652+
delimiter,
653+
leftParen,
654+
// expressions,
655+
rightParen
656+
], arena: self.arena)
657+
// interpolatedSegments.append(
658+
// RawExpressionSegmentSyntax(
659+
// backslash: backslash,
660+
// unexpectedBeforeDelimiter,
661+
// rawStringDelimiter: delimiter,
662+
// leftParen: leftParen,
663+
// expressions: expressions,
664+
// RawUnexpectedNodesSyntax(unexpectedBeforeRightParen, arena: self.arena),
665+
// rightParen: rightParen,
666+
// arena: self.arena
667+
// )
668+
// )
669+
}
670+
671+
segments.append(RawStringSegmentSyntax(content: stringSegment, unexpectedAfterContent, arena: self.arena))
672+
} else {
673+
break
674+
}
675+
}
676+
677+
let (unexpectedBetweenSegmentAndCloseQuote, closeQuote) = self.expect(
678+
anyIn: SimpleStringLiteralExprSyntax.CloseQuoteOptions.self,
679+
default: openQuote.closeTokenKind
680+
)
681+
let closeDelimiter = self.consume(if: .rawStringDelimiter)
682+
683+
return RawSimpleStringLiteralExprSyntax(
684+
RawUnexpectedNodesSyntax(combining: unexpectedBeforeOpenQuote, openDelimiter, arena: self.arena),
685+
openQuote: openQuote,
686+
segments: RawSimpleStringLiteralSegmentsSyntax(elements: segments, arena: self.arena),
687+
unexpectedBetweenSegmentAndCloseQuote,
688+
closeQuote: closeQuote,
689+
RawUnexpectedNodesSyntax([closeDelimiter], arena: self.arena),
690+
arena: self.arena
691+
)
692+
}
598693
}
599694

600695
// MARK: - Utilities
601696

697+
fileprivate extension RawTokenSyntax {
698+
var closeTokenKind: SimpleStringLiteralExprSyntax.CloseQuoteOptions {
699+
switch self {
700+
case .multilineStringQuote:
701+
return .multilineStringQuote
702+
case .stringQuote:
703+
return .stringQuote
704+
default:
705+
fatalError("Unsupported type")
706+
}
707+
}
708+
}
709+
602710
fileprivate extension SyntaxText {
603711
private func hasSuffix(_ other: String) -> Bool {
604712
var other = other

Sources/SwiftParser/generated/Parser+TokenSpecSet.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1780,6 +1780,60 @@ extension SameTypeRequirementSyntax {
17801780
}
17811781
}
17821782

1783+
extension SimpleStringLiteralExprSyntax {
1784+
enum OpenQuoteOptions: TokenSpecSet {
1785+
case stringQuote
1786+
case multilineStringQuote
1787+
1788+
init?(lexeme: Lexer.Lexeme) {
1789+
switch PrepareForKeywordMatch(lexeme) {
1790+
case TokenSpec(.stringQuote):
1791+
self = .stringQuote
1792+
case TokenSpec(.multilineStringQuote):
1793+
self = .multilineStringQuote
1794+
default:
1795+
return nil
1796+
}
1797+
}
1798+
1799+
var spec: TokenSpec {
1800+
switch self {
1801+
case .stringQuote:
1802+
return .stringQuote
1803+
case .multilineStringQuote:
1804+
return .multilineStringQuote
1805+
}
1806+
}
1807+
}
1808+
}
1809+
1810+
extension SimpleStringLiteralExprSyntax {
1811+
enum CloseQuoteOptions: TokenSpecSet {
1812+
case stringQuote
1813+
case multilineStringQuote
1814+
1815+
init?(lexeme: Lexer.Lexeme) {
1816+
switch PrepareForKeywordMatch(lexeme) {
1817+
case TokenSpec(.stringQuote):
1818+
self = .stringQuote
1819+
case TokenSpec(.multilineStringQuote):
1820+
self = .multilineStringQuote
1821+
default:
1822+
return nil
1823+
}
1824+
}
1825+
1826+
var spec: TokenSpec {
1827+
switch self {
1828+
case .stringQuote:
1829+
return .stringQuote
1830+
case .multilineStringQuote:
1831+
return .multilineStringQuote
1832+
}
1833+
}
1834+
}
1835+
}
1836+
17831837
extension SimpleTypeIdentifierSyntax {
17841838
enum NameOptions: TokenSpecSet {
17851839
case identifier

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,38 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
14851485
return .visitChildren
14861486
}
14871487

1488+
public override func visit(_ node: SimpleStringLiteralExprSyntax) -> SyntaxVisitorContinueKind {
1489+
if shouldSkip(node) {
1490+
return .skipChildren
1491+
}
1492+
1493+
var rawDelimiters: [TokenSyntax] = []
1494+
1495+
if let unexpectedBeforeOpenQuote = node.unexpectedBeforeOpenQuote?.onlyPresentToken(where: { $0.tokenKind.isRawStringDelimiter }) {
1496+
rawDelimiters += [unexpectedBeforeOpenQuote]
1497+
}
1498+
1499+
if let unexpectedAfterCloseQuote = node.unexpectedAfterCloseQuote?.onlyPresentToken(where: { $0.tokenKind.isRawStringDelimiter }) {
1500+
rawDelimiters += [unexpectedAfterCloseQuote]
1501+
}
1502+
1503+
if !rawDelimiters.isEmpty {
1504+
addDiagnostic(
1505+
node,
1506+
.forbiddenExtendedEscapingString,
1507+
fixIts: [
1508+
FixIt(
1509+
message: RemoveNodesFixIt(rawDelimiters),
1510+
changes: rawDelimiters.map { .makeMissing($0) }
1511+
)
1512+
],
1513+
handledNodes: rawDelimiters.map { $0.id }
1514+
)
1515+
}
1516+
1517+
return .visitChildren
1518+
}
1519+
14881520
public override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
14891521
if shouldSkip(node) {
14901522
return .skipChildren

0 commit comments

Comments
 (0)