Skip to content

feat(mix_generator): add @MixWidget annotation for Styler-driven widgets#909

Open
tilucasoli wants to merge 15 commits intomainfrom
tilucasoli/mix-widget-annotation
Open

feat(mix_generator): add @MixWidget annotation for Styler-driven widgets#909
tilucasoli wants to merge 15 commits intomainfrom
tilucasoli/mix-widget-annotation

Conversation

@tilucasoli
Copy link
Copy Markdown
Collaborator

@tilucasoli tilucasoli commented Apr 24, 2026

Related issue

None.

Description

Adds a new @MixWidget('Name', {stylable}) annotation that generates a StatelessWidget wrapper around a top-level Styler — either a variable holding a Styler, or a function that returns one. The generated widget's constructor mirrors the source's parameters merged with the Styler's hand-written call(...) signature (per-param positional/named/required preserved verbatim), and its build method invokes that call(...) with the widget's fields. When stylable: true, the wrapper also accepts an optional style parameter and merges it into the annotated Styler before invoking call(...).

Generated output examples

1. Variable form

Input:

@MixWidget('Card')
final card = BoxStyler().paddingAll(16).borderRounded(8).color(Color(0xFFFFFFFF));

Generated:

class Card extends StatelessWidget {
  final Widget? child;

  const Card({super.key, this.child});

  @override
  Widget build(BuildContext context) => card(child: child);
}

2. Function form (source params merged with call(...) params)

Input:

@MixWidget('Card1')
BoxStyler createCard(Widget child, {Color color = const Color(0xFFFFFFFF)}) =>
    BoxStyler().paddingAll(16).borderRounded(8).color(color);

Generated:

class Card1 extends StatelessWidget {
  final Widget child;
  final Color color;

  const Card1(this.child, {super.key, this.color = const Color(0xFFFFFFFF)});

  @override
  Widget build(BuildContext context) =>
      createCard(child, color: color)(child: child);
}

3. stylable: true (injects style field and merge(style))

Input:

@MixWidget('Card', stylable: true)
final card = BoxStyler().paddingAll(16);

Generated:

class Card extends StatelessWidget {
  final Widget? child;
  final BoxStyler? style;

  const Card({super.key, this.child, this.style});

  @override
  Widget build(BuildContext context) => card.merge(style)(child: child);
}

4. Positional call(...) keeps positional on the widget constructor

Input:

@MixWidget('Heading')
final heading = TextStyler().size(24);
// TextStyler.call(String text) => ...

Generated:

class Heading extends StatelessWidget {
  final String text;

  const Heading(this.text, {super.key});

  @override
  Widget build(BuildContext context) => heading(text);
}

