241 lines
8.4 KiB
Haxe
241 lines
8.4 KiB
Haxe
package bindx.macro;
|
|
|
|
import bindx.macro.GenericError;
|
|
import haxe.macro.Type;
|
|
import haxe.macro.Expr;
|
|
import haxe.macro.Context;
|
|
|
|
using haxe.macro.Tools;
|
|
using Lambda;
|
|
using StringTools;
|
|
using bindx.macro.MetaUtils;
|
|
|
|
class BindableMacros {
|
|
|
|
static inline var OLD_VALUE = "__oldValue__";
|
|
static inline var NEW_VALUE = "__newValue__";
|
|
|
|
/**
|
|
* default value: false
|
|
*/
|
|
static public inline var INLINE_SETTER = "inlineSetter";
|
|
/**
|
|
* default value: false
|
|
*/
|
|
static public inline var FORCE = "force";
|
|
|
|
static public inline var BINDABLE_FIELDS = ":bindableFields";
|
|
|
|
static var processed:Array<String> = [];
|
|
|
|
static var bindingSignalProvider:IBindingSignalProvider;
|
|
|
|
macro static public function buildIBindable():Array<Field> {
|
|
var type = Context.getLocalType();
|
|
var tName = type.toComplexType().toString();
|
|
if (processed.indexOf(tName) > -1) {
|
|
return null;
|
|
}
|
|
processed.push(tName);
|
|
|
|
var classType = type.getClass();
|
|
|
|
Context.onMacroContextReused(function () {
|
|
processed = [];
|
|
return true;
|
|
});
|
|
|
|
if (bindingSignalProvider == null) {
|
|
bindingSignalProvider = new bindx.macro.BindSignalProvider();
|
|
}
|
|
|
|
var fields = Context.getBuildFields();
|
|
|
|
var meta = classType.bindableMeta();
|
|
if (meta != null) injectBindableMeta(fields, meta);
|
|
|
|
if (classType.isInterface) {
|
|
var a = [];
|
|
for (f in fields) {
|
|
for (m in f.meta) if (m.name == MetaUtils.BINDABLE_META) {
|
|
a.push(macro $v { f.name } );
|
|
if (m.params.length > 0)
|
|
Context.warning('Interface doesn\'t support @:bindable meta params', m.pos);
|
|
}
|
|
}
|
|
var classMeta = classType.meta;
|
|
if (classMeta.has(BINDABLE_FIELDS))
|
|
classMeta.remove(BINDABLE_FIELDS);
|
|
classMeta.add(BINDABLE_FIELDS, a, classType.pos);
|
|
return fields;
|
|
}
|
|
|
|
var interfaceFields = getBindableFieldsFromInterfaces(classType);
|
|
|
|
var res = [];
|
|
for (f in fields)
|
|
if (f.hasBindableMeta()) {
|
|
if (!isFieldBindable(f, fields)) Context.error('can\'t bind field \'${f.name}\'', f.pos);
|
|
|
|
bindField(f, fields, res);
|
|
} else {
|
|
if (interfaceFields.exists(f.name))
|
|
Context.fatalError('Interface "${typeName(interfaceFields.get(f.name))}" expects @:bindable metadata', f.pos);
|
|
res.push(f);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
inline static function typeName(t: { module:String, name:String } ) {
|
|
return t.module + (t.module.length > 0 ? "." + t.name : t.name);
|
|
}
|
|
|
|
static function getBindableFieldsFromInterfaces(classType:ClassType):Map<String, ClassType> {
|
|
var interfaceFields = new Map();
|
|
|
|
function iter(t:ClassType) {
|
|
var meta = t.meta.get();
|
|
for (m in meta) if (m.name == BINDABLE_FIELDS) {
|
|
for (a in m.params) {
|
|
var value = switch a.expr { case EConst(CString(s)): s; case _: null; };
|
|
interfaceFields.set(value, t);
|
|
}
|
|
break;
|
|
}
|
|
for (it in t.interfaces) iter(it.t.get());
|
|
}
|
|
|
|
for (i in classType.interfaces) iter(i.t.get());
|
|
return interfaceFields;
|
|
}
|
|
|
|
static function bindField(field:Field, fields:Array<Field>, res:Array<Field>):Void {
|
|
var meta = field.bindableMeta();
|
|
bindingSignalProvider.getFieldDispatcher(field, res);
|
|
|
|
var forceParam = meta.findParam(FORCE);
|
|
var inlineSetter = meta.findParam(INLINE_SETTER);
|
|
if (forceParam.isNotNullAndTrue()) {
|
|
if (inlineSetter != null)
|
|
Warn.w('\'$INLINE_SETTER\' ignored. \'$FORCE\' mode', inlineSetter.pos, WarnPriority.INFO);
|
|
res.push(field);
|
|
return;
|
|
}
|
|
|
|
switch (field.kind) {
|
|
case FVar(type, expr):
|
|
var fieldName = field.name;
|
|
var setterName = 'set_$fieldName';
|
|
field.kind = FProp("default", "set", type, expr);
|
|
res.push(field);
|
|
var setter = macro function foo(value:$type) {
|
|
var $OLD_VALUE = this.$fieldName;
|
|
if ($i{OLD_VALUE} == value) return $i{OLD_VALUE};
|
|
this.$fieldName = value;
|
|
${bindingSignalProvider.getFieldChangedExpr(field, macro $i{OLD_VALUE}, macro $i{"value"})}
|
|
return value;
|
|
};
|
|
var setterAccess = [APrivate];
|
|
if (inlineSetter.isNotNullAndTrue()) setterAccess.push(AInline);
|
|
res.push({
|
|
name: setterName,
|
|
kind: FFun(switch (setter.expr) { case EFunction (_, func): func; case _: throw false; }),
|
|
pos: field.pos,
|
|
access: setterAccess
|
|
});
|
|
|
|
case FProp(get, set, type, expr):
|
|
if (inlineSetter != null)
|
|
Warn.w('$INLINE_SETTER ignored. Setter already exist', inlineSetter.pos, WarnPriority.INFO);
|
|
var fieldName = field.name;
|
|
var setter = fields.find(function (it) return it.name == 'set_$fieldName');
|
|
if (setter != null) {
|
|
switch (setter.kind) {
|
|
case FFun(func):
|
|
patchField = field;
|
|
func.expr = macro {
|
|
var $OLD_VALUE = this.$fieldName;
|
|
if ($i{OLD_VALUE} == $i{func.args[0].name}) return $i{OLD_VALUE};
|
|
$e{patchSetter(func.expr)};
|
|
};
|
|
patchField = null;
|
|
case _:
|
|
}
|
|
}
|
|
res.push(field);
|
|
|
|
case FFun(f):
|
|
if (inlineSetter != null)
|
|
Warn.w('methods doesn\'t support \'$INLINE_SETTER\'', inlineSetter.pos, WarnPriority.INFO);
|
|
res.push(field);
|
|
}
|
|
}
|
|
|
|
static var patchField:Field;
|
|
|
|
static function patchSetter(expr:Expr):Expr {
|
|
return switch (expr.expr) {
|
|
case EReturn(res):
|
|
var fieldName = patchField.name;
|
|
|
|
macro {
|
|
var $NEW_VALUE = ${res.map(patchSetter)};
|
|
${bindingSignalProvider.getFieldChangedExpr(patchField, macro $i{OLD_VALUE}, macro $i{NEW_VALUE})};
|
|
return $i{NEW_VALUE};
|
|
}
|
|
|
|
case _: expr.map(patchSetter);
|
|
}
|
|
}
|
|
|
|
static inline function injectBindableMeta(fields:Array<Field>, meta:MetadataEntry):Void {
|
|
for (f in fields) {
|
|
if (f.hasBindableMeta()) continue;
|
|
if (f.access.exists(function (it) return it.equals(APrivate))) continue;
|
|
|
|
var forceParam = meta.findParam(FORCE);
|
|
if (isFieldBindable(f, fields, forceParam.isNotNullAndTrue()))
|
|
switch (f.kind) {
|
|
case FFun(_):
|
|
case _: f.meta.push({name:MetaUtils.BINDABLE_META, pos:f.pos, params:meta.params});
|
|
}
|
|
}
|
|
}
|
|
|
|
static function isFieldBindable(field:Field, fields:Array<Field>, force = false):Bool {
|
|
if (field.name == "new") return false;
|
|
|
|
for (a in field.access)
|
|
if (a.equals(AMacro) || a.equals(ADynamic) || a.equals(AStatic))
|
|
return false;
|
|
|
|
var fn = field.name;
|
|
if (fn.startsWith("set_") || fn.startsWith("get_")) {
|
|
var propName = fn.substr(4);
|
|
for (f in fields) if (f.name == propName) {
|
|
switch (f.kind) {
|
|
case FProp(get, set, _, _) if (fn == get || fn == set):
|
|
return false;
|
|
case _:
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!force) {
|
|
var meta = field.bindableMeta();
|
|
var forceParam = meta != null ? meta.findParam(FORCE) : null;
|
|
force = forceParam.isNotNullAndTrue();
|
|
}
|
|
|
|
if (force) return switch (field.kind) {
|
|
case FProp("never", _, _, _): false;
|
|
case _: true;
|
|
}
|
|
|
|
return switch (field.kind) {
|
|
case FProp("never", _, _, _) | FProp(_, "never", _, _) | FProp(_, "dynamic", _, _) | FProp(_, "null", _, _): false;
|
|
case _: true;
|
|
}
|
|
}
|
|
} |