From 6a8a6863b9a3e6523b3a0350f54a9bf74eecdb72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulas=CC=A7=20Sancak?= Date: Sat, 4 Nov 2023 20:14:50 +0300 Subject: [PATCH 1/2] Fix code coverage targets --- .swiftpm/ThrowPublisherClient.xctestplan | 37 ++++++ .../xcschemes/ThrowPublisher-Package.xcscheme | 115 ++++++++++++++++++ .../xcschemes/ThrowPublisher.xcscheme | 66 ++++++++++ .../xcschemes/ThrowPublisherClient.xcscheme | 108 ++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 .swiftpm/ThrowPublisherClient.xctestplan create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher-Package.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisherClient.xcscheme diff --git a/.swiftpm/ThrowPublisherClient.xctestplan b/.swiftpm/ThrowPublisherClient.xctestplan new file mode 100644 index 0000000..92940fc --- /dev/null +++ b/.swiftpm/ThrowPublisherClient.xctestplan @@ -0,0 +1,37 @@ +{ + "configurations" : [ + { + "id" : "90C4A170-E408-4028-9F94-9180680D569E", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:", + "identifier" : "ThrowPublisherTests", + "name" : "ThrowPublisherTests" + } + ] + }, + "targetForVariableExpansion" : { + "containerPath" : "container:", + "identifier" : "ThrowPublisherClient", + "name" : "ThrowPublisherClient" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "ThrowPublisherTests", + "name" : "ThrowPublisherTests" + } + } + ], + "version" : 1 +} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher-Package.xcscheme new file mode 100644 index 0000000..82ced7a --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher-Package.xcscheme @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher.xcscheme new file mode 100644 index 0000000..d4ea2d3 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisherClient.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisherClient.xcscheme new file mode 100644 index 0000000..aa9b6c6 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisherClient.xcscheme @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 7af97059fdbbfa1937c9dc792fb1aebfd71c0638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ulas=CC=A7=20Sancak?= Date: Wed, 28 Feb 2024 17:04:52 +0300 Subject: [PATCH 2/2] Add variable support --- Sources/ThrowPublisherClient/main.swift | 47 +++- .../ThrowPublisherMacro.swift | 239 ++++++++++++------ .../ThrowPublisherTests.swift | 166 +++++++++++- 3 files changed, 358 insertions(+), 94 deletions(-) diff --git a/Sources/ThrowPublisherClient/main.swift b/Sources/ThrowPublisherClient/main.swift index e96fbc3..0616e22 100644 --- a/Sources/ThrowPublisherClient/main.swift +++ b/Sources/ThrowPublisherClient/main.swift @@ -34,6 +34,39 @@ struct MyStruct { func doSomething(arg: T, arg2: P) throws -> String where T: Equatable { "Something" } + + @ThrowPublisher + static func doSomething(arg: String) throws -> String { + "Something" + } + + @ThrowPublisher + var something: String { + get throws { + "Something" + } + } + + @ThrowPublisher + var somethingWithVoid: Void { + get throws { + print("Something") + } + } + + @ThrowPublisher + var somethingOptional: String? { + get throws { + "Something" + } + } + + @ThrowPublisher + static var somethingStatic: String? { + get throws { + "Something" + } + } } extension Array { @@ -45,7 +78,19 @@ extension Array { let myStruct = MyStruct() -myStruct.doSomething_publisher() +myStruct.something_publisher + .sink { completion in + switch completion { + case .finished: + print("finished") + case .failure(let error): + print(error.localizedDescription) + } + } receiveValue: { value in + print(value) + }.store(in: &cancellables) + +myStruct.doSomething_publisher(arg: "Something") .sink { completion in switch completion { case .finished: diff --git a/Sources/ThrowPublisherMacros/ThrowPublisherMacro.swift b/Sources/ThrowPublisherMacros/ThrowPublisherMacro.swift index 1795903..d7d16fa 100644 --- a/Sources/ThrowPublisherMacros/ThrowPublisherMacro.swift +++ b/Sources/ThrowPublisherMacros/ThrowPublisherMacro.swift @@ -28,94 +28,175 @@ enum ThrowPublisherError: Error, CustomStringConvertible { } public struct ThrowPublisherMacro: PeerMacro { + private static func expansionForFunction(functionDecl: FunctionDeclSyntax) throws -> [DeclSyntax] { + guard functionDecl.signature.effectSpecifiers?.asyncSpecifier == nil else { + throw ThrowPublisherError.asyncSpecifier + } + guard functionDecl.signature.effectSpecifiers?.throwsSpecifier != nil else { + throw ThrowPublisherError.noThrow + } - public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] { - guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else { - throw ThrowPublisherError.notFunction - } - guard functionDecl.signature.effectSpecifiers?.asyncSpecifier == nil else { - throw ThrowPublisherError.asyncSpecifier - } - guard functionDecl.signature.effectSpecifiers?.throwsSpecifier != nil else { - throw ThrowPublisherError.noThrow - } + let modifiers = functionDecl.modifiers.map { + $0.name.text + }.joined(separator: " ") - let modifiers = functionDecl.modifiers.map { - $0.name.text - }.joined(separator: " ") - - let returnType: String - if let name = functionDecl.signature.returnClause?.type.as(IdentifierTypeSyntax.self)?.name { - returnType = name.text - } else if let optionalTypeSyntax = functionDecl.signature.returnClause?.type.as(OptionalTypeSyntax.self), - let name = optionalTypeSyntax.wrappedType.as(IdentifierTypeSyntax.self)?.name { - returnType = "\(name.text)?" - } else { - returnType = "Void" - } + let returnType: String + if let name = functionDecl.signature.returnClause?.type.as(IdentifierTypeSyntax.self)?.name { + returnType = name.text + } else if let optionalTypeSyntax = functionDecl.signature.returnClause?.type.as(OptionalTypeSyntax.self), + let name = optionalTypeSyntax.wrappedType.as(IdentifierTypeSyntax.self)?.name { + returnType = "\(name.text)?" + } else { + returnType = "Void" + } - let functionName = functionDecl.name.text - var newFunctionName = "\(functionName)_publisher" + let functionName = functionDecl.name.text + var newFunctionName = "\(functionName)_publisher" - if let genericParameterClause = functionDecl.genericParameterClause { - newFunctionName += "\(genericParameterClause)" - } + if let genericParameterClause = functionDecl.genericParameterClause { + newFunctionName += "\(genericParameterClause)" + } - var genericPart: String? - if let genericWhereClause = functionDecl.genericWhereClause { - genericPart = "where \(genericWhereClause.requirements)" - } + var genericPart: String? + if let genericWhereClause = functionDecl.genericWhereClause { + genericPart = "where \(genericWhereClause.requirements)" + } - let parameters = "\(functionDecl.signature.parameterClause)" - let parametersWithCall = try functionDecl.signature.parameterClause.parameters.map { - if $0.firstName.text == "_" { - guard let secondName = $0.secondName else { throw ThrowPublisherError.wildcard } - return secondName.text - } - return "\($0.firstName.text): \($0.firstName.text)" - }.joined(separator: ", ") - - let resultString: String - if returnType == "Void" { - resultString = """ - try \(functionName)(\(parametersWithCall)) - return .success(()) - """ - } else { - resultString = """ - let result = try \(functionName)(\(parametersWithCall)) - return .success(result) - """ - } + let parameters = "\(functionDecl.signature.parameterClause)" + let parametersWithCall = try functionDecl.signature.parameterClause.parameters.map { + if $0.firstName.text == "_" { + guard let secondName = $0.secondName else { throw ThrowPublisherError.wildcard } + return secondName.text + } + return "\($0.firstName.text): \($0.firstName.text)" + }.joined(separator: ", ") - var returnPart = "AnyPublisher<\(returnType), Error>" - if let genericPart { - returnPart += " \(genericPart)" - } + let resultString: String + if returnType == "Void" { + resultString = """ + try \(functionName)(\(parametersWithCall)) + return .success(()) + """ + } else { + resultString = """ + let result = try \(functionName)(\(parametersWithCall)) + return .success(result) + """ + } - var startOfFunction = "func" - if !modifiers.isEmpty { - startOfFunction = "\(modifiers) \(startOfFunction)" - } - let hop = """ - \(startOfFunction) \(newFunctionName)\(parameters)-> \(returnPart){ - func getResult() -> Result<\(returnType), Error> { - do { - \(resultString) - } catch { - return .failure(error) - } - } - return getResult() - .publisher - .eraseToAnyPublisher() - } - """ + var returnPart = "AnyPublisher<\(returnType), Error>" + if let genericPart { + returnPart += " \(genericPart)" + } + + var startOfFunction = "func" + if !modifiers.isEmpty { + startOfFunction = "\(modifiers) \(startOfFunction)" + } + let hop = """ + \(startOfFunction) \(newFunctionName)\(parameters)-> \(returnPart){ + func getResult() -> Result<\(returnType), Error> { + do { + \(resultString) + } catch { + return .failure(error) + } + } + return getResult() + .publisher + .eraseToAnyPublisher() + } + """ + + return [""" + \(raw: hop) + """ + ] + } - return [""" - \(raw: hop) - """ - ] + private static func expansionForVariable(variableDecl: VariableDeclSyntax) throws -> [DeclSyntax] { + guard variableDecl.bindingSpecifier.tokenKind == .keyword(.var) else { + throw ThrowPublisherError.asyncSpecifier + } + + guard variableDecl.bindings.count == 1 else { throw ThrowPublisherError.asyncSpecifier } + let firstBinding = variableDecl.bindings.first! + + guard case .accessors(let accessors) = firstBinding.accessorBlock?.accessors else { + throw ThrowPublisherError.asyncSpecifier + } + if accessors.contains(where: { $0.effectSpecifiers?.asyncSpecifier != nil }) { + throw ThrowPublisherError.asyncSpecifier + } + guard accessors.contains(where: { $0.effectSpecifiers?.throwsSpecifier != nil }) else { + throw ThrowPublisherError.asyncSpecifier + } + + let modifiers = variableDecl.modifiers.map { + $0.name.text + }.joined(separator: " ") + + let returnType: String + if let name = firstBinding.typeAnnotation?.type.as(IdentifierTypeSyntax.self)?.name { + returnType = name.text + } else if let optionalTypeSyntax = firstBinding.typeAnnotation?.type.as(OptionalTypeSyntax.self), + let name = optionalTypeSyntax.wrappedType.as(IdentifierTypeSyntax.self)?.name { + returnType = "\(name.text)?" + } else { + returnType = "Void" + } + + let variableName = "\(firstBinding.pattern)" + let newVaraibleName = "\(variableName)_publisher" + + let resultString: String + if returnType == "Void" { + resultString = """ + try \(variableName) + return .success(()) + """ + } else { + resultString = """ + let result = try \(variableName) + return .success(result) + """ + } + + let returnPart = "AnyPublisher<\(returnType), Error>" + + var startOfVariable = "var" + if !modifiers.isEmpty { + startOfVariable = "\(modifiers) \(startOfVariable)" + } + + let hop = """ + \(startOfVariable) \(newVaraibleName): \(returnPart){ + func getResult() -> Result<\(returnType), Error> { + do { + \(resultString) + } catch { + return .failure(error) + } + } + return getResult() + .publisher + .eraseToAnyPublisher() + } + """ + + return [""" + \(raw: hop) + """ + ] + } + + public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] { + if let functionDecl = declaration.as(FunctionDeclSyntax.self) { + return try expansionForFunction(functionDecl: functionDecl) + } else if let variableDecl = declaration.as(VariableDeclSyntax.self) { + return try expansionForVariable(variableDecl: variableDecl) + } + throw ThrowPublisherError.notFunction } } @main diff --git a/Tests/ThrowPublisherTests/ThrowPublisherTests.swift b/Tests/ThrowPublisherTests/ThrowPublisherTests.swift index b89ca51..995fae8 100644 --- a/Tests/ThrowPublisherTests/ThrowPublisherTests.swift +++ b/Tests/ThrowPublisherTests/ThrowPublisherTests.swift @@ -292,17 +292,21 @@ final class ThrowPublisherTests: XCTestCase { #endif } - func testMacroWithNotFunction() throws { + func testMacroWithAsync() throws { #if canImport(ThrowPublisherMacros) assertMacroExpansion( """ @ThrowPublisher - var someVariable: String + func someFunc() async throws { + print("hop") + } """, expandedSource: """ - var someVariable: String + func someFunc() async throws { + print("hop") + } """, - diagnostics: [.init(message: "Declaration must be function.", line: 1, column: 1)], + diagnostics: [.init(message: "ThrowPublisher doesn't support async specifier.", line: 1, column: 1)], macros: testMacros ) #else @@ -310,21 +314,21 @@ final class ThrowPublisherTests: XCTestCase { #endif } - func testMacroWithAsync() throws { + func testMacroWithNotThrowing() throws { #if canImport(ThrowPublisherMacros) assertMacroExpansion( """ @ThrowPublisher - func someFunc() async throws { + func someFunc() { print("hop") } """, expandedSource: """ - func someFunc() async throws { + func someFunc() { print("hop") } """, - diagnostics: [.init(message: "ThrowPublisher doesn't support async specifier.", line: 1, column: 1)], + diagnostics: [.init(message: "Function doesn't throw.", line: 1, column: 1)], macros: testMacros ) #else @@ -332,21 +336,155 @@ final class ThrowPublisherTests: XCTestCase { #endif } - func testMacroWithNotThrowing() throws { + func testMacroVariable() throws { #if canImport(ThrowPublisherMacros) assertMacroExpansion( """ @ThrowPublisher - func someFunc() { - print("hop") + var someVariable: String { + get throws { + "Something" + } } """, expandedSource: """ - func someFunc() { - print("hop") + var someVariable: String { + get throws { + "Something" + } + } + + var someVariable_publisher: AnyPublisher { + func getResult() -> Result { + do { + let result = try someVariable + return .success(result) + } catch { + return .failure(error) + } + } + return getResult() + .publisher + .eraseToAnyPublisher() + } + """, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testMacroVariableWithOptional() throws { + #if canImport(ThrowPublisherMacros) + assertMacroExpansion( + """ + @ThrowPublisher + var someVariable: String? { + get throws { + "Something" + } + } + """, + expandedSource: """ + var someVariable: String? { + get throws { + "Something" + } + } + + var someVariable_publisher: AnyPublisher { + func getResult() -> Result { + do { + let result = try someVariable + return .success(result) + } catch { + return .failure(error) + } + } + return getResult() + .publisher + .eraseToAnyPublisher() + } + """, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testMacroVariableWithVoid() throws { + #if canImport(ThrowPublisherMacros) + assertMacroExpansion( + """ + @ThrowPublisher + var someVariable: Void { + get throws { + "Something" + } + } + """, + expandedSource: """ + var someVariable: Void { + get throws { + "Something" + } + } + + var someVariable_publisher: AnyPublisher { + func getResult() -> Result { + do { + try someVariable + return .success(()) + } catch { + return .failure(error) + } + } + return getResult() + .publisher + .eraseToAnyPublisher() + } + """, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testMacroVariableWithStatic() throws { + #if canImport(ThrowPublisherMacros) + assertMacroExpansion( + """ + @ThrowPublisher + static var someVariable: String { + get throws { + "Something" + } + } + """, + expandedSource: """ + static var someVariable: String { + get throws { + "Something" + } + } + + static var someVariable_publisher: AnyPublisher { + func getResult() -> Result { + do { + let result = try someVariable + return .success(result) + } catch { + return .failure(error) + } + } + return getResult() + .publisher + .eraseToAnyPublisher() } """, - diagnostics: [.init(message: "Function doesn't throw.", line: 1, column: 1)], macros: testMacros ) #else