Changes

  • New MixWidget annotation in packages/mix_annotations/lib/src/annotations.dart.
  • New MixWidgetGenerator builder in mix_generator (analyzer-facing class + pure-Dart MixWidgetParam model + MixWidgetBuilder string emitter), wired into build.yaml as a user-facing builder (auto_apply: dependents, no lib/src/specs/** scope restriction).
  • Validation: rejects annotation on non-top-level / non-Style<T> targets, Stylers without call(...), and stylable: true with a style param on either source or call signature.
  • Tests: unit tests for the model + emitter, integration smoke tests for variable / function / stylable / positional call(...) / required-nullable cases, and validation error-path tests.
  • Demo Card, CardV, CardH, H1, Badge wrappers added to box_widget.dart exercising the annotation end-to-end.
  • Design spec and implementation plan committed under docs/superpowers/.

Review Checklist

  • Testing: New unit + integration tests; full mix_generator suite passes (192/192) and melos run ci is green.
  • Breaking Changes: None — new additive annotation and builder.
  • Documentation Updates: Annotation has dartdoc; design spec lives in `docs/superpowers/specs/`. Public-site docs not updated.
  • Website Updates: Not yet.

Additional Information (optional)

`melos run analyze` reports a pre-existing collision in `mix_tailwinds` involving the demo `H1` symbol from `box_widget.dart`; the new generator code itself analyzes clean. Worth a follow-up to either rename the demo or scope it before merge.

tilucasoli and others added 11 commits April 23, 2026 10:09
Design doc for a new @MixWidget annotation and generator that turns
top-level Stylers (variable or function form) into StatelessWidget wrappers
by reusing each Styler's hand-written call() method as the widget entry
point. Includes stylable: true toggle for injectable Stylers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Task-by-task TDD plan covering the annotation addition, pure-Dart param
model + merge logic, string emitter, analyzer-facing generator, validation
cases, and build.yaml wiring for mix_widget_generator.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Card, CardV, CardH, H1, and Badge wrappers in box_widget.dart that
exercise the variable form, function form, positional `call(...)`, and
`stylable: true` paths of the new generator end-to-end against real
Mix Stylers.
@docs-page
Copy link
Copy Markdown

docs-page Bot commented Apr 24, 2026

To view this pull requests documentation preview, visit the following URL:

docs.page/btwld/mix~909

Documentation is deployed and generated using docs.page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions github-actions Bot removed the repo label Apr 24, 2026
Copy link
Copy Markdown
Collaborator Author

@tilucasoli tilucasoli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Clean separation between the pure-Dart MixWidgetParam / MixWidgetBuilder and the thin analyzer shim, with proportional unit + integration coverage. A few correctness concerns worth addressing before merge — left inline.

Not blocking, worth considering

  • Build scope in packages/mix/build.yaml: mix_generator is narrowed to lib/src/specs/** but there's no analogous entry for mix_widget_generator, so once downstream usages appear it will scan all of lib/** on every build. No correctness impact; adding a matching scope keeps incremental builds predictable.
  • Annotation target: MixWidget has no @Target({...}). You enforce top-level variable/function at generate time, but declaring @Target({TargetKind.topLevelVariable, TargetKind.function}) would also surface misuse at the call site.
  • Missing test cases: inherited call(...) from a base Styler, source function declaring a key parameter, generic Styler type parameters.

Already addressed

The demo @MixWidget usage in box_widget.dart and the barrel-import swap were reverted in bb0db2ce9 — good call, those were my biggest concerns.

'@MixWidget Styler ${styler.name} must declare a `call(...)` method.',
element: annotated,
);
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_findCall only inspects declared methods.

InterfaceElement.methods returns declared members only, not inherited ones. A user Styler that inherits call from a base class — e.g. class MyCard extends BoxStyler {} with no override — will fail validation with "must declare a call(...) method" even though invoking it works fine at runtime.

Fix: walk the superclass chain for call, or use an inherited-member lookup (styler.lookUpInheritedMethod('call', ...) / equivalent). Worth a regression test alongside.

}
InterfaceType? current = type;
while (current != null) {
if (current.element.name == 'Style') {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matching Style by plain name is too loose.

Any class named Style in any library will satisfy this check. Low-likelihood collision in practice, but trivial to tighten — compare current.element.library.uri against the mix package, or ask the type system whether the type is assignable to mix's Style<dynamic>.


MixWidgetParam _convertFormalParam(FormalParameterElement p) {
return MixWidgetParam(
name: p.name!,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: analyzer ≥7 allows null names on some synthetic/wildcard parameter paths. A bang works for typical user-authored code but consider throwing InvalidGenerationSource with a clearer message if p.name == null — otherwise a hit here surfaces as an opaque null-check error.


final callMethod = _findCall(stylerClass, element);
final callParams = _convertCallParams(callMethod);
const sourceParams = <MixWidgetParam>[];
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sourceParams is hardcoded to empty for the variable form, which makes the stylable collision guard below a call-params-only check. It's correct (a variable has no params), but a one-line comment saying why would save a future reader a grep.

tilucasoli and others added 3 commits April 24, 2026 09:57
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Walks the superclass chain in _findCall so a Styler that inherits
`call` from a base class (e.g. `class MyCard extends BoxStyler {}`)
is accepted instead of failing with "must declare a call(...) method".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously any class named `Style` in any library would satisfy the
superclass walk in `_resolveStyleClass`. Require the matched element
to live under `package:mix/` so unrelated `Style` types don't spuriously
qualify.

Tests now provide a synthetic `mix|lib/mix.dart` asset declaring
`class Style<T>` and import it via `package:mix/mix.dart`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant