Skip to content

Detect and error on partially-lazy dependency cycles#220

Merged
dfed merged 8 commits intomainfrom
dfed--partially-lazy-cycle-error
Apr 6, 2026
Merged

Detect and error on partially-lazy dependency cycles#220
dfed merged 8 commits intomainfrom
dfed--partially-lazy-cycle-error

Conversation

@dfed
Copy link
Copy Markdown
Owner

@dfed dfed commented Apr 6, 2026

Summary

Detects and errors on dependency cycles that would generate uncompilable Swift code, fixing a latent bug in both production and mock code generation. Also creates a dedicated mock error test file and adds comprehensive mock error test coverage.

Partially-lazy cycle detection

A partially-lazy cycle (mix of constant and Instantiator hops, e.g. A → B → Instantiator<C> → C → A) generates code with forward variable references that Swift rejects: closure captures 'X' before it is declared. This was a latent production bug — the existing test only did string comparison and never compiled the output.

New error case partiallyLazyDependencyCycleDetected with guidance to make all hops lazy:

Dependency cycle detected. Cycles with a mix of constant and lazy (Instantiator)
dependencies cannot be resolved. Make all dependencies in the cycle lazy by using Instantiator:
    A -> B -> Instantiator<C> -> A

Two-phase mock cycle validation

Mock-only types (generateMock: true without isRoot) need their own cycle validation because:

  1. Pre-promotion validation (cyclesOnly: true on shared validatePropertiesAreFulfillable): Catches cycles visible in the pre-promotion scope map. Uses cyclesOnly flag to skip unfulfillable property checks (mock-only types have unfulfillable received properties by design — they become mock parameters). Non-root mock types are added as entry points alongside root types.

  2. Post-promotion validation (validateMockRootScopeForCycles): Runs on each mock root scope AFTER createMockRootScopeGenerator promotes @Received dependencies. Catches cycles that only become visible after promotion (e.g., B @Receives Instantiator<A> where A instantiates B — promotion creates a cycle in the mock root scope that isn't visible pre-promotion).

No code duplication

The pre-promotion cycle check reuses the shared validatePropertiesAreFulfillable with a cyclesOnly: Bool parameter (no default — callers must be explicit). The post-promotion check is a separate validateMockRootScopeForCycles that walks the promoted scope tree checking both propertiesToGenerate and dependencies.

New SafeDIToolMockGenerationErrorTests.swift

Dedicated file for mock generation error tests (the mock test file was 12,725 lines). Contains 9 tests:

Test Scenario
mock_throwsError_whenPartiallyLazyCycleThroughInstantiatorBoundary Partially-lazy cycle (mock-only, no root)
mock_throwsError_whenConstantDependencyCycleExists All-constant cycle
mock_throwsError_whenUnfulfillableInstantiatedPropertyExists Unknown type
mock_throwsError_whenReceivedPropertyIsUnfulfillable Unfulfillable @received
mock_throwsError_whenInstantiatedPropertyRefersToSelf Self-referencing
mock_throwsError_whenInstantiatedPropertyHasForwardedArgument @forwarded on non-Instantiator
mock_throwsError_whenReceivedInstantiatorDependencyCycleExists_withRoot Received cycle caught by root validation
mock_throwsError_whenReceivedInstantiatorDependencyCycleExists_withoutRoot Received cycle caught by post-promotion validation

Production test changes

  • Moved run_writesConvenienceExtensionOnRootOfTree_whenPartiallyLazyInstantiationCycleExists to error test file
  • Removed corresponding DOT generation test (cycle now errors before DOT generation)

Test plan

  • 768 tests pass across 20 suites
  • Lint clean
  • 100% coverage on modified source
  • Fully-lazy and self-forwarding cycle tests unaffected

🤖 Generated with Claude Code

A partially-lazy cycle (constant + Instantiator hops) generates code
with forward variable references that Swift rejects. Previously this
was silently allowed. Now emits partiallyLazyDependencyCycleDetected
with guidance to make all hops lazy.

Moved the broken production test to the error test file. Created
SafeDIToolMockGenerationErrorTests.swift for mock error tests.

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

codecov bot commented Apr 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (7c1ef2b) to head (9ef7291).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##              main      #220   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           40        40           
  Lines         6055      6127   +72     
=========================================
+ Hits          6055      6127   +72     
Files with missing lines Coverage Δ
...afeDICore/Generators/DependencyTreeGenerator.swift 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Adds 6 additional error tests to SafeDIToolMockGenerationErrorTests:
- Constant dependency cycle
- Unfulfillable @Instantiated property
- Unfulfillable @received property
- Self-referencing instantiation
- @forwarded property on non-Instantiator child
- @received Instantiator dependency cycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed self-assigned this Apr 6, 2026
@dfed dfed marked this pull request as ready for review April 6, 2026 16:32
dfed and others added 2 commits April 6, 2026 09:40
The partially-lazy cycle check was only reached through root validation.
Mock-only types (generateMock: true without isRoot) bypassed it. Now
adds a separate validateMockScopeForCycles pass in generateMockCode
that checks for constant and partially-lazy cycles without also
rejecting unfulfillable properties (which are expected in mock-only
types as they become required mock parameters).

The mock error test no longer includes a root Parent — it exercises
the mock-only validation path directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the separate validateMockScopeForCycles function with a
cyclesOnly parameter on the shared validatePropertiesAreFulfillable.
When cyclesOnly is true, unfulfillable property collection is skipped
and mock-only types are added as entry points.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dfed and others added 4 commits April 6, 2026 09:44
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pre-promotion scope map doesn't show cycles that appear after
@received dependencies are promoted to the mock root. Now validates
each mock root scope after createMockRootScopeGenerator builds it,
catching received-Instantiator cycles that only exist in the promoted
mock tree.

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

Three copies of the cycle type classification (constant vs partially-lazy
vs received-Instantiator) are replaced by a single throwIfInvalidCycle
static method. Fully-lazy cycles (valid) fall through without throwing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed merged commit df34044 into main Apr 6, 2026
19 checks passed
@dfed dfed deleted the dfed--partially-lazy-cycle-error branch April 6, 2026 17:09
dfed added a commit that referenced this pull request Apr 6, 2026
This function was added by #220 on main after the original PR branch
diverged, so it retained OrderedSet references that the auto-merge
did not convert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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