Skip to content

Commit 8c76dc3

Browse files
authored
Merge pull request #623 from ahoppen/pr/diag-changes
Several improvements to diagnostics in the new parser
2 parents 17b81be + 0f7b847 commit 8c76dc3

File tree

11 files changed

+295
-73
lines changed

11 files changed

+295
-73
lines changed

Package.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ let package = Package(
5858
"README.md"
5959
]
6060
),
61+
.target(
62+
name: "SwiftDiagnostics",
63+
dependencies: ["SwiftSyntax"]
64+
),
6165
.target(
6266
name: "SwiftSyntax",
6367
dependencies: ["_CSwiftSyntax"],
@@ -105,7 +109,7 @@ let package = Package(
105109
),
106110
.target(
107111
name: "SwiftParser",
108-
dependencies: ["SwiftSyntax"],
112+
dependencies: ["SwiftDiagnostics", "SwiftSyntax"],
109113
exclude: [
110114
"README.md"
111115
]
@@ -116,7 +120,7 @@ let package = Package(
116120
),
117121
.executableTarget(
118122
name: "swift-parser-test",
119-
dependencies: ["SwiftSyntax", "SwiftParser", .product(name: "ArgumentParser", package: "swift-argument-parser")]
123+
dependencies: ["SwiftDiagnostics", "SwiftSyntax", "SwiftParser", .product(name: "ArgumentParser", package: "swift-argument-parser")]
120124
),
121125
.executableTarget(
122126
name: "generate-swift-syntax-builder",
@@ -162,7 +166,7 @@ let package = Package(
162166
),
163167
.testTarget(
164168
name: "SwiftParserTest",
165-
dependencies: ["SwiftParser", "_SwiftSyntaxTestSupport"]
169+
dependencies: ["SwiftDiagnostics", "SwiftParser", "_SwiftSyntaxTestSupport"]
166170
),
167171
]
168172
)

Sources/SwiftParser/Diagnostics/Diagnostic.swift renamed to Sources/SwiftDiagnostics/Diagnostic.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,25 @@
1212

1313
import SwiftSyntax
1414

15-
public struct Diagnostic {
15+
public struct Diagnostic: CustomDebugStringConvertible {
1616
/// The message that should be displayed to the user
1717
public let diagMessage: DiagnosticMessage
1818

1919
/// The node at whose start location the message should be displayed.
2020
public let node: Syntax
2121

22-
init(node: Syntax, message: DiagnosticMessage) {
22+
/// Nodes that should be highlighted in the source code.
23+
public let highlights: [Syntax]
24+
25+
/// Fix-Its that can be applied to resolve this diagnostic.
26+
/// Each Fix-It offers a different way to resolve the diagnostic. Usually, there's only one.
27+
public let fixIts: [FixIt]
28+
29+
public init(node: Syntax, message: DiagnosticMessage, highlights: [Syntax] = [], fixIts: [FixIt] = []) {
2330
self.diagMessage = message
2431
self.node = node
32+
self.highlights = highlights
33+
self.fixIts = fixIts
2534
}
2635

2736
/// The message that should be displayed to the user.
@@ -30,14 +39,24 @@ public struct Diagnostic {
3039
}
3140

3241
/// An ID that identifies the diagnostic's message.
33-
/// See ``DiagnosticMessageID``.
34-
public var diagnosticID: DiagnosticMessageID {
42+
/// See ``MessageID``.
43+
public var diagnosticID: MessageID {
3544
return diagMessage.diagnosticID
3645
}
3746

3847
/// The location at which the diagnostic should be displayed.
3948
public func location(converter: SourceLocationConverter) -> SourceLocation {
4049
return node.startLocation(converter: converter)
4150
}
51+
52+
public var debugDescription: String {
53+
if let root = node.root.as(SourceFileSyntax.self) {
54+
let locationConverter = SourceLocationConverter(file: "", tree: root)
55+
let location = location(converter: locationConverter)
56+
return "\(location): \(message)"
57+
} else {
58+
return "<unknown>: \(message)"
59+
}
60+
}
4261
}
4362

Sources/SwiftDiagnostics/FixIt.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//===--- FixIt.swift ------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
/// Types conforming to this protocol represent Fix-It messages that can be
16+
/// shown to the client.
17+
/// The messages should describe the change that the Fix-It will perform
18+
public protocol FixItMessage {
19+
/// The diagnostic message that should be displayed in the client.
20+
var message: String { get }
21+
22+
/// See ``MessageID``.
23+
var fixItID: MessageID { get }
24+
}
25+
26+
27+
/// A Fix-It that can be applied to resolve a diagnostic.
28+
public struct FixIt {
29+
public enum Change {
30+
/// Replace `oldNode` by `newNode`.
31+
case replace(oldNode: Syntax, newNode: Syntax)
32+
}
33+
34+
/// A description of what this Fix-It performs.
35+
public let message: FixItMessage
36+
37+
/// The changes that need to be performed when the Fix-It is applied.
38+
public let changes: [Change]
39+
40+
public init(message: FixItMessage, changes: [Change]) {
41+
assert(!changes.isEmpty, "A Fix-It must have at least one diagnostic associated with it")
42+
self.message = message
43+
self.changes = changes
44+
}
45+
}
46+
Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//===--- DiagnosticMessage.swift ------------------------------------------===//
1+
//===--- Message.swift ----------------------------------------------------===//
22
//
33
// This source file is part of the Swift.org open source project
44
//
@@ -17,35 +17,30 @@
1717
/// Two diagnostics with the same ID don’t need to necessarily have the exact
1818
/// same wording. Eg. it’s possible that the message contains more context when
1919
/// available.
20-
public struct DiagnosticMessageID: Hashable {
21-
private let value: String
20+
public struct MessageID: Hashable {
21+
private let domain: String
22+
private let id: String
2223

23-
public init(_ value: String) {
24-
self.value = value
24+
public init(domain: String, id: String) {
25+
self.domain = domain
26+
self.id = id
2527
}
2628
}
2729

30+
public enum DiagnosticSeverity {
31+
case error
32+
case warning
33+
case note
34+
}
35+
2836
/// Types conforming to this protocol represent diagnostic messages that can be
2937
/// shown to the client.
3038
public protocol DiagnosticMessage {
3139
/// The diagnostic message that should be displayed in the client.
3240
var message: String { get }
3341

34-
/// See ``DiagnosticMessageID``.
35-
var diagnosticID: DiagnosticMessageID { get }
36-
}
37-
38-
/// A diagnostic how's ID is determined by the diagnostic's type.
39-
public protocol TypedDiagnosticMessage: DiagnosticMessage {
40-
var diagnosticID: DiagnosticMessageID { get }
41-
}
42-
43-
public extension TypedDiagnosticMessage {
44-
static var diagnosticID: DiagnosticMessageID {
45-
return DiagnosticMessageID("\(self)")
46-
}
42+
/// See ``MessageID``.
43+
var diagnosticID: MessageID { get }
4744

48-
var diagnosticID: DiagnosticMessageID {
49-
return Self.diagnosticID
50-
}
45+
var severity: DiagnosticSeverity { get }
5146
}

Sources/SwiftParser/Declarations.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,21 +1155,25 @@ extension Parser {
11551155
arena: self.arena)
11561156
}
11571157

1158+
/// If a `throws` keyword appears right in front of the `arrow`, it is returned as `misplacedThrowsKeyword` so it can be synthesized in front of the arrow.
11581159
@_spi(RawSyntax)
1159-
public mutating func parseFunctionReturnClause() -> RawReturnClauseSyntax {
1160+
public mutating func parseFunctionReturnClause() -> (returnClause: RawReturnClauseSyntax, misplacedThrowsKeyword: RawTokenSyntax?) {
11601161
let arrow = self.eat(.arrow)
1162+
var misplacedThrowsKeyword: RawTokenSyntax? = nil
11611163
let unexpectedBeforeReturnType: RawUnexpectedNodesSyntax?
1162-
if let unexpectedToken = self.consume(ifAny: .tryKeyword, .throwKeyword, .throwsKeyword) {
1163-
unexpectedBeforeReturnType = RawUnexpectedNodesSyntax(elements: [RawSyntax(unexpectedToken)], arena: self.arena)
1164+
if let throwsKeyword = self.consume(ifAny: .rethrowsKeyword, .throwsKeyword) {
1165+
misplacedThrowsKeyword = throwsKeyword
1166+
unexpectedBeforeReturnType = RawUnexpectedNodesSyntax(elements: [RawSyntax(throwsKeyword)], arena: self.arena)
11641167
} else {
11651168
unexpectedBeforeReturnType = nil
11661169
}
11671170
let result = self.parseType()
1168-
return RawReturnClauseSyntax(
1171+
let returnClause = RawReturnClauseSyntax(
11691172
arrow: arrow,
11701173
unexpectedBeforeReturnType,
11711174
returnType: result,
11721175
arena: self.arena)
1176+
return (returnClause, misplacedThrowsKeyword)
11731177
}
11741178
}
11751179

@@ -1227,7 +1231,7 @@ extension Parser {
12271231
async = nil
12281232
}
12291233

1230-
let throwsKeyword: RawTokenSyntax?
1234+
var throwsKeyword: RawTokenSyntax?
12311235
if self.at(.throwsKeyword) || self.at(.rethrowsKeyword) {
12321236
throwsKeyword = self.consumeAnyToken()
12331237
} else {
@@ -1236,7 +1240,11 @@ extension Parser {
12361240

12371241
let output: RawReturnClauseSyntax?
12381242
if self.at(.arrow) {
1239-
output = self.parseFunctionReturnClause()
1243+
let result = self.parseFunctionReturnClause()
1244+
output = result.returnClause
1245+
if let misplacedThrowsKeyword = result.misplacedThrowsKeyword, throwsKeyword == nil {
1246+
throwsKeyword = RawTokenSyntax(missing: misplacedThrowsKeyword.tokenKind, arena: self.arena)
1247+
}
12401248
} else {
12411249
output = nil
12421250
}
@@ -1275,7 +1283,7 @@ extension Parser {
12751283

12761284
let result: RawReturnClauseSyntax
12771285
if self.at(.arrow) {
1278-
result = self.parseFunctionReturnClause()
1286+
result = self.parseFunctionReturnClause().returnClause
12791287
} else {
12801288
result = RawReturnClauseSyntax(
12811289
arrow: RawTokenSyntax(missing: .arrow, arena: self.arena),

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import SwiftDiagnostics
1314
import SwiftSyntax
1415

1516
extension UnexpectedNodesSyntax {
@@ -22,6 +23,25 @@ extension UnexpectedNodesSyntax {
2223
}
2324
}
2425

26+
fileprivate extension FixIt.Change {
27+
/// Replaced a present node with a missing node
28+
static func makeMissing(node: TokenSyntax) -> FixIt.Change {
29+
assert(node.presence == .present)
30+
return .replace(
31+
oldNode: Syntax(node),
32+
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
33+
)
34+
}
35+
36+
static func makePresent(node: TokenSyntax, leadingTrivia: Trivia = [], trailingTrivia: Trivia = []) -> FixIt.Change {
37+
assert(node.presence == .missing)
38+
return .replace(
39+
oldNode: Syntax(node),
40+
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia, presence: .present))
41+
)
42+
}
43+
}
44+
2545
public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
2646
private var diagnostics: [Diagnostic] = []
2747

@@ -42,8 +62,13 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
4262
// MARK: - Private helper functions
4363

4464
/// Produce a diagnostic.
45-
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: DiagnosticMessage) {
46-
diagnostics.append(Diagnostic(node: Syntax(node), message: message))
65+
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: DiagnosticMessage, highlights: [Syntax] = [], fixIts: [FixIt] = []) {
66+
diagnostics.append(Diagnostic(node: Syntax(node), message: message, highlights: highlights, fixIts: fixIts))
67+
}
68+
69+
/// Produce a diagnostic.
70+
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: StaticParserError, highlights: [Syntax] = [], fixIts: [FixIt] = []) {
71+
addDiagnostic(node, message as DiagnosticMessage, highlights: highlights, fixIts: fixIts)
4772
}
4873

4974
/// 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 {
7095
if shouldSkip(node) {
7196
return .skipChildren
7297
}
73-
addDiagnostic(node, UnexpectedNodesDiagnostic(unexpectedNodes: node))
98+
addDiagnostic(node, UnexpectedNodesError(unexpectedNodes: node))
7499
return .skipChildren
75100
}
76101

@@ -79,7 +104,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
79104
return .skipChildren
80105
}
81106
if node.presence == .missing {
82-
addDiagnostic(node, MissingTokenDiagnostic(missingToken: node))
107+
addDiagnostic(node, MissingTokenError(missingToken: node))
83108
}
84109
return .skipChildren
85110
}
@@ -90,13 +115,25 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
90115
if shouldSkip(node) {
91116
return .skipChildren
92117
}
118+
// Detect C-style for loops based on two semicolons which could not be parsed between the 'for' keyword and the '{'
93119
// This is mostly a proof-of-concept implementation to produce more complex diagnostics.
94-
if let unexpectedCondition = node.body.unexpectedBeforeLeftBrace {
95-
// Detect C-style for loops based on two semicolons which could not be parsed between the 'for' keyword and the '{'
96-
if unexpectedCondition.tokens(withKind: .semicolon).count == 2 {
97-
addDiagnostic(node, CStyleForLoopDiagnostic())
98-
markNodesAsHandled(node.inKeyword.id, unexpectedCondition.id)
99-
}
120+
if let unexpectedCondition = node.body.unexpectedBeforeLeftBrace,
121+
unexpectedCondition.tokens(withKind: .semicolon).count == 2 {
122+
// 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.
123+
addDiagnostic(node, .cStyleForLoop, highlights: ([
124+
Syntax(node.pattern),
125+
Syntax(node.unexpectedBetweenPatternAndTypeAnnotation),
126+
Syntax(node.typeAnnotation),
127+
Syntax(node.unexpectedBetweenTypeAnnotationAndInKeyword),
128+
Syntax(node.inKeyword),
129+
Syntax(node.unexpectedBetweenInKeywordAndSequenceExpr),
130+
Syntax(node.sequenceExpr),
131+
Syntax(node.unexpectedBetweenSequenceExprAndWhereClause),
132+
Syntax(node.whereClause),
133+
Syntax(node.unexpectedBetweenWhereClauseAndBody),
134+
Syntax(unexpectedCondition)
135+
] as [Syntax?]).compactMap({ $0 }))
136+
markNodesAsHandled(node.inKeyword.id, unexpectedCondition.id)
100137
}
101138
return .visitChildren
102139
}
@@ -105,12 +142,19 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
105142
if shouldSkip(node) {
106143
return .skipChildren
107144
}
108-
if let output = node.output, let unexpectedBeforeReturnType = output.unexpectedBetweenArrowAndReturnType {
109-
if let throwsInReturnPosition = unexpectedBeforeReturnType.tokens(withKind: .throwsKeyword).first {
110-
addDiagnostic(throwsInReturnPosition, ThrowsInReturnPositionDiagnostic())
111-
markNodesAsHandled(unexpectedBeforeReturnType.id, throwsInReturnPosition.id)
112-
return .visitChildren
113-
}
145+
if let output = node.output,
146+
let missingThrowsKeyword = node.throwsOrRethrowsKeyword,
147+
missingThrowsKeyword.presence == .missing,
148+
let unexpectedBeforeReturnType = output.unexpectedBetweenArrowAndReturnType,
149+
let throwsInReturnPosition = unexpectedBeforeReturnType.tokens(withKind: .throwsKeyword).first {
150+
addDiagnostic(throwsInReturnPosition, .throwsInReturnPosition, fixIts: [
151+
FixIt(message: StaticParserFixIt.moveThrowBeforeArrow, changes: [
152+
.makeMissing(node: throwsInReturnPosition),
153+
.makePresent(node: missingThrowsKeyword, trailingTrivia: .space),
154+
])
155+
])
156+
markNodesAsHandled(unexpectedBeforeReturnType.id, missingThrowsKeyword.id, throwsInReturnPosition.id)
157+
return .visitChildren
114158
}
115159
return .visitChildren
116160
}

0 commit comments

Comments
 (0)