A very basic prototype of macros using build_runner

Overview

Description

This is a basic prototype for 3 phase macros using package:build.

The general idea is that macros run in 3 different phases, and each phase has different capabilities available to it in terms of both introspection and code generation power.

Macros are applied as annotations on declarations. They are currently supported on classes, methods, and fields.

This phases approach allows us to provide consistent, correct results for the introspection apis that are available, while simultaneously providing a lot of power in the form of adding entirely new declarations to the program.

API Docs

The api docs can be seen here.

These are generated and published manually so they may become a bit stale, you can regenerate them by checking out the gh-pages branch, running dartdoc from the root of the repo, and sending a pull request to that branch.

Evaluation Guidelines

This prototype is not intended to reflect the final dev experience. Specifically, the IDE and codegen experience you see here is not a reflection of the final expected product. The actual feature would be built into the compilation pipelines you already use, and you would not see any generated files in your source directory.

We would like feedback focused on any other area though, such as:

  • Does this provide enough functionality for you to do everything you want to?
  • General feedback on the macro apis (anything exported by lib/definition.dart).
  • General feedback on the multi-phased approach.
  • Any other feedback unrelated to build_runner or the specific code generation process used in this prototype.

Note that if you do implement some macros, we would love for you to contribute them to the repo so we have more concrete examples of the use cases!

Intro to the Macro interfaces

Each type of macro has it's own interface, each of which have a single method that you must implement. These methods each take two arguments, the first argument is the object you use to introspect on the object that was annotated with the macro, and the second argument is a builder object used to modify the program.

For example, lets take a look at the ClassDeclarationMacro interface:

/// The interface for [DeclarationMacro]s that can be applied to classes.
abstract class ClassDeclarationMacro implements DeclarationMacro {
  void visitClassDeclaration(
      ClassDeclaration declaration, ClassDeclarationBuilder builder);
}

This macro is given a ClassDeclaration object as the first argument, which gives you all the reflective information available for a class, in the "declaration" phase (phase 2).

The second argument is a ClassDeclarationBuilder, which has an addToClass(Declaration declaration) method you can use to add new declarations to the class. A ClassDeclarationBuilder is also a DeclarationBuilder, which gives you the ability to add top level declarations with the addToLibrary(Declaration declaration) method.

Implementing Multiple Macro Interfaces

A single macro class is allowed to implement multiple macro interfaces, which allows it to run in several phases. As an example of this you can look at the example/macros/json.dart macro, which implements three different macro interfaces. First, is the ClassDeclarationMacro, which it uses to define the interface only of the fromJson constructor and toJson methods.

It then also implements the ConstructorDefinitionMacro and MethodDefinitionMacro interfaces which it uses to fill in those declarations with the full implementations.

Note that a macro can provide a full definition in the declaration phase, but the json macro needs more reflective information than is available to it in that phase, so it waits until the later phase where it can fully introspect on the program to fill in the implementations.

Phase 1 - Type Macros

These macros have almost no introspection capability, but are allowed to introduce entirely new classes to the application.

To make a macro run in this phase you should implement either ClassTypeMacro, FieldTypeMacro, or MethodTypeMacro, depending on which type of declaration your macro supports running on.

Phase 2 - Declaration Macros

These macros can introspect on the declaration they annotate, but cannot recursively introspect on the types referenced in those declarations. They are however allowed to ask questions about type relationships for any types they see, through the isSubtypeOf api.

These macros are allowed to introduce new public declarations to classes, as well as the current library, but not new types.

To make a macro run in this phase you should implement either ClassDeclarationMacro, FieldDeclarationMacro, or MethodDeclarationMacro, depending on which type of declaration your macro supports running on.

Phase 3 - Definition Macros

These macros can introspect fully on the declaration they annotate, including recursively introspecting on the types referenced in those declarations.

In exchange for this introspection power, these macros are only allowed to implement existing declarations. No new declarations can be added in this phase, so the static shape of the program is fully complete before these macros run.

To make a macro run in this phase you should implement either ClassDefinitionMacro, FieldDefinitionMacro, or MethodDefinitionMacro, depending on which type of declaration your macro supports running on.

Example Macros, Wiring Up a New Macro

VM example

You can see some examples under example/macros.

Once you create a macro you will need to add it to all the phases in which it should run, see the typesBuilder, declarationsBuilder, and definitionsBuilder methods in example/builders.dart, and pass in your new macro to the corresponding constructor for the phase in which it should run.

Flutter example

You can see flutter examples under flutter_example/macros.

Once you create a macro you will need to add it to all the phases in which it should run, see the typesBuilder, declarationsBuilder, and definitionsBuilder methods in flutter_example/lib/builders.dart, and pass in your new macro to the corresponding constructor for the phase in which it should run.

