Skip to content

Commit 9d53a62

Browse files
committed
Add Fix-Its for missing tokens and improve diagnostic in testMissingArgumentToAttribute
1 parent a3cdd02 commit 9d53a62

File tree

10 files changed

+165
-36
lines changed

10 files changed

+165
-36
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ let package = Package(
115115
),
116116
.target(
117117
name: "SwiftParser",
118-
dependencies: ["SwiftDiagnostics", "SwiftSyntax"],
118+
dependencies: ["SwiftBasicFormat", "SwiftDiagnostics", "SwiftSyntax"],
119119
exclude: [
120120
"CMakeLists.txt",
121121
"README.md",

Sources/SwiftParser/Attributes.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -685,11 +685,18 @@ extension Parser {
685685
let (unexpectedBeforeAtSign, atSign) = self.expect(.atSign)
686686
let (unexpectedBeforeDynamicReplacementToken, dynamicReplacementToken) = self.expectContextualKeyword("_dynamicReplacement")
687687
let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen)
688-
let (unexpectedBeforeLabel, label) = self.expect(.forKeyword)
688+
let (unexpectedBeforeLabel, label) = self.expect(.forKeyword, remapping: .identifier)
689689
let (unexpectedBeforeColon, colon) = self.expect(.colon)
690-
let (base, args) = self.parseDeclNameRef([
691-
.zeroArgCompoundNames, .keywordsUsingSpecialNames, .operators,
692-
])
690+
let base: RawTokenSyntax
691+
let args: RawDeclNameArgumentsSyntax?
692+
if label.isMissing && colon.isMissing && self.currentToken.isAtStartOfLine {
693+
base = RawTokenSyntax(missing: .identifier, arena: self.arena)
694+
args = nil
695+
} else {
696+
(base, args) = self.parseDeclNameRef([
697+
.zeroArgCompoundNames, .keywordsUsingSpecialNames, .operators,
698+
])
699+
}
693700
let method = RawDeclNameSyntax(declBaseName: RawSyntax(base), declNameArguments: args, arena: self.arena)
694701
let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
695702
return RawAttributeSyntax(

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,20 @@ fileprivate extension FixIt.Change {
3333
)
3434
}
3535

36-
static func makePresent(node: TokenSyntax, leadingTrivia: Trivia = [], trailingTrivia: Trivia = []) -> FixIt.Change {
37-
assert(node.presence == .missing)
36+
static func makePresent<T: SyntaxProtocol>(node: T) -> FixIt.Change {
3837
return .replace(
3938
oldNode: Syntax(node),
40-
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia, presence: .present))
39+
newNode: PresentMaker().visit(Syntax(node))
4140
)
4241
}
4342
}
4443

44+
fileprivate extension FixIt {
45+
init(message: StaticParserFixIt, changes: [Change]) {
46+
self.init(message: message as FixItMessage, changes: changes)
47+
}
48+
}
49+
4550
public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
4651
private var diagnostics: [Diagnostic] = []
4752

@@ -112,7 +117,11 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
112117
let previousParent = invalidIdentifier.parent?.as(UnexpectedNodesSyntax.self) {
113118
addDiagnostic(invalidIdentifier, InvalidIdentifierError(invalidIdentifier: invalidIdentifier), handledNodes: [previousParent.id])
114119
} else {
115-
addDiagnostic(node, MissingTokenError(missingToken: node))
120+
addDiagnostic(node, MissingTokenError(missingToken: node), fixIts: [
121+
FixIt(message: InsertTokenFixIt(missingToken: node), changes: [
122+
.makePresent(node: node)
123+
])
124+
])
116125
}
117126
}
118127
return .skipChildren
@@ -152,6 +161,21 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
152161
return handleMissingSyntax(node)
153162
}
154163

