haxe/ui/macros/ExternGenerator.hx
package haxe.ui.macros;
import haxe.macro.Expr.TypeParam;
import haxe.macro.Context;
import haxe.macro.TypeTools;
import sys.io.File;
import sys.FileSystem;
import haxe.io.Path;
import haxe.macro.ExprTools;
import haxe.macro.Type;
using StringTools;
/*
current usage: --macro haxe.ui.macros.ExternGenerator.generate('output/path/to/externs')
if you want to generate externs for everything, use:
--macro include('haxe.ui.components')
--macro include('haxe.ui.containers')
--macro include('haxe.ui.containers.dialogs')
--macro include('haxe.ui.containers.menus')
--macro include('haxe.ui.containers.properties')
*/
class ExternGenerator {
#if macro
private static var outputPath:String;
public static function generate(outputPath:String = null) {
if (outputPath == null) {
outputPath = "externs";
}
ExternGenerator.outputPath = outputPath;
haxe.macro.Context.onGenerate(onTypesGenerated);
}
private static function onTypesGenerated(types:Array<Type>) {
generateExterns(types);
}
private static function useType(fullPath:String):Bool {
if (fullPath.startsWith("haxe.ui")) {
return true;
}
return false;
}
private static function generateExterns(types:Array<Type>) {
var modules:Map<String, Array<Type>> = [];
function addType(module:String, type:Type) {
var types = modules.get(module);
if (types == null) {
types = [];
modules.set(module, types);
}
types.push(type);
}
for (t in types) {
switch (t) {
case TInst(classType, params):
if (!classType.get().isPrivate && useType(classType.toString())) {
addType(classType.get().module, t);
}
case TAbstract(abstractType, params):
if (!abstractType.get().isPrivate && useType(abstractType.toString())) {
addType(abstractType.get().module, t);
}
case TEnum(enumType, params):
if (!enumType.get().isPrivate && useType(enumType.toString())) {
addType(enumType.get().module, t);
}
case TType(defType, params):
if (!defType.get().isPrivate && useType(defType.toString())) {
addType(defType.get().module, t);
}
case _:
trace("UNKNOWN: ", t);
}
}
for (module in modules.keys()) {
generateModule(module, modules.get(module));
}
// were going to copy the originals here so macros will work with the externs
copyOriginals("haxe.ui.macros");
copyOriginals("haxe.ui.parsers");
copyOriginal("haxe.ui.core.ComponentClassMap");
copyOriginal("haxe.ui.core.ComponentFieldMap");
copyOriginal("haxe.ui.core.TypeMap");
copyOriginal("haxe.ui.util.EventInfo");
copyOriginal("haxe.ui.util.StringUtil");
copyOriginal("haxe.ui.util.TypeConverter");
copyOriginal("haxe.ui.util.SimpleExpressionEvaluator");
copyOriginal("haxe.ui.util.RTTI");
copyOriginal("haxe.ui.util.ExpressionUtil");
copyOriginal("haxe.ui.util.Defines");
copyOriginal("haxe.ui.util.GenericConfig");
copyOriginal("haxe.ui.util.Variant");
copyOriginal("haxe.ui.util.Listener");
copyOriginal("haxe.ui.Backend");
copyBackendOriginal("haxe.ui.backend.BackendImpl");
copyOriginal("haxe.ui.layouts.LayoutFactory");
copyOriginal("haxe.ui.data.DataSourceFactory");
//copyOriginal("haxe.ui.core.IEventDispatcher");
copyOriginals("haxe.ui.constants");
var moduleSourcePath = Path.normalize(rootDir() + "/haxe/ui/module.xml");
var moduleDestPath = Path.normalize(outputPath + "/haxe/ui/module.xml");
File.copy(moduleSourcePath, moduleDestPath);
}
private static function generateModule(module:String, types:Array<Type>) {
//trace(module);
var sb = new StringBuf();
sb.add('// generated file\n');
sb.add('package ');
sb.add(extractPackage({module: module}));
sb.add(';');
sb.add('\n\n');
for (t in types) {
switch (t) {
case TInst(classType, params):
if (classType.get().isInterface) {
generateInterface(classType.get(), sb);
} else {
generateExternClass(classType.get(), sb);
}
case TAbstract(abstractType, params):
generateAbstract(abstractType.get(), sb);
case TEnum(enumType, params):
generateEnum(enumType.get(), sb);
case TType(defType, params):
generateTypeDef(defType.get(), sb);
case _:
trace("UNKNOWN: ", t);
}
}
var filename = Path.normalize(outputPath + "/" + module.replace(".", "/") + ".hx");
writeFile(filename, sb);
}
private static function generateExternClass(classType:ClassType, sb:StringBuf) {
var fullName = buildFullName(classType);
if (fullName == "haxe.ui.backend.ComponentBase") {
sb.add('@:build(haxe.ui.macros.Macros.buildBehaviours())\n');
sb.add('@:autoBuild(haxe.ui.macros.Macros.buildBehaviours())\n');
sb.add('@:build(haxe.ui.macros.Macros.build())\n');
sb.add('@:autoBuild(haxe.ui.macros.Macros.build())\n');
}
if (classType.isPrivate) {
sb.add('private ');
}
sb.add('extern ');
sb.add('class ');
sb.add(buildName(classType));
if (classType.superClass != null) {
sb.add(' extends ');
sb.add(buildName(classType.superClass.t.get(), true));
}
if (classType.interfaces != null && classType.interfaces.length > 0) {
for (i in classType.interfaces) {
sb.add(' implements ');
sb.add(i.t.toString());
if (i.t.toString() == "haxe.ui.core.IClonable") {
sb.add('<');
sb.add(fullName);
sb.add('>');
}
}
}
sb.add(' {');
sb.add('\n');
if (classType.constructor != null) {
generateClassField(classType, classType.constructor.get(), sb);
}
for (f in classType.fields.get()) {
generateClassField(classType, f, sb);
}
for (f in classType.statics.get()) {
generateClassField(classType, f, sb, true);
}
sb.add('}');
sb.add('\n\n');
}
private static function generateInterface(classType:ClassType, sb:StringBuf) {
sb.add('interface ');
sb.add(buildName(classType));
if (classType.superClass != null) {
sb.add(' extends ');
sb.add(buildName(classType.superClass.t.get(), true));
}
if (classType.interfaces != null && classType.interfaces.length > 0) {
for (i in classType.interfaces) {
sb.add(' implements ');
sb.add(i.t.toString());
}
}
sb.add(' {');
sb.add('\n');
if (classType.constructor != null) {
generateClassField(classType, classType.constructor.get(), sb);
}
for (f in classType.fields.get()) {
if (f.name.startsWith("get_") || f.name.startsWith("set_")) {
continue;
}
generateClassField(classType, f, sb);
}
for (f in classType.statics.get()) {
if (f.name.startsWith("get_") || f.name.startsWith("set_")) {
continue;
}
generateClassField(classType, f, sb, true);
}
sb.add('}');
sb.add('\n\n');
}
private static function generateClassField(classType:{module:String, name:String}, field:ClassField, sb:StringBuf, isStatic:Bool = false, allowMethods:Bool = true, allowGettersSetters:Bool = true) {
if (field.name.startsWith("_")) {
return;
}
switch (field.kind) {
case FVar(AccNormal, AccNormal) | FVar(AccNormal, AccNo) | FVar(AccInline, AccNever) | FVar(AccNormal, AccNever): // var
generateVar(field.name, buildFullName(classType), field.type, sb, isStatic, !field.isPublic);
case FVar(AccCall, AccCall) | FVar(AccNormal, AccCall): // get / set
if (allowGettersSetters) {
generateGetterSetter(field.name, buildFullName(classType), field.type, sb, isStatic, !field.isPublic);
} else {
generateVar(field.name, buildFullName(classType), field.type, sb, isStatic, !field.isPublic);
}
case FVar(AccNo, AccCall): // null / set
if (allowGettersSetters) {
generateSetter(field.name, buildFullName(classType), field.type, sb, isStatic, !field.isPublic);
} else {
generateVar(field.name, buildFullName(classType), field.type, sb, isStatic, !field.isPublic);
}
case FVar(AccCall, AccNo) | FVar(AccCall, AccNever): // set / null
if (allowGettersSetters) {
generateGetter(field.name, buildFullName(classType), field.type, sb, isStatic, !field.isPublic);
} else {
generateVar(field.name, buildFullName(classType), field.type, sb, isStatic, !field.isPublic);
}
case FMethod(k):
if (allowMethods) {
generateMethod(field.name, buildFullName(classType), field.type, field.params, k, sb, isStatic, !field.isPublic);
}
case _:
}
}
private static function generateAbstract(abstractType:AbstractType, sb:StringBuf) {
if (abstractType.meta.has(":enum")) {
sb.add('enum ');
}
sb.add('abstract ');
sb.add(buildName(abstractType));
sb.add('(');
sb.add(typeToString(abstractType.type));
sb.add(') ');
if (abstractType.from.length > 0) {
var list = [];
for (f in abstractType.from) {
if (f.field != null) {
continue;
}
list.push('from ' + typeToString(f.t));
}
sb.add(list.join(' '));
sb.add(' ');
}
if (abstractType.to.length > 0) {
var list = [];
for (f in abstractType.to) {
list.push('to ' + typeToString(f.t));
}
sb.add(list.join(' '));
sb.add(' ');
}
sb.add('{');
sb.add('\n');
if (abstractType.impl != null) {
var classType = abstractType.impl.get();
for (f in classType.statics.get()) {
generateClassField(classType, f, sb, true, false, false);
}
}
sb.add('}');
sb.add('\n\n');
}
private static function generateEnum(enumType:EnumType, sb:StringBuf) {
sb.add('enum ');
sb.add(buildName(enumType));
sb.add(' {');
sb.add('\n');
for (name in enumType.names) {
sb.add(' ');
sb.add(name);
sb.add(';');
sb.add('\n');
}
sb.add('}');
sb.add('\n\n');
}
private static function generateTypeDef(defType:DefType, sb:StringBuf) {
sb.add('typedef ');
sb.add(buildName(defType));
sb.add(' = {');
sb.add('\n');
switch (defType.type) {
case TAnonymous(a):
for (f in a.get().fields) {
generateClassField({module: defType.module, name: f.name}, f, sb);
}
case _:
}
sb.add('}');
sb.add('\n\n');
}
private static function generateVar(name:String, className:String, type:Type, sb:StringBuf, isStatic:Bool = false, isPrivate:Bool = false) {
sb.add(' ');
if (isPrivate) {
sb.add('private ');
} else {
sb.add('public ');
}
if (isStatic) {
sb.add('static ');
}
sb.add('var ');
sb.add(name);
sb.add(':');
sb.add(typeToString(type, [name, className]));
sb.add(";");
sb.add("\n");
}
private static function generateGetter(name:String, className:String, type:Type, sb:StringBuf, isStatic:Bool = false, isPrivate:Bool = false) {
sb.add(' ');
if (isPrivate) {
sb.add('private ');
} else {
sb.add('public ');
}
if (isStatic) {
sb.add('static ');
}
sb.add('var ');
sb.add(name);
sb.add('(get, null)');
sb.add(':');
sb.add(typeToString(type, [name, className]));
sb.add(";");
sb.add("\n");
}
private static function generateSetter(name:String, className:String, type:Type, sb:StringBuf, isStatic:Bool = false, isPrivate:Bool = false) {
sb.add(' ');
if (isPrivate) {
sb.add('private ');
} else {
sb.add('public ');
}
if (isStatic) {
sb.add('static ');
}
sb.add('var ');
sb.add(name);
sb.add('(null, set)');
sb.add(':');
sb.add(typeToString(type, [name, className]));
sb.add(";");
sb.add("\n");
}
private static function generateGetterSetter(name:String, className:String, type:Type, sb:StringBuf, isStatic:Bool = false, isPrivate:Bool = false) {
sb.add(' ');
if (isPrivate) {
sb.add('private ');
} else {
sb.add('public ');
}
if (isStatic) {
sb.add('static ');
}
sb.add('var ');
sb.add(name);
sb.add('(get, set)');
sb.add(':');
sb.add(typeToString(type, [name, className]));
sb.add(";");
sb.add("\n");
}
private static function generateMethod(name:String, className:String, type:Type, params:Array<TypeParameter>, k:MethodKind, sb:StringBuf, isStatic:Bool = false, isPrivate:Bool = false) {
var methodArgs = null;
var methodReturn = null;
switch (type) {
case TFun(args, ret):
methodArgs = args;
methodReturn = ret;
case _:
}
sb.add(' ');
if (isPrivate) {
sb.add('private ');
} else {
sb.add('public ');
}
if (isStatic) {
sb.add('static ');
}
sb.add('function ');
sb.add(name);
sb.add(buildTypeParams(params));
sb.add('(');
var argList = [];
var n = 0;
for (arg in methodArgs) {
if (arg.name == "_") {
argList.push("arg" + n + ":Any");
continue;
}
if (arg.opt) {
argList.push('?${arg.name}:' + typeToString(arg.t, [name, className]));
} else {
argList.push('${arg.name}:' + typeToString(arg.t, [name, className]));
}
n++;
}
sb.add(argList.join(", "));
sb.add(')');
sb.add(':');
sb.add(typeToString(methodReturn, [name, className]));
sb.add(";");
sb.add("\n");
}
private static function typeToString(type:Type, replacements:Array<String> = null) {
if (replacements == null) {
replacements = [];
}
replacements.push("StdTypes");
var s = TypeTools.toString(type);
switch (type) {
case TInst(t, params):
var classType = t.get();
s = buildFullName(classType);
var paramList = [];
if (params.length > 0) {
for (p in params) {
paramList.push(typeToString(p, replacements));
}
}
if (paramList.length > 0) {
s += "<";
s += paramList.join(", ");
s += ">";
}
case TAbstract(t, params):
var abstractType = t.get();
s = buildFullName(abstractType);
//s = t.toString();
var paramList = [];
if (params.length > 0) {
for (p in params) {
paramList.push(typeToString(p, replacements));
}
}
if (paramList.length > 0) {
s += "<";
s += paramList.join(", ");
s += ">";
}
case TType(t, params):
var typeType = t.get();
s = buildFullName(typeType);
//s = t.toString();
var paramList = [];
if (params.length > 0) {
for (p in params) {
paramList.push(typeToString(p, replacements));
}
}
if (paramList.length > 0) {
s += "<";
s += paramList.join(", ");
s += ">";
}
case _:
}
if (replacements != null) {
for (r in replacements) {
if (r != "validators" && r != "behaviours") { // filty hack
s = s.replace(r + ".", "");
}
}
}
return s;
}
////////////////////////////////////////////////////////////////////////////////////
// util functions
////////////////////////////////////////////////////////////////////////////////////
private static function writeFile(filename:String, data:StringBuf) {
var stringData = data.toString();
var parts = filename.split("/");
parts.pop();
var path = parts.join("/");
FileSystem.createDirectory(path);
File.saveContent(filename, stringData);
}
private static function buildName(type:{module:String, name:String, params:Array<TypeParameter>}, full:Bool = false):String {
var sb = new StringBuf();
if (full) {
sb.add(buildFullName(type));
} else {
sb.add(type.name);
}
sb.add(buildTypeParams(type.params));
return sb.toString();
}
private static function buildTypeParams(params:Array<TypeParameter>) {
if (params == null || params.length == 0) {
return "";
}
var sb = new StringBuf();
sb.add('<');
var list = [];
for (p in params) {
if (p.defaultType != null) {
list.push(p.name + ":" + TypeTools.toString(p.defaultType));
} else {
list.push(p.name);
}
}
sb.add(list.join(", "));
sb.add('>');
return sb.toString();
}
private static function buildFullName(info:{module:String, name:String}):String {
var fullName = info.module;
if (!fullName.endsWith(info.name)) {
fullName += "." + info.name;
}
return fullName;
}
private static function extractPackage(info:{module:String}):String {
var pack = info.module.split(".");
pack.pop();
return pack.join(".");
}
////////////////////////////////////////////////////////////////////////////////////
// path functions
////////////////////////////////////////////////////////////////////////////////////
private static function rootDir():String {
var root = Path.normalize(Context.resolvePath("haxe/ui/Toolkit.hx"));
var parts = root.split("/");
parts.pop();
parts.pop();
parts.pop();
return Path.normalize(parts.join("/"));
}
private static function backendRootDir():String {
var root = Path.normalize(Context.resolvePath("haxe/ui/backend/ToolkitOptions.hx"));
var parts = root.split("/");
parts.pop();
parts.pop();
parts.pop();
parts.pop();
return Path.normalize(parts.join("/"));
}
////////////////////////////////////////////////////////////////////////////////////
// copy functions
////////////////////////////////////////////////////////////////////////////////////
private static function copyOriginals(pkg:String) {
var fullSourcePath = Path.normalize(rootDir() + "/" + pkg.replace(".", "/"));
var fullDestPath = Path.normalize(outputPath + "/" + pkg.replace(".", "/"));
copyDir(fullSourcePath, fullDestPath);
}
private static function copyOriginal(className:String) {
var fullSourcePath = Path.normalize(rootDir() + "/" + className.replace(".", "/") + ".hx");
var fullDestPath = Path.normalize(outputPath + "/" + className.replace(".", "/") + ".hx");
File.copy(fullSourcePath, fullDestPath);
}
private static function copyBackendOriginal(className:String) {
var fullSourcePath = Path.normalize(backendRootDir() + "/" + className.replace(".", "/") + ".hx");
var fullDestPath = Path.normalize(outputPath + "/" + className.replace(".", "/") + ".hx");
File.copy(fullSourcePath, fullDestPath);
}
public static function copyDir(source:String, dest:String) {
if (!FileSystem.exists(dest)) {
FileSystem.createDirectory(dest);
}
var contents = FileSystem.readDirectory(source);
for (item in contents) {
var fullSourcePath = Path.normalize(source + "/" + item);
var fullDestPath = Path.normalize(dest + "/" + item);
if (FileSystem.isDirectory(fullSourcePath)) {
copyDir(fullSourcePath, fullDestPath);
} else {
File.copy(fullSourcePath, fullDestPath);
}
}
}
#end
}