diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java index dfe41103d..99590522f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -4308,6 +4308,54 @@ public class ScriptDocument { return resolveTypeAndTrackUsage(typeName); } + /** + * Recursively resolve any unresolved type names inside a TypeInfo using position-aware resolution. + * + * This handles a different concern than {@link GenericContext#substitute(TypeInfo)} — + * it does not replace type variables with concrete bindings. Instead, it re-resolves + * unresolved simple names (e.g., inner class names like "Node" or script-declared type + * parameters like "T") that appear as type arguments in parameterized types. + * + * For example, if a type resolves to {@code List} but "Node" is still unresolved, + * this will attempt to resolve "Node" using the position-aware + * {@link #resolveType(String, int)} which can find inner classes and type parameters + * visible at the given position. + * + * @param type the type whose unresolved components should be re-resolved + * @param position the script text offset for scope-aware resolution + * @return the type with all resolvable components resolved, or the original if nothing changed + */ + private TypeInfo substituteTypeParams(TypeInfo type, int position) { + if (type == null) + return null; + + if (!type.isResolved() && type.getSimpleName() != null + && !type.getSimpleName().contains(".")) { + TypeInfo sub = resolveType(type.getSimpleName(), position); + return (sub != null && sub.isResolved()) ? sub : type; + } + + if (type.isParameterized()) { + List args = type.getAppliedTypeArgs(); + if (args != null && !args.isEmpty()) { + List substituted = new ArrayList<>(args.size()); + boolean anyChanged = false; + for (TypeInfo arg : args) { + TypeInfo sub = substituteTypeParams(arg, position); + if (sub != arg) + anyChanged = true; + substituted.add(sub); + } + if (anyChanged) { + TypeInfo raw = type.getRawType(); + return raw != null ? raw.parameterize(substituted) : type.parameterize(substituted); + } + } + } + + return type; + } + /** * Position-aware type resolution overload. * Resolves a type name with positional context so that an unqualified inner class name @@ -4330,7 +4378,7 @@ public class ScriptDocument { // If already resolved, no need for positional fallback if (resolved != null && resolved.isResolved()) { - return resolved; + return substituteTypeParams(resolved, position); } if (typeName != null && !typeName.contains(".")) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 466f1d8c4..78f342db5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -9,7 +9,7 @@ import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor; + import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; @@ -18,7 +18,6 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; /** * Metadata for a field (variable) declaration or reference. @@ -165,12 +164,8 @@ public final class FieldInfo { } // If the receiver is parameterized, substitute class type variables in the field type - if (GenericContext.hasGenerics(containingType)) { - Map receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType); - if (!receiverBindings.isEmpty()) { - type = TypeSubstitutor.substitute(type, receiverBindings); - } - } + if (GenericContext.hasGenerics(containingType)) + type = GenericContext.forReceiver(containingType).substitute(type); // Check if this is an enum constant if (field.isEnumConstant()) { @@ -531,6 +526,38 @@ public final class FieldInfo { public String getDocumentation() { return documentation; } public JSDocInfo getJSDocInfo() { return jsDocInfo; } public void setJSDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; } + + /** + * Create a copy of this field with its declared type substituted using the given context. + * Returns {@code this} unchanged if the context is null, the declared type is null, + * or substitution produces the same type (identity check). + * + * Used when a generic type is parameterized (e.g., {@code Box}) and its raw fields + * (declared with type variables like T) need to reflect the concrete type arguments. + * + * Example: for {@code class Box { T value; }}, calling this on the raw field {@code value: T} + * with a context binding {@code T → String} produces {@code value: String}. + * + * @param context the generic context containing type variable bindings; if null, returns {@code this} unchanged + * @return a new FieldInfo with the substituted declared type, or {@code this} if no substitution occurred + */ + public FieldInfo substituteTypeParams(GenericContext context) { + if (context == null || declaredType == null) { + return this; + } + TypeInfo subType = context.substitute(declaredType); + if (subType == declaredType) { + return this; + } + FieldInfo result = new FieldInfo(name, scope, subType, declarationOffset, resolved, + containingMethod, documentation, initStart, initEnd, modifiers, reflectionField); + result.jsDocInfo = this.jsDocInfo; + result.scopeInfo = this.scopeInfo; + result.isVarArg = this.isVarArg; + result.inferredType = this.inferredType; + return result; + } + public int getInitStart() { return initStart; } public int getInitEnd() { return initEnd; } public boolean hasInitializer() { return initStart >= 0 && initEnd > initStart; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index ccfc347c5..12ba570d3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -11,7 +11,7 @@ import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext; import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor; + import noppes.npcs.client.gui.util.script.interpreter.type.GenericParameterAdapter; import java.lang.reflect.Constructor; @@ -173,13 +173,12 @@ public final class MethodInfo { } // If the receiver is parameterized (e.g., List), substitute class type variables (E -> String) - Map receiverBindings = null; + GenericContext receiverCtx = null; if (GenericContext.hasGenerics(containingType)) { - receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType); - if (!receiverBindings.isEmpty()) { - returnType = TypeSubstitutor.substitute(returnType, receiverBindings); - } + receiverCtx = GenericContext.forReceiver(containingType); + returnType = receiverCtx.substitute(returnType); } + List params = new ArrayList<>(); // Use getGenericParameterTypes() to preserve generic information java.lang.reflect.Type[] genericParamTypes = method.getGenericParameterTypes(); @@ -190,9 +189,8 @@ public final class MethodInfo { paramType = TypeInfo.fromClass(method.getParameterTypes()[i]); } - if (receiverBindings != null && !receiverBindings.isEmpty()) { - paramType = TypeSubstitutor.substitute(paramType, receiverBindings); - } + if (receiverCtx != null) + paramType = receiverCtx.substitute(paramType); // Adapt Object params to receiver generic type args (e.g., Map.get(Object) -> Map.get(K)) TypeInfo adapted = GenericParameterAdapter.adaptParameterType(method, containingType, i, paramType); @@ -377,6 +375,37 @@ public final class MethodInfo { */ public boolean hasInheritanceMarker() { return isOverride() || isImplements(); } + /** + * Create a copy of this method with all type variables substituted using the given context. + * Applies substitution to both the return type and every parameter type recursively. + * + * Used when a generic type is parameterized (e.g., {@code Box}) and its raw methods + * (declared with type variables like T) need to reflect the concrete type arguments. + * + * Example: for {@code class Box { T get(); }}, calling this on the raw {@code get() → T} + * with a context binding {@code T → String} produces {@code get() → String}. + * + * @param context the generic context containing type variable bindings; if null, returns {@code this} unchanged + * @return a new MethodInfo with substituted types, or {@code this} if context is null + */ + public MethodInfo substituteTypeParams(GenericContext context) { + if (context == null) { + return this; + } + TypeInfo subReturnType = context.substitute(returnType); + List subParams = new ArrayList<>(parameters.size()); + for (FieldInfo param : parameters) { + subParams.add(param.substituteTypeParams(context)); + } + MethodInfo result = new MethodInfo(name, subReturnType, containingType, subParams, + fullDeclarationOffset, typeOffset, nameOffset, bodyStart, bodyEnd, + resolved, isDeclaration, modifiers, documentation, javaMethod); + result.jsDocInfo = this.jsDocInfo; + result.overridesFrom = this.overridesFrom; + result.implementsFrom = this.implementsFrom; + return result; + } + /** * Check if a position is inside this method's body. */ 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 394000384..455e8c6b3 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 @@ -8,12 +8,16 @@ import java.util.List; import java.util.Map; /** - * A context for resolving and substituting declared type parameters (T, E, K, V...) - * based on a receiver type. + * Single source of truth for generic type variable substitution. * - * Policy: - * - Prefer applied type arguments when present (e.g. List binds E -> String). - * - Fall back to declared bounds when no applied argument exists (e.g. T extends Entity -> Entity). + * Resolves type parameters (T, E, K, V...) to concrete types using: + * 1. Applied type arguments (e.g., List<String> binds E → String) — priority + * 2. Declared bounds fallback (e.g., <T extends Entity> binds T → Entity) — fallback + * + * Usage patterns: + * - {@link #forReceiver(TypeInfo)} — build context from a parameterized receiver type + * - {@link #fromBindings(List, List)} — build context from explicit params/args (anti-recursion safe) + * - {@link #substitute(TypeInfo)} — instance method with two-tier resolution */ public final class GenericContext { 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 5ee8d3783..b40c17bf9 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 @@ -153,24 +153,6 @@ public class ScriptTypeInfo extends TypeInfo { return super.getTypeParam(name); } - private static MethodInfo substituteMethod(MethodInfo m, GenericContext ctx) { - TypeInfo retType = m.getReturnType(); - TypeInfo subRet = ctx.substitute(retType); - List params = new ArrayList<>(m.getParameters()); - boolean changed = subRet != retType; - for (int i = 0; i < params.size(); i++) { - FieldInfo p = params.get(i); - TypeInfo subP = ctx.substitute(p.getTypeInfo()); - if (subP != p.getTypeInfo()) { - params.set(i, FieldInfo.reflectionParam(p.getName(), subP)); - changed = true; - } - } - return changed ? MethodInfo.external(m.getName(), subRet, m.getContainingType(), - params, m.getModifiers(), m.getDocumentation()) : m; - } - - @Override public TypeInfo parameterize(List typeArgs) { if (typeArgs == null || typeArgs.isEmpty()) return this; @@ -198,17 +180,14 @@ public class ScriptTypeInfo extends TypeInfo { // 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)); + for (MethodInfo m : entry.getValue()) substituted.add(m.substituteTypeParams(ctx)); result.methods.put(entry.getKey(), substituted); } for (MethodInfo ctor : this.constructors) { - result.constructors.add(substituteMethod(ctor, ctx)); + result.constructors.add(ctor.substituteTypeParams(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); + result.fields.put(entry.getKey(), entry.getValue().substituteTypeParams(ctx)); } return result; } finally { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 25044707a..6a8d2b855 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -796,14 +796,12 @@ public class TypeInfo { if (rawType != null && rawType != this) { List rawMethods = rawType.getAllMethods(); if (!rawMethods.isEmpty() && isParameterized()) { - Map bindings = TypeSubstitutor.createBindingsFromReceiver(this); - if (!bindings.isEmpty()) { - List substituted = new ArrayList<>(rawMethods.size()); - for (MethodInfo m : rawMethods) { - substituted.add(m.substituteTypeParams(bindings)); - } - return substituted; + GenericContext ctx = GenericContext.forReceiver(this); + List substituted = new ArrayList<>(rawMethods.size()); + for (MethodInfo m : rawMethods) { + substituted.add(m.substituteTypeParams(ctx)); } + return substituted; } return rawMethods; } @@ -840,14 +838,12 @@ public class TypeInfo { if (rawType != null && rawType != this) { List rawFields = rawType.getAllFields(); if (!rawFields.isEmpty() && isParameterized()) { - Map bindings = TypeSubstitutor.createBindingsFromReceiver(this); - if (!bindings.isEmpty()) { - List substituted = new ArrayList<>(rawFields.size()); - for (FieldInfo f : rawFields) { - substituted.add(f.substituteTypeParams(bindings)); - } - return substituted; + GenericContext ctx = GenericContext.forReceiver(this); + List substituted = new ArrayList<>(rawFields.size()); + for (FieldInfo f : rawFields) { + substituted.add(f.substituteTypeParams(ctx)); } + return substituted; } return rawFields; } @@ -1170,8 +1166,8 @@ public class TypeInfo { } else if (rawType != null && rawType != this) { MethodInfo rawMethod = rawType.getMethodInfo(methodName); if (rawMethod != null && isParameterized()) { - Map bindings = TypeSubstitutor.createBindingsFromReceiver(this); - if (!bindings.isEmpty()) return rawMethod.substituteTypeParams(bindings); + GenericContext ctx = GenericContext.forReceiver(this); + return rawMethod.substituteTypeParams(ctx); } return rawMethod; } @@ -1213,13 +1209,11 @@ public class TypeInfo { } else if (rawType != null && rawType != this) { java.util.List rawOverloads = rawType.getAllMethodOverloads(methodName); if (!rawOverloads.isEmpty() && isParameterized()) { - Map bindings = TypeSubstitutor.createBindingsFromReceiver(this); - if (!bindings.isEmpty()) { - for (MethodInfo m : rawOverloads) { - overloads.add(m.substituteTypeParams(bindings)); - } - return overloads; + GenericContext ctx = GenericContext.forReceiver(this); + for (MethodInfo m : rawOverloads) { + overloads.add(m.substituteTypeParams(ctx)); } + return overloads; } return rawOverloads; } @@ -1303,8 +1297,8 @@ public class TypeInfo { if (rawType != null && rawType != this) { FieldInfo rawField = rawType.getFieldInfo(fieldName); if (rawField != null && isParameterized()) { - Map bindings = TypeSubstitutor.createBindingsFromReceiver(this); - if (!bindings.isEmpty()) return rawField.substituteTypeParams(bindings); + GenericContext ctx = GenericContext.forReceiver(this); + return rawField.substituteTypeParams(ctx); } return rawField; } @@ -1429,11 +1423,9 @@ public class TypeInfo { if (rawType != null && rawType != this) { MethodInfo rawSAM = rawType.getSingleAbstractMethod(); if (rawSAM != null && isParameterized()) { - Map bindings = TypeSubstitutor.createBindingsFromReceiver(this); - if (!bindings.isEmpty()) { - cachedSAM = rawSAM.substituteTypeParams(bindings); - return cachedSAM; - } + GenericContext ctx = GenericContext.forReceiver(this); + cachedSAM = rawSAM.substituteTypeParams(ctx); + return cachedSAM; } cachedSAM = rawSAM; return cachedSAM; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java deleted file mode 100644 index 2554867d0..000000000 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java +++ /dev/null @@ -1,146 +0,0 @@ -package noppes.npcs.client.gui.util.script.interpreter.type; - -import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Utility for substituting type variables (like T, E, K, V) with concrete types. - * - * Used when resolving members on parameterized types: - * - List.get(int) returns String (not T/Object) - * - Map.get(Object) returns Integer (not V) - * - * Handles nested generics: - * - List>.get(0) returns Map - */ -public class TypeSubstitutor { - - /** - * Create a binding map from declared type parameters to applied type arguments. - * - * Example: For List where List is declared as interface List: - * - declaredParams = [TypeParamInfo("E", ...)] - * - appliedArgs = [TypeInfo(String)] - * - Result: {"E" -> TypeInfo(String)} - * - * @param declaredParams The declared type parameters (from the generic type definition) - * @param appliedArgs The applied type arguments (from the parameterized usage) - * @return A map from type parameter names to their concrete TypeInfo values - */ - public static Map createBindings(List declaredParams, List appliedArgs) { - Map bindings = new HashMap<>(); - - if (declaredParams == null || appliedArgs == null) { - return bindings; - } - - int count = Math.min(declaredParams.size(), appliedArgs.size()); - for (int i = 0; i < count; i++) { - String paramName = declaredParams.get(i).getName(); - TypeInfo argType = appliedArgs.get(i); - if (paramName != null && argType != null) { - bindings.put(paramName, argType); - } - } - - return bindings; - } - - /** - * Create bindings from a parameterized receiver type. - * - * @param receiverType The parameterized type (e.g., List) - * @return Bindings from the type's declared params to its applied args - */ - public static Map createBindingsFromReceiver(TypeInfo receiverType) { - if (receiverType == null) { - return new HashMap<>(); - } - - // Get the raw type to access declared type parameters - TypeInfo rawType = receiverType.getRawType(); - List declaredParams = null; - - // Get declared params from JSTypeInfo or reflection - if (rawType.isJSType() && rawType.getJSTypeInfo() != null) { - declaredParams = rawType.getJSTypeInfo().getTypeParams(); - } else { - declaredParams = rawType.getTypeParams(); - } - - // Get applied args from the parameterized type - List appliedArgs = receiverType.getAppliedTypeArgs(); - - return createBindings(declaredParams, appliedArgs); - } - - /** - * Substitute type variables in a type using the given bindings. - * - * @param type The type to substitute (may contain type variables like T, E) - * @param bindings The map from type variable names to concrete types - * @return The substituted type - */ - public static TypeInfo substitute(TypeInfo type, Map bindings) { - if (type == null || bindings == null || bindings.isEmpty()) { - return type; - } - - String typeName = type.getSimpleName(); - - // Check if this type is itself a type variable. - // Two cases: - // 1. Unresolved TypeInfo (Java reflection path: TypeInfo.fromGenericType creates these for T, E, K, V) - // 2. TypeInfo.typeParameter() (script-defined path: resolved but tagged as a type parameter) - // For tagged type parameters, use getTypeParameterName() as the lookup key. - if (!type.isResolved() || type.isTypeParameter()) { - String lookupKey = type.isTypeParameter() ? type.getTypeParameterName() : typeName; - TypeInfo substitution = bindings.get(lookupKey); - if (substitution != null) { - return substitution; - } - } - - // If the type has applied type arguments, substitute within them recursively - if (type.isParameterized()) { - List originalArgs = type.getAppliedTypeArgs(); - List substitutedArgs = new ArrayList<>(); - boolean changed = false; - - for (TypeInfo arg : originalArgs) { - TypeInfo substitutedArg = substitute(arg, bindings); - substitutedArgs.add(substitutedArg); - if (substitutedArg != arg) { - changed = true; - } - } - - if (changed) { - // Create a new parameterized type with substituted args - return type.getRawType().parameterize(substitutedArgs); - } - } - - // Handle array types - substitute the element type - if (type.getSimpleName().endsWith("[]")) { - TypeStringNormalizer.ArraySplit split = TypeStringNormalizer.splitArraySuffixes(typeName); - String elementTypeName = split.base; - int dims = split.dimensions; - TypeInfo elementSubstitution = elementTypeName != null ? bindings.get(elementTypeName) : null; - if (elementSubstitution != null) { - TypeInfo result = elementSubstitution; - for (int i = 0; i < dims; i++) { - result = TypeInfo.arrayOf(result); - } - return result; - } - } - - return type; - } - -}