Skip to content

Commit

Permalink
Merge pull request #69 from dafurman/supportNonEscapingClosureParams
Browse files Browse the repository at this point in the history
Fix #35 - Support nonescaping closure parameters
  • Loading branch information
Matejkob committed Jan 1, 2024
2 parents 52acf5a + 0185f33 commit 6fd5ae6
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import SwiftSyntax

extension FunctionParameterListSyntax {
/// - Returns: Whether or not the function parameter list supports generating and using properties to track received arguments and received invocations.
var supportsParameterTracking: Bool {
!isEmpty && !contains { $0.containsNonEscapingClosure }
}
}

extension FunctionParameterSyntax {
fileprivate var containsNonEscapingClosure: Bool {
if type.is(FunctionTypeSyntax.self) {
return true
}
guard let attributedType = type.as(AttributedTypeSyntax.self),
attributedType.baseType.is(FunctionTypeSyntax.self) else {
return false
}

return !attributedType.attributes.contains {
$0.attributeNameTextMatches(TokenSyntax.keyword(.escaping).text)
}
}

var usesAutoclosure: Bool {
type.as(AttributedTypeSyntax.self)?.attributes.contains {
$0.attributeNameTextMatches(TokenSyntax.keyword(.autoclosure).text)
} == true
}
}

private extension AttributeListSyntax.Element {
func attributeNameTextMatches(_ name: String) -> Bool {
self.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == name
}
}
9 changes: 7 additions & 2 deletions Sources/SpyableMacro/Factories/ClosureFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@ struct ClosureFactory {

if parameter.isInoutParameter {
LabeledExprSyntax(
expression: InOutExprSyntax(expression: DeclReferenceExprSyntax(baseName: baseName)))
expression: InOutExprSyntax(
expression: DeclReferenceExprSyntax(baseName: baseName)
)
)
} else {
LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: baseName))
let trailingTrivia: Trivia? = parameter.usesAutoclosure ? "()" : nil

LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: baseName), trailingTrivia: trailingTrivia)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ struct FunctionImplementationFactory {

callsCountFactory.incrementVariableExpression(variablePrefix: variablePrefix)

if !parameterList.isEmpty {
if parameterList.supportsParameterTracking {
receivedArgumentsFactory.assignValueToVariableExpression(
variablePrefix: variablePrefix,
parameterList: parameterList
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpyableMacro/Factories/SpyFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ struct SpyFactory {
try callsCountFactory.variableDeclaration(variablePrefix: variablePrefix)
try calledFactory.variableDeclaration(variablePrefix: variablePrefix)

if !parameterList.isEmpty {
if parameterList.supportsParameterTracking {
try receivedArgumentsFactory.variableDeclaration(
variablePrefix: variablePrefix,
parameterList: parameterList
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import SwiftSyntax
import XCTest

@testable import SpyableMacro

final class UT_FunctionParameterListSyntaxExtensions: XCTestCase {
func testSupportsParameterTracking() {
XCTAssertTrue(
FunctionParameterListSyntax {
"param: Int"
"param: inout Int"
"param: @autoclosure @escaping (Int) async throws -> Void"
}.supportsParameterTracking
)

XCTAssertFalse(
FunctionParameterListSyntax {
"param: (Int) -> Void"
}.supportsParameterTracking
)
}
}
8 changes: 4 additions & 4 deletions Tests/SpyableMacroTests/Factories/UT_ClosureFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ final class UT_ClosureFactory: XCTestCase {
text: inout String,
product: (UInt?, name: String),
added: (() -> Void)?,
removed: @escaping () -> Bool
removed: @autoclosure @escaping () -> Bool
) async throws -> (text: String, output: (() -> Void)?)
""",
prefixForVariable: "_prefix_",
expectingVariableDeclaration: """
var _prefix_Closure: ((inout String, (UInt?, name: String), (() -> Void)?, @escaping () -> Bool) async throws -> (text: String, output: (() -> Void)?) )?
var _prefix_Closure: ((inout String, (UInt?, name: String), (() -> Void)?, @autoclosure @escaping () -> Bool) async throws -> (text: String, output: (() -> Void)?) )?
"""
)
}
Expand Down Expand Up @@ -134,10 +134,10 @@ final class UT_ClosureFactory: XCTestCase {
func testCallExpressionEverything() throws {
try assertProtocolFunction(
withFunctionDeclaration: """
func _ignore_(text: inout String, product: (UInt?, name: String), added: (() -> Void)?, removed: @escaping () -> Bool) async throws -> String?
func _ignore_(text: inout String, product: (UInt?, name: String), added: (() -> Void)?, removed: @autoclosure @escaping () -> Bool) async throws -> String?
""",
prefixForVariable: "_prefix_",
expectingCallExpression: "try await _prefix_Closure!(&text, product, added, removed)"
expectingCallExpression: "try await _prefix_Closure!(&text, product, added, removed())"
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,52 @@ final class UT_FunctionImplementationFactory: XCTestCase {
"""
)
}

func testDeclarationWithEscapingAutoClosure() throws {
let variablePrefix = "functionName"

let protocolFunctionDeclaration = try FunctionDeclSyntax(
"func foo(action: @autoclosure @escaping () -> Void)"
) {}

let result = FunctionImplementationFactory().declaration(
variablePrefix: variablePrefix,
protocolFunctionDeclaration: protocolFunctionDeclaration
)

assertBuildResult(
result,
"""
func foo(action: @autoclosure @escaping () -> Void) {
functionNameCallsCount += 1
functionNameReceivedAction = (action)
functionNameReceivedInvocations.append((action))
functionNameClosure?(action())
}
"""
)
}

func testDeclarationWithNonEscapingClosure() throws {
let variablePrefix = "functionName"

let protocolFunctionDeclaration = try FunctionDeclSyntax(
"func foo(action: () -> Void)"
) {}

let result = FunctionImplementationFactory().declaration(
variablePrefix: variablePrefix,
protocolFunctionDeclaration: protocolFunctionDeclaration
)

assertBuildResult(
result,
"""
func foo(action: () -> Void) {
functionNameCallsCount += 1
functionNameClosure?(action)
}
"""
)
}
}
64 changes: 64 additions & 0 deletions Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,70 @@ final class UT_SpyFactory: XCTestCase {
)
}

func testDeclarationEscapingAutoClosureArgument() throws {
let declaration = DeclSyntax(
"""
protocol ViewModelProtocol {
func foo(action: @escaping @autoclosure () -> Void)
}
"""
)
let protocolDeclaration = try XCTUnwrap(ProtocolDeclSyntax(declaration))

let result = try SpyFactory().classDeclaration(for: protocolDeclaration)

assertBuildResult(
result,
"""
class ViewModelProtocolSpy: ViewModelProtocol {
var fooActionCallsCount = 0
var fooActionCalled: Bool {
return fooActionCallsCount > 0
}
var fooActionReceivedAction: (() -> Void)?
var fooActionReceivedInvocations: [() -> Void] = []
var fooActionClosure: ((@escaping @autoclosure () -> Void) -> Void)?
func foo(action: @escaping @autoclosure () -> Void) {
fooActionCallsCount += 1
fooActionReceivedAction = (action)
fooActionReceivedInvocations.append((action))
fooActionClosure?(action())
}
}
"""
)
}

func testDeclarationNonescapingClosureArgument() throws {
let declaration = DeclSyntax(
"""
protocol ViewModelProtocol {
func foo(action: () -> Void)
}
"""
)
let protocolDeclaration = try XCTUnwrap(ProtocolDeclSyntax(declaration))

let result = try SpyFactory().classDeclaration(for: protocolDeclaration)

assertBuildResult(
result,
"""
class ViewModelProtocolSpy: ViewModelProtocol {
var fooActionCallsCount = 0
var fooActionCalled: Bool {
return fooActionCallsCount > 0
}
var fooActionClosure: ((() -> Void) -> Void)?
func foo(action: () -> Void) {
fooActionCallsCount += 1
fooActionClosure?(action)
}
}
"""
)
}

func testDeclarationReturnValue() throws {
let declaration = DeclSyntax(
"""
Expand Down
44 changes: 44 additions & 0 deletions Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ final class UT_SpyableMacro: XCTestCase {
func initialize(name: String, secondName: String?)
func fetchConfig() async throws -> [String: String]
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
func fetchUsername(context: String, completion: @escaping (String) -> Void)
func onTapBack(context: String, action: () -> Void)
func onTapNext(context: String, action: @Sendable () -> Void)
func assert(_ message: @autoclosure () -> String)
}
"""

Expand Down Expand Up @@ -133,6 +137,46 @@ final class UT_SpyableMacro: XCTestCase {
return fetchDataReturnValue
}
}
var fetchUsernameContextCompletionCallsCount = 0
var fetchUsernameContextCompletionCalled: Bool {
return fetchUsernameContextCompletionCallsCount > 0
}
var fetchUsernameContextCompletionReceivedArguments: (context: String, completion: (String) -> Void)?
var fetchUsernameContextCompletionReceivedInvocations: [(context: String, completion: (String) -> Void)] = []
var fetchUsernameContextCompletionClosure: ((String, @escaping (String) -> Void) -> Void)?
func fetchUsername(context: String, completion: @escaping (String) -> Void) {
fetchUsernameContextCompletionCallsCount += 1
fetchUsernameContextCompletionReceivedArguments = (context, completion)
fetchUsernameContextCompletionReceivedInvocations.append((context, completion))
fetchUsernameContextCompletionClosure?(context, completion)
}
var onTapBackContextActionCallsCount = 0
var onTapBackContextActionCalled: Bool {
return onTapBackContextActionCallsCount > 0
}
var onTapBackContextActionClosure: ((String, () -> Void) -> Void)?
func onTapBack(context: String, action: () -> Void) {
onTapBackContextActionCallsCount += 1
onTapBackContextActionClosure?(context, action)
}
var onTapNextContextActionCallsCount = 0
var onTapNextContextActionCalled: Bool {
return onTapNextContextActionCallsCount > 0
}
var onTapNextContextActionClosure: ((String, @Sendable () -> Void) -> Void)?
func onTapNext(context: String, action: @Sendable () -> Void) {
onTapNextContextActionCallsCount += 1
onTapNextContextActionClosure?(context, action)
}
var assertCallsCount = 0
var assertCalled: Bool {
return assertCallsCount > 0
}
var assertClosure: ((@autoclosure () -> String) -> Void)?
func assert(_ message: @autoclosure () -> String) {
assertCallsCount += 1
assertClosure?(message())
}
}
""",
macros: sut
Expand Down

0 comments on commit 6fd5ae6

Please sign in to comment.