Skip to content

Commit 0ab9384

Browse files
committed
[SwiftParser] Update whitespace rule between attribute name and '('
In Swift 6 and later, whitespace is no longer permitted between an attribute name and the opening parenthesis. Update the parser so that in Swift 6+, a '(' without preceding whitespace is always treated as the start of the argument list, while a '(' with preceding whitespace is considered the start of the argument list only when it is unambiguous. This change makes the following closure be parsed correctly: ```swift { @mainactor (arg: Int) in ... } ``` rdar://147785544
1 parent 6df106d commit 0ab9384

File tree

4 files changed

+152
-71
lines changed

4 files changed

+152
-71
lines changed

Sources/SwiftParser/Attributes.swift

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -196,28 +196,35 @@ extension Parser {
196196
atSign = atSign.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena)
197197
}
198198
let attributeName = self.parseAttributeName()
199+
let attributeNameHasTrailingSpace = attributeName.raw.trailingTriviaByteLength > 0
200+
199201
let shouldParseArgument: Bool
200202
switch argumentMode {
201203
case .required:
202204
shouldParseArgument = true
203205
case .customAttribute:
204-
shouldParseArgument = self.withLookahead { $0.atAttributeOrSpecifierArgument() }
206+
shouldParseArgument = self.withLookahead {
207+
$0.atAttributeOrSpecifierArgument(lastTokenHadSpace: attributeNameHasTrailingSpace, forCustomAttribute: true)
208+
}
205209
case .optional:
206-
shouldParseArgument = self.at(TokenSpec(.leftParen, allowAtStartOfLine: false))
210+
shouldParseArgument = self.withLookahead {
211+
$0.atAttributeOrSpecifierArgument(lastTokenHadSpace: attributeNameHasTrailingSpace, forCustomAttribute: false)
212+
}
207213
case .noArgument:
208214
shouldParseArgument = false
209215
}
210216
if shouldParseArgument {
211217
var (unexpectedBeforeLeftParen, leftParen) = self.expect(TokenSpec(.leftParen, allowAtStartOfLine: false))
212-
if unexpectedBeforeLeftParen == nil
213-
&& (attributeName.raw.trailingTriviaByteLength > 0 || leftParen.leadingTriviaByteLength > 0)
214-
{
218+
219+
// Diagnose spaces between the name and the '('.
220+
if unexpectedBeforeLeftParen == nil && (attributeNameHasTrailingSpace || leftParen.leadingTriviaByteLength > 0) {
215221
let diagnostic = TokenDiagnostic(
216222
self.swiftVersion < .v6 ? .extraneousLeadingWhitespaceWarning : .extraneousLeadingWhitespaceError,
217223
byteOffset: 0
218224
)
219225
leftParen = leftParen.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena)
220226
}
227+
221228
let unexpectedBeforeArguments: RawUnexpectedNodesSyntax?
222229
let argument: RawAttributeSyntax.Arguments
223230
if let parseMissingArguments, leftParen.presence == .missing {
@@ -1074,44 +1081,70 @@ extension Parser {
10741081
// MARK: Lookahead
10751082

10761083
extension Parser.Lookahead {
1077-
mutating func atAttributeOrSpecifierArgument() -> Bool {
1084+
mutating func atAttributeOrSpecifierArgument(
1085+
lastTokenHadSpace: Bool = false,
1086+
forCustomAttribute: Bool = false
1087+
) -> Bool {
10781088
if !self.at(TokenSpec(.leftParen, allowAtStartOfLine: false)) {
10791089
return false
10801090
}
10811091

1082-
var lookahead = self.lookahead()
1083-
lookahead.skipSingle()
1084-
1085-
// If we have any keyword, identifier, or token that follows a function
1086-
// type's parameter list, this is a parameter list and not an attribute.
1087-
// Alternatively, we might have a token that illustrates we're not going to
1088-
// get anything following the attribute, which means the parentheses describe
1089-
// what follows the attribute.
1090-
switch lookahead.currentToken {
1091-
case TokenSpec(.arrow),
1092-
TokenSpec(.throw),
1093-
TokenSpec(.throws),
1094-
TokenSpec(.rethrows),
1095-
TokenSpec(.rightParen),
1096-
TokenSpec(.rightBrace),
1097-
TokenSpec(.rightSquare),
1098-
TokenSpec(.rightAngle):
1099-
return false
1100-
case _ where lookahead.at(.keyword(.async)):
1101-
return false
1102-
case _ where lookahead.at(.keyword(.reasync)):
1103-
return false
1104-
default:
1105-
return true
1092+
if self.swiftVersion >= .v6 {
1093+
if !lastTokenHadSpace && currentToken.leadingTriviaByteLength == 0 {
1094+
return true;
1095+
}
1096+
1097+
return withLookahead({
1098+
$0.skipSingle()
1099+
return $0.at(.atSign) || $0.atStartOfDeclaration()
1100+
})
1101+
} else {
1102+
if !forCustomAttribute {
1103+
return true
1104+
}
1105+
var lookahead = self.lookahead()
1106+
lookahead.skipSingle()
1107+
1108+
// If we have any keyword, identifier, or token that follows a function
1109+
// type's parameter list, this is a parameter list and not an attribute.
1110+
// Alternatively, we might have a token that illustrates we're not going to
1111+
// get anything following the attribute, which means the parentheses describe
1112+
// what follows the attribute.
1113+
switch lookahead.currentToken {
1114+
case TokenSpec(.arrow),
1115+
TokenSpec(.throw),
1116+
TokenSpec(.throws),
1117+
TokenSpec(.rethrows),
1118+
TokenSpec(.rightParen),
1119+
TokenSpec(.rightBrace),
1120+
TokenSpec(.rightSquare),
1121+
TokenSpec(.rightAngle):
1122+
return false
1123+
case _ where lookahead.at(.keyword(.async)):
1124+
return false
1125+
case _ where lookahead.at(.keyword(.reasync)):
1126+
return false
1127+
default:
1128+
return true
1129+
}
11061130
}
11071131
}
11081132

11091133
mutating func canParseCustomAttribute() -> Bool {
1110-
guard self.canParseType() else {
1134+
guard
1135+
let numTypeTokens = self.withLookahead({ $0.canParseSimpleType() ? $0.tokensConsumed : nil }),
1136+
numTypeTokens >= 1
1137+
else {
11111138
return false
11121139
}
1140+
// Check if the last token had trailing white spaces.
1141+
for _ in 0..<numTypeTokens - 1 {
1142+
self.consumeAnyToken()
1143+
}
1144+
let hasSpace = self.currentToken.trailingTriviaByteLength > 0
1145+
self.consumeAnyToken();
11131146

1114-
if self.withLookahead({ $0.atAttributeOrSpecifierArgument() }) {
1147+
if self.atAttributeOrSpecifierArgument(lastTokenHadSpace: hasSpace, forCustomAttribute: true) {
11151148
self.skipSingle()
11161149
}
11171150

Sources/SwiftParser/Lookahead.swift

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -163,40 +163,31 @@ extension Parser.Lookahead {
163163
// MARK: Skipping Tokens
164164

165165
extension Parser.Lookahead {
166-
mutating func skipTypeAttribute() {
167-
// These are keywords that we accept as attribute names.
168-
guard self.at(.identifier) || self.at(.keyword(.in), .keyword(.inout)) else {
169-
return
170-
}
166+
/// Skip *any* single attribute. I.e. a type attribute, a decl attribute, or
167+
/// a custom attribute.
168+
mutating func consumeAnyAttribute() {
169+
self.eat(.atSign)
170+
171+
let nameHadSpace = self.currentToken.trailingTriviaByteLength > 0;
171172

172173
// Determine which attribute it is.
173174
if let (attr, handle) = self.at(anyIn: TypeAttribute.self) {
174-
// Ok, it is a valid attribute, eat it, and then process it.
175175
self.eat(handle)
176176
switch attr {
177-
case .convention, .isolated:
178-
self.skipSingle()
177+
case .convention, .isolated, .differentiable:
178+
if self.atAttributeOrSpecifierArgument(lastTokenHadSpace: nameHadSpace) {
179+
self.skipSingle()
180+
}
179181
default:
180182
break
181183
}
182184
return
183185
}
184186

185187
if let (_, handle) = self.at(anyIn: Parser.DeclarationAttributeWithSpecialSyntax.self) {
186-
// This is a valid decl attribute so they should have put it on the decl
187-
// instead of the type.
188-
//
189-
// Recover by eating @foo(...)
190188
self.eat(handle)
191-
if self.at(.leftParen) {
192-
var lookahead = self.lookahead()
193-
lookahead.skipSingle()
194-
// If we found '->', or 'throws' after paren, it's likely a parameter
195-
// of function type.
196-
guard lookahead.at(.arrow) || lookahead.at(.keyword(.throws), .keyword(.rethrows), .keyword(.throw)) else {
197-
self.skipSingle()
198-
return
199-
}
189+
if self.atAttributeOrSpecifierArgument(lastTokenHadSpace: nameHadSpace) {
190+
self.skipSingle()
200191
}
201192
return
202193
}
@@ -212,18 +203,9 @@ extension Parser.Lookahead {
212203
return false
213204
}
214205

215-
while self.consume(if: .atSign) != nil {
216-
// Consume qualified names that may or may not involve generic arguments.
217-
repeat {
218-
self.consume(if: .identifier, .keyword(.rethrows))
219-
// We don't care whether this succeeds or fails to eat generic
220-
// parameters.
221-
_ = self.consumeGenericArguments()
222-
} while self.consume(if: .period) != nil
223-
224-
if self.atAttributeOrSpecifierArgument() {
225-
self.skipSingle()
226-
}
206+
var attributeProgress = LoopProgressCondition()
207+
while self.at(.atSign), self.hasProgressed(&attributeProgress) {
208+
self.consumeAnyAttribute()
227209
}
228210
return true
229211
}

Sources/SwiftParser/Types.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ extension Parser.Lookahead {
790790

791791
case .keyword(.dependsOn):
792792
let canParseDependsOn = self.withLookahead({
793+
let nameHadSpace = $0.currentToken.trailingTriviaByteLength > 0
793794
// Consume 'dependsOn'
794795
$0.consumeAnyToken()
795796

@@ -798,7 +799,7 @@ extension Parser.Lookahead {
798799
}
799800

800801
// `dependsOn` requires an argument list.
801-
guard $0.atAttributeOrSpecifierArgument() else {
802+
guard $0.atAttributeOrSpecifierArgument(lastTokenHadSpace: nameHadSpace) else {
802803
return false
803804
}
804805

@@ -817,11 +818,7 @@ extension Parser.Lookahead {
817818
}
818819
}
819820

820-
var attributeProgress = LoopProgressCondition()
821-
while self.at(.atSign), self.hasProgressed(&attributeProgress) {
822-
self.consumeAnyToken()
823-
self.skipTypeAttribute()
824-
}
821+
_ = self.consumeAttributeList()
825822

826823
return true
827824
}

Tests/SwiftParserTest/AttributeTests.swift

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,4 +1471,73 @@ final class AttributeTests: ParserTestCase {
14711471
"""
14721472
)
14731473
}
1474+
1475+
func testAttributeWithSpace() {
1476+
assertParse(
1477+
"""
1478+
@1️⃣ FooBar2️⃣ (arg) func foo() {}
1479+
""",
1480+
diagnostics: [
1481+
DiagnosticSpec(
1482+
locationMarker: "1️⃣",
1483+
message: "extraneous whitespace after '@' is not permitted",
1484+
fixIts: ["remove whitespace"]
1485+
),
1486+
DiagnosticSpec(
1487+
locationMarker: "2️⃣",
1488+
message: "extraneous whitespace before '(' is not permitted",
1489+
fixIts: ["remove whitespace"]
1490+
),
1491+
],
1492+
fixedSource: """
1493+
@FooBar(arg) func foo() {}
1494+
"""
1495+
)
1496+
}
1497+
1498+
func testAttributeMainActorClosure() {
1499+
assertParse(
1500+
"""
1501+
{ @MainActor (arg) in }
1502+
""",
1503+
substructure: ClosureExprSyntax(
1504+
leftBrace: .leftBraceToken(),
1505+
signature: ClosureSignatureSyntax(
1506+
attributes: AttributeListSyntax([
1507+
.attribute(
1508+
AttributeSyntax(
1509+
atSign: .atSignToken(),
1510+
attributeName: TypeSyntax(IdentifierTypeSyntax(name: .identifier("MainActor")))
1511+
)
1512+
)
1513+
]),
1514+
parameterClause: ClosureSignatureSyntax.ParameterClause(
1515+
ClosureParameterClauseSyntax(
1516+
leftParen: .leftParenToken(),
1517+
parameters: ClosureParameterListSyntax([
1518+
ClosureParameterSyntax(
1519+
attributes: [],
1520+
modifiers: [],
1521+
firstName: .identifier("arg")
1522+
)
1523+
]),
1524+
rightParen: .rightParenToken()
1525+
)
1526+
),
1527+
inKeyword: .keyword(.in)
1528+
),
1529+
statements: [],
1530+
rightBrace: .rightBraceToken()
1531+
)
1532+
)
1533+
}
1534+
1535+
func testTypeAttributeInExprContext() {
1536+
assertParse(
1537+
"""
1538+
var _ = [@Sendable () -> Void]()
1539+
""",
1540+
swiftVersion: .v5
1541+
)
1542+
}
14741543
}

0 commit comments

Comments
 (0)