Using a Macro

For this prototype you need to create your example files that use your macros with the extension .gen.dart. We will run codegen on only those files, applying the macros and generating a file for the output at each phase.

The final phase will create a regular .dart file which is what you should import. If you want to import other files which also use codegen, you should do so by importing the .gen.dart files, this will ensure they are visible to the build of your library (and only the appropriate info should be available).

Running macros

You will use the build_runner package to run builds, which you can run with the pub run build_runner build (or flutter pub run build_runner build) command. There is also a watch command you may want to use to get fast rebuilds.

For vm apps if you want to build and run an app in a single command, you can use the run command: pub run build_runner run example/main.dart.

For flutter apps you will need to run build_runner separately, and then launch the flutter app as normal.

Comments
  • Verify context of macros

    Verify context of macros

    Some macros may only make sense in a given context. For example, the field macro defined in https://github.com/jakemac53/macro_prototype/pull/20 only makes sense on fields within a RenderObject class because the generated code calls methods from that class.

    However, currently, there doesn't appear to be a mechanism to verify that the macro annotation is used in the correct context and instead of giving a good error message, broken code is generated if the macro is used in fields in other subclasses.

    enhancement 
    opened by goderbauer 3
  • Add an api to check if a type is exactly another type

    Add an api to check if a type is exactly another type

    We have the isSubtypeOf api today but there are cases where you want something more like isExactly.

    The specific use case I ran into is for autoListenable. I want to be able to find the widget class for a state class. The way I want to do that is to walk up the super hierarchy until I find the State class, and then grab the type argument from that.

    class MyState extends State<MyWidget> {}
    

    Today I just ended up doing a String name check.

    opened by jakemac53 2
  • Some IDEs use `gen` to indicate the output of code generation

    Some IDEs use `gen` to indicate the output of code generation

    IntelliJ, for example, uses gen as the default name for the directory that holds code generated by various tools. It might be confusing to use gen in the name of input files that are processed by code generators. An alternative might be src instead.

    opened by stevemessick 1
  • add initial autoListenable macro for fields

    add initial autoListenable macro for fields

    Towards https://github.com/jakemac53/macro_prototype/issues/23

    cc @goderbauer can you do a review here? Much of this is actually checked in generated code - you can ignore all the main.*.dart files except main.gen.dart which is the hand written one.

    I want to follow up with exploring use cases for listenable objects that come from the build context next.

    This also has some extra cleanup in the fromParts apis - I realized it doesn't make sense for them to auto-concat nested lists of code with a ,. Instead you can supply a separator if you wish, otherwise pieces of code are just concatenated directly.

    I also did remove the usage of autoDispose from the example - right now the prototype implementation doesn't support mixing these two macros together well, I filed a couple of related issues on that.

    opened by jakemac53 1
  • Prototype the api for a ClassDefinitionBuilder

    Prototype the api for a ClassDefinitionBuilder

    Fixes https://github.com/jakemac53/macro_prototype/issues/22

    Note that actually implementing this would be difficult in this prototype (although not impossible).

    cc @munificent how does this api look? Do you think it makes sense? https://github.com/jakemac53/macro_prototype/pull/25/files#diff-d78f06f14f4a152be813a37051933bcfb67bdac6fdab94a9b50e3230359131ffR287

    opened by jakemac53 1
  • MethodDeclarationMacro needs to be specialized for top level functions

    MethodDeclarationMacro needs to be specialized for top level functions

    Today the api is void visitMethodDeclaration(MethodDeclaration declaration, ClassDeclarationBuilder builder) but really we need a LibraryDeclarationBuilder for this case, as the addToClass method on ClassDeclarationBuilder doesn't make sense in this context :).

    enhancement 
    opened by jakemac53 1
  • Add Freezed macro example

    Add Freezed macro example

    This adds a macro that mimics the behaviour of freezed

    Not all features are implemented, and some workarounds were needed, but this is a good start.

    Some missing macro features:

    • class macros currently don't have informations about constructors (constructor.isFactory, isConst, redirected factory name)
    • I don't see a way to access annotations either
    opened by rrousselGit 0
  • fix global vars and constructors

    fix global vars and constructors

    Hi, I was trying out a few things and ran into some issues, here are a few fixes:

    -- fix issue where global vars declarations disappeared -- fix issue where enums in the file threw an Exception because of a cast -- fix issue where constructor macros were not working

    opened by TimWhiting 0
  • Global vars and constructors

    Global vars and constructors

    Hi, I was trying out a few things and ran into some issues, here are a few fixes:

    -- fix issue where global vars declarations disappeared -- fix issue where enums in the file threw an Exception because of a cast -- fix issue where constructor macros were not working

    Sorry it looks like another commit got mixed up in here. Let me fix that really quick

    opened by TimWhiting 0
  • Move the mutation API out to separate builder objects.

    Move the mutation API out to separate builder objects.

    (You don't have to land this. Just sending a PR to get your attention. :) )

    Here's a rough proposal for how it could look to move the mutation side of the macro API to objects that are passed separately to the macro. This means there is no longer a need to maintain separate pairs of Foo and TargetFoo classes.

    I don't think it's a huge win in this PR, but as these APIs fill out with more members, I think the separation would become more valuable.

    opened by munificent 0
  • Update apis to not have conflicting names

    Update apis to not have conflicting names

    This allows a macro to implement multiple types of macros. I updated the observable example as a use case, it can now annotate a whole class (making all fields observable), or just individual fields.

    @munificent PTAL and give some feedback on the names... probably just the first commit is all you really need to look at.

    opened by jakemac53 0
  • Enhanced Type manipulation

    Enhanced Type manipulation

    So far, a type only exposes limited informations

    It would be useful to add extra utilities, such as:

    • converting a runtimeType into a type (similar to TypeChecker from the analyzer)
    • expose an utility to obtain the nearest common interface between two types (like num for double+int, but Object for String+int)
    opened by rrousselGit 1
  • Add more introspection info for constructors

    Add more introspection info for constructors

    It would be good to have isFactory, isConst, and then in the case of redirecting factory constructors it would be good if we could get the name.

    The last bit is a bit interesting as we may want to allow providing an implementation of an external factory constructor, which is actually just to make it a redirecting constructor. That could get added in the last phase and then wouldn't be available in earlier phases etc, so we should thing about the consequences of that. Likely we can just say that is OK though.

    opened by jakemac53 0
  • Use-case: statically extract the dependencies of a function

    Use-case: statically extract the dependencies of a function

    Currently, both Riverpod and flutter_hooks sometimes relies on users having to define a list of dependencies used by a function

    An example would be:

    final provider = Provider<MyClass>((ref) {
      ref.watch(dependency);
      ref.watch(anotherDependency);
    
      return MyClass();
    },
      dependencies: { dependency, anotherDependency }, // the list of providers passed to "ref.watch"
      name: 'provider',
     );
    

    or:

    Widget build(context) {
      final cachedResult = useMemoized(
        () => MyClass(dependency),
        [dependency], // the list of variables used within the closure
      );
      final another = useMemoized(() => MyClass(foo, bar: bar), [foo, bar]);
     ...
    }
    

    The problem with this syntax is, it is error prone.

    The interesting thing is, in both examples, this list of dependency is known at compile time. This means that this secondary parameter could instead be generated by a macro directly with a 100% accuracy.

    In particular, I was thinking of changing the first example to be:

    @provider
    MyClass $provider(ProviderReference ref) {
      ref.watch(dependency);
      ref.watch(anotherDependency);
      return MyClass();
    }
    

    which would generate:

    final provider = Provider($provider, dependencies: { dependency, anotherDependency }, name: 'provider');
    

    Sadly it seems like the current macro API does not give us enough informations about the function content to do such a thing.

    opened by rrousselGit 8
  • Can't use the `typeDefinitionOf` api for flutter types

    Can't use the `typeDefinitionOf` api for flutter types

    Because the build runs on the VM, you can't import flutter, so you can't check if things are subtypes of known flutter types.

    A possibly solution could be a VM compatible version of flutter, but that seems like a very large hammer to use.

    bug 
    opened by jakemac53 0
  • Allow a class/mixin to implicitly apply a macro wherever it is mixed in or extended?

    Allow a class/mixin to implicitly apply a macro wherever it is mixed in or extended?

    I am not sure this is a good idea, but filing it to have a central area for discussion.

    The general idea is to have a mechanism available whereby mixing in or extending a particular class would auto apply some macros to the class that mixed it in or extended it.

    This would eliminate the need for annotations in some cases, and enable frameworks to "hide" the macro applications from their users. Consider for instance:

    class MyWidgetState extends State<MyWidget> with AutoListener {}
    

    Instead of:

    @autoListen
    class MyWidgetState extends State<MyWidget> {}
    

    Potentially frameworks could even add macro applications to their base classes (ie: State or Widget) in this example.

    Pros

    • Hides the magic in some cases
    • Potentially less boilerplate

    Cons

    • Hides the magic, making it more magical, and less expected
    • Two ways to do the same thing. Some macro authors may choose to use this just to avoid the annotation, making there two ways to do exactly the same thing.
    • Potential overhead in more complicated class hierarchies that otherwise aren't necessary (if the mixin is only used to apply the macro for instance)
    enhancement 
    opened by jakemac53 2
Owner
Jacob MacDonald
Jacob MacDonald
Dart R-file generator for build_runner

r_resources This package is made for R-file code generation using build_runner.

Ivanov Nikita 0 Dec 17, 2021
A Very Good Command Line Interface for Dart created by Very Good Ventures 🦄

Very Good CLI Developed with ?? by Very Good Ventures ?? A Very Good Command Line Interface for Dart. Installing $ dart pub global activate very_good_

Very Good Open Source 1.8k Jan 8, 2023
A test runner for Flutter and Dart created by Very Good Ventures 🦄

Very Good Test Runner Developed with ?? by Very Good Ventures ?? This package is a test runner for Flutter and Dart created by Very Good Ventures. It

Very Good Open Source 16 Dec 15, 2022
Basic Dart reverse shell code

dart_rs Basic Dart reverse shell based on this one by Potato-Industries. Pretty self explanatory. You’ll need Windows. I used a Windows 7 64-bit VM. F

null 21 Oct 2, 2022
MB Contact Form is a basic flutter widget which helps you for your contact page.

mb_contact_form is a third party flutter package. This is a simple version of Contact Form. This can be easily added to your flutter projects. This make your works simpler and your code shorter. This is recently updated and has null safety too.

Mouli Bheemaneti 2 Oct 17, 2022
Simple basic authentication app designed in flutter

flutter_login_signup Simple basic authentication app designed in flutter. App design is based on Login/Register Design designed by Frank Arinze Downlo

null 3 Dec 3, 2021
An android application built using Flutter that computes the Body Mass Index of person and suggestion to carry ,by taking Inputs (Weight, Height, and Age), Built using Flutter

BMI Calculator ?? Our Goal The objective of this tutorial is to look at how we can customise Flutter Widgets to achieve our own beautiful user interfa

dev_allauddin 7 Nov 2, 2022
A Dart package to web scraping data from websites easily and faster using less code lines.

Chaleno A flutter package to webscraping data from websites This package contains a set of high-level functions that make it easy to webscrap websites

António Nicolau 30 Dec 29, 2022
Notes app using flutter, firebase and cloud firestore

notes A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is

Sidheshwar S 8 Aug 10, 2022
A BMI calculator application using Flutter.

BMI Calculator Purpose Maintaining a healthy weight is important for overall health and can help to prevent and control many diseases and conditions.

Srijan Kumar Gupta 3 Apr 21, 2022
Rocket is a parsing framework for parsing binary data structures using efficient parsing algorithms

rocket Version 0.1.10 (BETA) Rocket is a parsing framework for parsing binary data structures using efficient parsing algorithms. Breaking change: The

null 5 Dec 5, 2021
Simple generative arts created using Flutter

Flutter Generative Art Try it out on DartPad Simple Generative Art created using Flutter. Watch the full video on YouTube to know how to build it from

Souvik Biswas 11 Aug 11, 2022
An application built using flutter that works as destiny checker, whether our mind wish is as we desire or not with 8 different option.

magic_8_ball_utter A new Flutter application. Getting Started This project is a starting point for a Flutter application. A few resources to get you s

dev_allauddin 3 Feb 3, 2022
An application built using Flutter that holds a static personal/professional informations related to me in the form of card.(Digital Visiting Card)

mi_card_flutter A new Flutter application. Getting Started This project is a starting point for a Flutter application. A few resources to get you star

dev_allauddin 3 Feb 3, 2022
Dart port for artwork-extractor using FFI

A simple library to extract & save artwork of a ?? music/audio file. This library is using a artwork-extractor library by alexmercerind. It is super s

null 3 Feb 23, 2022
An auto mapper for Dart. It allows mapping objects of different classes automatically and manually using JSON serialization.

AutoMapper for Dart An auto mapper for Dart. It allows mapping objects of different classes automatically and manually using JSON serialization. Examp

Leynier Gutiérrez González 7 Aug 24, 2022
A flutter package with classes to help testing applications using the canvas

Canvas test helpers MockCanvas is a utility class for writing tests for canvas operations. It supports the same API as the regular Canvas class from d

Blue Fire 12 Jan 31, 2022
Volt is a wrapper over the Revolt API for easily writing bots using the Dart language.

Volt is a wrapper over the Revolt API for easily writing bots using the Dart language. It is currently in active development so not all of the functionality has yet been implemented.

null 8 Dec 13, 2022
Log snapshot management solution (iOS/Android/Web/Server) built with Flutter/Dart using Bloc pattern and Firebase Firestore backend.

Log snapshot management solution (iOS/Android/Web/Server) built with Flutter/Dart using Bloc pattern and Firebase Firestore backend.

Alexey Perov 5 Nov 9, 2022