Search code examples
macroshaxe

Checking if build macro already processed ancestor node


Assume you have type-building macro, interface invoking @:autoBuild using aforementioned macro, class implementing the interface and class extending it. Macro will fail if the class doesn't contain specific method.

Like so:

Macro.hx

package;

import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;

class Macro
{
    macro public function build():Array<Field>
    {
        var fields = Context.getBuildFields();

        for (field in fields) {
            if (field.name == "hello") {
                //Do some modifications

                return fields;       
            }
        }

        Context.error('${Context.getLocalClass().toString()} doesn\'t contain a method `hello`', Context.currentPos());

        return null;
    }
}

I.hx

package;

@:autoBuild(Macro.build())
interface I {}

Foobar.hx

package;

class Foobar implements I
{
    public function new() {}

    public function hello(person:String)
    {
        return 'Hello $person!';
    }
}

Foo.hx

package;

@:keep
class Foo extends Foobar {}

As you can see, we're checking if field "hello" exists. However, Context.getBuildFields contains only fields of current class, and build will fail for Foo.

This is where my idea comes in: Why not just check if any ancestor was already processed? We'll change Macro.hx to reflect just that:

Macro.hx

package;

import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;

class Macro
{
    macro public function build():Array<Field>
    {
        var c = Context.getLocalClass().get();
        if(isAncestorAlreadyProcessed(c)) {
            return null;
        }

        var fields = Context.getBuildFields();

        for (field in fields) {
            if (field.name == "hello") {
                //Do some modifications

                c.meta.add(":processed", [], c.pos);  

                return fields;       
            }
        }      

        Context.error('${Context.getLocalClass().toString()} doesn\'t contain a method `hello`', Context.currentPos());

        return null;
    }

    private static function isAncestorAlreadyProcessed(c:ClassType)
    {
        if (c.meta.has(":processed")) return true;
        if (c.superClass == null) return false;

        return isAncestorAlreadyProcessed(c.superClass.t.get());
    }
}

And for the main questions: Do I misunderstand haxe macro type building? Is there a more viable way of making this work? Does my code fail in specific scenarios? Are there any harmful side-effects caused by this code?

I'm trying to resolve this issue.


Solution

  • No, this is the way to go, use metadata to store information of the classes you processed (source).

    Another way, if you don't need this information at runtime, is to use a static array on a dedicated class like here. Afterwards, you can even push this information in your compiled code, see here.

    Hope that helps.