Repository: pointfreeco/swift-case-paths (swift)

Issue:
Build fails if enum is defined in a public extension
**Describe the bug**
I have an enum that is defined in an extension that is `public`. The enum inherits the `public` access modifier from the extension, however, the `AllCasePaths` struct and `allCasePaths` properties do not. This causes the build to fail since these two pieces do not have the same access modifier as the enum that is configured with `CasePathable`.

If I add the `public` access modifier directly to the enum then everything works as expected. Even if I move the `public` keyword, we use `SwiftFormat`, that has a rule that moves that modifier to the extension. We would have to disable that rule to make it work. Would it be possible that this could work in both contexts, or is this a limitation of macros?

```swift
// This will fail to compile
public extension ABTest {
    @CasePathable
    enum Identifier {
    }
}

// This will compile without any issues
extension ABTest {
    @CasePathable
    public enum Identifier {
    }
}
```


**Environment**
  - swift-case-paths 1.2.3
  - Xcode 15.1
  - Swift 5.9

Pull request description:
This applies `public` access to everything inside the enum, which will gracefully degrade to the access control of the context (fixes #148).

This also applies the availability clause of the declaration to the extension.

Patch:
diff --git a/Sources/CasePathsMacros/CasePathableMacro.swift b/Sources/CasePathsMacros/CasePathableMacro.swift
index 9db4b97f..6e450484 100644
--- a/Sources/CasePathsMacros/CasePathableMacro.swift
+++ b/Sources/CasePathsMacros/CasePathableMacro.swift
@@ -38,7 +38,7 @@ extension CasePathableMacro: ExtensionMacro {
     }
     let ext: DeclSyntax =
       """
-      extension \(type.trimmed): \(raw: Self.qualifiedConformanceName) {}
+      \(declaration.attributes.availability)extension \(type.trimmed): \(raw: Self.qualifiedConformanceName) {}
       """
     return [ext.cast(ExtensionDeclSyntax.self)]
   }
@@ -64,8 +64,6 @@ extension CasePathableMacro: MemberMacro {
     }
     let enumName = enumDecl.name.trimmed
 
-    let access = enumDecl.modifiers.first(where: \.isNeededAccessLevelModifier)
-
     let rewriter = SelfRewriter(selfEquivalent: enumName)
     let memberBlock = rewriter.rewrite(enumDecl.memberBlock).cast(MemberBlockSyntax.self)
 
@@ -87,26 +85,25 @@ extension CasePathableMacro: MemberMacro {
       seenCaseNames.insert(name)
     }
 
