GenericContext overhaul, removed redundant TypeSubstitutor

This commit is contained in:
bigguy345 committed 2026-03-13 18:29:06 +02:00
1 parent 53446e06c6
commit f66d2f0193
7 files changed
+155 -222

No files matched your search

@@ -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<Node>} 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<TypeInfo> args = type.getAppliedTypeArgs();
if (args != null && !args.isEmpty()) {
List<TypeInfo> 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(".")) {
@@ -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<String, TypeInfo> 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<String>}) and its raw fields
* (declared with type variables like T) need to reflect the concrete type arguments.
*
* Example: for {@code class Box<T> { 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; }
@@ -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<String>), substitute class type variables (E -> String)
Map<String, TypeInfo> 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<FieldInfo> 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<String>}) and its raw methods
* (declared with type variables like T) need to reflect the concrete type arguments.
*
* Example: for {@code class Box<T> { 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<FieldInfo> 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.
*/
@@ -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<String> 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&lt;String&gt; binds E &rarr; String) — priority
* 2. Declared bounds fallback (e.g., &lt;T extends Entity&gt; binds T &rarr; 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 {
@@ -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<FieldInfo> 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<TypeInfo> 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<String, List<MethodInfo>> entry : this.methods.entrySet()) {
List<MethodInfo> 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<String, FieldInfo> 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 {
@@ -796,14 +796,12 @@ public class TypeInfo {
if (rawType != null && rawType != this) {
List<MethodInfo> rawMethods = rawType.getAllMethods();
if (!rawMethods.isEmpty() && isParameterized()) {
Map<String, TypeInfo> bindings = TypeSubstitutor.createBindingsFromReceiver(this);
if (!bindings.isEmpty()) {
List<MethodInfo> substituted = new ArrayList<>(rawMethods.size());
for (MethodInfo m : rawMethods) {
substituted.add(m.substituteTypeParams(bindings));
}
return substituted;
GenericContext ctx = GenericContext.forReceiver(this);
List<MethodInfo> 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<FieldInfo> rawFields = rawType.getAllFields();
if (!rawFields.isEmpty() && isParameterized()) {
Map<String, TypeInfo> bindings = TypeSubstitutor.createBindingsFromReceiver(this);
if (!bindings.isEmpty()) {
List<FieldInfo> substituted = new ArrayList<>(rawFields.size());
for (FieldInfo f : rawFields) {
substituted.add(f.substituteTypeParams(bindings));
}
return substituted;
GenericContext ctx = GenericContext.forReceiver(this);
List<FieldInfo> 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<String, TypeInfo> 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<MethodInfo> rawOverloads = rawType.getAllMethodOverloads(methodName);
if (!rawOverloads.isEmpty() && isParameterized()) {
Map<String, TypeInfo> 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<String, TypeInfo> 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<String, TypeInfo> 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;
@@ -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<String>.get(int) returns String (not T/Object)
* - Map<String, Integer>.get(Object) returns Integer (not V)
*
* Handles nested generics:
* - List<Map<String, Integer>>.get(0) returns Map<String, Integer>
*/
public class TypeSubstitutor {
/**
* Create a binding map from declared type parameters to applied type arguments.
*
* Example: For List<String> where List is declared as interface List<E>:
* - 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<String, TypeInfo> createBindings(List<TypeParamInfo> declaredParams, List<TypeInfo> appliedArgs) {
Map<String, TypeInfo> 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<String>)
* @return Bindings from the type's declared params to its applied args
*/
public static Map<String, TypeInfo> createBindingsFromReceiver(TypeInfo receiverType) {
if (receiverType == null) {
return new HashMap<>();
}
// Get the raw type to access declared type parameters
TypeInfo rawType = receiverType.getRawType();
List<TypeParamInfo> 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<TypeInfo> 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<String, TypeInfo> 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<TypeInfo> originalArgs = type.getAppliedTypeArgs();
List<TypeInfo> 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;
}
}