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_annotations/lib/src/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ class MixWidget {
/// Styler `call()` method. The constructor must expose a compatible named
/// `style` parameter. `style` and `styleSpec` are supplied or omitted by the
/// generator and never become wrapper fields.
// A constructor tear-off's signature differs per annotation, so no explicit
// function type fits; the generator narrows it via `toFunctionValue()`.
// ignore: prefer-explicit-function-type
final Function? target;

/// Selection of non-`key` styler `call()` value parameters exposed by the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,12 @@ class MixWidgetBuilder {
? '${model.factoryReference}(${_factoryArgs()})'
: model.factoryReference;

if (model.hasDirectTarget) {
_writeDirectTargetBuild(buffer, invocation);
// Bind the target type here so the emitter takes it non-null: interpolating
// a null reference would silently emit `null(...)` as the widget name.
if (model.targetTypeReference case final targetTypeReference?) {
_writeDirectTargetBuild(buffer, invocation, targetTypeReference);
buffer.writeln(' }');

return;
}

Expand All @@ -140,12 +143,17 @@ class MixWidgetBuilder {
buffer.writeln(' }');
}

void _writeDirectTargetBuild(StringBuffer buffer, String styleInvocation) {
final constructorSuffix = model.targetConstructorName == null
void _writeDirectTargetBuild(
StringBuffer buffer,
String styleInvocation,
String targetTypeReference,
) {
final constructorName = model.targetConstructorName;
final constructorSuffix = constructorName == null
? ''
: '.${model.targetConstructorName}';
: '.$constructorName';
final target =
'${model.targetTypeReference}${model.typeParameterInvocation}'
'$targetTypeReference${model.typeParameterInvocation}'
'$constructorSuffix';
final args = [
for (final p in model.callParams.where((p) => p.isPositional))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class MixWidgetModel {
/// all positional params first, then named params.
List<WidgetCallParam> get allParams {
final seen = <String>{};

return [
for (final parameter in [...factoryParams, ...callParams])
if (seen.add(parameter.name)) parameter,
Expand All @@ -168,7 +169,4 @@ class MixWidgetModel {

/// Type argument suffix for forwarding to the styler `call()` method.
String get typeParameterInvocation => _typeParameterSuffix((p) => p.name);

/// Whether `build()` instantiates a plain target widget directly.
bool get hasDirectTarget => targetTypeReference != null;
}
75 changes: 42 additions & 33 deletions packages/mix_generator/lib/src/mix_widget_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
}

final constructorName = constructor.name;

return _CallSource(
call: constructor,
baseExcluded: stylerBackedTargetParams,
Expand Down Expand Up @@ -406,6 +407,10 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
if (writtenStylerName == null) return false;
final spec = _findGeneratedStylerSpec(library, writtenStylerName);
if (spec == null) return false;
// Both remaining checks match the spec by name, so an unnamed element can
// never be the recipe's spec.
final specName = spec.name;
if (specName == null) return false;

// A target can itself refer to a same-build generated Styler, in which
// case analyzer reports InvalidType until the shared part is written.
Expand All @@ -416,15 +421,16 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
// Some build-test consumers re-export a lightweight Style stub from a
// barrel rather than its canonical library. Preserve semantic matching
// for that test shape without weakening non-Style targets.
return targetStyleType.getDisplayString() == 'Style<${spec.name}>';
return targetStyleType.getDisplayString() == 'Style<$specName>';
}
if (acceptedStyle.typeArguments.isEmpty) {
return false;
}

final acceptedSpec = acceptedStyle.typeArguments.first;

return acceptedSpec is InterfaceType &&
acceptedSpec.element.name == spec.name &&
acceptedSpec.element.name == specName &&
acceptedSpec.element.library.uri == spec.library.uri;
}

Expand Down Expand Up @@ -825,12 +831,12 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
required _WidgetParameterSelection factoryParameters,
}) {
final selectedParameters = <FormalParameterElement>[];
final availableNames = {
for (final parameter in function.formalParameters)
if (parameter.name case final String name) name,
};

if (!factoryParameters.includesAll) {
final availableNames = {
for (final parameter in function.formalParameters)
if (parameter.name case final String name) name,
};
for (final name in factoryParameters.names) {
if (!availableNames.contains(name)) {
fail(
Expand All @@ -850,12 +856,15 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
(name != null && factoryParameters.names.contains(name));
if (!selected) {
if (parameter.isRequired) {
// A wildcard parameter has no name to select, so report the
// analyzer's rendering rather than interpolating a null.
final label = name ?? parameter.displayName;
fail(
function,
'$_annotationLabel factoryParameters must include required '
'factory parameter `$name`.',
'factory parameter `$label`.',
todo:
'Add `$name` to `factoryParameters: .only({...})` or use '
'Add `$label` to `factoryParameters: .only({...})` or use '
'`factoryParameters: .all()`.',
);
}
Expand Down Expand Up @@ -1126,6 +1135,31 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {
return annotationType.name.lexeme;
}

ConstructorElement? _targetConstructorFor(
Element anchor,
ConstantReader annotation,
) {
final target = annotation.peek('target');
if (target == null || target.isNull) return null;

final constructor = target.objectValue.toFunctionValue();
if (constructor is! ConstructorElement) {
fail(
anchor,
'$_annotationLabel(target:) must be a constructor tear-off '
'(e.g., RemixButton.new).',
);
}
validateGenericTargetTearOff(
target: target,
constructor: constructor,
anchor: anchor,
annotationLabel: '@MixWidget(target:)',
);

return constructor;
}

@override
Future<String> generateForAnnotatedElement(
Element element,
Expand Down Expand Up @@ -1167,31 +1201,6 @@ class MixWidgetGenerator extends GeneratorForAnnotation<MixWidget> {

return MixWidgetBuilder(model).build();
}

ConstructorElement? _targetConstructorFor(
Element anchor,
ConstantReader annotation,
) {
final target = annotation.peek('target');
if (target == null || target.isNull) return null;

final constructor = target.objectValue.toFunctionValue();
if (constructor is! ConstructorElement) {
fail(
anchor,
'$_annotationLabel(target:) must be a constructor tear-off '
'(e.g., RemixButton.new).',
);
}
validateGenericTargetTearOff(
target: target,
constructor: constructor,
anchor: anchor,
annotationLabel: '@MixWidget(target:)',
);

return constructor;
}
}

/// The executable whose parameters define a generated widget's `call()`
Expand Down
Loading