Skip to content

Commit 0f7b847

Browse files
committed
Allow diagnostics to have a highlight range
1 parent 5f90433 commit 0f7b847

File tree

4 files changed

+37
-9
lines changed

4 files changed

+37
-9
lines changed

Sources/SwiftDiagnostics/Diagnostic.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ public struct Diagnostic: CustomDebugStringConvertible {
1919
/// The node at whose start location the message should be displayed.
2020
public let node: Syntax
2121

22+
/// Nodes that should be highlighted in the source code.
23+
public let highlights: [Syntax]
24+
2225
/// Fix-Its that can be applied to resolve this diagnostic.
2326
/// Each Fix-It offers a different way to resolve the diagnostic. Usually, there's only one.
2427
public let fixIts: [FixIt]
2528

26-
public init(node: Syntax, message: DiagnosticMessage, fixIts: [FixIt] = []) {
29+
public init(node: Syntax, message: DiagnosticMessage, highlights: [Syntax] = [], fixIts: [FixIt] = []) {
2730
self.diagMessage = message
2831
self.node = node
32+
self.highlights = highlights
2933
self.fixIts = fixIts
3034
}
3135

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
6262
// MARK: - Private helper functions
6363

6464
/// Produce a diagnostic.
65-
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: DiagnosticMessage, fixIts: [FixIt] = []) {
66-
diagnostics.append(Diagnostic(node: Syntax(node), message: message, fixIts: fixIts))
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))
6767
}
6868

6969
/// Produce a diagnostic.
70-
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: StaticParserError, fixIts: [FixIt] = []) {
71-
addDiagnostic(node, message as DiagnosticMessage, fixIts: fixIts)
70+
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: StaticParserError, highlights: [Syntax] = [], fixIts: [FixIt] = []) {
71+
addDiagnostic(node, message as DiagnosticMessage, highlights: highlights, fixIts: fixIts)
7272
}
7373

7474
/// If a diagnostic is generated that covers multiple syntax nodes, mark them as handles so they don't produce the generic diagnostics anymore.
@@ -119,7 +119,20 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
119119
// This is mostly a proof-of-concept implementation to produce more complex diagnostics.
120120
if let unexpectedCondition = node.body.unexpectedBeforeLeftBrace,
121121
unexpectedCondition.tokens(withKind: .semicolon).count == 2 {
122-
addDiagnostic(node, .cStyleForLoop)
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 }))
123136
markNodesAsHandled(node.inKeyword.id, unexpectedCondition.id)
124137
}
125138
return .visitChildren

Tests/SwiftParserTest/DiagnosticAssertions.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,17 @@ public class FixItApplier: SyntaxRewriter {
3232

3333
/// Asserts that the diagnostics `diag` inside `tree` occurs at `line` and
3434
/// `column`.
35-
/// If `message` is not `nil`, assert that the diagnostic has the given message.
3635
/// If `id` is not `nil`, assert that the diagnostic has the given message.
36+
/// If `message` is not `nil`, assert that the diagnostic has the given message.
37+
/// If `highlight` is not `nil`, assert that the highlighted range has this content.
3738
func XCTAssertDiagnostic<T: SyntaxProtocol>(
3839
_ diag: Diagnostic,
3940
in tree: T,
4041
line: Int,
4142
column: Int,
4243
id: MessageID? = nil,
4344
message: String? = nil,
45+
highlight: String? = nil,
4446
testFile: StaticString = #filePath,
4547
testLine: UInt = #line
4648
) {
@@ -54,6 +56,9 @@ func XCTAssertDiagnostic<T: SyntaxProtocol>(
5456
if let message = message {
5557
XCTAssertEqual(diag.message, message, file: testFile, line: testLine)
5658
}
59+
if let highlight = highlight {
60+
AssertStringsEqualWithDiff(diag.highlights.map(\.description).joined(), highlight, file: testFile, line: testLine)
61+
}
5762
}
5863

5964
/// Assert that producing diagnostics for `tree` generates a single diagnostic
@@ -67,6 +72,7 @@ func XCTAssertSingleDiagnostic<T: SyntaxProtocol>(
6772
column: Int,
6873
id: MessageID? = nil,
6974
message: String? = nil,
75+
highlight: String? = nil,
7076
expectedFixedSource: String? = nil,
7177
testFile: StaticString = #filePath,
7278
testLine: UInt = #line
@@ -76,7 +82,7 @@ func XCTAssertSingleDiagnostic<T: SyntaxProtocol>(
7682
XCTFail("Received \(diags.count) diagnostics but expected excatly one: \(diags)", file: testFile, line: testLine)
7783
return
7884
}
79-
XCTAssertDiagnostic(diags.first!, in: tree, line: line, column: column, id: id, message: message, testFile: testFile, testLine: testLine)
85+
XCTAssertDiagnostic(diags.first!, in: tree, line: line, column: column, id: id, message: message, highlight: highlight, testFile: testFile, testLine: testLine)
8086
if let expectedFixedSource = expectedFixedSource {
8187
let fixedSource = FixItApplier.applyFixes(in: diags, to: tree).description
8288
AssertStringsEqualWithDiff(fixedSource, expectedFixedSource, file: testFile, line: testLine)

Tests/SwiftParserTest/DiagnosticTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ public class DiagnosticTests: XCTestCase {
3131
Syntax(raw: $0.parseForEachStatement().raw).as(ForInStmtSyntax.self)!
3232
}
3333

34-
XCTAssertSingleDiagnostic(in: loop, line: 1, column: 1, message: "C-style for statement has been removed in Swift 3")
34+
XCTAssertSingleDiagnostic(
35+
in: loop,
36+
line: 1, column: 1,
37+
message: "C-style for statement has been removed in Swift 3",
38+
highlight: "let x = 0; x < 10; x += 1, y += 1 "
39+
)
3540
}
3641

3742
public func testMissingClosingParen() throws {

0 commit comments

Comments
 (0)