Search code examples
haxe

Could haxe macro be used to detect when object is dirty (any property has been changed)


Let say we have an object:

@:checkDirty
class Test {
   var a:Int;
   var b(default, default):String;
   var c(get, set):Array<Int>;

   public function new() {
     ...
   }
   public function get_c() {
      ...
   }
   public function set_c(n) {
     ... 
   }


}

Could we write a macro checkDirty so that any change to field/properties would set property dirty to true. Macro would generate dirty field as Bool and clearDirty function to set it to false.

var test = new Test();
trace(test.dirty); // false
test.a = 12;
trace(test.dirty); // true
test.clearDirty();
trace(test.dirty); //false
test.b = "test"
trace(test.dirty); //true

test.clearDirty();
test.c = [1,2,3];
trace(test.dirty); //true


Solution

  • I wrote a post (archive) about doing this kind of thing (except for emitting events) before - you can use a @:build macro to modify class members, be it appending an extra assignment into setter or replacing the field with a property.

    So a modified version might look like so:

    class Macro {
        public static macro function build():Array<Field> {
            var fields = Context.getBuildFields();
            for (field in fields.copy()) { // (copy fields so that we don't go over freshly added ones)
                switch (field.kind) {
                    case FVar(fieldType, fieldExpr), FProp("default", "default", fieldType, fieldExpr):
                        var fieldName = field.name;
                        if (fieldName == "dirty") continue;
                        var setterName = "set_" + fieldName;
                        var tmp_class = macro class {
                            public var $fieldName(default, set):$fieldType = $fieldExpr;
                            public function $setterName(v:$fieldType):$fieldType {
                                $i{fieldName} = v;
                                this.dirty = true;
                                return v;
                            }
                        };
                        for (mcf in tmp_class.fields) fields.push(mcf);
                        fields.remove(field);
                    case FProp(_, "set", t, e):
                        var setter = Lambda.find(fields, (f) -> f.name == "set_" + field.name);
                        if (setter == null) continue;
                        switch (setter.kind) {
                            case FFun(f):
                                f.expr = macro { dirty = true; ${f.expr}; };
                            default:
                        }
                    default:
                }
            }
            if (Lambda.find(fields, (f) -> f.name == "dirty") == null) fields.push((macro class {
                public var dirty:Bool = false;
            }).fields[0]);
            return fields;
        }
    }
    

    which, if used as

    @:build(Macro.build())
    @:keep class Some {
        public function new() {}
        public var one:Int;
        public var two(default, set):String;
        function set_two(v:String):String {
            two = v;
            return v;
        }
    }
    

    Would emit the following JS:

    var Some = function() {
        this.dirty = false;
    };
    Some.prototype = {
        set_two: function(v) {
            this.dirty = true;
            this.two = v;
            return v;
        }
        ,set_one: function(v) {
            this.one = v;
            this.dirty = true;
            return v;
        }
    };