Skip to content

Commit

Permalink
fix: add availability to mocks when parametrized protocols found in r…
Browse files Browse the repository at this point in the history
…equirements
  • Loading branch information
Kolos65 committed Aug 2, 2024
1 parent ccec0ef commit da977ec
Show file tree
Hide file tree
Showing 8 changed files with 469 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// MockFacotry.swift
// MockFactory.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 2024. 03. 28..
Expand All @@ -10,9 +10,10 @@ import SwiftSyntax
/// Factory to generate the mock service declaration.
///
/// Generates a class declaration that defines the mock implementation of the protocol.
enum MockFacotry: Factory {
enum MockFactory: Factory {
static func build(from requirements: Requirements) throws -> DeclSyntax {
let classDecl = ClassDeclSyntax(
attributes: try attributes(requirements),
modifiers: modifiers(requirements),
classKeyword: classKeyword(requirements),
name: .identifier(requirements.syntax.mockName),
Expand All @@ -32,7 +33,16 @@ enum MockFacotry: Factory {

// MARK: - Helpers

extension MockFacotry {
extension MockFactory {
private static func attributes(_ requirements: Requirements) throws -> AttributeListSyntax {
guard requirements.containsGenericExistentials else { return [] }
return try AttributeListSyntax {
// Runtime support for parametrized protocol types is only available from:
try Availability.from(iOS: "16.0", macOS: "13.0", tvOS: "16.0", watchOS: "9.0")
}
.with(\.trailingTrivia, .newline)
}

private static func modifiers(_ requirements: Requirements) -> DeclModifierListSyntax {
var modifiers = requirements.syntax.modifiers.trimmed
if !requirements.isActor {
Expand Down
2 changes: 1 addition & 1 deletion Sources/MockableMacro/MockableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public enum MockableMacro: PeerMacro {
}

let requirements = try Requirements(protocolDecl)
let declaration = try MockFacotry.build(from: requirements)
let declaration = try MockFactory.build(from: requirements)
let codeblock = CodeBlockItemListSyntax {
CodeBlockItemSyntax(item: .decl(declaration))
}
Expand Down
37 changes: 37 additions & 0 deletions Sources/MockableMacro/Requirements/Requirements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct Requirements {
let syntax: ProtocolDeclSyntax
let modifiers: DeclModifierListSyntax
let isActor: Bool
let containsGenericExistentials: Bool
var functions = [FunctionRequirement]()
var variables = [VariableRequirement]()
var initializers = [InitializerRequirement]()
Expand All @@ -33,6 +34,7 @@ struct Requirements {
self.initializers = Self.initInitializers(members)
self.variables = try Self.initVariables(members)
self.functions = try Self.initFunctions(members, startIndex: variables.count)
self.containsGenericExistentials = try Self.initContainsGenericExistentials(variables, functions)
}
}

Expand Down Expand Up @@ -106,4 +108,39 @@ extension Requirements {
.enumerated()
.map { InitializerRequirement(index: $0, syntax: $1) }
}

private static func initContainsGenericExistentials(
_ variables: [VariableRequirement],
_ functions: [FunctionRequirement]
) throws -> Bool {
let variables = try variables.filter {
let type = try $0.syntax.type
return hasParametrizedProtocolRequirement(type)
}

let functions = functions.filter {
guard let returnClause = $0.syntax.signature.returnClause else { return false }
let type = returnClause.type
return hasParametrizedProtocolRequirement(type)
}

return !variables.isEmpty || !functions.isEmpty
}

private static func hasParametrizedProtocolRequirement(_ type: TypeSyntax) -> Bool {
if let type = type.as(SomeOrAnyTypeSyntax.self),
type.someOrAnySpecifier.tokenKind == .keyword(.any),
let type = type.constraint.as(IdentifierTypeSyntax.self),
let argumentClause = type.genericArgumentClause,
!argumentClause.arguments.isEmpty {
return true
} else if let type = type.as(IdentifierTypeSyntax.self),
let argumentClause = type.genericArgumentClause {
return argumentClause.arguments.contains {
return hasParametrizedProtocolRequirement($0.argument)
}
} else {
return false
}
}
}
52 changes: 52 additions & 0 deletions Sources/MockableMacro/Utils/Availability.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// Availability.swift
// MockableMacro
//
// Created by Kolos Foltanyi on 30/07/2024.
//

import SwiftSyntax

enum AvailabilityVersionParseError: Error {
case invalidVersionString
}

enum Availability {
static func from(iOS: String, macOS: String, tvOS: String, watchOS: String) throws -> AttributeSyntax {
let arguments = try AvailabilityArgumentListSyntax {
try availability(platform: NS.iOS, version: iOS)
try availability(platform: NS.macOS, version: macOS)
try availability(platform: NS.tvOS, version: tvOS)
try availability(platform: NS.watchOS, version: watchOS)
AvailabilityArgumentSyntax(argument: .token(.binaryOperator("*")))
}

return AttributeSyntax(
attributeName: IdentifierTypeSyntax(name: NS.available),
leftParen: .leftParenToken(),
arguments: .availability(arguments),
rightParen: .rightParenToken()
)
}

private static func availability(platform: TokenSyntax, version: String) throws -> AvailabilityArgumentSyntax {
let version = version.split(separator: ".").map(String.init)

guard let major = version.first else {
throw AvailabilityVersionParseError.invalidVersionString
}
let components = version.dropFirst().compactMap {
return VersionComponentSyntax(number: .integerLiteral($0))
}

let versionSyntax = PlatformVersionSyntax(
platform: platform,
version: .init(
major: .integerLiteral(major),
components: .init(components)
)
)

return AvailabilityArgumentSyntax(argument: .availabilityVersionRestriction(versionSyntax))
}
}
4 changes: 4 additions & 0 deletions Sources/MockableMacro/Utils/Namespace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ enum NS {
static let addInvocation: TokenSyntax = "addInvocation"
static let performActions: TokenSyntax = "performActions"
static let policy: TokenSyntax = "policy"
static let iOS: TokenSyntax = "iOS"
static let macOS: TokenSyntax = "macOS"
static let tvOS: TokenSyntax = "tvOS"
static let watchOS: TokenSyntax = "watchOS"

static let _andSign: String = "&&"
static let _init: TokenSyntax = "init"
Expand Down
Loading

0 comments on commit da977ec

Please sign in to comment.