Skip to content

Commit bbbcf59

Browse files
committed
Emit diagnostics for missing nodes
1 parent a73be95 commit bbbcf59

File tree

7 files changed

+188
-53
lines changed

7 files changed

+188
-53
lines changed

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,40 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
114114
return .skipChildren
115115
}
116116

117+
private func handleMissingSyntax<T: SyntaxProtocol>(_ node: T) -> SyntaxVisitorContinueKind {
118+
if shouldSkip(node) {
119+
return .skipChildren
120+
}
121+
addDiagnostic(node, position: node.endPosition, MissingNodeError(missingNode: Syntax(node)))
122+
return .visitChildren
123+
}
124+
117125
// MARK: - Specialized diagnostic generation
118126

127+
public override func visit(_ node: MissingDeclSyntax) -> SyntaxVisitorContinueKind {
128+
return handleMissingSyntax(node)
129+
}
130+
131+
public override func visit(_ node: MissingExprSyntax) -> SyntaxVisitorContinueKind {
132+
return handleMissingSyntax(node)
133+
}
134+
135+
public override func visit(_ node: MissingPatternSyntax) -> SyntaxVisitorContinueKind {
136+
return handleMissingSyntax(node)
137+
}
138+
139+
public override func visit(_ node: MissingStmtSyntax) -> SyntaxVisitorContinueKind {
140+
return handleMissingSyntax(node)
141+
}
142+
143+
public override func visit(_ node: MissingSyntax) -> SyntaxVisitorContinueKind {
144+
return handleMissingSyntax(node)
145+
}
146+
147+
public override func visit(_ node: MissingTypeSyntax) -> SyntaxVisitorContinueKind {
148+
return handleMissingSyntax(node)
149+
}
150+
119151
public override func visit(_ node: ForInStmtSyntax) -> SyntaxVisitorContinueKind {
120152
if shouldSkip(node) {
121153
return .skipChildren
@@ -138,7 +170,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
138170
Syntax(node.unexpectedBetweenWhereClauseAndBody),
139171
Syntax(unexpectedCondition)
140172
] as [Syntax?]).compactMap({ $0 }))
141-
markNodesAsHandled(node.inKeyword.id, unexpectedCondition.id)
173+
markNodesAsHandled(node.inKeyword.id, node.sequenceExpr.id, unexpectedCondition.id)
142174
}
143175
return .visitChildren
144176
}

Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,26 @@ import SwiftSyntax
1616
let diagnosticDomain: String = "SwiftParser"
1717

