solid_generator 3.0.0-dev.4 copy "solid_generator: ^3.0.0-dev.4" to clipboard
solid_generator: ^3.0.0-dev.4 copied to clipboard

Solid source-to-lib code generator for Flutter reactive state.

3.0.0-dev.4 #

  • FIX: The builder's fast bailout for files with no @Solid* annotation and no Provider(...)/.environment<T>() call site now runs the same cheap cross-file registry seeding _populateCrossFileTypes already implements (#104/#105) against the file's UNRESOLVED parsed unit before giving up. A PURE CONSUMER — a file that holds a @SolidState-bearing class through plain constructor injection or an instance field, but carries no @Solid* annotation and no provider call site of its own — previously matched neither hint text, so the file was never even parsed and the cross-file seeding never ran for it: every read through the injected field (_authRepository.session) stayed silently un-lowered. Reproduced in a real app as a generated authentication bypass, where dart fix's dead_code/unnecessary_null_comparison passes collapsed a router guard's null check because the un-lowered field looked always-non-null (#106). Honest cost accounting: a hint-free file now always pays the syntactic parse (previously skipped entirely for such files); only a file with no custom-typed fields/params at all (just core-SDK/self-declared types) avoids the import walk and any resolver call, and that narrower path is what stays zero-cost — not "no work at all". A file whose field/param DOES name an externally-declared type pays a bounded import walk (bounded by the file's own import count), and a same-named-but-non-reactive match along the way (the cross_file_constructor_injected_no_state shape) does not stop that walk early — the probe's registries are threaded into the pipeline's own cross-file pass so the walk still only runs once per file. Lowering itself is applied by a new, narrower pass (collectPureConsumerCrossFileEdits) that only rewrites .value reads on plain-class members — it does not add implements Disposable or synthesize dispose(), since a pure consumer owns no reactive member of its own to dispose. The builder runs this lowering BEFORE the Provider/.environment auto-dispose pass, not after: lowering depends on value_rewriter.dart's tier-1 Expression.staticType resolution to type receivers the AST-only tiers can't (e.g. a for-in loop variable), so it needs the still-valid resolved unit the dispose pass's text edits would otherwise invalidate; the dispose pass, by contrast, degrades gracefully to its AST-only tiers on a re-parsed, unresolved unit. Both shapes originally named here as residual gaps are now closed. A pure-consumer StatelessWidget/State<X> gets the same .value lowering plus reactive rebuild through a sibling pass, collectPureConsumerWidgetEdits: it reuses rewriteBuildMethod verbatim on the class's build() method — the exact SignalBuilder-wrap-placement machinery an @SolidEnvironment-consuming widget's build() already goes through — with no widget-class Stateless→Stateful lift, since a plain constructor-injected field needs no BuildContext (unlike @SolidEnvironment, which needs context.read<T>()). The plain-class pass and the widget pass are unified by lowerPureConsumers: both collect their edits against the SAME pristine source text and resolved unit, and the two edit lists are merged and applied together in a single text transformation — safe because the two passes visit disjoint class-kind sets (plainClass vs statelessWidget/stateClass), so their edit ranges never overlap. (An earlier draft of this fix ran the two passes sequentially, re-parsing the widget pass's OUTPUT for the plain-class pass whenever the widget pass had edited anything — which silently dropped resolved-unit-dependent receiver resolution, e.g. a static-holder read, for a plain-class guard sharing a file with a widget pure consumer; fixed before release by collecting both edit sets up front instead of reordering.) Static-field-mediated DI (static final AuthRepository instance = …;, consumed elsewhere as Holder.instance.session) is also now recognized: the _populateCrossFileTypes DI-seeding loop's if (member.isStatic) continue; guard carried no stated rationale anywhere in the #104/#105 history — it simply mirrored @SolidState's own unrelated instance-only restriction — and is removed. Receiver resolution needed no new code at all: Holder.instance.session parses as a PropertyAccess whose target (Holder.instance) already resolves through the existing tier-1 Expression.staticType check to AuthRepository once the type is seeded, verified empirically against package:analyzer directly. The flutter_solidart import needed for a widget pure consumer's SignalBuilder wrap is spliced in — or, if the file already imports flutter_solidart with a show/hide combinator that doesn't expose SignalBuilder (e.g. a pre-existing show Signal for an unrelated hand-rolled signal), REPAIRED in place — based on the lowering pass's own report of whether it emitted a wrap, not a substring scan for SignalBuilder( in the assembled output (which both risked a false positive on a preserved source comment and couldn't distinguish an import that exposes the name from one that doesn't).

Two narrower, adjacent gaps close partially in the same pass. The bare-super.x gap from dev.3 (below) is now closed on the RESOLVED path only: a file that already enters the main lowering pipeline for another reason (its own @Solid* annotation, or a Provider/.environment<T>() call site) consults SuperFormalParameter.declaredFragment.element.type when the AST carries no explicit type annotation, seeding wantedTypes the same as if the type had been written out — verified empirically that the resolved element's type IS populated even without a source-level annotation. This is moot for a PURE consumer whose only link to the cross-file class is a bare super.x: such a file carries no annotation and no provider hint, so it takes the no-annotation fast path's UNRESOLVED syntactic probe first, finds nothing to seed, and short-circuits to a verbatim copy before any resolved unit is ever requested — that narrower shape remains a known, accepted gap. Separately, value_rewriter.dart's cross-class receiver resolution gained a fourth fallback tier: when an instance field is declared with no type annotation at all (final _service; — infers dynamic, since a same-named constructor field-formal parameter's explicit type does NOT propagate back onto the field's inferred type, confirmed empirically against a real resolved unit), the resolver now falls back to a matching FieldFormalParameter's explicit type, or an initializer-list-assigned SimpleFormalParameter's explicit type; multiple constructors supplying conflicting types bail to unresolved rather than guess.

3.0.0-dev.3 #

  • FIX: The cross-file class registry (_populateCrossFileTypes) now seeds wantedTypes from the declared type names of every class's instance fields and constructor parameters, not just @SolidEnvironment field types and Provider(...)/.environment<T>() call sites. A file that only constructor-receives a @SolidState-bearing class — the plain DI shape, e.g. CustomersRepository({required AuthRepository authRepository}) storing final AuthRepository _authRepository;, with no @SolidEnvironment field and no same-file .environment()/Provider() call site — previously left classRegistry empty for that type, so cross-class .value reads through it (_authRepository.session) were silently un-lowered: no compile error, just an always-non-null Signal object that dart fix's unnecessary_null_comparison could collapse into dead code (#104).
  • FIX: The seeding above (and, defensively, every other seeding path into wantedTypes) now mirrors Dart's own name-resolution rules instead of blindly matching on simple class name. A simple name that the CURRENT file itself declares as a class/enum/mixin is dropped from the wanted set before the cross-file import walk starts — a local top-level declaration always shadows a same-name import, so attributing an unrelated imported class's reactive members to it was provably wrong (e.g. a file with its own plain class Address plus an unrelated, unconnected @SolidState-annotated class Address imported from elsewhere previously emitted a non-compiling address.line1.value against the local class). Each import's show/hide combinators are also now honored: an import that hides the wanted name, or shows a list that excludes it, can no longer be credited as that name's source, closing a second, narrower collision window.
  • FIX: Constructor parameters using the explicit-typed super. shorthand (Foo(AuthRepository super.repo)) now seed wantedTypes the same as a plain or field-formal parameter. The far more common bare super.repo (no type written) is a known, accepted gap: the type isn't present in the source at that position at all — recovering it would require resolving repo against the superclass's matching field/parameter, which this syntactic AST walk does not do — so a bare-shorthand super. parameter still isn't a seeding source.
  • FIX: A field or constructor parameter declared with a generic collection type (final List<AuthRepository> repos;) now seeds wantedTypes from every type argument, at every nesting level, not just the outer container name (List). This lets the existing resolved-static-type rewrite tiers recognize collection-derived receivers whose element type is Solid-lowered — covered by golden fixtures for a for (final r in repos) { r.field } loop variable and a repos.first.field receiver (same resolved-type mechanism); deeper nesting (Map<K, List<T>> and beyond) is mechanically identical but likewise untested. Container names themselves (List, Map, String, and other dart:core/dart:async SDK types) are now filtered out of every seed in the new constructor-injection/field loop before being added, which also fixes a performance regression where annotation-blind seeding of primitive types on nearly every field defeated the wantedTypes.isEmpty fast-path.

3.0.0-dev.2 #

  • FIX: Cross-class .value rewrite now resolves constructor-injected instance fields (final AuthRepository _authRepository;), not just method/function parameters and @SolidEnvironment fields. Previously a bare instance field receiver silently kept its unlowered form, producing always-true null checks and compile errors against the unboxed Signal payload. Also covers a this.-prefixed receiver (this._authRepository.session) — this.<field> parses as a distinct AST shape from the bare _authRepository.session form and previously fell through unrewritten.
  • FIX: .environment() / Provider(...) dispose auto-injection is now type-aware — dispose: (context, provider) => provider.dispose() is injected per a four-tier decision: (1) the created type provably has dispose() (own declaration, or inherited — including transitively through a same-file base-class chain); (2) the created type is @Solid*-annotated, same-file OR cross-file (every Solid-lowered class synthesizes dispose()); (3) the type's declaration is visible and shows neither → skip, no injection; (4) the declaration isn't visible anywhere this check looked (typically a cross-file, non-@Solid* type) → inject anyway, preserving the pre-type-aware default so a wrong guess fails loudly at compile time rather than silently leaking a resource, with dispose: null as the explicit opt-out. Previously the injection was unconditional and made source-layer typechecking fail with a compile-time undefined_method error on .dispose() for any type with no dispose() method; omitting dispose: for such a type now injects nothing (same as an explicit dispose: null) instead of a load-bearing dispose: null workaround. This changes generated output for existing call sites that omit dispose: on a dispose-less type — the compile error goes away.
  • FIX: The dispose auto-injection above now also recognizes a cross-file @Solid*-annotated type provided via .environment<T>() / Provider<T>(...) even when nothing in the providing file consumes T through an @SolidEnvironment field. Previously such a controller — the dominant real-world shape, e.g. a top-level main() that provides a controller it never itself consumes — got NO dispose injection at all: its synthesized dispose() only exists after lowering, which was invisible to every check this rewriter had. This was a silent resource leak, not a compile error, because the call site was already valid Dart (dispose: simply absent).

3.0.0-dev.1 #

  • BREAKING: Raise the Dart SDK lower bound to ^3.10.0 to target the solidart v3 ecosystem.
  • CHORE: Upgrade analyzer to ^12.0.0 and adapt to its reshaped class/enum declaration AST (name and members moved onto namePart/body for primary constructors).
  • CHORE: Bump solid_annotations to ^3.0.0-dev.1, dart_style to ^3.1.8, and build/build_runner/build_test.

2.0.0+1 #

  • DOCS: Update README installation.

2.0.0 #

  • FEAT: SignalBuilder placement, .value rewrite, dispose synthesis, StatelessWidget→StatefulWidget split.
  • FEAT: Computed synthesis from getter form of @SolidState.
  • FEAT: Fine-grained reactivity with untracked-read semantics (.untracked).
  • FEAT: Support the untracked(() => …) function form for untracked writes inside reactive bodies (e.g. writing a collection signal in a @SolidEffect without a cyclic reaction). The call passes through to flutter_solidart's untracked; inner reads still receive .value but are not tracked. Previously this form was rejected.
  • FEAT: Effect lowering with initState materialization for State and plain-class targets.
  • FEAT: Resource lowering for Future/Stream with .when() / .refresh() call-site preservation.
  • FEAT: Environment field synthesis with Provider-backed DI and cross-class chain rewrites.

1.0.3 #

  • FIX: Missing flutter_solidart import in generated main.dart file, if no reactive annotations are used.

1.0.2 #

  • FIX: Generator not transpiling code correctly in some cases.

1.0.1 #

  • FIX: Remove Flutter SDK.

1.0.0+2 #

  • CHORE: Add flutter sdk to resolve score on pub.dev.

1.0.0 #

  • Initial version.