Repository: apple/swift-syntax (swift)

Issue:
SE-0367 unimplemented
### Issue Kind

Parse of Valid Source Failed

### Source Code

```swift
#if hasAttribute(foo)
@foo
#endif
struct MyStruct {}
```

### Description

This will currently produce a `expected declaration after attribute in conditional compilation clause` diagnostic, but since SE-0367 this is now valid code. See https://github.com/apple/swift/pull/60208 and https://github.com/apple/swift/pull/60720. The test cases in `DirectiveTests` will also need to be updated.

Pull request:
Fixes apple/swift-syntax#1119
rdar://103048943

Patch:
diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift
index ab51b7d0eb2..b4991786953 100644
--- a/Sources/SwiftParser/Declarations.swift
+++ b/Sources/SwiftParser/Declarations.swift
@@ -165,6 +165,11 @@ extension Parser {
   public mutating func parseDeclaration(inMemberDeclList: Bool = false) -> RawDeclSyntax {
     switch self.at(anyIn: PoundDeclarationStart.self) {
     case (.poundIfKeyword, _)?:
+      if self.withLookahead({ $0.consumeIfConfigOfAttributes() }) {
+        // If we are at a `#if` of attributes, the `#if` directive should be
+        // parsed when we're parsing the attributes.
+        break
+      }
       let directive = self.parsePoundIfDirective { parser in
         let parsedDecl = parser.parseDeclaration()
         let semicolon = parser.consume(if: .semicolon)
diff --git a/Sources/SwiftParser/Lookahead.swift b/Sources/SwiftParser/Lookahead.swift
index a882ae5b9df..b01880a5e66 100644
--- a/Sources/SwiftParser/Lookahead.swift
+++ b/Sources/SwiftParser/Lookahead.swift
@@ -207,42 +207,37 @@ extension Parser.Lookahead {
     return true
   }
 
+  /// Tries consuming a `#if` directive that only contains attributes.
+  /// Returns `true` if that was successful and `false` if
+  ///  - we are not at a valid `#if` directive (e.g. `#endif` is missing)
+  ///  - the directive contained non-attributes
+  ///  - the directive did not contain any attributes
   mutating func consumeIfConfigOfAttributes() -> Bool {
-    while true {
-      // #if / #else / #elseif
+    assert(self.at(.poundIfKeyword))
+    var didSeeAnyAttributes = false
+    var poundIfLoopProgress = LoopProgressCondition()
+    repeat {
+      assert(self.at(any: [.poundIfKeyword, .poundElseKeyword, .poundElseifKeyword]))
       self.consumeAnyToken()
 
-      // <expression>
+      // <expression> after `#if` or `#elseif`
       self.skipUntilEndOfLine()
 
-      while true {
-        if self.at(.atSign) {
+      var attributesLoopProgress = LoopProgressCondition()
+      ATTRIBUTE_LOOP: while attributesLoopProgress.evaluate(self.currentToken) {
+        switch self.currentToken.rawTokenKind {
+        case .atSign:
+          didSeeAnyAttributes = true
           _ = self.consumeAttributeList()
-          continue
-        }
-
-        if self.at(.poundIfKeyword) {
+        case .poundIfKeyword:
           _ = self.consumeIfConfigOfAttributes()
-          continue
+        default:
+          break ATTRIBUTE_LOOP
         }
-
-        break
       }
+    } while self.at(any: [.poundElseifKeyword, .poundElseKeyword]) && poundIfLoopProgress.evaluate(self.currentToken)
 
-      guard self.at(any: [.poundElseifKeyword, .poundElseKeyword]) else {
-        break
-      }
-    }
-
-    // If we ran out of tokens, say we consumed the rest.
-    if self.at(.eof) {
-      return true
-    }
-
-    guard self.currentToken.isAtStartOfLine else {
-      return false
-    }
-    return self.consume(if: .poundEndifKeyword) != nil
+    return didSeeAnyAttributes && self.currentToken.isAtStartOfLine && self.consume(if: .poundEndifKeyword) != nil
   }
 }
 
diff --git a/Sources/SwiftParser/TopLevel.swift b/Sources/SwiftParser/TopLevel.swift
index 3ceeaf71961..48dc1961a07 100644
--- a/Sources/SwiftParser/TopLevel.swift
+++ b/Sources/SwiftParser/TopLevel.swift
@@ -195,7 +195,9 @@ extension Parser {
   /// If we are not at the top level, such a closing brace should close the
   /// wrapping declaration instead of being consumed by lookeahead.
   private mutating func parseItem(isAtTopLevel: Bool = false, allowInitDecl: Bool = true) -> RawCodeBlockItemSyntax.Item {
-    if self.at(.poundIfKeyword) {
+    if self.at(.poundIfKeyword) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) {
+      // If config of attributes is parsed as part of declaration parsing as it
+      // doesn't constitute its own code block item.
       let directive = self.parsePoundIfDirective {
         $0.parseCodeBlockItem()
       } addSemicolonIfNeeded: { lastElement, newItemAtStartOfLine, parser in


Test patch:
diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift
index 4a12f72d40a..da6da92fc48 100644
--- a/Tests/SwiftParserTest/DeclarationTests.swift
+++ b/Tests/SwiftParserTest/DeclarationTests.swift
@@ -1371,6 +1371,17 @@ final class DeclarationTests: XCTestCase {
       )
     )
   }
+
+  func testAttributeInPoundIf() {
+    AssertParse(
+      """
+      #if hasAttribute(foo)
+      @foo
+      #endif
+      struct MyStruct {}
+      """
+    )
+  }
 }
 
 extension Parser.DeclAttributes {
diff --git a/Tests/SwiftParserTest/DirectiveTests.swift b/Tests/SwiftParserTest/DirectiveTests.swift
index 8859b1fd1c2..a34f0a99f25 100644
--- a/Tests/SwiftParserTest/DirectiveTests.swift
+++ b/Tests/SwiftParserTest/DirectiveTests.swift
@@ -147,25 +147,20 @@ final class DirectiveTests: XCTestCase {
         public struct S2 { }
 
       #if hasAttribute(foo)
-        @foo1️⃣
+        @foo
       #endif
         @inlinable
         func f1() { }
 
       #if hasAttribute(foo)
-        @foo2️⃣
+        @foo
       #else
         @available(*, deprecated, message: "nope")
-        @frozen3️⃣
+        @frozen
       #endif
         public struct S3 { }
       }
-      """,
-      diagnostics: [
-        DiagnosticSpec(locationMarker: "1️⃣", message: "expected declaration after attribute in conditional compilation clause"),
-        DiagnosticSpec(locationMarker: "2️⃣", message: "expected declaration after attribute in conditional compilation clause"),
-        DiagnosticSpec(locationMarker: "3️⃣", message: "expected declaration after attribute in conditional compilation clause"),
-      ]
+      """
     )
   }