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
6 changes: 3 additions & 3 deletions SPEC.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions example/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ analyzer:
avoid_print: ignore
unnecessary_statements: ignore
always_use_package_imports: ignore
# The generator's const-call-site rewriter is per-file: it can't know
# that a sibling file's class became const-eligible after lowering
# (e.g. `CounterPage` gains a const constructor once its `@SolidState`
# fields move to a generated State class), and `source/` can't carry
# the `const` pre-lowering since the class isn't const-eligible there.
# Generated call sites legitimately lack `const` in this case.
prefer_const_constructors: ignore
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
linter:
rules:
public_member_api_docs: false
2 changes: 1 addition & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import 'package:flutter/material.dart';
import 'counter.dart';

void main() {
runApp(const MaterialApp(home: CounterPage()));
runApp(MaterialApp(home: CounterPage()));
}
8 changes: 8 additions & 0 deletions examples/chat/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ analyzer:
use_setters_to_change_properties: ignore
deprecated_member_use: ignore
avoid_equals_and_hash_code_on_mutable_classes: ignore
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
linter:
rules:
public_member_api_docs: false
Expand Down
8 changes: 8 additions & 0 deletions examples/todos/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ analyzer:
# `Foo` from the generic call but `very_good_analysis` treats this as
# non-obvious. The type IS clear from the receiver; suppress here.
specify_nonobvious_property_types: ignore
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
linter:
rules:
public_member_api_docs: false
Expand Down
8 changes: 8 additions & 0 deletions examples/weather/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ analyzer:
use_setters_to_change_properties: ignore
deprecated_member_use: ignore
avoid_equals_and_hash_code_on_mutable_classes: ignore
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
linter:
rules:
public_member_api_docs: false
Expand Down
9 changes: 9 additions & 0 deletions packages/integration_tests/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ analyzer:
linter:
rules:
public_member_api_docs: false
# `source/` uses relative imports for same-package siblings, and the
# generator mirrors those paths 1:1 into `lib/`. Turn off
# `very_good_analysis`'s default `always_use_package_imports` so the
# generated `lib/` imports stay relative. Unlike `example/`, don't
# enable `prefer_relative_imports` as a tripwire here: `test/`
# deliberately imports the generated code via `package:integration_tests/...`
# (it exercises the public, generated `lib/` API, not a same-package
# sibling), so the rule would flag those intentional imports.
always_use_package_imports: false
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:solid_annotations/solid_annotations.dart';
class Counter implements Disposable {
final value = Signal<int>(0, name: 'value');

final history = ListSignal<int>(const [], name: 'history');
final history = ListSignal<int>([], name: 'history');

@override
void dispose() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_solidart/flutter_solidart.dart';
import 'package:integration_tests/cross_file_env/counter_controller.dart';
import 'package:provider/provider.dart';
import 'counter_controller.dart';

class CounterDisplay extends StatefulWidget {
const CounterDisplay({super.key});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Counter {
int value = 0;

@SolidState()
List<int> history = const [];
List<int> history = [];

// Empty stub — the source-layer analyzer needs to resolve `c.dispose()` in
// the `.environment<Counter>(... dispose: (_, c) => c.dispose())` callback.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// Cross-file env-injection consumer — imports `Counter` from a sibling
// source file via `package:integration_tests/cross_file_env/...`. The
// generator's resolver pass redirects same-package `lib/` URIs back to
// `source/` so the `@SolidState` annotations are visible to the rewriter
// even though the import resolves to `lib/cross_file_env/...` post-build.
// source file via a relative URI. The generator's resolver pass redirects
// relative source imports to their `lib/` counterparts so the
// `@SolidState` annotations are visible to the rewriter even though the
// import resolves to `lib/cross_file_env/...` post-build.

import 'package:flutter/material.dart';
import 'package:integration_tests/cross_file_env/counter_controller.dart';
import 'package:solid_annotations/solid_annotations.dart';

import 'counter_controller.dart';

class CounterDisplay extends StatelessWidget {
CounterDisplay({super.key});

Expand Down
10 changes: 5 additions & 5 deletions packages/integration_tests/test/cross_file_environment_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Runtime fence for the cross-file `@SolidEnvironment` chain rewrite.
// `Counter` is declared in `source/cross_file_env/counter_controller.dart`
// and consumed by `CounterDisplay` in `source/cross_file_env/counter_display.dart`
// via `package:integration_tests/cross_file_env/...`. The two files are
// generated independently — the generator's resolver pass redirects the
// same-package `lib/` URI back to `source/` so the rewriter sees the
// pre-transformation `@SolidState` annotations even though Dart resolves
// the import to `lib/`.
// via a relative URI. The two files are generated independently — the
// generator's resolver pass redirects relative source imports to their
// `lib/` counterparts so the rewriter sees the pre-transformation
// `@SolidState` annotations even though Dart resolves the import to
// `lib/`.
//
// This test asserts that the SignalBuilder placements emitted in
// `counter_display.dart` actually subscribe to the live ListSignal/Signal
Expand Down
4 changes: 4 additions & 0 deletions packages/solid_generator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.0.0-dev.5

- **FIX**: The dev.4 residual gap — a PURE CONSUMER whose ONLY link to a cross-file `@SolidState`-bearing class is a bare `super.x` constructor parameter (no type written at that position) — is now closed (#108), including issue #108's own primary example: a consumer that imports only its immediate superclass's file, never the file that actually declares the injected class. Detecting a pure consumer at all still requires the no-annotation fast path's UNRESOLVED syntactic probe, and a bare `super.x` still carries no type text at that position, but `_populateCrossFileTypes`'s seeding loop now resolves it from the AST alone: it reads the subclass's own `extends` clause for the superclass's simple name, locates that class's declaration (checking the current unit's own top-level declarations first — skipped when the `extends` clause is import-prefixed, since a prefixed reference can never resolve to a local declaration — then walking imports with the same shadowing/combinator/skip-set discipline the rest of the function already applies, PLUS two more: same-package only, since this package's `@SolidState` classes can never live in a cross-package file — critical in practice, since this same code path fires for `{super.key}` on every widget, and without the guard would otherwise chase `StatefulWidget`/`State` into `package:flutter`; and alias-aware, since an unprefixed name can only come from an unprefixed import and a prefixed name only from the import carrying that exact prefix — `_importExposesName`'s show/hide check alone can't tell a same-named decoy behind a DIFFERENT prefix from the real, unprefixed source), finds the super-constructor the subclass's initializer list targets (`super.named(...)` for a named constructor, else the implicit call to the unnamed one), and matches the bare parameter against that constructor's corresponding parameter — positionally among its other `super.` siblings (never ambiguous for code that compiles, since Dart itself rejects mixing a positional `super.x` with an explicit positional argument in the same invocation) or by name. When the matched parameter is itself an untyped `this.x` field formal, the same-named instance field's declared type is used instead; when it is ITSELF a further bare `super.x` (a chain of plain relaying subclasses forwarding the same parameter), the search recurses up the inheritance chain, up to five superclasses deep, before giving up. The syntactic walk is skipped entirely once the RESOLVED fallback (`_seedFromResolvedSuperFormal`, dev.3) already answered the question — running the full import-and-parse walk on top of a successful resolution was pure waste, and, unconditional, was the other half of the `{super.key}`-on-every-widget cost. Once the wanted class's simple name is known, locating ITS OWN declaration gets exactly one further hop beyond the import walk just described — through the file that hosts the superclass's own imports, same-package-only, never a general transitive walk: this is what closes issue #108's own primary example, where the injected class is declared in a file the pure consumer never imports at all, only its superclass's file does. A class reachable only through a LONGER chain of imports beyond that one extra hop remains a documented residual, not recognized. When the matched parameter's declared type is itself one of the superclass's OWN type parameters (`class Base<T> { Base(this.repo); final T repo; }`), the wanted name is mapped BY INDEX to the subclass's own `extends Base<AuthRepository>` type argument, recursing through nested generics, instead of seeding the placeholder name verbatim — a bare type-parameter reference is a syntactically ordinary type name, so without this mapping it silently seeded a name (`T`) that matches no class anywhere and the real dependency (`AuthRepository`) was never sought at all (fix review addendum finding 2). When the superclass can't be located, the targeted constructor can't be pinned down, the matched parameter resolves to no usable type, or the type-parameter mapping has no type argument to resolve against, every candidate name from the located superclass (every constructor parameter, every instance field) is seeded rather than guessing wrong or skipping silently — over-seeding a name is harmless, since the registry only fills from classes that actually carry `@SolidState` members, though it does further widen the pre-existing same-simple-name registry-collision surface (§4.9) by proposing names that would otherwise never have been candidates for the file at all. The fix lives in two shared helpers, `_seedFromSyntacticSuperFormal` (the chain search) and `_populateCrossFileTypesOneHop` (the one-hop class-location extension, sharing its registration rule with the main cross-file walk via `_registerWantedClassesFrom`), both reached from `_populateCrossFileTypes`'s single seeding loop, so the probe and the main pipeline pick them up identically.

## 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).
Expand Down
Loading
Loading