Skip to content

Add customMockName parameter for generateMock + hand-written mock coexistence#216

Merged
dfed merged 22 commits intomainfrom
dfed--generate-mock-with-handwritten
Apr 6, 2026
Merged

Add customMockName parameter for generateMock + hand-written mock coexistence#216
dfed merged 22 commits intomainfrom
dfed--generate-mock-with-handwritten

Conversation

@dfed
Copy link
Copy Markdown
Owner

@dfed dfed commented Apr 5, 2026

Summary

Adds customMockName: StaticString? to @Instantiable, enabling hand-written mock methods to coexist with generateMock: true. The generated mock() calls through to the custom-named method, eliminating the Swift overload ambiguity that previously made them incompatible.

Additionally, dependency-parameter defaults on custom mocks now bubble up to parent-generated mocks — @Forwarded properties and uncovered dependencies that would otherwise be required get the nearest child's custom mock default.

customMockName behavior

State Behavior
generateMock: true + hand-written mock(), no customMockName Error + fix-it: rename to customMock and add customMockName: "customMock"
generateMock: true + customMockName + hand-written customMock() Valid — generated mock() calls through
generateMock: true + customMockName + ALSO a mock() method Error — mock() conflicts with generated mock
customMockName set without generateMock: true Error + fix-it: add generateMock: true
customMockName set but no method with that name found Error + fix-it: generate compilable stub method

Dependency default bubbling — "nearest receiver's default wins"

When a child's custom mock has a default on a dependency parameter, that default bubbles up to the parent's generated mock:

  • Root's own custom mock default takes highest priority
  • Otherwise, shallowest @Received type with a custom mock default wins (BFS semantics via depth-tagged DFS)
  • @Forwarded types skipped (pass-through)
  • Ties at same depth broken by declaration order
  • Keyed on (label, type) — a String default won't bleed into an Int parameter with the same label

Generated mock parameter order

When a custom mock exists, the generated mock() signature matches the custom mock's parameter order. Transitive parameters not in the custom mock are appended alphabetically.

Mock method validation improvements

  • Non-dependency parameters must have defaults even on zero-dependency types (previously skipped)
  • A literal mock() method alongside customMockName is detected and flagged as conflicting

Error cases changed

  • Removed: mockMethodConflictsWithGenerateMock, mockMethodDependencyHasDefaultValue
  • Added: mockMethodNeedsCustomName, customMockNameWithoutGenerateMock, customMockNameMethodNotFound
  • Kept: mockMethodNonDependencyMissingDefaultValue (now also enforced for zero-dependency types)

Test plan

  • 752 tests pass (25+ new macro, error, and generator tests)
  • Lint clean
  • 100% coverage on diff

🤖 Generated with Claude Code

When an @INSTANTIABLE type has both generateMock: true and a
hand-written mock() method, SafeDI now generates a mock that calls
through to the hand-written one. This lets adopters customize mock
behavior while still getting generated tree construction.

