Skip to content

Commit ce30583

Browse files
committed
Emit diagnostics for missing nodes
1 parent b8ed226 commit ce30583

File tree

8 files changed

+200
-56
lines changed

8 files changed

+200
-56
lines changed

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 40 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,37 @@ 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 parent = missingNode.parent,
154+
let parentTypeName = parent.nodeTypeNameForDiagnostics(inherit: false) {
155+
message += " of \(parentTypeName)"
156+
hasNamedParent = true
157+
}
158+
} else {
159+
message = "Expected \(missingNode.nodeTypeNameForDiagnostics() ?? "syntax")"
160+
if let lastChild = missingNode.lastToken(viewMode: .fixedUp), lastChild.presence == .present {
161+
message += " after '\(lastChild.text)'"
162+
} else if let previousToken = missingNode.previousToken(viewMode: .fixedUp), previousToken.presence == .present {
163+
message += " after '\(previousToken.text)'"
164+
}
165+
}
166+
if !hasNamedParent {
167+
if let parent = missingNode.parent, let parentTypeName = parent.nodeTypeNameForDiagnostics(allowSourceFile: false) {
168+
message += " in \(parentTypeName)"
169+
}
170+
}
171+
return message
172+
}
173+
}
174+
138175
public struct MissingTokenError: ParserError {
139176
public let missingToken: TokenSyntax
140177

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: 33 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 identifier 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,12 +78,14 @@ 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<where g#^DIAG^#",
8486
diagnostics: [
8587
DiagnosticSpec(message: "Expected '=' in same type requirement"),
88+
DiagnosticSpec(message: "Expected right-hand type of same type requirement"),
8689
DiagnosticSpec(message: "Expected '>' to end generic parameter clause"),
8790
DiagnosticSpec(message: "Expected '{' to start class"),
8891
DiagnosticSpec(message: "Expected '}' to end class"),
@@ -171,7 +174,8 @@ final class DeclarationTests: XCTestCase {
171174
AssertParse(
172175
"_ = foo/* */?.description#^DIAG^#",
173176
diagnostics: [
174-
DiagnosticSpec(message: "Expected ':' after '? ...' in ternary expression")
177+
DiagnosticSpec(message: "Expected ':' after '? ...' in ternary expression"),
178+
DiagnosticSpec(message: "Expected expression"),
175179
]
176180
)
177181

@@ -545,9 +549,12 @@ final class DeclarationTests: XCTestCase {
545549
AssertParse(
546550
"""
547551
struct a {
548-
public
552+
public#^DIAG^#
549553
}
550-
"""
554+
""",
555+
diagnostics: [
556+
DiagnosticSpec(message: "Expected declaration after 'public' in struct")
557+
]
551558
)
552559
}
553560

@@ -638,7 +645,12 @@ final class DeclarationTests: XCTestCase {
638645

639646
func testExtraneousRightBraceRecovery() {
640647
AssertParse(
641-
"class ABC { let def = ghi(jkl: mno) } #^DIAG^#}",
648+
"""
649+
class ABC {
650+
let def = ghi(jkl: mno)
651+
}
652+
#^DIAG^#}
653+
""",
642654
diagnostics: [
643655
DiagnosticSpec(message: "Extraneous '}' at top level")
644656
]
@@ -653,7 +665,8 @@ final class DeclarationTests: XCTestCase {
653665
}
654666
""",
655667
diagnostics: [
656-
DiagnosticSpec(message: "Expected '->' in return clause")
668+
DiagnosticSpec(message: "Expected '->' in subscript"),
669+
DiagnosticSpec(message: "Expected return type in subscript"),
657670
]
658671
)
659672
}
@@ -693,16 +706,13 @@ final class DeclarationTests: XCTestCase {
693706
func testExpressionMember() {
694707
AssertParse(
695708
"""
696-
struct S {
697-
#^DIAG^#/ ###line 25 "line-directive.swift"
709+
struct S {#^EXPECTED_DECL^#
710+
#^UNEXPECTED_TEXT^#/ ###line 25 "line-directive.swift"
698711
}
699712
""",
700713
diagnostics: [
701-
DiagnosticSpec(
702-
message: """
703-
Unexpected text '/ ###line 25 "line-directive.swift"' in struct
704-
"""
705-
)
714+
DiagnosticSpec(locationMarker: "EXPECTED_DECL", message: "Expected declaration after '{' in struct"),
715+
DiagnosticSpec(locationMarker: "UNEXPECTED_TEXT", message: #"Unexpected text '/ ###line 25 "line-directive.swift"' in struct"#)
706716
]
707717
)
708718
}
@@ -887,12 +897,17 @@ final class DeclarationTests: XCTestCase {
887897
func testMalforedStruct() {
888898
AssertParse(
889899
"""
890-
struct n#^OPENINGBRACES^##if@#^ENDIF^##^CLOSINGBRACES^#
900+
struct n#^OPENING_BRACE^#
901+
#if#^AFTER_POUND_IF^#
902+
@#^END^#
891903
""",
892904
diagnostics: [
893-
DiagnosticSpec(locationMarker: "OPENINGBRACES", message: "Expected '{' to start struct"),
894-
DiagnosticSpec(locationMarker: "ENDIF", message: "Expected '#endif' in conditional compilation block"),
895-
DiagnosticSpec(locationMarker: "CLOSINGBRACES", message: "Expected '}' to end struct")
905+
DiagnosticSpec(locationMarker: "OPENING_BRACE", message: "Expected '{' to start struct"),
906+
DiagnosticSpec(locationMarker: "AFTER_POUND_IF", message: "Expected condition of conditional compilation clause"),
907+
DiagnosticSpec(locationMarker: "END", message: "Expected declaration after '@' in conditional compilation clause"),
908+
DiagnosticSpec(locationMarker: "END", message: "Expected name of attribute"),
909+
DiagnosticSpec(locationMarker: "END", message: "Expected '#endif' in conditional compilation block"),
910+
DiagnosticSpec(locationMarker: "END", message: "Expected '}' to end struct")
896911
]
897912
)
898913
}
@@ -994,7 +1009,8 @@ final class DeclarationTests: XCTestCase {
9941009
diagnostics: [
9951010
DiagnosticSpec(locationMarker: "DIAG_1", message: "Unexpected text '}' before subscript"),
9961011
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected argument list in function declaration"),
997-
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected '->' in return clause"),
1012+
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected '->' in subscript"),
1013+
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected return type in subscript"),
9981014
])
9991015
}
10001016

Tests/SwiftParserTest/Directives.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,20 +133,26 @@ final class DirectiveTests: XCTestCase {
133133
public struct S2 { }
134134
135135
#if hasAttribute(foo)
136-
@foo
136+
@foo#^DIAG_1^#
137137
#endif
138138
@inlinable
139139
func f1() { }
140140
141141
#if hasAttribute(foo)
142-
@foo
142+
@foo#^DIAG_2^#
143143
#else
144144
@available(*, deprecated, message: "nope")
145-
@frozen
145+
@frozen#^DIAG_3^#
146146
#endif
147147
public struct S3 { }
148148
}
149-
""")
149+
""",
150+
diagnostics: [
151+
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected declaration after 'foo' in conditional compilation clause"),
152+
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected declaration after 'foo' in conditional compilation clause"),
153+
DiagnosticSpec(locationMarker: "DIAG_3", message: "Expected declaration after 'frozen' in conditional compilation clause"),
154+
]
155+
)
150156
}
151157

152158
}

0 commit comments

Comments
 (0)