diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5a1eedb8ec8..fd8e6f14272 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -2,7 +2,7 @@ name: Pull request on: pull_request: - types: [opened, reopened, synchronize] + types: [opened, reopened, synchronize, ready_for_review] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -11,15 +11,20 @@ concurrency: jobs: tests: name: Test + # PRs created by GitHub Actions don't kick off further actions (https://github.com/peter-evans/create-pull-request/blob/d57e551ebc1a16dee0b8c9ea6d24dba7627a6e35/docs/concepts-guidelines.md#triggering-further-workflow-runs). + # As a workaround, we mark automerge PRs that are created by GitHub actions as draft and trigger the GitHub actions by marking the PR as ready for review. But we don't want to re-trigger testing this when a normal user's PR is marked as ready for review. + if: (github.event.action != 'ready_for_review') || (github.event.action == 'ready_for_review' && github.event.pull_request.user.login == 'github-actions[bot]') uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main soundness: name: Soundness + if: (github.event.action != 'ready_for_review') || (github.event.action == 'ready_for_review' && github.event.pull_request.user.login == 'github-actions[bot]') uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: api_breakage_check_enabled: false # https://github.com/swiftlang/swift-syntax/issues/3010 docs_check_additional_arguments: "--disable-parameters-and-returns-validation" verify_source_code: name: Validate generated code + if: (github.event.action != 'ready_for_review') || (github.event.action == 'ready_for_review' && github.event.pull_request.user.login == 'github-actions[bot]') runs-on: ubuntu-latest container: image: swift:latest @@ -32,6 +37,7 @@ jobs: run: "./swift-syntax-dev-utils verify-source-code --toolchain /usr" test_using_swift_syntax_dev_utils_linux: name: Run tests using swift-syntax-dev-utils (Linux) + if: (github.event.action != 'ready_for_review') || (github.event.action == 'ready_for_review' && github.event.pull_request.user.login == 'github-actions[bot]') runs-on: ubuntu-latest container: image: swift:latest @@ -44,6 +50,7 @@ jobs: run: "./swift-syntax-dev-utils test --enable-rawsyntax-validation --enable-test-fuzzing --toolchain /usr" test_using_swift_syntax_dev_utils_windows: name: Run tests using swift-syntax-dev-utils (Windows) + if: (github.event.action != 'ready_for_review') || (github.event.action == 'ready_for_review' && github.event.pull_request.user.login == 'github-actions[bot]') runs-on: windows-2022 steps: - name: Pull Docker image diff --git a/Sources/SwiftParser/Attributes.swift b/Sources/SwiftParser/Attributes.swift index 2f066fd6c39..3cfc6ae801d 100644 --- a/Sources/SwiftParser/Attributes.swift +++ b/Sources/SwiftParser/Attributes.swift @@ -66,7 +66,7 @@ extension Parser { case TokenSpec(._specialize): self = ._specialize case TokenSpec(._spi_available): self = ._spi_available case TokenSpec(.`rethrows`): self = .rethrows - case TokenSpec(.abi) where experimentalFeatures.contains(.abiAttribute): self = .abi + case TokenSpec(.abi): self = .abi case TokenSpec(.attached): self = .attached case TokenSpec(.available): self = .available case TokenSpec(.backDeployed): self = .backDeployed @@ -139,8 +139,12 @@ extension Parser { /// - parseMissingArguments: If provided, called instead of `parseArgument` when an argument list was required but no opening parenthesis was present. mutating func parseAttribute( argumentMode: AttributeArgumentMode, - parseArguments: (inout Parser) -> RawAttributeSyntax.Arguments, - parseMissingArguments: ((inout Parser) -> RawAttributeSyntax.Arguments)? = nil + parseArguments: (inout Parser) -> ( + unexpectedBefore: RawUnexpectedNodesSyntax?, arguments: RawAttributeSyntax.Arguments + ), + parseMissingArguments: ( + (inout Parser) -> (unexpectedBefore: RawUnexpectedNodesSyntax?, arguments: RawAttributeSyntax.Arguments) + )? = nil ) -> RawAttributeListSyntax.Element { var (unexpectedBeforeAtSign, atSign) = self.expect(.atSign) if atSign.trailingTriviaByteLength > 0 || self.currentToken.leadingTriviaByteLength > 0 { @@ -175,11 +179,12 @@ extension Parser { ) leftParen = leftParen.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena) } + let unexpectedBeforeArguments: RawUnexpectedNodesSyntax? let argument: RawAttributeSyntax.Arguments if let parseMissingArguments, leftParen.presence == .missing { - argument = parseMissingArguments(&self) + (unexpectedBeforeArguments, argument) = parseMissingArguments(&self) } else { - argument = parseArguments(&self) + (unexpectedBeforeArguments, argument) = parseArguments(&self) } let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen) return .attribute( @@ -189,6 +194,7 @@ extension Parser { attributeName: attributeName, unexpectedBeforeLeftParen, leftParen: leftParen, + unexpectedBeforeArguments, arguments: argument, unexpectedBeforeRightParen, rightParen: rightParen, @@ -224,41 +230,41 @@ extension Parser { switch peek(isAtAnyIn: DeclarationAttributeWithSpecialSyntax.self) { case .abi: return parseAttribute(argumentMode: .required) { parser in - return .abiArguments(parser.parseABIAttributeArguments()) + return parser.parseABIAttributeArguments() } parseMissingArguments: { parser in - return .abiArguments(parser.parseABIAttributeArguments(missingLParen: true)) + return parser.parseABIAttributeArguments(missingLParen: true) } case .available, ._spi_available: return parseAttribute(argumentMode: .required) { parser in - return .availability(parser.parseAvailabilityArgumentSpecList()) + return (nil, .availability(parser.parseAvailabilityArgumentSpecList())) } case .backDeployed, ._backDeploy: return parseAttribute(argumentMode: .required) { parser in - return .backDeployedArguments(parser.parseBackDeployedAttributeArguments()) + return (nil, .backDeployedArguments(parser.parseBackDeployedAttributeArguments())) } case .differentiable: return parseAttribute(argumentMode: .required) { parser in - return .differentiableArguments(parser.parseDifferentiableAttributeArguments()) + return (nil, .differentiableArguments(parser.parseDifferentiableAttributeArguments())) } case .derivative, .transpose: return parseAttribute(argumentMode: .required) { parser in - return .derivativeRegistrationArguments(parser.parseDerivativeAttributeArguments()) + return (nil, .derivativeRegistrationArguments(parser.parseDerivativeAttributeArguments())) } case .objc: return parseAttribute(argumentMode: .optional) { parser in - return .objCName(parser.parseObjectiveCSelector()) + return (nil, .objCName(parser.parseObjectiveCSelector())) } case ._specialize: return parseAttribute(argumentMode: .required) { parser in - return .specializeArguments(parser.parseSpecializeAttributeArgumentList()) + return (nil, .specializeArguments(parser.parseSpecializeAttributeArgumentList())) } case ._dynamicReplacement: return parseAttribute(argumentMode: .required) { parser in - return .dynamicReplacementArguments(parser.parseDynamicReplacementAttributeArguments()) + return (nil, .dynamicReplacementArguments(parser.parseDynamicReplacementAttributeArguments())) } case ._documentation: return parseAttribute(argumentMode: .required) { parser in - return .documentationArguments(parser.parseDocumentationAttributeArguments()) + return (nil, .documentationArguments(parser.parseDocumentationAttributeArguments())) } case ._effects: return parseAttribute(argumentMode: .required) { parser in @@ -268,20 +274,20 @@ extension Parser { while !parser.at(.rightParen, .endOfFile) { tokens.append(parser.consumeAnyToken()) } - return .effectsArguments(RawEffectsAttributeArgumentListSyntax(elements: tokens, arena: parser.arena)) + return (nil, .effectsArguments(RawEffectsAttributeArgumentListSyntax(elements: tokens, arena: parser.arena))) } case ._implements: return parseAttribute(argumentMode: .required) { parser in - return .implementsArguments(parser.parseImplementsAttributeArguments()) + return (nil, .implementsArguments(parser.parseImplementsAttributeArguments())) } case ._originallyDefinedIn: return parseAttribute(argumentMode: .required) { parser in - return .originallyDefinedInArguments(parser.parseOriginallyDefinedInAttributeArguments()) + return (nil, .originallyDefinedInArguments(parser.parseOriginallyDefinedInAttributeArguments())) } case .attached, .freestanding: return parseAttribute(argumentMode: .customAttribute) { parser in let arguments = parser.parseMacroRoleArguments() - return .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena)) + return (nil, .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena))) } case .rethrows: let (unexpectedBeforeAtSign, atSign) = self.expect(.atSign) @@ -308,7 +314,7 @@ extension Parser { pattern: .none, allowTrailingComma: true ) - return .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena)) + return (nil, .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena))) } } } @@ -786,9 +792,11 @@ extension Parser { /// Parse the arguments inside an `@abi(...)` attribute. /// /// - Parameter missingLParen: `true` if the opening paren for the argument list was missing. - mutating func parseABIAttributeArguments(missingLParen: Bool = false) -> RawABIAttributeArgumentsSyntax { - func makeMissingProviderArguments(unexpectedBefore: [RawSyntax]) -> RawABIAttributeArgumentsSyntax { - return RawABIAttributeArgumentsSyntax( + mutating func parseABIAttributeArguments( + missingLParen: Bool = false + ) -> (RawUnexpectedNodesSyntax?, RawAttributeSyntax.Arguments) { + func makeMissingProviderArguments(unexpectedBefore: [RawSyntax]) -> RawAttributeSyntax.Arguments { + let args = RawABIAttributeArgumentsSyntax( provider: .missing( RawMissingDeclSyntax( unexpectedBefore.isEmpty ? nil : RawUnexpectedNodesSyntax(elements: unexpectedBefore, arena: self.arena), @@ -800,6 +808,7 @@ extension Parser { ), arena: self.arena ) + return .abiArguments(args) } // Consider the three kinds of mistakes we might see here: @@ -815,16 +824,23 @@ extension Parser { // In lieu of that, we judge that recovering gracefully from #3 is more important than #2 and therefore do not even // attempt to parse the argument unless we've seen a left paren. guard !missingLParen && !self.at(.rightParen) else { - return makeMissingProviderArguments(unexpectedBefore: []) + return (nil, makeMissingProviderArguments(unexpectedBefore: [])) } let decl = parseDeclaration(in: .argumentList) + guard experimentalFeatures.contains(.abiAttribute) else { + return ( + RawUnexpectedNodesSyntax([decl], arena: self.arena), + .argumentList(RawLabeledExprListSyntax(elements: [], arena: self.arena)) + ) + } + guard let provider = RawABIAttributeArgumentsSyntax.Provider(decl) else { - return makeMissingProviderArguments(unexpectedBefore: [decl.raw]) + return (nil, makeMissingProviderArguments(unexpectedBefore: [decl.raw])) } - return RawABIAttributeArgumentsSyntax(provider: provider, arena: self.arena) + return (nil, .abiArguments(RawABIAttributeArgumentsSyntax(provider: provider, arena: self.arena))) } } diff --git a/Sources/SwiftParser/Types.swift b/Sources/SwiftParser/Types.swift index 416f06ddc1f..67c7604ce0f 100644 --- a/Sources/SwiftParser/Types.swift +++ b/Sources/SwiftParser/Types.swift @@ -1291,7 +1291,7 @@ extension Parser { case .isolated: return parseAttribute(argumentMode: .required) { parser in - return .argumentList(parser.parseIsolatedAttributeArguments()) + return (nil, .argumentList(parser.parseIsolatedAttributeArguments())) } case .convention, ._opaqueReturnTypeOf, nil: // Custom attribute return parseAttribute(argumentMode: .customAttribute) { parser in @@ -1299,7 +1299,7 @@ extension Parser { pattern: .none, allowTrailingComma: true ) - return .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena)) + return (nil, .argumentList(RawLabeledExprListSyntax(elements: arguments, arena: parser.arena))) } } diff --git a/Tests/SwiftParserTest/AttributeTests.swift b/Tests/SwiftParserTest/AttributeTests.swift index 6415986681d..1bc34070d1b 100644 --- a/Tests/SwiftParserTest/AttributeTests.swift +++ b/Tests/SwiftParserTest/AttributeTests.swift @@ -1283,6 +1283,49 @@ final class AttributeTests: ParserTestCase { ) } + func testABIAttributeWithoutFeature() throws { + assertParse( + """ + @abi(1️⃣func fn() -> Int2️⃣) + func fn1() -> Int { } + """, + substructure: FunctionDeclSyntax( + attributes: [ + .attribute( + AttributeSyntax( + attributeName: TypeSyntax("abi"), + leftParen: .leftParenToken(), + [Syntax(try FunctionDeclSyntax("func fn() -> Int"))], + arguments: .argumentList([]), + rightParen: .rightParenToken() + ) + ) + ], + name: "fn1", + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax {}, + returnClause: ReturnClauseSyntax(type: TypeSyntax("Int")) + ) + ) {}, + diagnostics: [ + DiagnosticSpec( + locationMarker: "1️⃣", + message: "unexpected code 'func fn() -> Int' in attribute" + ), + DiagnosticSpec( + locationMarker: "2️⃣", + message: "expected argument for '@abi' attribute", + fixIts: ["insert attribute argument"] + ), + ], + fixedSource: """ + @abi(func fn() -> Int) + func fn1() -> Int { } + """, + experimentalFeatures: [] + ) + } + func testSpaceBetweenAtAndAttribute() { assertParse( "@1️⃣ custom func foo() {}",