|
12 | 12 |
|
13 | 13 | import SwiftDiagnostics
|
14 | 14 | @_spi(RawSyntax) import SwiftSyntax
|
| 15 | +import SwiftBasicFormat |
15 | 16 |
|
16 | 17 | let diagnosticDomain: String = "SwiftParser"
|
17 | 18 |
|
@@ -131,79 +132,184 @@ public struct InvalidIdentifierError: ParserError {
|
131 | 132 | }
|
132 | 133 | }
|
133 | 134 |
|
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) |
161 | 142 | }
|
162 |
| - return message |
| 143 | + return String(result) |
163 | 144 | }
|
164 | 145 | }
|
165 | 146 |
|
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? |
169 | 150 |
|
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 |
172 | 154 | }
|
173 |
| -} |
174 | 155 |
|
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 | + } |
177 | 162 |
|
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!)" |
182 | 229 | } else {
|
183 |
| - message += " '\(missingToken.text)'" |
| 230 | + return "\(partDescriptions.joined(separator: ", "))" |
184 | 231 | }
|
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 { |
186 | 258 | 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 |
190 | 262 | }
|
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 | + } |
194 | 277 | }
|
195 | 278 | default:
|
196 |
| - message += " in \(parentTypeName)" |
| 279 | + break |
197 | 280 | }
|
198 | 281 | }
|
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 |
200 | 293 | }
|
201 | 294 |
|
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)" |
205 | 302 | }
|
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" |
207 | 313 | }
|
208 | 314 | }
|
209 | 315 |
|
|
0 commit comments