diff --git a/CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift b/CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift index 0734210ab5b..352ab27cc6d 100644 --- a/CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift +++ b/CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift @@ -80,6 +80,16 @@ public func keywordsByLength() -> [(Int, [KeywordSpec])] { return result.sorted(by: { $0.key < $1.key }) } +public func falseFriendKeywordsByLength() -> [(Int, [(Keyword.FalseFriend, KeywordSpec)])] { + var result = [Int: [(Keyword.FalseFriend, KeywordSpec)]]() + for keyword in Keyword.allCases { + for falseFriend in keyword.falseFriends { + result[falseFriend.text.utf8CodeUnitCount, default: []].append((falseFriend, keyword.spec)) + } + } + return result.sorted(by: { $0.key < $1.key }) +} + public enum Keyword: CaseIterable { // Please keep these sorted alphabetically @@ -739,4 +749,680 @@ public enum Keyword: CaseIterable { return KeywordSpec("yield") } } + + public struct ProgrammingLanguages: OptionSet, Hashable, CustomStringConvertible { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static let bash = ProgrammingLanguages(rawValue: 1 << 0) + public static let c = ProgrammingLanguages(rawValue: 1 << 1) + public static let cSharp = ProgrammingLanguages(rawValue: 1 << 2) + public static let cxx = ProgrammingLanguages(rawValue: 1 << 3) + public static let dart = ProgrammingLanguages(rawValue: 1 << 4) + public static let go = ProgrammingLanguages(rawValue: 1 << 5) + public static let java = ProgrammingLanguages(rawValue: 1 << 6) + public static let javaScript = ProgrammingLanguages(rawValue: 1 << 7) + public static let kotlin = ProgrammingLanguages(rawValue: 1 << 8) + public static let objC = ProgrammingLanguages(rawValue: 1 << 9) + public static let php = ProgrammingLanguages(rawValue: 1 << 10) + public static let powerShell = ProgrammingLanguages(rawValue: 1 << 11) + public static let python = ProgrammingLanguages(rawValue: 1 << 12) + public static let ruby = ProgrammingLanguages(rawValue: 1 << 13) + public static let rust = ProgrammingLanguages(rawValue: 1 << 14) + public static let scala = ProgrammingLanguages(rawValue: 1 << 15) + public static let typeScript = ProgrammingLanguages(rawValue: 1 << 16) + public static let visualBasic = ProgrammingLanguages(rawValue: 1 << 17) + public static let zig = ProgrammingLanguages(rawValue: 1 << 18) + + public static let cFamily: ProgrammingLanguages = [.c, .cxx, .objC] + public static let ecmaScript: ProgrammingLanguages = [.javaScript, .typeScript] + + public static let descriptions: [(ProgrammingLanguages, String)] = [ + (.bash, "Bash"), + (.c, "C"), + (.cSharp, "C#"), + (.cxx, "C++"), + (.dart, "Dart"), + (.go, "Go"), + (.java, "Java"), + (.javaScript, "JavaScript"), + (.kotlin, "Kotlin"), + (.objC, "Objective-C"), + (.php, "PHP"), + (.powerShell, "PowerShell"), + (.python, "Python"), + (.ruby, "Ruby"), + (.rust, "Rust"), + (.scala, "Scala"), + (.typeScript, "TypeScript"), + (.visualBasic, "Visual Basic"), + (.zig, "Zig"), + ] + + public var description: String { + ProgrammingLanguages.descriptions.filter { language, _ in + self.contains(language) + }.map(\.1) + .joined(separator: " / ") + } + } + + public enum FalseFriend: ExpressibleByStringLiteral { + case mispelling(StaticString) + case languages(StaticString, ProgrammingLanguages) + + public init(stringLiteral value: StaticString) { + self = .mispelling(value) + } + + public var text: StaticString { + switch self { + case .mispelling(let text), .languages(let text, _): + return text + } + } + + public var remark: String { + switch self { + case .mispelling: + return "misspelling" + case .languages(_, let programmingLanguages): + return "possible influences: \(programmingLanguages)" + } + } + } + + public var falseFriends: [FalseFriend] { + switch self { + case .__consuming: + return [ + "__consume", + "__consumed", + ] + case .__owned: + return [ + "__own", + "__owning", + ] + case .__setter_access: + return [] + case .__shared: + return [ + "__sharing" + ] + case ._alignment: + return [ + "_align", + "_aligned", + ] + case ._backDeploy: + return [] + case ._borrow: + return [ + .mispelling("_borrowed"), + .mispelling("_borrowing"), + ] + case ._borrowing: + return [ + "_borrow", + "_borrowed", + ] + case ._BridgeObject: + return [] + case ._cdecl: + return [ + "cdeclaration" + ] + case ._Class: + return [] + case ._compilerInitialized: + return [] + case ._const: + return ["_final"] + case ._consuming: + return [ + "_consume", + "_consumed", + ] + case ._documentation: + return [] + case ._dynamicReplacement: + return [] + case ._effects: + return [] + case ._expose: + return [] + case ._forward: + return [] + case ._implements: + return ["_extends"] + case ._linear: + return [] + case ._local: + return [] + case ._modify: + return [] + case ._move: + return [] + case ._mutating: + return [ + "_mutate", + "_mutated", + ] + case ._NativeClass: + return [] + case ._NativeRefCountedObject: + return [] + case ._noMetadata: + return [] + case ._nonSendable: + return [ + "_noSend", + "_nonSend", + "_noSending", + "_nonSending", + ] + case ._objcImplementation: + return ["_objectiveCImplementation"] + case ._objcRuntimeName: + return ["_objectiveCRuntimeName"] + case ._opaqueReturnTypeOf: + return [] + case ._optimize: + return [] + case ._originallyDefinedIn: + return [] + case ._PackageDescription: + return [] + case ._private: + return [] + case ._projectedValueProperty: + return [] + case ._read: + return [] + case ._RefCountedObject: + return [] + case ._semantics: + return [] + case ._specialize: + return [] + case ._spi: + return [] + case ._spi_available: + return [] + case ._swift_native_objc_runtime_base: + return [] + case ._Trivial: + return [] + case ._TrivialAtMost: + return [] + case ._TrivialStride: + return [] + case ._typeEraser: + return [] + case ._unavailableFromAsync: + return [] + case ._underlyingVersion: + return [] + case ._UnknownLayout: + return [] + case ._version: + return [] + case .accesses: + return [] + case .actor: + return [] + case .addressWithNativeOwner: + return [] + case .addressWithOwner: + return [] + // FIXME: dyn => any? + case .any: + return [ + "dyn" // Rust + ] + case .Any: + return [] + case .as: + return [] + case .assignment: + return [] + case .associatedtype: + return [] + case .associativity: + return [] + case .async: + return [] + case .attached: + return [ + "attach", + "attaching", + ] + case .autoclosure: + return [] + case .availability: + return [] + case .available: + return [] + case .await: + return [] + case .backDeployed: + return [] + case .before: + return [] + case .block: + return [] + case .borrowing: + return [ + "borrow", + "borrowed", + ] + case .break: + return [] + case .canImport: + return [] + case .case: + return [] + case .catch: + return [ + .languages("except", .python), + .languages("rescue", .ruby), + ] + case .class: + return [] + case .compiler: + return [] + case .consume: + return ["consuming"] + case .copy: + return ["clone"] + case .consuming: + return [ + "consume", + "consumed", + ] + case .continue: + return [ + .languages("next", .ruby), + "continuing", + ] + case .convenience: + return ["convenient"] + case .convention: + return [] + case .cType: + return [] + case .default: + return [] + case .defer: + return [] + case .deinit: + return [ + .languages("dealloc", .objC) + ] + case .dependsOn: + return [] + case .deprecated: + return [] + case .derivative: + return [] + case .didSet: + return [] + case .differentiable: + return [] + case .distributed: + return ["distributing"] + case .do: + return [] + case .dynamic: + return [] + case .each: + return [] + // - TODO: 1-to-N + case .else: + return [ + .languages("elif", [.bash, .python]), + .languages("elsif", .ruby), + // FIXME: VB case-insensitive + .languages("elseif", [.php, .powerShell, .visualBasic]), + ] + case .enum: + return [] + case .escaping: + return ["escape"] + case .exclusivity: + return [] + case .exported: + return ["export"] + case .extension: + return [ + .languages("impl", .rust), + // FIXME: partial => extension? + .languages("partial", .cSharp), + ] + case .fallthrough: + return [] + case .false: + return [ + .languages("NO", .objC) + ] + case .file: + return [] + case .fileprivate: + return [] + // TODO: .final + case .final: + return [] + case .for: + return [ + .languages("foreach", [.cSharp, .powerShell]) + ] + case .discard: + return [] + case .forward: + return [] + case .func: + // TODO: fun will be covered by Levenshtein Distance + return [ + .languages("fn", [.rust, .zig]), + .languages("def", [.python, .ruby, .rust, .scala]), + // FIXME: VB case-insensitive + .languages("sub", .visualBasic), + // FIXME: VB case-insensitive + .languages("function", [.ecmaScript, .visualBasic]), + ] + case .freestanding: + return [] + case .get: + return [] + case .guard: + return [ + .languages("unless", .ruby) + ] + case .higherThan: + return [] + case .if: + return [] + case .import: + return [ + .languages("include", .cFamily) + ] + case .in: + return [ + // FIXME: : => in? + .languages(":", [.cxx, .java]) + ] + case .indirect: + return [] + case .infix: + return [] + case .`init`: + return [ + .languages("constructor", [.ecmaScript, .kotlin]) + ] + case .initializes: + return [] + case .inline: + return [] + case .inout: + return [ + .languages("out", .cSharp), + .languages("ref", .cSharp), + ] + case .internal: + return [ + // FIXME: protected => internal? + // FIXME: VB case-insensitive + .languages("protected", [.cxx, .cSharp, .java, .kotlin, .php, .scala, .visualBasic]) + ] + case .introduced: + return [] + case .is: + return [ + .languages("instanceof", [.ecmaScript, .java, .php]) + ] + case .isolated: + return ["isolating"] + case .kind: + return [] + case .lazy: + return [ + .languages("late", .dart), + .languages("lateinit", .kotlin), + ] + case .left: + return [] + case .let: + return [ + // FIXME: VB case-insensitive + .languages("dim", .visualBasic), + .languages("val", [.kotlin, .scala]), + .languages("const", [.ecmaScript, .zig]), + .languages("final", .dart), + .languages("constexpr", .cxx), + ] + case .line: + return [] + case .linear: + return [] + case .lowerThan: + return [] + case .macro: + return [] + case .message: + return [] + case .metadata: + return [] + case .module: + return [ + // FIXME: mod => module? + .languages("mod", [.go, .rust]) + ] + case .mutableAddressWithNativeOwner: + return [] + case .mutableAddressWithOwner: + return [] + case .mutating: + return ["mutate"] + case .nil: + return [ + .languages("NaN", .ecmaScript), + .languages("None", .python), + .languages("null", [.cSharp, .java, .ecmaScript, .zig]), + .languages("NULL", .cFamily), + .languages("nullptr", .cxx), + .languages("undefined", [.ecmaScript, .zig]), + ] + case .noasync: + return [] + case .noDerivative: + return [] + case .noescape: + return ["noescaping"] + case .none: + return [] + case .nonisolated: + return ["nonisolating"] + case .nonmutating: + return [ + .languages("const", .cxx), + "nonmutate", + ] + case .objc: + return ["objectiveC"] + case .obsoleted: + return [] + case .of: + return [] + case .open: + return [] + case .operator: + return [] + case .optional: + return [] + case .override: + return [] + case .package: + return [] + case .postfix: + return [] + case .precedencegroup: + return [] + case .preconcurrency: + return [] + case .prefix: + return [] + case .private: + return [] + case .Protocol: + return [] + case .protocol: + return [ + .languages("trait", [.php, .rust, .scala]), + .languages("interface", [.cSharp, .java, .php, .typeScript]), + // FIXME: typeclass => protocol? + .languages("typeclass", .scala), + ] + case .public: + return [ + .languages("pub", [.rust, .zig]), + // FIXME: VB case-insensitive + .languages("friend", [.cxx, .visualBasic]), + ] + case .reasync: + return [] + case .renamed: + return [] + case .repeat: + return [] + case .required: + return [] + case .rethrows: + return [] + case .retroactive: + return [] + case .return: + return [] + case .reverse: + return [] + case .right: + return [] + case .safe: + return [] + case .scoped: + return [] + case .self: + return [ + .languages("this", [.cxx, .dart, .ecmaScript, .java, .kotlin, .scala]) + ] + case .sending: + return [ + "send", + "sendable", + ] + case .Self: + return [] + case .Sendable: + return [ + "Send", + "Sending", + ] + case .set: + return [] + case .some: + return [] + case .sourceFile: + return [] + case .spi: + return [] + case .spiModule: + return [] + case .static: + return [] + case .struct: + return [] + case .subscript: + return [] + case .super: + return [] + case .swift: + return [] + case .switch: + return [ + .languages("when", .kotlin), + .languages("match", .scala), + // FIXME: VB case-insensitive + .languages("select", .visualBasic), + ] + case .target: + return [] + case .then: + return [] + case .throw: + return [ + .languages("raise", .python) + ] + case .throws: + return [] + case .transpose: + return [] + case .true: + return [ + .languages("YES", .objC) + ] + case .try: + return [] + case .Type: + return [ + // FIXME: prototype => Type? + .languages("prototype", .ecmaScript) + ] + case .typealias: + return [ + .languages("type", [.go, .rust, .scala, .typeScript]), + .languages("alias", .ruby), + .languages("typedef", [.cFamily, .dart]), + ] + case .unavailable: + return [] + case .unchecked: + return [] + case .unowned: + return [] + case .unsafe: + return [] + case .unsafeAddress: + return [] + case .unsafeMutableAddress: + return [] + // FIXME: mut => var? + case .var: + return [ + .languages("mut", .rust) + ] + case .visibility: + return [] + case .weak: + return [] + case .where: + return [ + .languages("when", .cSharp) + ] + case .while: + return [ + .languages("until", .bash) + ] + case .willSet: + return [] + case .witness_method: + return [] + case .wrt: + return [] + case .yield: + return [] + } + } } diff --git a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/KeywordFile.swift b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/KeywordFile.swift index b9d66101534..b32efc6c01e 100644 --- a/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/KeywordFile.swift +++ b/CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/KeywordFile.swift @@ -54,6 +54,29 @@ let keywordFile = SourceFileSyntax(leadingTrivia: copyrightHeader) { } } + try! InitializerDeclSyntax( + """ + @_spi(RawSyntax) + public init?(falseFriendText text: SyntaxText) + """ + ) { + try! SwitchExprSyntax("switch text.count") { + for (length, falseFriendKeywords) in falseFriendKeywordsByLength() { + SwitchCaseSyntax("case \(raw: length):") { + try! SwitchExprSyntax("switch text") { + for (falseFriend, keyword) in falseFriendKeywords { + SwitchCaseSyntax("case \(literal: falseFriend.text.description): // \(raw: falseFriend.remark)") { + ExprSyntax("self = .\(keyword.varOrCaseName)") + } + } + SwitchCaseSyntax("default: return nil") + } + } + } + SwitchCaseSyntax("default: return nil") + } + } + DeclSyntax( """ /// This is really unfortunate. Really, we should have a `switch` in diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index 9208be5218e..0a04a87ee44 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -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.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. @@ -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)) @@ -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?] = [] @@ -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 } @@ -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, diff --git a/Sources/SwiftParser/Parser.swift b/Sources/SwiftParser/Parser.swift index 379e3d591f7..bd6080c8900 100644 --- a/Sources/SwiftParser/Parser.swift +++ b/Sources/SwiftParser/Parser.swift @@ -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( + 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 diff --git a/Sources/SwiftParser/Statements.swift b/Sources/SwiftParser/Statements.swift index fa0c5a7af53..e72c903ee65 100644 --- a/Sources/SwiftParser/Statements.swift +++ b/Sources/SwiftParser/Statements.swift @@ -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)?: @@ -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) @@ -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?, diff --git a/Sources/SwiftParser/TokenSpecSet.swift b/Sources/SwiftParser/TokenSpecSet.swift index e2b46112829..58656749e97 100644 --- a/Sources/SwiftParser/TokenSpecSet.swift +++ b/Sources/SwiftParser/TokenSpecSet.swift @@ -26,6 +26,20 @@ protocol TokenSpecSet: CaseIterable { init?(lexeme: Lexer.Lexeme, experimentalFeatures: Parser.ExperimentalFeatures) } +protocol MisspelledTokenSpecSet: TokenSpecSet { + associatedtype CorrectSpecSet: TokenSpecSet + + typealias FuzzyMatchSpecSet = EitherTokenSpecSet + + 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: TokenSpecSet { case lhs(LHS) @@ -58,6 +72,17 @@ enum EitherTokenSpecSet: 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 { @@ -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` @@ -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 @@ -266,7 +356,6 @@ enum ContextualDeclKeyword: TokenSpecSet { } } } - /// A `DeclarationKeyword` that is not a `ValueBindingPatternSyntax.BindingSpecifierOptions`. /// /// `ValueBindingPatternSyntax.BindingSpecifierOptions` are injected into @@ -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 + } + 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 diff --git a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift index 19ddeb2b670..ccbb88c529c 100644 --- a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift +++ b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift @@ -355,6 +355,23 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .visitChildren } + private func replaceMisspelledKeyword( + keyword: TokenSyntax, + unexpected: UnexpectedNodesSyntax?, + message: @autoclosure () -> some DiagnosticMessage + ) { + if keyword.isMissing { + exchangeTokens( + unexpected: unexpected, + unexpectedTokenCondition: { $0.tokenKind.isIdentifier }, + correctTokens: [keyword], + message: { _ in message() }, + moveFixIt: { ReplaceTokensFixIt(replaceTokens: $0, replacements: [keyword]) }, + removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) } + ) + } + } + // MARK: - Generic diagnostic generation public override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { @@ -520,6 +537,22 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .visitChildren } + public override func visit(_ node: AccessorDeclSyntax) -> SyntaxVisitorContinueKind { + guard !shouldSkip(node) else { + return .skipChildren + } + + if let modifier = node.modifier { + replaceMisspelledKeyword( + keyword: modifier.name, + unexpected: modifier.unexpectedBeforeName, + message: MisspelledKeyword(keywordRole: "accessor modifier", token: modifier.name) + ) + } + + return .visitChildren + } + public override func visit(_ node: AccessorEffectSpecifiersSyntax) -> SyntaxVisitorContinueKind { return handleEffectSpecifiers(node) } @@ -528,6 +561,13 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } + + replaceMisspelledKeyword( + keyword: node.associatedtypeKeyword, + unexpected: node.unexpectedBetweenModifiersAndAssociatedtypeKeyword, + message: .misspelledAssociatedtype + ) + // Emit a custom diagnostic for an unexpected 'each' before an associatedtype // name. removeToken( @@ -791,11 +831,31 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .visitChildren } + public override func visit(_ node: GuardStmtSyntax) -> SyntaxVisitorContinueKind { + guard !shouldSkip(node) else { + return .skipChildren + } + + replaceMisspelledKeyword( + keyword: node.guardKeyword, + unexpected: node.unexpectedBeforeGuardKeyword, + message: MisspelledKeyword(keywordRole: "'guard' keyword", token: node.guardKeyword) + ) + + return .visitChildren + } + public override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind { if shouldSkip(node) { return .skipChildren } + replaceMisspelledKeyword( + keyword: node.deinitKeyword, + unexpected: node.unexpectedBetweenModifiersAndDeinitKeyword, + message: .misspelledDeinit + ) + let name: TokenSyntax? = node .unexpectedBetweenDeinitKeywordAndEffectSpecifiers? .presentTokens(satisfying: \.tokenKind.isIdentifier) diff --git a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift index 2a1b2a02c40..0fae7ce4911 100644 --- a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift +++ b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift @@ -203,9 +203,15 @@ extension DiagnosticMessage where Self == StaticParserError { public static var missingConformanceRequirement: Self { .init("expected ':' or '==' to indicate a conformance or same-type requirement") } + public static var misspelledAssociatedtype: Self { + .init("expected 'associatedtype' keyword; did you mean 'associatedtype'?") + } public static var misspelledAsync: Self { .init("expected async specifier; did you mean 'async'?") } + public static var misspelledDeinit: Self { + .init("expected 'deinit' keyword; did you mean 'deinit'?") + } public static var misspelledThrows: Self { .init("expected throwing specifier; did you mean 'throws'?") } @@ -481,6 +487,15 @@ public struct MissingExpressionInStatement: ParserError { } } +public struct MisspelledKeyword: ParserError { + let keywordRole: String + let token: TokenSyntax + + public var message: String { + "expected \(keywordRole); did you mean '\(token.text)'?" + } +} + public struct NegatedAvailabilityCondition: ParserError { public let availabilityCondition: AvailabilityConditionSyntax public let negatedAvailabilityKeyword: TokenSyntax diff --git a/Sources/SwiftSyntax/generated/Keyword.swift b/Sources/SwiftSyntax/generated/Keyword.swift index b972a875aea..e262a82d491 100644 --- a/Sources/SwiftSyntax/generated/Keyword.swift +++ b/Sources/SwiftSyntax/generated/Keyword.swift @@ -804,6 +804,292 @@ public enum Keyword: UInt8, Hashable, Sendable { } } + @_spi(RawSyntax) + public init?(falseFriendText text: SyntaxText) { + switch text.count { + case 1: + switch text { + case ":": // possible influences: C++ / Java + self = .in + default: + return nil + } + case 2: + switch text { + case "NO": // possible influences: Objective-C + self = .false + case "fn": // possible influences: Rust / Zig + self = .func + default: + return nil + } + case 3: + switch text { + case "dyn": // misspelling + self = .any + case "def": // possible influences: Python / Ruby / Rust / Scala + self = .func + case "sub": // possible influences: Visual Basic + self = .func + case "out": // possible influences: C# + self = .inout + case "ref": // possible influences: C# + self = .inout + case "dim": // possible influences: Visual Basic + self = .let + case "val": // possible influences: Kotlin / Scala + self = .let + case "mod": // possible influences: Go / Rust + self = .module + case "NaN": // possible influences: JavaScript / TypeScript + self = .nil + case "pub": // possible influences: Rust / Zig + self = .public + case "YES": // possible influences: Objective-C + self = .true + case "mut": // possible influences: Rust + self = .var + default: + return nil + } + case 4: + switch text { + case "next": // possible influences: Ruby + self = .continue + case "elif": // possible influences: Bash / Python + self = .else + case "impl": // possible influences: Rust + self = .extension + case "late": // possible influences: Dart + self = .lazy + case "None": // possible influences: Python + self = .nil + case "null": // possible influences: C# / Java / JavaScript / TypeScript / Zig + self = .nil + case "NULL": // possible influences: C / C++ / Objective-C + self = .nil + case "this": // possible influences: C++ / Dart / Java / JavaScript / Kotlin / Scala / TypeScript + self = .self + case "send": // misspelling + self = .sending + case "Send": // misspelling + self = .Sendable + case "when": // possible influences: Kotlin + self = .switch + case "type": // possible influences: Go / Rust / Scala / TypeScript + self = .typealias + case "when": // possible influences: C# + self = .where + default: + return nil + } + case 5: + switch text { + case "__own": // misspelling + self = .__owned + case "clone": // misspelling + self = .copy + case "elsif": // possible influences: Ruby + self = .else + case "const": // possible influences: JavaScript / TypeScript / Zig + self = .let + case "final": // possible influences: Dart + self = .let + case "const": // possible influences: C++ + self = .nonmutating + case "trait": // possible influences: PHP / Rust / Scala + self = .protocol + case "match": // possible influences: Scala + self = .switch + case "raise": // possible influences: Python + self = .throw + case "alias": // possible influences: Ruby + self = .typealias + case "until": // possible influences: Bash + self = .while + default: + return nil + } + case 6: + switch text { + case "_align": // misspelling + self = ._alignment + case "_final": // misspelling + self = ._const + case "attach": // misspelling + self = .attached + case "borrow": // misspelling + self = .borrowing + case "except": // possible influences: Python + self = .catch + case "rescue": // possible influences: Ruby + self = .catch + case "elseif": // possible influences: PHP / PowerShell / Visual Basic + self = .else + case "escape": // misspelling + self = .escaping + case "export": // misspelling + self = .exported + case "unless": // possible influences: Ruby + self = .guard + case "mutate": // misspelling + self = .mutating + case "friend": // possible influences: C++ / Visual Basic + self = .public + case "select": // possible influences: Visual Basic + self = .switch + default: + return nil + } + case 7: + switch text { + case "_borrow": // misspelling + self = ._borrowing + case "_mutate": // misspelling + self = ._mutating + case "_noSend": // misspelling + self = ._nonSendable + case "consume": // misspelling + self = .consuming + case "dealloc": // possible influences: Objective-C + self = .deinit + case "partial": // possible influences: C# + self = .extension + case "foreach": // possible influences: C# / PowerShell + self = .for + case "include": // possible influences: C / C++ / Objective-C + self = .import + case "nullptr": // possible influences: C++ + self = .nil + case "Sending": // misspelling + self = .Sendable + case "typedef": // possible influences: C / C++ / Dart / Objective-C + self = .typealias + default: + return nil + } + case 8: + switch text { + case "__owning": // misspelling + self = .__owned + case "_aligned": // misspelling + self = ._alignment + case "_consume": // misspelling + self = ._consuming + case "_extends": // misspelling + self = ._implements + case "_mutated": // misspelling + self = ._mutating + case "_nonSend": // misspelling + self = ._nonSendable + case "borrowed": // misspelling + self = .borrowing + case "consumed": // misspelling + self = .consuming + case "function": // possible influences: JavaScript / TypeScript / Visual Basic + self = .func + case "lateinit": // possible influences: Kotlin + self = .lazy + case "sendable": // misspelling + self = .sending + default: + return nil + } + case 9: + switch text { + case "__consume": // misspelling + self = .__consuming + case "__sharing": // misspelling + self = .__shared + case "_borrowed": // misspelling + self = ._borrow + case "_borrowed": // misspelling + self = ._borrowing + case "_consumed": // misspelling + self = ._consuming + case "attaching": // misspelling + self = .attached + case "consuming": // misspelling + self = .consume + case "protected": // possible influences: C# / C++ / Java / Kotlin / PHP / Scala / Visual Basic + self = .internal + case "isolating": // misspelling + self = .isolated + case "constexpr": // possible influences: C++ + self = .let + case "undefined": // possible influences: JavaScript / TypeScript / Zig + self = .nil + case "nonmutate": // misspelling + self = .nonmutating + case "interface": // possible influences: C# / Java / PHP / TypeScript + self = .protocol + case "typeclass": // possible influences: Scala + self = .protocol + case "prototype": // possible influences: JavaScript / TypeScript + self = .Type + default: + return nil + } + case 10: + switch text { + case "__consumed": // misspelling + self = .__consuming + case "_borrowing": // misspelling + self = ._borrow + case "_noSending": // misspelling + self = ._nonSendable + case "continuing": // misspelling + self = .continue + case "convenient": // misspelling + self = .convenience + case "instanceof": // possible influences: Java / JavaScript / PHP / TypeScript + self = .is + case "noescaping": // misspelling + self = .noescape + case "objectiveC": // misspelling + self = .objc + default: + return nil + } + case 11: + switch text { + case "_nonSending": // misspelling + self = ._nonSendable + case "constructor": // possible influences: JavaScript / Kotlin / TypeScript + self = .`init` + default: + return nil + } + case 12: + switch text { + case "cdeclaration": // misspelling + self = ._cdecl + case "distributing": // misspelling + self = .distributed + case "nonisolating": // misspelling + self = .nonisolated + default: + return nil + } + case 22: + switch text { + case "_objectiveCRuntimeName": // misspelling + self = ._objcRuntimeName + default: + return nil + } + case 25: + switch text { + case "_objectiveCImplementation": // misspelling + self = ._objcImplementation + default: + return nil + } + default: + return nil + } + } + /// This is really unfortunate. Really, we should have a `switch` in /// `Keyword.defaultText` to return the keyword's kind but the constant lookup /// table is significantly faster. Ideally, we could also get the compiler to diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift index 35a680d090b..c3f6929c10d 100644 --- a/Tests/SwiftParserTest/DeclarationTests.swift +++ b/Tests/SwiftParserTest/DeclarationTests.swift @@ -317,6 +317,42 @@ final class DeclarationTests: ParserTestCase { } """ ) + + assertParse( + """ + protocol P { + 1️⃣associatedType A + 2️⃣associatetype B + 3️⃣associateType C + associatedtype D + } + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected 'associatedtype' keyword; did you mean 'associatedtype'?", + fixIts: ["replace 'associatedType' with 'associatedtype'"] + ), + DiagnosticSpec( + locationMarker: "2️⃣", + message: "expected 'associatedtype' keyword; did you mean 'associatedtype'?", + fixIts: ["replace 'associatetype' with 'associatedtype'"] + ), + DiagnosticSpec( + locationMarker: "3️⃣", + message: "expected 'associatedtype' keyword; did you mean 'associatedtype'?", + fixIts: ["replace 'associateType' with 'associatedtype'"] + ), + ], + fixedSource: """ + protocol P { + associatedtype A + associatedtype B + associatedtype C + associatedtype D + } + """ + ) } func testVariableDeclarations() { @@ -405,6 +441,37 @@ final class DeclarationTests: ParserTestCase { } """ ) + + assertParse( + """ + var foo: Int { + 1️⃣consume set { + test += 1 + } + 2️⃣borrow get {} + } + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected accessor modifier; did you mean 'consuming'?", + fixIts: ["replace 'consume' with 'consuming'"] + ), + DiagnosticSpec( + locationMarker: "2️⃣", + message: "expected accessor modifier; did you mean 'borrowing'?", + fixIts: ["replace 'borrow' with 'borrowing'"] + ), + ], + fixedSource: """ + var foo: Int { + consuming set { + test += 1 + } + borrowing get {} + } + """ + ) } func testAccessLevelModifier() { @@ -1615,6 +1682,42 @@ final class DeclarationTests: ParserTestCase { func testDeinitializers() { assertParse("deinit {}") assertParse("deinit") + assertParse( + """ + class A { + 1️⃣deInit {} + } + """, + diagnostics: [ + DiagnosticSpec( + message: "expected 'deinit' keyword; did you mean 'deinit'?", + fixIts: ["replace 'deInit' with 'deinit'"] + ) + ], + fixedSource: """ + class A { + deinit {} + } + """ + ) + assertParse( + """ + class A { + 1️⃣deInit + } + """, + diagnostics: [ + DiagnosticSpec( + message: "expected 'deinit' keyword; did you mean 'deinit'?", + fixIts: ["replace 'deInit' with 'deinit'"] + ) + ], + fixedSource: """ + class A { + deinit + } + """ + ) } func testAttributedMember() { diff --git a/Tests/SwiftParserTest/StatementTests.swift b/Tests/SwiftParserTest/StatementTests.swift index cd82807bb01..a5cbc6bccb3 100644 --- a/Tests/SwiftParserTest/StatementTests.swift +++ b/Tests/SwiftParserTest/StatementTests.swift @@ -917,6 +917,22 @@ final class StatementTests: ParserTestCase { } else {} """ ) + + assertParse( + """ + 1️⃣gaurd test else {} + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "expected 'guard' keyword; did you mean 'guard'?", + fixIts: ["replace 'gaurd' with 'guard'"] + ) + ], + fixedSource: """ + guard test else {} + """ + ) } func testTypedThrows() {