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: 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.6

- **FEAT**: A widget `build()` can read another class's `@SolidQuery` method cross-instance (e.g. `viewModel.customers().isLoading`) and now gets the `SignalBuilder` wrap and `flutter_solidart` import it needs, while the `query()` call sites and the `query.refresh` tear-off stay byte-identical (no `.value` rewrite). Previously only the class declaring a query could consume it, even though cross-instance `@SolidState` reads already worked. The cross-file query-name registry is origin-qualified exactly like the `@SolidState` registry (#110 parity): an ambiguous simple name resolves only on a receiver's resolved-library match, so a same-named non-query method is never spuriously tracked.

## 3.0.0-dev.5

- **FIX**: Bare `super.x` constructor parameters now seed the cross-file registry: the superclass is located syntactically (alias-aware, same-package imports only), the matched constructor parameter's type is resolved (recursing through super-formal chains, mapping generic type parameters to the extends-clause type arguments), and the registry walk also searches the located superclass file's own imports one hop further (#108).
Expand Down
149 changes: 146 additions & 3 deletions packages/solid_generator/lib/builder.dart

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions packages/solid_generator/lib/src/build_rewriter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ typedef BuildMethodRewrite = ({String text, bool emittedWrap});
/// as tracked reads for SignalBuilder placement without mutating the call
/// expression itself.
///
/// [classQueryNames] is the cross-class `@SolidQuery` name map (class name →
/// `@SolidQuery` method names) — the query counterpart of [classRegistry].
/// A zero-arg `<receiver>.<queryName>()` call whose receiver's declared type
/// names a class in this map is recorded as a tracked read the same way,
/// with no call-site edit. Empty map → no-op for the cross-instance query
/// branch. [classQueryNamesOrigins] / [classQueryNamesShadowedNames] are its
/// origin-qualified counterparts (issue #110), mirroring
/// [classRegistryOrigins] / [classRegistryShadowedNames].
///
/// [classRegistry] is the cross-class reactivity map (class name → reactive
/// field/getter names). Threaded through to the value-rewrite visitor so the
/// single-level `<param>.<reactiveField>` cross-class rewrite fires. Empty
Expand Down Expand Up @@ -111,6 +120,9 @@ BuildMethodRewrite rewriteBuildMethod(
Map<String, Map<String, Set<String>>> classRegistryOrigins = const {},
Map<String, Map<String, Set<String>>> classCollectionFieldsOrigins = const {},
Set<String> classRegistryShadowedNames = const {},
Map<String, Set<String>> classQueryNames = const {},
Map<String, Map<String, Set<String>>> classQueryNamesOrigins = const {},
Set<String> classQueryNamesShadowedNames = const {},
}) {
final methodStart = buildMethod.offset;
final methodEnd = buildMethod.end;
Expand All @@ -129,6 +141,9 @@ BuildMethodRewrite rewriteBuildMethod(
classRegistryOrigins: classRegistryOrigins,
classCollectionFieldsOrigins: classCollectionFieldsOrigins,
classRegistryShadowedNames: classRegistryShadowedNames,
classQueryNames: classQueryNames,
classQueryNamesOrigins: classQueryNamesOrigins,
classQueryNamesShadowedNames: classQueryNamesShadowedNames,
);
final wrapPlan = computeWrapPlan(
buildMethod,
Expand Down
37 changes: 35 additions & 2 deletions packages/solid_generator/lib/src/cross_file_consumer_rewriter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ PureConsumerLowering lowerPureConsumers(
Map<String, Map<String, Set<String>>> classRegistryOrigins = const {},
Map<String, Map<String, Set<String>>> classCollectionFieldsOrigins = const {},
Set<String> classRegistryShadowedNames = const {},
Map<String, Set<String>> classQueryNames = const {},
Map<String, Map<String, Set<String>>> classQueryNamesOrigins = const {},
Set<String> classQueryNamesShadowedNames = const {},
}) {
final widgetResult = collectPureConsumerWidgetEdits(
unit,
Expand All @@ -69,6 +72,9 @@ PureConsumerLowering lowerPureConsumers(
classRegistryOrigins: classRegistryOrigins,
classCollectionFieldsOrigins: classCollectionFieldsOrigins,
classRegistryShadowedNames: classRegistryShadowedNames,
classQueryNames: classQueryNames,
classQueryNamesOrigins: classQueryNamesOrigins,
classQueryNamesShadowedNames: classQueryNamesShadowedNames,
);
final crossFileEdits = collectPureConsumerCrossFileEdits(
unit,
Expand All @@ -78,6 +84,9 @@ PureConsumerLowering lowerPureConsumers(
classRegistryOrigins: classRegistryOrigins,
classCollectionFieldsOrigins: classCollectionFieldsOrigins,
classRegistryShadowedNames: classRegistryShadowedNames,
classQueryNames: classQueryNames,
classQueryNamesOrigins: classQueryNamesOrigins,
classQueryNamesShadowedNames: classQueryNamesShadowedNames,
);
if (widgetResult.edits.isEmpty && crossFileEdits.isEmpty) {
return (text: text, emittedSignalBuilder: false);
Expand Down Expand Up @@ -138,8 +147,14 @@ List<ValueEdit> collectPureConsumerCrossFileEdits(
Map<String, Map<String, Set<String>>> classRegistryOrigins = const {},
Map<String, Map<String, Set<String>>> classCollectionFieldsOrigins = const {},
Set<String> classRegistryShadowedNames = const {},
Map<String, Set<String>> classQueryNames = const {},
Map<String, Map<String, Set<String>>> classQueryNamesOrigins = const {},
Set<String> classQueryNamesShadowedNames = const {},
}) {
if (classRegistry.isEmpty && classRegistryShadowedNames.isEmpty) {
if (classRegistry.isEmpty &&
classRegistryShadowedNames.isEmpty &&
classQueryNames.isEmpty &&
classQueryNamesShadowedNames.isEmpty) {
return const <ValueEdit>[];
}
final edits = <ValueEdit>[];
Expand All @@ -161,6 +176,9 @@ List<ValueEdit> collectPureConsumerCrossFileEdits(
classRegistryOrigins: classRegistryOrigins,
classCollectionFieldsOrigins: classCollectionFieldsOrigins,
classRegistryShadowedNames: classRegistryShadowedNames,
classQueryNames: classQueryNames,
classQueryNamesOrigins: classQueryNamesOrigins,
classQueryNamesShadowedNames: classQueryNamesShadowedNames,
);
edits.addAll(result.edits);
}
Expand Down Expand Up @@ -216,8 +234,14 @@ collectPureConsumerWidgetEdits(
Map<String, Map<String, Set<String>>> classRegistryOrigins = const {},
Map<String, Map<String, Set<String>>> classCollectionFieldsOrigins = const {},
Set<String> classRegistryShadowedNames = const {},
Map<String, Set<String>> classQueryNames = const {},
Map<String, Map<String, Set<String>>> classQueryNamesOrigins = const {},
Set<String> classQueryNamesShadowedNames = const {},
}) {
if (classRegistry.isEmpty && classRegistryShadowedNames.isEmpty) {
if (classRegistry.isEmpty &&
classRegistryShadowedNames.isEmpty &&
classQueryNames.isEmpty &&
classQueryNamesShadowedNames.isEmpty) {
return (edits: const <ValueEdit>[], emittedSignalBuilder: false);
}
final edits = <ValueEdit>[];
Expand All @@ -240,6 +264,9 @@ collectPureConsumerWidgetEdits(
classRegistryOrigins: classRegistryOrigins,
classCollectionFieldsOrigins: classCollectionFieldsOrigins,
classRegistryShadowedNames: classRegistryShadowedNames,
classQueryNames: classQueryNames,
classQueryNamesOrigins: classQueryNamesOrigins,
classQueryNamesShadowedNames: classQueryNamesShadowedNames,
);
if (rewritten.emittedWrap) emittedSignalBuilder = true;
if (rewritten.text != original) {
Expand All @@ -258,6 +285,9 @@ collectPureConsumerWidgetEdits(
classRegistryOrigins: classRegistryOrigins,
classCollectionFieldsOrigins: classCollectionFieldsOrigins,
classRegistryShadowedNames: classRegistryShadowedNames,
classQueryNames: classQueryNames,
classQueryNamesOrigins: classQueryNamesOrigins,
classQueryNamesShadowedNames: classQueryNamesShadowedNames,
);
edits.addAll(result.edits);
continue;
Expand All @@ -272,6 +302,9 @@ collectPureConsumerWidgetEdits(
classRegistryOrigins: classRegistryOrigins,
classCollectionFieldsOrigins: classCollectionFieldsOrigins,
classRegistryShadowedNames: classRegistryShadowedNames,
classQueryNames: classQueryNames,
classQueryNamesOrigins: classQueryNamesOrigins,
classQueryNamesShadowedNames: classQueryNamesShadowedNames,
);
edits.addAll(result.edits);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/solid_generator/lib/src/state_class_rewriter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ RewriteResult rewriteStateClass(
Map<String, Map<String, Set<String>>> classRegistryOrigins = const {},
Map<String, Map<String, Set<String>>> classCollectionFieldsOrigins = const {},
Set<String> classRegistryShadowedNames = const {},
Map<String, Set<String>> classQueryNames = const {},
Map<String, Map<String, Set<String>>> classQueryNamesOrigins = const {},
Set<String> classQueryNamesShadowedNames = const {},
}) {
final className = classDecl.name.lexeme;
// getter→Computed only ships for `StatelessWidget`. The in-place merge
Expand Down Expand Up @@ -157,6 +160,9 @@ RewriteResult rewriteStateClass(
classRegistryOrigins: classRegistryOrigins,
classCollectionFieldsOrigins: classCollectionFieldsOrigins,
classRegistryShadowedNames: classRegistryShadowedNames,
classQueryNames: classQueryNames,
classQueryNamesOrigins: classQueryNamesOrigins,
classQueryNamesShadowedNames: classQueryNamesShadowedNames,
);
pieces.add(buildRewrite.text);
// `buildRewrite.emittedWrap` is sourced from `rewriteBuildMethod`'s
Expand Down
6 changes: 6 additions & 0 deletions packages/solid_generator/lib/src/stateless_rewriter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ RewriteResult rewriteStatelessWidget(
Map<String, Map<String, Set<String>>> classRegistryOrigins = const {},
Map<String, Map<String, Set<String>>> classCollectionFieldsOrigins = const {},
Set<String> classRegistryShadowedNames = const {},
Map<String, Set<String>> classQueryNames = const {},
Map<String, Map<String, Set<String>>> classQueryNamesOrigins = const {},
Set<String> classQueryNamesShadowedNames = const {},
}) {
final className = classDecl.name.lexeme;
final stateClassName = '_${className}State';
Expand Down Expand Up @@ -116,6 +119,9 @@ RewriteResult rewriteStatelessWidget(
classRegistryOrigins: classRegistryOrigins,
classCollectionFieldsOrigins: classCollectionFieldsOrigins,
classRegistryShadowedNames: classRegistryShadowedNames,
classQueryNames: classQueryNames,
classQueryNamesOrigins: classQueryNamesOrigins,
classQueryNamesShadowedNames: classQueryNamesShadowedNames,
);
final buildMethodText = buildRewrite.text;

Expand Down
100 changes: 100 additions & 0 deletions packages/solid_generator/lib/src/value_rewriter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,26 @@ bool _isOnPrefixedCallbackName(String name) {
/// Empty (the default) for every caller that has no
/// origin data to offer, which degrades this exactly to the pre-#110
/// name-only behavior.
///
/// [classQueryNames] is the cross-class `@SolidQuery` name map (class name →
/// `@SolidQuery` method names) — the query counterpart of [classRegistry].
/// A zero-arg cross-instance `<receiver>.<queryName>()` call whose
/// receiver's resolved declared type names a class in this map is a tracked
/// read (offset recorded, same as a same-class query call) but receives NO
/// source edit: the call already lowers to `Resource.call() => state` and
/// every trailing `.isLoading`/`.asReady`/`.asError` chain resolves through
/// upstream `flutter_solidart` extensions unchanged. Empty (the default)
/// for every caller that has not opted into cross-class query recognition —
/// currently only build-method callers.
///
/// [classQueryNamesOrigins] and [classQueryNamesShadowedNames] are the query
/// counterparts of [classRegistryOrigins] / [classRegistryShadowedNames]
/// (issue #110) — a name `builder.dart` could not resolve unambiguously by
/// simple name alone (two-plus distinct cross-file query-bearing classes
/// sharing the name, or a cross-file class whose name is ALSO declared
/// locally) routes through [classQueryNamesOrigins] with a mandatory tier-1
/// library-URI match instead of the flat [classQueryNames] — see
/// [_ValueRewriteVisitor._queryNamesForCrossClassName].
ValueRewriteResult collectValueEdits(
AstNode node,
Set<String> reactiveFields,
Expand All @@ -186,6 +206,9 @@ ValueRewriteResult collectValueEdits(
Map<String, Map<String, Set<String>>> classRegistryOrigins = const {},
Map<String, Map<String, Set<String>>> classCollectionFieldsOrigins = const {},
Set<String> classRegistryShadowedNames = const {},
Map<String, Set<String>> classQueryNames = const {},
Map<String, Map<String, Set<String>>> classQueryNamesOrigins = const {},
Set<String> classQueryNamesShadowedNames = const {},
}) {
final visitor = _ValueRewriteVisitor(
reactiveFields,
Expand All @@ -199,6 +222,9 @@ ValueRewriteResult collectValueEdits(
classRegistryOrigins,
classCollectionFieldsOrigins,
classRegistryShadowedNames,
classQueryNames,
classQueryNamesOrigins,
classQueryNamesShadowedNames,
);
node.accept(visitor);
return ValueRewriteResult(
Expand Down Expand Up @@ -365,6 +391,9 @@ class _ValueRewriteVisitor extends RecursiveAstVisitor<void> {
this._classRegistryOrigins,
this._classCollectionFieldsOrigins,
this._classRegistryShadowedNames,
this._classQueryNames,
this._classQueryNamesOrigins,
this._classQueryNamesShadowedNames,
);

final Set<String> _reactiveFields;
Expand Down Expand Up @@ -446,6 +475,25 @@ class _ValueRewriteVisitor extends RecursiveAstVisitor<void> {
/// a mandatory tier-1 library-URI match instead of the flat maps.
final Set<String> _classRegistryShadowedNames;

/// Cross-class `@SolidQuery` name map (class name → `@SolidQuery` method
/// names) — the query counterpart of [_classRegistry]. Drives
/// [_isCrossClassQueryCall]; empty map → the cross-instance query branch
/// no-ops.
final Map<String, Set<String>> _classQueryNames;

/// Per-name origin-qualified counterpart of [_classQueryNames] (issue
/// #110) — `name -> originUri -> query method names` — populated by
/// `builder.dart` ONLY for a name it flagged in
/// [_classQueryNamesShadowedNames]. Mirrors [_classRegistryOrigins].
final Map<String, Map<String, Set<String>>> _classQueryNamesOrigins;

/// Names `builder.dart` could not resolve unambiguously by simple name
/// alone for [_classQueryNames] (issue #110) — mirrors
/// [_classRegistryShadowedNames]. [_queryNamesForCrossClassName] routes
/// these names through [_classQueryNamesOrigins] with a mandatory tier-1
/// library-URI match instead of the flat [_classQueryNames].
final Set<String> _classQueryNamesShadowedNames;

final List<ValueEdit> edits = [];

/// Tracked-read offsets keyed by signal name — drives the placement
Expand Down Expand Up @@ -551,10 +599,62 @@ class _ValueRewriteVisitor extends RecursiveAstVisitor<void> {
// resolves to upstream extensions.
_recordTrackedRead(node.offset, node.methodName.name);
_recordTrackedQueryName(node.methodName.name);
} else if (_untrackedDepth == 0 && _isCrossClassQueryCall(node)) {
// Cross-instance `<receiver>.<queryName>()` — the query counterpart of
// [_maybeRewriteCrossClass]'s `<receiver>.<reactiveField>` shape. NO
// source edit: the call is byte-identical because it lowers to
// `Resource<T>.call() -> ResourceState<T>` and the trailing
// `.isLoading`/`.asReady`/`.asError` chain resolves through upstream
// `flutter_solidart` extensions unchanged. Only the offset is recorded
// so SignalBuilder placement wraps the enclosing widget subtree.
_recordTrackedRead(node.offset, node.methodName.name);
}
super.visitMethodInvocation(node);
}

/// True if [node] is a cross-instance `<receiver>.<queryName>()` call: a
/// zero-arg `MethodInvocation` with a target (never bare — that shape is
/// [_isQueryShape]'s same-class branch above), not shadowed when the
/// target is a bare identifier, whose receiver's declared type — resolved
/// the same way [_maybeRewriteCrossClass] resolves a `<receiver>.<field>`
/// prefix — names a class in [_queryNamesForCrossClassName] whose set
/// contains the called method's name.
bool _isCrossClassQueryCall(MethodInvocation node) {
final target = node.target;
if (target == null) return false;
if (node.argumentList.arguments.isNotEmpty) return false;
if (target is SimpleIdentifier && _isShadowed(target.name)) return false;
final receiverType = _resolveReceiverType(target);
final declaredTypeName =
receiverType?.name ??
(target is SimpleIdentifier ? _environmentFields[target.name] : null);
if (declaredTypeName == null) return false;
final queryNamesOfType = _queryNamesForCrossClassName(
declaredTypeName,
receiverType?.libraryUri,
);
return queryNamesOfType?.contains(node.methodName.name) ?? false;
}

/// Query-name resolver for [declaredTypeName] — the query counterpart of
/// [_fieldsForCrossClassName]. Same safety invariant (issue #110): a name
/// NOT in [_classQueryNamesShadowedNames] is served straight from the flat
/// [_classQueryNames]; a FLAGGED name only resolves when [libraryUri] is
/// non-null (tier 1 — a real resolved `staticType`) AND matches one of the
/// origins [_classQueryNamesOrigins] recorded for [declaredTypeName]. See
/// [_fieldsForCrossClassName]'s doc comment for the full invariant this
/// mirrors.
Set<String>? _queryNamesForCrossClassName(
String declaredTypeName,
String? libraryUri,
) {
if (!_classQueryNamesShadowedNames.contains(declaredTypeName)) {
return _classQueryNames[declaredTypeName];
}
if (libraryUri == null) return null;
return _classQueryNamesOrigins[declaredTypeName]?[libraryUri];
}

/// True if [node] is a zero-arg `MethodInvocation` with a bare
/// `SimpleIdentifier` target whose name is not shadowed — the structural
/// shape of every detection site that consults [_queryNames] or
Expand Down
2 changes: 1 addition & 1 deletion packages/solid_generator/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: solid_generator
description: Solid source-to-lib code generator for Flutter reactive state.
version: 3.0.0-dev.5
version: 3.0.0-dev.6
homepage: https://solid.mariuti.com
repository: https://github.com/nank1ro/solid
issue_tracker: https://github.com/nank1ro/solid/issues
Expand Down
Loading
Loading