language icon indicating copy to clipboard operation
language copied to clipboard

Macro API can't generate new types based on methods and properties from the base class

Open tilucasoli opened this issue 1 year ago • 7 comments

I was trying to use the new dart feature Macro to generate the repetitive code in the Mix framework (@leoafarias). Initially, I aimed to simplify the API by generating new classes from a base class. For instance:

@GenerateModifier()
class OpacityModifier {
  final double opacity;

  const OpacityModifier(this.opacity);

  @override
  Widget build(Widget child) {
    assert(
      opacity >= 0.0 && opacity <= 1.0,
      'The opacity must be between 0.0 and 1.0 (inclusive).',
    );

    return Opacity(opacity: opacity, child: child);
  }
}

and then it will generate three more classes based on these properties and methods, OpacityModifierSpec, OpacityModifierAttribute, and OpacityUtility, they will be like:

class OpacityModifierSpec extends WidgetModifierSpec<OpacityModifierSpec> {
  final double opacity;
  const OpacityModifierSpec(this.opacity);

  @override
  OpacityModifierSpec lerp(OpacityModifierSpec? other, double t) {
    return OpacityModifierSpec(
      lerpDouble(opacity, other?.opacity, t) ?? opacity,
    );
  }

  @override
  OpacityModifierSpec copyWith({double? opacity}) {
    return OpacityModifierSpec(opacity ?? this.opacity);
  }

  @override
  get props => [opacity];

  @override
  Widget build(Widget child) {
    assert(
      opacity >= 0.0 && opacity <= 1.0,
      'The opacity must be between 0.0 and 1.0 (inclusive).',
    );

    return Opacity(opacity: opacity, child: child);
  }
}

class OpacityModifierAttribute extends WidgetModifierAttribute<
    OpacityModifierAttribute, OpacityModifierSpec> {
  final double opacity;
  const OpacityModifierAttribute(this.opacity);

  @override
  OpacityModifierAttribute merge(OpacityModifierAttribute? other) {
    return OpacityModifierAttribute(other?.opacity ?? opacity);
  }

  @override
  OpacityModifierSpec resolve(MixData mix) {
    return OpacityModifierSpec(opacity);
  }

  @override
  get props => [opacity];
}

class OpacityUtility<T extends Attribute>
    extends MixUtility<T, OpacityModifierAttribute> {
  const OpacityUtility(super.builder);
  T call(double value) => builder(OpacityModifierAttribute(value));
}

The problem is that the ClassTypeBuilder on ClassTypesMacro can't return the properties and methods. Why can't ClassTypesMacro access these values? What do you suggest to solve this?

tilucasoli avatar May 23 '24 19:05 tilucasoli

The problem is that the ClassTypeBuilder on ClassTypesMacro can't return the properties and methods. Why can't ClassTypesMacro access these values? What do you suggest to solve this?

Before I try to explain why (it's complicated), can I ask exactly what you need it for exactly? I don't see anything that specifically looks like you need to do this, in the types phase, in the example provided here.

First one assumption I am making, is that there were some typos here, correct me if I am wrong about them:

@GenerateModifier()
class OpacityModifier {
  final double opacity;

  /// Should this have been OpacityModifier? I am in general a bit confused how this class comes into play.
  /// Is it really an interface?
  const OpacityModifierSpec(this.opacity); 
  ...
}

// Should the extended type have been WidgetModifierSpec<OpacityModifier>?
class OpacityModifierSpec extends WidgetModifierSpec<OpacityModifierSpec> { ... }

As far as generating the OpacityModifierSpec, OpacityModifierAttribute, and OpacityUtility classes, all you need to generate is the header initially. It sounds like you are trying to generate the entire class up front. Instead, you should try to fill in the declarations and definitions in later phases, by applying macros to the classes which you add in the first phase.

So, you should have a ClassTypesMacro, which when it calls declareType, it only generates the following code, with additional macro annotations that will fill in the later code:

@GenerateModifierSpec()
class OpacityModifierSpec extends WidgetModifierSpec<OpacityModifier> {}

@GenerateModifierAttribute()
class OpacityModifierAttribute extends WidgetModifierAttribute<
    OpacityModifierAttribute, OpacityModifierSpec> {}

@GenerateUtility()
class OpacityUtility<T extends Attribute>
    extends MixUtility<T, OpacityModifierAttribute> {}

Each of these macros will implement ClassDeclarationsMacro, and possibly ClassDefinitionsMacro. These will add all the methods, and the implementations of those methods, to each of the classes.

If you need access to the original class you would provide that as an argument to the macros - so they would look like @GenerateModifierSpec(OpacityModifier) as an example. This will not work today I don't think though, as it isn't implemented yet.

Let me know if I can help further, or if there is some additional thing I am missing here.

jakemac53 avatar May 23 '24 21:05 jakemac53

@jakemac53, Thank you for the answer!

Currently, for each attribute, we need to create those 3 classes, so my idea with the macros features is to generate them from a new simple class that contains only the attributes, the constructor, and the build method. However, as you said it's impossible to do today because I can't get those infos in the type phase.

tilucasoli avatar May 24 '24 13:05 tilucasoli

By attribute, do you mean field, as in final double opacity;?

You could require the macro to be applied to each field, although that isn't as nice to be sure.

It is possible we might in the future allow looking at the user written macros in this types phase also, but note that it means your macro would not compose well with other macros that want to add fields.

So, let's say somebody wrote a data class macro which generate fields based on a constructor signature. Your macro would not ever be able to see those fields.

jakemac53 avatar May 24 '24 14:05 jakemac53

Sorry, I didn't give you the context. Our package is to help Flutter Developers stylize their Widgets, and attributes are how we named the methods that can apply style to the widget.

If you need access to the original class you would provide that as an argument to the macros - so they would look like @GenerateModifierSpec(OpacityModifier) as an example. This will not work today I don't think though, as it isn't implemented yet.

In your first answer, you mention providing the class as an argument. Will I be able to access the constructors, methods, and properties of this class in this manner?

tilucasoli avatar May 24 '24 16:05 tilucasoli

In your first answer, you mention providing the class as an argument. Will I be able to access the constructors, methods, and properties of this class in this manner?

After the types phase, yes

jakemac53 avatar May 24 '24 17:05 jakemac53

Sorry, I didn't give you the context. Our package is to help Flutter Developers stylize their Widgets, and attributes are how we named the methods that can apply style to the widget.

In the example above, which thing is an attribute?

jakemac53 avatar May 24 '24 17:05 jakemac53

It is the class Attribute. Each attribute can be applied as a Style for our own Widgets, and then they can be resolved in the Spec.

tilucasoli avatar May 24 '24 17:05 tilucasoli