diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericContext.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericContext.java index 243ceb8d1..243cd7af3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericContext.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericContext.java @@ -39,6 +39,47 @@ public final class GenericContext { this.boundFallbacks = boundFallbacks != null ? boundFallbacks : new HashMap<>(); } + /** + * Create a GenericContext directly from declared type parameters and applied type arguments. + * + * Unlike {@link #forReceiver(TypeInfo)}, this does NOT introspect a TypeInfo, so it cannot + * trigger recursive parameterization. Use this inside {@code parameterize()} implementations + * to avoid the cycle: parameterize → forReceiver → substitute → parameterize. + * + * @param declaredParams the declared type parameters (e.g., [T, E]) + * @param appliedArgs the applied type arguments (e.g., [String, Integer]) + * @return a context mapping param names to applied args (with bound fallbacks for unmatched params) + */ + public static GenericContext fromBindings(List declaredParams, List appliedArgs) { + if (declaredParams == null || declaredParams.isEmpty()) { + return EMPTY; + } + + Map applied = new HashMap<>(); + if (appliedArgs != null) { + int count = Math.min(declaredParams.size(), appliedArgs.size()); + for (int i = 0; i < count; i++) { + TypeParamInfo declared = declaredParams.get(i); + TypeInfo arg = appliedArgs.get(i); + if (declared == null || declared.getName() == null || declared.getName().isEmpty() || arg == null) { + continue; + } + applied.put(declared.getName(), arg); + } + } + + Map bounds = new HashMap<>(); + for (TypeParamInfo declared : declaredParams) { + if (declared == null) continue; + String name = declared.getName(); + if (name == null || name.isEmpty()) continue; + TypeInfo bound = declared.getBoundTypeInfo(); + bounds.put(name, (bound != null && bound.isResolved()) ? bound : TypeInfo.OBJECT); + } + + return new GenericContext(applied, bounds); + } + /** * Create a GenericContext for a receiver type. * @@ -85,7 +126,13 @@ public final class GenericContext { String name = declared.getName(); if (name == null || name.isEmpty()) continue; TypeInfo bound = declared.getBoundTypeInfo(); - bounds.put(name, (bound != null && bound.isResolved()) ? bound : TypeInfo.OBJECT); + TypeInfo effectiveBound = (bound != null && bound.isResolved()) ? bound : TypeInfo.OBJECT; + + List additionalBounds = declared.getAdditionalBoundTypes(); + if (additionalBounds != null && !additionalBounds.isEmpty()) { + effectiveBound = IntersectionTypeInfo.of(effectiveBound, additionalBounds); + } + bounds.put(name, effectiveBound); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index b75c4afcb..2c49b036a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -11,8 +11,10 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Represents a type defined within the script itself (not from Java classpath). @@ -40,6 +42,11 @@ public class ScriptTypeInfo extends TypeInfo { // Generic type parameters declared on this type (e.g., , , ) private final List declaredTypeParams = new ArrayList<>(); + + // Recursion guard: tracks ScriptTypeInfo instances currently being parameterized on this thread + // to break the cycle: parameterize → substitute → parameterize (self-referential types) + private static final ThreadLocal> PARAMETERIZING = + ThreadLocal.withInitial(HashSet::new); // Parent class reference (for inner class resolution) private ScriptTypeInfo outerClass; @@ -166,34 +173,48 @@ public class ScriptTypeInfo extends TypeInfo { @Override public TypeInfo parameterize(List typeArgs) { if (typeArgs == null || typeArgs.isEmpty()) return this; - ScriptTypeInfo result = new ScriptTypeInfo(getSimpleName(), getFullName(), getKind(), - declarationOffset, bodyStart, bodyEnd, modifiers); - result.setRawType(this); - result.setAppliedTypeArgs(typeArgs); - result.superClass = this.superClass; - result.superClassName = this.superClassName; - result.implementedInterfaces.addAll(this.implementedInterfaces); - result.implementedInterfaceNames.addAll(this.implementedInterfaceNames); - result.declaredTypeParams.addAll(this.declaredTypeParams); - result.outerClass = this.outerClass; - GenericContext ctx = GenericContext.forReceiver(result); - // Parameterize methods, constructors, and fields with the generic context of this type. - for (Map.Entry> entry : this.methods.entrySet()) { - List substituted = new ArrayList<>(); - for (MethodInfo m : entry.getValue()) substituted.add(substituteMethod(m, ctx)); - result.methods.put(entry.getKey(), substituted); + Set active = PARAMETERIZING.get(); + if (!active.add(this)) { + // Re-entrant call on the same raw type — fall back to shallow parameterization + // (no method/field substitution) to break the infinite recursion. + return super.parameterize(typeArgs); } - for (MethodInfo ctor : this.constructors) { - result.constructors.add(substituteMethod(ctor, ctx)); + + try { + ScriptTypeInfo result = new ScriptTypeInfo(getSimpleName(), getFullName(), getKind(), + declarationOffset, bodyStart, bodyEnd, modifiers); + result.setRawType(this); + result.setAppliedTypeArgs(typeArgs); + result.superClass = this.superClass; + result.superClassName = this.superClassName; + result.implementedInterfaces.addAll(this.implementedInterfaces); + result.implementedInterfaceNames.addAll(this.implementedInterfaceNames); + result.declaredTypeParams.addAll(this.declaredTypeParams); + result.outerClass = this.outerClass; + // Use fromBindings instead of forReceiver to avoid infinite recursion: + // forReceiver(result) → substitute() → parameterize() → forReceiver() → ... + GenericContext ctx = GenericContext.fromBindings(this.declaredTypeParams, typeArgs); + + // Parameterize methods, constructors, and fields with the generic context of this type. + for (Map.Entry> entry : this.methods.entrySet()) { + List substituted = new ArrayList<>(); + for (MethodInfo m : entry.getValue()) substituted.add(substituteMethod(m, ctx)); + result.methods.put(entry.getKey(), substituted); + } + for (MethodInfo ctor : this.constructors) { + result.constructors.add(substituteMethod(ctor, ctx)); + } + for (Map.Entry entry : this.fields.entrySet()) { + FieldInfo f = entry.getValue(); + TypeInfo subType = ctx.substitute(f.getTypeInfo()); + result.fields.put(entry.getKey(), subType != f.getTypeInfo() + ? FieldInfo.external(f.getName(), subType, null, f.getModifiers()) : f); + } + return result; + } finally { + active.remove(this); } - for (Map.Entry entry : this.fields.entrySet()) { - FieldInfo f = entry.getValue(); - TypeInfo subType = ctx.substitute(f.getTypeInfo()); - result.fields.put(entry.getKey(), subType != f.getTypeInfo() - ? FieldInfo.external(f.getName(), subType, null, f.getModifiers()) : f); - } - return result; } // ==================== INHERITANCE ====================