164+
public override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind {
165+
if shouldSkip(node) {
166+
return .skipChildren
167+
}
168+
if let argument = node.argument, argument.isMissingAllTokens {
169+
addDiagnostic(argument, MissingAttributeArgument(attributeName: node.attributeName), fixIts: [
170+
FixIt(message: .insertAttributeArguments, changes: [
171+
.makePresent(node: argument)
172+
])
173+
], handledNodes: [argument.id])
174+
return .visitChildren
175+
}
176+
return .visitChildren
177+
}
178+
155179
public override func visit(_ node: ForInStmtSyntax) -> SyntaxVisitorContinueKind {
156180
if shouldSkip(node) {
157181
return .skipChildren
@@ -193,17 +217,17 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
193217
let unexpectedBeforeReturnType = output.unexpectedBetweenArrowAndReturnType,
194218
let throwsInReturnPosition = unexpectedBeforeReturnType.tokens(withKind: .throwsKeyword).first {
195219
addDiagnostic(throwsInReturnPosition, .throwsInReturnPosition, fixIts: [
196-
FixIt(message: StaticParserFixIt.moveThrowBeforeArrow, changes: [
220+
FixIt(message: .moveThrowBeforeArrow, changes: [
197221
.makeMissing(node: throwsInReturnPosition),
198-
.makePresent(node: missingThrowsKeyword, trailingTrivia: .space),
222+
.makePresent(node: missingThrowsKeyword),
199223
])
200224
], handledNodes: [unexpectedBeforeReturnType.id, missingThrowsKeyword.id, throwsInReturnPosition.id])
201225
return .visitChildren
202226
}
203227
return .visitChildren
204228
}
205229

206-
override public func visit(_ node: ParameterClauseSyntax) -> SyntaxVisitorContinueKind {
230+
public override func visit(_ node: ParameterClauseSyntax) -> SyntaxVisitorContinueKind {
207231
if shouldSkip(node) {
208232
return .skipChildren
209233
}

Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public extension ParserFixIt {
8686
}
8787
}
8888

89-
// MARK: - Static diagnostics
89+
// MARK: - Errors (please sort alphabetically)
9090

9191
/// Please order the cases in this enum alphabetically by case name.
9292
public enum StaticParserError: String, DiagnosticMessage {
@@ -104,16 +104,6 @@ public enum StaticParserError: String, DiagnosticMessage {
104104
public var severity: DiagnosticSeverity { .error }
105105
}
106106

107-
public enum StaticParserFixIt: String, FixItMessage {
108-
case moveThrowBeforeArrow = "Move 'throws' before '->'"
109-
110-
public var message: String { self.rawValue }
111-
112-
public var fixItID: MessageID {
113-
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(self)")
114-
}
115-
}
116-
117107
// MARK: - Diagnostics (please sort alphabetically)
118108

119109
public struct ExtaneousCodeAtTopLevel: ParserError {
@@ -148,7 +138,7 @@ public struct MissingNodeError: ParserError {
148138
var message: String
149139
var hasNamedParent = false
150140
if let parent = missingNode.parent,
151-
let childName = parent.childNameForDiagnostics(missingNode.index) {
141+
let childName = parent.childNameForDiagnostics(missingNode.index) {
152142
message = "Expected \(childName)"
153143
if let parentTypeName = parent.nodeTypeNameForDiagnostics(inherit: false) {
154144
message += " of \(parentTypeName)"
@@ -173,6 +163,15 @@ public struct MissingNodeError: ParserError {
173163
}
174164
}
175165

166+
public struct MissingAttributeArgument: ParserError {
167+
/// The name of the attribute that's missing the argument, without `@`.
168+
public let attributeName: TokenSyntax
169+
170+
public var message: String {
171+
return "Expected argument for '@\(attributeName)' attribute"
172+
}
173+
}
174+
176175
public struct MissingTokenError: ParserError {
177176
public let missingToken: TokenSyntax
178177

@@ -226,3 +225,22 @@ public struct UnexpectedNodesError: ParserError {
226225
return message
227226
}
228227
}
228+
229+
// MARK: - Fix-Its (please sort alphabetically)
230+
231+
public enum StaticParserFixIt: String, FixItMessage {
232+
case insertAttributeArguments = "Insert attribute argument"
233+
case moveThrowBeforeArrow = "Move 'throws' before '->'"
234+
235+
public var message: String { self.rawValue }
236+
237+
public var fixItID: MessageID {
238+
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(self)")
239+
}
240+
}
241+
242+
public struct InsertTokenFixIt: ParserFixIt {
243+
let missingToken: TokenSyntax
244+
245+
public var message: String { "Insert '\(missingToken.text)'" }
246+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//===--- PresenceUtils.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+
@_spi(RawSyntax) import SwiftSyntax
14+
import SwiftBasicFormat
15+
16+
/// Walks a tree and checks whether the tree contained any present tokens.
17+
class PresentNodeChecker: SyntaxAnyVisitor {
18+
var hasPresentToken: Bool = false
19+
20+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
21+
if hasPresentToken {
22+
// If we already saw a present token, we don't need to continue.
23+
return .skipChildren
24+
} else {
25+
return .visitChildren
26+
}
27+
}
28+
29+
override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind {
30+
if node.presence == .present {
31+
hasPresentToken = true
32+
}
33+
return .visitChildren
34+
}
35+
}
36+
37+
extension SyntaxProtocol {
38+
/// Returns `true` if all tokens nodes in this tree are missing.
39+
var isMissingAllTokens: Bool {
40+
let checker = PresentNodeChecker(viewMode: .all)
41+
checker.walk(Syntax(self))
42+
return !checker.hasPresentToken
43+
}
44+
}
45+
46+
/// Transforms a syntax tree by making all missing tokens present.
47+
class PresentMaker: SyntaxRewriter {
48+
override func visit(_ token: TokenSyntax) -> Syntax {
49+
if token.presence == .missing {
50+
let presentToken: TokenSyntax
51+
let (rawKind, text) = token.tokenKind.decomposeToRaw()
52+
if let text = text, !text.isEmpty {
53+
presentToken = TokenSyntax(token.tokenKind, presence: .present)
54+
} else {
55+
let newKind = TokenKind.fromRaw(kind: rawKind, text: rawKind.defaultText.map(String.init) ?? "<#\(rawKind.nameForDiagnostics)#>")
56+
presentToken = TokenSyntax(newKind, presence: .present)
57+
}
58+
return Syntax(Format().format(syntax: presentToken))
59+
} else {
60+
return Syntax(token)
61+
}
62+
}
63+
}

Sources/SwiftParser/Parser.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,19 @@ extension Parser {
323323
/// a missing token of the requested kind.
324324
@_spi(RawSyntax)
325325
public mutating func expect(
326-
_ kind: RawTokenKind
326+
_ kind: RawTokenKind,
327+
remapping: RawTokenKind? = nil
327328
) -> (unexpected: RawUnexpectedNodesSyntax?, token: RawTokenSyntax) {
328329
return expectImpl(
329-
consume: { $0.consume(if: kind) },
330+
consume: { $0.consume(if: kind, remapping: remapping) },
330331
canRecoverTo: { $0.canRecoverTo([kind]) },
331-
makeMissing: { $0.missingToken(kind, text: nil) }
332+
makeMissing: {
333+
if let remapping = remapping {
334+
return $0.missingToken(remapping, text: kind.defaultText)
335+
} else {
336+
return $0.missingToken(kind, text: nil)
337+
}
338+
}
332339
)
333340
}
334341

Sources/SwiftParser/TokenConsumer.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,15 @@ extension TokenConsumer {
139139
@_spi(RawSyntax)
140140
public mutating func consume(
141141
if kind: RawTokenKind,
142+
remapping: RawTokenKind? = nil,
142143
where condition: (Lexer.Lexeme) -> Bool = { _ in true}
143144
) -> Token? {
144145
if self.at(kind, where: condition) {
145-
return self.consumeAnyToken()
146+
if let remapping = remapping {
147+
return self.consumeAnyToken(remapping: remapping)
148+
} else {
149+
return self.consumeAnyToken()
150+
}
146151
}
147152
return nil
148153
}

Sources/SwiftSyntax/TokenKind.swift.gyb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ public enum RawTokenKind: Equatable, Hashable {
149149
case ${token.swift_kind()}
150150
% end
151151

152-
var defaultText: SyntaxText? {
152+
@_spi(RawSyntax)
153+
public var defaultText: SyntaxText? {
153154
switch self {
154155
case .eof: return ""
155156
% for token in SYNTAX_TOKENS:

Sources/SwiftSyntax/gyb_generated/TokenKind.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1259,7 +1259,8 @@ public enum RawTokenKind: Equatable, Hashable {
12591259
case stringInterpolationAnchor
12601260
case yield
12611261

1262-
var defaultText: SyntaxText? {
1262+
@_spi(RawSyntax)
1263+
public var defaultText: SyntaxText? {
12631264
switch self {
12641265
case .eof: return ""
12651266
case .associatedtypeKeyword: return "associatedtype"

Tests/SwiftParserTest/Attributes.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@ final class AttributeTests: XCTestCase {
66
func testMissingArgumentToAttribute() {
77
AssertParse(
88
"""
9-
@_dynamicReplacement(#^DIAG_1^#
9+
@_dynamicReplacement(#^DIAG^#
1010
func #^DIAG_2^#test_dynamic_replacement_for2() {
1111
}
1212
""",
1313
diagnostics: [
14-
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected declaration after attribute"),
15-
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected 'for' in attribute argument"),
16-
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected ':' in attribute argument"),
17-
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected ')' to end attribute"),
18-
]
14+
DiagnosticSpec(message: "Expected argument for '@_dynamicReplacement' attribute", fixIts: ["Insert attribute argument"]),
15+
DiagnosticSpec(message: "Expected ')' to end attribute", fixIts: ["Insert ')'"]),
16+
],
17+
fixedSource: """
18+
@_dynamicReplacement(for: <#identifier#>)
19+
func test_dynamic_replacement_for2() {
20+
}
21+
"""
1922
)
2023
}
2124

0 commit comments

Comments
 (0)