Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SwiftParser] Improve diagnostics for misspelled keywords #2794

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions Sources/SwiftParser/Declarations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,16 @@ extension Parser {
)

let recoveryResult: (match: DeclarationKeyword, handle: RecoveryConsumptionHandle)?
if let atResult = self.at(anyIn: DeclarationKeyword.self) {
if let atResult = self.at(anyIn: EitherTokenSpecSet<DeclarationKeyword, MisspelledPureDeclarationKeyword>.self) {
// We are at a keyword that starts a declaration. Parse that declaration.
recoveryResult = (atResult.spec, .noRecovery(atResult.handle))
let spec: DeclarationKeyword
switch atResult.spec {
case .lhs(let declarationKeyword):
spec = declarationKeyword
case .rhs(let mispelledDeclarationKeyword):
spec = .lhs(mispelledDeclarationKeyword.correctSpecSet)
}
recoveryResult = (spec, .noRecovery(atResult.handle))
} else if atFunctionDeclarationWithoutFuncKeyword() {
// We aren't at a declaration keyword and it looks like we are at a function
// declaration. Parse a function declaration.
Expand Down Expand Up @@ -885,7 +892,10 @@ extension Parser {
_ attrs: DeclAttributes,
_ handle: RecoveryConsumptionHandle
) -> RawAssociatedTypeDeclSyntax {
let (unexpectedBeforeAssocKeyword, assocKeyword) = self.eat(handle)
let (unexpectedBeforeAssocKeyword, assocKeyword) = self.expect(
keyword: .associatedtype,
handle: handle
)

// Detect an attempt to use a type parameter pack.
let eachKeyword = self.consume(if: .keyword(.each))
Expand Down Expand Up @@ -1018,7 +1028,10 @@ extension Parser {
_ attrs: DeclAttributes,
_ handle: RecoveryConsumptionHandle
) -> RawDeinitializerDeclSyntax {
let (unexpectedBeforeDeinitKeyword, deinitKeyword) = self.eat(handle)
let (unexpectedBeforeDeinitKeyword, deinitKeyword) = self.expect(
keyword: .deinit,
handle: handle
)

var unexpectedNameAndSignature: [RawSyntax?] = []

Expand Down Expand Up @@ -1406,7 +1419,7 @@ extension Parser {
// Check there is an identifier before consuming
var look = self.lookahead()
let _ = look.consumeAttributeList()
let hasModifier = look.consume(ifAnyIn: AccessorModifier.self) != nil
let hasModifier = look.consume(ifAnyIn: MisspelledAccessorModifier.FuzzyMatchSpecSet.self) != nil
guard let (kind, _) = look.at(anyIn: AccessorDeclSyntax.AccessorSpecifierOptions.self) ?? forcedKind else {
return nil
}
Expand All @@ -1417,7 +1430,10 @@ extension Parser {
// get and set.
let modifier: RawDeclModifierSyntax?
if hasModifier {
let (unexpectedBeforeName, name) = self.expect(anyIn: AccessorModifier.self, default: .mutating)
let (unexpectedBeforeName, name) = self.expectPossibleMisspelling(
anyIn: MisspelledAccessorModifier.self,
default: .mutating
)
modifier = RawDeclModifierSyntax(
unexpectedBeforeName,
name: name,
Expand Down
50 changes: 50 additions & 0 deletions Sources/SwiftParser/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,56 @@ extension Parser {
return self.eat(recoveryHandle)
}
}

mutating func expect(
spec: TokenSpec,
handle: RecoveryConsumptionHandle
) -> (unexpectedBeforeKeyword: RawUnexpectedNodesSyntax?, keywordToken: RawTokenSyntax) {
let tokenKind = spec.synthesizedTokenKind
if case .keyword(let keyword) = spec.synthesizedTokenKind {
return self.expect(keyword: keyword, handle: handle)
} else {
return (nil, missingToken(tokenKind.decomposeToRaw().rawKind, text: tokenKind.defaultText))
}
}

mutating func expect(
keyword: Keyword,
handle: RecoveryConsumptionHandle
) -> (unexpectedBeforeKeyword: RawUnexpectedNodesSyntax?, keywordToken: RawTokenSyntax) {
var (unexpectedBeforeKeyword, keywordToken) = self.eat(handle)

if keywordToken.tokenText != keyword.defaultText {
if let _ = unexpectedBeforeKeyword {
unexpectedBeforeKeyword = RawUnexpectedNodesSyntax(
combining: unexpectedBeforeKeyword,
keywordToken,
arena: self.arena
)
} else {
unexpectedBeforeKeyword = RawUnexpectedNodesSyntax([keywordToken], arena: self.arena)
}
keywordToken = missingToken(keyword)
}

return (unexpectedBeforeKeyword, keywordToken)
}

mutating func expectPossibleMisspelling<T: MisspelledTokenSpecSet>(
anyIn: T.Type,
default defaultKind: T.CorrectSpecSet
) -> (RawUnexpectedNodesSyntax?, RawTokenSyntax) {
if let (spec, handle) = self.at(anyIn: T.FuzzyMatchSpecSet.self) {
switch spec {
case .lhs(let misspelled):
return self.expect(spec: misspelled.correctSpecSet.spec, handle: .noRecovery(handle))
case .rhs:
return self.eat(.noRecovery(handle))
}
} else {
return (nil, missingToken(defaultKind.spec))
}
}
}

// MARK: Splitting Tokens
Expand Down
13 changes: 9 additions & 4 deletions Sources/SwiftParser/Statements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ extension Parser {
}

let optLabel = self.parseOptionalStatementLabel()
switch self.canRecoverTo(anyIn: CanBeStatementStart.self) {
let recovery = self.canRecoverTo(anyIn: MisspelledCanBeStatementStart.FuzzyMatchSpecSet.self).map {
($0.match.correctSpecSet, $0.handle)
}
switch recovery {
case (.for, let handle)?:
return label(self.parseForStatement(forHandle: handle), with: optLabel)
case (.while, let handle)?:
Expand Down Expand Up @@ -140,7 +143,7 @@ extension Parser {
extension Parser {
/// Parse a guard statement.
mutating func parseGuardStatement(guardHandle: RecoveryConsumptionHandle) -> RawGuardStmtSyntax {
let (unexpectedBeforeGuardKeyword, guardKeyword) = self.eat(guardHandle)
let (unexpectedBeforeGuardKeyword, guardKeyword) = self.expect(keyword: .guard, handle: guardHandle)
let conditions = self.parseConditionList(isGuardStatement: true)
let (unexpectedBeforeElseKeyword, elseKeyword) = self.expect(.keyword(.else))
let body = self.parseCodeBlock(introducer: guardKeyword)
Expand Down Expand Up @@ -915,10 +918,12 @@ extension Parser.Lookahead {
_ = self.consume(if: .identifier, followedBy: .colon)
let switchSubject: CanBeStatementStart?
if allowRecovery {
switchSubject = self.canRecoverTo(anyIn: CanBeStatementStart.self)?.0
switchSubject =
self.canRecoverTo(anyIn: MisspelledCanBeStatementStart.FuzzyMatchSpecSet.self)?.match.correctSpecSet
} else {
switchSubject = self.at(anyIn: CanBeStatementStart.self)?.0
switchSubject = self.at(anyIn: MisspelledCanBeStatementStart.FuzzyMatchSpecSet.self)?.spec.correctSpecSet
}

switch switchSubject {
case .return?,
.throw?,
Expand Down
165 changes: 164 additions & 1 deletion Sources/SwiftParser/TokenSpecSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ protocol TokenSpecSet: CaseIterable {
init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures)
}

protocol MisspelledTokenSpecSet: TokenSpecSet {
associatedtype CorrectSpecSet: TokenSpecSet

typealias FuzzyMatchSpecSet = EitherTokenSpecSet<Self, CorrectSpecSet>

var correctSpecSet: CorrectSpecSet { get }
}

extension MisspelledTokenSpecSet {
var spec: TokenSpec {
TokenSpec(.identifier, recoveryPrecedence: correctSpecSet.spec.recoveryPrecedence)
}
}

/// A way to combine two token spec sets into an aggregate token spec set.
enum EitherTokenSpecSet<LHS: TokenSpecSet, RHS: TokenSpecSet>: TokenSpecSet {
case lhs(LHS)
Expand Down Expand Up @@ -58,6 +72,17 @@ enum EitherTokenSpecSet<LHS: TokenSpecSet, RHS: TokenSpecSet>: TokenSpecSet {
}
}

extension EitherTokenSpecSet where LHS: MisspelledTokenSpecSet, RHS == LHS.CorrectSpecSet {
var correctSpecSet: RHS {
switch self {
case .lhs(let misspelled):
return misspelled.correctSpecSet
case .rhs(let correct):
return correct
}
}
}

// MARK: - Subsets

enum AccessorModifier: TokenSpecSet {
Expand Down Expand Up @@ -89,6 +114,42 @@ enum AccessorModifier: TokenSpecSet {
}
}

enum MisspelledAccessorModifier: MisspelledTokenSpecSet {
case consuming
case borrowing
case nonmutating

var correctSpecSet: AccessorModifier {
switch self {
case .consuming: return .consuming
case .borrowing: return .borrowing
case .nonmutating: return .nonmutating
}
}

init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
let text = lexeme.tokenText
switch text.count {
case 6:
switch text {
case "borrow": self = .borrowing
default: return nil
}
case 7:
switch text {
case "consume": self = .consuming
default: return nil
}
case 11:
switch text {
case "nonMutating": self = .nonmutating
default: return nil
}
default: return nil
}
}
}

enum CanBeStatementStart: TokenSpecSet {
case `break`
case `continue`
Expand Down Expand Up @@ -151,6 +212,35 @@ enum CanBeStatementStart: TokenSpecSet {
}
}

enum MisspelledCanBeStatementStart: MisspelledTokenSpecSet {
case `guard`
case `switch`

var correctSpecSet: CanBeStatementStart {
switch self {
case .guard: return .guard
case .switch: return .switch
}
}

init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
let text = lexeme.tokenText
switch text.count {
case 5:
switch text {
case "gaurd": self = .guard
default: return nil
}
case 6:
switch text {
case "siwtch": self = .switch
default: return nil
}
default: return nil
}
}
}

enum CompilationCondition: TokenSpecSet {
case swift
case compiler
Expand Down Expand Up @@ -266,7 +356,6 @@ enum ContextualDeclKeyword: TokenSpecSet {
}
}
}

/// A `DeclarationKeyword` that is not a `ValueBindingPatternSyntax.BindingSpecifierOptions`.
///
/// `ValueBindingPatternSyntax.BindingSpecifierOptions` are injected into
Expand Down Expand Up @@ -339,6 +428,80 @@ enum PureDeclarationKeyword: TokenSpecSet {
}
}

enum MisspelledPureDeclarationKeyword: MisspelledTokenSpecSet {
case `associatedtype`
case `class`
case `deinit`
case `func`
case `init`
case `precedencegroup`
case `protocol`
case `typealias`

var correctSpecSet: PureDeclarationKeyword {
switch self {
case .associatedtype: return .associatedtype
case .class: return .class
case .deinit: return .deinit
case .func: return .func
case .`init`: return .`init`
case .precedencegroup: return .precedencegroup
case .protocol: return .protocol
case .typealias: return .typealias
}
}

init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) {
let text = lexeme.tokenText
switch text.count {
case 3:
switch text {
case "def": self = .func
case "fun": self = .func
default: return nil
}
case 6:
switch text {
case "deInit": self = .deinit
case "object": self = .class
default: return nil
}
case 8:
switch text {
case "function": self = .func
default: return nil
}
AppAppWorks marked this conversation as resolved.
Show resolved Hide resolved
case 9:
switch text {
case "interface": self = .protocol
case "typeAlias": self = .typealias
default: return nil
}
case 11:
switch text {
case "constructor": self = .`init`
default: return nil
}
case 13:
switch text {
case "associatetype", "associateType": self = .associatedtype
default: return nil
}
case 14:
switch text {
case "associatedType": self = .associatedtype
default: return nil
}
case 15:
switch text {
case "precedenceGroup": self = .precedencegroup
default: return nil
}
default: return nil
}
}
}

typealias DeclarationKeyword = EitherTokenSpecSet<
PureDeclarationKeyword,
ValueBindingPatternSyntax.BindingSpecifierOptions
Expand Down
Loading