diff --git a/packages/mix_annotations/lib/src/annotations.dart b/packages/mix_annotations/lib/src/annotations.dart index 68bd32d42..ecca5ee00 100644 --- a/packages/mix_annotations/lib/src/annotations.dart +++ b/packages/mix_annotations/lib/src/annotations.dart @@ -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 diff --git a/packages/mix_generator/lib/src/core/builders/mix_widget_builder.dart b/packages/mix_generator/lib/src/core/builders/mix_widget_builder.dart index db66fd401..133bd1bd8 100644 --- a/packages/mix_generator/lib/src/core/builders/mix_widget_builder.dart +++ b/packages/mix_generator/lib/src/core/builders/mix_widget_builder.dart @@ -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; } @@ -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)) diff --git a/packages/mix_generator/lib/src/core/models/mix_widget_model.dart b/packages/mix_generator/lib/src/core/models/mix_widget_model.dart index eab9df721..afcada5b3 100644 --- a/packages/mix_generator/lib/src/core/models/mix_widget_model.dart +++ b/packages/mix_generator/lib/src/core/models/mix_widget_model.dart @@ -156,6 +156,7 @@ class MixWidgetModel { /// all positional params first, then named params. List get allParams { final seen = {}; + return [ for (final parameter in [...factoryParams, ...callParams]) if (seen.add(parameter.name)) parameter, @@ -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; } diff --git a/packages/mix_generator/lib/src/mix_widget_generator.dart b/packages/mix_generator/lib/src/mix_widget_generator.dart index f9db55613..9e800f70d 100644 --- a/packages/mix_generator/lib/src/mix_widget_generator.dart +++ b/packages/mix_generator/lib/src/mix_widget_generator.dart @@ -375,6 +375,7 @@ class MixWidgetGenerator extends GeneratorForAnnotation { } final constructorName = constructor.name; + return _CallSource( call: constructor, baseExcluded: stylerBackedTargetParams, @@ -406,6 +407,10 @@ class MixWidgetGenerator extends GeneratorForAnnotation { 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. @@ -416,15 +421,16 @@ class MixWidgetGenerator extends GeneratorForAnnotation { // 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; } @@ -825,12 +831,12 @@ class MixWidgetGenerator extends GeneratorForAnnotation { required _WidgetParameterSelection factoryParameters, }) { final selectedParameters = []; - 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( @@ -850,12 +856,15 @@ class MixWidgetGenerator extends GeneratorForAnnotation { (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()`.', ); } @@ -1126,6 +1135,31 @@ class MixWidgetGenerator extends GeneratorForAnnotation { 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 generateForAnnotatedElement( Element element, @@ -1167,31 +1201,6 @@ class MixWidgetGenerator extends GeneratorForAnnotation { 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()`