From 54d8aa48fb464128d75a310c30fcde8bf93f238b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 10 Mar 2026 15:34:04 +0200 Subject: [PATCH] Implement highlight and type resolution for ScriptTypeInfo parameterized java class i.e.: public class Box{ private T value; public Box(T value) { this.value = value; } public T get() { return value; } } --- .../script/interpreter/FieldChainMarker.java | 5 +- .../script/interpreter/ScriptDocument.java | 143 +++++++++++++++--- .../interpreter/hover/TokenHoverInfo.java | 21 +++ .../interpreter/js_parser/TypeParamInfo.java | 4 + .../interpreter/type/GenericContext.java | 9 +- .../interpreter/type/ScriptTypeInfo.java | 92 +++++++++++ .../script/interpreter/type/TypeInfo.java | 55 +++++-- .../interpreter/type/TypeSubstitutor.java | 13 +- 8 files changed, 296 insertions(+), 46 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java index edd06f0a3..2eeff85c0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java @@ -370,8 +370,9 @@ public class FieldChainMarker { if (!currentType.hasField(segment)) { // Field not found — check if the segment names an inner class. // This handles chains like Outer.Inner where Inner is a nested type, not a field. - if (currentType instanceof ScriptTypeInfo) { - ScriptTypeInfo innerClass = ((ScriptTypeInfo) currentType).getInnerClass(segment); + TypeInfo rawType = currentType.getRawType(); + if (rawType instanceof ScriptTypeInfo) { + ScriptTypeInfo innerClass = ((ScriptTypeInfo) rawType).getInnerClass(segment); if (innerClass != null) { return new MarkResult( new ScriptLine.Mark(pos[0], pos[1], innerClass.getTokenType(), innerClass), 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 3ec3fae19..f9e8a4d26 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 @@ -11,6 +11,7 @@ import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSScriptAnalyzer; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParser; @@ -88,7 +89,7 @@ public class ScriptDocument { // (the first declarator "int x," is captured; continuations are scanned manually). private static final Pattern FIELD_DECL_PATTERN = Pattern.compile( "\\b([A-Za-z_][a-zA-Z0-9_.<>,[ \\t]\\[\\]]*)[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*)[ \\t]*(=|;|,)"); - private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:<([^>]*)>)?"); // Cast expression pattern: captures type name inside cast parentheses // Matches: (Type), (Type[]), (pkg.Type), (Type) @@ -754,18 +755,20 @@ public class ScriptDocument { final int bodyStart; final int bodyEnd; final int modifiers; + final String typeParamsClause; final String extendsClause; final String implementsClause; final JSDocInfo jsDoc; RawTypeDeclaration(String name, TypeInfo.Kind kind, int declOffset, int bodyStart, int bodyEnd, - int modifiers, String extendsClause, String implementsClause, JSDocInfo jsDoc) { + int modifiers, String typeParamsClause, String extendsClause, String implementsClause, JSDocInfo jsDoc) { this.name = name; this.kind = kind; this.declOffset = declOffset; this.bodyStart = bodyStart; this.bodyEnd = bodyEnd; this.modifiers = modifiers; + this.typeParamsClause = typeParamsClause; this.extendsClause = extendsClause; this.implementsClause = implementsClause; this.jsDoc = jsDoc; @@ -783,7 +786,7 @@ public class ScriptDocument { private void parseScriptTypes() { // ========== PASS 1: Discover all type declarations (flat list) ========== Pattern typeDecl = Pattern.compile( - "(?:(?:public|private|protected|static|final|abstract)\\s+)*(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:\\(\\))?\\s*(?:extends\\s+([A-Za-z_][a-zA-Z0-9_.]*))?\\s*(?:implements\\s+([A-Za-z_][a-zA-Z0-9_.,\\s]*))?\\s*\\{"); + "(?:(?:public|private|protected|static|final|abstract)\\s+)*(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:<([^>]*)>)?\\s*(?:\\(\\))?\\s*(?:extends\\s+([A-Za-z_][a-zA-Z0-9_.]*))?\\s*(?:implements\\s+([A-Za-z_][a-zA-Z0-9_.,\\s]*))?\\s*\\{"); List rawDeclarations = new ArrayList<>(); Matcher m = typeDecl.matcher(text); @@ -793,8 +796,9 @@ public class ScriptDocument { String kindStr = m.group(1); String typeName = m.group(2); - String extendsClause = m.group(3); - String implementsClause = m.group(4); + String typeParamsClause = m.group(3); + String extendsClause = m.group(4); + String implementsClause = m.group(5); int bodyStart = text.indexOf('{', m.start()); int bodyEnd = findMatchingBrace(bodyStart); @@ -815,7 +819,7 @@ public class ScriptDocument { rawDeclarations.add(new RawTypeDeclaration( typeName, kind, m.start(), bodyStart, bodyEnd, - modifiers, extendsClause, implementsClause, jsDoc)); + modifiers, typeParamsClause, extendsClause, implementsClause, jsDoc)); } // ========== PASS 2: Build hierarchy using stack-based containment ========== @@ -851,6 +855,11 @@ public class ScriptDocument { scriptType.setJSDocInfo(raw.jsDoc); } + // Parse generic type parameters (e.g., , , ) + if (raw.typeParamsClause != null && !raw.typeParamsClause.trim().isEmpty()) { + parseTypeParamsClause(raw.typeParamsClause, scriptType, raw.declOffset); + } + // Register in all lookup maps for O(1) access by any naming convention scriptTypesByFullName.put(scriptType.getFullName(), scriptType); scriptTypesByDotName.put(scriptType.getDotSeparatedName(), scriptType); @@ -888,6 +897,67 @@ public class ScriptDocument { } } + /** + * Parse a generic type parameters clause like "E", "K, V", or "T extends Entity" + * into TypeParamInfo objects and add them to the script type. + */ + private void parseTypeParamsClause(String clause, ScriptTypeInfo scriptType, int declOffset) { + String[] params = clause.split(","); + for (String param : params) { + String trimmed = param.trim(); + if (trimmed.isEmpty()) continue; + + int extendsIdx = trimmed.indexOf(" extends "); + if (extendsIdx >= 0) { + String paramName = trimmed.substring(0, extendsIdx).trim(); + String boundName = trimmed.substring(extendsIdx + 9).trim(); + TypeParamInfo typeParam = new TypeParamInfo(paramName, boundName, null); + TypeInfo boundTypeInfo = resolveType(boundName, declOffset); + if (boundTypeInfo != null && boundTypeInfo.isResolved()) { + typeParam.setBoundTypeInfo(boundTypeInfo); + } + scriptType.addDeclaredTypeParam(typeParam); + } else { + scriptType.addDeclaredTypeParam(new TypeParamInfo(trimmed, null, null)); + } + } + } + + private TypeInfo parameterizeWithClause(TypeInfo baseType, String typeArgsClause, int position) { + String[] argNames = typeArgsClause.split(","); + List typeArgs = new ArrayList<>(); + for (String argName : argNames) { + String trimmed = argName.trim(); + if (trimmed.isEmpty()) continue; + TypeInfo argType = resolveType(trimmed, position); + if (argType == null) { + argType = TypeInfo.unresolved(trimmed, trimmed); + } + typeArgs.add(argType); + } + if (!typeArgs.isEmpty()) { + return baseType.parameterize(typeArgs); + } + return baseType; + } + + private void markTypeParamDeclarations(List marks, int afterNameEnd, ScriptTypeInfo typeInfo) { + int ltPos = text.indexOf('<', afterNameEnd); + if (ltPos < 0 || ltPos > afterNameEnd + 5) return; + int gtPos = text.indexOf('>', ltPos); + if (gtPos < 0) return; + + String clause = text.substring(ltPos + 1, gtPos); + int offset = ltPos + 1; + for (TypeParamInfo param : typeInfo.getDeclaredTypeParams()) { + int paramIdx = clause.indexOf(param.getName(), offset - (ltPos + 1)); + if (paramIdx >= 0) { + int paramStart = ltPos + 1 + paramIdx; + int paramEnd = paramStart + param.getName().length(); + marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.IMPORTED_CLASS, param)); + } + } + } /** * Parse fields and methods inside a script-defined type. */ @@ -3702,15 +3772,20 @@ public class ScriptDocument { return resolved; } - // Positional fallback: only applies to simple names (no dots) that could be - // inner classes visible from the current scope. Dot-separated names like - // "Outer.Inner" are already handled by scriptTypesByDotName in resolveType(). if (typeName != null && !typeName.contains(".")) { - // Walk the enclosing type hierarchy upward from the innermost type at this position. - // At each level, check if there's an inner class matching the name. - // This mirrors Java's scoping rules where inner classes shadow outer-scope types. ScriptTypeInfo enclosing = findEnclosingScriptType(position); while (enclosing != null) { + // Check if the name is a declared type parameter on this class (e.g., E in class Box) + TypeParamInfo typeParam = enclosing.getDeclaredTypeParam(typeName); + if (typeParam != null) { + TypeInfo boundType = typeParam.getBoundTypeInfo(); + if (boundType != null && boundType.isResolved()) { + return TypeInfo.typeParameter(typeName, boundType); + } + return TypeInfo.typeParameter(typeName); + } + + // Check inner classes ScriptTypeInfo inner = enclosing.getInnerClass(typeName); if (inner != null) { return inner; @@ -4827,7 +4902,7 @@ public class ScriptDocument { // Extended pattern to capture extends and implements clauses // Groups: 1=class|interface|enum, 2=TypeName, 3=extends clause, 4=implements clause Pattern classWithInheritance = Pattern.compile( - "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:\\(\\))?\\s*" + + "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:<[^>]*>)?\\s*(?:\\(\\))?\\s*" + "(?:(extends)\\s+([A-Za-z_][a-zA-Z0-9_.]*))?\\s*" + "(?:(implements)\\s+([A-Za-z_][a-zA-Z0-9_.,\\s]*))?\\s*(?=\\{)"); @@ -4867,6 +4942,11 @@ public class ScriptDocument { } marks.add(new ScriptLine.Mark(m.start(2), m.end(2), nameType, typeInfo)); + // Mark generic type parameter declarations (e.g., , ) + if (typeInfo != null && typeInfo.hasDeclaredTypeParams()) { + markTypeParamDeclarations(marks, m.end(2), typeInfo); + } + // Mark extends clause if (m.group(3) != null) { //Extends key word @@ -5537,8 +5617,9 @@ public class ScriptDocument { } // Regular type - check for method existence using hierarchy search if it's a ScriptTypeInfo boolean hasMethod = false; - if (receiverType instanceof ScriptTypeInfo) { - hasMethod = ((ScriptTypeInfo) receiverType).hasMethodInHierarchy(methodName); + TypeInfo rawReceiver = receiverType.getRawType(); + if (rawReceiver instanceof ScriptTypeInfo) { + hasMethod = ((ScriptTypeInfo) rawReceiver).hasMethodInHierarchy(methodName); } else { hasMethod = receiverType.hasMethod(methodName); } @@ -6139,6 +6220,13 @@ public class ScriptDocument { } TypeInfo baseType = resolveType(typeName, position); if (baseType == null) return null; + + // Parameterize with generic type arguments (e.g., new Box(...)) + String typeArgsClause = newMatcher.group(2); + if (typeArgsClause != null && !typeArgsClause.trim().isEmpty()) { + baseType = parameterizeWithClause(baseType, typeArgsClause, position); + } + String rest = expr.substring(newMatcher.end()); int dims = 0; for (int i = 0; i < rest.length() && rest.charAt(i) != '('; i++) { @@ -6600,8 +6688,9 @@ public class ScriptDocument { // Method call - get return type with argument-based overload resolution // Check for method existence using hierarchy search if it's a ScriptTypeInfo boolean hasMethod = false; - if (currentType instanceof ScriptTypeInfo) { - hasMethod = ((ScriptTypeInfo) currentType).hasMethodInHierarchy(segment.name); + TypeInfo rawType = currentType.getRawType(); + if (rawType instanceof ScriptTypeInfo) { + hasMethod = ((ScriptTypeInfo) rawType).hasMethodInHierarchy(segment.name); } else { hasMethod = currentType.hasMethod(segment.name); } @@ -6621,10 +6710,11 @@ public class ScriptDocument { boolean hasField = false; FieldInfo fieldInfo = null; - if (currentType instanceof ScriptTypeInfo) { - hasField = ((ScriptTypeInfo) currentType).hasFieldInHierarchy(segment.name); + TypeInfo rawType = currentType.getRawType(); + if (rawType instanceof ScriptTypeInfo) { + hasField = ((ScriptTypeInfo) rawType).hasFieldInHierarchy(segment.name); if (hasField) { - fieldInfo = ((ScriptTypeInfo) currentType).getFieldInfoInHierarchy(segment.name); + fieldInfo = currentType.getFieldInfo(segment.name); } } else { hasField = currentType.hasField(segment.name); @@ -6639,8 +6729,8 @@ public class ScriptDocument { // Field not found — check if the segment names an inner class. // This handles chains like Outer.Inner where Inner is a nested type, not a field. - if (currentType instanceof ScriptTypeInfo) { - ScriptTypeInfo innerClass = ((ScriptTypeInfo) currentType).getInnerClass(segment.name); + if (rawType instanceof ScriptTypeInfo) { + ScriptTypeInfo innerClass = ((ScriptTypeInfo) rawType).getInnerClass(segment.name); if (innerClass != null) { return innerClass; } @@ -7088,7 +7178,14 @@ public class ScriptDocument { } // Otherwise treat it as a type name - return resolveType(typeName); + TypeInfo baseType = resolveType(typeName); + if (baseType != null) { + String typeArgsClause = newMatcher.group(2); + if (typeArgsClause != null && !typeArgsClause.trim().isEmpty()) { + baseType = parameterizeWithClause(baseType, typeArgsClause, position); + } + } + return baseType; } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 3e2912bf6..a452aaf2a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -519,6 +519,11 @@ public class TokenHoverInfo { private void extractClassInfo(Token token) { TypeInfo typeInfo = token.getTypeInfo(); if (typeInfo == null) return; + + if (typeInfo.isTypeParameter()) { + extractTypeParameterInfo(typeInfo); + return; + } packageName = getPackageName(typeInfo); @@ -718,6 +723,19 @@ public class TokenHoverInfo { } } + private void extractTypeParameterInfo(TypeInfo typeInfo) { + iconIndicator = "T"; + String paramName = typeInfo.getTypeParameterName(); + + addSegment("type parameter ", TokenType.MODIFIER.getHexColor()); + addSegment(paramName, TokenType.IMPORTED_CLASS.getHexColor()); + + if (typeInfo.getJavaClass() != null && typeInfo.getJavaClass() != Object.class) { + addSegment(" extends ", TokenType.MODIFIER.getHexColor()); + addSegment(typeInfo.getSimpleName(), getColorForTypeInfo(typeInfo)); + } + } + private void extractMethodCallInfo(Token token) { MethodCallInfo callInfo = token.getMethodCallInfo(); MethodInfo methodInfo = callInfo != null ? callInfo.getResolvedMethod() : token.getMethodInfo(); @@ -935,6 +953,9 @@ public class TokenHoverInfo { if (type == null) return null; + if (type.isTypeParameter()) + return null; + TypeInfo base = type.isArray() ? type.getElementType() : type; if (base == null) base = type; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java index 846a23f47..cfe997be8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java @@ -30,6 +30,10 @@ public class TypeParamInfo { return boundTypeInfo; } + public void setBoundTypeInfo(TypeInfo boundTypeInfo) { + this.boundTypeInfo = boundTypeInfo; + } + /** * Resolve the bound type using TypeResolver. * Called during Phase 2 after all types are loaded. 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 916623586..25f457e7c 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 @@ -150,9 +150,12 @@ public final class GenericContext { return null; } - // Direct type variable substitution (unresolved types represent type variables like T). - if (!type.isResolved()) { - TypeInfo substitution = resolveTypeVariable(type.getSimpleName()); + // Direct type variable substitution. + // Handles unresolved types (Java reflection path) and TypeInfo.typeParameter() (script-defined path). + // Bounded type parameters have the bound's simpleName, so use getTypeParameterName() for the lookup. + if (!type.isResolved() || type.isTypeParameter()) { + String lookupKey = type.isTypeParameter() ? type.getTypeParameterName() : type.getSimpleName(); + TypeInfo substitution = resolveTypeVariable(lookupKey); if (substitution != null) { return substitution; } 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 0ed37fcdc..bf9dd7590 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 @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.util.script.interpreter.type; import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; @@ -37,6 +38,9 @@ public class ScriptTypeInfo extends TypeInfo { // Enum constants (for enum types only) - name -> EnumConstantInfo private final Map enumConstants = new HashMap<>(); + // Generic type parameters declared on this type (e.g., , , ) + private final List declaredTypeParams = new ArrayList<>(); + // Parent class reference (for inner class resolution) private ScriptTypeInfo outerClass; @@ -101,6 +105,94 @@ public class ScriptTypeInfo extends TypeInfo { public ScriptTypeInfo getOuterClass() { return outerClass; } public List getInnerClasses() { return innerClasses; } + // ==================== GENERIC TYPE PARAMETERS ==================== + + public void addDeclaredTypeParam(TypeParamInfo param) { + declaredTypeParams.add(param); + } + + public List getDeclaredTypeParams() { + return declaredTypeParams; + } + + public TypeParamInfo getDeclaredTypeParam(String name) { + for (TypeParamInfo param : declaredTypeParams) { + if (param.getName().equals(name)) { + return param; + } + } + return null; + } + + public boolean hasDeclaredTypeParams() { + return !declaredTypeParams.isEmpty(); + } + + @Override + public List getTypeParams() { + if (!declaredTypeParams.isEmpty()) { + return declaredTypeParams; + } + return super.getTypeParams(); + } + + @Override + public TypeParamInfo getTypeParam(String name) { + TypeParamInfo param = getDeclaredTypeParam(name); + if (param != null) { + return param; + } + 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; + 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 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 (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 ==================== /** 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 f7b552323..b6c65bb3b 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 @@ -86,6 +86,7 @@ public class TypeInfo { private boolean isPrimitive; // Whether this is a primitive type (e.g., int, boolean) private boolean isObjectLiteral; + private String typeParameterName; // Non-null if this TypeInfo represents a type parameter (e.g., "T") // JavaScript/TypeScript type info (for types from .d.ts files) private final JSTypeInfo jsTypeInfo; // The JS type info (null if Java type) @@ -95,11 +96,11 @@ public class TypeInfo { // Applied type arguments (concrete types provided for generics, e.g., in Map) // These are TypeInfo instances that can themselves be parameterized for nested generics - private final List appliedTypeArgs = new ArrayList<>(); + private List appliedTypeArgs = new ArrayList<>(); // For parameterized types, the raw/base type (e.g., List for List) // Null for non-parameterized types - private final TypeInfo rawType; + private TypeInfo rawType; // For array types, the element type (e.g., IItemStack for IItemStack[]) // Null for non-array types @@ -178,6 +179,35 @@ public class TypeInfo { return new TypeInfo(simpleName, fullPath, pkg, Kind.UNKNOWN, null, false, null); } + /** + * Create a TypeInfo representing an unbounded type parameter (e.g., {@code T} in {@code class Box}). + * Uses Object.class for member lookup (erasure semantics) but displays as the parameter name. + */ + public static TypeInfo typeParameter(String paramName) { + TypeInfo ti = new TypeInfo(paramName, paramName, "", Kind.CLASS, Object.class, true, null); + ti.typeParameterName = paramName; + return ti; + } + + /** + * Create a TypeInfo representing a bounded type parameter (e.g., {@code T extends Entity}). + * Delegates member lookup to the bound type but records the original parameter name for display. + */ + public static TypeInfo typeParameter(String paramName, TypeInfo boundType) { + TypeInfo ti = new TypeInfo(boundType.simpleName, boundType.fullName, boundType.packageName, + boundType.kind, boundType.javaClass, true, boundType.enclosingType, boundType.jsTypeInfo); + ti.typeParameterName = paramName; + return ti; + } + + public boolean isTypeParameter() { + return typeParameterName != null; + } + + public String getTypeParameterName() { + return typeParameterName; + } + public static TypeInfo fromClass(Class clazz) { if (clazz == null) return null; @@ -541,6 +571,14 @@ public class TypeInfo { public List getAppliedTypeArgs() { return appliedTypeArgs; } + + protected void setRawType(TypeInfo rawType) { + this.rawType = rawType; + } + + protected void setAppliedTypeArgs(List args) { + this.appliedTypeArgs = new ArrayList<>(args); + } /** * Create a parameterized version of this type with the given type arguments. @@ -1215,19 +1253,6 @@ public class TypeInfo { return null; } - /** - * Resolves a type parameter to its bound TypeInfo. - * For example, if this type has "T extends EntityPlayerMP", resolveTypeParamToTypeInfo("T") returns the TypeInfo for EntityPlayerMP. - * If no type parameter is found with that name, returns null. - */ - public TypeInfo resolveTypeParamToTypeInfo(String typeName) { - TypeParamInfo param = getTypeParam(typeName); - if (param != null) { - return param.getBoundTypeInfo(); - } - return null; - } - @Override public String toString() { return "TypeInfo{" + fullName + ", " + kind + ", resolved=" + resolved + "}"; 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 index 3ee5f6263..dbbbbe8be 100644 --- 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 @@ -92,9 +92,16 @@ public class TypeSubstitutor { String typeName = type.getSimpleName(); - // Check if this type is itself a type variable - if (!type.isResolved()) { - TypeInfo substitution = bindings.get(typeName); + // 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 — bounded params have + // the bound's simpleName as their simpleName (e.g., "Entity" for T extends Entity), so using + // simpleName would incorrectly look up "Entity" instead of "T". + if (!type.isResolved() || type.isTypeParameter()) { + String lookupKey = type.isTypeParameter() ? type.getTypeParameterName() : typeName; + TypeInfo substitution = bindings.get(lookupKey); if (substitution != null) { return substitution; }