From ec1f38bbe60cba1a91493fde8f07dee3ca828187 Mon Sep 17 00:00:00 2001 From: Alexandru Mariuti Date: Sat, 29 Aug 2026 09:32:53 +0700 Subject: [PATCH] fix(solid_generator): doc-comment .value rewrite + effect materialization lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two quirks: - A doc-comment reference to a reactive field (/// [count]) is no longer rewritten to [count.value] — the value rewriter skips comment references. - @SolidEffect lowers to a declared 'late final Effect ;' assigned at its materialization site instead of a bare ';' touch, so generated output no longer trips unnecessary_statements. Same closure, same timing. Adds doc_comment_reference_preserved + two_effects_ordering goldens. Reviewed by 2 adversarial reviewers (0 bugs; 1 doc nit fixed, multi-effect golden added, factory-redirect edge documented). --- packages/solid_generator/CHANGELOG.md | 5 + .../lib/src/plain_class_rewriter.dart | 16 +-- .../lib/src/signal_emitter.dart | 134 ++++++++++-------- .../lib/src/state_class_rewriter.dart | 20 +-- .../lib/src/stateless_rewriter.dart | 30 ++-- .../lib/src/value_rewriter.dart | 11 ++ packages/solid_generator/pubspec.yaml | 2 +- .../doc_comment_reference_preserved.dart | 14 ++ .../golden/inputs/two_effects_ordering.dart | 21 +++ .../outputs/collection_mixin_breadth.g.dart | 28 ++-- ...puted_reading_same_class_collection.g.dart | 8 +- .../doc_comment_reference_preserved.g.dart | 14 ++ .../effect_block_body_shadowing.g.dart | 16 +-- .../outputs/effect_on_plain_class.g.dart | 8 +- .../outputs/effect_on_state_class.g.dart | 8 +- .../effect_reads_widget_ctor_field.g.dart | 8 +- .../effect_with_signal_and_computed.g.dart | 8 +- .../plain_class_user_ctor_with_effect.g.dart | 8 +- .../outputs/query_on_plain_class.g.dart | 8 +- .../query_with_signal_computed_effect.g.dart | 8 +- .../outputs/simple_effect_with_deps.g.dart | 8 +- ...stateless_lift_with_user_init_state.g.dart | 8 +- .../outputs/two_effects_ordering.g.dart | 26 ++++ .../outputs/untracked_function_call.g.dart | 10 +- .../untracked_in_disposable_class.g.dart | 10 +- .../untracked_merges_existing_hide.g.dart | 10 +- .../test/integration/golden_helpers.dart | 2 + 27 files changed, 277 insertions(+), 172 deletions(-) create mode 100644 packages/solid_generator/test/golden/inputs/doc_comment_reference_preserved.dart create mode 100644 packages/solid_generator/test/golden/inputs/two_effects_ordering.dart create mode 100644 packages/solid_generator/test/golden/outputs/doc_comment_reference_preserved.g.dart create mode 100644 packages/solid_generator/test/golden/outputs/two_effects_ordering.g.dart diff --git a/packages/solid_generator/CHANGELOG.md b/packages/solid_generator/CHANGELOG.md index b1e4db0..cb71678 100644 --- a/packages/solid_generator/CHANGELOG.md +++ b/packages/solid_generator/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.0-dev.9 + +- **FIX**: A documentation-comment reference to a reactive field (`/// … [count] …`) is no longer rewritten to `[count.value]` — the value rewriter skips comment references (which resolve as declarations, not runtime reads), so the generated doc comment stays a valid reference. The member body still gets its `.value` append. +- **FIX**: `@SolidEffect` now lowers to a declared `late final Effect ;` field assigned at its materialization site (` = Effect(...)` in the synthesized constructor / `initState`) instead of a `late final = Effect(...)` field plus a bare `;` touch. The old touch tripped `unnecessary_statements` in the generated output; the assignment does not. Behaviour is unchanged (same closure, same materialization timing). + ## 3.0.0-dev.8 - **FEAT**: Recognize `.previousReady` and `.previousError` (alongside the existing `previousState`) as tracked reads, so a `build()` reading only a retained-state getter gets the `SignalBuilder` wrap + `flutter_solidart` import. Same-class and cross-instance (origin-qualified). Backs solidart 3.0.0-dev.2's new `Resource.previousReady`/`previousError`. diff --git a/packages/solid_generator/lib/src/plain_class_rewriter.dart b/packages/solid_generator/lib/src/plain_class_rewriter.dart index da9adf8..07f7372 100644 --- a/packages/solid_generator/lib/src/plain_class_rewriter.dart +++ b/packages/solid_generator/lib/src/plain_class_rewriter.dart @@ -122,7 +122,7 @@ RewriteResult rewritePlainClass( final pieces = []; final disposeNames = []; - final effectNames = []; + final effects = []; // `dispose` emission is deferred until after the walk so the merge sees // the fully-populated `disposeNames` list. The slot index reserves the // member's source-order position in `pieces` so a user-declared @@ -131,7 +131,7 @@ RewriteResult rewritePlainClass( MethodDeclaration? disposeMethod; var disposeSlot = -1; // User-declared constructors (zero or more) are recorded for a merged - // emit after the walk — the merge needs `effectNames` populated to + // emit after the walk — the merge needs `effects` populated to // splice in the Effect-materialization reads at the end of each user // body. Slot indices preserve source-order position. Only generative // constructors get the merge; factory constructors round-trip verbatim @@ -175,10 +175,10 @@ RewriteResult rewritePlainClass( final effect = effectByName[name]!; pieces.add(emitEffectField(effect)); disposeNames.add(effect.methodName); - effectNames.add(effect.methodName); + effects.add(effect); } else if (queryByName.containsKey(name)) { // Queries are lazy — joining `disposeNames` only, never - // `effectNames`, so the synthesized constructor below skips them. + // `effects`, so the synthesized constructor below skips them. final query = queryByName[name]!; emitQueryFields( query, @@ -233,7 +233,7 @@ RewriteResult rewritePlainClass( // merged result. final mergedHeaderAndBody = mergeConstructor( ctor, - effectNames, + effects, source, className, ); @@ -283,8 +283,8 @@ RewriteResult rewritePlainClass( } else { pieces.add(disposeText); } - if (effectNames.isNotEmpty && userCtors.isEmpty) { - pieces.insert(0, emitConstructor(className, effectNames)); + if (effects.isNotEmpty && userCtors.isEmpty) { + pieces.insert(0, emitConstructor(className, effects)); } final header = _buildHeaderWithDisposable(classDecl, source); @@ -319,7 +319,7 @@ RewriteResult rewritePlainClass( if (hasSetSignalField) 'SetSignal', if (hasMapSignalField) 'MapSignal', if (solidGetters.isNotEmpty) 'Computed', - if (effectNames.isNotEmpty) 'Effect', + if (effects.isNotEmpty) 'Effect', if (solidQueries.isNotEmpty) 'Resource', // A multi-dep query synthesizes a Record-Computed source field, // requiring `Computed` in the import set. diff --git a/packages/solid_generator/lib/src/signal_emitter.dart b/packages/solid_generator/lib/src/signal_emitter.dart index a57d684..f9ad438 100644 --- a/packages/solid_generator/lib/src/signal_emitter.dart +++ b/packages/solid_generator/lib/src/signal_emitter.dart @@ -168,19 +168,36 @@ String emitComputedField(GetterModel g) { return ' late final ${g.getterName} = $ctor;'; } -/// Emits one `late final = Effect(, name: '');` line. -/// Mirrors [emitComputedField] (same closure shape, same `late final` -/// rationale, same body-text contract); the only differences are the absent -/// type parameter and the `Effect` ctor. -/// -/// `Effect(...)` takes a zero-param `void Function()` callback per the -/// upstream `flutter_solidart` API and returns an `Effect` object whose -/// `.dispose()` joins the unified disposal list emitted by [emitDispose]. -String emitEffectField(EffectModel e) { +/// Emits the field DECLARATION for one `@SolidEffect` — +/// `late final Effect ;` — without an initializer. The `Effect(...)` +/// construction is emitted separately as an assignment at the materialization +/// site ([emitInitState] / [emitConstructor] and their merge variants) via +/// [emitEffectInit]. +/// +/// Split declaration + assignment (the canonical `flutter_solidart` idiom: +/// declare the field, create the `Effect` in `initState` / the constructor) +/// instead of `late final = Effect(...)` + a bare `;` touch: the +/// touch is a no-value expression statement that trips `unnecessary_statements` +/// in the generated output, whereas an assignment does not. The Effect closure +/// also reads instance members (sibling Signals), which a NON-late field +/// initializer cannot — so the field stays `late final`. +/// +/// `Effect` (from `flutter_solidart`) is non-generic; its `.dispose()` joins +/// the unified disposal list emitted by [emitDispose]. +String emitEffectField(EffectModel e) => ' late final Effect ${e.methodName};'; + +/// Emits the materialization ASSIGNMENT for one `@SolidEffect` — +/// ` = Effect(, name: '');`. Spliced (indented, in +/// source-declaration order) into the synthesized/merged constructor or +/// `initState` by the callers below. Assigning the `late final` field there +/// both runs the `Effect(...)` autorun once and registers dependencies — +/// the plain-class / State-class analogue of the old bare-read touch, minus +/// the `unnecessary_statements` lint. Mirrors [emitEffectField]'s closure +/// shape and body-text contract. +String emitEffectInit(EffectModel e) { final debugName = e.annotationName ?? e.methodName; final closure = e.isBlockBody ? '() ${e.bodyText}' : '() => ${e.bodyText}'; - final ctor = "Effect($closure, name: '$debugName')"; - return ' late final ${e.methodName} = $ctor;'; + return "${e.methodName} = Effect($closure, name: '$debugName');"; } /// Emits one `late final = Resource(, name: '');` @@ -488,40 +505,36 @@ String mergeDispose( '${source.substring(lbrace + 1, method.end)}'; } -/// Emits an `initState()` method that materializes every `late final` Effect -/// field by reading it as a bare-identifier statement (`;`), in -/// source-declaration order. -/// -/// In Dart, `late final field = expr` defers the initializer until the field -/// is first read. Without this synthesized read, the Effect's factory -/// constructor — and its `effect.run()` autorun, which registers reactive -/// dependencies — would never fire during the widget's mounted lifetime. The -/// `dispose()` body's `.dispose()` call is the first read, by -/// which point signal mutations have already happened. -/// -/// Touching each Effect by name in `initState` triggers the `late final` -/// initializer at mount time, so `Effect(...)`'s autorun runs once with the -/// initial signal values and subscribes to subsequent changes. -/// -/// [effectNamesInDeclarationOrder] should mirror the source order of the -/// emitted `late final … = Effect(...)` fields. Caller is responsible for -/// only invoking this when the list is non-empty — the resulting `initState` -/// is otherwise a pure-overhead `super.initState()` no-op. -String emitInitState(List effectNamesInDeclarationOrder) { +/// Emits an `initState()` method that materializes every `late final Effect` +/// field by assigning it (` = Effect(...);`), in source-declaration +/// order. +/// +/// The Effect's factory constructor — and its `effect.run()` autorun, which +/// registers reactive dependencies — fires when the field is assigned. Doing +/// that assignment in `initState` runs the autorun once with the initial +/// signal values, at mount time, and subscribes to subsequent changes. The +/// fields are declared uninitialized ([emitEffectField]) precisely so this +/// site owns their construction. +/// +/// [effectsInDeclarationOrder] should mirror the source order of the emitted +/// `late final Effect …;` field declarations. Caller is responsible for only +/// invoking this when the list is non-empty — the resulting `initState` is +/// otherwise a pure-overhead `super.initState()` no-op. +String emitInitState(List effectsInDeclarationOrder) { final buffer = StringBuffer() ..writeln(' @override') ..writeln(' void initState() {') ..writeln(' super.initState();'); - for (final name in effectNamesInDeclarationOrder) { - buffer.writeln(' $name;'); + for (final e in effectsInDeclarationOrder) { + buffer.writeln(' ${emitEffectInit(e)}'); } buffer.write(' }'); return buffer.toString(); } -/// Splices Effect-materialization reads (`;`, in declaration -/// order) into an existing `initState()` body immediately after the -/// `super.initState();` call, so Effects subscribe to signals before any +/// Splices Effect-materialization assignments (` = Effect(...);`, in +/// declaration order) into an existing `initState()` body immediately after +/// the `super.initState();` call, so Effects subscribe to signals before any /// user code in `initState` runs. /// /// Splice point: the end of the first statement when it is recognised as @@ -534,7 +547,7 @@ String emitInitState(List effectNamesInDeclarationOrder) { /// expression body (`=> …`) — the merge is only well-defined for a block. String mergeInitState( MethodDeclaration method, - List effectNamesInDeclarationOrder, + List effectsInDeclarationOrder, String source, String className, ) { @@ -553,8 +566,8 @@ String mergeInitState( } else { insertAt = body.block.leftBracket.offset + 1; } - final reads = effectNamesInDeclarationOrder - .map((name) => ' $name;') + final reads = effectsInDeclarationOrder + .map((e) => ' ${emitEffectInit(e)}') .join('\n'); // Auto-add `@override` if the user omitted it — mirrors mergeDispose. The // lift path (`StatelessWidget` source has no `initState` to override, so @@ -578,38 +591,37 @@ bool _isSuperInitStateCall(Statement stmt) { return expr.target is SuperExpression && expr.methodName.name == 'initState'; } -/// Emits a no-arg constructor whose body materializes every `late final` -/// Effect field by reading it as a bare-identifier statement -/// (`;`), in source-declaration order. +/// Emits a no-arg constructor whose body materializes every `late final Effect` +/// field by assigning it (` = Effect(...);`), in source-declaration +/// order. /// /// A plain Dart class has no `initState` lifecycle hook, but the same -/// `late final` Effect-materialization rule applies — without a synthesized -/// read, the Effect's factory constructor never runs and the autorun never -/// registers dependencies. Reading each Effect inside the generated -/// constructor body is the plain-class analogue of [emitInitState] for State -/// classes: the Effects activate at construction time, so a `Counter()` -/// instantiation is enough to start the autoruns. +/// materialization rule applies — without the assignment, the Effect is never +/// constructed and its autorun never registers dependencies. Assigning each +/// Effect inside the generated constructor body is the plain-class analogue of +/// [emitInitState] for State classes: the Effects activate at construction +/// time, so a `Counter()` instantiation is enough to start the autoruns. /// /// Caller is responsible for only invoking this when -/// [effectNamesInDeclarationOrder] is non-empty — otherwise an empty -/// constructor is emitted, which is just noise relative to Dart's implicit -/// default constructor for a Signal-only class (see `rewritePlainClass`). +/// [effectsInDeclarationOrder] is non-empty — otherwise an empty constructor is +/// emitted, which is just noise relative to Dart's implicit default constructor +/// for a Signal-only class (see `rewritePlainClass`). String emitConstructor( String className, - List effectNamesInDeclarationOrder, + List effectsInDeclarationOrder, ) { final buffer = StringBuffer()..writeln(' $className() {'); - for (final name in effectNamesInDeclarationOrder) { - buffer.writeln(' $name;'); + for (final e in effectsInDeclarationOrder) { + buffer.writeln(' ${emitEffectInit(e)}'); } buffer.write(' }'); return buffer.toString(); } -/// Splices Effect-materialization reads (`;`, in declaration -/// order) into the END of a user-declared plain-class constructor body, -/// after any user statements. The user's body is preserved verbatim — only -/// the trailing `}` is shifted to make room for the materialization lines. +/// Splices Effect-materialization assignments (` = Effect(...);`, in +/// declaration order) into the END of a user-declared plain-class constructor +/// body, after any user statements. The user's body is preserved verbatim — +/// only the trailing `}` is shifted to make room for the materialization lines. /// /// Empty bodies (`;` / `{}`) are normalized to `{}` with the /// materialization lines as the only contents. Initializer lists, factory @@ -626,7 +638,7 @@ String emitConstructor( /// depth guard rather than an expected user path. String mergeConstructor( ConstructorDeclaration ctor, - List effectNamesInDeclarationOrder, + List effectsInDeclarationOrder, String source, String className, ) { @@ -642,8 +654,8 @@ String mergeConstructor( // is stripped, not appearances elsewhere in the header (initializer // list literals etc.). header = header.replaceFirst(RegExp(r'^\s*const\s+'), ''); - final reads = effectNamesInDeclarationOrder - .map((name) => ' $name;') + final reads = effectsInDeclarationOrder + .map((e) => ' ${emitEffectInit(e)}') .join('\n'); if (body is EmptyFunctionBody) { // `Foo();` → `Foo() { ... }`. diff --git a/packages/solid_generator/lib/src/state_class_rewriter.dart b/packages/solid_generator/lib/src/state_class_rewriter.dart index 6babe6f..45b93d2 100644 --- a/packages/solid_generator/lib/src/state_class_rewriter.dart +++ b/packages/solid_generator/lib/src/state_class_rewriter.dart @@ -107,7 +107,7 @@ RewriteResult rewriteStateClass( // names, and Query method names interleave in source-declaration order — // the contract `emitDispose` relies on for reverse-disposal correctness. final disposeNames = []; - final effectNames = []; + final effects = []; final pieces = []; // Set during the `build` branch when the rewriter emitted at least one // `SignalBuilder` wrap — drives the `flutter_solidart` import even when @@ -115,7 +115,7 @@ RewriteResult rewriteStateClass( // reading through to a sibling class's reactive state). var buildHasSignalBuilder = false; // `initState`/`dispose` emission is deferred until after the walk so the - // merge sees the fully-populated `effectNames` / `disposeNames` lists. + // merge sees the fully-populated `effects` / `disposeNames` lists. // The slot index reserves the member's source-order position in `pieces` // so it round-trips byte-identical when the user declared one. MethodDeclaration? initStateMethod; @@ -137,7 +137,7 @@ RewriteResult rewriteStateClass( // Env fields lower to `late final … = context.read();` in source- // declaration order. They are NOT added to `disposeNames` (the host // never owns disposal of injected instances) and NOT added to - // `effectNames` (env fields are lazy and need no initState + // `effects` (env fields are lazy and need no initState // materialization). pieces.add(emitEnvironmentField(env)); continue; @@ -184,10 +184,10 @@ RewriteResult rewriteStateClass( final effect = effectByName[name]!; pieces.add(emitEffectField(effect)); disposeNames.add(effect.methodName); - effectNames.add(effect.methodName); + effects.add(effect); } else if (queryByName.containsKey(name)) { // Queries are lazy — joining `disposeNames` only, never - // `effectNames` / `initState` materialization. The first reactive + // `effects` / `initState` materialization. The first reactive // call site triggers the late-final initializer. final query = queryByName[name]!; emitQueryFields( @@ -226,11 +226,11 @@ RewriteResult rewriteStateClass( // one iff at least one Effect needs materialization — otherwise skip, so // Signal-only goldens round-trip byte-identical. if (initStateMethod != null) { - pieces[initStateSlot] = effectNames.isEmpty + pieces[initStateSlot] = effects.isEmpty ? source.substring(initStateMethod.offset, initStateMethod.end) - : mergeInitState(initStateMethod, effectNames, source, className); - } else if (effectNames.isNotEmpty) { - pieces.add(emitInitState(effectNames)); + : mergeInitState(initStateMethod, effects, source, className); + } else if (effects.isNotEmpty) { + pieces.add(emitInitState(effects)); } if (disposeMethod != null) { pieces[disposeSlot] = mergeDispose( @@ -277,7 +277,7 @@ RewriteResult rewriteStateClass( if (hasListSignalField) 'ListSignal', if (hasSetSignalField) 'SetSignal', if (hasMapSignalField) 'MapSignal', - if (effectNames.isNotEmpty) 'Effect', + if (effects.isNotEmpty) 'Effect', // Queries emit `Resource(...)` fields; their `().when(...)` // call sites in `build` are wrapped in `SignalBuilder` by // `rewriteBuildMethod` when at least one query is present. diff --git a/packages/solid_generator/lib/src/stateless_rewriter.dart b/packages/solid_generator/lib/src/stateless_rewriter.dart index 7d7d64b..ae5cf5c 100644 --- a/packages/solid_generator/lib/src/stateless_rewriter.dart +++ b/packages/solid_generator/lib/src/stateless_rewriter.dart @@ -152,7 +152,7 @@ RewriteResult rewriteStatelessWidget( ); final initStateText = _emitInitStateText( userInitState: members.userInitState, - effectNamesInDeclarationOrder: reactiveBlock.effectNamesInDeclarationOrder, + effectsInDeclarationOrder: reactiveBlock.effectsInDeclarationOrder, source: source, className: className, ); @@ -192,7 +192,7 @@ RewriteResult rewriteStatelessWidget( reactiveFieldsText: reactiveBlock.fieldsText, disposeNamesInDeclarationOrder: reactiveBlock.disposeNamesInDeclarationOrder, - effectNamesInDeclarationOrder: reactiveBlock.effectNamesInDeclarationOrder, + effectsInDeclarationOrder: reactiveBlock.effectsInDeclarationOrder, stateFieldsText: partition.stateFieldsText, buildMethodText: buildMethodText, initStateText: initStateText, @@ -268,7 +268,7 @@ RewriteResult rewriteStatelessWidget( /// before it, so the emitted `late final` lines must appear after the /// declarations they read in the rewritten State class. /// -/// `effectNamesInDeclarationOrder` is the Effect-only subset of +/// `effectsInDeclarationOrder` is the Effect-only subset of /// `disposeNamesInDeclarationOrder`, pulled out so the rewriter can /// synthesize `initState()` that materializes each `late final` Effect field /// at mount time. Queries are intentionally NOT in this list — Resources are @@ -278,11 +278,11 @@ RewriteResult rewriteStatelessWidget( /// `@SolidEnvironment` env fields are emitted in source-declaration order /// alongside Signal/Computed/Effect/Resource fields but are NEVER added to /// `disposeNames` (env fields are not host-disposed) and NEVER added to -/// `effectNames` (env fields are lazy and need no initState materialization). +/// `effects` (env fields are lazy and need no initState materialization). ({ String fieldsText, List disposeNamesInDeclarationOrder, - List effectNamesInDeclarationOrder, + List effectsInDeclarationOrder, }) _emitReactiveBlock( ClassDeclaration classDecl, @@ -310,7 +310,7 @@ _emitReactiveBlock( : {for (final q in solidQueries) q.methodName: q.innerTypeText}; final lines = []; final disposeNames = []; - final effectNames = []; + final effects = []; for (final member in classDecl.members) { if (member is FieldDeclaration) { @@ -323,7 +323,7 @@ _emitReactiveBlock( } final env = envByName[name]; if (env != null) { - // No disposeNames / effectNames push — env fields are not host- + // No disposeNames / effects push — env fields are not host- // disposed and not initState-materialized. lines.add(emitEnvironmentField(env)); } @@ -342,7 +342,7 @@ _emitReactiveBlock( if (e != null) { lines.add(emitEffectField(e)); disposeNames.add(e.methodName); - effectNames.add(e.methodName); + effects.add(e); continue; } final q = queryByName[name]; @@ -364,7 +364,7 @@ _emitReactiveBlock( return ( fieldsText: lines.join('\n'), disposeNamesInDeclarationOrder: disposeNames, - effectNamesInDeclarationOrder: effectNames, + effectsInDeclarationOrder: effects, ); } @@ -687,23 +687,23 @@ String _appendSuperDispose(String methodText) { /// and [emitInitState] in the reactive/synthesized branches. String _emitInitStateText({ required MethodDeclaration? userInitState, - required List effectNamesInDeclarationOrder, + required List effectsInDeclarationOrder, required String source, required String className, }) { if (userInitState != null) { - final body = effectNamesInDeclarationOrder.isEmpty + final body = effectsInDeclarationOrder.isEmpty ? source.substring(userInitState.offset, userInitState.end) : mergeInitState( userInitState, - effectNamesInDeclarationOrder, + effectsInDeclarationOrder, source, className, ); return ' ${_prependSuperInitState(body, userInitState)}'; } - if (effectNamesInDeclarationOrder.isEmpty) return ''; - return emitInitState(effectNamesInDeclarationOrder); + if (effectsInDeclarationOrder.isEmpty) return ''; + return emitInitState(effectsInDeclarationOrder); } /// Returns [methodText] with `super.initState();` ensured as the first @@ -762,7 +762,7 @@ String _emitStateClass({ required String stateClassName, required String reactiveFieldsText, required List disposeNamesInDeclarationOrder, - required List effectNamesInDeclarationOrder, + required List effectsInDeclarationOrder, required String stateFieldsText, required String buildMethodText, required String initStateText, diff --git a/packages/solid_generator/lib/src/value_rewriter.dart b/packages/solid_generator/lib/src/value_rewriter.dart index a7448d7..c41edab 100644 --- a/packages/solid_generator/lib/src/value_rewriter.dart +++ b/packages/solid_generator/lib/src/value_rewriter.dart @@ -582,6 +582,17 @@ class _ValueRewriteVisitor extends RecursiveAstVisitor { super.visitVariableDeclaration(node); } + @override + void visitComment(Comment node) { + // Do NOT descend into documentation comments. A doc-comment reference + // (`/// … [count] …`) is a `CommentReference` whose identifier would + // otherwise be visited like any code read and get a `.value` append — + // producing `[count.value]`, an invalid reference that trips + // `comment_references`. Comment references never need the reactive + // rewrite (they resolve as declarations, not runtime reads) and must + // not count as tracked reads, so the whole subtree is skipped. + } + @override void visitMethodInvocation(MethodInvocation node) { // `untracked(() => ...)` passes through verbatim and resolves to diff --git a/packages/solid_generator/pubspec.yaml b/packages/solid_generator/pubspec.yaml index e6ae834..54c1da7 100644 --- a/packages/solid_generator/pubspec.yaml +++ b/packages/solid_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: solid_generator description: Solid source-to-lib code generator for Flutter reactive state. -version: 3.0.0-dev.8 +version: 3.0.0-dev.9 homepage: https://solid.mariuti.com repository: https://github.com/nank1ro/solid issue_tracker: https://github.com/nank1ro/solid/issues diff --git a/packages/solid_generator/test/golden/inputs/doc_comment_reference_preserved.dart b/packages/solid_generator/test/golden/inputs/doc_comment_reference_preserved.dart new file mode 100644 index 0000000..0cd7fd0 --- /dev/null +++ b/packages/solid_generator/test/golden/inputs/doc_comment_reference_preserved.dart @@ -0,0 +1,14 @@ +import 'package:solid_annotations/solid_annotations.dart'; + +// A doc-comment reference to a reactive field (`[count]`) must survive the +// `.value` rewrite untouched — the value rewriter appends `.value` to runtime +// reads of `count`, but a `[count]` in a documentation comment is a resolved +// reference, not a read, and rewriting it to `[count.value]` breaks it +// (`comment_references`). The getter BODY still gets the append. +class Counter { + @SolidState() + int count = 0; + + /// Twice the [count]. Reads [count] then doubles it. + int get doubled => count * 2; +} diff --git a/packages/solid_generator/test/golden/inputs/two_effects_ordering.dart b/packages/solid_generator/test/golden/inputs/two_effects_ordering.dart new file mode 100644 index 0000000..cea57e0 --- /dev/null +++ b/packages/solid_generator/test/golden/inputs/two_effects_ordering.dart @@ -0,0 +1,21 @@ +import 'package:solid_annotations/solid_annotations.dart'; + +// Two `@SolidEffect` methods on one class: locks in that both the field +// declarations and the materialization assignments are emitted in +// source-declaration order (`first` before `second`), so a future change to +// the effect walk order is caught. Plain class → the synthesized constructor +// carries both assignments. +class Counter { + @SolidState() + int count = 0; + + @SolidEffect() + void first() { + print('first: $count'); + } + + @SolidEffect() + void second() { + print('second: $count'); + } +} diff --git a/packages/solid_generator/test/golden/outputs/collection_mixin_breadth.g.dart b/packages/solid_generator/test/golden/outputs/collection_mixin_breadth.g.dart index 8a4229a..75a9470 100644 --- a/packages/solid_generator/test/golden/outputs/collection_mixin_breadth.g.dart +++ b/packages/solid_generator/test/golden/outputs/collection_mixin_breadth.g.dart @@ -22,24 +22,24 @@ class _BreadthState extends State { ); late final hasOne = Computed(() => tags.contains(1), name: 'hasOne'); late final keys = Computed>(() => counts.keys, name: 'keys'); - late final log = Effect(() { - print( - 'len=${xs.length} ' - 'first=${xs.first} ' - 'idx0=${xs[0]} ' - 'has-one=${tags.contains(1)} ' - 'keys=${counts.keys} ' - 'a=${counts['a']} ' - 'has-a=${counts.containsKey('a')} ' - 'indexOf=${xs.indexOf(0)} ' - 'indexWhere=${xs.indexWhere((i) => i > 5)}', - ); - }, name: 'log'); + late final Effect log; @override void initState() { super.initState(); - log; + log = Effect(() { + print( + 'len=${xs.length} ' + 'first=${xs.first} ' + 'idx0=${xs[0]} ' + 'has-one=${tags.contains(1)} ' + 'keys=${counts.keys} ' + 'a=${counts['a']} ' + 'has-a=${counts.containsKey('a')} ' + 'indexOf=${xs.indexOf(0)} ' + 'indexWhere=${xs.indexWhere((i) => i > 5)}', + ); + }, name: 'log'); } @override diff --git a/packages/solid_generator/test/golden/outputs/computed_reading_same_class_collection.g.dart b/packages/solid_generator/test/golden/outputs/computed_reading_same_class_collection.g.dart index b5185ea..fcde09e 100644 --- a/packages/solid_generator/test/golden/outputs/computed_reading_same_class_collection.g.dart +++ b/packages/solid_generator/test/golden/outputs/computed_reading_same_class_collection.g.dart @@ -3,7 +3,9 @@ import 'package:solid_annotations/solid_annotations.dart'; class Inventory implements Disposable { Inventory() { - log; + log = Effect(() { + print('count=${items.length}, first=${items[0]}'); + }, name: 'log'); } final items = ListSignal([], name: 'items'); @@ -15,9 +17,7 @@ class Inventory implements Disposable { late final count = Computed(() => items.length, name: 'count'); - late final log = Effect(() { - print('count=${items.length}, first=${items[0]}'); - }, name: 'log'); + late final Effect log; @override void dispose() { diff --git a/packages/solid_generator/test/golden/outputs/doc_comment_reference_preserved.g.dart b/packages/solid_generator/test/golden/outputs/doc_comment_reference_preserved.g.dart new file mode 100644 index 0000000..de8dcd8 --- /dev/null +++ b/packages/solid_generator/test/golden/outputs/doc_comment_reference_preserved.g.dart @@ -0,0 +1,14 @@ +import 'package:flutter_solidart/flutter_solidart.dart'; +import 'package:solid_annotations/solid_annotations.dart'; + +class Counter implements Disposable { + final count = Signal(0, name: 'count'); + + /// Twice the [count]. Reads [count] then doubles it. + int get doubled => count.value * 2; + + @override + void dispose() { + count.dispose(); + } +} diff --git a/packages/solid_generator/test/golden/outputs/effect_block_body_shadowing.g.dart b/packages/solid_generator/test/golden/outputs/effect_block_body_shadowing.g.dart index a10d665..a0c6aea 100644 --- a/packages/solid_generator/test/golden/outputs/effect_block_body_shadowing.g.dart +++ b/packages/solid_generator/test/golden/outputs/effect_block_body_shadowing.g.dart @@ -10,18 +10,18 @@ class EffectShadowing extends StatefulWidget { class _EffectShadowingState extends State { final counter = Signal(0, name: 'counter'); - late final logCounter = Effect(() { - print('outer: ${counter.value}'); - { - const counter = 'shadowed'; - print('inner: $counter'); - } - }, name: 'logCounter'); + late final Effect logCounter; @override void initState() { super.initState(); - logCounter; + logCounter = Effect(() { + print('outer: ${counter.value}'); + { + const counter = 'shadowed'; + print('inner: $counter'); + } + }, name: 'logCounter'); } @override diff --git a/packages/solid_generator/test/golden/outputs/effect_on_plain_class.g.dart b/packages/solid_generator/test/golden/outputs/effect_on_plain_class.g.dart index 689a812..d00a25e 100644 --- a/packages/solid_generator/test/golden/outputs/effect_on_plain_class.g.dart +++ b/packages/solid_generator/test/golden/outputs/effect_on_plain_class.g.dart @@ -3,14 +3,14 @@ import 'package:solid_annotations/solid_annotations.dart'; class Counter implements Disposable { Counter() { - log; + log = Effect(() { + print('value: ${value.value}'); + }, name: 'log'); } final value = Signal(0, name: 'value'); - late final log = Effect(() { - print('value: ${value.value}'); - }, name: 'log'); + late final Effect log; @override void dispose() { diff --git a/packages/solid_generator/test/golden/outputs/effect_on_state_class.g.dart b/packages/solid_generator/test/golden/outputs/effect_on_state_class.g.dart index e675294..31e6eff 100644 --- a/packages/solid_generator/test/golden/outputs/effect_on_state_class.g.dart +++ b/packages/solid_generator/test/golden/outputs/effect_on_state_class.g.dart @@ -16,14 +16,14 @@ class _CounterState extends State { const Duration(seconds: 1), ).listen((_) {}); - late final logCounter = Effect(() { - print('Counter: ${counter.value}'); - }, name: 'logCounter'); + late final Effect logCounter; @override void initState() { super.initState(); - logCounter; + logCounter = Effect(() { + print('Counter: ${counter.value}'); + }, name: 'logCounter'); debugPrint('init'); } diff --git a/packages/solid_generator/test/golden/outputs/effect_reads_widget_ctor_field.g.dart b/packages/solid_generator/test/golden/outputs/effect_reads_widget_ctor_field.g.dart index f33c0ce..2311983 100644 --- a/packages/solid_generator/test/golden/outputs/effect_reads_widget_ctor_field.g.dart +++ b/packages/solid_generator/test/golden/outputs/effect_reads_widget_ctor_field.g.dart @@ -12,14 +12,14 @@ class TempLogger extends StatefulWidget { class _TempLoggerState extends State { final celsius = Signal(0, name: 'celsius'); - late final logTemp = Effect(() { - debugPrint('${widget.label} is at ${celsius.value}'); - }, name: 'logTemp'); + late final Effect logTemp; @override void initState() { super.initState(); - logTemp; + logTemp = Effect(() { + debugPrint('${widget.label} is at ${celsius.value}'); + }, name: 'logTemp'); } @override diff --git a/packages/solid_generator/test/golden/outputs/effect_with_signal_and_computed.g.dart b/packages/solid_generator/test/golden/outputs/effect_with_signal_and_computed.g.dart index bde75a5..49e2382 100644 --- a/packages/solid_generator/test/golden/outputs/effect_with_signal_and_computed.g.dart +++ b/packages/solid_generator/test/golden/outputs/effect_with_signal_and_computed.g.dart @@ -14,14 +14,14 @@ class _EffectWithDepsState extends State { () => counter.value * 2, name: 'doubleCounter', ); - late final logBoth = Effect(() { - print('${counter.value} / ${doubleCounter.value}'); - }, name: 'logBoth'); + late final Effect logBoth; @override void initState() { super.initState(); - logBoth; + logBoth = Effect(() { + print('${counter.value} / ${doubleCounter.value}'); + }, name: 'logBoth'); } @override diff --git a/packages/solid_generator/test/golden/outputs/plain_class_user_ctor_with_effect.g.dart b/packages/solid_generator/test/golden/outputs/plain_class_user_ctor_with_effect.g.dart index 9735e15..177dee4 100644 --- a/packages/solid_generator/test/golden/outputs/plain_class_user_ctor_with_effect.g.dart +++ b/packages/solid_generator/test/golden/outputs/plain_class_user_ctor_with_effect.g.dart @@ -5,14 +5,14 @@ class Counter implements Disposable { Counter({int init = 0}) { value.value = init; - log; + log = Effect(() { + print('value: ${value.value}'); + }, name: 'log'); } late final value = Signal.lazy(name: 'value'); - late final log = Effect(() { - print('value: ${value.value}'); - }, name: 'log'); + late final Effect log; @override void dispose() { diff --git a/packages/solid_generator/test/golden/outputs/query_on_plain_class.g.dart b/packages/solid_generator/test/golden/outputs/query_on_plain_class.g.dart index df49978..4d9064e 100644 --- a/packages/solid_generator/test/golden/outputs/query_on_plain_class.g.dart +++ b/packages/solid_generator/test/golden/outputs/query_on_plain_class.g.dart @@ -3,14 +3,14 @@ import 'package:solid_annotations/solid_annotations.dart'; class Counter implements Disposable { Counter() { - log; + log = Effect(() { + print('value: ${value.value}'); + }, name: 'log'); } final value = Signal(0, name: 'value'); - late final log = Effect(() { - print('value: ${value.value}'); - }, name: 'log'); + late final Effect log; late final fetchSnapshot = Resource( () async => 0, diff --git a/packages/solid_generator/test/golden/outputs/query_with_signal_computed_effect.g.dart b/packages/solid_generator/test/golden/outputs/query_with_signal_computed_effect.g.dart index fa9d620..e44ab6f 100644 --- a/packages/solid_generator/test/golden/outputs/query_with_signal_computed_effect.g.dart +++ b/packages/solid_generator/test/golden/outputs/query_with_signal_computed_effect.g.dart @@ -14,9 +14,7 @@ class _DashboardState extends State { () => counter.value * 2, name: 'doubleCounter', ); - late final logBoth = Effect(() { - print('${counter.value} / ${doubleCounter.value}'); - }, name: 'logBoth'); + late final Effect logBoth; late final fetchSnapshot = Resource( () async => 0, name: 'fetchSnapshot', @@ -25,7 +23,9 @@ class _DashboardState extends State { @override void initState() { super.initState(); - logBoth; + logBoth = Effect(() { + print('${counter.value} / ${doubleCounter.value}'); + }, name: 'logBoth'); } @override diff --git a/packages/solid_generator/test/golden/outputs/simple_effect_with_deps.g.dart b/packages/solid_generator/test/golden/outputs/simple_effect_with_deps.g.dart index f2ef10c..be8ed3e 100644 --- a/packages/solid_generator/test/golden/outputs/simple_effect_with_deps.g.dart +++ b/packages/solid_generator/test/golden/outputs/simple_effect_with_deps.g.dart @@ -10,14 +10,14 @@ class Counter extends StatefulWidget { class _CounterState extends State { final counter = Signal(0, name: 'counter'); - late final logCounter = Effect(() { - print('Counter changed: ${counter.value}'); - }, name: 'logCounter'); + late final Effect logCounter; @override void initState() { super.initState(); - logCounter; + logCounter = Effect(() { + print('Counter changed: ${counter.value}'); + }, name: 'logCounter'); } @override diff --git a/packages/solid_generator/test/golden/outputs/stateless_lift_with_user_init_state.g.dart b/packages/solid_generator/test/golden/outputs/stateless_lift_with_user_init_state.g.dart index d861f32..cc0040b 100644 --- a/packages/solid_generator/test/golden/outputs/stateless_lift_with_user_init_state.g.dart +++ b/packages/solid_generator/test/golden/outputs/stateless_lift_with_user_init_state.g.dart @@ -10,14 +10,14 @@ class Boot extends StatefulWidget { class _BootState extends State { final counter = Signal(0, name: 'counter'); - late final logCounter = Effect(() { - print('Counter: ${counter.value}'); - }, name: 'logCounter'); + late final Effect logCounter; @override void initState() { super.initState(); - logCounter; + logCounter = Effect(() { + print('Counter: ${counter.value}'); + }, name: 'logCounter'); debugPrint('init'); } diff --git a/packages/solid_generator/test/golden/outputs/two_effects_ordering.g.dart b/packages/solid_generator/test/golden/outputs/two_effects_ordering.g.dart new file mode 100644 index 0000000..70d8daf --- /dev/null +++ b/packages/solid_generator/test/golden/outputs/two_effects_ordering.g.dart @@ -0,0 +1,26 @@ +import 'package:flutter_solidart/flutter_solidart.dart'; +import 'package:solid_annotations/solid_annotations.dart'; + +class Counter implements Disposable { + Counter() { + first = Effect(() { + print('first: ${count.value}'); + }, name: 'first'); + second = Effect(() { + print('second: ${count.value}'); + }, name: 'second'); + } + + final count = Signal(0, name: 'count'); + + late final Effect first; + + late final Effect second; + + @override + void dispose() { + second.dispose(); + first.dispose(); + count.dispose(); + } +} diff --git a/packages/solid_generator/test/golden/outputs/untracked_function_call.g.dart b/packages/solid_generator/test/golden/outputs/untracked_function_call.g.dart index 10eeb42..10df9e1 100644 --- a/packages/solid_generator/test/golden/outputs/untracked_function_call.g.dart +++ b/packages/solid_generator/test/golden/outputs/untracked_function_call.g.dart @@ -11,15 +11,15 @@ class HistoryRecorder extends StatefulWidget { class _HistoryRecorderState extends State { final counter = Signal(0, name: 'counter'); final history = ListSignal([], name: 'history'); - late final recordHistory = Effect(() { - final c = counter.value; - untracked(() => history.value = [...history.value, c]); - }, name: 'recordHistory'); + late final Effect recordHistory; @override void initState() { super.initState(); - recordHistory; + recordHistory = Effect(() { + final c = counter.value; + untracked(() => history.value = [...history.value, c]); + }, name: 'recordHistory'); } @override diff --git a/packages/solid_generator/test/golden/outputs/untracked_in_disposable_class.g.dart b/packages/solid_generator/test/golden/outputs/untracked_in_disposable_class.g.dart index a301d0b..2428772 100644 --- a/packages/solid_generator/test/golden/outputs/untracked_in_disposable_class.g.dart +++ b/packages/solid_generator/test/golden/outputs/untracked_in_disposable_class.g.dart @@ -3,17 +3,17 @@ import 'package:solid_annotations/solid_annotations.dart' hide untracked; class HistoryController implements Disposable { HistoryController() { - record; + record = Effect(() { + final c = counter.value; + untracked(() => history.value = [...history.value, c]); + }, name: 'record'); } final counter = Signal(0, name: 'counter'); final history = ListSignal([], name: 'history'); - late final record = Effect(() { - final c = counter.value; - untracked(() => history.value = [...history.value, c]); - }, name: 'record'); + late final Effect record; @override void dispose() { diff --git a/packages/solid_generator/test/golden/outputs/untracked_merges_existing_hide.g.dart b/packages/solid_generator/test/golden/outputs/untracked_merges_existing_hide.g.dart index 6e7d0b5..6bd1073 100644 --- a/packages/solid_generator/test/golden/outputs/untracked_merges_existing_hide.g.dart +++ b/packages/solid_generator/test/golden/outputs/untracked_merges_existing_hide.g.dart @@ -4,17 +4,17 @@ import 'package:solid_annotations/solid_annotations.dart' class FilteredController implements Disposable { FilteredController() { - record; + record = Effect(() { + final c = counter.value; + untracked(() => log.value = [...log.value, c]); + }, name: 'record'); } final counter = Signal(0, name: 'counter'); final log = ListSignal([], name: 'log'); - late final record = Effect(() { - final c = counter.value; - untracked(() => log.value = [...log.value, c]); - }, name: 'record'); + late final Effect record; @override void dispose() { diff --git a/packages/solid_generator/test/integration/golden_helpers.dart b/packages/solid_generator/test/integration/golden_helpers.dart index e3a9160..59c6230 100644 --- a/packages/solid_generator/test/integration/golden_helpers.dart +++ b/packages/solid_generator/test/integration/golden_helpers.dart @@ -29,6 +29,7 @@ const List goldenNames = [ 'simple_computed_with_deps', 'block_body_computed', 'computed_read_in_build', + 'doc_comment_reference_preserved', 'dispose_order', 'text_arg_gets_value', 'onpressed_untracked', @@ -49,6 +50,7 @@ const List goldenNames = [ 'effect_block_body_shadowing', 'effect_on_state_class', 'effect_on_plain_class', + 'two_effects_ordering', 'simple_query_with_future', 'simple_query_with_stream', 'query_with_signal_computed_effect',