Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,9 @@ Rules:
- **The builder's no-annotation fast path** (a file with zero `@Solid*`-annotated classes) hands the generator a fully-RESOLVED `CompilationUnit`. Clause 1 uses `InterfaceType.lookUpMethod`; clause 2 is checked BOTH via the name-based registry below AND, secondarily, via the resolved element's own annotation metadata. The registry is the primary cross-file mechanism: it walks the importing file's imports in order, and a same-named class with no `@SolidState` members no longer halts the search — the wanted type name stays outstanding until an actually-annotated match is found (or every import is exhausted), so a non-annotated decoy earlier in import order can't strand a real annotated class in a later import unrecognized. The secondary resolved-element-annotation check remains a belt-and-suspenders backstop for cases the registry doesn't reach; it is not the mechanism the design relies on. A resolved-but-unproven type is clause 3 (skip): resolution IS visibility.
- **The main lowering path** (a file with at least one `@Solid*`-annotated class) re-parses the fully ASSEMBLED, already-lowered output text with NO resolver at all. Clause 1 is a same-file-only AST check (a Solid-lowered class's synthesized `dispose()` is already textually present in that reassembled text by the time this check runs). Clause 2 is name-based only, via the registry below. A type whose declaration lives in another file and ISN'T in the registry — e.g. a plain, dispose-less repository imported and provided but never itself consuming anything — cannot be proven plain on this path and falls to clause 4 (inject); `dispose: null` is required there even though the type genuinely has no `dispose()`.

Clause 2's registry is the SAME class-name → reactive-member-name map (`classRegistry`) the builder threads through the `.value` cross-class rewrite (§5.1) — populated same-file by a fast member-scan and cross-file by walking imports (the resolver pass documented under §3.6's env-field rule), extended to also seed from every type created at a `Provider(...)` / `.environment<T>()` call site in the file (not just `@SolidEnvironment` field types), so a controller that's provided but never consumed via `@SolidEnvironment` still gets recognized. A type name enters the registry ONLY when its declaration carries at least one `@SolidState` field or getter. Matching is by SIMPLE class name, not library identity — two distinct types sharing a name across different libraries could in principle collide; a pre-existing risk of the name-based registry design, not newly introduced by this rule.
Clause 2's registry is the SAME class-name → reactive-member-name map (`classRegistry`) the builder threads through the `.value` cross-class rewrite (§5.1) — populated same-file by a fast member-scan and cross-file by walking imports (the resolver pass documented under §3.6's env-field rule), extended to also seed from every type created at a `Provider(...)` / `.environment<T>()` call site in the file (not just `@SolidEnvironment` field types), so a controller that's provided but never consumed via `@SolidEnvironment` still gets recognized. It is further extended to seed from the declared type name of every class's instance field and constructor parameter in the file — the plain constructor-injection DI shape (`CustomersRepository({required AuthRepository authRepository})`, `final AuthRepository _authRepository;`), which has no `@SolidEnvironment` field and creates nothing at a `Provider(...)` / `.environment<T>()` call site, so neither of the other two rules ever seeds it (issue #104). A type name enters the registry ONLY when its declaration carries at least one `@SolidState` field or getter; a candidate name that matches nothing found is harmless.

Two rules narrow the cross-file import walk to mirror Dart's own name resolution, closing collision windows the annotation-blind seeding above would otherwise open: a simple name that the CURRENT file itself declares as a class/enum/mixin/etc. is dropped from the wanted set before the import walk starts — a local top-level declaration always shadows a same-name import (no error, no ambiguity), so cross-file attribution for that name is provably wrong and the import walk never even looks for it; and each import's `show`/`hide` combinators are honored — an import that hides the wanted name, or carries a `show` list that doesn't include it, cannot be credited as that name's source and is skipped for that name (its other names are unaffected). Matching is otherwise by SIMPLE class name, not library identity — two distinct types sharing a name across two DIFFERENT imported libraries, neither of them local to the current file and neither excluded by a combinator, could in principle still collide; a residual risk of the name-based registry design, not resolved by the two rules above.

The injected `provider.dispose()` always compiles for Solid-lowered types because Section 10 attaches `implements Disposable` and a synthesized `dispose()` to every annotated class. For non-Solid types, the user declares `void dispose()` (or a known disposable base) on the source class for clause 1 to fire; otherwise nothing is injected (clause 3) unless the type is cross-file and unproven (clause 4), and the user may always opt in or out by writing `dispose:` explicitly.

Expand Down
7 changes: 7 additions & 0 deletions packages/solid_generator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 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 `show`s 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 a golden fixture for a `for (final r in repos) { r.field }` loop variable; a `repos.first.field` receiver was verified manually to rewrite the same way (same resolved-type mechanism) but has no persisted fixture, and 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.
Expand Down
Loading
Loading