Skip to content

Commit b6f45b7

Browse files
flashspysahoppen
authored andcommitted
Improved diagnostics for misplaced AttributeList in variable declaration
1 parent 7233193 commit b6f45b7

File tree

7 files changed

+130
-9
lines changed

7 files changed

+130
-9
lines changed

CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxRewriterFile.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,32 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
3131
"""
3232
) {
3333
DeclSyntax("public let viewMode: SyntaxTreeViewMode")
34+
DeclSyntax(
35+
"""
36+
/// The arena in which the parents of rewritten nodes should be allocated.
37+
///
38+
/// The `SyntaxRewriter` subclass is responsible for generating the rewritten nodes. To incorporate them into the
39+
/// tree, all of the rewritten node's parents also need to be re-created. This is the arena in which those
40+
/// intermediate nodes should be allocated.
41+
private let arena: SyntaxArena?
42+
"""
43+
)
3444

3545
DeclSyntax(
3646
"""
3747
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
3848
self.viewMode = viewMode
49+
self.arena = nil
50+
}
51+
"""
52+
)
53+
54+
DeclSyntax(
55+
"""
56+
@_spi(RawSyntax)
57+
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate, arena: SyntaxArena? = nil) {
58+
self.viewMode = viewMode
59+
self.arena = arena
3960
}
4061
"""
4162
)
@@ -346,7 +367,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
346367
// Sanity check, ensure the new children are the same length.
347368
precondition(newLayout.count == node.raw.layoutView!.children.count)
348369
349-
let arena = SyntaxArena()
370+
let arena = self.arena ?? SyntaxArena()
350371
let newRaw = node.raw.layoutView!.replacingLayout(with: Array(newLayout), arena: arena)
351372
// 'withExtendedLifetime' to keep 'SyntaxArena's of them alive until here.
352373
return withExtendedLifetime(rewrittens) {

Sources/SwiftParser/Declarations.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,11 +1262,33 @@ extension Parser {
12621262
let (unexpectedBeforeIntroducer, introducer) = self.eat(handle)
12631263
let hasTryBeforeIntroducer = unexpectedBeforeIntroducer?.containsToken(where: { TokenSpec(.try) ~= $0 }) ?? false
12641264

1265+
var attrs = attrs
12651266
var elements = [RawPatternBindingSyntax]()
12661267
do {
12671268
var keepGoing: RawTokenSyntax? = nil
12681269
var loopProgress = LoopProgressCondition()
12691270
repeat {
1271+
var unexpectedBeforePattern: RawUnexpectedNodesSyntax?
1272+
1273+
if self.at(.atSign), attrs.attributes.isEmpty {
1274+
let recoveredAttributes = self.parseAttributeList()
1275+
unexpectedBeforePattern = RawUnexpectedNodesSyntax(
1276+
[recoveredAttributes],
1277+
arena: self.arena
1278+
)
1279+
1280+
/// Create a version of the same attribute with all tokens missing.
1281+
class TokenMissingMaker: SyntaxRewriter {
1282+
override func visit(_ token: TokenSyntax) -> TokenSyntax {
1283+
return .init(token.tokenKind, presence: .missing)
1284+
}
1285+
}
1286+
let tokenMissingMaker = TokenMissingMaker(arena: self.arena)
1287+
let missingAttributes = tokenMissingMaker.rewrite(
1288+
Syntax(raw: RawSyntax(recoveredAttributes), rawNodeArena: arena)
1289+
).raw
1290+
attrs.attributes = missingAttributes.cast(RawAttributeListSyntax.self)
1291+
}
12701292

12711293
var (pattern, typeAnnotation) = self.parseTypedPattern()
12721294

@@ -1344,6 +1366,7 @@ extension Parser {
13441366
keepGoing = self.consume(if: .comma)
13451367
elements.append(
13461368
RawPatternBindingSyntax(
1369+
unexpectedBeforePattern,
13471370
pattern: pattern,
13481371
typeAnnotation: typeAnnotation,
13491372
initializer: initializer,

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
262262
unexpectedTokenCondition: { EffectSpecifier(token: $0) != nil },
263263
correctTokens: [effectSpecifiers?.asyncSpecifier, effectSpecifiers?.throwsClause?.throwsSpecifier],
264264
message: { EffectsSpecifierAfterArrow(effectsSpecifiersAfterArrow: $0) },
265-
moveFixIt: { MoveTokensInFrontOfFixIt(movedTokens: $0, inFrontOf: .arrow) },
265+
moveFixIt: { MoveNodesInFrontOfFixIt(movedNodes: $0, inFrontOf: .arrow) },
266266
removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) }
267267
)
268268
}
@@ -324,7 +324,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
324324
unexpectedTokenCondition: { AsyncEffectSpecifier(token: $0) != nil },
325325
correctTokens: [node.asyncSpecifier],
326326
message: { AsyncMustPrecedeThrows(asyncKeywords: $0, throwsKeyword: throwsClause.throwsSpecifier) },
327-
moveFixIt: { MoveTokensInFrontOfFixIt(movedTokens: $0, inFrontOf: throwsClause.throwsSpecifier.tokenKind) },
327+
moveFixIt: { MoveNodesInFrontOfFixIt(movedNodes: $0, inFrontOf: throwsClause.throwsSpecifier.tokenKind) },
328328
removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) }
329329
)
330330
}
@@ -2052,6 +2052,27 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
20522052
moveFixIt: { MoveTokensAfterFixIt(movedTokens: $0, after: .equal) },
20532053
removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) }
20542054
)
2055+
2056+
if node.attributes.isMissingAllTokens,
2057+
let unexpected = node.bindings.compactMap({ $0.unexpectedBeforePattern }).first,
2058+
unexpected.only?.is(AttributeListSyntax.self) ?? false
2059+
{
2060+
let fixIt = FixIt(
2061+
message: MoveNodesInFrontOfFixIt(movedNodes: [unexpected], inFrontOf: node.bindingSpecifier.tokenKind),
2062+
changes: [
2063+
.makeMissing(unexpected),
2064+
.makePresent(node.attributes, trailingTrivia: .space),
2065+
]
2066+
)
2067+
2068+
addDiagnostic(
2069+
unexpected,
2070+
.misplacedAttributeInVarDecl,
2071+
fixIts: [fixIt],
2072+
handledNodes: [node.attributes.id, unexpected.id]
2073+
)
2074+
}
2075+
20552076
return .visitChildren
20562077
}
20572078

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ extension DiagnosticMessage where Self == StaticParserError {
191191
public static var maximumNestingLevelOverflow: Self {
192192
.init("parsing has exceeded the maximum nesting level")
193193
}
194+
public static var misplacedAttributeInVarDecl: Self {
195+
.init("misplaced attribute in variable declaration")
196+
}
194197
public static var missingColonAndExprInTernaryExpr: Self {
195198
.init("expected ':' and expression after '? ...' in ternary expression")
196199
}
@@ -710,15 +713,15 @@ public struct MoveTokensAfterFixIt: ParserFixIt {
710713
}
711714
}
712715

713-
public struct MoveTokensInFrontOfFixIt: ParserFixIt {
714-
/// The token that should be moved
715-
public let movedTokens: [TokenSyntax]
716+
public struct MoveNodesInFrontOfFixIt<Node: SyntaxProtocol>: ParserFixIt {
717+
/// The nodes that should be moved
718+
public let movedNodes: [Node]
716719

717-
/// The token before which 'movedTokens' should be moved
720+
/// The token before which `movedNodes` should be moved
718721
public let inFrontOf: TokenKind
719722

720723
public var message: String {
721-
"move \(nodesDescription(movedTokens, format: false)) in front of '\(inFrontOf.nameForDiagnostics)'"
724+
"move \(nodesDescription(movedNodes, format: false)) in front of '\(inFrontOf.nameForDiagnostics)'"
722725
}
723726
}
724727

Sources/SwiftSyntax/generated/SyntaxRewriter.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,22 @@
2424
open class SyntaxRewriter {
2525
public let viewMode: SyntaxTreeViewMode
2626

27+
/// The arena in which the parents of rewritten nodes should be allocated.
28+
///
29+
/// The `SyntaxRewriter` subclass is responsible for generating the rewritten nodes. To incorporate them into the
30+
/// tree, all of the rewritten node's parents also need to be re-created. This is the arena in which those
31+
/// intermediate nodes should be allocated.
32+
private let arena: SyntaxArena?
33+
2734
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
2835
self.viewMode = viewMode
36+
self.arena = nil
37+
}
38+
39+
@_spi(RawSyntax)
40+
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate, arena: SyntaxArena? = nil) {
41+
self.viewMode = viewMode
42+
self.arena = arena
2943
}
3044

3145
/// Rewrite `node`, keeping its parent unless `detach` is `true`.
@@ -3966,7 +3980,7 @@ open class SyntaxRewriter {
39663980
// Sanity check, ensure the new children are the same length.
39673981
precondition(newLayout.count == node.raw.layoutView!.children.count)
39683982

3969-
let arena = SyntaxArena()
3983+
let arena = self.arena ?? SyntaxArena()
39703984
let newRaw = node.raw.layoutView!.replacingLayout(with: Array(newLayout), arena: arena)
39713985
// 'withExtendedLifetime' to keep 'SyntaxArena's of them alive until here.
39723986
return withExtendedLifetime(rewrittens) {

Tests/SwiftParserTest/AttributeTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,4 +1022,26 @@ final class AttributeTests: ParserTestCase {
10221022
]
10231023
)
10241024
}
1025+
1026+
func testMisplacedAttributeInVariableDecl() {
1027+
assertParse(
1028+
"""
1029+
struct A {
1030+
var 1️⃣@State name: String
1031+
}
1032+
""",
1033+
diagnostics: [
1034+
DiagnosticSpec(
1035+
message: "misplaced attribute in variable declaration",
1036+
fixIts: ["move attributes in front of 'var'"]
1037+
)
1038+
],
1039+
fixedSource:
1040+
"""
1041+
struct A {
1042+
@State var name: String
1043+
}
1044+
"""
1045+
)
1046+
}
10251047
}

Tests/SwiftParserTest/DeclarationTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3308,4 +3308,21 @@ final class DeclarationTests: ParserTestCase {
33083308
experimentalFeatures: .transferringArgsAndResults
33093309
)
33103310
}
3311+
3312+
func testMisplacedAttributeInVarDeclWithMultipleBindings() {
3313+
assertParse(
3314+
"""
3315+
let a = "a", 1️⃣@foo b = "b"
3316+
""",
3317+
diagnostics: [
3318+
DiagnosticSpec(
3319+
message: "misplaced attribute in variable declaration",
3320+
fixIts: ["move attributes in front of 'let'"]
3321+
)
3322+
],
3323+
fixedSource: """
3324+
@foo let a = "a", b = "b"
3325+
"""
3326+
)
3327+
}
33113328
}

0 commit comments

Comments
 (0)