Skip to content

Commit 7c07c8d

Browse files
authored
Merge pull request #141 from ThomasDutartre/feat/support-polymorphism-functions
Support polymorphism on functions
2 parents 25f8ed6 + b6007fc commit 7c07c8d

27 files changed

+4352
-83
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
/*.swiftinterface
77
/*.xcodeproj
88
xcuserdata/
9-
.claude
9+
.claude
10+
Examples/Package.resolved
11+
Package.resolved

CLAUDE.md

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ The codebase follows a clear separation between public API and implementation:
3131
- `Sources/SpyableMacro/` - Macro implementation
3232
- `Macro/SpyableMacro.swift` - Main macro entry point
3333
- `Factories/` - Code generation logic split by concern
34+
- `VariablePrefixFactory.swift` - Generates unique variable prefixes with polymorphism support
3435
- `Extractors/` - Protocol syntax extraction
3536
- `Extensions/` - SwiftSyntax utilities
37+
- `Helpers/` - Utility classes
38+
- `TypeSanitizer.swift` - Type name sanitization for variable naming
3639
- `Diagnostics/` - Error handling
3740

3841
### Key Design Patterns
@@ -50,6 +53,131 @@ For a protocol `MyProtocol`, the macro generates `MyProtocolSpy` with:
5053
- `{method}ReturnValue` - Stubbed return value (non-void methods)
5154
- `{method}ThrowableError` - Error to throw (throwing methods)
5255

56+
## Polymorphism Support
57+
58+
Swift-Spyable automatically handles polymorphic functions (methods with the same name but different parameter or return types) by generating descriptive variable names that include type information. This ensures each method overload gets unique spy variables without naming conflicts.
59+
60+
### Implementation Architecture
61+
62+
The polymorphism detection system consists of three main components:
63+
64+
#### 1. VariablePrefixFactory
65+
Located in `Sources/SpyableMacro/Factories/VariablePrefixFactory.swift`, this factory generates unique textual representations for function declarations:
66+
67+
- **Non-descriptive mode** (default): Uses function name + parameter names (e.g., `displayTextName`)
68+
- **Descriptive mode** (polymorphism detected): Includes parameter and return types (e.g., `displayTextStringNameStringString`)
69+
70+
The factory automatically switches to descriptive mode when `SpyFactory` detects multiple functions with the same non-descriptive prefix.
71+
72+
#### 2. TypeSanitizer Helper
73+
Located in `Sources/SpyableMacro/Helpers/TypeSanitizer.swift`, this utility sanitizes Swift type names for use in variable identifiers:
74+
75+
- Removes forbidden characters: `[`, `]`, `<`, `>`, `(`, `)`, `,`, ` `, `-`, `&`, `:`
76+
- Handles optionals: `String?` becomes `OptionalString`, `String??` becomes `OptionalOptionalString`
77+
- Processes function attributes: `@escaping` becomes `escaping`, `@Sendable` becomes `Sendable`
78+
- Sanitizes complex nested types like `[String: [Int]]``StringInt`
79+
80+
#### 3. SpyFactory Integration
81+
The main `SpyFactory` orchestrates polymorphism detection by:
82+
83+
1. Pre-scanning all functions to build a frequency map of non-descriptive prefixes
84+
2. Identifying functions that would have naming conflicts (frequency > 1)
85+
3. Automatically enabling descriptive mode for conflicting functions
86+
4. Generating unique variable names for each method overload
87+
88+
### Polymorphism Examples
89+
90+
Given these polymorphic methods:
91+
```swift
92+
protocol DisplayService {
93+
func display(text: Int, name: String)
94+
func display(text: String, name: String)
95+
func display(text: String, name: String) -> String
96+
}
97+
```
98+
99+
Swift-Spyable generates:
100+
```swift
101+
class DisplayServiceSpy: DisplayService {
102+
// For display(text: Int, name: String)
103+
var displayTextIntNameStringCalled = false
104+
var displayTextIntNameStringCallsCount = 0
105+
// ... other spy variables
106+
107+
// For display(text: String, name: String)
108+
var displayTextStringNameStringCalled = false
109+
var displayTextStringNameStringCallsCount = 0
110+
// ... other spy variables
111+
112+
// For display(text: String, name: String) -> String
113+
var displayTextStringNameStringStringCalled = false
114+
var displayTextStringNameStringStringCallsCount = 0
115+
var displayTextStringNameStringStringReturnValue: String!
116+
// ... other spy variables
117+
}
118+
```
119+
120+
### Testing Strategy for Polymorphic Functions
121+
122+
#### Unit Tests
123+
- `Tests/SpyableMacroTests/Factories/UT_VariablePrefixFactory.swift` - Comprehensive tests for variable prefix generation
124+
- `Tests/SpyableMacroTests/Helpers/UT_TypeSanitizer.swift` - Type sanitization edge cases
125+
126+
#### Test Categories
127+
1. **Basic Polymorphism**: Same function name with different parameter types
128+
2. **Return Type Polymorphism**: Same signature with different return types
129+
3. **Complex Type Handling**: Generics, optionals, collections, protocol compositions
130+
4. **Edge Cases**: Function attributes (`@escaping`, `@Sendable`), nested optionals, custom types
131+
132+
#### Integration Testing
133+
The Examples project provides real-world usage scenarios for polymorphic protocols, ensuring the generated spies compile and work correctly in practice.
134+
135+
### Common Edge Cases and Solutions
136+
137+
#### 1. Nested Generics and Collections
138+
```swift
139+
func transform(data: [String: [Int]]) -> Dictionary<String, Array<Int>>
140+
```
141+
Generates: `transformDataStringIntDictionaryStringArrayInt`
142+
143+
#### 2. Multiple Optionals
144+
```swift
145+
func find(key: String) -> String??
146+
```
147+
Generates: `findKeyStringOptionalOptionalString`
148+
149+
#### 3. Protocol Compositions
150+
```swift
151+
func combine(objects: [Codable & Hashable]) -> any Codable
152+
```
153+
Generates: `combineObjectsCodableHashableanyCodable`
154+
155+
#### 4. Function Attributes
156+
```swift
157+
func async(completion: @escaping (Result<String, Error>) -> Void)
158+
```
159+
Generates: `asyncCompletionescapingResultStringErrorVoid`
160+
161+
### Developer Guidelines for Polymorphism Features
162+
163+
#### When Adding New Type Support:
164+
1. Update `TypeSanitizer.sanitize()` method for new forbidden characters or type patterns
165+
2. Add corresponding test cases in `UT_TypeSanitizer.swift`
166+
3. Test both `sanitize()` and `sanitizeWithOptionalHandling()` methods
167+
4. Verify integration with `VariablePrefixFactory` descriptive mode
168+
169+
#### When Modifying Variable Generation:
170+
1. Ensure changes work in both descriptive and non-descriptive modes
171+
2. Test with complex nested types and protocol compositions
172+
3. Verify no regression in non-polymorphic scenarios
173+
4. Update `UT_VariablePrefixFactory.swift` with new test cases
174+
175+
#### Debugging Polymorphism Issues:
176+
1. Use `swift test -Xswiftc -Xfrontend -Xswiftc -dump-macro-expansions` to see generated variable names
177+
2. Check if `SpyFactory` correctly detects naming conflicts in the frequency map
178+
3. Verify `TypeSanitizer` properly handles the specific type patterns causing issues
179+
4. Test with isolated minimal examples to isolate the problem
180+
53181
## Development Workflow
54182

55183
### Adding New Features
@@ -63,7 +191,11 @@ For a protocol `MyProtocol`, the macro generates `MyProtocolSpy` with:
63191
- Unit tests use `assertBuildResult` for macro expansion testing
64192
- Each factory has dedicated test files (e.g., `UT_CalledFactory.swift`)
65193
- Integration tests live in the Examples project
66-
- Always test edge cases: generics, async/throws, access levels
194+
- Always test edge cases: generics, async/throws, access levels, polymorphism
195+
- Polymorphism-specific tests:
196+
- `UT_VariablePrefixFactory.swift` - Tests both descriptive and non-descriptive modes
197+
- `UT_TypeSanitizer.swift` - Tests type name sanitization for complex Swift types
198+
- Focus on edge cases: nested generics, multiple optionals, function attributes
67199

68200
### Debugging Macros
69201
1. Use `swift test -Xswiftc -Xfrontend -Xswiftc -dump-macro-expansions` to see generated code
@@ -88,6 +220,8 @@ For a protocol `MyProtocol`, the macro generates `MyProtocolSpy` with:
88220
3. Methods with multiple parameters generate tuple types for arguments
89221
4. Generic constraints are preserved in generated spies
90222
5. Associated types are handled but may require manual implementation
223+
6. Polymorphic methods automatically generate unique variable names using type information
224+
7. Type names are sanitized to create valid Swift identifiers (removing special characters, handling optionals)
91225

92226
### CI/CD
93227
- GitHub Actions automatically formats code on main branch pushes

Examples/Package.resolved

Lines changed: 0 additions & 14 deletions
This file was deleted.

Examples/README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Swift-Spyable Examples
2+
3+
This directory contains practical examples demonstrating the Swift-Spyable macro library features.
4+
5+
## Files Overview
6+
7+
- **ViewModel.swift** & **ViewModelTests.swift**: Basic usage with async/await and error handling
8+
- **AccessLevels.swift**: Demonstrates spy generation with different access levels
9+
- **Polymorphism.swift** & **PolymorphismTests.swift**: Advanced polymorphism examples
10+
11+
## Polymorphism Support
12+
13+
Swift-Spyable supports method overloading (polymorphism) by generating unique spy properties for each method signature. This allows you to independently test and mock different overloads of the same method.
14+
15+
### How Polymorphism Works
16+
17+
When you have overloaded methods in your protocol:
18+
19+
```swift
20+
@Spyable
21+
protocol DataProcessor {
22+
func compute(value: String) -> String
23+
func compute(value: Int) -> String
24+
func compute(value: Bool) -> String
25+
}
26+
```
27+
28+
Swift-Spyable generates unique spy properties by incorporating the parameter and return types into the property names:
29+
30+
```swift
31+
class DataProcessorSpy: DataProcessor {
32+
// For compute(value: String) -> String
33+
var computeValueStringStringCallsCount = 0
34+
var computeValueStringStringCalled: Bool { ... }
35+
var computeValueStringStringReceivedValue: String?
36+
var computeValueStringStringReturnValue: String!
37+
38+
// For compute(value: Int) -> String
39+
var computeValueIntStringCallsCount = 0
40+
var computeValueIntStringCalled: Bool { ... }
41+
var computeValueIntStringReceivedValue: Int?
42+
var computeValueIntStringReturnValue: String!
43+
44+
// For compute(value: Bool) -> String
45+
var computeValueBoolStringCallsCount = 0
46+
var computeValueBoolStringCalled: Bool { ... }
47+
var computeValueBoolStringReceivedValue: Bool?
48+
var computeValueBoolStringReturnValue: String!
49+
}
50+
```
51+
52+
### Naming Convention
53+
54+
The spy property names follow this pattern:
55+
`{methodName}{ParameterName}{ParameterType}{ReturnType}{PropertyType}`
56+
57+
Examples:
58+
- `computeValueStringStringReturnValue` = method `compute`, parameter `value` of type `String`, returns `String`, this is the `ReturnValue` property
59+
- `fetchIdIntStringCallsCount` = method `fetch`, parameter `id` of type `Int`, returns `String`, this is the `CallsCount` property
60+
61+
### Different Types of Overloading Supported
62+
63+
#### 1. Different Parameter Types
64+
```swift
65+
func process(data: String) -> String
66+
func process(data: Int) -> String
67+
func process(data: Bool) -> String
68+
```
69+
70+
#### 2. Same Parameter Type, Different Return Types
71+
```swift
72+
func convert(value: Int) -> Bool
73+
func convert(value: Int) -> String
74+
func convert(value: Int) -> [Int]
75+
```
76+
77+
#### 3. Different Parameter Names/Labels
78+
```swift
79+
func process(data: String) -> String
80+
func process(item: String) -> String
81+
func process(content: String) -> String
82+
```
83+
84+
#### 4. Async and Throwing Variants
85+
```swift
86+
func fetch(id: String) async -> String
87+
func fetch(id: Int) async -> String
88+
func validate(input: String) throws -> Bool
89+
func validate(input: Int) throws -> Bool
90+
```
91+
92+
### Testing Polymorphic Methods
93+
94+
Each overload is tracked independently:
95+
96+
```swift
97+
func testPolymorphism() {
98+
let spy = DataProcessorSpy()
99+
100+
// Setup different return values for each overload
101+
spy.computeValueStringStringReturnValue = "String result"
102+
spy.computeValueIntStringReturnValue = "Int result"
103+
spy.computeValueBoolStringReturnValue = "Bool result"
104+
105+
// Call different overloads
106+
let stringResult = spy.compute(value: "test")
107+
let intResult = spy.compute(value: 42)
108+
let boolResult = spy.compute(value: true)
109+
110+
// Verify each overload was called exactly once
111+
XCTAssertEqual(spy.computeValueStringStringCallsCount, 1)
112+
XCTAssertEqual(spy.computeValueIntStringCallsCount, 1)
113+
XCTAssertEqual(spy.computeValueBoolStringCallsCount, 1)
114+
115+
// Verify correct arguments were captured
116+
XCTAssertEqual(spy.computeValueStringStringReceivedValue, "test")
117+
XCTAssertEqual(spy.computeValueIntStringReceivedValue, 42)
118+
XCTAssertEqual(spy.computeValueBoolStringReceivedValue, true)
119+
}
120+
```
121+
122+
## Key Benefits of Polymorphism Support
123+
124+
1. **Independent Tracking**: Each method overload is tracked separately with its own call counts, arguments, and return values
125+
2. **Type Safety**: The generated spy properties maintain type safety for parameters and return values
126+
3. **Comprehensive Testing**: You can test complex scenarios where the same method name behaves differently based on parameter types
127+
4. **Real-world Compatibility**: Supports common Swift patterns like async/await, throwing functions, and generic constraints
128+
129+
This polymorphism support makes Swift-Spyable suitable for testing complex protocols with overloaded methods, which is common in real-world Swift applications.

0 commit comments

Comments
 (0)