1818
extension SyntaxProtocol {
19-
var nodeTypeNameForDiagnostics: String? {
19+
/// Return a name of this syntax node that can be used to describe it in
20+
/// diagnostics.
21+
/// Nodes that mostly exist for the syntax tree's structure and don't have a
22+
/// correspondence in the source code that's meeingful to the user by default
23+
/// use the name of the parent node that encloses it. Pass `false` to `inherit`
24+
/// to prevent this name inheritance.
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? {
2028
if let name = Syntax(self).as(SyntaxEnum.self).nameForDiagnostics {
21-
return name
29+
if Syntax(self).is(SourceFileSyntax.self) && !allowSourceFile {
30+
return nil
31+
} else {
32+
return name
33+
}
2234
}
23-
if let parent = self.parent {
24-
return parent.nodeTypeNameForDiagnostics
35+
if inherit {
36+
if let parent = self.parent {
37+
return parent.nodeTypeNameForDiagnostics(inherit: inherit, allowSourceFile: allowSourceFile)
38+
}
2539
}
2640
return nil
2741
}
@@ -114,11 +128,42 @@ public struct ExtaneousCodeAtTopLevel: ParserError {
114128
}
115129
}
116130

131+
public struct MissingNodeError: ParserError {
132+
public let missingNode: Syntax
133+
134+
public var message: String {
135+
var message: String
136+
var hasNamedParent = false
137+
if let parent = missingNode.parent,
138+
let childName = parent.childNameForDiagnostics(missingNode.index) {
139+
message = "Expected \(childName)"
140+
if let parent = missingNode.parent,
141+
let parentTypeName = parent.nodeTypeNameForDiagnostics(inherit: false) {
142+
message += " of \(parentTypeName)"
143+
hasNamedParent = true
144+
}
145+
} else {
146+
message = "Expected \(missingNode.nodeTypeNameForDiagnostics() ?? "syntax")"
147+
if let lastChild = missingNode.lastToken(viewMode: .fixedUp), lastChild.presence == .present {
148+
message += " after '\(lastChild.text)'"
149+
} else if let previousToken = missingNode.previousToken(viewMode: .fixedUp), previousToken.presence == .present {
150+
message += " after '\(previousToken.text)'"
151+
}
152+
}
153+
if !hasNamedParent {
154+
if let parent = missingNode.parent, let parentTypeName = parent.nodeTypeNameForDiagnostics(allowSourceFile: false) {
155+
message += " in \(parentTypeName)"
156+
}
157+
}
158+
return message
159+
}
160+
}
161+
117162
public struct MissingTokenError: ParserError {
118163
public let missingToken: TokenSyntax
119164

120165
public var message: String {
121-
guard let parent = missingToken.parent, let parentTypeName = parent.nodeTypeNameForDiagnostics else {
166+
guard let parent = missingToken.parent, let parentTypeName = parent.nodeTypeNameForDiagnostics() else {
122167
return "Expected '\(missingToken.text)'"
123168
}
124169
switch missingToken.tokenKind {
@@ -141,7 +186,7 @@ public struct UnexpectedNodesError: ParserError {
141186
public let unexpectedNodes: UnexpectedNodesSyntax
142187

143188
public var message: String {
144-
let parentTypeName = unexpectedNodes.parent?.nodeTypeNameForDiagnostics
189+
let parentTypeName = unexpectedNodes.parent?.nodeTypeNameForDiagnostics()
145190
let shortContent = unexpectedNodes.contentForDiagnosticsIfShortSingleLine
146191
switch (parentTypeName, shortContent) {
147192
case (let parentTypeName?, let shortContent?):

Tests/SwiftParserTest/Attributes.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ final class AttributeTests: XCTestCase {
1111
}
1212
""",
1313
diagnostics: [
14+
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected declaration"),
1415
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected 'for' in attribute argument"),
1516
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected ':' in attribute argument"),
1617
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected ')' to end attribute"),
@@ -27,7 +28,9 @@ final class AttributeTests: XCTestCase {
2728
""",
2829
diagnostics: [
2930
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected ':' in '@differentiable' argument"),
31+
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected parameters of '@differentiable' argument"),
3032
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected '=' in same type requirement"),
33+
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected right-hand type of same type requirement"),
3134
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected ')' to end attribute"),
3235
]
3336
)
@@ -36,21 +39,23 @@ final class AttributeTests: XCTestCase {
3639
func testMissingClosingParenToAttribute() {
3740
AssertParse(
3841
"""
39-
@_specialize(e#^DIAG_1^#
42+
@_specialize(e#^DIAG^#
4043
""",
4144
diagnostics: [
42-
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected ':' in attribute argument"),
43-
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected ')' to end attribute"),
45+
DiagnosticSpec(message: "Expected declaration"),
46+
DiagnosticSpec(message: "Expected ':' in attribute argument"),
47+
DiagnosticSpec(message: "Expected ')' to end attribute"),
4448
]
4549
)
4650
}
4751

4852
func testMultipleInvalidSpecializeParams() {
4953
AssertParse(
5054
"""
51-
@_specialize(e#^DIAG_1^#, exported#^DIAG_2^#)
55+
@_specialize(e#^DIAG_1^#, exported#^DIAG_2^#)#^DIAG_3^#
5256
""",
5357
diagnostics: [
58+
DiagnosticSpec(locationMarker: "DIAG_3", message: "Expected declaration after ')'"),
5459
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected ':' in attribute argument"),
5560
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected ':' in attribute argument"),
5661
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected 'false' in attribute argument"),

Tests/SwiftParserTest/Declarations.swift

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ final class DeclarationTests: XCTestCase {
4141
DiagnosticSpec(locationMarker: "DIAG1", message: "Expected '' in function"),
4242
DiagnosticSpec(locationMarker: "DIAG1", message: "Expected argument list in function declaration"),
4343
DiagnosticSpec(locationMarker: "DIAG2", message: "Expected '=' in same type requirement"),
44+
DiagnosticSpec(locationMarker: "DIAG2", message: "Expected right-hand type of same type requirement"),
4445
])
4546
}
4647

@@ -77,13 +78,15 @@ final class DeclarationTests: XCTestCase {
7778
AssertParse("class T where t#^DIAG^#",
7879
diagnostics: [
7980
DiagnosticSpec(message: "Expected '=' in same type requirement"),
81+
DiagnosticSpec(message: "Expected right-hand type of same type requirement"),
8082
DiagnosticSpec(message: "Expected '{' to start class"),
8183
DiagnosticSpec(message: "Expected '}' to end class"),
8284
])
8385
AssertParse("class B<#^DIAG_1^#where g#^DIAG_2^#",
8486
diagnostics: [
8587
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected '>' to end generic parameter clause"),
8688
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected '=' in same type requirement"),
89+
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected right-hand type of same type requirement"),
8790
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected '{' to start class"),
8891
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected '}' to end class"),
8992
])
@@ -147,7 +150,8 @@ final class DeclarationTests: XCTestCase {
147150
AssertParse(
148151
"_ = foo/* */?.description#^DIAG^#",
149152
diagnostics: [
150-
DiagnosticSpec(message: "Expected ':' after '? ...' in ternary expression")
153+
DiagnosticSpec(message: "Expected ':' after '? ...' in ternary expression"),
154+
DiagnosticSpec(message: "Expected expression"),
151155
]
152156
)
153157

@@ -396,9 +400,12 @@ final class DeclarationTests: XCTestCase {
396400
AssertParse(
397401
"""
398402
struct a {
399-
public
403+
public#^DIAG^#
400404
}
401-
"""
405+
""",
406+
diagnostics: [
407+
DiagnosticSpec(message: "Expected declaration after 'public' in struct")
408+
]
402409
)
403410
}
404411

@@ -489,7 +496,12 @@ final class DeclarationTests: XCTestCase {
489496

490497
func testExtraneousRightBraceRecovery() {
491498
AssertParse(
492-
"class ABC { let def = ghi(jkl: mno) } #^DIAG^#}",
499+
"""
500+
class ABC {
501+
let def = ghi(jkl: mno)
502+
}
503+
#^DIAG^#}
504+
""",
493505
diagnostics: [
494506
DiagnosticSpec(message: "Extraneous '}' at top level")
495507
]
@@ -504,8 +516,8 @@ final class DeclarationTests: XCTestCase {
504516
}
505517
""",
506518
diagnostics: [
507-
// FIXME: This diagnostic should be more contextual
508-
DiagnosticSpec(message: "Expected '->' in return clause")
519+
DiagnosticSpec(message: "Expected '->' in subscript"),
520+
DiagnosticSpec(message: "Expected return type in subscript"),
509521
]
510522
)
511523
}
@@ -552,16 +564,13 @@ final class DeclarationTests: XCTestCase {
552564
func testExpressionMember() {
553565
AssertParse(
554566
"""
555-
struct S {
556-
#^DIAG^#/ ###line 25 "line-directive.swift"
567+
struct S {#^EXPECTED_DECL^#
568+
#^UNEXPECTED_TEXT^#/ ###line 25 "line-directive.swift"
557569
}
558570
""",
559571
diagnostics: [
560-
DiagnosticSpec(
561-
message: """
562-
Unexpected text '/ ###line 25 "line-directive.swift"' found in struct
563-
"""
564-
)
572+
DiagnosticSpec(locationMarker: "EXPECTED_DECL", message: "Expected declaration after '{' in struct"),
573+
DiagnosticSpec(locationMarker: "UNEXPECTED_TEXT", message: #"Unexpected text '/ ###line 25 "line-directive.swift"' found in struct"#)
565574
]
566575
)
567576
}
@@ -735,12 +744,17 @@ final class DeclarationTests: XCTestCase {
735744
func testMalforedStruct() {
736745
AssertParse(
737746
"""
738-
struct n#^OPENINGBRACES^##if@#^ENDIF^##^CLOSINGBRACES^#
747+
struct n#^OPENING_BRACE^#
748+
#if#^AFTER_POUND_IF^#
749+
@#^END^#
739750
""",
740751
diagnostics: [
741-
DiagnosticSpec(locationMarker: "OPENINGBRACES", message: "Expected '{' to start struct"),
742-
DiagnosticSpec(locationMarker: "ENDIF", message: "Expected '#endif' in conditional compilation block"),
743-
DiagnosticSpec(locationMarker: "CLOSINGBRACES", message: "Expected '}' to end struct")
752+
DiagnosticSpec(locationMarker: "OPENING_BRACE", message: "Expected '{' to start struct"),
753+
DiagnosticSpec(locationMarker: "AFTER_POUND_IF", message: "Expected condition of conditional compilation clause"),
754+
DiagnosticSpec(locationMarker: "END", message: "Expected declaration after '@' in conditional compilation clause"),
755+
DiagnosticSpec(locationMarker: "END", message: "Expected name of attribute"),
756+
DiagnosticSpec(locationMarker: "END", message: "Expected '#endif' in conditional compilation block"),
757+
DiagnosticSpec(locationMarker: "END", message: "Expected '}' to end struct")
744758
]
745759
)
746760
}

0 commit comments

Comments
 (0)