Skip to content

Add mockOnly parameter to @Instantiable#248

Merged
dfed merged 21 commits intomainfrom
dfed/mock-default-forwarded
Apr 12, 2026
Merged

Add mockOnly parameter to @Instantiable#248
dfed merged 21 commits intomainfrom
dfed/mock-default-forwarded

Conversation

@dfed
Copy link
Copy Markdown
Owner

@dfed dfed commented Apr 11, 2026

Summary

  • Adds mockOnly: Bool = false to @Instantiable, allowing types to provide a hand-written mock() method without requiring init/instantiate() or Instantiable conformance
  • When a dependency's type has a mockOnly declaration, the mock generator uses Type.mock() as the default — forwarded deps become defaulted parameters, and instantiated deps become optional tree children
  • Supports customMockName for naming the hand-written method
  • Combines mockOnly + non-mockOnly @Instantiable declarations for the same type via mergedWithMockProvider(_:) (e.g., production init from one, mock from the other)
  • Works on extensions of stdlib/Foundation types (e.g., @Instantiable(mockOnly: true) extension String)
  • Raises codecov target from 99.9% to 100%

Test plan

  • 12 macro expansion tests (success cases, mutual exclusion errors, missing mock method errors, customMockName interactions, extension branch coverage)
  • 10 code generation tests (forwarded defaults, instantiated tree children, primitive types, custom mock names, same-type merge permutations, production isolation)
  • 5 error tests (duplicate mock providers — all ordering permutations)
  • 6 FixableInstantiableError unit tests (description + fixIt for each new case)
  • 2 production isolation tests (verify production output is unchanged when mockOnly is added)
  • All 777 tests pass, lint clean

🤖 Generated with Claude Code

…tions

When `mockOnly: true`, a type provides a hand-written mock method without
requiring `init`/`instantiate()` or `Instantiable` conformance. The mock
generator uses these mocks as defaults — forwarded deps get `= Type.mock()`
defaults, and instantiated deps become optional tree children with
`Type.mock()` as the default builder.

This eliminates the need for callers to manually provide mocks for
forwarded types and types whose `@Instantiable` is in another module.

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

codecov Bot commented Apr 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (3f402be) to head (6aa69b8).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##              main      #248    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           41        41            
  Lines         5799      5986   +187     
==========================================
+ Hits          5799      5986   +187     
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/GenerateCommand.swift 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

dfed and others added 13 commits April 11, 2026 16:09
… tests

Switch mockOnly error diagnostics from message-only to error+changes pattern
so IDEs show actionable fix-it suggestions. Add 6 unit tests for the new
FixableInstantiableError cases (description + fixIt for each).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers the (true, false) merge path in resolveSafeDIFulfilledTypes where
a mockOnly entry exists and a non-mockOnly entry with its own mock method
arrives — ensuring the duplicate mock provider error is thrown.

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

Covers the extension-branch paths for mockOnlyWithGenerateMock,
mockOnlyWithIsRoot, and the mockOnlyArgumentInvalid throw — these were
uncovered since prior tests only exercised the concrete-declaration branch.

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

When a mockOnly declaration merges with a non-mockOnly production
declaration, the merged entry has mockOnly=false but carries the
mockInitializer from the mockOnly provider. The mockOnlyTypes computation
was filtering on instantiable.mockOnly, so merged entries were excluded
and forwarded parameters didn't get Type.mock() defaults.

Fix by deriving mockOnlyTypes from the presence of a mockInitializer on
non-generateMock entries instead of checking the mockOnly flag.

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

Add test for the (true, false) merge path where a mockOnly entry is
already in the map and a non-mockOnly with generateMock arrives second.
Raise codecov project target from 99.9% to 100%.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, add merge docs

- Update customMockName error message/fix-it to mention mockOnly as valid alternative
- Rename customMockNameWithoutGenerateMock to customMockNameWithoutMockGeneration
- Rename mockOnlyTypes to handWrittenMockTypes (reflects actual semantics post-merge fix)
- Document merge precedence rules in Manual.md mockOnly section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Module info files are regenerated each build — no cross-version
deserialization needed. The synthesized Codable conformance is sufficient.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The merge logic copied mockInitializer and mockReturnType but not
customMockName, so a mockOnly declaration with customMockName: "preview"
would emit Type.mock() instead of Type.preview() after merging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add two tests verifying production code output is identical with and
  without mockOnly declarations (including customMockName and mockAttributes)
- Extract mergedWithMockProvider(_:) on Instantiable to centralize mock
  field copying — prevents future omissions like the customMockName bug

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test @Instantiated child with merged customMockName exercises the tree
  generation path (SafeDIOverrides + builder), not just forwarded defaults
- Test mockOnly + customMockName: "preview" without a preview() method
  verifies the diagnostic names the custom method, not just "mock"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Clarify that undecorated extensions are not detected, and point users
  to mockOnly as the way to provide mocks from separate extensions
- Update customMockName docs (both Manual and Instantiable.swift) to
  describe both generateMock and mockOnly usage
- Update @forwarded section to list all three sources of defaults:
  init defaults, mockOnly providers, and merged declarations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use second person ("you must") instead of "the user must"
