diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index ad77e9d0fed..f76aabcc20d 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -32,6 +32,8 @@ extension TokenConsumer { } else { return true } + case (.primaryExpressionStart(.atSign), let handle)?: + break case (_, _)?: return true case nil: @@ -1168,6 +1170,8 @@ extension Parser { arena: self.arena ) ) + case (.atSign, _)?: + return RawExprSyntax(self.parseStringLiteral()) case (.rawStringDelimiter, _)?, (.stringQuote, _)?, (.multilineStringQuote, _)?, (.singleQuote, _)?: return RawExprSyntax(self.parseStringLiteral()) case (.extendedRegexDelimiter, _)?, (.regexSlash, _)?: diff --git a/Sources/SwiftParser/StringLiterals.swift b/Sources/SwiftParser/StringLiterals.swift index ffd756e6b49..9c422389172 100644 --- a/Sources/SwiftParser/StringLiterals.swift +++ b/Sources/SwiftParser/StringLiterals.swift @@ -467,8 +467,12 @@ extension Parser { /// Parse opening raw string delimiter if exist. let openDelimiter = self.consume(if: .rawStringDelimiter) + /// Try to parse @ in order to recover from Objective-C style literals + let unexpectedAtSign = self.consume(if: .atSign) + /// Parse open quote. var (unexpectedBeforeOpenQuote, openQuote) = self.expect(.stringQuote, .multilineStringQuote, default: .stringQuote) + unexpectedBeforeOpenQuote = RawUnexpectedNodesSyntax(combining: unexpectedAtSign, unexpectedBeforeOpenQuote, arena: self.arena) var openQuoteKind: RawTokenKind = openQuote.tokenKind if openQuote.isMissing, let singleQuote = self.consume(if: .singleQuote) { unexpectedBeforeOpenQuote = RawUnexpectedNodesSyntax(combining: unexpectedBeforeOpenQuote, singleQuote, arena: self.arena) diff --git a/Sources/SwiftParser/TokenSpecSet.swift b/Sources/SwiftParser/TokenSpecSet.swift index 96abcc6d484..9b5a1a136d9 100644 --- a/Sources/SwiftParser/TokenSpecSet.swift +++ b/Sources/SwiftParser/TokenSpecSet.swift @@ -569,6 +569,7 @@ enum ParameterModifier: TokenSpecSet { enum PrimaryExpressionStart: TokenSpecSet { case anyKeyword + case atSign // For recovery case capitalSelfKeyword case dollarIdentifier case falseKeyword @@ -598,6 +599,7 @@ enum PrimaryExpressionStart: TokenSpecSet { init?(lexeme: Lexer.Lexeme) { switch PrepareForKeywordMatch(lexeme) { case TokenSpec(.Any): self = .anyKeyword + case TokenSpec(.atSign): self = .atSign case TokenSpec(.Self): self = .capitalSelfKeyword case TokenSpec(.dollarIdentifier): self = .dollarIdentifier case TokenSpec(.false): self = .falseKeyword @@ -630,6 +632,7 @@ enum PrimaryExpressionStart: TokenSpecSet { var spec: TokenSpec { switch self { case .anyKeyword: return .keyword(.Any) + case .atSign: return .atSign case .capitalSelfKeyword: return .keyword(.Self) case .dollarIdentifier: return .dollarIdentifier case .falseKeyword: return .keyword(.false) diff --git a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift index 181e546b9c3..c18632be79c 100644 --- a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift +++ b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift @@ -1003,6 +1003,17 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } + // recover from Objective-C style literals + if let atSign = node.unexpectedBetweenOpenDelimiterAndOpenQuote?.onlyToken(where: { $0.tokenKind == .atSign }) { + addDiagnostic( + node, + .stringLiteralAtSign, + fixIts: [ + FixIt(message: RemoveNodesFixIt(atSign), changes: .makeMissing(atSign)) + ], + handledNodes: [atSign.id] + ) + } if let singleQuote = node.unexpectedBetweenOpenDelimiterAndOpenQuote?.onlyToken(where: { $0.tokenKind == .singleQuote }) { let fixIt = FixIt( message: ReplaceTokensFixIt(replaceTokens: [singleQuote], replacement: node.openQuote), diff --git a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift index 3cd88f30a2a..cd7ef6d296c 100644 --- a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift +++ b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift @@ -194,6 +194,9 @@ extension DiagnosticMessage where Self == StaticParserError { public static var standaloneSemicolonStatement: Self { .init("standalone ';' statements are not allowed") } + public static var stringLiteralAtSign: Self { + .init("string literals in Swift are not preceded by an '@' sign") + } public static var subscriptsCannotHaveNames: Self { .init("subscripts cannot have a name") } diff --git a/Tests/SwiftParserTest/translated/RecoveryTests.swift b/Tests/SwiftParserTest/translated/RecoveryTests.swift index a3c04523276..43c4da2aa9d 100644 --- a/Tests/SwiftParserTest/translated/RecoveryTests.swift +++ b/Tests/SwiftParserTest/translated/RecoveryTests.swift @@ -1881,16 +1881,20 @@ final class RecoveryTests: XCTestCase { } func testRecovery157() { + // QoI: Bad error message when using Objective-C literals (@"Hello") assertParse( #""" - // QoI: Bad error message when using Objective-C literals (@"Hello") in Swift files let myString = 1️⃣@"foo" """#, diagnostics: [ - // TODO: Old parser expected error on line 2: string literals in Swift are not preceded by an '@' sign, Fix-It replacements: 16 - 17 = '' - DiagnosticSpec(message: "expected expression in variable"), - DiagnosticSpec(message: #"extraneous code '@"foo"' at top level"#), - ] + DiagnosticSpec( + message: "string literals in Swift are not preceded by an '@' sign", + fixIts: ["remove '@'"] + ) + ], + fixedSource: """ + let myString = "foo" + """ ) }