Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Access Level Inheritance #130

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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