- Describe user-facing behavior instead of internals ("tree child",
  "default builder", "initializer and dependencies")
- Simplify customMockName description
- Consolidate three @forwarded default cases to two (merged declaration
  is an implementation detail of mockOnly, not a user-facing concept)
- Use "combines" instead of "merges" for the dual-declaration explanation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Lead with example, then behavior, then rules (matching existing
  manual pattern of introduce-capability → show-example → add-caveats)
- Move use cases list after the behavioral explanation
- "redundant but allowed" → "has no effect when mockOnly is true"
- Extract "Splitting production and mock declarations" as a subheading
- Reword merge explanation behaviorally instead of implementation-centric
- Fix Instantiable.swift doc comments: "the user must" → "you must",
  "the mock generator references" → "SafeDI uses"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread Documentation/Manual.md Outdated
Comment thread Documentation/Manual.md Outdated
Comment thread Documentation/Manual.md Outdated
@dfed dfed force-pushed the dfed/mock-default-forwarded branch 2 times, most recently from 694312b to 8a3c099 Compare April 12, 2026 05:57
dfed and others added 3 commits April 12, 2026 07:41
- mockOnlyWithGenerateMock: removes `generateMock: true` from the attribute
- mockOnlyWithIsRoot: removes `isRoot: true` from the attribute
- mockOnlyMissingMockMethod: generates a stub mock method with correct
  parameters and a compilable body

Add removeArgument(labeled:from:on:) helper that handles comma cleanup
for all argument positions (first, middle, last, only).

Tests verify fix-its produce compilable code via fixedSource assertions,
including edge cases: removing first/last of multiple args, generating
stubs with multiple dependencies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The guard branch returning [] was structurally unreachable — we only
call removeArgument when the labeled argument is known to exist.
Per CLAUDE.md: dead branches for unreachable paths should not exist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed force-pushed the dfed/mock-default-forwarded branch from 3e4f932 to c6f90d2 Compare April 12, 2026 14:53
dfed and others added 4 commits April 12, 2026 08:42
Flatten three-deep if nesting into single comma-separated conditions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mockInitializer, mockReturnType, and customMockName doc comments were
framed only around generateMock. Updated to reflect mockOnly usage too.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add InstantiableVisitor.mockMethodName alongside the existing macroName
and instantiateMethodName constants. Replace 8 hardcoded "mock" string
literals across ScopeGenerator, DependencyTreeGenerator,
InstantiableVisitor, and InstantiableMacro.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…and test fixture naming

Add four sections to CLAUDE.md based on lessons from the mockOnly
implementation:

- Mock generation flow: end-to-end pipeline from generateMockCode
  through ScopeGenerator, with note about production/mock path isolation
- Validation boundaries: macro (local context, fix-its) vs plugin
  (full context, cross-type errors)
- Serialization: Codable models are hermetic per build, no backward
  compatibility needed
- Test fixture naming: executeSafeDIToolTest names files from type
  names, which affects processing order

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed marked this pull request as ready for review April 12, 2026 16:11
@dfed dfed merged commit 45a7215 into main Apr 12, 2026
17 checks passed
@dfed dfed deleted the dfed/mock-default-forwarded branch April 12, 2026 16:11
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6aa69b84ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +747 to +749
} else if let mockMethodName = context.handWrittenMockTypes[dependency.property.typeDescription] {
let mockTypeName = dependency.property.typeDescription.asSource
mockParameters.append("\(bodyIndent)\(dependency.property.label): \(typeSource) = \(mockTypeName).\(mockMethodName)()")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip mock defaults when provider needs arguments

This branch always defaults forwarded parameters to Type.mock() (or customMockName) whenever a hand-written mock exists, but it never checks whether that mock method has required parameters. If a forwarded dependency’s provider is, for example, static func mock(token: String) -> Child, generated code becomes child: Child = Child.mock() and fails to compile. Please gate this defaulting on the provider having no required arguments (or only defaulted ones), otherwise keep the forwarded parameter required.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — this is a real edge case. When a hand-written mock has required dependency-matching parameters, the generated Type.mock() default won't compile. Fixed in #249 by tightening the handWrittenMockTypes filter to only include types whose mock can be called with zero arguments.

Comment on lines +780 to +784
changes: Self.generateCustomMockStub(
named: expectedMethodName,
typeName: extensionDeclaration.extendedType.typeDescription.asSource,
dependencies: [],
isExtension: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Generate a valid stub for missing mockOnly extension method

For @Instantiable(mockOnly: true) extensions with no mock provider, this diagnostic path asks generateCustomMockStub to treat the declaration as extension-based instantiation, which produces a stub that calls .instantiate(). mockOnly extensions are explicitly allowed for external/stdlib types that do not implement instantiate(), so applying the offered fix-it can immediately introduce uncompilable code. The mockOnly fix-it should emit a body that does not depend on instantiate().

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a real concern. The diagnostic at this line is the enforcement that a mock method must exist — it fires when the method is missing. The fix-it is just a convenience scaffold; the user has to fill in the body with real construction logic anyway since SafeDI can't know how to build an arbitrary third-party type. The stub body being imperfect is fine.

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