diff --git a/Sources/SwiftParser/Attributes.swift b/Sources/SwiftParser/Attributes.swift index b64d87386b1..27cc1b80363 100644 --- a/Sources/SwiftParser/Attributes.swift +++ b/Sources/SwiftParser/Attributes.swift @@ -196,28 +196,35 @@ extension Parser { atSign = atSign.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena) } let attributeName = self.parseAttributeName() + let attributeNameHasTrailingSpace = attributeName.raw.trailingTriviaByteLength > 0 + let shouldParseArgument: Bool switch argumentMode { case .required: shouldParseArgument = true case .customAttribute: - shouldParseArgument = self.withLookahead { $0.atAttributeOrSpecifierArgument() } + shouldParseArgument = self.withLookahead { + $0.atAttributeOrSpecifierArgument(lastTokenHadSpace: attributeNameHasTrailingSpace, forCustomAttribute: true) + } case .optional: - shouldParseArgument = self.at(TokenSpec(.leftParen, allowAtStartOfLine: false)) + shouldParseArgument = self.withLookahead { + $0.atAttributeOrSpecifierArgument(lastTokenHadSpace: attributeNameHasTrailingSpace, forCustomAttribute: false) + } case .noArgument: shouldParseArgument = false } if shouldParseArgument { var (unexpectedBeforeLeftParen, leftParen) = self.expect(TokenSpec(.leftParen, allowAtStartOfLine: false)) - if unexpectedBeforeLeftParen == nil - && (attributeName.raw.trailingTriviaByteLength > 0 || leftParen.leadingTriviaByteLength > 0) - { + + // Diagnose spaces between the name and the '('. + if unexpectedBeforeLeftParen == nil && (attributeNameHasTrailingSpace || leftParen.leadingTriviaByteLength > 0) { let diagnostic = TokenDiagnostic( self.swiftVersion < .v6 ? .extraneousLeadingWhitespaceWarning : .extraneousLeadingWhitespaceError, byteOffset: 0 ) leftParen = leftParen.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena) } + let unexpectedBeforeArguments: RawUnexpectedNodesSyntax? let argument: RawAttributeSyntax.Arguments if let parseMissingArguments, leftParen.presence == .missing { @@ -1074,44 +1081,70 @@ extension Parser { // MARK: Lookahead extension Parser.Lookahead { - mutating func atAttributeOrSpecifierArgument() -> Bool { + mutating func atAttributeOrSpecifierArgument( + lastTokenHadSpace: Bool, + forCustomAttribute: Bool = false + ) -> Bool { if !self.at(TokenSpec(.leftParen, allowAtStartOfLine: false)) { return false } - var lookahead = self.lookahead() - lookahead.skipSingle() - - // If we have any keyword, identifier, or token that follows a function - // type's parameter list, this is a parameter list and not an attribute. - // Alternatively, we might have a token that illustrates we're not going to - // get anything following the attribute, which means the parentheses describe - // what follows the attribute. - switch lookahead.currentToken { - case TokenSpec(.arrow), - TokenSpec(.throw), - TokenSpec(.throws), - TokenSpec(.rethrows), - TokenSpec(.rightParen), - TokenSpec(.rightBrace), - TokenSpec(.rightSquare), - TokenSpec(.rightAngle): - return false - case _ where lookahead.at(.keyword(.async)): - return false - case _ where lookahead.at(.keyword(.reasync)): - return false - default: - return true + if self.swiftVersion >= .v6 { + if !lastTokenHadSpace && currentToken.leadingTriviaByteLength == 0 { + return true + } + + return withLookahead({ + $0.skipSingle() + return $0.at(.atSign) || $0.atStartOfDeclaration() + }) + } else { + if !forCustomAttribute { + return true + } + var lookahead = self.lookahead() + lookahead.skipSingle() + + // If we have any keyword, identifier, or token that follows a function + // type's parameter list, this is a parameter list and not an attribute. + // Alternatively, we might have a token that illustrates we're not going to + // get anything following the attribute, which means the parentheses describe + // what follows the attribute. + switch lookahead.currentToken { + case TokenSpec(.arrow), + TokenSpec(.throw), + TokenSpec(.throws), + TokenSpec(.rethrows), + TokenSpec(.rightParen), + TokenSpec(.rightBrace), + TokenSpec(.rightSquare), + TokenSpec(.rightAngle): + return false + case _ where lookahead.at(.keyword(.async)): + return false + case _ where lookahead.at(.keyword(.reasync)): + return false + default: + return true + } } } mutating func canParseCustomAttribute() -> Bool { - guard self.canParseType() else { + guard + let numTypeTokens = self.withLookahead({ $0.canParseSimpleType() ? $0.tokensConsumed : nil }), + numTypeTokens >= 1 + else { return false } + // Check if the last token had trailing white spaces. + for _ in 0.. 0 + self.consumeAnyToken() - if self.withLookahead({ $0.atAttributeOrSpecifierArgument() }) { + if self.atAttributeOrSpecifierArgument(lastTokenHadSpace: hasSpace, forCustomAttribute: true) { self.skipSingle() } diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index 5fd9088e689..09a9b0e3ce6 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -51,12 +51,6 @@ extension TokenConsumer { case nil: break } - if self.at(.atSign) || self.at(.keyword(.inout)) { - var lookahead = self.lookahead() - if lookahead.canParseType() { - return true - } - } if self.at(.atSign) && self.peek(isAt: .stringQuote) { // Invalid Objective-C-style string literal return true diff --git a/Sources/SwiftParser/Lookahead.swift b/Sources/SwiftParser/Lookahead.swift index 55f9ab2978d..4c16f78b8b9 100644 --- a/Sources/SwiftParser/Lookahead.swift +++ b/Sources/SwiftParser/Lookahead.swift @@ -163,19 +163,21 @@ extension Parser.Lookahead { // MARK: Skipping Tokens extension Parser.Lookahead { - mutating func skipTypeAttribute() { - // These are keywords that we accept as attribute names. - guard self.at(.identifier) || self.at(.keyword(.in), .keyword(.inout)) else { - return - } + /// Skip *any* single attribute. I.e. a type attribute, a decl attribute, or + /// a custom attribute. + mutating func consumeAnyAttribute() { + self.eat(.atSign) + + let nameHadSpace = self.currentToken.trailingTriviaByteLength > 0 // Determine which attribute it is. if let (attr, handle) = self.at(anyIn: TypeAttribute.self) { - // Ok, it is a valid attribute, eat it, and then process it. self.eat(handle) switch attr { - case .convention, .isolated: - self.skipSingle() + case .convention, .isolated, .differentiable: + if self.atAttributeOrSpecifierArgument(lastTokenHadSpace: nameHadSpace) { + self.skipSingle() + } default: break } @@ -183,20 +185,9 @@ extension Parser.Lookahead { } if let (_, handle) = self.at(anyIn: Parser.DeclarationAttributeWithSpecialSyntax.self) { - // This is a valid decl attribute so they should have put it on the decl - // instead of the type. - // - // Recover by eating @foo(...) self.eat(handle) - if self.at(.leftParen) { - var lookahead = self.lookahead() - lookahead.skipSingle() - // If we found '->', or 'throws' after paren, it's likely a parameter - // of function type. - guard lookahead.at(.arrow) || lookahead.at(.keyword(.throws), .keyword(.rethrows), .keyword(.throw)) else { - self.skipSingle() - return - } + if self.atAttributeOrSpecifierArgument(lastTokenHadSpace: nameHadSpace) { + self.skipSingle() } return } @@ -212,18 +203,9 @@ extension Parser.Lookahead { return false } - while self.consume(if: .atSign) != nil { - // Consume qualified names that may or may not involve generic arguments. - repeat { - self.consume(if: .identifier, .keyword(.rethrows)) - // We don't care whether this succeeds or fails to eat generic - // parameters. - _ = self.consumeGenericArguments() - } while self.consume(if: .period) != nil - - if self.atAttributeOrSpecifierArgument() { - self.skipSingle() - } + var attributeProgress = LoopProgressCondition() + while self.at(.atSign), self.hasProgressed(&attributeProgress) { + self.consumeAnyAttribute() } return true } diff --git a/Sources/SwiftParser/TopLevel.swift b/Sources/SwiftParser/TopLevel.swift index 0bbb7f4cfb9..5bdc10fddf1 100644 --- a/Sources/SwiftParser/TopLevel.swift +++ b/Sources/SwiftParser/TopLevel.swift @@ -255,6 +255,10 @@ extension Parser { return self.parseStatementItem() } else if self.atStartOfDeclaration(isAtTopLevel: isAtTopLevel, allowInitDecl: allowInitDecl, allowRecovery: true) { return .decl(self.parseDeclaration()) + } else if self.at(.atSign), peek(isAt: .identifier) { + // Force parsing '@' as a declaration, as there's no valid + // expression or statement starting with an attribute. + return .decl(self.parseDeclaration()) } else { return .init(expr: RawMissingExprSyntax(arena: self.arena)) } diff --git a/Sources/SwiftParser/Types.swift b/Sources/SwiftParser/Types.swift index 46d150a74b6..94fa029b0f5 100644 --- a/Sources/SwiftParser/Types.swift +++ b/Sources/SwiftParser/Types.swift @@ -790,6 +790,7 @@ extension Parser.Lookahead { case .keyword(.dependsOn): let canParseDependsOn = self.withLookahead({ + let nameHadSpace = $0.currentToken.trailingTriviaByteLength > 0 // Consume 'dependsOn' $0.consumeAnyToken() @@ -798,7 +799,7 @@ extension Parser.Lookahead { } // `dependsOn` requires an argument list. - guard $0.atAttributeOrSpecifierArgument() else { + guard $0.atAttributeOrSpecifierArgument(lastTokenHadSpace: nameHadSpace) else { return false } @@ -817,11 +818,7 @@ extension Parser.Lookahead { } } - var attributeProgress = LoopProgressCondition() - while self.at(.atSign), self.hasProgressed(&attributeProgress) { - self.consumeAnyToken() - self.skipTypeAttribute() - } + _ = self.consumeAttributeList() return true } @@ -1186,7 +1183,9 @@ extension Parser { // using `nonsisolated` without an argument is allowed in // an inheritance clause. // - The '(nonsending)' was omitted. - if !self.at(.leftParen) { + if !self.withLookahead({ + $0.atAttributeOrSpecifierArgument(lastTokenHadSpace: nonisolatedKeyword.trailingTriviaByteLength > 0) + }) { // `nonisolated P<...>` is allowed in an inheritance clause. if withLookahead({ $0.canParseTypeIdentifier() }) { let nonisolatedSpecifier = RawNonisolatedTypeSpecifierSyntax( @@ -1214,16 +1213,18 @@ extension Parser { ) return .nonisolatedTypeSpecifier(nonisolatedSpecifier) - } + } else { + let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen) + let (unexpectedBeforeModifier, modifier) = self.expect(.keyword(.nonsending)) + let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen) - // Avoid being to greedy about `(` since this modifier should be associated with - // function types, it's possible that the argument is omitted and what follows - // is a function type i.e. `nonisolated () async -> Void`. - if self.at(.leftParen) && !withLookahead({ $0.atAttributeOrSpecifierArgument() }) { let argument = RawNonisolatedSpecifierArgumentSyntax( - leftParen: missingToken(.leftParen), - nonsendingKeyword: missingToken(.keyword(.nonsending)), - rightParen: missingToken(.rightParen), + unexpectedBeforeLeftParen, + leftParen: leftParen, + unexpectedBeforeModifier, + nonsendingKeyword: modifier, + unexpectedBeforeRightParen, + rightParen: rightParen, arena: self.arena ) @@ -1233,31 +1234,8 @@ extension Parser { argument: argument, arena: self.arena ) - return .nonisolatedTypeSpecifier(nonisolatedSpecifier) } - - let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen) - let (unexpectedBeforeModifier, modifier) = self.expect(.keyword(.nonsending)) - let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen) - - let argument = RawNonisolatedSpecifierArgumentSyntax( - unexpectedBeforeLeftParen, - leftParen: leftParen, - unexpectedBeforeModifier, - nonsendingKeyword: modifier, - unexpectedBeforeRightParen, - rightParen: rightParen, - arena: self.arena - ) - - let nonisolatedSpecifier = RawNonisolatedTypeSpecifierSyntax( - unexpectedBeforeNonisolatedKeyword, - nonisolatedKeyword: nonisolatedKeyword, - argument: argument, - arena: self.arena - ) - return .nonisolatedTypeSpecifier(nonisolatedSpecifier) } private mutating func parseSimpleTypeSpecifier( diff --git a/Tests/SwiftParserTest/AttributeTests.swift b/Tests/SwiftParserTest/AttributeTests.swift index 98cadfc7318..b5eb6ae1193 100644 --- a/Tests/SwiftParserTest/AttributeTests.swift +++ b/Tests/SwiftParserTest/AttributeTests.swift @@ -1471,4 +1471,73 @@ final class AttributeTests: ParserTestCase { """ ) } + + func testAttributeWithSpace() { + assertParse( + """ + @1️⃣ FooBar2️⃣ (arg) func foo() {} + """, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "extraneous whitespace after '@' is not permitted", + fixIts: ["remove whitespace"] + ), + DiagnosticSpec( + locationMarker: "2️⃣", + message: "extraneous whitespace before '(' is not permitted", + fixIts: ["remove whitespace"] + ), + ], + fixedSource: """ + @FooBar(arg) func foo() {} + """ + ) + } + + func testAttributeMainActorClosure() { + assertParse( + """ + { @MainActor (arg) in } + """, + substructure: ClosureExprSyntax( + leftBrace: .leftBraceToken(), + signature: ClosureSignatureSyntax( + attributes: AttributeListSyntax([ + .attribute( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: TypeSyntax(IdentifierTypeSyntax(name: .identifier("MainActor"))) + ) + ) + ]), + parameterClause: ClosureSignatureSyntax.ParameterClause( + ClosureParameterClauseSyntax( + leftParen: .leftParenToken(), + parameters: ClosureParameterListSyntax([ + ClosureParameterSyntax( + attributes: [], + modifiers: [], + firstName: .identifier("arg") + ) + ]), + rightParen: .rightParenToken() + ) + ), + inKeyword: .keyword(.in) + ), + statements: [], + rightBrace: .rightBraceToken() + ) + ) + } + + func testTypeAttributeInExprContext() { + assertParse( + """ + var _ = [@Sendable () -> Void]() + """, + swiftVersion: .v5 + ) + } } diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift index 80f254ed402..d72f00824b1 100644 --- a/Tests/SwiftParserTest/DeclarationTests.swift +++ b/Tests/SwiftParserTest/DeclarationTests.swift @@ -3643,12 +3643,33 @@ final class UsingDeclarationTests: ParserTestCase { assertParse( """ - @MainActor - using - """, - substructure: CodeBlockSyntax( - DeclReferenceExprSyntax(baseName: .identifier("using")) - ) + 1️⃣@MainActor + using2️⃣ + """, + substructure: CodeBlockItemSyntax( + item: CodeBlockItemSyntax.Item( + UsingDeclSyntax( + UnexpectedNodesSyntax([ + Syntax( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: TypeSyntax(IdentifierTypeSyntax(name: .identifier("MainActor"))) + ) + ) + ]), + usingKeyword: .keyword(.using), + specifier: UsingDeclSyntax.Specifier(.identifier("", presence: .missing)) + ) + ) + ), + diagnostics: [ + DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '@MainActor' before using"), + DiagnosticSpec(locationMarker: "2️⃣", message: "expected identifier in using", fixIts: ["insert identifier"]), + ], + fixedSource: """ + @MainActor + using <#identifier#> + """ ) assertParse(