Skip to content

Commit

Permalink
Merge pull request #130 from Matejkob/access-levels
Browse files Browse the repository at this point in the history
Add Access Level Inheritance
  • Loading branch information
Matejkob authored Dec 3, 2024
2 parents 7d07458 + c53222a commit d9b6878
Show file tree
Hide file tree
Showing 8 changed files with 414 additions and 69 deletions.
146 changes: 146 additions & 0 deletions Examples/Sources/AccessLevels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import Spyable

// MARK: - Open

// Only classes and overridable class members can be declared 'open'.

// MARK: - Public

@Spyable
public protocol PublicServiceProtocol {
var name: String { get }
var anyProtocol: any Codable { get set }
var secondName: String? { get }
var address: String! { get }
var added: () -> Void { get set }
var removed: (() -> Void)? { get set }

func initialize(name: String, _ secondName: String?)
func fetchConfig(arg: UInt8) async throws -> [String: String]
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
func save(name: any Codable, surname: any Codable)
func insert(name: (any Codable)?, surname: (any Codable)?)
func append(name: (any Codable) -> (any Codable)?)
func get() async throws -> any Codable
func read() -> String!
func wrapDataInArray<T>(_ data: T) -> [T]
}

func testPublicServiceProtocol() {
let spy = PublicServiceProtocolSpy()

spy.name = "Spy"
}

// MARK: - Package

@Spyable
package protocol PackageServiceProtocol {
var name: String { get }
var anyProtocol: any Codable { get set }
var secondName: String? { get }
var address: String! { get }
var added: () -> Void { get set }
var removed: (() -> Void)? { get set }

func initialize(name: String, _ secondName: String?)
func fetchConfig(arg: UInt8) async throws -> [String: String]
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
func save(name: any Codable, surname: any Codable)
func insert(name: (any Codable)?, surname: (any Codable)?)
func append(name: (any Codable) -> (any Codable)?)
func get() async throws -> any Codable
func read() -> String!
func wrapDataInArray<T>(_ data: T) -> [T]
}

func testPackageServiceProtocol() {
let spy = PackageServiceProtocolSpy()

spy.name = "Spy"
}

// MARK: - Internal

@Spyable
internal protocol InternalServiceProtocol {
var name: String { get }
var anyProtocol: any Codable { get set }
var secondName: String? { get }
var address: String! { get }
var added: () -> Void { get set }
var removed: (() -> Void)? { get set }

func initialize(name: String, _ secondName: String?)
func fetchConfig(arg: UInt8) async throws -> [String: String]
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
func save(name: any Codable, surname: any Codable)
func insert(name: (any Codable)?, surname: (any Codable)?)
func append(name: (any Codable) -> (any Codable)?)
func get() async throws -> any Codable
func read() -> String!
func wrapDataInArray<T>(_ data: T) -> [T]
}

func testInternalServiceProtocol() {
let spy = InternalServiceProtocolSpy()

spy.name = "Spy"
}

// MARK: - Fileprivate

@Spyable
// swiftformat:disable:next
private protocol FileprivateServiceProtocol {
var name: String { get }
var anyProtocol: any Codable { get set }
var secondName: String? { get }
var address: String! { get }
var added: () -> Void { get set }
var removed: (() -> Void)? { get set }

func initialize(name: String, _ secondName: String?)
func fetchConfig(arg: UInt8) async throws -> [String: String]
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
func save(name: any Codable, surname: any Codable)
func insert(name: (any Codable)?, surname: (any Codable)?)
func append(name: (any Codable) -> (any Codable)?)
func get() async throws -> any Codable
func read() -> String!
func wrapDataInArray<T>(_ data: T) -> [T]
}

func testFileprivateServiceProtocol() {
let spy = FileprivateServiceProtocolSpy()

spy.name = "Spy"
}

// MARK: - Private

@Spyable
private protocol PrivateServiceProtocol {
var name: String { get }
var anyProtocol: any Codable { get set }
var secondName: String? { get }
var address: String! { get }
var added: () -> Void { get set }
var removed: (() -> Void)? { get set }

func initialize(name: String, _ secondName: String?)
func fetchConfig(arg: UInt8) async throws -> [String: String]
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
func save(name: any Codable, surname: any Codable)
func insert(name: (any Codable)?, surname: (any Codable)?)
func append(name: (any Codable) -> (any Codable)?)
func get() async throws -> any Codable
func read() -> String!
func wrapDataInArray<T>(_ data: T) -> [T]
}

func testPrivateServiceProtocol() {
let spy = PrivateServiceProtocolSpy()

spy.name = "Spy"
}
75 changes: 56 additions & 19 deletions Sources/SpyableMacro/Extractors/Extractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,40 @@ import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacros

/// A utility responsible for extracting specific syntax elements from Swift Syntax.
/// `Extractor` is a utility designed to analyze and extract specific syntax elements from the protocol declartion.
///
/// This struct provides methods to retrieve detailed syntax elements from abstract syntax trees,
/// such as protocol declarations and arguments from attribute..
/// This struct provides methods for working with protocol declarations, access levels,
/// and attributes, simplifying the task of retrieving and validating syntax information.
struct Extractor {
/// Extracts a `ProtocolDeclSyntax` instance from a given declaration.
///
/// This method takes a declaration conforming to `DeclSyntaxProtocol` and attempts
/// to downcast it to `ProtocolDeclSyntax`. If the downcast succeeds, the protocol declaration
/// is returned. Otherwise, it emits an error indicating that the operation is only applicable
/// to protocol declarations.
/// This method ensures that the provided declaration conforms to `ProtocolDeclSyntax`.
/// If the declaration is not a protocol, an error is thrown.
///
/// - Parameter declaration: The declaration to be examined, conforming to `DeclSyntaxProtocol`.
/// - Returns: A `ProtocolDeclSyntax` instance if the input declaration is a protocol declaration.
/// - Throws: `SpyableDiagnostic.onlyApplicableToProtocol` if the input is not a protocol declaration.
/// - Parameter declaration: The declaration to examine, conforming to `DeclSyntaxProtocol`.
/// - Returns: A `ProtocolDeclSyntax` instance if the input is a protocol declaration.
/// - Throws: `SpyableDiagnostic.onlyApplicableToProtocol` if the input is not a protocol.
func extractProtocolDeclaration(
from declaration: DeclSyntaxProtocol
) throws -> ProtocolDeclSyntax {
guard let protocolDeclaration = declaration.as(ProtocolDeclSyntax.self) else {
throw SpyableDiagnostic.onlyApplicableToProtocol
}

return protocolDeclaration
}

/// Extracts a preprocessor flag value from an attribute if present.
/// Extracts a preprocessor flag value from an attribute if present and valid.
///
/// This method analyzes an `AttributeSyntax` to find an argument labeled `behindPreprocessorFlag`.
/// If found, it verifies that the argument's value is a static string literal. It then returns
/// this string value. If the specific argument is not found, or if its value is not a static string,
/// the method provides relevant diagnostics and returns `nil`.
/// This method searches for an argument labeled `behindPreprocessorFlag` within the
/// given attribute. If the argument is found, its value is validated to ensure it is
/// a static string literal.
///
/// - Parameters:
/// - attribute: The attribute syntax to analyze.
/// - context: The macro expansion context in which this operation is performed.
/// - Returns: The static string literal value of the `behindPreprocessorFlag` argument if present and valid.
/// - Throws: Diagnostic errors for various failure cases, such as the absence of the argument or non-static string values.
/// - context: The macro expansion context in which the operation is performed.
/// - Returns: The static string literal value of the `behindPreprocessorFlag` argument,
/// or `nil` if the argument is missing or invalid.
/// - Throws: Diagnostic errors if the argument is invalid or absent.
func extractPreprocessorFlag(
from attribute: AttributeSyntax,
in context: some MacroExpansionContext
Expand Down Expand Up @@ -84,4 +81,44 @@ struct Extractor {

return literalSegment.content.text
}

/// Extracts the access level modifier from a protocol declaration.
///
/// This method identifies the first access level modifier present in the protocol
/// declaration. Supported access levels include `public`, `internal`, `fileprivate`,
/// `private`, and `package`.
///
/// - Parameter protocolDeclSyntax: The protocol declaration to analyze.
/// - Returns: The `DeclModifierSyntax` representing the access level, or `nil` if no
/// valid access level modifier is found.
func extractAccessLevel(from protocolDeclSyntax: ProtocolDeclSyntax) -> DeclModifierSyntax? {
protocolDeclSyntax.modifiers.first(where: \.name.isAccessLevelSupportedInProtocol)
}
}

extension TokenSyntax {
/// Determines if the token represents a supported access level modifier for protocols.
///
/// Supported access levels are:
/// - `public`
/// - `package`
/// - `internal`
/// - `fileprivate`
/// - `private`
///
/// - Returns: `true` if the token matches one of the supported access levels; otherwise, `false`.
fileprivate var isAccessLevelSupportedInProtocol: Bool {
let supportedAccessLevels: [TokenSyntax] = [
.keyword(.public),
.keyword(.package),
.keyword(.internal),
.keyword(.fileprivate),
.keyword(.private),
]

return
supportedAccessLevels
.map { $0.text }
.contains(text)
}
}
7 changes: 7 additions & 0 deletions Sources/SpyableMacro/Factories/SpyFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ struct SpyFactory {
)
},
memberBlockBuilder: {
InitializerDeclSyntax(
signature: FunctionSignatureSyntax(
parameterClause: FunctionParameterClauseSyntax(parameters: [])
),
bodyBuilder: {}
)

for variableDeclaration in variableDeclarations {
try variablesImplementationFactory.variablesDeclarations(
protocolVariableDeclaration: variableDeclaration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ struct VariablesImplementationFactory {
{
let accessorRemovalVisitor = AccessorRemovalVisitor()
accessorRemovalVisitor.visit(protocolVariableDeclaration)
/*
var name: String
*/
} else {
/*
var name: String
*/
try protocolVariableDeclarationWithGetterAndSetter(binding: binding)

try underlyingVariableDeclaration(binding: binding)
Expand Down
24 changes: 24 additions & 0 deletions Sources/SpyableMacro/Macro/AccessLevelModifierRewriter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import SwiftSyntax

final class AccessLevelModifierRewriter: SyntaxRewriter {
let newAccessLevel: DeclModifierSyntax

init(newAccessLevel: DeclModifierSyntax) {
/// Property / method must be declared `fileprivate` because it matches a requirement in `private` protocol.
if newAccessLevel.name.text == TokenSyntax.keyword(.private).text {
self.newAccessLevel = DeclModifierSyntax(name: .keyword(.fileprivate))
} else {
self.newAccessLevel = newAccessLevel
}
}

override func visit(_ node: DeclModifierListSyntax) -> DeclModifierListSyntax {
if node.parent?.is(FunctionParameterSyntax.self) == true {
return node
}

return DeclModifierListSyntax {
newAccessLevel
}
}
}
11 changes: 10 additions & 1 deletion Sources/SpyableMacro/Macro/SpyableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ public enum SpyableMacro: PeerMacro {
) throws -> [DeclSyntax] {
let protocolDeclaration = try extractor.extractProtocolDeclaration(from: declaration)

let spyClassDeclaration = try spyFactory.classDeclaration(for: protocolDeclaration)
var spyClassDeclaration = try spyFactory.classDeclaration(for: protocolDeclaration)

if let accessLevel = extractor.extractAccessLevel(from: protocolDeclaration) {
let accessLevelModifierRewriter = AccessLevelModifierRewriter(newAccessLevel: accessLevel)

spyClassDeclaration =
accessLevelModifierRewriter
.rewrite(spyClassDeclaration)
.cast(ClassDeclSyntax.self)
}

if let flag = try extractor.extractPreprocessorFlag(from: node, in: context) {
return [
Expand Down
Loading

0 comments on commit d9b6878

Please sign in to comment.