Skip to content

Commit a4d8510

Browse files
committed
Merge missing token diagnostics if they occur at the same source location
1 parent 42fbea4 commit a4d8510

File tree

10 files changed

+266
-126
lines changed

10 files changed

+266
-126
lines changed

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
117117
let previousParent = invalidIdentifier.parent?.as(UnexpectedNodesSyntax.self) {
118118
addDiagnostic(invalidIdentifier, InvalidIdentifierError(invalidIdentifier: invalidIdentifier), handledNodes: [previousParent.id])
119119
} else {
120-
addDiagnostic(node, MissingTokenError(missingToken: node), fixIts: [
121-
FixIt(message: InsertTokenFixIt(missingToken: node), changes: [
122-
.makePresent(node: node)
123-
])
124-
])
120+
return handleMissingSyntax(node)
125121
}
126122
}
127123
return .skipChildren
@@ -131,7 +127,44 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
131127
if shouldSkip(node) {
132128
return .skipChildren
133129
}
134-
addDiagnostic(node, position: node.endPosition, MissingNodeError(missingNode: Syntax(node)))
130+
131+
let position: AbsolutePosition
132+
let fixIts: [FixIt]
133+
if let token = Syntax(node).as(TokenSyntax.self) {
134+
position = token.position
135+
fixIts = [
136+
FixIt(message: InsertTokenFixIt(missingToken: token), changes: [
137+
.makePresent(node: node)
138+
])
139+
]
140+
} else {
141+
position = node.endPosition
142+
fixIts = []
143+
}
144+
145+
// Find the previous sibling, skipping over siblings that don't actually contain any source text.
146+
var previousSibling: Syntax? = node.previousSibling(viewMode: .all)
147+
while let previousSiblingUnwrapped = previousSibling,
148+
!previousSiblingUnwrapped.is(TokenSyntax.self),
149+
!previousSiblingUnwrapped.raw.kind.isMissing,
150+
previousSiblingUnwrapped.raw.isEmpty {
151+
previousSibling = previousSibling?.previousSibling(viewMode: .all)
152+
}
153+
154+
if let previousSibling = previousSibling,
155+
let previousMissingTokenDiag = diagnostics.filter({ $0.node == Syntax(previousSibling) }).first,
156+
let previousMissingTokenDiagMessage = previousMissingTokenDiag.diagMessage as? MissingNodesError {
157+
// Merge into previous diagnostic
158+
addDiagnostic(
159+
node,
160+
position: position,
161+
MissingNodesError(missingNodes: previousMissingTokenDiagMessage.missingNodes + [Syntax(node)]),
162+
fixIts: previousMissingTokenDiag.fixIts + fixIts,
163+
handledNodes: [previousMissingTokenDiag.node.id]
164+
)
165+
} else {
166+
addDiagnostic(node, position: position, MissingNodesError(missingNode: Syntax(node)), fixIts: fixIts)
167+
}
135168
return .visitChildren
136169
}
137170

Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift

Lines changed: 160 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import SwiftDiagnostics
1414
@_spi(RawSyntax) import SwiftSyntax
15+
import SwiftBasicFormat
1516

1617
let diagnosticDomain: String = "SwiftParser"
1718

@@ -131,79 +132,184 @@ public struct InvalidIdentifierError: ParserError {
131132
}
132133
}
133134

134-
public struct MissingNodeError: ParserError {
135-
public let missingNode: Syntax
136-
137-
public var message: String {
138-
var message: String
139-
var hasNamedParent = false
140-
if let parent = missingNode.parent,
141-
let childName = parent.childNameForDiagnostics(missingNode.index) {
142-
message = "Expected \(childName)"
143-
if let parentTypeName = parent.nodeTypeNameForDiagnostics(inherit: false) {
144-
message += " of \(parentTypeName)"
145-
hasNamedParent = true
146-
}
147-
} else {
148-
message = "Expected \(missingNode.nodeTypeNameForDiagnostics() ?? "syntax")"
149-
if let missingDecl = missingNode.as(MissingDeclSyntax.self), let lastModifier = missingDecl.modifiers?.last {
150-
message += " after '\(lastModifier.name.text)' modifier"
151-
} else if let missingDecl = missingNode.as(MissingDeclSyntax.self), missingDecl.attributes != nil {
152-
message += " after attribute"
153-
} else if let previousToken = missingNode.previousToken(viewMode: .fixedUp), previousToken.presence == .present {
154-
message += " after '\(previousToken.text)'"
155-
}
156-
}
157-
if !hasNamedParent {
158-
if let parent = missingNode.parent, let parentTypeName = parent.nodeTypeNameForDiagnostics(allowSourceFile: false) {
159-
message += " in \(parentTypeName)"
160-
}
135+
private extension String {
136+
/// Remove any leading or trailing whitespace
137+
func trimmingWhitespaces() -> String {
138+
var result: Substring = Substring(self)
139+
result = result.drop(while: { $0 == " " })
140+
while result.last == " " {
141+
result = result.dropLast(1)
161142
}
162-
return message
143+
return String(result)
163144
}
164145
}
165146

166-
public struct MissingAttributeArgument: ParserError {
167-
/// The name of the attribute that's missing the argument, without `@`.
168-
public let attributeName: TokenSyntax
147+
public struct MissingNodesError: ParserError {
148+
public let missingNodes: [Syntax]
149+
public let commonParent: Syntax?
169150

170-
public var message: String {
171-
return "Expected argument for '@\(attributeName)' attribute"
151+
init(missingNode: Syntax) {
152+
self.missingNodes = [missingNode]
153+
self.commonParent = missingNode.parent
172154
}
173-
}
174155

175-
public struct MissingTokenError: ParserError {
176-
public let missingToken: TokenSyntax
156+
init(missingNodes: [Syntax]) {
157+
assert(!missingNodes.isEmpty)
158+
self.missingNodes = missingNodes
159+
self.commonParent = missingNodes.first!.parent
160+
assert(missingNodes.allSatisfy({ $0.parent == self.commonParent }))
161+
}
177162

178-
public var message: String {
179-
var message = "Expected"
180-
if missingToken.text.isEmpty {
181-
message += " \(missingToken.tokenKind.decomposeToRaw().rawKind.nameForDiagnostics)"
163+
/// Returns a list of the tokens that are missing
164+
private var missingNodesDescription: String {
165+
if let codeBlock = commonParent?.as(CodeBlockSyntax.self),
166+
missingNodes.contains(Syntax(codeBlock.leftBrace)),
167+
missingNodes.contains(Syntax(codeBlock.rightBrace)) {
168+
return "code block"
169+
} else if let memberDeclBlock = commonParent?.as(MemberDeclBlockSyntax.self),
170+
missingNodes.contains(Syntax(memberDeclBlock.leftBrace)),
171+
missingNodes.contains(Syntax(memberDeclBlock.rightBrace)) {
172+
return "member block"
173+
}
174+
175+
enum Part {
176+
case sourceText(String)
177+
case tokenWithoutDefaultText(RawTokenKind)
178+
case node(Syntax)
179+
180+
var description: String {
181+
switch self {
182+
case .sourceText(let content):
183+
return "'\(content.trimmingWhitespaces())'"
184+
case .tokenWithoutDefaultText(let tokenKind):
185+
return tokenKind.nameForDiagnostics
186+
case .node(let node):
187+
if let parent = node.parent,
188+
let childName = parent.childNameForDiagnostics(node.index) {
189+
return "\(childName)"
190+
} else {
191+
return "\(node.nodeTypeNameForDiagnostics() ?? "syntax")"
192+
}
193+
}
194+
}
195+
}
196+
197+
var parts: [Part] = []
198+
for missingNode in missingNodes {
199+
if let missingToken = missingNode.as(TokenSyntax.self) {
200+
let newPart: Part
201+
let (rawKind, text) = missingToken.tokenKind.decomposeToRaw()
202+
if let text = text, !text.isEmpty {
203+
let presentToken = TokenSyntax(missingToken.tokenKind, presence: .present)
204+
newPart = .sourceText("\(Syntax(Format().format(syntax: presentToken)))")
205+
} else {
206+
let newKind: TokenKind
207+
if let defaultText = rawKind.defaultText {
208+
newKind = TokenKind.fromRaw(kind: rawKind, text: String(syntaxText: defaultText))
209+
let presentToken = TokenSyntax(newKind, presence: .present)
210+
newPart = .sourceText("\(Syntax(Format().format(syntax: presentToken)))")
211+
} else {
212+
newPart = .tokenWithoutDefaultText(rawKind)
213+
}
214+
}
215+
216+
switch (parts.last, newPart) {
217+
case (.sourceText(let previousContent), .sourceText(let newContent)):
218+
parts[parts.count - 1] = .sourceText(previousContent + newContent)
219+
default:
220+
parts.append(newPart)
221+
}
222+
} else {
223+
parts.append(.node(missingNode))
224+
}
225+
}
226+
let partDescriptions = parts.map({ $0.description })
227+
if partDescriptions.count > 1 {
228+
return "\(partDescriptions[0..<partDescriptions.count - 1].joined(separator: ", ")) and \(partDescriptions.last!)"
182229
} else {
183-
message += " '\(missingToken.text)'"
230+
return "\(partDescriptions.joined(separator: ", "))"
184231
}
185-
if let parent = missingToken.parent, let parentTypeName = parent.nodeTypeNameForDiagnostics() {
232+
}
233+
234+
/// If applicable, returns a string that describes after which node the nodes are expected.
235+
private var afterClause: String? {
236+
if !missingNodes.first!.is(TokenSyntax.self) {
237+
if let missingDecl = missingNodes.first?.as(MissingDeclSyntax.self), let lastModifier = missingDecl.modifiers?.last {
238+
return "after '\(lastModifier.name.text)' modifier"
239+
} else if let missingDecl = missingNodes.first?.as(MissingDeclSyntax.self), missingDecl.attributes != nil {
240+
return "after attribute"
241+
} else if let previousToken = missingNodes.first?.previousToken(viewMode: .fixedUp), previousToken.presence == .present {
242+
return "after '\(previousToken.text)'"
243+
}
244+
}
245+
return nil
246+
}
247+
248+
/// If applicable, returns a string that describes the node in which the missing nodes are expected.
249+
private var parentContextClause: String? {
250+
enum ContextType {
251+
case toStart
252+
case `in`
253+
case toEnd
254+
}
255+
var nodePosition = ContextType.in
256+
257+
if let missingToken = missingNodes.first?.as(TokenSyntax.self), let commonParent = commonParent {
186258
switch missingToken.tokenKind {
187-
case .leftAngle, .leftBrace, .leftParen, .leftSquareBracket:
188-
if parent.children(viewMode: .fixedUp).first?.as(TokenSyntax.self) == missingToken {
189-
message += " to start \(parentTypeName)"
259+
case .leftBrace, .leftAngle, .leftParen, .leftSquareBracket:
260+
if commonParent.children(viewMode: .fixedUp).first?.as(TokenSyntax.self) == missingToken {
261+
nodePosition = .toStart
190262
}
191-
case .rightAngle, .rightBrace, .rightParen, .rightSquareBracket:
192-
if parent.children(viewMode: .fixedUp).last?.as(TokenSyntax.self) == missingToken {
193-
message += " to end \(parentTypeName)"
263+
default:
264+
break
265+
}
266+
}
267+
if let missingToken = missingNodes.last?.as(TokenSyntax.self), let commonParent = commonParent {
268+
switch missingToken.tokenKind {
269+
case .rightBrace, .rightAngle, .rightParen, .rightSquareBracket:
270+
if commonParent.children(viewMode: .fixedUp).last?.as(TokenSyntax.self) == missingToken {
271+
if nodePosition == .toStart {
272+
// If the missing tokens encomposs both the start and end, emit an 'in' context clause
273+
nodePosition = .in
274+
} else {
275+
nodePosition = .toEnd
276+
}
194277
}
195278
default:
196-
message += " in \(parentTypeName)"
279+
break
197280
}
198281
}
199-
return message
282+
if let commonParent = commonParent, let parentTypeName = commonParent.nodeTypeNameForDiagnostics(allowSourceFile: false) {
283+
switch nodePosition {
284+
case .toStart:
285+
return "to start \(parentTypeName)"
286+
case .in:
287+
return "in \(parentTypeName)"
288+
case .toEnd:
289+
return "to end \(parentTypeName)"
290+
}
291+
}
292+
return nil
200293
}
201294

202-
public var handledNodes: [Syntax] {
203-
if let previous = missingToken.previousToken(viewMode: .all), previous.parent!.is(UnexpectedNodesSyntax.self) {
204-
return [Syntax(previous.parent!)]
295+
public var message: String {
296+
var message = "Expected \(self.missingNodesDescription)"
297+
if let afterClause = afterClause {
298+
message += " \(afterClause)"
299+
}
300+
if let parentContextClause = parentContextClause {
301+
message += " \(parentContextClause)"
205302
}
206-
return []
303+
return message
304+
}
305+
}
306+
307+
public struct MissingAttributeArgument: ParserError {
308+
/// The name of the attribute that's missing the argument, without `@`.
309+
public let attributeName: TokenSyntax
310+
311+
public var message: String {
312+
return "Expected argument for '@\(attributeName)' attribute"
207313
}
208314
}
209315

Sources/SwiftSyntax/Syntax.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,31 @@ public extension SyntaxProtocol {
207207
return parent != nil
208208
}
209209

210+
func previousSibling(viewMode: SyntaxTreeViewMode) -> Syntax? {
211+
guard let parent = self.parent else {
212+
return nil
213+
}
214+
let siblings = NonNilRawSyntaxChildren(parent, viewMode: viewMode)
215+
if let previousAbsoluteRawSibling = siblings[..<self.index].last {
216+
return Syntax(SyntaxData(previousAbsoluteRawSibling, parent: parent))
217+
} else {
218+
return nil
219+
}
220+
}
221+
222+
func nextSibling(viewMode: SyntaxTreeViewMode) -> Syntax? {
223+
guard let parent = self.parent else {
224+
return nil
225+
}
226+
let siblings = NonNilRawSyntaxChildren(parent, viewMode: viewMode)
227+
let nextSiblingIndex = siblings.index(after: self.index)
228+
if let previousAbsoluteRawSibling = siblings[nextSiblingIndex...].last {
229+
return Syntax(SyntaxData(previousAbsoluteRawSibling, parent: parent))
230+
} else {
231+
return nil
232+
}
233+
}
234+
210235
/// Recursively walks through the tree to find the token semantically before
211236
/// this node.
212237
var previousToken: TokenSyntax? {

Sources/SwiftSyntax/SyntaxKind.swift.gyb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public enum SyntaxKind {
2727
case ${node.swift_syntax_kind}
2828
% end
2929

30-
var isSyntaxCollection: Bool {
30+
public var isSyntaxCollection: Bool {
3131
switch self {
3232
% for node in SYNTAX_NODES:
3333
% if node.base_kind == "SyntaxCollection":
@@ -39,7 +39,7 @@ public enum SyntaxKind {
3939
}
4040
}
4141

42-
var isUnknown: Bool {
42+
public var isUnknown: Bool {
4343
switch self {
4444
% for name in SYNTAX_BASE_KINDS:
4545
% if name not in ["Syntax", "SyntaxCollection"]:
@@ -51,7 +51,7 @@ public enum SyntaxKind {
5151
}
5252
}
5353

54-
var isMissing: Bool {
54+
public var isMissing: Bool {
5555
switch self {
5656
% for name in SYNTAX_BASE_KINDS:
5757
% if name not in ["Syntax", "SyntaxCollection"]:

Sources/SwiftSyntax/gyb_generated/SyntaxKind.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ public enum SyntaxKind {
288288
case availabilityVersionRestriction
289289
case versionTuple
290290

291-
var isSyntaxCollection: Bool {
291+
public var isSyntaxCollection: Bool {
292292
switch self {
293293
case .codeBlockItemList: return true
294294
case .unexpectedNodes: return true
@@ -339,7 +339,7 @@ public enum SyntaxKind {
339339
}
340340
}
341341

342-
var isUnknown: Bool {
342+
public var isUnknown: Bool {
343343
switch self {
344344
case .unknownDecl: return true
345345
case .unknownExpr: return true
@@ -351,7 +351,7 @@ public enum SyntaxKind {
351351
}
352352
}
353353

354-
var isMissing: Bool {
354+
public var isMissing: Bool {
355355
switch self {
356356
case .missingDecl: return true
357357
case .missingExpr: return true

0 commit comments

Comments
 (0)