feat(mix_generator): generate named-only MixWidget constructors from enum variants#999
Open
leoafarias wants to merge 4 commits into
Open
feat(mix_generator): generate named-only MixWidget constructors from enum variants#999leoafarias wants to merge 4 commits into
leoafarias wants to merge 4 commits into
Conversation
…enum variants Add `@MixWidget(variants:)`, which derives a widget's constructors from an `EnumVariant` enum instead of a factory parameter. The generated wrapper has no unnamed constructor, no public `variant:` argument, and no public variant field: each named constructor pins one enum value into a private field, and `build()` applies it to the recipe result with `applyVariant` before the style reaches the target widget. Supporting runtime API in `mix`: - `variantsFromEnum` registers a `VariantStyle` for every enum value. Its builder must return a style for each value, so a `switch` expression makes variant coverage exhaustive at compile time. - `applyVariant` applies a single `NamedVariant` and throws when none is registered, so a forgotten mapping fails loudly instead of silently resolving to the base style. `applyVariants` keeps its no-op semantics. `variants:` is validated rather than silently skipped: the type must be an enum, mix in `EnumVariant`, be visible from the annotated library, have at least one reachable value, avoid colliding with generated widget type parameters, and not be combined with a `variant` factory parameter. The existing `variant` factory-parameter convention is unchanged, including its unnamed constructor and public field. Both modes are covered by tests and can coexist in one library. Consolidate the coupled `variantParamName`/`variantConstructors` model fields into a single nullable `WidgetVariantDomain`, so the "name is set iff constructors exist" invariant holds by construction, and share enum-constant enumeration between the two discovery paths. Also fix the shared widget test stub, whose `framework.dart` re-exported `key.dart` without importing it. `Widget.key` therefore resolved to InvalidType and every `super.key` chained through it did too, which blocked direct-target fixtures built on the shared stub.
Apply `dart format` to three files flagged by `format:check`, and make the `MixWidget.variants` test fixture enum public so its constants are not reported as unused private fields.
Address findings from an independent review of the `variants:` feature. Model the variant domain as a sealed hierarchy instead of one class whose mode was inferred from `declaredTypeCode != null`. `ForwardedVariantDomain` and `OwnedVariantDomain` state their shape directly, so the builder switches on type rather than re-deriving a boolean at four call sites, and an inconsistent combination is no longer constructible. Reject a styler that does not declare `applyVariant`. It comes from `VariantStyleMixin`, not from `Style<S>`, so a hand-rolled styler previously satisfied `@MixWidget` and then failed to compile inside the generated part instead of reporting against the annotation. Compare domains rather than parameter names when detecting a conflict with the `variant` factory-parameter convention. A parameter merely named `variant` that is positional, nullable, or non-enum never backs a domain, so it was being rejected with an inaccurate message; it now forwards normally alongside the private variant field. Reject a parameter named `_variant`, which would declare the private field a second time. Dart only forbids privately named parameters when they are named, so a positional one reached the generated part. Cover the paths that previously had only builder-level unit tests: variable-backed factories, the styler `call()` path without `target:`, and each new validation.
…ection
Address an adversarial review of the `variants:` feature.
Move the changelog entries off already-published versions. `mix` and
`mix_annotations` 2.2.0-beta.1 and `mix_generator` 2.2.0-beta.2 are on
pub.dev and cannot be republished, so their artifacts would never contain
these APIs. Each entry moves to the next unpublished version and its pubspec
is bumped to match. Raise `mix_generator`'s `mix_annotations` lower bound to
the release declaring `MixWidget.variants`, and document that generated code
needs `mix` 2.2.0-beta.2 for `applyVariant`.
Detect a conflicting `variant` factory parameter before curation.
`factoryParameters: .only({})` hid the parameter from the curated list, so
the conflict went unreported: the recipe resolved its own default variant and
the generated widget applied a second one on top of it. Detection now reads
the function's declared parameters, while the forwarded domain still requires
the parameter to survive curation.
Match `applyVariant` on `Variant.key` rather than `==`. Dart forbids enums
from overriding `==`, so `NamedVariant('solid') == MyVariant.solid` was true
while the reverse was false, making a strict lookup depend on operand order.
Qualify the exhaustiveness claim: `variantsFromEnum` covers the whole enum
only when passed `E.values`, and a subset leaves the rest to `applyVariant`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #998.
Summary
Adds
@MixWidget(variants:), which derives a widget's constructors from an enum-backedNamedVariantdomain instead of a factory parameter. The generated wrapper has no unnamed constructor, no publicvariant:argument, and no public variant field.Generated output (verified against the real packages, not stubs):
The value is stored privately because Dart forbids privately named parameters — that is precisely what makes the widget selectable only through its named constructors.
Design note: explicit annotation instead of a naming convention
The issue proposed discovering the enum by convention (same library, named
<WidgetName>Variant). I implemented an explicitvariants:field instead, because the convention is not actually opt-in: any existing user who happens to haveenum CardVariant with EnumVariantbesidecardStyle()would silently loseCard(...)on upgrade — a breaking public-API change with no source change on their side. The convention also forces the enum into the same library as the recipe, which fights the usual separatevariants.dartsplit.The explicit field costs one annotation parameter and makes "unchanged when it does not apply" true by construction rather than by luck.
Runtime API in
mixvariantsFromEnum(values, builder)— registers aVariantStyleper enum value. The builder must return a style for every value, so aswitchexpression makes coverage exhaustive at compile time.applyVariant(NamedVariant)— applies one variant and throws when none is registered.applyVariants(plural) keeps its silent no-op semantics.The throw is the load-bearing half of "missing variant-style mappings produce a clear failure" — previously unreachable, since
applyVariantsreturns the base style unchanged when nothing matches. Paired withvariantsFromEnum, "unregistered" reliably means "genuinely forgotten". A deliberately unstyled variant registers an empty styler.Validation
variants:is validated rather than silently skipped. The type must be an enum, mix inEnumVariant, be visible from the annotated library, have at least one reachable value, avoid colliding with generated widget type parameters, and not be combined with avariantfactory parameter.This differs deliberately from the legacy convention, which degrades silently on a type-parameter collision to stay non-breaking. Here, degrading would emit an unnamed-constructor widget — the opposite of what was asked for — so it fails.
Backwards compatibility
The existing
variantfactory-parameter convention is untouched: unnamed constructor, public field, forwarding to the recipe. Both modes are tested and coexist in one library. A byte-identical-output regression test guards the no-variant path.Consolidation
variantParamName+variantConstructorsmodel fields into one nullableWidgetVariantDomain, so the "name is set iff constructors exist" invariant holds by construction. Net field count unchanged; the two modes differ by exactly one documented bit._variantConstructorsFor(two real consumers), while callers decide what a collision means.Verification
mix/ 354mix_generator/ 14mix_annotationstests pass;packages/mixanalyzes clean.build_runner: generated output matched the target exactly,flutter analyzewas clean, and widget tests confirmed.solid/.softrender different variants and that a missing mapping throwsStateErrorat build time.Incidental fix
The shared widget test stub's
framework.dartre-exportedkey.dartwithout importing it, soWidget.keyresolved toInvalidTypeand everysuper.keychained through it did too. Existing tests missed this because their stylers declaredKey? keydirectly. It blocked all direct-target fixtures built on the shared stub; the fix is one import.Note on branch name
The branch is
fix/998as requested, though the change is additive and all three changelog entries are taggedFEAT—feat/998would match the repo convention. Flagging in case the branch name matters to release tooling.