diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be2405a7..9ce0a8ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,13 +16,13 @@ concurrency: jobs: macos: name: macOS (Xcode ${{ matrix.xcode }}) - runs-on: macos-15 + runs-on: macos-26 strategy: matrix: xcode: - - '16.4' + - '26.5' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Print Swift version @@ -32,13 +32,13 @@ jobs: macos-library-evolution: name: macOS Library Evolution (Xcode ${{ matrix.xcode }}) - runs-on: macos-15 + runs-on: macos-26 strategy: matrix: xcode: - - '16.4' + - '26.5' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Print Swift version @@ -50,80 +50,25 @@ jobs: strategy: matrix: swift: - - '6.1' + - '6.3' name: Ubuntu (Swift ${{ matrix.swift }}) runs-on: ubuntu-latest container: swift:${{ matrix.swift }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Run tests run: swift test --parallel - name: Run tests (release) run: swift test -c release --parallel - wasm: - name: Wasm - runs-on: ubuntu-latest - env: - OMIT_MACRO_TESTS: 1 - steps: - - uses: actions/checkout@v4 - - uses: bytecodealliance/actions/wasmtime/setup@v1 - - name: Install Swift and Swift SDK for WebAssembly - run: | - PREFIX=/opt/swift - set -ex - curl -f -o /tmp/swift.tar.gz "https://download.swift.org/swift-6.0.2-release/ubuntu2204/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE-ubuntu22.04.tar.gz" - sudo mkdir -p $PREFIX; sudo tar -xzf /tmp/swift.tar.gz -C $PREFIX --strip-component 1 - $PREFIX/usr/bin/swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip --checksum 6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4 - echo "$PREFIX/usr/bin" >> $GITHUB_PATH - - - name: Build tests - run: swift build --swift-sdk wasm32-unknown-wasi --build-tests -Xlinker -z -Xlinker stack-size=$((1024 * 1024)) - - name: Run tests - run: wasmtime --dir . .build/debug/swift-case-pathsPackageTests.wasm - check-macro-compatibility: name: Check Macro Compatibility runs-on: macos-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Run Swift Macro Compatibility Check uses: Matejkob/swift-macro-compatibility-check@v1 with: run-tests: false major-versions-only: true - - # windows: - # name: Windows (Swift ${{ matrix.swift }}, ${{ matrix.config }}) - # strategy: - # matrix: - # os: [windows-latest] - # config: - # - debug - # #- release - # swift: ['6.0'] - # fail-fast: false - # runs-on: ${{ matrix.os }} - # steps: - # - uses: compnerd/gha-setup-swift@main - # with: - # branch: swift-${{ matrix.swift }}-branch - # tag: ${{ matrix.swift }}-DEVELOPMENT-SNAPSHOT-2024-06-03-a - # - uses: actions/checkout@v4 - # - name: Build - # run: swift build -c ${{ matrix.config }} - - # android: - # strategy: - # matrix: - # swift: - # - "6.0.2" - # name: Android - # runs-on: ubuntu-latest - # env: - # OMIT_MACRO_TESTS: 1 - # steps: - # - uses: actions/checkout@v4 - # - uses: skiptools/swift-android-action@v2 diff --git a/Package.resolved b/Package.resolved index 9c26ecfd..c6fa31b8 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "59cc99159ed0e92e8d8f4c968aec66867f0cad74c08e37e6ff142f59cf8d8ff5", + "originHash" : "554047fdf95319c47fc997977c4a6a4828cefd919ce227259d0bd6daf4672dcd", "pins" : [ { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", - "version" : "1.3.3" + "revision" : "b9b59eb58c946236d6f16305c576ad194c36444e", + "version" : "1.6.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin", "state" : { - "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", - "version" : "1.4.5" + "revision" : "647c708be89f834fa6a6d4945442793a77ddf5b6", + "version" : "1.5.0" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-macro-testing", "state" : { - "revision" : "9ab11325daa51c7c5c10fcf16c92bac906717c7e", - "version" : "0.6.4" + "revision" : "2e494c632d510715c96a694ff25e2a8d4ac3f64b", + "version" : "0.6.5" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "a8b7c5e0ed33d8ab8887d1654d9b59f2cbad529b", - "version" : "1.18.7" + "revision" : "ad5e3190cc63dc288f28546f9c6827efc1e9d495", + "version" : "1.19.2" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "4799286537280063c85a32f09884cfbca301b1a1", - "version" : "602.0.0" + "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", + "version" : "603.0.2" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "b2ed9eabefe56202ee4939dd9fc46b6241c88317", - "version" : "1.6.1" + "revision" : "cb281f343fd953280336dcbd3822cdf47c182f5b", + "version" : "1.10.0" } } ], diff --git a/Package.swift b/Package.swift index be1193f6..b0354a1c 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,10 @@ let package = Package( name: "CasePathsCore", targets: ["CasePathsCore"] ), + .library( + name: "CasePathsMacrosSupport", + targets: ["CasePathsMacrosSupport"] + ), ], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"605.0.0"), @@ -41,8 +45,8 @@ let package = Package( .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ] ), - .macro( - name: "CasePathsMacros", + .target( + name: "CasePathsMacrosSupport", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), @@ -51,9 +55,21 @@ let package = Package( .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), ] ), + .macro( + name: "CasePathsMacros", + dependencies: [ + "CasePathsMacrosSupport", + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + ] + ), .testTarget( name: "CasePathsTests", - dependencies: ["CasePaths"] + dependencies: [ + "CasePaths", + "CasePathsMacrosSupport", + ] ), ], swiftLanguageModes: [.v6] @@ -75,6 +91,7 @@ if ProcessInfo.processInfo.environment["OMIT_MACRO_TESTS"] == nil { name: "CasePathsMacrosTests", dependencies: [ "CasePathsMacros", + "CasePathsMacrosSupport", .product( name: "MacroTesting", package: "swift-macro-testing" diff --git a/Sources/CasePathsMacros/CasePathableMacro.swift b/Sources/CasePathsMacros/CasePathableMacro.swift index c12891cd..0ab753b5 100644 --- a/Sources/CasePathsMacros/CasePathableMacro.swift +++ b/Sources/CasePathsMacros/CasePathableMacro.swift @@ -1,532 +1,37 @@ -import SwiftDiagnostics +import CasePathsMacrosSupport import SwiftSyntax -import SwiftSyntaxBuilder import SwiftSyntaxMacros -public struct CasePathableMacro { - static let moduleName = "CasePaths" - static let casePathTypeName = "AnyCasePath" -} +public enum CasePathableMacro {} extension CasePathableMacro: ExtensionMacro { - public static func expansion( + public static func expansion( of node: AttributeSyntax, - attachedTo declaration: D, - providingExtensionsOf type: T, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo protocols: [TypeSyntax], - in context: C + in context: some MacroExpansionContext ) throws -> [ExtensionDeclSyntax] { - // if protocols.isEmpty { - // return [] - // } - guard let enumDecl = declaration.as(EnumDeclSyntax.self) - else { - // TODO: Diagnostic? - return [] - } - var conformances: [String] = [] - if let inheritanceClause = enumDecl.inheritanceClause { - for type in ["CasePathable", "CasePathIterable"] { - if !inheritanceClause.inheritedTypes.contains(where: { - [type, type.qualified].contains($0.type.trimmedDescription) - }) { - conformances.append("\(moduleName).\(type)") - } - } - } else { - conformances = ["CasePathable", "CasePathIterable"].qualified - } - guard !conformances.isEmpty else { return [] } - return [ - DeclSyntax( - """ - \(declaration.attributes.availability)extension \(type.trimmed): \ - \(raw: conformances.joined(separator: ", ")) {} - """ - ) - .cast(ExtensionDeclSyntax.self) - ] + try CasePathsMacrosSupport.CasePathableMacro.expansion( + of: node, + attachedTo: declaration, + providingExtensionsOf: type, + conformingTo: protocols, + in: context + ) } } extension CasePathableMacro: MemberMacro { - public static func expansion< - Declaration: DeclGroupSyntax, Context: MacroExpansionContext - >( + public static func expansion( of node: AttributeSyntax, - providingMembersOf declaration: Declaration, - in context: Context + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext ) throws -> [DeclSyntax] { - guard let enumDecl = declaration.as(EnumDeclSyntax.self) - else { - throw DiagnosticsError( - diagnostics: [ - CasePathableMacroDiagnostic - .notAnEnum(declaration) - .diagnose(at: declaration.keyword) - ] - ) - } - let enumName = enumDecl.name.trimmed - - let enumCaseDecls = enumDecl.memberBlock.members - .flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] } - - var seenCaseNames: Set = [] - for enumCaseDecl in enumCaseDecls { - let name = enumCaseDecl.name.text - if seenCaseNames.contains(name) { - throw DiagnosticsError( - diagnostics: [ - CasePathableMacroDiagnostic.overloadedCaseName(name).diagnose( - at: Syntax(enumCaseDecl.name)) - ] - ) - } - seenCaseNames.insert(name) - } - - let selfRewriter = SelfRewriter(selfEquivalent: enumName) - let memberBlock = selfRewriter.rewrite(enumDecl.memberBlock).cast(MemberBlockSyntax.self) - let rootSubscriptCases = generateCases(from: memberBlock.members, enumName: enumName) { - "if root.is(\\.\($0.name.text)) { return \\.\($0.name.text) }" - } - let elementRewriter = ElementRewriter() - let casePaths = generateDeclSyntax( - from: memberBlock.members, - enumName: enumName, - elementRewriter: elementRewriter + try CasePathsMacrosSupport.CasePathableMacro.expansion( + of: node, + providingMembersOf: declaration, + in: context ) - let allCases = generateCases(from: memberBlock.members, enumName: enumName) { - "allCasePaths.append(\\.\($0.name.text))" - } - - let subscriptReturn = allCases.isEmpty ? #"\.never"# : #"return \.never"# - - var decls: [DeclSyntax] = [ - """ - public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { - public subscript(root: \(enumName)) -> CasePaths.PartialCaseKeyPath<\(enumName)> { - \(raw: rootSubscriptCases.map { "\($0.description)\n" }.joined())\(raw: subscriptReturn) - } - \(raw: casePaths.map(\.description).joined(separator: "\n")) - public func makeIterator() -> Swift.IndexingIterator<[CasePaths.PartialCaseKeyPath<\(enumName)>]> { - \(raw: allCases.isEmpty ? "let" : "var") allCasePaths: \ - [CasePaths.PartialCaseKeyPath<\(enumName)>] = []\ - \(raw: allCases.map { "\n\($0.description)" }.joined()) - return allCasePaths.makeIterator() - } - } - """, - """ - public static var allCasePaths: AllCasePaths { AllCasePaths() } - """, - ] - - if elementRewriter.didRewriteElement { - decls.append("public typealias _$Element = Element") - } - - return decls - } - - static func generateCases( - from elements: MemberBlockItemListSyntax, - enumName: TokenSyntax, - body: (EnumCaseElementSyntax) -> String - ) -> [String] { - elements.flatMap { - if let decl = $0.decl.as(EnumCaseDeclSyntax.self) { - return decl.elements.map(body) - } - if let ifConfigDecl = $0.decl.as(IfConfigDeclSyntax.self) { - let ifClauses = ifConfigDecl.clauses.flatMap { decl -> [String] in - guard let elements = decl.elements?.as(MemberBlockItemListSyntax.self) else { - return [] - } - let title = "\(decl.poundKeyword.text) \(decl.condition?.description ?? "")" - return [title] - + generateCases(from: elements, enumName: enumName, body: body) - } - return ifClauses + ["#endif"] - } - return [] - } - } - - static func generateDeclSyntax( - from elements: MemberBlockItemListSyntax, - enumName: TokenSyntax, - elementRewriter: ElementRewriter - ) -> [String] { - elements.flatMap { - if let decl = $0.decl.as(EnumCaseDeclSyntax.self) { - return generateDeclSyntax(from: decl, enumName: enumName).map { - elementRewriter.rewrite($0).description - } - } - if let ifConfigDecl = $0.decl.as(IfConfigDeclSyntax.self) { - let ifClauses = ifConfigDecl.clauses.flatMap { decl -> [String] in - guard let elements = decl.elements?.as(MemberBlockItemListSyntax.self) else { - return [] - } - let title = "\(decl.poundKeyword.text) \(decl.condition?.description ?? "")" - return [title] - + generateDeclSyntax( - from: elements, enumName: enumName, elementRewriter: elementRewriter - ) - } - return ifClauses + ["#endif"] - } - return [] - } - } - - static func generateDeclSyntax( - from decl: EnumCaseDeclSyntax, - enumName: TokenSyntax - ) -> [DeclSyntax] { - decl.elements.map { - let caseName = $0.name.trimmed - let associatedValueName = $0.trimmedTypeDescription - let hasPayload = $0.parameterClause.map { !$0.parameters.isEmpty } ?? false - let embed: String = hasPayload ? "\(enumName).\(caseName)" : "{ \(enumName).\(caseName) }" - let bindingNames: String - let returnName: String - if hasPayload, let associatedValue = $0.parameterClause { - let parameterNames = (0.. Bool in lhs < rhs }) - ?? 0 - let leadingTrivia = - leadingTriviaLines - .map { String($0.dropFirst(indent)) } - .joined(separator: "\n") - .trimmingSuffix(while: { $0.isWhitespace && !$0.isNewline }) - return """ - \(raw: leadingTrivia)public var \(caseName): \ - \(raw: casePathTypeName.qualified)<\(enumName), \(raw: associatedValueName)> { - ._$embed(\(raw: embed)) { - guard case\(raw: hasPayload ? " let" : "").\(caseName)\(raw: bindingNames) = $0 else { \ - return nil \ - } - return \(raw: returnName) - } - } - """ - } - } -} - -enum CasePathableMacroDiagnostic { - case notAnEnum(DeclGroupSyntax) - case overloadedCaseName(String) -} - -extension CasePathableMacroDiagnostic: DiagnosticMessage { - var message: String { - switch self { - case let .notAnEnum(decl): - return """ - '@CasePathable' cannot be applied to\ - \(decl.keywordDescription.map { " \($0)" } ?? "") type\ - \(decl.nameDescription.map { " '\($0)'" } ?? "") - """ - case let .overloadedCaseName(name): - return """ - '@CasePathable' cannot be applied to overloaded case name '\(name)' - """ - } - } - - var diagnosticID: MessageID { - switch self { - case .notAnEnum: - return MessageID(domain: "MetaEnumDiagnostic", id: "notAnEnum") - case .overloadedCaseName: - return MessageID(domain: "MetaEnumDiagnostic", id: "overloadedCaseName") - } - } - - var severity: DiagnosticSeverity { - switch self { - case .notAnEnum: - return .error - case .overloadedCaseName: - return .error - } - } - - func diagnose(at node: Syntax) -> Diagnostic { - Diagnostic(node: node, message: self) - } -} - -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) - } - @unknown default: return nil - } - 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 { - case let syntax as ActorDeclSyntax: - return Syntax(syntax.actorKeyword) - case let syntax as ClassDeclSyntax: - return Syntax(syntax.classKeyword) - case let syntax as ExtensionDeclSyntax: - return Syntax(syntax.extensionKeyword) - case let syntax as ProtocolDeclSyntax: - return Syntax(syntax.protocolKeyword) - case let syntax as StructDeclSyntax: - return Syntax(syntax.structKeyword) - case let syntax as EnumDeclSyntax: - return Syntax(syntax.enumKeyword) - default: - return Syntax(self) - } - } - - var keywordDescription: String? { - switch self { - case let syntax as ActorDeclSyntax: - return syntax.actorKeyword.trimmedDescription - case let syntax as ClassDeclSyntax: - return syntax.classKeyword.trimmedDescription - case let syntax as ExtensionDeclSyntax: - return syntax.extensionKeyword.trimmedDescription - case let syntax as ProtocolDeclSyntax: - return syntax.protocolKeyword.trimmedDescription - case let syntax as StructDeclSyntax: - return syntax.structKeyword.trimmedDescription - case let syntax as EnumDeclSyntax: - return syntax.enumKeyword.trimmedDescription - default: - return nil - } - } - - var nameDescription: String? { - switch self { - case let syntax as ActorDeclSyntax: - return syntax.name.trimmedDescription - case let syntax as ClassDeclSyntax: - return syntax.name.trimmedDescription - case let syntax as ExtensionDeclSyntax: - return syntax.extendedType.trimmedDescription - case let syntax as ProtocolDeclSyntax: - return syntax.name.trimmedDescription - case let syntax as StructDeclSyntax: - return syntax.name.trimmedDescription - case let syntax as EnumDeclSyntax: - return syntax.name.trimmedDescription - default: - return nil - } - } -} - -extension EnumCaseElementListSyntax.Element { - var trimmedTypeDescription: String { - if var associatedValue = self.parameterClause, !associatedValue.parameters.isEmpty { - if associatedValue.parameters.count == 1, - let type = associatedValue.parameters.first?.type.trimmed - { - return type.is(SomeOrAnyTypeSyntax.self) - ? "(\(type))" - : "\(type)" - } else { - for index in associatedValue.parameters.indices { - associatedValue.parameters[index].type.trailingTrivia = "" - associatedValue.parameters[index].defaultValue = nil - if associatedValue.parameters[index].firstName?.tokenKind == .wildcard { - associatedValue.parameters[index].colon = nil - associatedValue.parameters[index].firstName = nil - associatedValue.parameters[index].secondName = nil - } - } - - // Remove trailing comma from the last parameter for tuple type generation - if let lastIndex = associatedValue.parameters.indices.last { - associatedValue.parameters[lastIndex] = associatedValue.parameters[lastIndex] - .with(\.trailingComma, nil) - } - - return "(\(associatedValue.parameters.trimmed))" - } - } else { - return "Void" - } - } -} - -extension SyntaxStringInterpolation { - mutating func appendInterpolation(_ node: Node?) { - if let node { - self.appendInterpolation(node) - } - } -} - -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 - - init(selfEquivalent: TokenSyntax) { - self.selfEquivalent = selfEquivalent - } - - override func visit(_ node: IdentifierTypeSyntax) -> TypeSyntax { - guard node.name.text == "Self" - else { return super.visit(node) } - return super.visit(node.with(\.name, self.selfEquivalent)) - } -} - -final class ElementRewriter: SyntaxRewriter { - var didRewriteElement = false - - override func visit(_ node: IdentifierTypeSyntax) -> TypeSyntax { - guard node.name.text == "Element" - else { return super.visit(node) } - didRewriteElement = true - return super.visit(node.with(\.name, "_$Element")) - } -} - -extension [String] { - fileprivate var qualified: [String] { - map(\.qualified) - } -} - -extension String { - fileprivate var qualified: String { - "\(CasePathableMacro.moduleName).\(self)" - } -} - -extension StringProtocol { - @inline(__always) - func trimmingSuffix(while condition: (Element) throws -> Bool) rethrows -> Self.SubSequence { - var view = self[...] - - while let character = view.last, try condition(character) { - view = view.dropLast() - } - - return view } } diff --git a/Sources/CasePathsMacrosSupport/CasePathableMacro.swift b/Sources/CasePathsMacrosSupport/CasePathableMacro.swift new file mode 100644 index 00000000..00f90b3d --- /dev/null +++ b/Sources/CasePathsMacrosSupport/CasePathableMacro.swift @@ -0,0 +1,550 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct CasePathableMacro { + static let moduleName = "CasePaths" + static let casePathTypeName = "AnyCasePath" + + private static func shouldGenerate( + for node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax + ) -> Bool { + func isCasePathable(_ type: TypeSyntax) -> Bool { + type.as(IdentifierTypeSyntax.self)?.name.text == "CasePathable" + || type.as(MemberTypeSyntax.self)?.name.text == "CasePathable" + } + if isCasePathable(node.attributeName) { return true } + return !declaration.attributes.contains { element in + guard case let .attribute(attribute) = element else { return false } + return isCasePathable(attribute.attributeName) + } + } +} + +extension CasePathableMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: D, + providingExtensionsOf type: T, + conformingTo protocols: [TypeSyntax], + in context: C + ) throws -> [ExtensionDeclSyntax] { + // if protocols.isEmpty { + // return [] + // } + guard let enumDecl = declaration.as(EnumDeclSyntax.self) + else { + // TODO: Diagnostic? + return [] + } + guard shouldGenerate(for: node, attachedTo: declaration) else { return [] } + var conformances: [String] = [] + if let inheritanceClause = enumDecl.inheritanceClause { + for type in ["CasePathable", "CasePathIterable"] { + if !inheritanceClause.inheritedTypes.contains(where: { + [type, type.qualified].contains($0.type.trimmedDescription) + }) { + conformances.append("\(moduleName).\(type)") + } + } + } else { + conformances = ["CasePathable", "CasePathIterable"].qualified + } + guard !conformances.isEmpty else { return [] } + return [ + DeclSyntax( + """ + \(declaration.attributes.availability)extension \(type.trimmed): \ + \(raw: conformances.joined(separator: ", ")) {} + """ + ) + .cast(ExtensionDeclSyntax.self) + ] + } +} + +extension CasePathableMacro: MemberMacro { + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + try expansion(of: node, providingMembersOf: declaration, in: context) + } + + public static func expansion< + Declaration: DeclGroupSyntax, Context: MacroExpansionContext + >( + of node: AttributeSyntax, + providingMembersOf declaration: Declaration, + in context: Context + ) throws -> [DeclSyntax] { + guard let enumDecl = declaration.as(EnumDeclSyntax.self) + else { + throw DiagnosticsError( + diagnostics: [ + CasePathableMacroDiagnostic + .notAnEnum(declaration) + .diagnose(at: declaration.keyword) + ] + ) + } + guard shouldGenerate(for: node, attachedTo: declaration) else { return [] } + let enumName = enumDecl.name.trimmed + + let enumCaseDecls = enumDecl.memberBlock.members + .flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] } + + var seenCaseNames: Set = [] + for enumCaseDecl in enumCaseDecls { + let name = enumCaseDecl.name.text + if seenCaseNames.contains(name) { + throw DiagnosticsError( + diagnostics: [ + CasePathableMacroDiagnostic.overloadedCaseName(name).diagnose( + at: Syntax(enumCaseDecl.name)) + ] + ) + } + seenCaseNames.insert(name) + } + + let selfRewriter = SelfRewriter(selfEquivalent: enumName) + let memberBlock = selfRewriter.rewrite(enumDecl.memberBlock).cast(MemberBlockSyntax.self) + let rootSubscriptCases = generateCases(from: memberBlock.members, enumName: enumName) { + "if root.is(\\.\($0.name.text)) { return \\.\($0.name.text) }" + } + let elementRewriter = ElementRewriter() + let casePaths = generateDeclSyntax( + from: memberBlock.members, + enumName: enumName, + elementRewriter: elementRewriter + ) + let allCases = generateCases(from: memberBlock.members, enumName: enumName) { + "allCasePaths.append(\\.\($0.name.text))" + } + + let subscriptReturn = allCases.isEmpty ? #"\.never"# : #"return \.never"# + + var decls: [DeclSyntax] = [ + """ + public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { + public subscript(root: \(enumName)) -> CasePaths.PartialCaseKeyPath<\(enumName)> { + \(raw: rootSubscriptCases.map { "\($0.description)\n" }.joined())\(raw: subscriptReturn) + } + \(raw: casePaths.map(\.description).joined(separator: "\n")) + public func makeIterator() -> Swift.IndexingIterator<[CasePaths.PartialCaseKeyPath<\(enumName)>]> { + \(raw: allCases.isEmpty ? "let" : "var") allCasePaths: \ + [CasePaths.PartialCaseKeyPath<\(enumName)>] = []\ + \(raw: allCases.map { "\n\($0.description)" }.joined()) + return allCasePaths.makeIterator() + } + } + """, + """ + public static var allCasePaths: AllCasePaths { AllCasePaths() } + """, + ] + + if elementRewriter.didRewriteElement { + decls.append("public typealias _$Element = Element") + } + + return decls + } + + static func generateCases( + from elements: MemberBlockItemListSyntax, + enumName: TokenSyntax, + body: (EnumCaseElementSyntax) -> String + ) -> [String] { + elements.flatMap { + if let decl = $0.decl.as(EnumCaseDeclSyntax.self) { + return decl.elements.map(body) + } + if let ifConfigDecl = $0.decl.as(IfConfigDeclSyntax.self) { + let ifClauses = ifConfigDecl.clauses.flatMap { decl -> [String] in + guard let elements = decl.elements?.as(MemberBlockItemListSyntax.self) else { + return [] + } + let title = "\(decl.poundKeyword.text) \(decl.condition?.description ?? "")" + return [title] + + generateCases(from: elements, enumName: enumName, body: body) + } + return ifClauses + ["#endif"] + } + return [] + } + } + + static func generateDeclSyntax( + from elements: MemberBlockItemListSyntax, + enumName: TokenSyntax, + elementRewriter: ElementRewriter + ) -> [String] { + elements.flatMap { + if let decl = $0.decl.as(EnumCaseDeclSyntax.self) { + return generateDeclSyntax(from: decl, enumName: enumName).map { + elementRewriter.rewrite($0).description + } + } + if let ifConfigDecl = $0.decl.as(IfConfigDeclSyntax.self) { + let ifClauses = ifConfigDecl.clauses.flatMap { decl -> [String] in + guard let elements = decl.elements?.as(MemberBlockItemListSyntax.self) else { + return [] + } + let title = "\(decl.poundKeyword.text) \(decl.condition?.description ?? "")" + return [title] + + generateDeclSyntax( + from: elements, enumName: enumName, elementRewriter: elementRewriter + ) + } + return ifClauses + ["#endif"] + } + return [] + } + } + + static func generateDeclSyntax( + from decl: EnumCaseDeclSyntax, + enumName: TokenSyntax + ) -> [DeclSyntax] { + decl.elements.map { + let caseName = $0.name.trimmed + let associatedValueType = valueType(for: $0) + let hasPayload = $0.parameterClause.map { !$0.parameters.isEmpty } ?? false + let embed: String = hasPayload ? "\(enumName).\(caseName)" : "{ \(enumName).\(caseName) }" + let bindingNames: String + let returnName: String + if hasPayload, let associatedValue = $0.parameterClause { + let parameterNames = (0.. Bool in lhs < rhs }) + ?? 0 + let leadingTrivia = + leadingTriviaLines + .map { String($0.dropFirst(indent)) } + .joined(separator: "\n") + .trimmingSuffix(while: { $0.isWhitespace && !$0.isNewline }) + return """ + \(raw: leadingTrivia)public var \(caseName): \ + \(raw: casePathTypeName.qualified)<\(enumName), \(associatedValueType)> { + ._$embed(\(raw: embed)) { + guard case\(raw: hasPayload ? " let" : "").\(caseName)\(raw: bindingNames) = $0 else { \ + return nil \ + } + return \(raw: returnName) + } + } + """ + } + } +} + +extension CasePathableMacro { + public static func valueType(for element: EnumCaseElementSyntax) -> TypeSyntax { + guard var associatedValue = element.parameterClause, !associatedValue.parameters.isEmpty + else { return TypeSyntax("Void") } + if associatedValue.parameters.count == 1, + let type = associatedValue.parameters.first?.type.trimmed + { + return type.is(SomeOrAnyTypeSyntax.self) ? TypeSyntax("(\(type))") : type + } + for index in associatedValue.parameters.indices { + associatedValue.parameters[index].type.trailingTrivia = "" + associatedValue.parameters[index].defaultValue = nil + if associatedValue.parameters[index].firstName?.tokenKind == .wildcard { + associatedValue.parameters[index].colon = nil + associatedValue.parameters[index].firstName = nil + associatedValue.parameters[index].secondName = nil + } + } + if let lastIndex = associatedValue.parameters.indices.last { + associatedValue.parameters[lastIndex] = associatedValue.parameters[lastIndex] + .with(\.trailingComma, nil) + } + return TypeSyntax("(\(associatedValue.parameters.trimmed))") + } +} + +enum CasePathableMacroDiagnostic { + case notAnEnum(DeclGroupSyntax) + case overloadedCaseName(String) +} + +extension CasePathableMacroDiagnostic: DiagnosticMessage { + var message: String { + switch self { + case let .notAnEnum(decl): + return """ + '@CasePathable' cannot be applied to\ + \(decl.keywordDescription.map { " \($0)" } ?? "") type\ + \(decl.nameDescription.map { " '\($0)'" } ?? "") + """ + case let .overloadedCaseName(name): + return """ + '@CasePathable' cannot be applied to overloaded case name '\(name)' + """ + } + } + + var diagnosticID: MessageID { + switch self { + case .notAnEnum: + return MessageID(domain: "MetaEnumDiagnostic", id: "notAnEnum") + case .overloadedCaseName: + return MessageID(domain: "MetaEnumDiagnostic", id: "overloadedCaseName") + } + } + + var severity: DiagnosticSeverity { + switch self { + case .notAnEnum: + return .error + case .overloadedCaseName: + return .error + } + } + + func diagnose(at node: Syntax) -> Diagnostic { + Diagnostic(node: node, message: self) + } +} + +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) + } + @unknown default: return nil + } + 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 { + case let syntax as ActorDeclSyntax: + return Syntax(syntax.actorKeyword) + case let syntax as ClassDeclSyntax: + return Syntax(syntax.classKeyword) + case let syntax as ExtensionDeclSyntax: + return Syntax(syntax.extensionKeyword) + case let syntax as ProtocolDeclSyntax: + return Syntax(syntax.protocolKeyword) + case let syntax as StructDeclSyntax: + return Syntax(syntax.structKeyword) + case let syntax as EnumDeclSyntax: + return Syntax(syntax.enumKeyword) + default: + return Syntax(self) + } + } + + var keywordDescription: String? { + switch self { + case let syntax as ActorDeclSyntax: + return syntax.actorKeyword.trimmedDescription + case let syntax as ClassDeclSyntax: + return syntax.classKeyword.trimmedDescription + case let syntax as ExtensionDeclSyntax: + return syntax.extensionKeyword.trimmedDescription + case let syntax as ProtocolDeclSyntax: + return syntax.protocolKeyword.trimmedDescription + case let syntax as StructDeclSyntax: + return syntax.structKeyword.trimmedDescription + case let syntax as EnumDeclSyntax: + return syntax.enumKeyword.trimmedDescription + default: + return nil + } + } + + var nameDescription: String? { + switch self { + case let syntax as ActorDeclSyntax: + return syntax.name.trimmedDescription + case let syntax as ClassDeclSyntax: + return syntax.name.trimmedDescription + case let syntax as ExtensionDeclSyntax: + return syntax.extendedType.trimmedDescription + case let syntax as ProtocolDeclSyntax: + return syntax.name.trimmedDescription + case let syntax as StructDeclSyntax: + return syntax.name.trimmedDescription + case let syntax as EnumDeclSyntax: + return syntax.name.trimmedDescription + default: + return nil + } + } +} + +extension SyntaxStringInterpolation { + mutating func appendInterpolation(_ node: Node?) { + if let node { + self.appendInterpolation(node) + } + } +} + +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 + + init(selfEquivalent: TokenSyntax) { + self.selfEquivalent = selfEquivalent + } + + override func visit(_ node: IdentifierTypeSyntax) -> TypeSyntax { + guard node.name.text == "Self" + else { return super.visit(node) } + return super.visit(node.with(\.name, self.selfEquivalent)) + } +} + +final class ElementRewriter: SyntaxRewriter { + var didRewriteElement = false + + override func visit(_ node: IdentifierTypeSyntax) -> TypeSyntax { + guard node.name.text == "Element" + else { return super.visit(node) } + didRewriteElement = true + return super.visit(node.with(\.name, "_$Element")) + } +} + +extension [String] { + fileprivate var qualified: [String] { + map(\.qualified) + } +} + +extension String { + fileprivate var qualified: String { + "\(CasePathableMacro.moduleName).\(self)" + } +} + +extension StringProtocol { + @inline(__always) + func trimmingSuffix(while condition: (Element) throws -> Bool) rethrows -> Self.SubSequence { + var view = self[...] + + while let character = view.last, try condition(character) { + view = view.dropLast() + } + + return view + } +} diff --git a/Tests/CasePathsMacrosTests/CasePathsMacrosSupportTests.swift b/Tests/CasePathsMacrosTests/CasePathsMacrosSupportTests.swift new file mode 100644 index 00000000..55ba20ad --- /dev/null +++ b/Tests/CasePathsMacrosTests/CasePathsMacrosSupportTests.swift @@ -0,0 +1,229 @@ +#if canImport(MacroTesting) + import CasePathsMacrosSupport + import MacroTesting + import SwiftSyntax + import SwiftSyntaxMacros + import Testing + + private enum CaseBindableMacro {} + + extension CaseBindableMacro: ExtensionMacro { + static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + try CasePathableMacro.expansion( + of: node, + attachedTo: declaration, + providingExtensionsOf: type, + conformingTo: protocols, + in: context + ) + } + } + + extension CaseBindableMacro: MemberMacro { + static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + var decls = try CasePathableMacro.expansion( + of: node, + providingMembersOf: declaration, + in: context + ) + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { return decls } + let elements = enumDecl.memberBlock.members + .flatMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements ?? [] } + let cases = elements.map { element -> String in + let hasPayload = element.parameterClause.map { !$0.parameters.isEmpty } ?? false + guard hasPayload else { return "case \(element.name.text)" } + let type = CasePathableMacro.valueType(for: element) + return "case \(element.name.text)(SwiftUI.Binding<\(type)>)" + } + decls.append( + """ + public enum BindingEnumeration { + \(raw: cases.joined(separator: "\n")) + } + """ + ) + return decls + } + } + + @Suite( + .macros([ + CaseBindableMacro.self, + CasePathableMacro.self, + ]) + ) + struct CasePathsMacrosSupportTests { + @Test func basics() { + assertMacro { + """ + @CaseBindable enum Foo { + case bar + case baz(Int) + case fizz(buzz: String) + } + """ + } expansion: { + #""" + enum Foo { + case bar + case baz(Int) + case fizz(buzz: String) + + public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { + public subscript(root: Foo) -> CasePaths.PartialCaseKeyPath { + if root.is(\.bar) { + return \.bar + } + if root.is(\.baz) { + return \.baz + } + if root.is(\.fizz) { + return \.fizz + } + return \.never + } + public var bar: CasePaths.AnyCasePath { + ._$embed({ + Foo.bar + }) { + guard case .bar = $0 else { + return nil + } + return () + } + } + public var baz: CasePaths.AnyCasePath { + ._$embed(Foo.baz) { + guard case let .baz(v0) = $0 else { + return nil + } + return v0 + } + } + public var fizz: CasePaths.AnyCasePath { + ._$embed(Foo.fizz) { + guard case let .fizz(v0) = $0 else { + return nil + } + return v0 + } + } + public func makeIterator() -> Swift.IndexingIterator<[CasePaths.PartialCaseKeyPath]> { + var allCasePaths: [CasePaths.PartialCaseKeyPath] = [] + allCasePaths.append(\.bar) + allCasePaths.append(\.baz) + allCasePaths.append(\.fizz) + return allCasePaths.makeIterator() + } + } + + public static var allCasePaths: AllCasePaths { + AllCasePaths() + } + + public enum BindingEnumeration { + case bar + case baz(SwiftUI.Binding) + case fizz(SwiftUI.Binding) + } + } + + extension Foo: CasePaths.CasePathable, CasePaths.CasePathIterable { + } + """# + } + } + + @Test func `with '@CasePathable'`() { + assertMacro { + """ + @CaseBindable @CasePathable enum Foo { + case bar + case baz(Int) + case fizz(buzz: String) + } + """ + } expansion: { + #""" + enum Foo { + case bar + case baz(Int) + case fizz(buzz: String) + + public enum BindingEnumeration { + case bar + case baz(SwiftUI.Binding) + case fizz(SwiftUI.Binding) + } + + public struct AllCasePaths: CasePaths.CasePathReflectable, Swift.Sendable, Swift.Sequence { + public subscript(root: Foo) -> CasePaths.PartialCaseKeyPath { + if root.is(\.bar) { + return \.bar + } + if root.is(\.baz) { + return \.baz + } + if root.is(\.fizz) { + return \.fizz + } + return \.never + } + public var bar: CasePaths.AnyCasePath { + ._$embed({ + Foo.bar + }) { + guard case .bar = $0 else { + return nil + } + return () + } + } + public var baz: CasePaths.AnyCasePath { + ._$embed(Foo.baz) { + guard case let .baz(v0) = $0 else { + return nil + } + return v0 + } + } + public var fizz: CasePaths.AnyCasePath { + ._$embed(Foo.fizz) { + guard case let .fizz(v0) = $0 else { + return nil + } + return v0 + } + } + public func makeIterator() -> Swift.IndexingIterator<[CasePaths.PartialCaseKeyPath]> { + var allCasePaths: [CasePaths.PartialCaseKeyPath] = [] + allCasePaths.append(\.bar) + allCasePaths.append(\.baz) + allCasePaths.append(\.fizz) + return allCasePaths.makeIterator() + } + } + + public static var allCasePaths: AllCasePaths { + AllCasePaths() + } + } + + extension Foo: CasePaths.CasePathable, CasePaths.CasePathIterable { + } + """# + } + } + } +#endif diff --git a/Tests/CasePathsTests/DeprecatedTests.swift b/Tests/CasePathsTests/DeprecatedTests.swift index c6fb4230..894ef735 100644 --- a/Tests/CasePathsTests/DeprecatedTests.swift +++ b/Tests/CasePathsTests/DeprecatedTests.swift @@ -9,7 +9,7 @@ protocol TestClassProtocol: AnyObject {} final class DeprecatedTests: XCTestCase { func testSimplePayload() { enum Enum { case payload(Int) } - let path = /Enum.payload + let path: AnyCasePath = /Enum.payload for _ in 1...2 { XCTAssertEqual(path.extract(from: .payload(42)), 42) XCTAssertEqual(path.extract(from: .payload(42)), 42) @@ -19,7 +19,7 @@ final class DeprecatedTests: XCTestCase { func testSimpleLabeledPayload() { enum Enum { case payload(label: Int) } - let path = /Enum.payload(label:) + let path: AnyCasePath = /Enum.payload(label:) for _ in 1...2 { XCTAssertEqual(path.extract(from: .payload(label: 42)), 42) } @@ -31,8 +31,8 @@ final class DeprecatedTests: XCTestCase { case payload(a: Int) case payload(b: Int) } - let pathA = /Enum.payload(a:) - let pathB = /Enum.payload(b:) + let pathA: AnyCasePath = /Enum.payload(a:) + let pathB: AnyCasePath = /Enum.payload(b:) for _ in 1...2 { XCTAssertEqual(pathA.extract(from: .payload(a: 42)), 42) XCTAssertEqual(pathA.extract(from: .payload(b: 42)), nil) @@ -80,8 +80,8 @@ final class DeprecatedTests: XCTestCase { func testNoPayload() { enum Enum { case a, b } - let pathA = /Enum.a - let pathB = /Enum.b + let pathA: AnyCasePath = /Enum.a + let pathB: AnyCasePath = /Enum.b for _ in 1...2 { XCTAssertNotNil(pathA.extract(from: .a)) XCTAssertNotNil(pathB.extract(from: .b)) @@ -102,9 +102,9 @@ final class DeprecatedTests: XCTestCase { case unit1(Unit1) case unit2(Unit2) } - let path1 = /Enum.void - let path2 = /Enum.unit1 - let path3 = /Enum.unit2 + let path1: AnyCasePath = /Enum.void + let path2: AnyCasePath = /Enum.unit1 + let path3: AnyCasePath = /Enum.unit2 for _ in 1...2 { XCTAssertNotNil(path1.extract(from: .void(()))) XCTAssertNotNil(path2.extract(from: .unit1(.init()))) @@ -134,8 +134,8 @@ final class DeprecatedTests: XCTestCase { case uninhabited(Uninhabited) case value } - let path1 = /Enum.never - let path2 = /Enum.uninhabited + let path1: AnyCasePath = /Enum.never + let path2: AnyCasePath = /Enum.uninhabited for _ in 1...2 { XCTAssertNil(path1.extract(from: .value)) XCTAssertNil(path2.extract(from: .value)) @@ -162,7 +162,7 @@ final class DeprecatedTests: XCTestCase { #if !arch(wasm32) func testClosurePayload() throws { enum Enum { case closure(() -> Void) } - let path = /Enum.closure + let path: AnyCasePath Void> = /Enum.closure for _ in 1...2 { var invoked = false let closure = try unwrap(path.extract(from: .closure { invoked = true })) @@ -181,8 +181,8 @@ final class DeprecatedTests: XCTestCase { case indirect(Enum) case direct } - let shallowPath = /Enum.indirect - let deepPath = /Enum.indirect + let shallowPath: AnyCasePath = /Enum.indirect + let deepPath: AnyCasePath = /Enum.indirect for _ in 1...2 { XCTAssertEqual(shallowPath.extract(from: .indirect(.direct)), .direct) XCTAssertEqual( @@ -201,8 +201,8 @@ final class DeprecatedTests: XCTestCase { case direct(Int) } - let indirectPath = /Enum.indirect - let directPath = /Enum.direct + let indirectPath: AnyCasePath = /Enum.indirect + let directPath: AnyCasePath = /Enum.direct for _ in 1...2 { XCTAssertEqual(indirectPath.extract(from: .indirect(42)), 42) @@ -271,7 +271,7 @@ final class DeprecatedTests: XCTestCase { func testOptionalPayload() { enum Enum { case int(Int?) } - let path = /Enum.int + let path: AnyCasePath = /Enum.int for _ in 1...2 { XCTAssertEqual(path.extract(from: .int(.some(42))), .some(.some(42))) XCTAssertEqual(path.extract(from: .int(.none)), .some(.none)) @@ -282,7 +282,7 @@ final class DeprecatedTests: XCTestCase { func testAnyPayload() { enum Enum { case any(Any) } - let path = /Enum.any + let path: AnyCasePath = /Enum.any for _ in 1...2 { XCTAssertEqual(path.extract(from: .any(42)) as? Int, 42) } @@ -294,7 +294,7 @@ final class DeprecatedTests: XCTestCase { enum Enum { case anyObject(AnyObject) } let object = Class() let nsObject = NSObject() - let path = /Enum.anyObject + let path: AnyCasePath = /Enum.anyObject for _ in 1...2 { XCTAssert(try unwrap(path.extract(from: .anyObject(object))) === object) XCTAssert(try unwrap(path.extract(from: .anyObject(nsObject))) === nsObject) @@ -307,7 +307,7 @@ final class DeprecatedTests: XCTestCase { func testProtocolPayload() { struct Error: Swift.Error, Equatable {} enum Enum { case error(Swift.Error) } - let path = /Enum.error + let path: AnyCasePath = /Enum.error for _ in 1...2 { XCTAssertEqual(path.extract(from: .error(Error())) as? Error, Error()) } @@ -323,8 +323,8 @@ final class DeprecatedTests: XCTestCase { } let superclass = Superclass() let subclass = Subclass() - let superclassPath = /Enum.superclass - let subclassPath = /Enum.subclass + let superclassPath: AnyCasePath = /Enum.superclass + let subclassPath: AnyCasePath = /Enum.subclass for _ in 1...2 { XCTAssert( try unwrap(superclassPath.extract(from: .superclass(superclass))) === superclass @@ -370,11 +370,11 @@ final class DeprecatedTests: XCTestCase { case any(Any) } - let boolPath = /Enum.bool - let intPath = /Enum.int - let voidPath = /Enum.void - let structPath = /Enum.structure - let anyPath = /Enum.any + let boolPath: AnyCasePath = /Enum.bool + let intPath: AnyCasePath = /Enum.int + let voidPath: AnyCasePath = /Enum.void + let structPath: AnyCasePath = /Enum.structure + let anyPath: AnyCasePath = /Enum.any for _ in 1...2 { XCTAssertNil(boolPath.extract(from: .int(42))) XCTAssertNil(boolPath.extract(from: .void(()))) @@ -436,8 +436,8 @@ final class DeprecatedTests: XCTestCase { case int(Int) } - let protoPath = /Enum.proto - let intPath = /Enum.int + let protoPath: AnyCasePath = /Enum.proto + let intPath: AnyCasePath = /Enum.int for _ in 1...2 { XCTAssertNil(protoPath.extract(from: .int(100))) @@ -459,8 +459,8 @@ final class DeprecatedTests: XCTestCase { case proto(TestClassProtocol) case int(Int) } - let protoPath = /Enum.proto - let intPath = /Enum.int + let protoPath: AnyCasePath = /Enum.proto + let intPath: AnyCasePath = /Enum.int let object = Class() @@ -639,7 +639,7 @@ final class DeprecatedTests: XCTestCase { func testEmbed() { enum Foo: Equatable { case bar(Int) } - let fooBar = /Foo.bar + let fooBar: AnyCasePath = /Foo.bar XCTAssertEqual(.bar(42), fooBar.embed(42)) XCTAssertEqual(.bar(42), (/Foo.self).embed(Foo.bar(42))) XCTAssertEqual(.bar(42), AnyCasePath(Foo.bar).embed(42)) @@ -658,13 +658,13 @@ final class DeprecatedTests: XCTestCase { func testVoidAnyCasePath() { enum Foo: Equatable { case bar } - let fooBar = /Foo.bar + let fooBar: AnyCasePath = /Foo.bar XCTAssertEqual(.bar, fooBar.embed(())) XCTAssertEqual(.bar, AnyCasePath(Foo.bar).embed(())) } func testCasePaths() { - let some = /String?.some + let some: AnyCasePath = /String?.some XCTAssertEqual( .some("Hello"), some.extract(from: "Hello") @@ -681,10 +681,10 @@ final class DeprecatedTests: XCTestCase { ) struct MyError: Equatable, Error {} - var success = /Result.success - var failure = /Result.failure - var mySuccess = /Result.success - var myFailure = /Result.failure + var success: AnyCasePath, String> = /Result.success + var failure: AnyCasePath, Error> = /Result.failure + var mySuccess: AnyCasePath, String> = /Result.success + var myFailure: AnyCasePath, MyError> = /Result.failure for _ in 1...2 { XCTAssertEqual( @@ -739,7 +739,7 @@ final class DeprecatedTests: XCTestCase { } func testIdentity() { - let id = /Int.self + let id: AnyCasePath = /Int.self XCTAssertEqual( .some(42), id.extract(from: 42) @@ -774,7 +774,7 @@ final class DeprecatedTests: XCTestCase { case bar(none: Int) } - let fooBarSome = /Foo.bar(some:) + let fooBarSome: AnyCasePath = /Foo.bar(some:) XCTAssertEqual( .some(42), fooBarSome.extract(from: .bar(some: 42)) @@ -843,7 +843,7 @@ final class DeprecatedTests: XCTestCase { case baz } - let fooBar = /Foo.bar + let fooBar: AnyCasePath = /Foo.bar XCTAssertEqual( .baz, fooBar.extract(from: .bar(.baz)) @@ -876,10 +876,10 @@ final class DeprecatedTests: XCTestCase { case baz(Never) } - let fooBar = /Foo.bar + let fooBar: AnyCasePath = /Foo.bar XCTAssertNil(fooBar.extract(from: Foo.foo)) - let fooBaz = /Foo.baz + let fooBaz: AnyCasePath = /Foo.baz XCTAssertNil(fooBaz.extract(from: Foo.foo)) } @@ -917,7 +917,7 @@ final class DeprecatedTests: XCTestCase { } var didRun = false - let fooBar = /Foo.bar + let fooBar: AnyCasePath Void> = /Foo.bar guard let bar = fooBar.extract(from: .bar { didRun = true }) else { XCTFail() @@ -994,8 +994,8 @@ final class DeprecatedTests: XCTestCase { } func testAppending() { - let success = /Result.success - let int = /Int?.some + let success: AnyCasePath, Int?> = /Result.success + let int: AnyCasePath = /Int?.some let success2int = success .. int XCTAssertEqual( .some(42), @@ -1064,7 +1064,7 @@ final class DeprecatedTests: XCTestCase { func testCustomStringConvertible() { XCTAssertEqual( - "\(/Result.success)", + "\(/Result.success as AnyCasePath, String>)", "AnyCasePath, String>" ) } @@ -1079,9 +1079,9 @@ final class DeprecatedTests: XCTestCase { XCTAssertNil((/Result.failure).extract(from: result)) - let success: (Result) -> String? = /Result.success + let success: (Result) -> String? = /Result.success XCTAssertEqual(success(result), "hello, world") - let failure: (Result) -> Error? = /Result.failure + let failure: (Result) -> Error? = /Result.failure XCTAssertNil(failure(result)) } @@ -1095,22 +1095,22 @@ final class DeprecatedTests: XCTestCase { var opt: Foo? = .foo("blob1") XCTAssertEqual("blob1", (/Foo.foo).extract(from: opt)) XCTAssertNil((/Foo.bar).extract(from: opt)) - XCTAssertNil((/Foo.baz).extract(from: opt)) + XCTAssertNil((/Foo.baz as (Foo?) -> Void?)(opt)) opt = .bar("blob2") XCTAssertNil((/Foo.foo).extract(from: opt)) XCTAssertEqual("blob2", (/Foo.bar).extract(from: opt)) - XCTAssertNil((/Foo.baz).extract(from: opt)) + XCTAssertNil((/Foo.baz as (Foo?) -> Void?)(opt)) opt = .baz XCTAssertNil((/Foo.foo).extract(from: opt)) XCTAssertNil((/Foo.bar).extract(from: opt)) - XCTAssertNotNil((/Foo.baz).extract(from: opt)) + XCTAssertNotNil((/Foo.baz as (Foo?) -> Void?)(opt)) opt = nil XCTAssertNil((/Foo.foo).extract(from: opt)) XCTAssertNil((/Foo.bar).extract(from: opt)) - XCTAssertNil((/Foo.baz).extract(from: opt)) + XCTAssertNil((/Foo.baz as (Foo?) -> Void?)(opt)) let extractExpression: (Foo?) -> String? = /Foo.foo XCTAssertNotNil(extractExpression(.some(.foo("blob1")))) @@ -1151,7 +1151,7 @@ final class DeprecatedTests: XCTestCase { } func testExtractSuccessFromFailedResultWithErrorProtocolError() { - let path = /Result.success + let path: AnyCasePath, String> = /Result.success func check(_ error: Error) { let result = Result.failure(error) @@ -1236,7 +1236,7 @@ final class DeprecatedTests: XCTestCase { func testConcurrency_SharedCasePath() async throws { enum Enum { case payload(Int) } - let casePath = /Enum.payload + let casePath: AnyCasePath = /Enum.payload await withTaskGroup(of: Void.self) { group in for index in 1...maxIterations {