Skip to content

Commit a4b1728

Browse files
committed
Improve diagnostic in testMissingArgumentToAttribute
1 parent c9da601 commit a4b1728

File tree

10 files changed

+152
-32
lines changed

10 files changed

+152
-32
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ let package = Package(
107107
),
108108
.target(
109109
name: "SwiftParser",
110-
dependencies: ["SwiftDiagnostics", "SwiftSyntax"],
110+
dependencies: ["SwiftDiagnostics", "SwiftSyntax", "SwiftSyntaxBuilder"],
111111
exclude: [
112112
"CMakeLists.txt",
113113
"README.md",

Sources/SwiftParser/Attributes.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -684,14 +684,21 @@ extension Parser {
684684
let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen)
685685
let label: RawTokenSyntax
686686
if self.currentToken.tokenText == "for" {
687-
label = self.consumeAnyToken()
687+
label = self.consume(remapping: .identifier)
688688
} else {
689-
label = RawTokenSyntax(missing: .forKeyword, arena: self.arena)
689+
label = RawTokenSyntax(missing: .identifier, text: "for", arena: self.arena)
690690
}
691691
let (unexpectedBeforeColon, colon) = self.expect(.colon)
692-
let (base, args) = self.parseDeclNameRef([
693-
.zeroArgCompoundNames, .keywordsUsingSpecialNames, .operators,
694-
])
692+
let base: RawTokenSyntax
693+
let args: RawDeclNameArgumentsSyntax?
694+
if label.isMissing && colon.isMissing && self.currentToken.isAtStartOfLine {
695+
base = RawTokenSyntax(missing: .identifier, arena: self.arena)
696+
args = nil
697+
} else {
698+
(base, args) = self.parseDeclNameRef([
699+
.zeroArgCompoundNames, .keywordsUsingSpecialNames, .operators,
700+
])
701+
}
695702
let method = RawDeclNameSyntax(declBaseName: RawSyntax(base), declNameArguments: args, arena: self.arena)
696703
let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
697704
return RawAttributeSyntax(

Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 32 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

@@ -109,13 +114,33 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
109114
return .skipChildren
110115
}
111116
if node.presence == .missing {
112-
addDiagnostic(node, MissingTokenError(missingToken: node))
117+
addDiagnostic(node, MissingTokenError(missingToken: node), fixIts: [
118+
FixIt(message: InsertTokenFixIt(missingToken: node), changes: [
119+
.makePresent(node: node)
120+
])
121+
])
113122
}
114123
return .skipChildren
115124
}
116125

117126
// MARK: - Specialized diagnostic generation
118127

128+
public override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind {
129+
if shouldSkip(node) {
130+
return .skipChildren
131+
}
132+
if let argument = node.argument, argument.isMissingAllTokens {
133+
addDiagnostic(argument, MissingAttributeArgument(attributeName: node.attributeName), fixIts: [
134+
FixIt(message: .insertAttributeArguments, changes: [
135+
.makePresent(node: argument)
136+
])
137+
])
138+
markNodesAsHandled(argument.id)
139+
return .visitChildren
140+
}
141+
return .visitChildren
142+
}
143+
119144
public override func visit(_ node: ForInStmtSyntax) -> SyntaxVisitorContinueKind {
120145
if shouldSkip(node) {
121146
return .skipChildren
@@ -153,9 +178,9 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
153178
let unexpectedBeforeReturnType = output.unexpectedBetweenArrowAndReturnType,
154179
let throwsInReturnPosition = unexpectedBeforeReturnType.tokens(withKind: .throwsKeyword).first {
155180
addDiagnostic(throwsInReturnPosition, .throwsInReturnPosition, fixIts: [
156-
FixIt(message: StaticParserFixIt.moveThrowBeforeArrow, changes: [
181+
FixIt(message: .moveThrowBeforeArrow, changes: [
157182
.makeMissing(node: throwsInReturnPosition),
158-
.makePresent(node: missingThrowsKeyword, trailingTrivia: .space),
183+
.makePresent(node: missingThrowsKeyword),
159184
])
160185
])
161186
markNodesAsHandled(unexpectedBeforeReturnType.id, missingThrowsKeyword.id, throwsInReturnPosition.id)
@@ -164,7 +189,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
164189
return .visitChildren
165190
}
166191

167-
override public func visit(_ node: ParameterClauseSyntax) -> SyntaxVisitorContinueKind {
192+
public override func visit(_ node: ParameterClauseSyntax) -> SyntaxVisitorContinueKind {
168193
if shouldSkip(node) {
169194
return .skipChildren
170195
}

Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public extension ParserFixIt {
8080
}
8181
}
8282

83-
// MARK: - Static diagnostics
83+
// MARK: - Errors (please sort alphabetically)
8484

8585
/// Please order the cases in this enum alphabetically by case name.
8686
public enum StaticParserError: String, DiagnosticMessage {
@@ -98,16 +98,6 @@ public enum StaticParserError: String, DiagnosticMessage {
9898
public var severity: DiagnosticSeverity { .error }
9999
}
100100

101-
public enum StaticParserFixIt: String, FixItMessage {
102-
case moveThrowBeforeArrow = "Move 'throws' before '->'"
103-
104-
public var message: String { self.rawValue }
105-
106-
public var fixItID: MessageID {
107-
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(self)")
108-
}
109-
}
110-
111101
// MARK: - Diagnostics (please sort alphabetically)
112102

113103
public struct ExtaneousCodeAtTopLevel: ParserError {
@@ -122,6 +112,15 @@ public struct ExtaneousCodeAtTopLevel: ParserError {
122112
}
123113
}
124114

115+
public struct MissingAttributeArgument: ParserError {
116+
/// The name of the attribute that's missing the argument, without `@`.
117+
public let attributeName: TokenSyntax
118+
119+
public var message: String {
120+
return "Expected argument for '@\(attributeName)' attribute"
121+
}
122+
}
123+
125124
public struct MissingTokenError: ParserError {
126125
public let missingToken: TokenSyntax
127126

@@ -168,3 +167,22 @@ public struct UnexpectedNodesError: ParserError {
168167
return message
169168
}
170169
}
170+
171+
// MARK: - Fix-Its (please sort alphabetically)
172+
173+
public enum StaticParserFixIt: String, FixItMessage {
174+
case insertAttributeArguments = "Insert attribute argument"
175+
case moveThrowBeforeArrow = "Move 'throws' before '->'"
176+
177+
public var message: String { self.rawValue }
178+
179+
public var fixItID: MessageID {
180+
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(self)")
181+
}
182+
}
183+
184+
public struct InsertTokenFixIt: ParserFixIt {
185+
let missingToken: TokenSyntax
186+
187+
public var message: String { "Insert '\(missingToken.text)'" }
188+
}
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 struct SwiftSyntaxBuilder.Format
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/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
@@ -1249,7 +1249,8 @@ public enum RawTokenKind: Equatable, Hashable {
12491249
case stringInterpolationAnchor
12501250
case yield
12511251

1252-
var defaultText: SyntaxText? {
1252+
@_spi(RawSyntax)
1253+
public var defaultText: SyntaxText? {
12531254
switch self {
12541255
case .eof: return ""
12551256
case .associatedtypeKeyword: return "associatedtype"

Sources/SwiftSyntaxBuilder/generated/Format.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1845,7 +1845,7 @@ extension Format {
18451845
}
18461846
return result
18471847
}
1848-
func format(syntax: TokenSyntax) -> TokenSyntax {
1848+
public func format(syntax: TokenSyntax) -> TokenSyntax {
18491849
switch syntax.tokenKind {
18501850
case .associatedtypeKeyword:
18511851
return syntax.withTrailingTrivia(.space)

Sources/generate-swift-syntax-builder/Templates/FormatFile.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ private func createBuildableCollectionNodeFormatFunction(node: Node) -> Function
229229
private func createTokenFormatFunction() -> FunctionDecl {
230230
let tokenType = SyntaxBuildableType(syntaxKind: "Token")
231231
return FunctionDecl(
232+
modifiers: Token.public,
232233
identifier: .identifier("format"),
233234
signature: createFormatFunctionSignature(type: tokenType)
234235
) {

Tests/SwiftParserTest/Attributes.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +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_1", message: "Expected 'for' in attribute argument"),
15-
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected ':' in attribute argument"),
16-
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected ')' to end attribute"),
17-
]
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+
"""
1822
)
1923
}
2024

0 commit comments

Comments
 (0)