Fixed not being able to do "+=" on Custom Classes and getters and setters on constructor. Also, fixed not being able to call functions from a CustomClass super class.
477 lines
14 KiB
Haxe
477 lines
14 KiB
Haxe
package hscript.customclass;
|
|
|
|
import hscript.utils.UnsafeReflect;
|
|
import haxe.Constraints.Function;
|
|
import hscript.Expr;
|
|
import hscript.Expr.VarDecl;
|
|
import hscript.Expr.FunctionDecl;
|
|
import hscript.Expr.FieldDecl;
|
|
|
|
using Lambda;
|
|
using StringTools;
|
|
|
|
/**
|
|
* The Custom Class core.
|
|
*
|
|
* Provides handlers for custom classes.
|
|
*
|
|
* Rest in peace, CustomClassHandler.hx
|
|
*
|
|
* Based on Polymod Hscript class system
|
|
* @see https://github.com/larsiusprime/polymod/tree/master/polymod/hscript/_internal
|
|
*/
|
|
@:access(hscript.customclass.CustomClassDecl)
|
|
class CustomClass implements IHScriptCustomAccessBehaviour {
|
|
public var interp:Interp;
|
|
|
|
public var superClass:Dynamic;
|
|
public var superConstructor(default, null):Dynamic;
|
|
|
|
public var className(get, never):String;
|
|
private function get_className():String {
|
|
return __class.toString();
|
|
}
|
|
|
|
private var __class:CustomClassDecl;
|
|
private var __cachedSuperFields:Null<Map<String, Dynamic>> = null;
|
|
|
|
private var __cachedFieldDecls:Map<String, FieldDecl> = null;
|
|
private var __cachedFunctionDecls:Map<String, FunctionDecl> = null;
|
|
private var __cachedVarDecls:Map<String, VarDecl> = null;
|
|
|
|
public var __allowSetGet:Bool = false;
|
|
|
|
private var isInline:Bool = false;
|
|
private var ogVariables:Map<String, Dynamic>;
|
|
|
|
public function new(__class:CustomClassDecl, args:Array<Dynamic>, ?extendFieldDecl:Map<String, Dynamic>, ?ogInterp:Interp, ?callNew:Bool = true) {
|
|
this.__class = __class;
|
|
this.interp = new Interp(this);
|
|
|
|
if (ogInterp != null) {
|
|
interp.importFailedCallback = ogInterp.importFailedCallback;
|
|
interp.errorHandler = ogInterp.errorHandler;
|
|
interp.allowStaticVariables = ogInterp.allowStaticVariables;
|
|
interp.staticVariables = ogInterp.staticVariables;
|
|
// todo: make it so you can use variables from the same scope as where the class was defined
|
|
if(__class.isInline != null && __class.isInline) {
|
|
isInline = __class.isInline;
|
|
ogVariables = ogInterp.variables;
|
|
interp.allowPublicVariables = __class.staticInterp.allowPublicVariables;
|
|
interp.publicVariables = __class.staticInterp.publicVariables;
|
|
}
|
|
}
|
|
|
|
buildImports();
|
|
buildUsings();
|
|
|
|
if (extendFieldDecl != null)
|
|
__cachedSuperFields = extendFieldDecl;
|
|
|
|
buildClass();
|
|
|
|
if (hasFunction('new') && callNew) {
|
|
buildSuperConstructor();
|
|
callFunction('new', args);
|
|
if (this.superClass == null && this.__class.classDecl.extend != null)
|
|
this.interp.error(ECustom("super() not called"));
|
|
} else if (__class.classDecl.extend != null) {
|
|
createSuperClass(args);
|
|
}
|
|
}
|
|
|
|
function buildClass() {
|
|
__cachedFieldDecls = [];
|
|
__cachedFunctionDecls = [];
|
|
__cachedVarDecls = [];
|
|
|
|
if (__cachedSuperFields == null)
|
|
__cachedSuperFields = [];
|
|
|
|
for (f in __class.classDecl.fields) {
|
|
if (f.access.contains(AStatic))
|
|
continue; // Skip static field. It's handled by CustomClassDecl.hx
|
|
__cachedFieldDecls.set(f.name, f);
|
|
switch (f.kind) {
|
|
case KFunction(fn):
|
|
if(f.name.startsWith('set_') || f.name.startsWith('get_'))
|
|
__allowSetGet = true;
|
|
__cachedFunctionDecls.set(f.name, fn);
|
|
#if hscriptPos
|
|
var fexpr:Expr = {
|
|
e: ExprDef.EFunction(fn.args, fn.body, f.name, fn.ret, false, false),
|
|
pmin: fn.body.pmin,
|
|
pmax: fn.body.pmax,
|
|
line: fn.body.line,
|
|
origin: fn.body.origin
|
|
};
|
|
#else
|
|
var fexpr = Expr.EFunction(fn.args, fn.body, f.name, fn.ret, false, false);
|
|
#end
|
|
var f0 = this.interp.expr(fexpr);
|
|
this.interp.variables.set(f.name, f0);
|
|
case KVar(v):
|
|
__cachedVarDecls.set(f.name, v);
|
|
if (v.expr != null) {
|
|
var varValue = this.interp.expr(v.expr);
|
|
this.interp.variables.set(f.name, varValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!__cachedSuperFields.empty()) {
|
|
for (f => v in __cachedSuperFields) {
|
|
this.hset(f, v);
|
|
}
|
|
__cachedSuperFields.clear();
|
|
}
|
|
}
|
|
|
|
function buildSuperConstructor() {
|
|
superConstructor = Reflect.makeVarArgs(function(args:Array<Dynamic>) {
|
|
createSuperClass(args);
|
|
});
|
|
}
|
|
|
|
private function createSuperClass(?args:Array<Dynamic>) {
|
|
if (args == null)
|
|
args = [];
|
|
|
|
if(__class.superClassDecl is CustomClassDecl)
|
|
superClass = new CustomClass(__class.superClassDecl, args, __cachedSuperFields, this.interp);
|
|
else {
|
|
if (__cachedSuperFields != null) {
|
|
Reflect.setField(__class.superClassDecl, "__cachedFields", __cachedSuperFields); // Static field
|
|
}
|
|
|
|
var disallowCopy = Type.getInstanceFields(__class.superClassDecl);
|
|
|
|
superClass = Type.createInstance(__class.superClassDecl, args);
|
|
superClass.__customClass = this;
|
|
superClass.__real_fields = disallowCopy;
|
|
|
|
if(isInline) {
|
|
for(s => v in ogVariables)
|
|
if(!disallowCopy.contains(s))
|
|
interp.variables.set(s, v);
|
|
}
|
|
}
|
|
/*
|
|
var extendString = new Printer().typeToString(__class.classDecl.extend);
|
|
if (__class.pkg != null && extendString.indexOf(".") == -1) {
|
|
extendString = __class.pkg.join(".") + "." + extendString;
|
|
}
|
|
|
|
if (Interp.customClassExist(extendString)) {
|
|
var customSuperClass:CustomClass = new CustomClass(Interp.getCustomClass(extendString), args, _cachedSuperFields, this.interp);
|
|
superClass = customSuperClass;
|
|
} else {
|
|
var c = Type.resolveClass('${extendString}_HSX');
|
|
if (c == null) {
|
|
interp.error(ECustom("could not resolve super class: " + extendString));
|
|
}
|
|
if (_cachedSuperFields != null) {
|
|
Reflect.setField(c, "__cachedFields", _cachedSuperFields); // Static field
|
|
}
|
|
|
|
superClass = Type.createInstance(c, args);
|
|
superClass.__customClass = this;
|
|
superClass.__real_fields = Type.getInstanceFields(c);
|
|
}
|
|
*/
|
|
}
|
|
|
|
function buildImports() {
|
|
var i:Int = 0;
|
|
for (_import in __class.imports) {
|
|
var importedClass = _import.fullPath;
|
|
var importAlias = _import.as;
|
|
|
|
if (Interp.customClassExist(importedClass) && this.interp.importFailedCallback != null) {
|
|
this.interp.importFailedCallback(importedClass.split("."), importAlias);
|
|
continue;
|
|
}
|
|
|
|
#if hscriptPos
|
|
var e:Expr = {
|
|
e: ExprDef.EImport(importedClass, importAlias),
|
|
pmin: 0,
|
|
pmax: 0,
|
|
origin: this.className,
|
|
line: i
|
|
};
|
|
#else
|
|
var e = Expr.EImport(importedClass, importAlias);
|
|
#end
|
|
this.interp.expr(e);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
inline function buildUsings() {
|
|
for (us in __class.usings) {
|
|
@:privateAccess this.interp.useUsing(us);
|
|
}
|
|
}
|
|
|
|
public function callFunction(name:String, ?args:Array<Dynamic>):Dynamic {
|
|
var r:Dynamic = null;
|
|
|
|
if (hasField(name)) {
|
|
var fn = getFunction(name);
|
|
try {
|
|
if (fn == null)
|
|
interp.error(ECustom('${name} is not a function'));
|
|
|
|
r = UnsafeReflect.callMethodUnsafe(null, fn, args == null ? [] : args);
|
|
} catch (e:hscript.Expr.Error) {
|
|
// A script error occurred while executing the custom class function.
|
|
// Purge the function from the cache so it is not called again.
|
|
purgeFunction(name);
|
|
}
|
|
}
|
|
else {
|
|
var fixedArgs = [];
|
|
for (a in args) {
|
|
if ((a is CustomClass)) {
|
|
var customClass:CustomClass = cast(a, CustomClass);
|
|
fixedArgs.push(customClass.superClass != null ? customClass.getSuperclass() : customClass);
|
|
} else {
|
|
fixedArgs.push(a);
|
|
}
|
|
}
|
|
var superFn:Function = null;
|
|
if(superClass is CustomClass) {
|
|
superFn = cast(superClass, CustomClass).hget(name);
|
|
}
|
|
else {
|
|
var fixedName = '_HX_SUPER__${name}';
|
|
superFn = Reflect.field(superClass, fixedName);
|
|
}
|
|
|
|
if (superFn == null || !Reflect.isFunction(superFn)) {
|
|
this.interp.error(ECustom('Error while calling function super.${name}(): EInvalidAccess'
|
|
+ '\n'
|
|
+ 'InvalidAccess error: Super function "${name}" does not exist! Define it or call the correct superclass function.'));
|
|
}
|
|
r = Reflect.callMethod(superClass is CustomClass ? null : superClass, superFn, fixedArgs);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
// Field check
|
|
|
|
private function hasField(name:String):Bool {
|
|
return __cachedFieldDecls != null ? __cachedFieldDecls.exists(name) : false;
|
|
}
|
|
|
|
private function getField(name:String):FieldDecl {
|
|
return __cachedFieldDecls != null ? __cachedFieldDecls.get(name) : null;
|
|
}
|
|
|
|
private function hasVar(name:String):Bool {
|
|
return __cachedVarDecls != null ? __cachedVarDecls.exists(name) : false;
|
|
}
|
|
|
|
private function getVar(name:String):VarDecl {
|
|
return __cachedVarDecls != null ? __cachedVarDecls.get(name) : null;
|
|
}
|
|
|
|
private function hasFunction(name:String):Bool {
|
|
return __cachedFunctionDecls != null ? __cachedFunctionDecls.exists(name) : false;
|
|
}
|
|
|
|
private function getFunction(name:String):Function {
|
|
var fn = this.interp.variables.get(name);
|
|
return Reflect.isFunction(fn) ? fn : null;
|
|
}
|
|
|
|
// SuperClass field check
|
|
|
|
private function cacheSuperField(name:String, value:Dynamic) {
|
|
if (__cachedSuperFields != null) {
|
|
__cachedSuperFields.set(name, value);
|
|
}
|
|
}
|
|
|
|
var __superClassFieldList:Array<String> = null;
|
|
// TODO: superHasField when it extends another Custom Class
|
|
public function superHasField(name:String):Bool {
|
|
if (superClass == null)
|
|
return false;
|
|
|
|
// Reflect.hasField(this, name) is REALLY expensive so we use a cache.
|
|
if (__superClassFieldList == null) {
|
|
var realFields = Reflect.fields(superClass).concat(Type.getInstanceFields(Type.getClass(superClass)));
|
|
__superClassFieldList = realFields;
|
|
}
|
|
|
|
return __superClassFieldList.indexOf(name) != -1;
|
|
}
|
|
|
|
/**
|
|
* Remove a function from the cache.
|
|
* This is useful when a function is broken and needs to be skipped.
|
|
* @param name The name of the function to remove from the cache.
|
|
*/
|
|
private function purgeFunction(name:String):Void {
|
|
if (__cachedFunctionDecls != null) {
|
|
__cachedFunctionDecls.remove(name);
|
|
}
|
|
}
|
|
|
|
// Access fields
|
|
|
|
public function hget(name:String):Dynamic {
|
|
return resolveField(name);
|
|
}
|
|
|
|
public function hset(name:String, val:Dynamic):Dynamic {
|
|
switch (name) {
|
|
default:
|
|
if (hasVar(name)) {
|
|
if (__allowSetGet && hasFunction('set_${name}'))
|
|
return __callSetter(name, val);
|
|
|
|
this.interp.variables.set(name, val);
|
|
} else if (this.superClass != null) {
|
|
if (this.superClass is CustomClass) {
|
|
var superCustomClass:CustomClass = cast(this.superClass, CustomClass);
|
|
try {
|
|
superCustomClass.__allowSetGet = this.__allowSetGet;
|
|
return superCustomClass.hset(name, val);
|
|
} catch (e:Dynamic) {}
|
|
}
|
|
if (Type.getClass(this.superClass) == null) {
|
|
// Anonymous structure
|
|
if (Reflect.hasField(this.superClass, name)) {
|
|
Reflect.setField(this.superClass, name, val);
|
|
}
|
|
return val;
|
|
}
|
|
else if (superHasField(name)) {
|
|
if (__allowSetGet)
|
|
Reflect.setProperty(this.superClass, name, val);
|
|
else
|
|
Reflect.setField(this.superClass, name, val);
|
|
}
|
|
else {
|
|
throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '"
|
|
+ Type.getClassName(Type.getClass(this.superClass)) + "'";
|
|
}
|
|
} else {
|
|
throw "field '" + name + "' does not exist in custom class '" + this.className + "'";
|
|
}
|
|
}
|
|
return val;
|
|
}
|
|
|
|
public function __callGetter(name:String):Dynamic {
|
|
__allowSetGet = false;
|
|
var r = callFunction('get_${name}');
|
|
__allowSetGet = true;
|
|
return r;
|
|
}
|
|
|
|
public function __callSetter(name:String, val:Dynamic):Dynamic {
|
|
__allowSetGet = false;
|
|
var r = callFunction('set_${name}', [val]);
|
|
__allowSetGet = true;
|
|
return r;
|
|
}
|
|
|
|
private function resolveField(name:String):Dynamic {
|
|
switch (name) {
|
|
case "superClass":
|
|
return this.superClass;
|
|
case "createSuperClass":
|
|
return this.createSuperClass;
|
|
case "hasFunction":
|
|
return this.hasFunction;
|
|
case "callFunction":
|
|
return this.callFunction;
|
|
default:
|
|
if (hasFunction(name)) {
|
|
var fn:Function = Reflect.makeVarArgs(function(args:Array<Dynamic>) {
|
|
return this.callFunction(name, args);
|
|
});
|
|
|
|
return fn;
|
|
}
|
|
|
|
if (hasVar(name)) {
|
|
var value:Dynamic = null;
|
|
|
|
if (__allowSetGet && hasFunction('get_${name}')) {
|
|
value = __callGetter(name);
|
|
} else if (this.interp.variables.exists(name)) {
|
|
value = this.interp.variables.get(name);
|
|
} else {
|
|
var v = getVar(name);
|
|
|
|
if (v.expr != null) {
|
|
value = this.interp.expr(v.expr);
|
|
this.interp.variables.set(name, value);
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
if (this.superClass != null) {
|
|
if (Type.getClass(this.superClass) == null) {
|
|
// Anonymous structure
|
|
if (Reflect.hasField(this.superClass, name)) {
|
|
return Reflect.field(this.superClass, name);
|
|
} else {
|
|
throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '"
|
|
+ Type.getClassName(Type.getClass(this.superClass)) + "'";
|
|
}
|
|
}
|
|
|
|
if (this.superClass is CustomClass) {
|
|
var superCustomClass:CustomClass = cast(this.superClass, CustomClass);
|
|
try {
|
|
superCustomClass.__allowSetGet = this.__allowSetGet;
|
|
return superCustomClass.hget(name);
|
|
} catch (e) {}
|
|
}
|
|
|
|
var fields = Type.getInstanceFields(Type.getClass(this.superClass));
|
|
if (fields.contains(name)) {
|
|
return __allowSetGet ? Reflect.getProperty(this.superClass, name) : Reflect.field(this.superClass, name);
|
|
} else {
|
|
throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '"
|
|
+ Type.getClassName(Type.getClass(this.superClass)) + "'";
|
|
}
|
|
} else {
|
|
throw "field '" + name + "' does not exist in custom class '" + this.className + "'";
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the real superClass if the Custom Class
|
|
* extends another Custom Class, and so on until
|
|
* it reaches a real class, otherwise it will
|
|
* return the last fetched Custom Class
|
|
* @return Null<Dynamic>
|
|
*/
|
|
public function getSuperclass():Null<Dynamic> {
|
|
var cls:Null<Dynamic> = this.superClass;
|
|
|
|
// Check if the superClass is another custom class,
|
|
// so it will find for a real class, otherwise
|
|
// returns the last super CustomClass parent.
|
|
while(cls != null && cls is CustomClass) {
|
|
var next = cast(cls, CustomClass).superClass;
|
|
if(next == null)
|
|
break; // Return the Custom Class itself
|
|
cls = next;
|
|
}
|
|
|
|
return cls;
|
|
}
|
|
}
|