Last active
June 24, 2021 13:40
-
-
Save andyli/5011520 to your computer and use it in GitHub Desktop.
Java abstract class implemented in Haxe macros.
Related: https://groups.google.com/forum/?fromgroups=#!topic/haxelang/WzeI-N1XbIg
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import haxe.macro.Context; | |
import haxe.macro.Expr; | |
import haxe.macro.Type; | |
using Lambda; | |
/** | |
Old school abstract class. | |
Classes that implements it, and their sub-classes, will be able to declare abstract methods (methods that without body). | |
There will be a check in compile-time such that no public constructor is allowed without all abstract methods implemented. | |
*/ | |
@:autoBuild(AbstractClassBuilder.build()) | |
interface AbstractClass {} | |
private typedef StringMap<T> = #if haxe3 Map<String, T>; #else Hash<T>; #end | |
class AbstractClassBuilder { | |
#if macro | |
/** | |
The without-body declared methods of an AbstractClass. | |
Uses fully-qualified class names as keys, stores StringMaps of method name keys. | |
Filled in by build() called by @:autoBuild. | |
*/ | |
static var declAbstracts:StringMap<StringMap<Bool>> = new StringMap(); | |
/** | |
All abstract methods, including the inherited ones, of an AbstractClass. | |
Uses fully-qualified class names as keys, stores StringMaps of method name keys. | |
Filled in by checkAbstractClass() during onGenerate. | |
*/ | |
static var allAbstracts:StringMap<StringMap<Bool>> = new StringMap(); | |
/** | |
The remainning AbstractClasses that need to be processed by checkAbstractClass(). | |
Uses fully-qualified class names as keys. | |
Filled in during onGenerate. | |
*/ | |
static var typesToProcess:StringMap<ClassType> = new StringMap(); | |
/** | |
Returns fully-qualified class name. | |
Used to get a key for StringMap. | |
*/ | |
static function getFullClassName(cls:ClassType):String { | |
return (cls.pack.length > 0 ? cls.pack.join(".") + "." : "") + cls.name; | |
} | |
static function __init__():Void { | |
Context.onGenerate(function (types:Array<Type>):Void { | |
for (t in types) { | |
switch (t) { | |
case TInst(clsr, _): | |
var clsName = getFullClassName(clsr.get()); | |
if (declAbstracts.exists(clsName)) { | |
typesToProcess.set(clsName, clsr.get()); | |
} | |
default: | |
} | |
} | |
while (!typesToProcess.empty()) { | |
checkAbstractClass(typesToProcess.iterator().next()); | |
} | |
}); | |
} | |
static function checkAbstractClass(cls:ClassType):Void { | |
var allAbsFields:StringMap<Bool> = new StringMap(); | |
var clsName = getFullClassName(cls); | |
allAbstracts.set(clsName, allAbsFields); | |
//copy the declared ones | |
var absFields = declAbstracts.get(clsName); | |
for (k in absFields.keys()) { | |
allAbsFields.set(k, true); | |
} | |
if (cls.superClass != null && declAbstracts.exists(getFullClassName(cls.superClass.t.get()))) { | |
//Super class is AbstractClass. | |
var supClsName = getFullClassName(cls.superClass.t.get()); | |
if (!allAbstracts.exists(supClsName)) { | |
checkAbstractClass(cls.superClass.t.get()); | |
} | |
//copy the inherited ones | |
var supallAbsFields = allAbstracts.get(supClsName); | |
for (k in supallAbsFields.keys()) { | |
allAbsFields.set(k, true); | |
} | |
var fields = cls.fields.get(); | |
for (field in fields) { | |
if (!absFields.exists(field.name) && allAbsFields.exists(field.name)) { | |
allAbsFields.remove(field.name); | |
} | |
} | |
} | |
if (!allAbsFields.empty() && cls.constructor != null && cls.constructor.get().isPublic) { | |
var missingFields = []; | |
for (k in allAbsFields.keys()) missingFields.push(k); | |
Context.error("Abstract class cannot have public constructor. Missing implementation of: " + missingFields.join(", "), cls.pos); | |
} | |
typesToProcess.remove(clsName); | |
} | |
static public function build():Array<Field> { | |
var fields = Context.getBuildFields(); | |
var cls = Context.getLocalClass().get(); | |
var clsName = getFullClassName(cls); | |
if (declAbstracts.exists(clsName)){ | |
//The class has been built already. | |
//It occurs when a class extends an AbstractClass, and it also implements AbstractClass. | |
return fields; | |
} | |
if (cls.isInterface) { | |
return fields; | |
} | |
var absFields = new StringMap(); | |
declAbstracts.set(clsName, absFields); | |
for (f in fields) { | |
if (f.access.has(AStatic)) continue; //we only care non-static methods | |
switch (f.kind) { | |
case FFun(fun): | |
if (fun.expr == null) { //method without an implementation | |
absFields.set(f.name, true); | |
//give a method body so it compiles | |
fun.expr = if (fun.ret == null || switch (fun.ret) { case TPath(p): p.pack.length == 0 && p.name == "Void"; default: false; }) | |
macro throw "abstract method, must override"; | |
else | |
macro return throw "abstract method, must override"; | |
} | |
default: | |
} | |
} | |
return fields; | |
} | |
#end | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Color implements AbstractClass { | |
public function toRGBX():RGBX; | |
public function clone():Color; | |
} | |
class RGBX { | |
public function new(){} | |
} | |
class HSB extends Color { //src/Main.hx:11: lines 11-13 : Abstract class cannot have public constructor. Missing implementation of: toRGBX, clone | |
public function new(){} | |
} | |
class HSL extends Color { | |
public function new(){} | |
override public function toRGBX():RGBX { | |
return new RGBX(); | |
} | |
override public function clone():HSL { | |
return new HSL(); | |
} | |
} | |
class Main { | |
static public function main() { | |
} | |
} |
Thanks, very nice! Is this implemented in a haxelib/github library somewhere?
Hi Andy ! Did I do something wrong ? AbstractClass.hx:14: characters 2-11 : Type not found : AbstractClassBuilder
Sorry, my bad. I didn't switch to the right package.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
As of revision 9, it should be compatible with other macro libs (eg. tink), since the check is moved to onGenerate.