-    let casePaths = generateDeclSyntax(from: memberBlock.members, with: access, enumName: enumName)
+    let casePaths = generateDeclSyntax(from: memberBlock.members, enumName: enumName)
 
     return [
       """
-      \(access)struct AllCasePaths {
+      public struct AllCasePaths {
       \(raw: casePaths.map(\.description).joined(separator: "\n"))
       }
-      \(access)static var allCasePaths: AllCasePaths { AllCasePaths() }
+      public static var allCasePaths: AllCasePaths { AllCasePaths() }
       """
     ]
   }
 
   static func generateDeclSyntax(
     from elements: MemberBlockItemListSyntax,
-    with access: DeclModifierListSyntax.Element?,
     enumName: TokenSyntax
   ) -> [DeclSyntax] {
     elements.flatMap {
       if let elements = $0.decl.as(EnumCaseDeclSyntax.self)?.elements {
-        return generateDeclSyntax(from: elements, with: access, enumName: enumName)
+        return generateDeclSyntax(from: elements, enumName: enumName)
       }
       if let ifConfigDecl = $0.decl.as(IfConfigDeclSyntax.self) {
         let ifClauses = ifConfigDecl.clauses.flatMap { decl -> [DeclSyntax] in
@@ -115,7 +112,7 @@ extension CasePathableMacro: MemberMacro {
           }
           let title = "\(decl.poundKeyword.text) \(decl.condition?.description ?? "")"
           return ["\(raw: title)"]
-            + generateDeclSyntax(from: elements, with: access, enumName: enumName)
+            + generateDeclSyntax(from: elements, enumName: enumName)
         }
         return ifClauses + ["#endif"]
       }
@@ -125,7 +122,6 @@ extension CasePathableMacro: MemberMacro {
 
   static func generateDeclSyntax(
     from enumCaseDecls: EnumCaseElementListSyntax,
-    with access: DeclModifierListSyntax.Element?,
     enumName: TokenSyntax
   ) -> [DeclSyntax] {
     enumCaseDecls.map {
@@ -146,7 +142,7 @@ extension CasePathableMacro: MemberMacro {
       }
 
       return """
-        \(access)var \(caseName): \
+        public var \(caseName): \
         \(raw: qualifiedCasePathTypeName)<\(enumName), \(raw: associatedValueName)> {
         \(raw: qualifiedCasePathTypeName)<\(enumName), \(raw: associatedValueName)>(
         embed: \(raw: hasPayload ? "\(enumName).\(caseName)" : "{ \(enumName).\(caseName) }"),
@@ -207,6 +203,96 @@ extension CasePathableMacroDiagnostic: DiagnosticMessage {
   }
 }
 
+extension AttributeListSyntax {
+  var availability: AttributeListSyntax? {
+    var elements = [AttributeListSyntax.Element]()
+    for element in self {
+      if let availability = element.availability {
+        elements.append(availability)
+      }
+    }
+    if elements.isEmpty {
+      return nil
+    }
+    return AttributeListSyntax(elements)
+  }
+}
+
+extension AttributeListSyntax.Element {
+  var availability: AttributeListSyntax.Element? {
+    switch self {
+    case .attribute(let attribute):
+      if let availability = attribute.availability {
+        return .attribute(availability)
+      }
+    case .ifConfigDecl(let ifConfig):
+      if let availability = ifConfig.availability {
+        return .ifConfigDecl(availability)
+      }
+    }
+    return nil
+  }
+}
+
+extension AttributeSyntax {
+  var availability: AttributeSyntax? {
+    if attributeName.identifier == "available" {
+      return self
+    } else {
+      return nil
+    }
+  }
+}
+
+extension IfConfigClauseSyntax {
+  var availability: IfConfigClauseSyntax? {
+    if let availability = elements?.availability {
+      return with(\.elements, availability)
+    } else {
+      return nil
+    }
+  }
+
+  var clonedAsIf: IfConfigClauseSyntax {
+    detached.with(\.poundKeyword, .poundIfToken())
+  }
+}
+
+extension IfConfigClauseSyntax.Elements {
+  var availability: IfConfigClauseSyntax.Elements? {
+    switch self {
+    case .attributes(let attributes):
+      if let availability = attributes.availability {
+        return .attributes(availability)
+      } else {
+        return nil
+      }
+    default:
+      return nil
+    }
+  }
+}
+
+extension IfConfigDeclSyntax {
+  var availability: IfConfigDeclSyntax? {
+    var elements = [IfConfigClauseListSyntax.Element]()
+    for clause in clauses {
+      if let availability = clause.availability {
+        if elements.isEmpty {
+          elements.append(availability.clonedAsIf)
+        } else {
+          elements.append(availability)
+        }
+      }
+    }
+    if elements.isEmpty {
+      return nil
+    } else {
+      return with(\.clauses, IfConfigClauseListSyntax(elements))
+    }
+  }
+}
+
 extension DeclGroupSyntax {
   var keyword: Syntax {
     switch self {
@@ -266,16 +352,6 @@ extension DeclGroupSyntax {
   }
 }
 
-extension DeclModifierSyntax {
-  var isNeededAccessLevelModifier: Bool {
-    switch self.name.tokenKind {
-    case .keyword(.public): return true
-    case .keyword(.package): return true
-    default: return false
-    }
-  }
-}
-
 extension EnumCaseElementListSyntax.Element {
   var trimmedTypeDescription: String {
     if var associatedValue = self.parameterClause, !associatedValue.parameters.isEmpty {
@@ -311,6 +387,20 @@ extension SyntaxStringInterpolation {
   }
 }
 
+extension TypeSyntax {
+  var identifier: String? {
+    for token in tokens(viewMode: .all) {
+      switch token.tokenKind {
+      case .identifier(let identifier):
+        return identifier
+      default:
+        break
+      }
+    }
+    return nil
+  }
+}
+
 final class SelfRewriter: SyntaxRewriter {
   let selfEquivalent: TokenSyntax
 


Test patch:
diff --git a/Tests/CasePathsMacrosTests/CasePathableMacroTests.swift b/Tests/CasePathsMacrosTests/CasePathableMacroTests.swift
index 0b878b72..1851a756 100644
--- a/Tests/CasePathsMacrosTests/CasePathableMacroTests.swift
+++ b/Tests/CasePathsMacrosTests/CasePathableMacroTests.swift
@@ -31,8 +31,8 @@ final class CasePathableMacroTests: XCTestCase {
         case fizz(buzz: String)
         case fizzier(Int, buzzier: String)
 
-        struct AllCasePaths {
-          var bar: CasePaths.AnyCasePath<Foo, Void> {
+        public struct AllCasePaths {
+          public var bar: CasePaths.AnyCasePath<Foo, Void> {
             CasePaths.AnyCasePath<Foo, Void>(
               embed: {
                 Foo.bar
@@ -45,7 +45,7 @@ final class CasePathableMacroTests: XCTestCase {
               }
             )
           }
-          var baz: CasePaths.AnyCasePath<Foo, Int> {
+          public var baz: CasePaths.AnyCasePath<Foo, Int> {
             CasePaths.AnyCasePath<Foo, Int>(
               embed: Foo.baz,
               extract: {
@@ -56,7 +56,7 @@ final class CasePathableMacroTests: XCTestCase {
               }
             )
           }
-          var fizz: CasePaths.AnyCasePath<Foo, String> {
+          public var fizz: CasePaths.AnyCasePath<Foo, String> {
             CasePaths.AnyCasePath<Foo, String>(
               embed: Foo.fizz,
               extract: {
@@ -67,7 +67,7 @@ final class CasePathableMacroTests: XCTestCase {
               }
             )
           }
-          var fizzier: CasePaths.AnyCasePath<Foo, (Int, buzzier: String)> {
+          public var fizzier: CasePaths.AnyCasePath<Foo, (Int, buzzier: String)> {
             CasePaths.AnyCasePath<Foo, (Int, buzzier: String)>(
               embed: Foo.fizzier,
               extract: {
@@ -79,7 +79,7 @@ final class CasePathableMacroTests: XCTestCase {
             )
           }
         }
-        static var allCasePaths: AllCasePaths { AllCasePaths() }
+        public static var allCasePaths: AllCasePaths { AllCasePaths() }
       }
 
       extension Foo: CasePaths.CasePathable {
@@ -176,8 +176,8 @@ final class CasePathableMacroTests: XCTestCase {
       package enum Foo {
         case bar(Int)
 
-        package struct AllCasePaths {
-          package var bar: CasePaths.AnyCasePath<Foo, Int> {
+        public struct AllCasePaths {
+          public var bar: CasePaths.AnyCasePath<Foo, Int> {
             CasePaths.AnyCasePath<Foo, Int>(
               embed: Foo.bar,
               extract: {
@@ -189,7 +189,7 @@ final class CasePathableMacroTests: XCTestCase {
             )
           }
         }
-        package static var allCasePaths: AllCasePaths { AllCasePaths() }
+        public static var allCasePaths: AllCasePaths { AllCasePaths() }
       }
 
       extension Foo: CasePaths.CasePathable {
@@ -207,8 +207,8 @@ final class CasePathableMacroTests: XCTestCase {
       private enum Foo {
         case bar(Int)
 
-        struct AllCasePaths {
-          var bar: CasePaths.AnyCasePath<Foo, Int> {
+        public struct AllCasePaths {
+          public var bar: CasePaths.AnyCasePath<Foo, Int> {
             CasePaths.AnyCasePath<Foo, Int>(
               embed: Foo.bar,
               extract: {
@@ -220,7 +220,7 @@ final class CasePathableMacroTests: XCTestCase {
             )
           }
         }
-        static var allCasePaths: AllCasePaths { AllCasePaths() }
+        public static var allCasePaths: AllCasePaths { AllCasePaths() }
       }
 
       extension Foo: CasePaths.CasePathable {
@@ -275,10 +275,10 @@ final class CasePathableMacroTests: XCTestCase {
       """
       enum Foo: CasePathable {
 
-          struct AllCasePaths {
+          public struct AllCasePaths {
 
           }
-          static var allCasePaths: AllCasePaths { AllCasePaths() }
+          public static var allCasePaths: AllCasePaths { AllCasePaths() }
       }
       """
     }
@@ -291,10 +291,10 @@ final class CasePathableMacroTests: XCTestCase {
       """
       enum Foo: CasePaths.CasePathable {
 
-          struct AllCasePaths {
+          public struct AllCasePaths {
 
           }
-          static var allCasePaths: AllCasePaths { AllCasePaths() }
+          public static var allCasePaths: AllCasePaths { AllCasePaths() }
       }
       """
     }
@@ -312,8 +312,8 @@ final class CasePathableMacroTests: XCTestCase {
       enum Foo {
         case bar(_ int: Int, _ bool: Bool)
 
-        struct AllCasePaths {
-          var bar: CasePaths.AnyCasePath<Foo, (Int, Bool)> {
+        public struct AllCasePaths {
+          public var bar: CasePaths.AnyCasePath<Foo, (Int, Bool)> {
             CasePaths.AnyCasePath<Foo, (Int, Bool)>(
               embed: Foo.bar,
               extract: {
@@ -325,7 +325,7 @@ final class CasePathableMacroTests: XCTestCase {
             )
           }
         }
-        static var allCasePaths: AllCasePaths { AllCasePaths() }
+        public static var allCasePaths: AllCasePaths { AllCasePaths() }
       }
 
       extension Foo: CasePaths.CasePathable {
@@ -346,8 +346,8 @@ final class CasePathableMacroTests: XCTestCase {
       enum Foo {
         case bar(Bar<Self>)
 
-        struct AllCasePaths {
-          var bar: CasePaths.AnyCasePath<Foo, Bar<Foo>> {
+        public struct AllCasePaths {
+          public var bar: CasePaths.AnyCasePath<Foo, Bar<Foo>> {
             CasePaths.AnyCasePath<Foo, Bar<Foo>>(
               embed: Foo.bar,
               extract: {
@@ -359,7 +359,7 @@ final class CasePathableMacroTests: XCTestCase {
             )
           }
         }
-        static var allCasePaths: AllCasePaths { AllCasePaths() }
+        public static var allCasePaths: AllCasePaths { AllCasePaths() }
       }
 
       extension Foo: CasePaths.CasePathable {
@@ -380,8 +380,8 @@ final class CasePathableMacroTests: XCTestCase {
       enum Foo {
         case bar(int: Int = 42, bool: Bool = true)
 
-        struct AllCasePaths {
-          var bar: CasePaths.AnyCasePath<Foo, (int: Int, bool: Bool)> {
+        public struct AllCasePaths {
+          public var bar: CasePaths.AnyCasePath<Foo, (int: Int, bool: Bool)> {
             CasePaths.AnyCasePath<Foo, (int: Int, bool: Bool)>(
               embed: Foo.bar,
               extract: {
@@ -393,7 +393,7 @@ final class CasePathableMacroTests: XCTestCase {
             )
           }
         }
-        static var allCasePaths: AllCasePaths { AllCasePaths() }
+        public static var allCasePaths: AllCasePaths { AllCasePaths() }
       }
 
       extension Foo: CasePaths.CasePathable {
@@ -448,8 +448,8 @@ final class CasePathableMacroTests: XCTestCase {
         #endif
         #endif
 
-        struct AllCasePaths {
-          var bar: CasePaths.AnyCasePath<Foo, Void> {
+        public struct AllCasePaths {
+          public var bar: CasePaths.AnyCasePath<Foo, Void> {
             CasePaths.AnyCasePath<Foo, Void>(
               embed: {
                 Foo.bar
@@ -463,7 +463,7 @@ final class CasePathableMacroTests: XCTestCase {
             )
           }
           #if os(macOS)
-          var macCase: CasePaths.AnyCasePath<Foo, Void> {
+          public var macCase: CasePaths.AnyCasePath<Foo, Void> {
             CasePaths.AnyCasePath<Foo, Void>(
               embed: {
                 Foo.macCase
@@ -476,7 +476,7 @@ final class CasePathableMacroTests: XCTestCase {
               }
             )
           }
-          var macSecond: CasePaths.AnyCasePath<Foo, Int> {
+          public var macSecond: CasePaths.AnyCasePath<Foo, Int> {
             CasePaths.AnyCasePath<Foo, Int>(
               embed: Foo.macSecond,
               extract: {
@@ -488,7 +488,7 @@ final class CasePathableMacroTests: XCTestCase {
             )
           }
           #elseif os(iOS)
-          var iosCase: CasePaths.AnyCasePath<Foo, Void> {
+          public var iosCase: CasePaths.AnyCasePath<Foo, Void> {
             CasePaths.AnyCasePath<Foo, Void>(
               embed: {
                 Foo.iosCase
@@ -502,7 +502,7 @@ final class CasePathableMacroTests: XCTestCase {
             )
           }
           #else
-          var elseCase: CasePaths.AnyCasePath<Foo, String> {
+          public var elseCase: CasePaths.AnyCasePath<Foo, String> {
             CasePaths.AnyCasePath<Foo, String>(
               embed: Foo.elseCase,
               extract: {
@@ -513,7 +513,7 @@ final class CasePathableMacroTests: XCTestCase {
               }
             )
           }
-          var elseLast: CasePaths.AnyCasePath<Foo, Void> {
+          public var elseLast: CasePaths.AnyCasePath<Foo, Void> {
             CasePaths.AnyCasePath<Foo, Void>(
               embed: {
                 Foo.elseLast
@@ -529,7 +529,7 @@ final class CasePathableMacroTests: XCTestCase {
           #endif
           #if DEBUG
           #if INNER
-          var twoLevelsDeep: CasePaths.AnyCasePath<Foo, Void> {
+          public var twoLevelsDeep: CasePaths.AnyCasePath<Foo, Void> {
             CasePaths.AnyCasePath<Foo, Void>(
               embed: {
                 Foo.twoLevelsDeep
@@ -542,7 +542,7 @@ final class CasePathableMacroTests: XCTestCase {
               }
             )
           }
-          var twoLevels: CasePaths.AnyCasePath<Foo, Double> {
+          public var twoLevels: CasePaths.AnyCasePath<Foo, Double> {
             CasePaths.AnyCasePath<Foo, Double>(
               embed: Foo.twoLevels,
               extract: {
@@ -556,7 +556,7 @@ final class CasePathableMacroTests: XCTestCase {
           #endif
           #endif
         }
-        static var allCasePaths: AllCasePaths { AllCasePaths() }
+        public static var allCasePaths: AllCasePaths { AllCasePaths() }
       }
 
       extension Foo: CasePaths.CasePathable {
@@ -565,4 +565,42 @@ final class CasePathableMacroTests: XCTestCase {
     }
   }
 
+  func testAvailability() {
+    assertMacro {
+      """
+      @available(iOS, unavailable)
+      @CasePathable
+      enum Foo {
+        case bar
+      }
+      """
+    } expansion: {
+      """
+      @available(iOS, unavailable)
+      enum Foo {
+        case bar
+
+        public struct AllCasePaths {
+          public var bar: CasePaths.AnyCasePath<Foo, Void> {
+            CasePaths.AnyCasePath<Foo, Void>(
+              embed: {
+                Foo.bar
+              },
+              extract: {
+                guard case .bar = $0 else {
+                  return nil
+                }
+                return ()
+              }
+            )
+          }
+        }
+        public static var allCasePaths: AllCasePaths { AllCasePaths() }
+      }
+
+      @available(iOS, unavailable) extension Foo: CasePaths.CasePathable {
+      }
+      """
+    }
+  }
 }