Skip to content

Allow leading dot syntax for StaticParserError #951

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

Merged
merged 3 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 8 additions & 4 deletions Sources/SwiftParser/Diagnostics/DiagnosticExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ import SwiftDiagnostics
import SwiftSyntax

extension FixIt {
init(message: StaticParserFixIt, changes: Changes) {
self.init(message: message as FixItMessage, changes: changes)
}

public init(message: FixItMessage, changes: [Changes]) {
self.init(message: message, changes: FixIt.Changes(combining: changes))
}

// These overloads shouldn't be needed, but are currently required for the
// Swift 5.5 compiler to handle non-trivial FixIt initializations using
// leading-dot syntax.
// TODO: These can be dropped once we require a minimum of Swift 5.6 to
// compile the library.
init(message: StaticParserFixIt, changes: Changes) {
self.init(message: message as FixItMessage, changes: changes)
}
init(message: StaticParserFixIt, changes: [Changes]) {
self.init(message: message as FixItMessage, changes: FixIt.Changes(combining: changes))
}
Expand Down
39 changes: 9 additions & 30 deletions Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,6 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
self.handledNodes.append(contentsOf: handledNodes)
}

/// Produce a diagnostic.
func addDiagnostic<T: SyntaxProtocol>(
_ node: T,
position: AbsolutePosition? = nil,
_ message: StaticParserError,
highlights: [Syntax] = [],
notes: [Note] = [],
fixIts: [FixIt] = [],
handledNodes: [SyntaxIdentifier] = []
) {
addDiagnostic(
node,
position: position,
message as DiagnosticMessage,
highlights: highlights,
notes: notes,
fixIts: fixIts,
handledNodes: handledNodes
)
}

