fix(solid_generator): seed cross-file registry from constructor-injected holder types - #105
Merged
Merged
Conversation
…ted holder types Closes #104. _populateCrossFileTypes seeded wantedTypes only from @SolidEnvironment field types and Provider/.environment<T>() call sites, so a class reached solely through plain constructor injection (a field like `final AuthRepository _authRepository;`) never entered the registry and its @SolidState reads stayed silently un-lowered — always-non-null Signal comparisons that dart fix can collapse into dead code. wantedTypes is now additionally seeded from every class's instance-field and constructor-parameter declared type names (incl. explicitly-typed super parameters and generic type arguments, recursively), with resolution mirroring Dart's own rules: names declared in the current unit shadow imports, import show/hide combinators are honored, and dart:core/dart:async SDK names are skipped. Bare `super.x` shorthand (no type in source) remains a documented syntactic-walk gap. Seven golden fixtures cover the issue shape (reads, writes, ??=, this.), param-only seeding, super-param, collection generics via for-in, the local-shadowing decoy, the hide-combinator case, and a no-@SolidState negative.
…wering A @SolidState getter lowering to a Computed whose body reads cross-file @SolidState state through a constructor-injected field was the one untested combination of the #104 registry seeding with the derived-state path. Empirically GREEN: the getter lowering already receives the seeded registry, so both the null-guard and the null-assert chain lower to .value inside the Computed lambda. Coverage-only — no generator change.
nank1ro
added a commit
that referenced
this pull request
Aug 26, 2026
…umer files (#107) * fix(solid_generator): lower cross-file @SolidState reads in pure-consumer files Closes #106. Files with no @solid* annotation text and no provider hint took a verbatim-passthrough fast path and never entered the pipeline, so the #105 registry seeding never ran for them: a pure consumer's cross-file state reads stayed silently un-lowered, and dart fix could collapse the resulting always-non-null guards into dead code (reproduced as a generated authentication bypass in a real router guard). The bailout now probes the #105 syntactic seeding first and, when the cross-file registry is non-empty, runs a dedicated plain-class lowering pass (no Disposable/dispose synthesis) with the resolved unit BEFORE dispose call-site injection — ordering matters: injecting first would hand the lowering pass edited text and lose staticType receiver resolution (loop variables), reintroducing the bug. The probe's registries are threaded into the pipeline (no duplicate import walk); genuinely dependency-free files keep a zero-import-walk path. Documented residual gaps: widget-class pure consumers (SignalBuilder placement is a larger change) and static-field-mediated DI (seeding deliberately skips static fields). Four golden fixtures: pure consumer (reads/writes/!.-chain), router-guard if/else shape, provider-hint + loop-variable combo, and a solid-free negative proving the verbatim path survives. * fix(solid_generator): close the pure-consumer residual gaps Widget-class pure consumers now get the SignalBuilder-wrapped build() the @SolidEnvironment path already uses (no stateful lift — a constructor-injected field needs no context), with a combinator-aware import repair driven by the wrap's own emitted flag (a show/hide- restricted flutter_solidart import is widened, never substring-guessed). Static-field-mediated DI seeds the registry (the isStatic skip had no recorded rationale). Bare super.x params seed from the resolved element type on the main path; a pure consumer whose only link is a bare super.x still misses the syntactic probe gate — documented. A new value_rewriter tier resolves an untyped field's type from its field-formal or initializer-list constructor parameter, bailing on conflicting constructors. The .first collection receiver is pinned by fixture. Both pure-consumer lowering passes are now pure edit-collectors over the same pristine source and resolved unit, merged and applied in a single transformation — running either pass on the other's edited text starved it of staticType resolution and silently reintroduced the #106 guard collapse for tier-1-only reads co-located with a widget consumer (caught in review, RED-proven by fixture).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #104.
Problem
_populateCrossFileTypesseededwantedTypesonly from@SolidEnvironmentfield types andProvider(...)/.environment<T>()call sites. A class reached solely through plain constructor injection never entered the registry, so its@SolidStatereads through the injected reference stayed silently un-lowered — producing always-non-nullSignalcomparisons thatdart fixcan collapse into dead code (reproduced in a real app migration; see the issue).Fix
Seed
wantedTypesadditionally from every class's instance-field and constructor-parameter declared type names, including explicitly-typedsuper.parameters and generic type arguments (recursively —List<AuthRepository>seedsAuthRepository). Resolution mirrors Dart's own name rules:show/hidecombinators are honored in the walk,dart:core/dart:asyncSDK names are skipped (keeps thewantedTypes.isEmptyfast path effective — primitives no longer force the import walk).SPEC.md §4.9 rule 7 updated accordingly (per the #103 precedent).
Review guide — golden fixtures (
test/golden/)cross_file_constructor_injected_holder!-chain, write,??=,this.all lower to.value(RED captured pre-fix)cross_file_constructor_param_type_onlycross_file_super_param_type_onlysuper.param seedingcross_file_collection_generic_arg_seedingcross_file_local_shadowing_decoycross_file_import_hide_combinatorhide-excluded import can't claim a namecross_file_constructor_injected_no_stateKnown residual gaps (documented, not regressions)
super.xshorthand carries no type in source — unrecoverable by a syntactic walk (code comment + CHANGELOG caveat).dynamicunderstrict-inferenceand is a separate, pre-existingvalue_rewriterreceiver-resolution gap — worth its own issue if wanted.repos.first.fieldrewrite was manually verified but has no persisted fixture (CHANGELOG says so explicitly).Verification
317/317 solid_generator tests (incl. idempotency for all new fixtures), 11/11 integration tests,
dart analyze --fatal-infosclean, format clean, all example apps rebuilt with zero output drift (none use this DI shape yet). Version cut:solid_generator 3.0.0-dev.3;solid_annotationsuntouched.