Skip to content

Commit

Permalink
support closures
Browse files Browse the repository at this point in the history
  • Loading branch information
NikSativa committed Sep 16, 2024
1 parent e0aac33 commit 17a0a8c
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 33 deletions.
9 changes: 8 additions & 1 deletion MacroAndCompilerPlugin/AccessorKeyword+Macro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
import Foundation
import SharedTypes

internal extension Array where Element == AccessorKeyword {
internal extension Array where Element == VarKeyword {
static func ~=(lhs: [Element], rhs: Element) -> Bool {
return lhs.contains(rhs)
}
}

internal extension Array where Element == FuncKeyword {
static func ~=(lhs: [Element], rhs: Element) -> Bool {
return lhs.contains(rhs)
}
}

#endif
4 changes: 4 additions & 0 deletions MacroAndCompilerPlugin/SpryableDiagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ enum SpryableDiagnostic: String, DiagnosticMessage, Error {
case notAVariable
case onlyApplicableToVar
case notAFunction
case nonEscapingClosureNotSupported
case subscriptsNotSupported
case operatorsNotSupported
case invalidVariableRequirement
Expand All @@ -27,13 +28,16 @@ enum SpryableDiagnostic: String, DiagnosticMessage, Error {
return "Operator requirements are not supported by @Spryable."
case .invalidVariableRequirement:
return "Invalid variable requirement. Missing type annotation."
case .nonEscapingClosureNotSupported:
return "'Non-escaping' closures are not supported by `@Spryable`. You should write the body of the function of your 'Fake' manually."
}
}

/// Specifies the severity level of each diagnostic case.
var severity: DiagnosticSeverity {
switch self {
case .invalidVariableRequirement,
.nonEscapingClosureNotSupported,
.notAFunction,
.notAVariable,
.onlyApplicableToClass,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public enum SpryableAccessorMacro: AccessorMacro {
throw SpryableDiagnostic.invalidVariableRequirement
}

let options = node.options
let options = node.varOptions
var effectSpecifiers: AccessorEffectSpecifiersSyntax?
if options ~= .async || options ~= .throws {
effectSpecifiers = .init(asyncSpecifier: options ~= .async ? .keyword(.async) : nil,
Expand Down
18 changes: 15 additions & 3 deletions MacroAndCompilerPlugin/SpryableMacro/SpryableBodyMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,30 @@ public enum SpryableBodyMacro: BodyMacro {
throw SpryableDiagnostic.notAFunction
}

let parameters = syntax.signature.parameterClause.parameters.enumerated().map { idx, param in
let parameters = try syntax.signature.parameterClause.parameters.enumerated().map { _, param in
if param.isNonEscapingClosure {
throw SpryableDiagnostic.nonEscapingClosureNotSupported
}

let name = param.secondName ?? param.firstName
if name.text != TokenSyntax.wildcardToken().text {
return param
} else {
return param.with(\.secondName, .identifier("arg\(idx)"))
return param.with(\.secondName, .identifier("Argument.skipped"))
}
}

let options = node.funcOptions
let arguments = LabeledExprListSyntax {
for (idx, parameter) in parameters.enumerated() {
let name = parameter.secondName ?? parameter.firstName
let name: TokenSyntax = {
if parameter.isEscapingClosure, !(options ~= .asRealClosure) {
return idx == parameters.count - 1 ? "Argument.closure" : "Argument.closure,"
} else {
return parameter.secondName ?? parameter.firstName
}
}()

if idx == 0 {
LabeledExprSyntax(label: "arguments", expression: DeclReferenceExprSyntax(baseName: name))
} else {
Expand Down
48 changes: 42 additions & 6 deletions MacroAndCompilerPlugin/SwiftSyntax+SpryKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,15 @@ internal extension VariableDeclSyntax {
}

internal extension MemberAccessExprSyntax {
var keyword: AccessorKeyword? {
var varKeyword: VarKeyword? {
guard let name = declName.baseName.identifier?.name else {
return nil
}

return .init(rawValue: name)
}

var funcKeyword: FuncKeyword? {
guard let name = declName.baseName.identifier?.name else {
return nil
}
Expand All @@ -46,11 +54,11 @@ internal extension MemberAccessExprSyntax {
}

internal extension VariableDeclSyntax {
var options: [AccessorKeyword] {
var options: [AccessorKeyword] = attributes.flatMap { attr in
var options: [VarKeyword] {
var options: [VarKeyword] = attributes.flatMap { attr in
attr.as(AttributeSyntax.self)?.arguments?.as(LabeledExprListSyntax.self).map { args in
args.compactMap { arg in
arg.expression.as(MemberAccessExprSyntax.self)?.keyword
arg.expression.as(MemberAccessExprSyntax.self)?.varKeyword
}
} ?? []
}
Expand All @@ -64,9 +72,9 @@ internal extension VariableDeclSyntax {
}

internal extension AttributeSyntax {
var options: [AccessorKeyword] {
var varOptions: [VarKeyword] {
var options = arguments?.as(LabeledExprListSyntax.self)?.compactMap { expr in
expr.expression.as(MemberAccessExprSyntax.self)?.keyword
expr.expression.as(MemberAccessExprSyntax.self)?.varKeyword
} ?? []

if !(options ~= .get) {
Expand All @@ -75,6 +83,34 @@ internal extension AttributeSyntax {

return options
}

var funcOptions: [FuncKeyword] {
var options = arguments?.as(LabeledExprListSyntax.self)?.compactMap { expr in
expr.expression.as(MemberAccessExprSyntax.self)?.funcKeyword
} ?? []

if options.isEmpty {
options.append(.asRealClosure)
}

return options
}
}

internal extension FunctionParameterSyntax {
var isClosure: Bool {
return isNonEscapingClosure || isEscapingClosure
}

var isEscapingClosure: Bool {
return type.as(AttributedTypeSyntax.self)?.attributes.contains(where: { elem in
return elem.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.tokenKind == .identifier("escaping")
}) == true
}

var isNonEscapingClosure: Bool {
return type.as(FunctionTypeSyntax.self) != nil
}
}

internal extension Macro {
Expand Down
10 changes: 0 additions & 10 deletions SharedTypes/AccessorKeyword.swift

This file was deleted.

11 changes: 11 additions & 0 deletions SharedTypes/FuncKeyword.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#if swift(>=6.0)
import Foundation

/// Parameters for @SpryableFunc
public enum FuncKeyword: String, Hashable, CaseIterable {
/// spryify parameter as Argument.closure
case asArgument
/// spryify parameter as real closure which you can handle from stub. Default behavior
case asRealClosure
}
#endif
15 changes: 15 additions & 0 deletions SharedTypes/VarKeyword.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#if swift(>=6.0)
import Foundation

/// Parameters for @SpryableVar
public enum VarKeyword: String, Hashable, CaseIterable {
/// generate 'get'. Always generating it
case get
/// generate 'set'
case set
/// add 'async' parameter to 'get'
case async
/// add 'throws' parameter to 'get'
case `throws`
}
#endif
28 changes: 26 additions & 2 deletions Source/Argument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ public enum Argument {
/// Every value matches this qualification.
case anything

/// Every value matches this qualification, but not 'Argument.anything'.
case skipped

/// Any closure
case closure

/// Custom validator
case validator((Any?) -> Bool)

Expand Down Expand Up @@ -42,15 +48,19 @@ public enum Argument {
extension Argument: Equatable {
public static func ==(lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.nil, .nil),
case (.closure, .closure),
(.nil, .nil),
(.nonNil, .nonNil),
(.skipped, .skipped),
(.validator, .validator):
return true

case (.anything, _),
(.closure, _),
(.nil, _),
(.nonNil, _),
(.validator(_), _):
(.skipped, _),
(.validator, _):
return false
}
}
Expand All @@ -69,6 +79,10 @@ extension Argument: CustomStringConvertible {
return "Argument.nil"
case .validator:
return "Argument.validator"
case .closure:
return "Argument.closure"
case .skipped:
return "Argument.skipped"
}
}

Expand Down Expand Up @@ -121,13 +135,23 @@ private func isEqualArgs(specifiedArg: Any?, actualArg: Any?) -> Bool {
if let specifiedArgAsArgumentEnum = specifiedArg as? Argument {
switch specifiedArgAsArgumentEnum {
case .anything:
if let actualArg = actualArg as? Argument {
return actualArg != Argument.skipped
}
return true
case .skipped:
if let actualArg = actualArg as? Argument {
return actualArg != Argument.anything
}
return true
case .nonNil:
return !isNil(actualArg)
case .nil:
return isNil(actualArg)
case .validator(let validator):
return validator(actualArg)
case .closure:
return isClosure(actualArg)
}
}

Expand Down
10 changes: 10 additions & 0 deletions Source/Helpers/InternalHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ internal func isNil(_ value: Any?) -> Bool {
}
}

/// This is a helper function to find out if a value is closure.
internal func isClosure(_ value: Any?) -> Bool {
if let unwrappedValue = value {
let mirror = Mirror(reflecting: unwrappedValue)
return String(describing: mirror.subjectType).contains(" -> ")
} else {
return true
}
}

// MARK: - String Extensions

extension String {
Expand Down
4 changes: 2 additions & 2 deletions Source/SpryableMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ public macro Spryable() =
#externalMacro(module: "MacroAndCompilerPlugin", type: "SpryableExtensionMacro")

@attached(accessor)
public macro SpryableVar(_ accessors: SharedTypes.AccessorKeyword... = [.get]) =
public macro SpryableVar(_ accessors: SharedTypes.VarKeyword... = [.get]) =
#externalMacro(module: "MacroAndCompilerPlugin", type: "SpryableAccessorMacro")

@attached(body)
public macro SpryableFunc() =
public macro SpryableFunc(_ accessors: SharedTypes.FuncKeyword... = [.asRealClosure]) =
#externalMacro(module: "MacroAndCompilerPlugin", type: "SpryableBodyMacro")
#endif
43 changes: 43 additions & 0 deletions Tests/ArgumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ final class ArgumentTests: XCTestCase {
XCTAssertEqual(Argument.nonNil.description, "Argument.nonNil")
XCTAssertEqual(Argument.nil.description, "Argument.nil")
XCTAssertEqual(Argument.validator { _ in true }.description, "Argument.validator")
XCTAssertEqual(Argument.closure.description, "Argument.closure")
}

func test_is_equal_args_list() {
Expand Down Expand Up @@ -40,6 +41,31 @@ final class ArgumentTests: XCTestCase {
]
XCTAssertTrue(subjectAction())

// .skipped
specifiedArgs = [
Argument.anything,
Argument.anything,
Argument.skipped
]
actualArgs = [
"asdf",
3 as Int?,
Argument.anything
]
XCTAssertTrue(subjectAction())

specifiedArgs = [
Argument.anything,
Argument.anything,
Argument.skipped
]
actualArgs = [
"asdf",
3 as Int?,
Argument.skipped
]
XCTAssertTrue(subjectAction())

// .nonNil
specifiedArgs = [Argument.nonNil]
actualArgs = [nil as String?]
Expand Down Expand Up @@ -124,5 +150,22 @@ final class ArgumentTests: XCTestCase {
specifiedArgs = [SpryEquatableTestHelper(isEqual: true)]
actualArgs = [SpryEquatableTestHelper(isEqual: true)]
XCTAssertTrue(subjectAction())

specifiedArgs = [Argument.closure]
actualArgs = [{}]
XCTAssertTrue(subjectAction())

// .skipped != .anything
specifiedArgs = [
Argument.anything,
Argument.anything,
Argument.anything
]
actualArgs = [
"asdf",
3 as Int?,
Argument.skipped
]
XCTAssertFalse(subjectAction())
}
}
Loading

0 comments on commit 17a0a8c

Please sign in to comment.