Skip to content

Commit a5fa949

Browse files
committed
Add diagnostic for invalid whitespace in @ Attribute
1 parent 6645096 commit a5fa949

File tree

5 files changed

+116
-5
lines changed

5 files changed

+116
-5
lines changed

Sources/SwiftParser/Attributes.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,33 @@ extension Parser {
166166
}
167167

168168
mutating func parseAttribute(argumentMode: AttributeArgumentMode, parseArguments: (inout Parser) -> RawAttributeSyntax.Argument) -> RawAttributeListSyntax.Element {
169-
let (unexpectedBeforeAtSign, atSign) = self.expect(.atSign)
170-
let attributeName = self.parseType()
169+
var (unexpectedBeforeAtSign, atSign) = self.expect(.atSign)
170+
var attributeName = self.parseType()
171+
var unexpectedBetweenAtSignAndAttributeName: RawUnexpectedNodesSyntax?
172+
173+
if atSign.trailingTriviaByteLength != 0 {
174+
unexpectedBeforeAtSign = RawUnexpectedNodesSyntax(
175+
combining: unexpectedBeforeAtSign, atSign,
176+
arena: self.arena
177+
)
178+
atSign = RawTokenSyntax(
179+
kind: .atSign,
180+
text: atSign.tokenText,
181+
leadingTriviaPieces: atSign.leadingTriviaPieces,
182+
presence: .missing,
183+
arena: self.arena
184+
)
185+
} else if attributeName.raw.leadingTriviaByteLength != 0 {
186+
unexpectedBetweenAtSignAndAttributeName = RawUnexpectedNodesSyntax(attributeName)
187+
// `withLeadingTrivia` only returns `nil` if there is no token in `attributeName`.
188+
// But since `attributeName` has leadingTriviaLength != 0 there must be trivia and thus a token.
189+
// So we can safely force-unwrap here.
190+
attributeName = attributeName
191+
.raw
192+
.withLeadingTrivia([], arena: self.arena)!
193+
.as(RawTypeSyntax.self)!
194+
}
195+
171196
let shouldParseArgument: Bool
172197
switch argumentMode {
173198
case .required:
@@ -185,6 +210,7 @@ extension Parser {
185210
RawAttributeSyntax(
186211
unexpectedBeforeAtSign,
187212
atSignToken: atSign,
213+
unexpectedBetweenAtSignAndAttributeName,
188214
attributeName: attributeName,
189215
unexpectedBeforeLeftParen,
190216
leftParen: leftParen,
@@ -199,6 +225,7 @@ extension Parser {
199225
RawAttributeSyntax(
200226
unexpectedBeforeAtSign,
201227
atSignToken: atSign,
228+
unexpectedBetweenAtSignAndAttributeName,
202229
attributeName: attributeName,
203230
leftParen: nil,
204231
argument: nil,

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,47 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
456456
],
457457
handledNodes: [argument.id]
458458
)
459-
return .visitChildren
459+
}
460+
if let unexpectedAtSign = node.unexpectedBeforeAtSignToken?.onlyToken(where: { $0.tokenKind == .atSign && !$0.trailingTrivia.isEmpty }) {
461+
addDiagnostic(
462+
unexpectedAtSign,
463+
position: node.unexpectedBeforeAtSignToken?.endPosition.advanced(by: -1),
464+
StaticParserError.invalidWhitespaceBetweenAttributeAtSignAndIdentifier,
465+
fixIts: [
466+
FixIt(
467+
message: StaticParserFixIt.removeExtraneousWhitespace,
468+
changes: [
469+
.makeMissing(unexpectedAtSign, transferTrivia: false),
470+
.makePresent(node.atSignToken),
471+
]
472+
)
473+
],
474+
handledNodes: [
475+
node.id,
476+
unexpectedAtSign.id,
477+
node.atSignToken.id
478+
]
479+
)
480+
} else if let unexpectedIdentifier = node.unexpectedBetweenAtSignTokenAndAttributeName?.onlyToken(where: { $0.tokenKind.isIdentifier && !$0.leadingTrivia.isEmpty }),
481+
node.attributeName.isMissingAllTokens {
482+
addDiagnostic(
483+
unexpectedIdentifier,
484+
StaticParserError.invalidWhitespaceBetweenAttributeAtSignAndIdentifier,
485+
fixIts: [
486+
FixIt(
487+
message: StaticParserFixIt.removeExtraneousWhitespace,
488+
changes: [
489+
.makeMissing(unexpectedIdentifier, transferTrivia: false),
490+
.makePresent(node.attributeName), // TODO: Provide synthesized attributeName as a replacement.
491+
]
492+
)
493+
],
494+
handledNodes: [
495+
node.id,
496+
unexpectedIdentifier.id,
497+
node.attributeName.id
498+
]
499+
)
460500
}
461501
return .visitChildren
462502
}

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ extension DiagnosticMessage where Self == StaticParserError {
171171
public static var invalidWhitespaceAfterPeriod: Self {
172172
.init("extraneous whitespace after '.' is not permitted")
173173
}
174+
public static var invalidWhitespaceBetweenAttributeAtSignAndIdentifier: Self {
175+
.init("extraneous whitespace after '@' is not permitted")
176+
}
174177
public static var joinConditionsUsingComma: Self {
175178
.init("expected ',' joining parts of a multi-clause condition")
176179
}

Sources/SwiftSyntax/Raw/RawSyntax.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ extension RawSyntax {
293293
/// - Parameters:
294294
/// - leadingTrivia: The trivia to attach.
295295
/// - arena: SyntaxArena to the result node data resides.
296-
func withLeadingTrivia(_ leadingTrivia: Trivia, arena: SyntaxArena) -> RawSyntax? {
296+
@_spi(RawSyntax)
297+
public func withLeadingTrivia(_ leadingTrivia: Trivia, arena: SyntaxArena) -> RawSyntax? {
297298
switch view {
298299
case .token(let tokenView):
299300
return .makeMaterializedToken(
@@ -319,7 +320,8 @@ extension RawSyntax {
319320
/// - Parameters:
320321
/// - trailingTrivia: The trivia to attach.
321322
/// - arena: SyntaxArena to the result node data resides.
322-
func withTrailingTrivia(_ trailingTrivia: Trivia, arena: SyntaxArena) -> RawSyntax? {
323+
@_spi(RawSyntax)
324+
public func withTrailingTrivia(_ trailingTrivia: Trivia, arena: SyntaxArena) -> RawSyntax? {
323325
switch view {
324326
case .token(let tokenView):
325327
return .makeMaterializedToken(

Tests/SwiftParserTest/AttributeTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,4 +631,43 @@ final class AttributeTests: XCTestCase {
631631
"""
632632
)
633633
}
634+
635+
func testInvalidWhitespaceBetweenAtSignAndIdenfifierIsDiagnosed() {
636+
assertParse(
637+
"""
638+
@1️⃣ MyAttribute
639+
func foo() {}
640+
""",
641+
diagnostics: [
642+
DiagnosticSpec(
643+
message: "extraneous whitespace after '@' is not permitted",
644+
fixIts: ["remove whitespace"]
645+
)
646+
],
647+
fixedSource: """
648+
@MyAttribute
649+
func foo() {}
650+
"""
651+
)
652+
}
653+
654+
func testInvalidNewlineBetweenAtSignAndIdenfifierIsDiagnosed() {
655+
assertParse(
656+
"""
657+
@
658+
MyAttribute
659+
func foo() {}
660+
""",
661+
diagnostics: [
662+
DiagnosticSpec(
663+
message: "extraneous whitespace after '@' is not permitted",
664+
fixIts: ["remove whitespace"]
665+
)
666+
],
667+
fixedSource: """
668+
@MyAttribute
669+
func foo() {}
670+
"""
671+
)
672+
}
634673
}

0 commit comments

Comments
 (0)