haxe/ui/macros/helpers/ClassBuilder.hx
package haxe.ui.macros.helpers;
import haxe.macro.Expr.ComplexType;
import haxe.macro.Expr;
import haxe.macro.ExprTools;
import haxe.macro.Type.ClassField;
import haxe.macro.Type.ClassType;
import haxe.macro.Type.Ref;
import haxe.macro.TypeTools;
using Lambda;
class ClassBuilder {
public var fields:Array<Field>;
public var type:haxe.macro.Type;
public var classType:ClassType;
public var pos:Position;
public function new(fields:Array<Field> = null, type:haxe.macro.Type = null, pos:Position = null) {
this.fields = fields;
this.type = type;
if (type != null) {
try {
#if macro
this.classType = TypeTools.getClass(type);
#else
this.classType = null;
#end
} catch (e:Dynamic) {}
}
this.pos = pos;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// General
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public var vars(get, null):Array<FieldBuilder>;
private function get_vars():Array<FieldBuilder> {
var r = [];
for (f in fields) {
switch (f.kind) {
case FVar(_, _):
r.push(new FieldBuilder(f, this));
case _:
}
}
return r;
}
public var complexType(get, null):ComplexType;
private function get_complexType():ComplexType {
return TypeTools.toComplexType(type);
}
public var typePath(get, null):TypePath;
private function get_typePath():TypePath {
return switch (complexType) {
case TPath(p): p;
case _: null;
}
}
public var fullPath(get, null):String;
private function get_fullPath():String {
#if macro
var s = TypeTools.toString(type);
var n = s.indexOf("<");
if (n != -1) {
s = s.substring(0, n);
}
return s;
#else
return null;
#end
}
public var name(get, null):String;
private function get_name():String {
var fullPathCopy = fullPath;
var n1 = fullPathCopy.indexOf("<");
var n2 = fullPathCopy.indexOf(">");
if (n1 != -1 && n2 != -1) {
fullPathCopy = fullPathCopy.substring(0, n1) + fullPathCopy.substring(n2 + 1);
}
return fullPathCopy.split(".").pop();
}
public var pkg(get, null):Array<String>;
private function get_pkg():Array<String> {
var fullPathCopy = fullPath;
var n1 = fullPathCopy.indexOf("<");
var n2 = fullPathCopy.indexOf(">");
if (n1 != -1 && n2 != -1) {
fullPathCopy = fullPathCopy.substring(0, n1) + fullPathCopy.substring(n2 + 1);
}
var parts = fullPathCopy.split(".");
parts.pop();
return parts;
}
public var isPrivate(get, null):Bool;
private function get_isPrivate():Bool {
return switch (type) {
case TInst(c, _):
c.get().isPrivate || c.get().meta.has(":noCompletion");
case TType(tt, _):
return tt.get().meta.has(":noCompletion");
case _:
false;
}
}
public var isInterface(get, null):Bool;
private function get_isInterface():Bool {
return switch (type) {
case TInst(c, _):
c.get().isInterface;
case _:
false;
}
}
public var isAbstractClass(get, null):Bool;
private function get_isAbstractClass():Bool {
return #if (haxe_ver >= 4.2) classType.isAbstract #else false #end;
}
public function findField(name:String):Field {
var r = null;
if (fields != null) {
for (f in fields) {
if (f.name == name) {
r = f;
break;
}
}
}
return r;
}
private static var CLASS_FIELD_CACHE:Map<String, Map<String, ClassField>> = null;
private static function cacheClassFields(c:ClassType) {
if (CLASS_FIELD_CACHE != null) {
return;
}
CLASS_FIELD_CACHE = new Map<String, Map<String, ClassField>>();
var fullName = c.pack.join(".") + "." + c.name;
var map = CLASS_FIELD_CACHE.get(fullName);
if (map == null) {
map = new Map<String, ClassField>();
CLASS_FIELD_CACHE.set(fullName, map);
}
var ref = c;
while (ref != null) {
for (f in ref.fields.get()) {
map.set(f.name, f);
}
if (ref.superClass != null) {
ref = ref.superClass.t.get();
} else {
break;
}
}
}
// this is a _highly_ specialized version of "findField", that because we know
// that we are running inside haxeui can make so assumptions and so big perf
// improvements, most notably:
// * once we get to 'haxe.ui.core.ComponentCommon' we know we dont care anymore
// * we can cache all fields from 'haxe.ui.core.Component' and upwards
// this means that once we get to those classes we no longer need to
// do any iterations, this leads to _huge_ performance increases that
// that scale (around 30 times faster!)
static public function findFieldEx(c:ClassType, name:String):Null<ClassField> {
var fullName = c.pack.join(".") + "." + c.name;
if (fullName == "haxe.ui.core.Component") {
if (CLASS_FIELD_CACHE != null) {
var map = CLASS_FIELD_CACHE.get(fullName);
if (map != null) {
return map.get(name);
}
} else {
cacheClassFields(c);
}
}
var field = c.fields.get().find(function(field) return field.name == name);
if (fullName == "haxe.ui.core.ComponentCommon") {
return field;
}
var field = if (field != null) field; else if (c.superClass != null) findFieldEx(c.superClass.t.get(), name); else null;
return field;
}
public function hasField(name:String, recursive:Bool = false):Bool {
if (classType == null) {
return false;
}
if (recursive == true) {
return findFieldEx(classType, name) != null;
}
return (findField(name) != null);
}
public function hasFieldSuper(name:String):Bool {
if (classType == null || classType.superClass == null) {
return false;
}
var superClassType = classType.superClass.t.get();
return findFieldEx(superClassType, name) != null;
}
public function getFieldsWithMeta(meta:String):Array<FieldBuilder> {
var fs = [];
for (f in fields) {
for (m in f.meta) {
if (m.name == meta || m.name == ':${meta}') {
fs.push(new FieldBuilder(f, this));
}
}
}
return fs;
}
public function getFieldMetaValue(meta:String, paramIndex:Int = 0):String {
var v = null;
for (f in fields) {
for (m in f.meta) {
if (m.name == meta || m.name == ':${meta}') {
v = ExprTools.toString(m.params[paramIndex]);
}
}
}
return v;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Interfaces
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function hasInterface(interfaceRequired:String):Bool {
var has:Bool = false;
switch (type) {
case TInst(t, _):
while (t != null) {
for (i in t.get().interfaces) {
var interfaceName:String = i.t.toString();
if (interfaceName == interfaceRequired) {
has = true;
break;
}
}
if (has == false) {
if (t.get().superClass != null) {
t = t.get().superClass.t;
} else {
t = null;
}
} else {
break;
}
}
case TAbstract(t, _):
switch (t.get().type) {
case TInst(t, _):
while (t != null) {
for (i in t.get().interfaces) {
var interfaceName:String = i.t.toString();
if (interfaceName == interfaceRequired) {
has = true;
break;
}
}
if (has == false) {
if (t.get().superClass != null) {
t = t.get().superClass.t;
} else {
t = null;
}
} else {
break;
}
}
case _:
}
case _:
}
return has;
}
public function hasDirectInterface(interfaceRequired:String):Bool {
var has:Bool = false;
switch (type) {
case TInst(t, _):
for (i in t.get().interfaces) {
var interfaceName:String = i.t.toString();
if (interfaceName == interfaceRequired) {
has = true;
break;
}
}
case TAbstract(t, _):
switch (t.get().type) {
case TInst(t, _):
for (i in t.get().interfaces) {
var interfaceName:String = i.t.toString();
if (interfaceName == interfaceRequired) {
has = true;
break;
}
}
case _:
}
case _:
}
return has;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Hierarchy
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function hasSuperClass(classRequired:String):Bool {
var has:Bool = false;
switch (type) {
case TInst(t, _):
if (t.toString() == classRequired) {
has = true;
} else {
while (t != null) {
if (t.get().superClass != null) {
t = t.get().superClass.t;
if (t.toString() == classRequired) {
has = true;
break;
}
} else {
t = null;
}
}
}
case TAbstract(t, _):
switch (t.get().type) {
case TInst(t, _):
if (t.toString() == classRequired) {
has = true;
} else {
while (t != null) {
if (t.get().superClass != null) {
t = t.get().superClass.t;
if (t.toString() == classRequired) {
has = true;
break;
}
} else {
t = null;
}
}
}
case _:
}
case _:
}
return has;
}
public var superClass(get, null):Null<{ t:Ref<ClassType>, params:Array<haxe.macro.Type> }>;
private function get_superClass():Null<{ t:Ref<ClassType>, params:Array<haxe.macro.Type> }> {
var superClass:Null<{ t:Ref<ClassType>, params:Array<haxe.macro.Type> }> = null;
switch (type) {
case TInst(t, _):
superClass = t.get().superClass;
case TAbstract(t, _):
switch (t.get().type) {
case TInst(t, _):
superClass = t.get().superClass;
case _:
}
case _:
}
return superClass;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Meta
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function hasClassMeta(items:Array<String>):Bool {
var r = false;
for (item in items) {
if (classType.meta.has(item) == true || classType.meta.has(':${item}') == true) {
r = true;
break;
}
}
return r;
}
public function addMeta(name:String, params:Array<Expr> = null) {
if (params == null) {
params = [];
}
classType.meta.add(name, params, pos);
}
public function getClassMeta(name:String, index:Int = 0):MetadataEntry {
if (hasClassMeta([name]) == false) {
throw 'Meta not found: ${name}';
}
var meta = null;
if (classType.meta.has(name)) {
meta = classType.meta.extract(name);
} else if (classType.meta.has(':${name}')) {
meta = classType.meta.extract(':${name}');
}
return meta[index];
}
public function getClassMetaValues(name:String, index:Int = 0):Array<Dynamic> {
var values = [];
var meta = getClassMeta(name);
for (p in meta.params) {
values.push(metaParam(p));
}
return values;
}
public function getClassMetaValue(name:String, index:Int = 0, paramIndex:Int = 0):Dynamic {
var meta = getClassMeta(name);
var param = meta.params[paramIndex];
var v = metaParam(param);
return v;
}
private function metaParam(param:Expr):Dynamic {
var v = null;
switch (param.expr) {
case EConst(CString(str)):
v = str;
case EConst(CIdent(str)):
v = str;
case _:
v = ExprTools.toString(param);
}
return v;
}
public function hasFieldMeta(f:Field, items:Array<String>):Bool {
var r = false;
for (item in items) {
for (m in f.meta) {
if (m.name == item || m.name == ':${item}') {
r = true;
break;
}
}
}
return r;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Vars
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function findVar(name:String):VarBuilder {
var v = null;
for (f in fields) {
if (f.name == name) {
switch (f.kind) {
case FVar(_):
v = new VarBuilder(f, this);
break;
default:
}
}
}
return v;
}
public function addVar(name:String, t:ComplexType, e:Expr = null, access:Array<Access> = null, meta:Metadata = null):Field {
if (access == null) {
if (StringTools.startsWith(name, "_")) {
access = [APrivate];
} else {
access = [APublic];
}
}
if (meta == null) {
meta = [];
}
var newField = {
name: name,
doc: null,
meta: meta,
access: access,
kind : FVar(t, e),
pos : pos
}
fields.push(newField);
return newField;
}
public function addProp(name:String, t:ComplexType, e:Expr = null, get:String = null, set:String = null, access:Array<Access> = null, meta:Metadata = null):Field {
if (access == null) {
if (StringTools.startsWith(name, "_")) {
access = [APrivate];
} else {
access = [APublic];
}
}
if (meta == null) {
meta = [];
}
var newField = {
name: name,
doc: null,
meta: meta,
access: access,
kind : FProp(get, set, t, e),
pos : pos
}
fields.push(newField);
return newField;
}
public function hasVar(name:String):Bool {
return (findVar(name) != null);
}
public function removeVar(name:String) {
var v = null;
for (f in fields) {
if (f.name == name) {
switch (f.kind) {
case FVar(_):
v = f;
break;
default:
}
}
}
if (v != null) {
fields.remove(v);
}
}
public function getVarsWithMeta(meta:String):Array<VarBuilder> {
var vars = [];
for (f in fields) {
switch (f.kind) {
case FVar(v):
for (m in f.meta) {
if (m.name == meta || m.name == ':${meta}') {
vars.push(new VarBuilder(f, this));
}
}
default:
}
}
return vars;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Properties
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function addGetter(name:String, t:ComplexType, e:Expr, access:Array<Access> = null, addVar:Bool = true, isOverride:Bool = false):FieldBuilder {
if (e == null) {
e = macro {}
}
if (access == null) {
if (StringTools.startsWith(name, "_")) {
access = [APrivate];
} else {
access = [APublic];
}
}
if (addVar == true) {
var field:Field = findField(name);
if (field == null) {
field = {
name: name,
doc: null,
meta: [],
access: access,
kind: FProp("get", "null", t),
pos: pos
}
fields.push(field);
} else {
var newKind;
switch (field.kind) {
case FProp(existingGet, existingSet, existingType, existingExpr):
newKind = FProp("get", existingSet, existingType, existingExpr);
case _:
}
#if macro
field.kind = newKind;
#end
}
}
var fn = findFunction('get_${name}');
if (fn == null) {
var access = [APrivate];
if (isOverride == true) {
access.push(AOverride);
}
fn = addFunction('get_${name}', e, t, access);
} else {
fn.fn.expr = e;
}
return new FieldBuilder(findField(name), this);
}
public function addSetter(name:String, t:ComplexType, e:Expr, access:Array<Access> = null, paramName:String = "value", addVar:Bool = true, isOverride:Bool = false):FieldBuilder {
if (e == null) {
e = macro {
return value;
}
}
if (access == null) {
if (StringTools.startsWith(name, "_")) {
access = [APrivate];
} else {
access = [APublic];
}
}
if (addVar == true) {
var field:Field = findField(name);
if (field == null) {
field = {
name: name,
doc: null,
meta: [],
access: access,
kind: FProp("null", "set", t),
pos: pos
}
fields.push(field);
} else {
var newKind;
switch (field.kind) {
case FProp(existingGet, existingSet, existingType, existingExpr):
newKind = FProp(existingGet, "set", existingType, existingExpr);
case _:
}
#if macro
field.kind = newKind;
#end
}
}
var fn = findFunction('set_${name}');
if (fn == null) {
var access = [APrivate];
if (isOverride == true) {
access.push(AOverride);
}
fn = addFunction('set_${name}', e, [{ name: paramName, type: t }], t, access);
} else {
fn.fn.expr = e;
}
return new FieldBuilder(findField(name), this);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Functions
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public var ctor(get, null):FunctionBuilder;
private function get_ctor():FunctionBuilder {
return findFunction("new");
}
public function addFunction(name:String, e:Expr = null, args:Array<FunctionArg> = null, r:ComplexType = null, access:Array<Access> = null):FunctionBuilder {
if (e == null) {
e = macro {}
}
if (access == null) {
if (StringTools.startsWith(name, "_")) {
access = [APrivate];
} else {
access = [APublic];
}
}
if (args == null) {
args = [];
}
var newField:Field = {
name: name,
doc: null,
meta: [],
access: access,
kind: FFun({
params : [],
args : args,
expr: e,
ret : r
}),
pos: pos
}
fields.push(newField);
return findFunction(name);
}
public function findFunction(name:String):FunctionBuilder {
var fn = null;
for (f in fields) {
if (f.name == name) {
switch (f.kind) {
case FFun(ff):
fn = new FunctionBuilder(f, ff);
break;
default:
}
}
}
return fn;
}
public function findFunctionsWithMeta(meta:String):Array<FunctionBuilder> {
var fns:Array<FunctionBuilder> = [];
for (f in fields) {
if (hasFieldMeta(f, [meta])) {
switch (f.kind) {
case FFun(fn):
fns.push(new FunctionBuilder(f, fn));
default:
}
}
}
return fns;
}
public function hasFunction(name:String):Bool {
return (findFunction(name) != null);
}
public function removeFunction(name:String) {
var fn = null;
for (f in fields) {
if (f.name == name) {
switch (f.kind) {
case FFun(_):
fn = f;
break;
default:
}
}
}
if (fn != null) {
fields.remove(fn);
}
}
public function addToFunction(name:String, e:Expr = null, cb:CodeBuilder = null, where:CodePos = null) {
if (where == null) {
where = CodePos.End;
}
var fn = findFunction(name);
if (fn == null) {
throw 'Could not find function: ${name}';
}
fn.add(e, where);
}
}