Skip to content

Commit acd3067

Browse files
authored
Merge pull request #739 from ahoppen/ahoppen/diagnose-missing-nodes
Emit diagnostics for missing nodes
2 parents 1858fcd + cecf603 commit acd3067

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+5602
-274
lines changed

Sources/SwiftDiagnostics/Diagnostic.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@ public struct Diagnostic: CustomDebugStringConvertible {
1919
/// The node at whose start location the message should be displayed.
2020
public let node: Syntax
2121

22+
/// The position at which the location should be anchored.
23+
/// By default, this is the start location of `node`.
24+
public let position: AbsolutePosition
25+
2226
/// Nodes that should be highlighted in the source code.
2327
public let highlights: [Syntax]
2428

2529
/// Fix-Its that can be applied to resolve this diagnostic.
2630
/// Each Fix-It offers a different way to resolve the diagnostic. Usually, there's only one.
2731
public let fixIts: [FixIt]
2832

29-
public init(node: Syntax, message: DiagnosticMessage, highlights: [Syntax] = [], fixIts: [FixIt] = []) {
30-
self.diagMessage = message
33+
public init(node: Syntax, position: AbsolutePosition? = nil, message: DiagnosticMessage, highlights: [Syntax] = [], fixIts: [FixIt] = []) {
3134
self.node = node
35+
self.position = position ?? node.positionAfterSkippingLeadingTrivia
36+
self.diagMessage = message
3237
self.highlights = highlights
3338
self.fixIts = fixIts
3439
}
@@ -46,7 +51,7 @@ public struct Diagnostic: CustomDebugStringConvertible {
4651

4752
/// The location at which the diagnostic should be displayed.
4853
public func location(converter: SourceLocationConverter) -> SourceLocation {
49-
return node.startLocation(converter: converter)
54+
return converter.location(for: position)
5055
}
5156

5257
public var debugDescription: String {

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,15 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
6767
// MARK: - Private helper functions
6868

6969
/// Produce a diagnostic.
70-
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: DiagnosticMessage, highlights: [Syntax] = [], fixIts: [FixIt] = [], handledNodes: [SyntaxIdentifier] = []) {
70+
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, position: AbsolutePosition? = nil, _ message: DiagnosticMessage, highlights: [Syntax] = [], fixIts: [FixIt] = [], handledNodes: [SyntaxIdentifier] = []) {
7171
diagnostics.removeAll(where: { handledNodes.contains($0.node.id) })
72-
diagnostics.append(Diagnostic(node: Syntax(node), message: message, highlights: highlights, fixIts: fixIts))
72+
diagnostics.append(Diagnostic(node: Syntax(node), position: position, message: message, highlights: highlights, fixIts: fixIts))
7373
self.handledNodes.append(contentsOf: handledNodes)
7474
}
7575

7676
/// Produce a diagnostic.
77-
private func addDiagnostic<T: SyntaxProtocol>(_ node: T, _ message: StaticParserError, highlights: [Syntax] = [], fixIts: [FixIt] = [], handledNodes: [SyntaxIdentifier] = []) {
78-
addDiagnostic(node, message as DiagnosticMessage, highlights: highlights, fixIts: fixIts, handledNodes: handledNodes)
77+
private func addDiagnostic<T: SyntaxProtocol>(_ node: T,position: AbsolutePosition? = nil, _ message: StaticParserError, highlights: [Syntax] = [], fixIts: [FixIt] = [], handledNodes: [SyntaxIdentifier] = []) {
78+
addDiagnostic(node, position: position, message as DiagnosticMessage, highlights: highlights, fixIts: fixIts, handledNodes: handledNodes)
7979
}
8080

8181
/// Whether the node should be skipped for diagnostic emission.
@@ -118,8 +118,40 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
118118
return .skipChildren
119119
}
120120

121+
private func handleMissingSyntax<T: SyntaxProtocol>(_ node: T) -> SyntaxVisitorContinueKind {
122+
if shouldSkip(node) {
123+
return .skipChildren
124+
}
125+
addDiagnostic(node, position: node.endPosition, MissingNodeError(missingNode: Syntax(node)))
126+
return .visitChildren
127+
}
128+
121129
// MARK: - Specialized diagnostic generation
122130

131+
public override func visit(_ node: MissingDeclSyntax) -> SyntaxVisitorContinueKind {
132+
return handleMissingSyntax(node)
133+
}
134+
135+
public override func visit(_ node: MissingExprSyntax) -> SyntaxVisitorContinueKind {
136+
return handleMissingSyntax(node)
137+
}
138+
139+
public override func visit(_ node: MissingPatternSyntax) -> SyntaxVisitorContinueKind {
140+
return handleMissingSyntax(node)
141+
}
142+
143+
public override func visit(_ node: MissingStmtSyntax) -> SyntaxVisitorContinueKind {
144+
return handleMissingSyntax(node)
145+
}
146+
147+
public override func visit(_ node: MissingSyntax) -> SyntaxVisitorContinueKind {
148+
return handleMissingSyntax(node)
149+
}
150+
151+
public override func visit(_ node: MissingTypeSyntax) -> SyntaxVisitorContinueKind {
152+
return handleMissingSyntax(node)
153+
}
154+
123155
public override func visit(_ node: ForInStmtSyntax) -> SyntaxVisitorContinueKind {
124156
if shouldSkip(node) {
125157
return .skipChildren
@@ -145,7 +177,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
145177
Syntax(node.unexpectedBetweenWhereClauseAndBody),
146178
Syntax(unexpectedCondition)
147179
] as [Syntax?]).compactMap({ $0 }),
148-
handledNodes: [node.inKeyword.id, unexpectedCondition.id]
180+
handledNodes: [node.inKeyword.id, node.sequenceExpr.id, unexpectedCondition.id]
149181
)
150182
}
151183
return .visitChildren

Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@ extension SyntaxProtocol {
2222
/// default.
2323
/// Pass inherit: false to prevent this behavior, in which case `nil` will be
2424
/// returned instead.
25-
func nodeTypeNameForDiagnostics(inherit: Bool = true) -> String? {
25+
/// If `allowSourceFile` is `false`, `nil` will be returned if the inherited
26+
/// node type name is "source file".
27+
func nodeTypeNameForDiagnostics(inherit: Bool = true, allowSourceFile: Bool = true) -> String? {
2628
if let name = Syntax(self).as(SyntaxEnum.self).nameForDiagnostics {
27-
return name
29+
if Syntax(self).is(SourceFileSyntax.self) && !allowSourceFile {
30+
return nil
31+
} else {
32+
return name
33+
}
2834
}
2935
if inherit {
3036
if let parent = self.parent {
31-
return parent.nodeTypeNameForDiagnostics(inherit: inherit)
37+
return parent.nodeTypeNameForDiagnostics(inherit: inherit, allowSourceFile: allowSourceFile)
3238
}
3339
}
3440
return nil
@@ -135,6 +141,38 @@ public struct InvalidIdentifierError: ParserError {
135141
}
136142
}
137143

144+
public struct MissingNodeError: ParserError {
145+
public let missingNode: Syntax
146+
147+
public var message: String {
148+
var message: String
149+
var hasNamedParent = false
150+
if let parent = missingNode.parent,
151+
let childName = parent.childNameForDiagnostics(missingNode.index) {
152+
message = "Expected \(childName)"
153+
if let parentTypeName = parent.nodeTypeNameForDiagnostics(inherit: false) {
154+
message += " of \(parentTypeName)"
155+
hasNamedParent = true
156+
}
157+
} else {
158+
message = "Expected \(missingNode.nodeTypeNameForDiagnostics() ?? "syntax")"
159+
if let missingDecl = missingNode.as(MissingDeclSyntax.self), let lastModifier = missingDecl.modifiers?.last {
160+
message += " after '\(lastModifier.name.text)' modifier"
161+
} else if let missingDecl = missingNode.as(MissingDeclSyntax.self), missingDecl.attributes != nil {
162+
message += " after attribute"
163+
} else if let previousToken = missingNode.previousToken(viewMode: .fixedUp), previousToken.presence == .present {
164+
message += " after '\(previousToken.text)'"
165+
}
166+
}
167+
if !hasNamedParent {
168+
if let parent = missingNode.parent, let parentTypeName = parent.nodeTypeNameForDiagnostics(allowSourceFile: false) {
169+
message += " in \(parentTypeName)"
170+
}
171+
}
172+
return message
173+
}
174+
}
175+
138176
public struct MissingTokenError: ParserError {
139177
public let missingToken: TokenSyntax
140178

Sources/SwiftParser/Directives.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ extension Parser {
8989
do {
9090
var elementsProgress = LoopProgressCondition()
9191
while !self.at(any: [.eof, .poundElseKeyword, .poundElseifKeyword, .poundEndifKeyword]) && elementsProgress.evaluate(currentToken) {
92-
guard let element = parseElement(&self), element.raw.byteLength > 0 else {
92+
guard let element = parseElement(&self), !element.isEmpty else {
9393
break
9494
}
9595
elements.append(element)

Sources/SwiftParser/Expressions.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,21 +1617,27 @@ extension Parser {
16171617

16181618
switch elementKind! {
16191619
case .array(let el):
1620-
elements.append(RawSyntax(RawArrayElementSyntax(
1621-
expression: el, trailingComma: comma, arena: self.arena)))
1622-
if el.is(RawMissingExprSyntax.self) {
1620+
let element = RawArrayElementSyntax(
1621+
expression: el, trailingComma: comma, arena: self.arena
1622+
)
1623+
if element.isEmpty {
16231624
break COLLECTION_LOOP
1625+
} else {
1626+
elements.append(RawSyntax(element))
16241627
}
16251628
case .dictionary(let key, let unexpectedBeforeColon, let colon, let value):
1626-
elements.append(RawSyntax(RawDictionaryElementSyntax(
1629+
let element = RawDictionaryElementSyntax(
16271630
keyExpression: key,
16281631
unexpectedBeforeColon,
16291632
colon: colon,
16301633
valueExpression: value,
16311634
trailingComma: comma,
1632-
arena: self.arena)))
1633-
if key.is(RawMissingExprSyntax.self), colon.isMissing, value.is(RawMissingExprSyntax.self) {
1635+
arena: self.arena
1636+
)
1637+
if element.isEmpty {
16341638
break COLLECTION_LOOP
1639+
} else {
1640+
elements.append(RawSyntax(element))
16351641
}
16361642
}
16371643

Sources/SwiftParser/Statements.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,7 @@ extension Parser {
865865
let expr: RawExprSyntax?
866866
if
867867
!self.at(any: [
868-
RawTokenKind.rightBrace, .semicolon, .eof,
868+
.rightBrace, .caseKeyword, .semicolon, .eof,
869869
.poundIfKeyword, .poundErrorKeyword, .poundWarningKeyword,
870870
.poundEndifKeyword, .poundElseKeyword, .poundElseifKeyword
871871
])

Sources/SwiftParser/TopLevel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ extension Parser {
119119
let item = self.parseItem(isAtTopLevel: isAtTopLevel)
120120
let semi = self.consume(if: .semicolon)
121121

122-
if item.raw.byteLength == 0 && semi == nil {
122+
if item.isEmpty && semi == nil {
123123
return nil
124124
}
125125
return .init(item: item, semicolon: semi, errorTokens: nil, arena: self.arena)

Sources/SwiftSyntax/Misc.swift.gyb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ extension Syntax {
3939
% for node in NON_BASE_SYNTAX_NODES:
4040
case .${node.swift_syntax_kind}(let node):
4141
return node
42+
% end
43+
}
44+
}
45+
46+
public func childNameForDiagnostics(_ index: SyntaxChildrenIndex) -> String? {
47+
switch self.as(SyntaxEnum.self) {
48+
case .token(let node):
49+
return node.childNameForDiagnostics(index)
50+
case .unknown(let node):
51+
return node.childNameForDiagnostics(index)
52+
% for node in NON_BASE_SYNTAX_NODES:
53+
case .${node.swift_syntax_kind}(let node):
54+
return node.childNameForDiagnostics(index)
4255
% end
4356
}
4457
}

Sources/SwiftSyntax/Raw/RawSyntaxNodeProtocol.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ public extension RawSyntaxNodeProtocol {
4242
func write<Target>(to target: inout Target) where Target : TextOutputStream {
4343
raw.write(to: &target)
4444
}
45+
46+
var isEmpty: Bool {
47+
return raw.byteLength == 0
48+
}
4549
}
4650

4751
/// `RawSyntax` itself conforms to `RawSyntaxNodeProtocol`.

Sources/SwiftSyntax/Syntax.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ public protocol SyntaxProtocol: CustomStringConvertible,
131131
/// Converts the given `Syntax` node to this type. Returns `nil` if the
132132
/// conversion is not possible.
133133
init?(_ syntaxNode: Syntax)
134+
135+
/// Return a name with which the child at the given `index` can be referred to
136+
/// in diagnostics.
137+
func childNameForDiagnostics(_ index: SyntaxChildrenIndex) -> String?
134138
}
135139

136140
extension SyntaxProtocol {

Sources/SwiftSyntax/SyntaxBaseNodes.swift.gyb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ public struct ${node.name}: ${node.name}Protocol, SyntaxHashable {
120120
public func asProtocol(_: ${node.name}Protocol.Protocol) -> ${node.name}Protocol {
121121
return Syntax(self).asProtocol(${node.name}Protocol.self)!
122122
}
123+
124+
public func childNameForDiagnostics(_ index: SyntaxChildrenIndex) -> String? {
125+
return Syntax(self).childNameForDiagnostics(index)
126+
}
123127
}
124128

125129
extension ${node.name}: CustomReflectable {

Sources/SwiftSyntax/SyntaxChildren.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public struct SyntaxChildrenIndexData: Comparable {
2020
fileprivate let offset: UInt32
2121
/// The index of the node in the parent.
2222
/// See `AbsoluteSyntaxPosition.indexInParent`
23-
fileprivate let indexInParent: UInt32
23+
let indexInParent: UInt32
2424
/// Unique value for a node within its own tree.
2525
/// See `SyntaxIdentifier.indexIntree`
2626
fileprivate let indexInTree: SyntaxIndexInTree
@@ -61,7 +61,7 @@ public struct SyntaxChildrenIndex: Comparable, ExpressibleByNilLiteral {
6161
/// SyntaxChildrenIndex` an enumbecause the optional value can be
6262
/// force-unwrapped when we know that an index is not the end index, saving a
6363
/// switch case comparison.
64-
fileprivate let data: SyntaxChildrenIndexData?
64+
let data: SyntaxChildrenIndexData?
6565

6666
fileprivate init(offset: UInt32, indexInParent: UInt32,
6767
indexInTree: SyntaxIndexInTree) {

Sources/SwiftSyntax/SyntaxCollections.swift.gyb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ public struct ${node.name}: SyntaxCollection, SyntaxHashable {
217217
self = withTrailingTrivia(newValue ?? [])
218218
}
219219
}
220+
221+
public func childNameForDiagnostics(_ index: SyntaxChildrenIndex) -> String? {
222+
return nil
223+
}
220224
}
221225

222226
/// Conformance for `${node.name}` to the `BidirectionalCollection` protocol.

Sources/SwiftSyntax/SyntaxNodes.swift.gyb.template

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,21 @@ public struct ${node.name}: ${base_type}Protocol, SyntaxHashable {
171171
return ${node.name}(newData)
172172
}
173173
% end
174+
175+
public func childNameForDiagnostics(_ index: SyntaxChildrenIndex) -> String? {
176+
switch index.data?.indexInParent {
177+
% for (index, child) in enumerate(node.children):
178+
case ${index}:
179+
% if child.name_for_diagnostics:
180+
return "${child.name_for_diagnostics}"
181+
% else:
182+
return nil
183+
% end
184+
% end
185+
default:
186+
fatalError("Invalid index")
187+
}
188+
}
174189
}
175190

176191
extension ${node.name}: CustomReflectable {

Sources/SwiftSyntax/SyntaxOtherNodes.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ public struct UnknownSyntax: SyntaxProtocol, SyntaxHashable {
3939
let data = SyntaxData.forRoot(raw)
4040
self.init(data)
4141
}
42+
43+
public func childNameForDiagnostics(_ index: SyntaxChildrenIndex) -> String? {
44+
return nil
45+
}
4246
}
4347

4448
extension UnknownSyntax: CustomReflectable {
@@ -192,6 +196,10 @@ public struct TokenSyntax: SyntaxProtocol, SyntaxHashable {
192196
public var totalLength: SourceLength {
193197
return raw.totalLength
194198
}
199+
200+
public func childNameForDiagnostics(_ index: SyntaxChildrenIndex) -> String? {
201+
return nil
202+
}
195203
}
196204

197205
extension TokenSyntax: CustomReflectable {

0 commit comments

Comments
 (0)