Skip to content

Commit 263bbfd

Browse files
committed
Add diagnostic for invalid whitespace in @ Attribute
1 parent bf18c71 commit 263bbfd

File tree

5 files changed

+121
-5
lines changed

5 files changed

+121
-5
lines changed

Sources/SwiftParser/Attributes.swift

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,34 @@ 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,
176+
atSign,
177+
arena: self.arena
178+
)
179+
atSign = RawTokenSyntax(
180+
kind: .atSign,
181+
text: atSign.tokenText,
182+
leadingTriviaPieces: atSign.leadingTriviaPieces,
183+
presence: .missing,
184+
arena: self.arena
185+
)
186+
} else if attributeName.raw.leadingTriviaByteLength != 0 {
187+
unexpectedBetweenAtSignAndAttributeName = RawUnexpectedNodesSyntax(attributeName)
188+
// `withLeadingTrivia` only returns `nil` if there is no token in `attributeName`.
189+
// But since `attributeName` has leadingTriviaLength != 0 there must be trivia and thus a token.
190+
// So we can safely force-unwrap here.
191+
attributeName = attributeName
192+
.raw
193+
.withLeadingTrivia([], arena: self.arena)!
194+
.as(RawTypeSyntax.self)!
195+
}
196+
171197
let shouldParseArgument: Bool
172198
switch argumentMode {
173199
case .required:
@@ -185,6 +211,7 @@ extension Parser {
185211
RawAttributeSyntax(
186212
unexpectedBeforeAtSign,
187213
atSignToken: atSign,
214+
unexpectedBetweenAtSignAndAttributeName,
188215
attributeName: attributeName,
189216
unexpectedBeforeLeftParen,
190217
leftParen: leftParen,
@@ -199,6 +226,7 @@ extension Parser {
199226
RawAttributeSyntax(
200227
unexpectedBeforeAtSign,
201228
atSignToken: atSign,
229+
unexpectedBetweenAtSignAndAttributeName,
202230
attributeName: attributeName,
203231
leftParen: nil,
204232
argument: nil,

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,51 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
457457
],
458458
handledNodes: [argument.id]
459459
)
460-
return .visitChildren
460+
}
461+
if let unexpectedAtSign = node.unexpectedBeforeAtSignToken?.onlyToken(where: { $0.tokenKind == .atSign && !$0.trailingTrivia.isEmpty }),
462+
node.atSignToken.presence == .missing
463+
{
464+
addDiagnostic(
465+
unexpectedAtSign,
466+
position: unexpectedAtSign.endPositionBeforeTrailingTrivia,
467+
StaticParserError.invalidWhitespaceBetweenAttributeAtSignAndIdentifier,
468+
fixIts: [
469+
FixIt(
470+
message: StaticParserFixIt.removeExtraneousWhitespace,
471+
changes: [
472+
.makeMissing(unexpectedAtSign, transferTrivia: false),
473+
.makePresent(node.atSignToken),
474+
]
475+
)
476+
],
477+
handledNodes: [
478+
node.id,
479+
unexpectedAtSign.id,
480+
node.atSignToken.id,
481+
]
482+
)
483+
} else if node.attributeName.isMissingAllTokens,
484+
let unexpectedBetweenAtSignTokenAndAttributeName = node.unexpectedBetweenAtSignTokenAndAttributeName,
485+
unexpectedBetweenAtSignTokenAndAttributeName.trailingTriviaLength.utf8Length != 0
486+
{
487+
addDiagnostic(
488+
unexpectedBetweenAtSignTokenAndAttributeName,
489+
StaticParserError.invalidWhitespaceBetweenAttributeAtSignAndIdentifier,
490+
fixIts: [
491+
FixIt(
492+
message: StaticParserFixIt.removeExtraneousWhitespace,
493+
changes: [
494+
.makeMissing(unexpectedBetweenAtSignTokenAndAttributeName, transferTrivia: false),
495+
.makePresent(node.attributeName),
496+
]
497+
)
498+
],
499+
handledNodes: [
500+
node.id,
501+
unexpectedBetweenAtSignTokenAndAttributeName.id,
502+
node.attributeName.id,
503+
]
504+
)
461505
}
462506
return .visitChildren
463507
}

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+
@1️⃣
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)