Constraints:
- Dependency parameters on the hand-written mock must not have default
  values (to avoid ambiguity with the generated mock's signatures)
- Non-dependency parameters must have default values (enforced always,
  not just with generateMock)
- Types with no dependencies remain mutually exclusive (identical
  signatures would be ambiguous)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.95%. Comparing base (1114b38) to head (617a881).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             main     #216    +/-   ##
========================================
  Coverage   99.94%   99.95%            
========================================
  Files          40       40            
  Lines        5652     6070   +418     
========================================
+ Hits         5649     6067   +418     
  Misses          3        3            
Files with missing lines Coverage Δ
...s/SafeDICore/Errors/FixableInstantiableError.swift 100.00% <100.00%> (ø)
...eDICore/Extensions/AttributeSyntaxExtensions.swift 100.00% <100.00%> (ø)
...afeDICore/Generators/DependencyTreeGenerator.swift 100.00% <100.00%> (ø)
Sources/SafeDICore/Generators/ScopeGenerator.swift 100.00% <100.00%> (ø)
Sources/SafeDICore/Models/InstantiableStruct.swift 100.00% <100.00%> (ø)
...rces/SafeDICore/Visitors/InstantiableVisitor.swift 100.00% <100.00%> (ø)
...ources/SafeDIMacros/Macros/InstantiableMacro.swift 100.00% <100.00%> (ø)
Sources/SafeDITool/SafeDITool.swift 99.70% <100.00%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

dfed and others added 4 commits April 5, 2026 10:40
When there are no declarations, there are no dependencies, so the
macro rejects and the generator skips the combination of generateMock
+ hand-written mock. The mockInitializer branch was unreachable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a hand-written mock exists, the generated mock should only expose
parameters from the mock's signature, not default-valued init
parameters that the mock chose not to expose.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ock ambiguity

When generateMock: true and a hand-written mock coexist, the hand-written
method must have a different name (specified via customMockName) to avoid
Swift overload ambiguity. The generated mock() calls through to the
custom-named method. Replaces the old mockMethodConflictsWithGenerateMock
and mockMethodDependencyHasDefaultValue errors with mockMethodNeedsCustomName,
customMockNameWithoutGenerateMock, and customMockNameMethodNotFound.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed changed the title Allow generateMock: true alongside hand-written mock() methods Add customMockName parameter for generateMock + hand-written mock coexistence Apr 5, 2026
dfed and others added 12 commits April 5, 2026 16:30
…hrough

Confirms that when a customMock method has parameters in a different
order from the init, the generated mock's call-through matches the
custom mock's parameter order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a custom mock method exists, the generated mock() signature now
orders its parameters to match the custom mock's parameter order, with
any transitive parameters (not in the custom mock) appended alphabetically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a child's custom mock has a default value on a dependency parameter,
that default now bubbles up to the parent's generated mock. A @forwarded
property or uncovered @received dependency that would otherwise be required
gets the default from the nearest receiver's custom mock (BFS: shallowest
depth wins, declaration order breaks ties, @forwarded types skipped).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ased types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
generateCustomMockStub now produces a return statement that calls the
init (concrete) or instantiate() (extension) with all dependency params,
instead of an empty body. Also removes no-op `+ 0` in sort comparator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…on, type-safe default bubbling

1. Detect `mock()` method alongside `customMockName` — when both a
   custom-named mock and a literal `mock()` exist, the latter conflicts
   with the generated mock. Now caught via `conflictingMockFunctionSyntax`.

2. Non-dependency parameters on mock methods must have defaults even when
   the type has zero dependencies. Removed the `!dependencies.isEmpty`
   guard that was skipping validation for zero-dep types.

3. Dependency default bubbling now keys on (label, type) via
   MockParameterIdentifier instead of just label. Prevents a String
   default from incorrectly being applied to an Int parameter with the
   same label.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. When customMockName is already set and a stray mock() exists, the
   fix-it now removes the conflicting method instead of incorrectly
   trying to rename it and duplicate customMockName. New error case
   mockMethodConflictsWithGeneratedMock with "Remove this mock()" fix-it.

2. Extension mock non-dependency default validation now runs for ALL
   mock overloads (iterating allMockFunctions), not just the first.
   Previously, later overloads with different return types could have
   required non-dependency parameters without triggering a diagnostic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers the extension branch of mockMethodConflictsWithGeneratedMock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The example now shows stub session and noop logger defaults, making it
clear why a custom mock is useful vs the generated one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The macro's fix-its are self-discoverable; documenting individual ones
is inconsistent with the rest of the manual.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dfed and others added 3 commits April 5, 2026 21:08
Co-authored-by: Dan Federman <dfed@me.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed marked this pull request as ready for review April 6, 2026 04:58
dfed and others added 2 commits April 5, 2026 22:04
The original test used @received (dependency comes from parent). The
rewrite to customMockName incorrectly changed it to @Instantiated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed merged commit ea652c4 into main Apr 6, 2026
19 checks passed
@dfed dfed deleted the dfed--generate-mock-with-handwritten branch April 6, 2026 05:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant