Skip to content

Commit d9b6878

Browse files
authored
Merge pull request #130 from Matejkob/access-levels
Add Access Level Inheritance
2 parents 7d07458 + c53222a commit d9b6878

File tree

8 files changed

+414
-69
lines changed

8 files changed

+414
-69
lines changed

Examples/Sources/AccessLevels.swift

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import Spyable
2+
3+
// MARK: - Open
4+
5+
// Only classes and overridable class members can be declared 'open'.
6+
7+
// MARK: - Public
8+
9+
@Spyable
10+
public protocol PublicServiceProtocol {
11+
var name: String { get }
12+
var anyProtocol: any Codable { get set }
13+
var secondName: String? { get }
14+
var address: String! { get }
15+
var added: () -> Void { get set }
16+
var removed: (() -> Void)? { get set }
17+
18+
func initialize(name: String, _ secondName: String?)
19+
func fetchConfig(arg: UInt8) async throws -> [String: String]
20+
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
21+
func save(name: any Codable, surname: any Codable)
22+
func insert(name: (any Codable)?, surname: (any Codable)?)
23+
func append(name: (any Codable) -> (any Codable)?)
24+
func get() async throws -> any Codable
25+
func read() -> String!
26+
func wrapDataInArray<T>(_ data: T) -> [T]
27+
}
28+
29+
func testPublicServiceProtocol() {
30+
let spy = PublicServiceProtocolSpy()
31+
32+
spy.name = "Spy"
33+
}
34+
35+
// MARK: - Package
36+
37+
@Spyable
38+
package protocol PackageServiceProtocol {
39+
var name: String { get }
40+
var anyProtocol: any Codable { get set }
41+
var secondName: String? { get }
42+
var address: String! { get }
43+
var added: () -> Void { get set }
44+
var removed: (() -> Void)? { get set }
45+
46+
func initialize(name: String, _ secondName: String?)
47+
func fetchConfig(arg: UInt8) async throws -> [String: String]
48+
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
49+
func save(name: any Codable, surname: any Codable)
50+
func insert(name: (any Codable)?, surname: (any Codable)?)
51+
func append(name: (any Codable) -> (any Codable)?)
52+
func get() async throws -> any Codable
53+
func read() -> String!
54+
func wrapDataInArray<T>(_ data: T) -> [T]
55+
}
56+
57+
func testPackageServiceProtocol() {
58+
let spy = PackageServiceProtocolSpy()
59+
60+
spy.name = "Spy"
61+
}
62+
63+
// MARK: - Internal
64+
65+
@Spyable
66+
internal protocol InternalServiceProtocol {
67+
var name: String { get }
68+
var anyProtocol: any Codable { get set }
69+
var secondName: String? { get }
70+
var address: String! { get }
71+
var added: () -> Void { get set }
72+
var removed: (() -> Void)? { get set }
73+
74+
func initialize(name: String, _ secondName: String?)
75+
func fetchConfig(arg: UInt8) async throws -> [String: String]
76+
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
77+
func save(name: any Codable, surname: any Codable)
78+
func insert(name: (any Codable)?, surname: (any Codable)?)
79+
func append(name: (any Codable) -> (any Codable)?)
80+
func get() async throws -> any Codable
81+
func read() -> String!
82+
func wrapDataInArray<T>(_ data: T) -> [T]
83+
}
84+
85+
func testInternalServiceProtocol() {
86+
let spy = InternalServiceProtocolSpy()
87+
88+
spy.name = "Spy"
89+
}
90+
91+
// MARK: - Fileprivate
92+
93+
@Spyable
94+
// swiftformat:disable:next
95+
private protocol FileprivateServiceProtocol {
96+
var name: String { get }
97+
var anyProtocol: any Codable { get set }
98+
var secondName: String? { get }
99+
var address: String! { get }
100+
var added: () -> Void { get set }
101+
var removed: (() -> Void)? { get set }
102+
103+
func initialize(name: String, _ secondName: String?)
104+
func fetchConfig(arg: UInt8) async throws -> [String: String]
105+
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
106+
func save(name: any Codable, surname: any Codable)
107+
func insert(name: (any Codable)?, surname: (any Codable)?)
108+
func append(name: (any Codable) -> (any Codable)?)
109+
func get() async throws -> any Codable
110+
func read() -> String!
111+
func wrapDataInArray<T>(_ data: T) -> [T]
112+
}
113+
114+
func testFileprivateServiceProtocol() {
115+
let spy = FileprivateServiceProtocolSpy()
116+
117+
spy.name = "Spy"
118+
}
119+
120+
// MARK: - Private
121+
122+
@Spyable
123+
private protocol PrivateServiceProtocol {
124+
var name: String { get }
125+
var anyProtocol: any Codable { get set }
126+
var secondName: String? { get }
127+
var address: String! { get }
128+
var added: () -> Void { get set }
129+
var removed: (() -> Void)? { get set }
130+
131+
func initialize(name: String, _ secondName: String?)
132+
func fetchConfig(arg: UInt8) async throws -> [String: String]
133+
func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
134+
func save(name: any Codable, surname: any Codable)
135+
func insert(name: (any Codable)?, surname: (any Codable)?)
136+
func append(name: (any Codable) -> (any Codable)?)
137+
func get() async throws -> any Codable
138+
func read() -> String!
139+
func wrapDataInArray<T>(_ data: T) -> [T]
140+
}
141+
142+
func testPrivateServiceProtocol() {
143+
let spy = PrivateServiceProtocolSpy()
144+
145+
spy.name = "Spy"
146+
}

Sources/SpyableMacro/Extractors/Extractor.swift

+56-19
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,40 @@ import SwiftDiagnostics
22
import SwiftSyntax
33
import SwiftSyntaxMacros
44

5-
/// A utility responsible for extracting specific syntax elements from Swift Syntax.
5+
/// `Extractor` is a utility designed to analyze and extract specific syntax elements from the protocol declartion.
66
///
7-
/// This struct provides methods to retrieve detailed syntax elements from abstract syntax trees,
8-
/// such as protocol declarations and arguments from attribute..
7+
/// This struct provides methods for working with protocol declarations, access levels,
8+
/// and attributes, simplifying the task of retrieving and validating syntax information.
99
struct Extractor {
1010
/// Extracts a `ProtocolDeclSyntax` instance from a given declaration.
1111
///
12-
/// This method takes a declaration conforming to `DeclSyntaxProtocol` and attempts
13-
/// to downcast it to `ProtocolDeclSyntax`. If the downcast succeeds, the protocol declaration
14-
/// is returned. Otherwise, it emits an error indicating that the operation is only applicable
15-
/// to protocol declarations.
12+
/// This method ensures that the provided declaration conforms to `ProtocolDeclSyntax`.
13+
/// If the declaration is not a protocol, an error is thrown.
1614
///
17-
/// - Parameter declaration: The declaration to be examined, conforming to `DeclSyntaxProtocol`.
18-
/// - Returns: A `ProtocolDeclSyntax` instance if the input declaration is a protocol declaration.
19-
/// - Throws: `SpyableDiagnostic.onlyApplicableToProtocol` if the input is not a protocol declaration.
15+
/// - Parameter declaration: The declaration to examine, conforming to `DeclSyntaxProtocol`.
16+
/// - Returns: A `ProtocolDeclSyntax` instance if the input is a protocol declaration.
17+
/// - Throws: `SpyableDiagnostic.onlyApplicableToProtocol` if the input is not a protocol.
2018
func extractProtocolDeclaration(
2119
from declaration: DeclSyntaxProtocol
2220
) throws -> ProtocolDeclSyntax {
2321
guard let protocolDeclaration = declaration.as(ProtocolDeclSyntax.self) else {
2422
throw SpyableDiagnostic.onlyApplicableToProtocol
2523
}
26-
2724
return protocolDeclaration
2825
}
2926

30-
/// Extracts a preprocessor flag value from an attribute if present.
27+
/// Extracts a preprocessor flag value from an attribute if present and valid.
3128
///
32-
/// This method analyzes an `AttributeSyntax` to find an argument labeled `behindPreprocessorFlag`.
33-
/// If found, it verifies that the argument's value is a static string literal. It then returns
34-
/// this string value. If the specific argument is not found, or if its value is not a static string,
35-
/// the method provides relevant diagnostics and returns `nil`.
29+
/// This method searches for an argument labeled `behindPreprocessorFlag` within the
30+
/// given attribute. If the argument is found, its value is validated to ensure it is
31+
/// a static string literal.
3632
///
3733
/// - Parameters:
3834
/// - attribute: The attribute syntax to analyze.
39-
/// - context: The macro expansion context in which this operation is performed.
40-
/// - Returns: The static string literal value of the `behindPreprocessorFlag` argument if present and valid.
41-
/// - Throws: Diagnostic errors for various failure cases, such as the absence of the argument or non-static string values.
35+
/// - context: The macro expansion context in which the operation is performed.
36+
/// - Returns: The static string literal value of the `behindPreprocessorFlag` argument,
37+
/// or `nil` if the argument is missing or invalid.
38+
/// - Throws: Diagnostic errors if the argument is invalid or absent.
4239
func extractPreprocessorFlag(
4340
from attribute: AttributeSyntax,
4441
in context: some MacroExpansionContext
@@ -84,4 +81,44 @@ struct Extractor {
8481

8582
return literalSegment.content.text
8683
}
84+
85+
/// Extracts the access level modifier from a protocol declaration.
86+
///
87+
/// This method identifies the first access level modifier present in the protocol
88+
/// declaration. Supported access levels include `public`, `internal`, `fileprivate`,
89+
/// `private`, and `package`.
90+
///
91+
/// - Parameter protocolDeclSyntax: The protocol declaration to analyze.
92+
/// - Returns: The `DeclModifierSyntax` representing the access level, or `nil` if no
93+
/// valid access level modifier is found.
94+
func extractAccessLevel(from protocolDeclSyntax: ProtocolDeclSyntax) -> DeclModifierSyntax? {
95+
protocolDeclSyntax.modifiers.first(where: \.name.isAccessLevelSupportedInProtocol)
96+
}
97+
}
98+
99+
extension TokenSyntax {
100+
/// Determines if the token represents a supported access level modifier for protocols.
101+
///
102+
/// Supported access levels are:
103+
/// - `public`
104+
/// - `package`
105+
/// - `internal`
106+
/// - `fileprivate`
107+
/// - `private`
108+
///
109+
/// - Returns: `true` if the token matches one of the supported access levels; otherwise, `false`.
110+
fileprivate var isAccessLevelSupportedInProtocol: Bool {
111+
let supportedAccessLevels: [TokenSyntax] = [
112+
.keyword(.public),
113+
.keyword(.package),
114+
.keyword(.internal),
115+
.keyword(.fileprivate),
116+
.keyword(.private),
117+
]
118+
119+
return
120+
supportedAccessLevels
121+
.map { $0.text }
122+
.contains(text)
123+
}
87124
}

Sources/SpyableMacro/Factories/SpyFactory.swift

+7
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ struct SpyFactory {
116116
)
117117
},
118118
memberBlockBuilder: {
119+
InitializerDeclSyntax(
120+
signature: FunctionSignatureSyntax(
121+
parameterClause: FunctionParameterClauseSyntax(parameters: [])
122+
),
123+
bodyBuilder: {}
124+
)
125+
119126
for variableDeclaration in variableDeclarations {
120127
try variablesImplementationFactory.variablesDeclarations(
121128
protocolVariableDeclaration: variableDeclaration

Sources/SpyableMacro/Factories/VariablesImplementationFactory.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ struct VariablesImplementationFactory {
6060
{
6161
let accessorRemovalVisitor = AccessorRemovalVisitor()
6262
accessorRemovalVisitor.visit(protocolVariableDeclaration)
63-
/*
64-
var name: String
65-
*/
6663
} else {
64+
/*
65+
var name: String
66+
*/
6767
try protocolVariableDeclarationWithGetterAndSetter(binding: binding)
6868

6969
try underlyingVariableDeclaration(binding: binding)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import SwiftSyntax
2+
3+
final class AccessLevelModifierRewriter: SyntaxRewriter {
4+
let newAccessLevel: DeclModifierSyntax
5+
6+
init(newAccessLevel: DeclModifierSyntax) {
7+
/// Property / method must be declared `fileprivate` because it matches a requirement in `private` protocol.
8+
if newAccessLevel.name.text == TokenSyntax.keyword(.private).text {
9+
self.newAccessLevel = DeclModifierSyntax(name: .keyword(.fileprivate))
10+
} else {
11+
self.newAccessLevel = newAccessLevel
12+
}
13+
}
14+
15+
override func visit(_ node: DeclModifierListSyntax) -> DeclModifierListSyntax {
16+
if node.parent?.is(FunctionParameterSyntax.self) == true {
17+
return node
18+
}
19+
20+
return DeclModifierListSyntax {
21+
newAccessLevel
22+
}
23+
}
24+
}

Sources/SpyableMacro/Macro/SpyableMacro.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@ public enum SpyableMacro: PeerMacro {
3232
) throws -> [DeclSyntax] {
3333
let protocolDeclaration = try extractor.extractProtocolDeclaration(from: declaration)
3434

35-
let spyClassDeclaration = try spyFactory.classDeclaration(for: protocolDeclaration)
35+
var spyClassDeclaration = try spyFactory.classDeclaration(for: protocolDeclaration)
36+
37+
if let accessLevel = extractor.extractAccessLevel(from: protocolDeclaration) {
38+
let accessLevelModifierRewriter = AccessLevelModifierRewriter(newAccessLevel: accessLevel)
39+
40+
spyClassDeclaration =
41+
accessLevelModifierRewriter
42+
.rewrite(spyClassDeclaration)
43+
.cast(ClassDeclSyntax.self)
44+
}
3645

3746
if let flag = try extractor.extractPreprocessorFlag(from: node, in: context) {
3847
return [

0 commit comments

Comments
 (0)