Skip to content

Commit 1289d74

Browse files
committed
Merge missing token diagnostics if they occur at the same source location
1 parent 9348629 commit 1289d74

15 files changed

+531
-258
lines changed

Sources/SwiftParser/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ add_library(SwiftParser STATIC
3030
TriviaParser.swift
3131
Types.swift
3232

33+
Diagnostics/DiagnosticExtensions.swift
34+
Diagnostics/MissingNodesError.swift
3335
Diagnostics/ParserDiagnosticMessages.swift
3436
Diagnostics/ParseDiagnosticsGenerator.swift
3537
Diagnostics/PresenceUtils.swift
38+
Diagnostics/SyntaxExtensions.swift
39+
Diagnostics/Utils.swift
3640

3741
gyb_generated/DeclarationAttribute.swift
3842
gyb_generated/DeclarationModifier.swift
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===--- DiagnosticExtensions.swift ---------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftDiagnostics
14+
import SwiftSyntax
15+
16+
extension FixIt {
17+
init(message: StaticParserFixIt, changes: [Change]) {
18+
self.init(message: message as FixItMessage, changes: changes)
19+
}
20+
}
21+
22+
extension FixIt.Change {
23+
/// Replaced a present node with a missing node
24+
static func makeMissing(node: TokenSyntax) -> FixIt.Change {
25+
assert(node.presence == .present)
26+
return .replace(
27+
oldNode: Syntax(node),
28+
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: [], trailingTrivia: [], presence: .missing))
29+
)
30+
}
31+
32+
static func makePresent<T: SyntaxProtocol>(node: T) -> FixIt.Change {
33+
return .replace(
34+
oldNode: Syntax(node),
35+
newNode: PresentMaker().visit(Syntax(node))
36+
)
37+
}
38+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
//===--- MissingNodesError.swift ------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftDiagnostics
14+
@_spi(RawSyntax) import SwiftSyntax
15+
import SwiftBasicFormat
16+
17+
// MARK: - Shared code
18+
19+
/// Returns a string that describes `missingNodes`.
20+
/// `missingNodes` are expected to all be children of `commonParent`.
21+
private func missingNodesDescription(missingNodes: [Syntax], commonParent: Syntax?) -> String {
22+
assert(missingNodes.allSatisfy({ $0.parent == commonParent }))
23+
24+
// If all tokens in the parent are missing, return the parent type name.
25+
if let commonParent = commonParent,
26+
commonParent.isMissingAllTokens,
27+
let firstToken = commonParent.firstToken(viewMode: .all),
28+
let lastToken = commonParent.lastToken(viewMode: .all),
29+
missingNodes.contains(Syntax(firstToken)),
30+
missingNodes.contains(Syntax(lastToken)) {
31+
switch commonParent.as(SyntaxEnum.self) {
32+
case .codeBlock:
33+
return "code block"
34+
case .memberDeclBlock:
35+
return "member block"
36+
default:
37+
if let nodeTypeName = commonParent.nodeTypeNameForDiagnostics {
38+
return nodeTypeName
39+
}
40+
}
41+
}
42+
43+
enum DescriptionPart {
44+
case tokensWithDefaultText([TokenSyntax])
45+
case tokenWithoutDefaultText(RawTokenKind)
46+
case node(Syntax)
47+
48+
var description: String {
49+
switch self {
50+
case .tokensWithDefaultText(let tokens):
51+
let tokenContents = tokens.map({ Format().format(syntax: $0).description }).joined()
52+
return "'\(tokenContents.trimmingSpaces())'"
53+
case .tokenWithoutDefaultText(let tokenKind):
54+
return tokenKind.nameForDiagnostics
55+
case .node(let node):
56+
if let parent = node.parent,
57+
let childName = parent.childNameForDiagnostics(node.index) {
58+
return "\(childName)"
59+
} else {
60+
return "\(node.nodeTypeNameForDiagnostics ?? "syntax")"
61+
}
62+
}
63+
}
64+
}
65+
66+
var parts: [DescriptionPart] = []
67+
for missingNode in missingNodes {
68+
if let missingToken = missingNode.as(TokenSyntax.self) {
69+
let newPart: DescriptionPart
70+
let (rawKind, text) = missingToken.tokenKind.decomposeToRaw()
71+
if let text = text, !text.isEmpty {
72+
let presentToken = TokenSyntax(missingToken.tokenKind, presence: .present)
73+
newPart = .tokensWithDefaultText([presentToken])
74+
} else {
75+
if let defaultText = rawKind.defaultText {
76+
let newKind = TokenKind.fromRaw(kind: rawKind, text: String(syntaxText: defaultText))
77+
let presentToken = TokenSyntax(newKind, presence: .present)
78+
newPart = .tokensWithDefaultText([presentToken])
79+
} else {
80+
newPart = .tokenWithoutDefaultText(rawKind)
81+
}
82+
}
83+
84+
switch (parts.last, newPart) {
85+
case (.tokensWithDefaultText(let previousTokens), .tokensWithDefaultText(let newTokens)):
86+
parts[parts.count - 1] = .tokensWithDefaultText(previousTokens + newTokens)
87+
default:
88+
parts.append(newPart)
89+
}
90+
} else {
91+
parts.append(.node(missingNode))
92+
}
93+
}
94+
let partDescriptions = parts.map({ $0.description })
95+
switch partDescriptions.count {
96+
case 0:
97+
return "syntax"
98+
case 1:
99+
return "\(partDescriptions.first!)"
100+
default:
101+
return "\(partDescriptions[0..<partDescriptions.count - 1].joined(separator: ", ")) and \(partDescriptions.last!)"
102+
}
103+
}
104+
105+
// MARK: - Error
106+
107+
public struct MissingNodesError: ParserError {
108+
public let missingNodes: [Syntax]
109+
public let commonParent: Syntax?
110+
111+
init(missingNodes: [Syntax]) {
112+
assert(!missingNodes.isEmpty)
113+
self.missingNodes = missingNodes
114+
self.commonParent = missingNodes.first?.parent
115+
assert(missingNodes.allSatisfy({ $0.parent == self.commonParent }))
116+
}
117+
118+
/// If applicable, returns a string that describes after which node the nodes are expected.
119+
private var afterClause: String? {
120+
guard let firstMissingNode = missingNodes.first else {
121+
return nil
122+
}
123+
if let missingDecl = firstMissingNode.as(MissingDeclSyntax.self) {
124+
if let lastModifier = missingDecl.modifiers?.last {
125+
return "after '\(lastModifier.name.text)' modifier"
126+
} else if missingDecl.attributes != nil {
127+
return "after attribute"
128+
}
129+
}
130+
131+
// The after clause only provides value if the first missing node is not a token.
132+
// TODO: Revisit whether we want to have this clause at all.
133+
if !firstMissingNode.is(TokenSyntax.self) {
134+
if let previousToken = firstMissingNode.previousToken(viewMode: .fixedUp), previousToken.presence == .present {
135+
return "after '\(previousToken.text)'"
136+
}
137+
}
138+
return nil
139+
}
140+
141+
/// If applicable, returns a string that describes the node in which the missing nodes are expected.
142+
private var parentContextClause: String? {
143+
// anchorParent is the first parent that has a type name for diagnostics.
144+
guard let anchorParent = commonParent?.ancestorOrSelf(where: { $0.nodeTypeNameForDiagnostics != nil }) else {
145+
return nil
146+
}
147+
let anchorTypeName = anchorParent.nodeTypeNameForDiagnostics!
148+
if anchorParent.is(SourceFileSyntax.self) {
149+
return nil
150+
}
151+
152+
var isFirstTokenStartMarker: Bool
153+
switch missingNodes.first?.as(TokenSyntax.self)?.tokenKind {
154+
case .leftBrace, .leftAngle, .leftParen, .leftSquareBracket:
155+
isFirstTokenStartMarker = true
156+
default:
157+
isFirstTokenStartMarker = false
158+
}
159+
160+
var isLastTokenEndMarker: Bool
161+
switch missingNodes.last?.as(TokenSyntax.self)?.tokenKind {
162+
case .rightBrace, .rightAngle, .rightParen, .rightSquareBracket, .stringQuote, .multilineStringQuote, .rawStringDelimiter(_):
163+
isLastTokenEndMarker = true
164+
default:
165+
isLastTokenEndMarker = false
166+
}
167+
168+
switch (isFirstTokenStartMarker, isLastTokenEndMarker) {
169+
case (true, false) where Syntax(anchorParent.firstToken(viewMode: .all)) == missingNodes.first:
170+
return "to start \(anchorTypeName)"
171+
case (false, true) where Syntax(anchorParent.lastToken(viewMode: .all)) == missingNodes.last:
172+
return "to end \(anchorTypeName)"
173+
default:
174+
return "in \(anchorTypeName)"
175+
}
176+
}
177+
178+
public var message: String {
179+
var message = "Expected \(missingNodesDescription(missingNodes: missingNodes, commonParent: commonParent))"
180+
if let afterClause = afterClause {
181+
message += " \(afterClause)"
182+
}
183+
if let parentContextClause = parentContextClause {
184+
message += " \(parentContextClause)"
185+
}
186+
return message
187+
}
188+
}
189+
190+
// MARK: - Fix-It
191+
192+
public struct InsertTokenFixIt: ParserFixIt {
193+
public let missingNodes: [Syntax]
194+
public let commonParent: Syntax?
195+
196+
init(missingNodes: [Syntax]) {
197+
assert(!missingNodes.isEmpty)
198+
self.missingNodes = missingNodes
199+
self.commonParent = missingNodes.first?.parent
200+
assert(missingNodes.allSatisfy({ $0.parent == self.commonParent }))
201+
}
202+
203+
public var message: String { "Insert \(missingNodesDescription(missingNodes: missingNodes, commonParent: commonParent))" }
204+
}
205+
206+
// MARK: - Generate Error
207+
208+
extension ParseDiagnosticsGenerator {
209+
func handleMissingSyntax<T: SyntaxProtocol>(_ node: T) -> SyntaxVisitorContinueKind {
210+
if shouldSkip(node) {
211+
return .skipChildren
212+
}
213+
214+
// Walk all upcoming sibling to see if they are also missing to handle them in this diagnostic.
215+
// If this is the case, handle all of them in this diagnostic.
216+
var missingNodes = [Syntax(node)]
217+
if let parent = node.parent {
218+
let siblings = parent.children(viewMode: .all)
219+
let siblingsAfter = siblings[siblings.index(after: node.index)...]
220+
for sibling in siblingsAfter {
221+
if sibling.as(TokenSyntax.self)?.presence == .missing {
222+
// Handle missing sibling tokens
223+
missingNodes += [sibling]
224+
} else if sibling.raw.kind.isMissing {
225+
// Handle missing sibling nodes (e.g. MissingDeclSyntax)
226+
missingNodes += [sibling]
227+
} else if sibling.isCollection && sibling.children(viewMode: .sourceAccurate).count == 0 {
228+
// Skip over any syntax collections without any elements while looking ahead for further missing nodes.
229+
} else {
230+
// Otherwise we have found a present node, so stop looking ahead.
231+
break
232+
}
233+
}
234+
} else {
235+
missingNodes = []
236+
}
237+
238+
let fixIt = FixIt(
239+
message: InsertTokenFixIt(missingNodes: missingNodes),
240+
changes: missingNodes.map(FixIt.Change.makePresent)
241+
)
242+
243+
addDiagnostic(
244+
node,
245+
position: node.endPosition,
246+
MissingNodesError(missingNodes: missingNodes),
247+
fixIts: [fixIt],
248+
handledNodes: missingNodes.map(\.id)
249+
)
250+
return .visitChildren
251+
}
252+
}

0 commit comments

Comments
 (0)