/// Whether the node should be skipped for diagnostic emission.
/// Every visit method must check this at the beginning.
func shouldSkip<T: SyntaxProtocol>(_ node: T) -> Bool {
Expand All @@ -99,11 +78,11 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
///
/// If `incorrectContainer` contains only tokens that satisfy `unexpectedTokenCondition`, emit a diagnostic with message `message` that marks this token as misplaced.
/// If `correctTokens` contains missing tokens, also emit a Fix-It with message `fixIt` that marks the unexpected token as missing and instead inserts `correctTokens`.
public func exchangeTokens(
public func exchangeTokens<Message: DiagnosticMessage>(
unexpected: UnexpectedNodesSyntax?,
unexpectedTokenCondition: (TokenSyntax) -> Bool,
correctTokens: [TokenSyntax?],
message: (_ misplacedTokens: [TokenSyntax]) -> DiagnosticMessage,
message: (_ misplacedTokens: [TokenSyntax]) -> Message,
moveFixIt: (_ misplacedTokens: [TokenSyntax]) -> FixItMessage,
removeRedundantFixIt: (_ misplacedTokens: [TokenSyntax]) -> FixItMessage? = { _ in nil }
) {
Expand Down Expand Up @@ -147,10 +126,10 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {

/// If `unexpected` only contains a single token that satisfies `predicate`,
/// emits a diagnostic with `message` that removes this token.
public func removeToken(
public func removeToken<Message: DiagnosticMessage>(
_ unexpected: UnexpectedNodesSyntax?,
where predicate: (TokenSyntax) -> Bool,
message: (TokenSyntax) -> DiagnosticMessage
message: (TokenSyntax) -> Message
) {
guard let unexpected = unexpected,
let misplacedToken = unexpected.onlyToken(where: predicate)
Expand Down Expand Up @@ -509,7 +488,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
unexpected: node.output?.unexpectedBetweenArrowAndReturnType,
unexpectedTokenCondition: { $0.tokenKind == .throwsKeyword },
correctTokens: [node.throwsOrRethrowsKeyword],
message: { _ in StaticParserError.throwsInReturnPosition },
message: { _ in .throwsInReturnPosition },
moveFixIt: { MoveTokensInFrontOfFixIt(movedTokens: $0, inFrontOf: .arrow) }
)
return .visitChildren
Expand Down Expand Up @@ -577,7 +556,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
unexpected: node.unexpectedBeforeReturnKeyword,
unexpectedTokenCondition: { $0.tokenKind == .tryKeyword },
correctTokens: [node.expression?.as(TryExprSyntax.self)?.tryKeyword],
message: { _ in StaticParserError.tryMustBePlacedOnReturnedExpr },
message: { _ in .tryMustBePlacedOnReturnedExpr },
moveFixIt: { MoveTokensAfterFixIt(movedTokens: $0, after: .returnKeyword) }
)
}
Expand Down Expand Up @@ -616,7 +595,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
unexpected: node.unexpectedBeforeThrowKeyword,
unexpectedTokenCondition: { $0.tokenKind == .tryKeyword },
correctTokens: [node.expression.as(TryExprSyntax.self)?.tryKeyword],
message: { _ in StaticParserError.tryMustBePlacedOnThrownExpr },
message: { _ in .tryMustBePlacedOnThrownExpr },
moveFixIt: { MoveTokensAfterFixIt(movedTokens: $0, after: .throwKeyword) }
)
return .visitChildren
Expand Down Expand Up @@ -701,7 +680,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
removeToken(
node.unexpectedBetweenIdentifierAndInheritanceClause,
where: { $0.tokenKind == .ellipsis },
message: { _ in StaticParserError.associatedTypeCannotUsePack }
message: { _ in .associatedTypeCannotUsePack }
)
return .visitChildren
}
Expand All @@ -717,7 +696,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
unexpected: node.unexpectedBetweenModifiersAndLetOrVarKeyword,
unexpectedTokenCondition: { $0.tokenKind == .tryKeyword },
correctTokens: missingTries,
message: { _ in StaticParserError.tryOnInitialValueExpression },
message: { _ in .tryOnInitialValueExpression },
moveFixIt: { MoveTokensAfterFixIt(movedTokens: $0, after: .equal) },
removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) }
)
Expand Down
163 changes: 124 additions & 39 deletions Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,42 +64,102 @@ public extension ParserFixIt {

// MARK: - Errors (please sort alphabetically)

/// Please order the cases in this enum alphabetically by case name.
public enum StaticParserError: String, DiagnosticMessage {
case allStatmentsInSwitchMustBeCoveredByCase = "all statements inside a switch must be covered by a 'case' or 'default' label"
case caseOutsideOfSwitchOrEnum = "'case' can only appear inside a 'switch' statement or 'enum' declaration"
case classConstraintCanOnlyBeUsedInProtocol = "'class' constraint can only appear on protocol declarations"
case consecutiveDeclarationsOnSameLine = "consecutive declarations on a line must be separated by ';'"
case consecutiveStatementsOnSameLine = "consecutive statements on a line must be separated by ';'"
case cStyleForLoop = "C-style for statement has been removed in Swift 3"
case defaultCannotBeUsedWithWhere = "'default' cannot be used with a 'where' guard expression"
case defaultOutsideOfSwitch = "'default' label can only appear inside a 'switch' statement"
case deinitCannotHaveName = "deinitializers cannot have a name"
case deinitCannotHaveParameters = "deinitializers cannot have parameters"
case editorPlaceholderInSourceFile = "editor placeholder in source file"
case expectedExpressionAfterTry = "expected expression after 'try'"
case invalidFlagAfterPrecedenceGroupAssignment = "expected 'true' or 'false' after 'assignment'"
case missingColonAndExprInTernaryExpr = "expected ':' and expression after '? ...' in ternary expression"
case missingColonInTernaryExpr = "expected ':' after '? ...' in ternary expression"
case operatorShouldBeDeclaredWithoutBody = "operator should not be declared with body"
case standaloneSemicolonStatement = "standalone ';' statements are not allowed"
case subscriptsCannotHaveNames = "subscripts cannot have a name"
case throwsInReturnPosition = "'throws' may only occur before '->'"
case tryMustBePlacedOnReturnedExpr = "'try' must be placed on the returned expression"
case tryMustBePlacedOnThrownExpr = "'try' must be placed on the thrown expression"
case tryOnInitialValueExpression = "'try' must be placed on the initial value expression"
case unexpectedSemicolon = "unexpected ';' separator"
case associatedTypeCannotUsePack = "associated types cannot be variadic"

public var message: String { self.rawValue }
/// A parser error with a static message.
public struct StaticParserError: DiagnosticMessage {
public let message: String
private let messageID: String

/// This should only be called within a static var on DiagnosticMessage, such
/// as the examples below. This allows us to pick up the messageID from the
/// var name.
fileprivate init(_ message: String, messageID: String = #function) {
self.message = message
self.messageID = messageID
}

public var diagnosticID: MessageID {
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(self)")
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(messageID)")
}

public var severity: DiagnosticSeverity { .error }
}

extension DiagnosticMessage where Self == StaticParserError {
/// Please order the diagnostics alphabetically by property name.
public static var allStatmentsInSwitchMustBeCoveredByCase: Self {
.init("all statements inside a switch must be covered by a 'case' or 'default' label")
}
public static var associatedTypeCannotUsePack: Self {
.init("associated types cannot be variadic")
}
public static var caseOutsideOfSwitchOrEnum: Self {
.init("'case' can only appear inside a 'switch' statement or 'enum' declaration")
}
public static var classConstraintCanOnlyBeUsedInProtocol: Self {
.init("'class' constraint can only appear on protocol declarations")
}
public static var consecutiveDeclarationsOnSameLine: Self {
.init("consecutive declarations on a line must be separated by ';'")
}
public static var consecutiveStatementsOnSameLine: Self {
.init("consecutive statements on a line must be separated by ';'")
}
public static var cStyleForLoop: Self {
.init("C-style for statement has been removed in Swift 3")
}
public static var defaultCannotBeUsedWithWhere: Self {
.init("'default' cannot be used with a 'where' guard expression")
}
public static var defaultOutsideOfSwitch: Self {
.init("'default' label can only appear inside a 'switch' statement")
}
public static var deinitCannotHaveName: Self {
.init("deinitializers cannot have a name")
}
public static var deinitCannotHaveParameters: Self {
.init("deinitializers cannot have parameters")
}
public static var editorPlaceholderInSourceFile: Self {
.init("editor placeholder in source file")
}
public static var expectedExpressionAfterTry: Self {
.init("expected expression after 'try'")
}
public static var invalidFlagAfterPrecedenceGroupAssignment: Self {
.init("expected 'true' or 'false' after 'assignment'")
}
public static var missingColonAndExprInTernaryExpr: Self {
.init("expected ':' and expression after '? ...' in ternary expression")
}
public static var missingColonInTernaryExpr: Self {
.init("expected ':' after '? ...' in ternary expression")
}
public static var operatorShouldBeDeclaredWithoutBody: Self {
.init("operator should not be declared with body")
}
public static var standaloneSemicolonStatement: Self {
.init("standalone ';' statements are not allowed")
}
public static var subscriptsCannotHaveNames: Self {
.init("subscripts cannot have a name")
}
public static var throwsInReturnPosition: Self {
.init("'throws' may only occur before '->'")
}
public static var tryMustBePlacedOnReturnedExpr: Self {
.init("'try' must be placed on the returned expression")
}
public static var tryMustBePlacedOnThrownExpr: Self {
.init("'try' must be placed on the thrown expression")
}
public static var tryOnInitialValueExpression: Self {
.init("'try' must be placed on the initial value expression")
}
public static var unexpectedSemicolon: Self {
.init("unexpected ';' separator")
}
}

// MARK: - Diagnostics (please sort alphabetically)

public struct EffectsSpecifierAfterArrow: ParserError {
Expand Down Expand Up @@ -221,18 +281,43 @@ public struct UnknownDirectiveError: ParserError {

// MARK: - Fix-Its (please sort alphabetically)

public enum StaticParserFixIt: String, FixItMessage {
case insertSemicolon = "insert ';'"
case insertAttributeArguments = "insert attribute argument"
case joinIdentifiers = "join the identifiers together"
case joinIdentifiersWithCamelCase = "join the identifiers together with camel-case"
case removeOperatorBody = "remove operator body"
case wrapKeywordInBackticks = "if this name is unavoidable, use backticks to escape it"

public var message: String { self.rawValue }
/// A parser fix-it with a static message.
public struct StaticParserFixIt: FixItMessage {
public let message: String
private let messageID: String

/// This should only be called within a static var on FixItMessage, such
/// as the examples below. This allows us to pick up the messageID from the
/// var name.
fileprivate init(_ message: String, messageID: String = #function) {
self.message = message
self.messageID = messageID
}

public var fixItID: MessageID {
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(self)")
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(messageID)")
}
}

extension FixItMessage where Self == StaticParserFixIt {
/// Please order alphabetically by property name.
public static var insertSemicolon: Self {
.init("insert ';'")
}
public static var insertAttributeArguments: Self {
.init("insert attribute argument")
}
public static var joinIdentifiers: Self {
.init("join the identifiers together")
}
public static var joinIdentifiersWithCamelCase: Self {
.init("join the identifiers together with camel-case")
}
public static var removeOperatorBody: Self {
.init("remove operator body")
}
public static var wrapKeywordInBackticks: Self {
.init("if this name is unavoidable, use backticks to escape it")
}
}

Expand Down
5 changes: 5 additions & 0 deletions Tests/SwiftParserTest/DiagnosticInfrastructureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ public class DiagnosticInfrastructureTests: XCTestCase {

XCTAssertEqual(StaticParserError.throwsInReturnPosition.diagnosticID, MessageID(domain: "SwiftParser", id: "StaticParserError.throwsInReturnPosition"))
XCTAssertEqual(StaticParserError.throwsInReturnPosition.severity, .error)

XCTAssertEqual(
StaticParserFixIt.insertSemicolon.fixItID,
MessageID(domain: "SwiftParser", id: "StaticParserFixIt.insertSemicolon")
)
}
}
8 changes: 8 additions & 0 deletions Tests/SwiftParserTest/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -354,5 +354,13 @@ final class TypeParameterPackTests: XCTestCase {
}
""")
}
func testParameterPacks28() throws {
// We allow whitespace between the generic parameter and the '...', this is
// consistent with regular variadic parameters.
AssertParse(
"""
func f1<T ...>(_ x: T ...) -> (T ...) {}
""")
}
}