-
Notifications
You must be signed in to change notification settings - Fork 441
Several improvements to diagnostics in the new parser #623
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
Changes from all commits
c9ce8de
828a3a0
12d50a5
77ede91
e3dd491
6311794
e2cc07e
5f90433
0f7b847
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
//===--- FixIt.swift ------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import SwiftSyntax | ||
|
||
/// Types conforming to this protocol represent Fix-It messages that can be | ||
/// shown to the client. | ||
/// The messages should describe the change that the Fix-It will perform | ||
public protocol FixItMessage { | ||
/// The diagnostic message that should be displayed in the client. | ||
var message: String { get } | ||
|
||
/// See ``MessageID``. | ||
var fixItID: MessageID { get } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do these (or notes in my scheme) need IDs? Seems like clients would only ever care about the ID of the diagnostic itself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might be useful if clients want to hide certain fix-its or otherwise transform them. Adding IDs to things never hurts IMO. |
||
} | ||
|
||
|
||
/// A Fix-It that can be applied to resolve a diagnostic. | ||
public struct FixIt { | ||
public enum Change { | ||
/// Replace `oldNode` by `newNode`. | ||
case replace(oldNode: Syntax, newNode: Syntax) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect we'll end up needing more than this but no harm in starting with this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Absolutely, that’s why I made it an enum. I’ll add more cases as uses come up. |
||
} | ||
|
||
/// A description of what this Fix-It performs. | ||
public let message: FixItMessage | ||
|
||
/// The changes that need to be performed when the Fix-It is applied. | ||
public let changes: [Change] | ||
|
||
public init(message: FixItMessage, changes: [Change]) { | ||
assert(!changes.isEmpty, "A Fix-It must have at least one diagnostic associated with it") | ||
self.message = message | ||
self.changes = changes | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import SwiftDiagnostics | ||
import SwiftSyntax | ||
|
||
extension UnexpectedNodesSyntax { | ||
|
@@ -22,6 +23,25 @@ extension UnexpectedNodesSyntax { | |
} | ||
} | ||
|
||
fileprivate extension FixIt.Change { | ||
/// Replaced a present node with a missing node | ||
static func makeMissing(node: TokenSyntax) -> FixIt.Change { | ||
assert(node.presence == .present) | ||
return .replace( | ||
oldNode: Syntax(node), | ||
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing)) | ||
) | ||
} | ||
|
||
static func makePresent(node: TokenSyntax, leadingTrivia: Trivia = [], trailingTrivia: Trivia = []) -> FixIt.Change { | ||
assert(node.presence == .missing) | ||
return .replace( | ||
oldNode: Syntax(node), | ||
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia, presence: .present)) | ||
) | ||
} | ||
} | ||
|
||
public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { | ||
private var diagnostics: [Diagnostic] = [] | ||
|
||
|
@@ -42,8 +62,13 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { | |
// MARK: - Private helper functions | ||
|
||
/// Produce a diagnostic. | ||
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: DiagnosticMessage) { | ||
diagnostics.append(Diagnostic(node: Syntax(node), message: message)) | ||
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: DiagnosticMessage, highlights: [Syntax] = [], fixIts: [FixIt] = []) { | ||
diagnostics.append(Diagnostic(node: Syntax(node), message: message, highlights: highlights, fixIts: fixIts)) | ||
} | ||
|
||
/// Produce a diagnostic. | ||
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: StaticParserError, highlights: [Syntax] = [], fixIts: [FixIt] = []) { | ||
addDiagnostic(node, message as DiagnosticMessage, highlights: highlights, fixIts: fixIts) | ||
} | ||
|
||
/// If a diagnostic is generated that covers multiple syntax nodes, mark them as handles so they don't produce the generic diagnostics anymore. | ||
|
@@ -70,7 +95,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { | |
if shouldSkip(node) { | ||
return .skipChildren | ||
} | ||
addDiagnostic(node, UnexpectedNodesDiagnostic(unexpectedNodes: node)) | ||
addDiagnostic(node, UnexpectedNodesError(unexpectedNodes: node)) | ||
return .skipChildren | ||
} | ||
|
||
|
@@ -79,7 +104,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { | |
return .skipChildren | ||
} | ||
if node.presence == .missing { | ||
addDiagnostic(node, MissingTokenDiagnostic(missingToken: node)) | ||
addDiagnostic(node, MissingTokenError(missingToken: node)) | ||
} | ||
return .skipChildren | ||
} | ||
|
@@ -90,13 +115,25 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { | |
if shouldSkip(node) { | ||
return .skipChildren | ||
} | ||
// Detect C-style for loops based on two semicolons which could not be parsed between the 'for' keyword and the '{' | ||
// This is mostly a proof-of-concept implementation to produce more complex diagnostics. | ||
if let unexpectedCondition = node.body.unexpectedBeforeLeftBrace { | ||
// Detect C-style for loops based on two semicolons which could not be parsed between the 'for' keyword and the '{' | ||
if unexpectedCondition.tokens(withKind: .semicolon).count == 2 { | ||
addDiagnostic(node, CStyleForLoopDiagnostic()) | ||
markNodesAsHandled(node.inKeyword.id, unexpectedCondition.id) | ||
} | ||
if let unexpectedCondition = node.body.unexpectedBeforeLeftBrace, | ||
unexpectedCondition.tokens(withKind: .semicolon).count == 2 { | ||
// FIXME: This is aweful. We should have a way to either get all children between two cursors in a syntax node or highlight a range from one node to another. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ouch. Can we define a range as being all of the source between two There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I totally agree that this is awful. The reason why I didn’t want to use a range was that that might now become a runtime error if the end node is not after the start node (or even in a different syntax tree). As I mentioned in the PR description, I would like to implement a few more diagnostics first to find common patterns that we use in diagnostic generation before designing the entire diagnostic system. E.g. if it turns out that this is the only diagnostic that needs to highlight >3 syntax nodes, I would maybe be fine with it. Or it would certainly influence our design. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically, I would like to get this merged so I can start implementing new diagnostics on top of it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yes, let's merge and we can iterate in the tree. |
||
addDiagnostic(node, .cStyleForLoop, highlights: ([ | ||
Syntax(node.pattern), | ||
Syntax(node.unexpectedBetweenPatternAndTypeAnnotation), | ||
Syntax(node.typeAnnotation), | ||
Syntax(node.unexpectedBetweenTypeAnnotationAndInKeyword), | ||
Syntax(node.inKeyword), | ||
Syntax(node.unexpectedBetweenInKeywordAndSequenceExpr), | ||
Syntax(node.sequenceExpr), | ||
Syntax(node.unexpectedBetweenSequenceExprAndWhereClause), | ||
Syntax(node.whereClause), | ||
Syntax(node.unexpectedBetweenWhereClauseAndBody), | ||
Syntax(unexpectedCondition) | ||
] as [Syntax?]).compactMap({ $0 })) | ||
markNodesAsHandled(node.inKeyword.id, unexpectedCondition.id) | ||
} | ||
return .visitChildren | ||
} | ||
|
@@ -105,12 +142,19 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { | |
if shouldSkip(node) { | ||
return .skipChildren | ||
} | ||
if let output = node.output, let unexpectedBeforeReturnType = output.unexpectedBetweenArrowAndReturnType { | ||
if let throwsInReturnPosition = unexpectedBeforeReturnType.tokens(withKind: .throwsKeyword).first { | ||
addDiagnostic(throwsInReturnPosition, ThrowsInReturnPositionDiagnostic()) | ||
markNodesAsHandled(unexpectedBeforeReturnType.id, throwsInReturnPosition.id) | ||
return .visitChildren | ||
} | ||
if let output = node.output, | ||
let missingThrowsKeyword = node.throwsOrRethrowsKeyword, | ||
missingThrowsKeyword.presence == .missing, | ||
let unexpectedBeforeReturnType = output.unexpectedBetweenArrowAndReturnType, | ||
let throwsInReturnPosition = unexpectedBeforeReturnType.tokens(withKind: .throwsKeyword).first { | ||
addDiagnostic(throwsInReturnPosition, .throwsInReturnPosition, fixIts: [ | ||
FixIt(message: StaticParserFixIt.moveThrowBeforeArrow, changes: [ | ||
.makeMissing(node: throwsInReturnPosition), | ||
.makePresent(node: missingThrowsKeyword, trailingTrivia: .space), | ||
]) | ||
]) | ||
markNodesAsHandled(unexpectedBeforeReturnType.id, missingThrowsKeyword.id, throwsInReturnPosition.id) | ||
return .visitChildren | ||
} | ||
return .visitChildren | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO we want a
Note
(but not with that name because it's the same as severity). Notes can have messages and a possibly-empty list of changes. Diagnostics can have any numbers of notes. Ie. it's just additional information on a diagnostic.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Let’s add them when the need comes up.