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
3 changes: 3 additions & 0 deletions packages/mix_generator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
`@MixableSpec(target:)` or `@MixWidget(target:)`. Reject a target type
parameter named `Key` when it would shadow the generated Flutter key
parameter (#1023).
- **FIX**: Allow `@MixableSpec(target:)` to use plain Widget constructors whose
named `style` parameter accepts the generated Styler, without requiring the
target to extend `StyleWidget` (#1022).

## 2.2.0-beta.2

Expand Down
3 changes: 3 additions & 0 deletions packages/mix_generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ The generator emits several surfaces with deliberately different shapes, chosen

- **`@MixableSpec`** emits a *rich* mixin (`mixin _$<Name> implements Spec<T>, Diagnosticable`) that fully completes the Spec contract on its own. Specs are immutable value types with no shared concrete behavior to inherit, so the generated mixin can be self-contained — user code only writes `with _$<Name>`.
- **`@MixableSpec(target: Widget.new)`** also emits a full generated Styler class into the same `.g.dart` part file as the spec mixin. The generated class owns fields, constructors, factories, fluent methods, `call()`, merge, resolve, diagnostics, and props.
The target can be any `Widget` constructor with a named `style` parameter
that accepts the generated Styler; it does not need to extend `StyleWidget`.
A `styleSpec` parameter is supported when it is optional.
Direct, uninstantiated generic targets are supported; generated `call()` methods preserve and forward their type parameters and bounds. Instantiated targets such as `Widget<int>.new` and generic constructor tear-offs through typedefs are rejected because their substitutions are not yet supported. A target type parameter named `Key` is also rejected when the constructor forwards Flutter's `key`, because it would shadow the generated `Key? key` parameter.
- **`@MixableStyler`** emits a legacy *slim* mixin (`mixin _$<Name>Mixin on Style<S>, Diagnosticable`) that fills in per-field plumbing for handwritten styler classes.
- **`@Mixable`** emits a *slim* mixin (`mixin _$<Name>Mixin on Mix<T>[, DefaultValue<T>][, Diagnosticable]`) for the same reason — Mix subclasses commonly compose intermediate base classes (e.g., `class BoxConstraintsMix extends ConstraintsMix<BoxConstraints>`) and the user keeps that inheritance chain.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1026,8 +1026,8 @@ class SpecStylerClassBuilder {
return buildMixableSpecTargetCall(
annotation: annotation,
specElement: specElement,
specName: specName,
stylerName: stylerName,
allowExactGeneratedStyler: true,
hostElement: specElement,
hostLibrary: specElement.library,
);
Expand Down
5 changes: 0 additions & 5 deletions packages/mix_generator/lib/src/core/checkers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ const styleChecker = TypeChecker.fromUrl(
'package:mix/src/core/style.dart#Style',
);

/// `StyleWidget<S>` abstract class from `package:mix`.
const styleWidgetChecker = TypeChecker.fromUrl(
'package:mix/src/core/style_widget.dart#StyleWidget',
);

/// `StyleSpec<S>` from `package:mix`.
const styleSpecChecker = TypeChecker.fromUrl(
'package:mix/src/core/style_spec.dart#StyleSpec',
Expand Down
173 changes: 143 additions & 30 deletions packages/mix_generator/lib/src/core/helpers/widget_call_planner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ const stylerBackedTargetParams = {'style', 'styleSpec'};
String? buildMixableSpecTargetCall({
required ConstantReader annotation,
required InterfaceElement specElement,
required String specName,
required String stylerName,
required bool allowExactGeneratedStyler,
required Element hostElement,
required LibraryElement hostLibrary,
bool validateTargetVisibility = false,
Expand Down Expand Up @@ -74,7 +74,8 @@ String? buildMixableSpecTargetCall({
constructor: constructor,
widgetName: widgetName,
specElement: specElement,
specName: specName,
stylerName: stylerName,
allowExactGeneratedStyler: allowExactGeneratedStyler,
anchor: specElement,
);

Expand All @@ -87,6 +88,11 @@ String? buildMixableSpecTargetCall({
annotationLabel: '@MixableSpec(target:)',
keyOwner: 'the target constructor',
);
final callParams = qualifyTargetMemberDefaults(
call.params,
constructor: constructor,
targetTypeReference: widgetName,
);
if (call.forwardsKey) {
for (final typeParameter in constructor.enclosingElement.typeParameters) {
if (typeParameter.name != 'Key') continue;
Expand All @@ -109,13 +115,50 @@ String? buildMixableSpecTargetCall({

return renderWidgetCall(
widgetName: widgetName,
params: call.params,
params: callParams,
forwardsKey: call.forwardsKey,
typeParams: typeParams,
indent: indent,
);
}

/// Qualifies default-value identifiers that refer to static members of the
/// target class so they remain in scope when copied into generated code.
List<WidgetCallParam> qualifyTargetMemberDefaults(
List<WidgetCallParam> params, {
required ConstructorElement constructor,
required String targetTypeReference,
}) {
final target = constructor.enclosingElement;

return [
for (final param in params)
if (param.defaultValueCode case final code?
when RegExp(r'^[_$A-Za-z][_$A-Za-z0-9]*$').hasMatch(code) &&
_isStaticTargetMember(target, code))
WidgetCallParam(
name: param.name,
typeCode: param.typeCode,
isPositional: param.isPositional,
isRequired: param.isRequired,
defaultValueCode: '$targetTypeReference.$code',
)
else
param,
];
}

bool _isStaticTargetMember(InterfaceElement target, String name) {
for (final method in target.methods) {
if (method.name == name && method.isStatic) return true;
}
for (final field in target.fields) {
if (field.name == name && field.isStatic) return true;
}

return false;
}

/// Returns display names of optional positional parameters in declaration
/// order, with `<unnamed>` substituted for nameless parameters.
List<String> optionalPositionalNames(
Expand Down Expand Up @@ -192,33 +235,21 @@ void validateMixableSpecTargetConstructor({
required ConstructorElement constructor,
required String widgetName,
required InterfaceElement specElement,
required String specName,
required String stylerName,
required bool allowExactGeneratedStyler,
required Element anchor,
}) {
final styleWidgetSupertype = findSupertypeMatching(
constructor.enclosingElement.thisType,
styleWidgetChecker,
);
if (styleWidgetSupertype == null) {
fail(
anchor,
'Widget $widgetName must extend StyleWidget<$specName> '
'to be used as @MixableSpec(target:).',
);
}

final widgetSpecArg = styleWidgetSupertype.typeArguments.first;
if (widgetSpecArg is! InterfaceType || widgetSpecArg.element != specElement) {
final targetType = constructor.enclosingElement.thisType;
if (!widgetChecker.isAssignableFromType(targetType)) {
fail(
anchor,
'Spec generic mismatch: $specName annotated, but '
'$widgetName extends StyleWidget<${widgetSpecArg.getDisplayString()}>.',
'@MixableSpec(target:) must reference a Widget constructor, but '
'`$widgetName` is not a Widget subtype.',
);
}

final optionalPositional = optionalPositionalNames(
constructor.formalParameters,
);
final parameters = constructor.formalParameters;
final optionalPositional = optionalPositionalNames(parameters);
if (optionalPositional.isNotEmpty) {
fail(
anchor,
Expand All @@ -229,15 +260,97 @@ void validateMixableSpecTargetConstructor({
);
}

for (final parameter in constructor.formalParameters) {
if (parameter.name == 'style' && parameter.isNamed) return;
final styleParameter = parameters
.where((parameter) => parameter.name == 'style' && parameter.isNamed)
.firstOrNull;
if (styleParameter == null) {
fail(
anchor,
'@MixableSpec(target:) requires $widgetName to expose a named '
'`style` constructor parameter so the generated call() can pass '
'`style: this`.',
);
}

fail(
anchor,
'@MixableSpec(target:) requires $widgetName to expose a named '
'`style` constructor parameter so the generated call() can pass '
'`style: this`.',
final styleSpecParameter = parameters
.where((parameter) => parameter.name == 'styleSpec')
.firstOrNull;
if (styleSpecParameter != null && styleSpecParameter.isRequired) {
fail(
anchor,
'@MixableSpec(target:) cannot omit required `styleSpec` on $widgetName.',
);
}

if (!_targetStyleAcceptsGeneratedStyler(
styleParameter.type,
specElement: specElement,
stylerName: stylerName,
allowExactGeneratedStyler: allowExactGeneratedStyler,
)) {
fail(
anchor,
'@MixableSpec(target:) $widgetName `style` parameter cannot accept '
'the generated `$stylerName`.',
);
}
}

/// Whether [targetStyleType] can receive the Styler generated for
/// [specElement] in the same build.
///
/// On a clean build, the generated class is unresolved until the shared part
/// is written. A prior generated part can instead expose it as a resolved
/// interface. Both states are accepted for the exact generated Styler; other
/// resolved types must accept its known `Style<S>` supertype.
///
/// [allowExactGeneratedStyler] is false for legacy mixins, where `this` is
/// only statically known to satisfy the mixin's `Style<S>` constraint.
bool _targetStyleAcceptsGeneratedStyler(
DartType targetStyleType, {
required InterfaceElement specElement,
required String stylerName,
required bool allowExactGeneratedStyler,
}) {
final specName = specElement.name;
if (specName == null) return false;

if (targetStyleType is DynamicType ||
targetStyleType is InvalidType ||
targetStyleType.isDartCoreObject) {
return true;
}
if (targetStyleType is! InterfaceType) return false;

final targetElement = targetStyleType.element;
final isExactGeneratedStyler =
allowExactGeneratedStyler &&
targetElement.name == stylerName &&
targetElement.library.uri == specElement.library.uri;
if (isExactGeneratedStyler) return true;

var acceptedStyle = findSupertypeMatching(targetStyleType, styleChecker);
if (acceptedStyle == null &&
targetElement.name == 'Style' &&
targetElement.library.uri.toString() == 'package:mix/mix.dart') {
// Lightweight build-test fixtures declare Style directly at the public
// barrel URI. Match that exact identity rather than rendered type text.
acceptedStyle = targetStyleType;
}
if (acceptedStyle == null) return false;
if (acceptedStyle.typeArguments.isEmpty) return false;

final acceptedSpec = acceptedStyle.typeArguments.first;
final acceptsSpec =
acceptedSpec is InterfaceType &&
acceptedSpec.element.name == specName &&
acceptedSpec.element.library.uri == specElement.library.uri;
if (!acceptsSpec) return false;

return specElement.library.typeSystem.isAssignableTo(
acceptedStyle,
targetStyleType,
strictCasts: false,
);
}

Expand Down
43 changes: 6 additions & 37 deletions packages/mix_generator/lib/src/mix_widget_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
);
final callParams = targetConstructor == null
? call.params
: _qualifyDirectTargetDefaults(
: qualifyTargetMemberDefaults(
call.params,
constructor: targetConstructor,
targetTypeReference: callSource.targetTypeReference!,
Expand Down Expand Up @@ -237,7 +237,7 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
);
final callParams = targetConstructor == null
? call.params
: _qualifyDirectTargetDefaults(
: qualifyTargetMemberDefaults(
call.params,
constructor: targetConstructor,
targetTypeReference: callSource.targetTypeReference!,
Expand Down Expand Up @@ -292,39 +292,6 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
);
}

List<WidgetCallParam> _qualifyDirectTargetDefaults(
List<WidgetCallParam> params, {
required ConstructorElement constructor,
required String targetTypeReference,
}) {
final target = constructor.enclosingElement;
return [
for (final param in params)
if (param.defaultValueCode case final code?
when RegExp(r'^[_$A-Za-z][_$A-Za-z0-9]*$').hasMatch(code) &&
_isStaticTargetMember(target, code))
WidgetCallParam(
name: param.name,
typeCode: param.typeCode,
isPositional: param.isPositional,
isRequired: param.isRequired,
defaultValueCode: '$targetTypeReference.$code',
)
else
param,
];
}

bool _isStaticTargetMember(InterfaceElement target, String name) {
for (final method in target.methods) {
if (method.name == name && method.isStatic) return true;
}
for (final field in target.fields) {
if (field.name == name && field.isStatic) return true;
}
return false;
}

_CallSource _directTargetCallSource({
required Element anchor,
required ConstructorElement constructor,
Expand Down Expand Up @@ -597,14 +564,15 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
final specElement = _findGeneratedStylerSpec(library, writtenStylerName);
if (specElement == null) return null;

final specName = specElement.name!;
final annotationObject = mixableSpecAnnotationChecker.firstAnnotationOf(
specElement,
);
if (annotationObject == null) return null;

final target = ConstantReader(annotationObject).peek('target');
if (target == null || target.isNull) {
final specName = specElement.name!;

fail(
anchor,
'$_annotationLabel factory returns the same-build generated styler '
Expand All @@ -623,7 +591,8 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
constructor: constructor,
widgetName: mixableSpecTargetWidgetName(constructor),
specElement: specElement,
specName: specName,
stylerName: writtenStylerName,
allowExactGeneratedStyler: true,
anchor: anchor,
);

Expand Down
4 changes: 1 addition & 3 deletions packages/mix_generator/lib/src/styler_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ class StylerGenerator extends GeneratorForAnnotation<MixableStyler> {
required ClassElement stylerElement,
required String stylerName,
required InterfaceElement specElement,
required String specName,
}) {
final specAnnotation = mixableSpecAnnotationChecker.firstAnnotationOf(
specElement,
Expand All @@ -95,8 +94,8 @@ class StylerGenerator extends GeneratorForAnnotation<MixableStyler> {
return buildMixableSpecTargetCall(
annotation: ConstantReader(specAnnotation),
specElement: specElement,
specName: specName,
stylerName: stylerName,
allowExactGeneratedStyler: false,
hostElement: stylerElement,
hostLibrary: stylerElement.library,
validateTargetVisibility: true,
Expand Down Expand Up @@ -140,7 +139,6 @@ class StylerGenerator extends GeneratorForAnnotation<MixableStyler> {
stylerElement: classElement,
stylerName: stylerName,
specElement: specType.element,
specName: specName,
);
final fields = _extractFields(classElement, stylerName);
final config = _extractAnnotationConfig(annotation);
Expand Down
Loading
Loading