diff --git a/dependencies.gradle b/dependencies.gradle index 33eb048c1..dce55cad2 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -43,8 +43,8 @@ dependencies { // compileOnly(files("./tools/bukkit-api/spigot-api-ONLY.jar")) // compileOnly(files("./tools/bukkit-api/Vault-1.4.1.jar")) - shadowImplementation(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar")) - annotationProcessor(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar")) + shadowImplementation(project.files("tools/standalone-JaninoLoader-1.0.2.jar")) + annotationProcessor(project.files("tools/standalone-JaninoLoader-1.0.2.jar")) testImplementation("junit:junit:4.13.2") 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/LoopVariableParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/LoopVariableParser.java index b71a8438a..657ada594 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/LoopVariableParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/LoopVariableParser.java @@ -12,9 +12,9 @@ import java.util.regex.Pattern; class LoopVariableParser { private static final Pattern ENHANCED_FOR = Pattern.compile( - "\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*:\\s*([^)]+)\\)"); + "\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>,\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*:\\s*([^)]+)\\)"); private static final Pattern CLASSIC_FOR = Pattern.compile( - "\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*="); + "\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>,\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*="); private static final Pattern FOR_IN = Pattern.compile( "\\bfor\\s*\\(\\s*(?:var|let|const)\\s+(\\w+)\\s+in\\s+([^)]+)\\)"); private static final Pattern FOR_OF = Pattern.compile( 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 de3e56d5e..6f95538c6 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; @@ -87,8 +88,8 @@ public class ScriptDocument { // Also matches ',' as a delimiter so that "int x, y, z;" is recognized // (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_]*)"); + "\\b([A-Za-z_][a-zA-Z0-9_.<>,[ \\t\\n\\r]\\[\\]]*)[ \\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_]*)\\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; @@ -782,8 +785,10 @@ public class ScriptDocument { */ private void parseScriptTypes() { // ========== PASS 1: Discover all type declarations (flat list) ========== + // Regex only anchors on keyword + name — generic clauses are extracted manually + // below to correctly handle nested angle brackets like >. 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_]*)"); List rawDeclarations = new ArrayList<>(); Matcher m = typeDecl.matcher(text); @@ -793,15 +798,58 @@ public class ScriptDocument { String kindStr = m.group(1); String typeName = m.group(2); - String extendsClause = m.group(3); - String implementsClause = m.group(4); - int bodyStart = text.indexOf('{', m.start()); - int bodyEnd = findMatchingBrace(bodyStart); - if (bodyEnd < 0) { - bodyEnd = text.length(); + // Manually extract everything between the type name and the opening '{', + // handling nested angle brackets so > is captured whole. + int scanPos = m.end(); + while (scanPos < text.length() && Character.isWhitespace(text.charAt(scanPos))) + scanPos++; + + // Extract type params clause <...> if present (depth-aware) + String typeParamsClause = null; + if (scanPos < text.length() && text.charAt(scanPos) == '<') { + int depth = 1, i = scanPos + 1; + while (i < text.length() && depth > 0) { + char c = text.charAt(i++); + if (c == '<') depth++; + else if (c == '>') depth--; + } + if (depth == 0) { + typeParamsClause = text.substring(scanPos + 1, i - 1); + scanPos = i; + while (scanPos < text.length() && Character.isWhitespace(text.charAt(scanPos))) + scanPos++; + } } + // Find the opening brace, extracting extends/implements from text between here and '{' + int bracePos = scanPos; + int angleDepth = 0; + while (bracePos < text.length()) { + char c = text.charAt(bracePos); + if (c == '<') angleDepth++; + else if (c == '>') angleDepth--; + else if (c == '{' && angleDepth == 0) break; + bracePos++; + } + if (bracePos >= text.length()) continue; + + String betweenNameAndBrace = text.substring(scanPos, bracePos).trim(); + + // Parse extends/implements from that window + String extendsClause = null; + String implementsClause = null; + java.util.regex.Matcher extM = Pattern.compile( + "extends\\s+([A-Za-z_][a-zA-Z0-9_.]*(?:<[^{]*>)?)").matcher(betweenNameAndBrace); + if (extM.find()) extendsClause = extM.group(1).trim(); + java.util.regex.Matcher implM = Pattern.compile( + "implements\\s+(.+)$").matcher(betweenNameAndBrace); + if (implM.find()) implementsClause = implM.group(1).trim(); + + int bodyStart = bracePos; + int bodyEnd = findMatchingBrace(bodyStart); + if (bodyEnd < 0) bodyEnd = text.length(); + TypeInfo.Kind kind; switch (kindStr) { case "interface": kind = TypeInfo.Kind.INTERFACE; break; @@ -809,13 +857,13 @@ public class ScriptDocument { default: kind = TypeInfo.Kind.CLASS; break; } - String fullMatch = text.substring(m.start(), m.end()); + String fullMatch = text.substring(m.start(), bodyStart + 1); int modifiers = parseModifiers(fullMatch); JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, m.start()); 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 +899,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); @@ -861,6 +914,11 @@ public class ScriptDocument { // Resolve extends clause (parent class inheritance) if (raw.extendsClause != null && !raw.extendsClause.trim().isEmpty()) { String parentName = raw.extendsClause.trim(); + // Strip generic args (e.g., "NumberBox" -> "NumberBox") for type resolution + int genIdx = parentName.indexOf('<'); + if (genIdx >= 0) { + parentName = parentName.substring(0, genIdx); + } TypeInfo parentType = resolveType(parentName, raw.declOffset); if (parentType == null) { parentType = TypeInfo.unresolved(parentName, parentName); @@ -875,6 +933,11 @@ public class ScriptDocument { String trimmedName = ifaceName.trim(); if (trimmedName.isEmpty()) continue; + // Strip generic args (e.g., "Comparable" -> "Comparable") + int genIdx = trimmedName.indexOf('<'); + if (genIdx >= 0) { + trimmedName = trimmedName.substring(0, genIdx); + } TypeInfo ifaceType = resolveType(trimmedName, raw.declOffset); if (ifaceType == null) { ifaceType = TypeInfo.unresolved(trimmedName, trimmedName); @@ -888,6 +951,138 @@ 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"); + String paramName = extendsIdx >= 0 ? trimmed.substring(0, extendsIdx).trim() : trimmed; + + int endIdx = 0; + while (endIdx < paramName.length() && + (Character.isJavaIdentifierPart(paramName.charAt(endIdx)) || paramName.charAt(endIdx) == '.')) { + endIdx++; + } + paramName = paramName.substring(0, endIdx); + + String boundName = null; + if (extendsIdx >= 0) { + String afterExtends = trimmed.substring(extendsIdx + 7).trim(); + // Split on & to get the primary bound (first one); additional bounds are interfaces + String primaryBound = afterExtends.split("&")[0].trim(); + if (!primaryBound.isEmpty()) { + int boundEndIdx = 0; + while (boundEndIdx < primaryBound.length() && + (Character.isJavaIdentifierPart(primaryBound.charAt(boundEndIdx)) || primaryBound.charAt(boundEndIdx) == '.')) { + boundEndIdx++; + } + if (boundEndIdx > 0) { + boundName = primaryBound.substring(0, boundEndIdx); + } + } + } + + TypeParamInfo typeParam = new TypeParamInfo(paramName, boundName, null); + if (boundName != null) { + TypeInfo boundTypeInfo = resolveType(boundName, declOffset); + if (boundTypeInfo != null && boundTypeInfo.isResolved()) { + typeParam.setBoundTypeInfo(boundTypeInfo); + } + } + scriptType.addDeclaredTypeParam(typeParam); + } + } + + 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 depth = 1, i = ltPos + 1; + while (i < text.length() && depth > 0) { + char c = text.charAt(i++); + if (c == '<') depth++; + else if (c == '>') depth--; + } + if (depth != 0) return; + int gtPos = i - 1; + + markTypeParamsInClause(marks, ltPos + 1, gtPos - 1, typeInfo.getDeclaredTypeParams()); + } + + private void markTypeParamsInClause(List marks, int clauseStart, int clauseEnd, List typeParams) { + if (clauseStart >= clauseEnd) return; + + String clause = text.substring(clauseStart, clauseEnd); + Map paramMap = new HashMap<>(); + for (TypeParamInfo param : typeParams) { + paramMap.put(param.getName(), param); + } + + int i = 0; + while (i < clause.length()) { + char c = clause.charAt(i); + + if (c == '<') { + int innerStart = clauseStart + i + 1; + int depth = 1; + int j = i + 1; + while (j < clause.length() && depth > 0) { + char innerC = clause.charAt(j); + if (innerC == '<') depth++; + else if (innerC == '>') depth--; + j++; + } + if (depth == 0) { + markTypeParamsInClause(marks, innerStart, clauseStart + j - 1, typeParams); + i = j; + } else { + i++; + } + } else if (c == '&') { + i++; + } else if (c == ',') { + i++; + } else if (Character.isJavaIdentifierStart(c)) { + int start = i; + while (i < clause.length() && Character.isJavaIdentifierPart(clause.charAt(i))) { + i++; + } + String name = clause.substring(start, i); + TypeParamInfo param = paramMap.get(name); + if (param != null) { + int absStart = clauseStart + start; + int absEnd = clauseStart + i; + marks.add(new ScriptLine.Mark(absStart, absEnd, TokenType.GENERIC_TYPE_PARAM, param)); + } + } else { + i++; + } + } + } /** * Parse fields and methods inside a script-defined type. */ @@ -1326,18 +1521,64 @@ public class ScriptDocument { return params; } - // Pattern: Type varName (with optional spaces) + // Collapse all whitespace runs to single spaces, tracking original positions via posMap. + // posMap[normalizedIndex] = originalIndex, enabling correct absolute offsets after matching. + StringBuilder normalized = new StringBuilder(paramList.length()); + int[] posMap = new int[paramList.length() + 1]; + boolean lastWasSpace = false; + for (int i = 0; i < paramList.length(); i++) { + char c = paramList.charAt(i); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + if (!lastWasSpace) { + posMap[normalized.length()] = i; + normalized.append(' '); + lastWasSpace = true; + } + } else { + posMap[normalized.length()] = i; + normalized.append(c); + lastWasSpace = false; + } + } + posMap[normalized.length()] = paramList.length(); + String normalizedStr = normalized.toString(); + + // Split by top-level commas (not inside < > brackets), then parse each parameter individually + List paramRanges = new ArrayList<>(); + int depth = 0; + int start = 0; + for (int i = 0; i < normalizedStr.length(); i++) { + char c = normalizedStr.charAt(i); + if (c == '<') depth++; + else if (c == '>') depth--; + else if (c == ',' && depth == 0) { + paramRanges.add(new int[]{start, i}); + start = i + 1; + } + } + paramRanges.add(new int[]{start, normalizedStr.length()}); + + // Matches: type (with optional generics, arrays, dots) + optional varargs + whitespace + paramName Pattern paramPattern = Pattern.compile( - "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); - Matcher m = paramPattern.matcher(paramList); - while (m.find()) { - String typeName = m.group(1); + "\\s*([a-zA-Z_][a-zA-Z0-9_.<>,? \\[\\]]*)(?:\\.{3})?\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*"); + + for (int[] range : paramRanges) { + String segment = normalizedStr.substring(range[0], range[1]); + if (segment.trim().isEmpty()) continue; + + Matcher m = paramPattern.matcher(segment); + if (!m.matches()) continue; + + String rawType = m.group(1).replaceAll("\\s+", ""); String paramName = m.group(2); - boolean isVarArg = typeName.endsWith("..."); - if (isVarArg) typeName = typeName.substring(0, typeName.length() - 3); - TypeInfo typeInfo = resolveType(typeName, paramListStart); - // Store the absolute position of the parameter name - int paramNameStart = paramListStart + m.start(2); + String between = segment.substring(m.end(1), m.start(2)); + boolean isVarArg = between.contains("..."); + + TypeInfo typeInfo = resolveType(rawType, paramListStart); + + // m.start(2) is relative to segment; add range[0] to get position in normalizedStr, then posMap to original + int normalizedNamePos = range[0] + m.start(2); + int paramNameStart = paramListStart + posMap[normalizedNamePos]; FieldInfo fieldInfo = FieldInfo.parameter(paramName, typeInfo, paramNameStart, null); fieldInfo.setVarArg(isVarArg); params.add(fieldInfo); @@ -1579,7 +1820,9 @@ public class ScriptDocument { * For JavaScript: Parses "var/let/const varName = expr;" with type inference */ private void parseLocalVariables() { - for (MethodInfo method : getAllMethods()) { + List allMethodsAndConstructors = getAllMethods(); + allMethodsAndConstructors.addAll(getAllConstructors()); + for (MethodInfo method : allMethodsAndConstructors) { Map> locals = new HashMap<>(); methodLocals.put(method.getDeclarationOffset(), locals); @@ -3127,6 +3370,14 @@ public class ScriptDocument { break; } } + if (!insideMethod) { + for (MethodInfo constructor : getAllConstructors()) { + if (constructor.containsPosition(position)) { + insideMethod = true; + break; + } + } + } if (insideMethod) continue; boolean insideInner = false; @@ -3210,6 +3461,11 @@ public class ScriptDocument { for (MethodInfo method : getAllMethods()) { if (method.containsPosition(cAbsNamePos)) { cInsideMethod = true; break; } } + if (!cInsideMethod) { + for (MethodInfo constructor : getAllConstructors()) { + if (constructor.containsPosition(cAbsNamePos)) { cInsideMethod = true; break; } + } + } if (cInsideMethod) return; boolean cInsideInner = false; for (InnerCallableScope sc : innerScopes) { @@ -3281,6 +3537,11 @@ public class ScriptDocument { for (MethodInfo method : getAllMethods()) { if (method.containsPosition(firstVarAbsPos)) { greedyInsideMethod = true; break; } } + if (!greedyInsideMethod) { + for (MethodInfo constructor : getAllConstructors()) { + if (constructor.containsPosition(firstVarAbsPos)) { greedyInsideMethod = true; break; } + } + } } if (greedyInsideMethod) continue; @@ -3317,6 +3578,11 @@ public class ScriptDocument { for (MethodInfo method : getAllMethods()) { if (method.containsPosition(cAbsNamePos)) { cInsideMethod = true; break; } } + if (!cInsideMethod) { + for (MethodInfo constructor : getAllConstructors()) { + if (constructor.containsPosition(cAbsNamePos)) { cInsideMethod = true; break; } + } + } } if (cInsideMethod) return; @@ -3360,6 +3626,14 @@ public class ScriptDocument { break; } } + if (!insideMethod) { + for (MethodInfo constructor : getAllConstructors()) { + if (constructor.containsPosition(position)) { + insideMethod = true; + break; + } + } + } } if (insideMethod) @@ -3425,6 +3699,11 @@ public class ScriptDocument { for (MethodInfo method : getAllMethods()) { if (method.containsPosition(cAbsNamePos)) { cInsideMethod = true; break; } } + if (!cInsideMethod) { + for (MethodInfo constructor : getAllConstructors()) { + if (constructor.containsPosition(cAbsNamePos)) { cInsideMethod = true; break; } + } + } } if (cInsideMethod) return; @@ -3702,15 +3981,29 @@ 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. + TypeStringNormalizer.ArraySplit arraySplit = TypeStringNormalizer.splitArraySuffixes(typeName); + String baseTypeName = arraySplit.base; + int arrayDims = arraySplit.dimensions; + 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(baseTypeName); + if (typeParam != null) { + TypeInfo boundType = typeParam.getBoundTypeInfo(); + if (boundType == null) { + typeParam.resolveBoundType(); + boundType = typeParam.getBoundTypeInfo(); + } + TypeInfo result = (boundType != null && boundType.isResolved()) + ? TypeInfo.typeParameter(baseTypeName, boundType) + : TypeInfo.typeParameter(baseTypeName); + for (int i = 0; i < arrayDims; i++) result = TypeInfo.arrayOf(result); + return result; + } + + // Check inner classes ScriptTypeInfo inner = enclosing.getInnerClass(typeName); if (inner != null) { return inner; @@ -3765,6 +4058,44 @@ public class ScriptDocument { // Fully-qualified Java types else if (baseName.contains(".")) { resolved = typeResolver.resolveFullName(baseName); + // Fallback: "SimpleName.InnerClass" where SimpleName is a known import + // e.g., "Map.Entry" when java.util.Map is imported → java.util.Map$Entry + if (resolved == null || !resolved.isResolved()) { + int firstDot = baseName.indexOf('.'); + String outerSimple = baseName.substring(0, firstDot); + String innerPath = baseName.substring(firstDot + 1); + TypeInfo outerType = typeResolver.resolveSimpleName(outerSimple, importsBySimpleName, wildcardPackages); + if (outerType != null && outerType.isResolved() && outerType.getJavaClass() != null) { + Class currentClass = outerType.getJavaClass(); + for (String part : innerPath.split("\\.")) { + Class found = null; + for (Class inner : currentClass.getClasses()) { + if (inner.getSimpleName().equals(part)) { + found = inner; + break; + } + } + currentClass = found; + if (currentClass == null) break; + } + if (currentClass != null) { + resolved = TypeInfo.fromClass(currentClass); + // Track import usage for the outer class + ImportData usedImport = importsBySimpleName.get(outerSimple); + if (usedImport != null) { + usedImport.incrementUsage(); + } else if (wildcardPackages != null) { + String resultPkg = outerType.getPackageName(); + for (ImportData imp : imports) { + if (imp.isWildcard() && resultPkg != null && resultPkg.equals(imp.getFullPath())) { + imp.incrementUsage(); + break; + } + } + } + } + } + } } // Imported/simple else { @@ -3957,8 +4288,68 @@ public class ScriptDocument { } } + /** + * Two-phase object literal parsing. + * + *

Phase 1 (structure-only): parse every object literal with a no-op type + * resolver. This produces {@link ObjectLiteralParser.ObjectLiteralAnalysis} entries + * whose property names are correct but whose value types are all {@code ANY}. + * The synthetic {@link TypeInfo} shapes built from those names are then attached to + * the corresponding {@link InnerCallableScope}s via + * {@link ObjectLiteralParser#attachObjectLiteralContext}, so that + * {@code resolveThisType()} can return the containing object literal type for + * {@code this.property} chains.

+ * + *

Phase 2 (full resolution): re-parse with the real + * {@code this::resolveExpressionType} callback. Because scopes now have their + * {@code containingObjectType} wired, expressions like {@code return this.faah.length} + * inside method bodies resolve correctly instead of falling back to {@code ANY}.

+ */ private void parseObjectLiterals() { if (!isJavaScript()) return; + + List braceRanges = findObjectLiteralBraces(); + if (braceRanges.isEmpty()) + return; + + // Phase 1: structure-only — property names extracted, value types = ANY + ObjectLiteralParser.ExpressionTypeResolverFn nullResolver = (expr, pos) -> null; + for (int[] range : braceRanges) { + String objectLiteral = text.substring(range[0], range[1]); + ObjectLiteralParser.ObjectLiteralAnalysis analysis = + ObjectLiteralParser.parse(objectLiteral, range[0], true, true, nullResolver, + this::getScriptMethodInfo); + if (analysis != null) + objectLiterals.put(range[0], analysis); + } + + for (InnerCallableScope scope : innerScopes) + ObjectLiteralParser.attachObjectLiteralContext(scope, this); + + // Phase 2: full resolution — accurate value / return types via resolveExpressionType + for (int[] range : braceRanges) { + String objectLiteral = text.substring(range[0], range[1]); + ObjectLiteralParser.ObjectLiteralAnalysis analysis = + ObjectLiteralParser.parse(objectLiteral, range[0], true, true, this::resolveExpressionType, + this::getScriptMethodInfo); + if (analysis != null) + objectLiterals.put(range[0], analysis); + } + + // Re-attach with fully-resolved types (reset so the guard allows re-attachment) + for (InnerCallableScope scope : innerScopes) { + scope.setContainingObjectType(null); + ObjectLiteralParser.attachObjectLiteralContext(scope, this); + } + } + + /** + * Scan the document for opening braces that are likely object-literal starts and + * return their {@code [start, end)} ranges. Shared by both parsing phases to avoid + * duplicate scanning. + */ + private List findObjectLiteralBraces() { + List ranges = new ArrayList<>(); int i = 0; while (i < text.length()) { int brace = text.indexOf('{', i); @@ -3972,18 +4363,10 @@ public class ScriptDocument { i = brace + 1; continue; } - String objectLiteral = text.substring(brace, end); - ObjectLiteralParser.ObjectLiteralAnalysis analysis = - ObjectLiteralParser.parse(objectLiteral, brace, true, true, this::resolveExpressionType, this::getScriptMethodInfo); - if (analysis != null) - objectLiterals.put(brace, analysis); - + ranges.add(new int[]{brace, end}); i = brace + 1; } - // Wire each object-literal inner scope to its containing object type (needs the cache) - for (InnerCallableScope scope : innerScopes) - ObjectLiteralParser.attachObjectLiteralContext(scope, this); - + return ranges; } private boolean isLikelyObjectLiteralStart(int bracePos) { @@ -4737,8 +5120,8 @@ 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*" + - "(?:(extends)\\s+([A-Za-z_][a-zA-Z0-9_.]*))?\\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*(?=\\{)"); Matcher m = classWithInheritance.matcher(text); @@ -4777,11 +5160,16 @@ 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 marks.add(new ScriptLine.Mark(m.start(3), m.end(3), TokenType.IMPORT_KEYWORD)); - markExtendsClause(marks, m.start(4), m.group(4)); + markExtendsClause(marks, m.start(4), m.group(4), typeInfo); } // Mark implements clause @@ -4820,36 +5208,50 @@ public class ScriptDocument { * Mark the extends clause with proper coloring. * The parent class is colored based on its resolved type. */ - private void markExtendsClause(List marks, int clauseStart, String parentName) { + private void markExtendsClause(List marks, int clauseStart, String parentName, ScriptTypeInfo typeInfo) { String trimmedName = parentName.trim(); if (trimmedName.isEmpty()) return; + int actualStart = clauseStart; + while (actualStart < clauseStart + trimmedName.length() && + Character.isWhitespace(text.charAt(actualStart))) { + actualStart++; + } + + int baseNameEnd = actualStart; + while (baseNameEnd < text.length() && Character.isJavaIdentifierPart(text.charAt(baseNameEnd))) { + baseNameEnd++; + } + String baseName = text.substring(actualStart, baseNameEnd); - // Resolve the parent type with position context for inner class names TypeInfo parentType = resolveType(trimmedName, clauseStart); TokenType tokenType; if (parentType != null && parentType.isResolved()) { - // Use the type's specific token type (CLASS, INTERFACE, ENUM) tokenType = parentType.getTokenType(); } else { - // Unresolved - mark as undefined tokenType = TokenType.UNDEFINED_VAR; if (parentType == null) { parentType = TypeInfo.unresolved(trimmedName, trimmedName); } } - // Find actual position in text (might have leading whitespace in the group) - int actualStart = clauseStart; - String fullClause = parentName; - while (actualStart < clauseStart + fullClause.length() && - Character.isWhitespace(text.charAt(actualStart))) { - actualStart++; - } + marks.add(new ScriptLine.Mark(actualStart, baseNameEnd, tokenType, parentType)); - marks.add(new ScriptLine.Mark(actualStart, actualStart + trimmedName.length(), tokenType, parentType)); + if (typeInfo != null && typeInfo.hasDeclaredTypeParams() && baseNameEnd < text.length() && text.charAt(baseNameEnd) == '<') { + int ltPos = baseNameEnd; + int depth = 1; + int i = ltPos + 1; + while (i < text.length() && depth > 0) { + char c = text.charAt(i++); + if (c == '<') depth++; + else if (c == '>') depth--; + } + if (depth == 0) { + markTypeParamsInClause(marks, ltPos + 1, i - 1, typeInfo.getDeclaredTypeParams()); + } + } } /** @@ -5403,7 +5805,7 @@ public class ScriptDocument { } // Parse the arguments (first pass - without expected types for overload resolution) - List arguments = parseMethodArguments(openParen + 1, closeParen, null); + List arguments = parseMethodArguments(openParen + 1, closeParen, null, null); // Check if this is a static access (Class.method() style) boolean isStaticAccess = isStaticAccessCall(nameStart); @@ -5447,8 +5849,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); } @@ -5468,7 +5871,7 @@ public class ScriptDocument { } else { // Second pass: Re-resolve arguments with expected parameter types if method was found if (resolvedMethod != null && resolvedMethod.getParameters().size() == arguments.size()) - arguments = parseMethodArguments(openParen + 1, closeParen, resolvedMethod); + arguments = parseMethodArguments(openParen + 1, closeParen, resolvedMethod, receiverType); MethodCallInfo callInfo = new MethodCallInfo(methodName, nameStart, nameEnd, openParen, @@ -5501,7 +5904,7 @@ public class ScriptDocument { // Second pass: Re-resolve arguments with expected parameter types if method was found if (resolvedMethod != null && resolvedMethod.getParameters().size() == arguments.size()) { - arguments = parseMethodArguments(openParen + 1, closeParen, resolvedMethod); + arguments = parseMethodArguments(openParen + 1, closeParen, resolvedMethod, null); } // Check if this is a method from a script type @@ -5533,7 +5936,7 @@ public class ScriptDocument { List globalArguments = arguments; if (resolvedGlobal.getParameters().size() == arguments.size()) { - globalArguments = parseMethodArguments(openParen + 1, closeParen, resolvedGlobal); + globalArguments = parseMethodArguments(openParen + 1, closeParen, resolvedGlobal, null); } MethodCallInfo callInfo = new MethodCallInfo( @@ -5557,7 +5960,7 @@ public class ScriptDocument { TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); MethodInfo implicitMethod = callerType.getBestMethodOverload(methodName, argTypes); if (implicitMethod != null && implicitMethod.getParameters().size() == arguments.size()) { - arguments = parseMethodArguments(openParen + 1, closeParen, implicitMethod); + arguments = parseMethodArguments(openParen + 1, closeParen, implicitMethod, callerType); } MethodCallInfo callInfo = new MethodCallInfo( methodName, nameStart, nameEnd, openParen, closeParen, @@ -5725,9 +6128,10 @@ public class ScriptDocument { * @param start Start position of arguments (after opening paren) * @param end End position of arguments (at closing paren) * @param methodInfo Optional MethodInfo to provide expected parameter types for validation + * @param receiverType Optional receiver/containing type for substituting generic type parameters in argument types * @return List of parsed arguments with resolved types */ - public List parseMethodArguments(int start, int end, MethodInfo methodInfo) { + public List parseMethodArguments(int start, int end, MethodInfo methodInfo, TypeInfo receiverType) { List args = new ArrayList<>(); if (start >= end) @@ -5751,7 +6155,7 @@ public class ScriptDocument { while (actualStart < i && Character.isWhitespace(text.charAt(actualStart))) { actualStart++; } - int actualEnd = i; + int actualEnd = i; while (actualEnd > actualStart && Character.isWhitespace(text.charAt(actualEnd - 1))) { actualEnd--; } @@ -5767,6 +6171,15 @@ public class ScriptDocument { TypeInfo argType = resolveArgumentType(argText, actualStart, expectedParamType); + // Substitute type parameters using receiver's generic context + if (receiverType != null && argType != null && GenericContext.hasGenerics(receiverType)) { + GenericContext ctx = GenericContext.forReceiver(receiverType); + TypeInfo substituted = ctx.substitute(argType); + if (substituted != null && substituted.isResolved()) { + argType = substituted; + } + } + String samConflictError = CURRENT_SAM_CONFLICT_ERROR.get(); if (samConflictError != null) { CURRENT_SAM_CONFLICT_ERROR.remove(); @@ -5820,7 +6233,7 @@ public class ScriptDocument { // Check if this looks like a parameter declaration: "Type varName" // Pattern: identifier followed by whitespace and another identifier - if (argText.matches("^[A-Za-z_][a-zA-Z0-9_<>\\[\\],\\s]*\\s+[a-zA-Z_][a-zA-Z0-9_]*$")) { + if (argText.matches("^[A-Za-z_][a-zA-Z0-9_<>\\[\\],\\s\\n\\r]*\\s+[a-zA-Z_][a-zA-Z0-9_]*$")) { // Split into tokens String[] parts = argText.split("\\s+"); if (parts.length >= 2) { @@ -6049,6 +6462,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++) { @@ -6510,8 +6930,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); } @@ -6531,10 +6952,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); @@ -6549,8 +6971,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; } @@ -6998,7 +7420,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; } } @@ -7278,7 +7707,7 @@ for (ScriptTypeInfo type:scriptTypes.values()) { TypeInfo superClass = enclosingType != null ? enclosingType.getSuperClass() : null; // Parse arguments and find matching parent constructor - List arguments = parseMethodArguments(openParen + 1, closeParen, null); + List arguments = parseMethodArguments(openParen + 1, closeParen, null, superClass); TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); // Find matching constructor in parent class @@ -8699,12 +9128,22 @@ for (ScriptTypeInfo type:scriptTypes.values()) { while (searchPos < text.length() && Character.isWhitespace(text.charAt(searchPos))) searchPos++; - if (searchPos < text.length() && text.charAt(searchPos) == '(') { + // Skip generic args or diamond operator (e.g., or <>) + if (searchPos < text.length() && text.charAt(searchPos) == '<') { + int closeAngle = text.indexOf('>', searchPos); + if (closeAngle >= 0) { + searchPos = closeAngle + 1; + while (searchPos < text.length() && Character.isWhitespace(text.charAt(searchPos))) + searchPos++; + } + } + + if (searchPos < text.length() && text.charAt(searchPos) == '(') { int openParen = searchPos; - int closeParen = findMatchingParen(openParen); + int closeParen = findMatchingParen(openParen); if (closeParen >= 0) { - // For constructor declarations, verify it's followed by opening brace + // For constructor declarations, verify it's followed by opening brace if (isConstructorDecl && !isNewCreation) { int braceSearch = closeParen + 1; while (braceSearch < text.length() && Character.isWhitespace(text.charAt(braceSearch))) @@ -8719,7 +9158,7 @@ for (ScriptTypeInfo type:scriptTypes.values()) { // Parse arguments to find matching constructor List arguments = parseMethodArguments(openParen + 1, closeParen, - null); + null, info); int argCount = arguments.size(); TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType) .toArray(TypeInfo[]::new); @@ -8753,7 +9192,6 @@ for (ScriptTypeInfo type:scriptTypes.values()) { ).setConstructor(true); ctorCall.isClassTypeAccess = true; } else { - // Use factory method for regular types ctorCall = MethodCallInfo.constructor( info, constructor, start, end, openParen, closeParen, arguments ); @@ -9053,6 +9491,11 @@ for (ScriptTypeInfo type:scriptTypes.values()) { return method; } } + for (MethodInfo constructor : getAllConstructors()) { + if (constructor.containsPosition(position)) { + return constructor; + } + } return null; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index f3deb5ecf..28dea5e08 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -4,6 +4,7 @@ import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; 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.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; @@ -180,6 +181,14 @@ public class ScriptLine { public void applyTokenMetadata(Token token, Object metadata) { if (metadata instanceof TypeInfo) { token.setTypeInfo((TypeInfo) metadata); + } else if (metadata instanceof TypeParamInfo) { + TypeParamInfo typeParam = (TypeParamInfo) metadata; + TypeInfo boundType = typeParam.getBoundTypeInfo(); + if (boundType != null) { + token.setTypeInfo(TypeInfo.typeParameter(typeParam.getName(), boundType)); + } else { + token.setTypeInfo(TypeInfo.typeParameter(typeParam.getName())); + } } else if (metadata instanceof MethodCallInfo) { MethodCallInfo callInfo = (MethodCallInfo) metadata; if (callInfo.isConstructor()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java index 565dd9717..8089ebe27 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter.field; import noppes.npcs.client.gui.util.script.ScopeInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; @@ -139,8 +140,8 @@ public class AssignmentInfo { // Check type compatibility if (targetType != null && sourceType != null) { if (!TypeChecker.isTypeCompatible(targetType, sourceType)) { - this.requiredType = targetType.getSimpleName(); - this.providedType = sourceType.getSimpleName(); + this.requiredType = MethodCallInfo.getTypeDisplayName(targetType); + this.providedType = MethodCallInfo.getTypeDisplayName(sourceType); setError(ErrorType.TYPE_MISMATCH, buildTypeMismatchMessage()); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java index f86b3e146..031c733cc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java @@ -160,7 +160,8 @@ public class EnumConstantInfo { List arguments = ScriptDocument.INSTANCE.parseMethodArguments( openParenPos + 1, closeParenPos, - null + null, + enumType ); // Find matching constructor 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..56f1c6e32 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 @@ -5,6 +5,7 @@ import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; 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.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; @@ -153,6 +154,7 @@ public class TokenHoverInfo { case INTERFACE_DECL: case ENUM_DECL: case TYPE_DECL: + case GENERIC_TYPE_PARAM: info.extractClassInfo(token); break; @@ -519,6 +521,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); @@ -557,6 +564,7 @@ public class TokenHoverInfo { : TokenType.IMPORTED_CLASS.getHexColor(); // Render generics with correct segment colors if present addTypeSegments(typeInfo); + addDeclaredTypeParamSegments(typeInfo); // Extends Class superclass = clazz.getSuperclass(); @@ -616,8 +624,7 @@ public class TokenHoverInfo { : scriptType.getKind() == TypeInfo.Kind.ENUM ? TokenType.ENUM_DECL.getHexColor() : TokenType.IMPORTED_CLASS.getHexColor(); addTypeSegments(typeInfo); - - // Extends clause for ScriptTypeInfo + addDeclaredTypeParamSegments(typeInfo); if (scriptType.hasSuperClass()) { addSegment(" extends ", TokenType.MODIFIER.getHexColor()); TypeInfo superClass = scriptType.getSuperClass(); @@ -688,6 +695,7 @@ public class TokenHoverInfo { iconIndicator = "I"; addSegment("interface ", TokenType.MODIFIER.getHexColor()); addTypeSegments(typeInfo); + addDeclaredTypeParamSegments(typeInfo); if (jsType.getExtendsType() != null) { addSegment(" extends ", TokenType.MODIFIER.getHexColor()); @@ -718,6 +726,22 @@ public class TokenHoverInfo { } } + private void extractTypeParameterInfo(TypeInfo typeInfo) { + iconIndicator = "T"; + String paramName = typeInfo.getTypeParameterName(); + + addSegment("type parameter ", TokenType.MODIFIER.getHexColor()); + addSegment("<", TokenType.DEFAULT.getHexColor()); + addSegment(paramName, TokenType.GENERIC_TYPE_PARAM.getHexColor()); + + TypeInfo bound = typeInfo.getBoundType(); + if (bound != null && bound.getJavaClass() != null && bound.getJavaClass() != Object.class) { + addSegment(" extends ", TokenType.MODIFIER.getHexColor()); + addSegment(bound.getSimpleName(), getColorForTypeInfo(bound)); + } + addSegment(">", TokenType.DEFAULT.getHexColor()); + } + private void extractMethodCallInfo(Token token) { MethodCallInfo callInfo = token.getMethodCallInfo(); MethodInfo methodInfo = callInfo != null ? callInfo.getResolvedMethod() : token.getMethodInfo(); @@ -769,7 +793,7 @@ public class TokenHoverInfo { buildBasicMethodDeclaration(methodInfo, containingType); return; } - buildMethodDeclaration(methodInfo.getJavaMethod(), containingType); + buildMethodDeclaration(methodInfo, containingType); extractJavadoc(methodInfo.getJavaMethod()); return; } @@ -935,6 +959,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; @@ -1037,6 +1064,29 @@ public class TokenHoverInfo { splitAndAddTypeName(typeName, getColorForTypeInfo(typeInfo)); } + private void addDeclaredTypeParamSegments(TypeInfo typeInfo) { + List params = typeInfo.getTypeParams(); + if (params == null || params.isEmpty()) return; + if (typeInfo.isParameterized()) return; + + addSegment("<", TokenType.DEFAULT.getHexColor()); + for (int i = 0; i < params.size(); i++) { + if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); + TypeParamInfo param = params.get(i); + addSegment(param.getName(), TokenType.GENERIC_TYPE_PARAM.getHexColor()); + if (param.getBoundTypeName() != null && !"Object".equals(param.getBoundTypeName())) { + addSegment(" extends ", TokenType.MODIFIER.getHexColor()); + TypeInfo boundType = param.getBoundTypeInfo(); + if (boundType != null && boundType.isResolved()) { + addTypeSegments(boundType); + } else { + addSegment(param.getBoundTypeName(), TokenType.IMPORTED_CLASS.getHexColor()); + } + } + } + addSegment(">", TokenType.DEFAULT.getHexColor()); + } + private void splitAndAddTypeName(String typeName, int typeColor) { if (typeName == null || !typeName.endsWith("[]")) { addSegment(typeName, typeColor); @@ -1111,7 +1161,8 @@ public class TokenHoverInfo { declaration.add(new TextSegment(text, color)); } - private void buildMethodDeclaration(Method method, TypeInfo containingType) { + private void buildMethodDeclaration(MethodInfo methodInfo, TypeInfo containingType) { + Method method = methodInfo.getJavaMethod(); int mods = method.getModifiers(); // Annotations (show @Contract if present, etc.) @@ -1128,9 +1179,9 @@ public class TokenHoverInfo { if (Modifier.isSynchronized(mods)) addSegment("synchronized ", TokenType.MODIFIER.getHexColor()); // Return type - check for actual type color (handle array suffix coloring) - Class returnType = method.getReturnType(); + TypeInfo returnType = methodInfo.getReturnType(); String returnTypeName = returnType.getSimpleName(); - splitAndAddTypeName(returnTypeName, getColorForTypeInfo(containingType)); + splitAndAddTypeName(returnTypeName, getColorForTypeInfo(methodInfo.getReturnType())); addSegment(" ", TokenType.DEFAULT.getHexColor()); // Method name @@ -1138,20 +1189,19 @@ public class TokenHoverInfo { // Parameters addSegment("(", TokenType.DEFAULT.getHexColor()); - Class[] paramTypes = method.getParameterTypes(); - java.lang.reflect.Parameter[] params = method.getParameters(); - for (int i = 0; i < paramTypes.length; i++) { + List params = methodInfo.getParameters(); + for (int i = 0; i < params.size(); i++) { if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); - int paramTypeColor = getColorForClass(paramTypes[i]); - String paramTypeName = paramTypes[i].getSimpleName(); - splitAndAddTypeName(paramTypeName, paramTypeColor); - if (method.isVarArgs() && i == paramTypes.length - 1) { - addSegment("...", TokenType.DEFAULT.getHexColor()); + FieldInfo param = params.get(i); + TypeInfo paramType = param.getTypeInfo(); + if (paramType != null) { + addTypeSegments(paramType); + if (param.isVarArg()) { + addSegment("...", TokenType.DEFAULT.getHexColor()); + } + addSegment(" ", TokenType.DEFAULT.getHexColor()); } - addSegment(" ", TokenType.DEFAULT.getHexColor()); - // Try to get parameter name if available - String paramName = params.length > i ? params[i].getName() : "arg" + i; - addSegment(paramName, TokenType.PARAMETER.getHexColor()); + addSegment(param.getName(), TokenType.PARAMETER.getHexColor()); } addSegment(")", TokenType.DEFAULT.getHexColor()); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java index d30503e8b..938e334b1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java @@ -47,9 +47,9 @@ final class DtsModScanner { for (DtsFileRef ref : dtsFiles) { domainCounts.put(ref.getDomain(), domainCounts.getOrDefault(ref.getDomain(), 0) + 1); } - System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files across " + domainCounts.size() + " domains"); +// System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files across " + domainCounts.size() + " domains"); for (Map.Entry entry : domainCounts.entrySet()) { - System.out.println("[JSTypeRegistry] Domain " + entry.getKey() + ": " + entry.getValue() + " files"); +// System.out.println("[JSTypeRegistry] Domain " + entry.getKey() + ": " + entry.getValue() + " files"); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java index 0926ceafe..a13c54ddb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java @@ -204,31 +204,6 @@ public class JSTypeInfo { } } - /** - * Resolves a type parameter to its bound TypeInfo. - * For example, if this type has "T extends EntityPlayerMP", resolveTypeParam("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; - } - - /** - * Resolves a type parameter to its bound type name (for backward compatibility). - * @deprecated Use resolveTypeParamToTypeInfo instead - */ - @Deprecated - public String resolveTypeParam(String typeName) { - TypeParamInfo param = getTypeParam(typeName); - if (param != null && param.getBoundTypeInfo() != null) { - return param.getBoundTypeInfo().getSimpleName(); - } - return typeName; - } /** * Get a method by name, including inherited methods. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java index 83e11d317..c08711051 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java @@ -93,7 +93,7 @@ public class JSTypeRegistry { if (dtsFiles.isEmpty()) { Set fallbackFiles = findAllDtsFilesInResources("assets/customnpcs/api"); - System.out.println("[JSTypeRegistry] Found " + fallbackFiles.size() + " .d.ts files in resources"); + // System.out.println("[JSTypeRegistry] Found " + fallbackFiles.size() + " .d.ts files in resources"); if (fallbackFiles.contains("hooks.d.ts")) { loadResourceFile(parser, "hooks.d.ts"); @@ -126,7 +126,7 @@ public class JSTypeRegistry { registerES5Builtins(); initialized = true; - System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from resources"); + // System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from resources"); } catch (Exception e) { System.err.println("[JSTypeRegistry] Failed to load type definitions from resources: " + e.getMessage()); e.printStackTrace(); @@ -276,7 +276,7 @@ public class JSTypeRegistry { } } catch (Exception e) { // File might not exist, that's ok - System.out.println("[JSTypeRegistry] Could not load " + fileName + ": " + e.getMessage()); + // System.out.println("[JSTypeRegistry] Could not load " + fileName + ": " + e.getMessage()); } } @@ -294,7 +294,7 @@ public class JSTypeRegistry { resolveAllJSDocTypes(); resolveInheritance(); initialized = true; - System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks"); + // System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks"); } catch (IOException e) { System.err.println("[JSTypeRegistry] Failed to load type definitions: " + e.getMessage()); e.printStackTrace(); @@ -315,7 +315,7 @@ public class JSTypeRegistry { resolveAllJSDocTypes(); resolveInheritance(); initialized = true; - System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from VSIX"); + // System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from VSIX"); } catch (IOException e) { System.err.println("[JSTypeRegistry] Failed to load VSIX: " + e.getMessage()); e.printStackTrace(); @@ -360,7 +360,7 @@ public class JSTypeRegistry { if (!typesByJavaFqn.containsKey(javaFqn)) { typesByJavaFqn.put(javaFqn, type); } else { - System.out.println("[JSTypeRegistry] Duplicate javaFqn " + javaFqn + " from " + getCurrentSource()); + // System.out.println("[JSTypeRegistry] Duplicate javaFqn " + javaFqn + " from " + getCurrentSource()); } } } @@ -1048,13 +1048,13 @@ public class JSTypeRegistry { private void logTypeCollision(String fullName, String incomingSource) { String existingSource = typeOrigins.getOrDefault(fullName, "unknown"); - System.out.println("[JSTypeRegistry] Duplicate type " + fullName + " from " + incomingSource + " (kept " + existingSource + ")"); + // System.out.println("[JSTypeRegistry] Duplicate type " + fullName + " from " + incomingSource + " (kept " + existingSource + ")"); } private void logAliasCollision(String alias, String existingType, String incomingType, String incomingSource) { String existingSource = aliasOrigins.getOrDefault(alias, "unknown"); - System.out.println("[JSTypeRegistry] Duplicate alias " + alias + " from " + incomingSource + " (kept " + existingSource + ")"); - System.out.println("[JSTypeRegistry] Alias " + alias + " existing=" + existingType + " incoming=" + incomingType); + // System.out.println("[JSTypeRegistry] Duplicate alias " + alias + " from " + incomingSource + " (kept " + existingSource + ")"); + // System.out.println("[JSTypeRegistry] Alias " + alias + " existing=" + existingType + " incoming=" + incomingType); } /** @@ -1162,9 +1162,9 @@ public class JSTypeRegistry { } // Log merge details - System.out.println("[JSTypeRegistry] Merged type " + existing.getFullName() + - " with " + methodsMerged + " methods, " + fieldsMerged + - " fields from " + incomingSource); + // System.out.println("[JSTypeRegistry] Merged type " + existing.getFullName() + + // " with " + methodsMerged + " methods, " + fieldsMerged + + // " fields from " + incomingSource); } /** 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/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index 9b4716ec9..1a744d215 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -428,8 +428,8 @@ public class MethodCallInfo { TypeInfo argType = arg.getResolvedType(); if (argType != null && paramType != null) { if (!TypeChecker.isTypeCompatible(paramType, argType)) { - setArgTypeError(i, "Expected " + paramType.getDisplayName() + - " but got " + argType.getDisplayName()); + setArgTypeError(i, "Expected " + getTypeDisplayName(paramType) + + " but got " + getTypeDisplayName(argType)); } } else if (paramType == null) { setArgTypeError(i, "Parameter type of '" + para.getName() + "' is unresolved"); @@ -439,6 +439,20 @@ public class MethodCallInfo { } } + /** + * in error "Expect T but got String", if T has a resolved bound aka "int" show it instead of T to avoid confusion. + * So the error would be "Expect int but got String" + */ + public static String getTypeDisplayName(TypeInfo type) { + if(type.isTypeParameter()) { + TypeInfo bound = type.getBoundType(); + if (bound != null) + return bound.getDisplayName(); + } + + return type.getDisplayName(); + } + @Override public String toString() { StringBuilder sb = new StringBuilder("MethodCallInfo{"); 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 a16943e4b..ccfc347c5 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 @@ -163,10 +163,10 @@ public final class MethodInfo { if (reflectedReturn.isAssignableFrom(overrideClass)) { if (reflectedReturn != overrideClass) { returnType = overrideReturnType; - System.out.println("[DtsJavaBridge] Overrode return type for " - + method.getDeclaringClass().getName() + "." + name - + "(" + method.getParameterCount() + ") from " - + reflectedReturn.getName() + " to " + overrideClass.getName()); +// System.out.println("[DtsJavaBridge] Overrode return type for " +// + method.getDeclaringClass().getName() + "." + name +// + "(" + method.getParameterCount() + ") from " +// + reflectedReturn.getName() + " to " + overrideClass.getName()); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index 2cb57da62..9306a3c67 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -30,6 +30,7 @@ public enum TokenType { ENUM_CONSTANT(0x55FFFF, 84, true, false), // enum constant values (blue, bold+italic) - like IntelliJ CLASS_DECL(0x00AAAA, 85), // class names in declarations IMPORTED_CLASS(0x00AAAA, 75), // imported class usages + GENERIC_TYPE_PARAM(0x00FA9A, 76), // generic type parameters like T, E, K, V (light green) TYPE_DECL(0x00AAAA, 70), // package paths, type references // Methods @@ -127,6 +128,8 @@ public enum TokenType { case INTERFACE_DECL: case GLOBAL_FIELD: return 'b'; // aqua + case GENERIC_TYPE_PARAM: + return 'a'; // green (distinct from class types) case CLASS_DECL: case IMPORTED_CLASS: case TYPE_DECL: 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..243ceb8d1 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 @@ -85,9 +85,7 @@ public final class GenericContext { String name = declared.getName(); if (name == null || name.isEmpty()) continue; TypeInfo bound = declared.getBoundTypeInfo(); - if (bound != null && bound.isResolved()) { - bounds.put(name, bound); - } + bounds.put(name, (bound != null && bound.isResolved()) ? bound : TypeInfo.OBJECT); } } @@ -150,9 +148,11 @@ 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). + 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..c4b8ff7f1 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,97 @@ 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, 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; + } + // ==================== INHERITANCE ==================== /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index a9516f3e5..9f7e5369e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -33,6 +33,9 @@ public final class TypeChecker { if (expected == null) return true; // void can accept anything (shouldn't happen) if (actual == null) return true; // Can't verify, assume compatible + // Exact match by reference equality + if(expected.equals(actual)) return true; + // Handle array types: compatible if element types are compatible if (expected.isArray() && actual.isArray()) { TypeInfo expectedElement = expected.getElementType(); @@ -78,13 +81,6 @@ public final class TypeChecker { if (expectedName == null || actualName == null) return true; - // Exact match by simple name - if (expectedName.equals(actualName)) return true; - - // Exact match by full name - if (expected.getFullName() != null && actual.getFullName() != null) { - if (expected.getFullName().equals(actual.getFullName())) return true; - } // Primitive widening conversions if ("number".equals(expectedName) && isNumericType(actualName)) { @@ -104,7 +100,16 @@ public final class TypeChecker { return true; } } - + + // Same-name match: handles ScriptTypeInfo, non-Java types, and same-class Java types. + // Must run before isAssignableFrom so type arg checking isn't skipped for same-class pairs. + if (expected.getFullName().equals(actual.getFullName())) { + if (expected.isParameterized() && actual.isParameterized()) { + return areTypeArgumentsCompatible(expected, actual); + } + return true; + } + // Object type compatibility (check inheritance) if (expected.getJavaClass() != null && actual.getJavaClass() != null) { Class expectedClass = expected.getJavaClass(); @@ -160,6 +165,45 @@ public final class TypeChecker { return false; } + private static boolean areTypeArgumentsCompatible(TypeInfo expected, TypeInfo actual) { + java.util.List expectedArgs = expected.getAppliedTypeArgs(); + java.util.List actualArgs = actual.getAppliedTypeArgs(); + + // Actual has no type args — it's a raw type being assigned to a parameterized type. + // In Java this is always valid (unchecked warning, not an error). + if (actualArgs.isEmpty()) { + return true; + } + + // If all of actual's type args are unresolved type parameters (e.g., Box + // from diamond "new Box<>()"), treat as raw/inferred — always compatible. + boolean allActualAreTypeParams = true; + for (TypeInfo actualArg : actualArgs) { + if (!actualArg.isTypeParameter()) { + allActualAreTypeParams = false; + break; + } + } + if (allActualAreTypeParams) { + return true; + } + + for (int i = 0; i < expectedArgs.size(); i++) { + TypeInfo expectedArg = expectedArgs.get(i); + TypeInfo actualArg = actualArgs.get(i); + + if (expectedArg.isTypeParameter() || actualArg.isTypeParameter()) { + continue; + } + + if (!isTypeCompatible(expectedArg, actualArg) && !isTypeCompatible(actualArg, expectedArg)) { + return false; + } + } + + return true; + } + /** * Check if the type is a numeric type (including wrappers). */ 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..2e1fa9aa5 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 @@ -14,11 +14,7 @@ import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import java.lang.reflect.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Supplier; /** @@ -86,6 +82,8 @@ 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") + private TypeInfo boundType; // For type parameters with bounds, stores the bound for resolution // JavaScript/TypeScript type info (for types from .d.ts files) private final JSTypeInfo jsTypeInfo; // The JS type info (null if Java type) @@ -95,11 +93,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 +176,42 @@ 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}). + * Keeps the type parameter's own identity (paramName as simpleName/fullName) but stores the + * bound separately for display. Uses the bound's javaClass/kind for member resolution (erasure semantics). + */ + public static TypeInfo typeParameter(String paramName, TypeInfo boundType) { + Class javaClass = (boundType != null && boundType.javaClass != null) ? boundType.javaClass : Object.class; + Kind kind = (boundType != null) ? boundType.kind : Kind.CLASS; + TypeInfo ti = new TypeInfo(paramName, paramName, "", kind, javaClass, true, null); + ti.typeParameterName = paramName; + ti.boundType = boundType; + return ti; + } + + public boolean isTypeParameter() { + return typeParameterName != null; + } + + public String getTypeParameterName() { + return typeParameterName; + } + + public TypeInfo getBoundType() { + return boundType; + } + public static TypeInfo fromClass(Class clazz) { if (clazz == null) return null; @@ -277,12 +311,14 @@ public class TypeInfo { } if (type instanceof TypeVariable) { - // Type variable like T, E, K, V TypeVariable typeVar = (TypeVariable) type; String varName = typeVar.getName(); - // Create an unresolved type representing the type variable - // The substitution system will handle replacing this with the actual type - return TypeInfo.unresolved(varName, varName); + // Check for bounds - if bounded, create typeParameter with bound + Type[] bounds = typeVar.getBounds(); + if (bounds != null && bounds.length > 0 && bounds[0] != Object.class) { + return TypeInfo.typeParameter(varName, fromGenericType(bounds[0])); + } + return TypeInfo.typeParameter(varName); } if (type instanceof WildcardType) { @@ -454,14 +490,18 @@ public class TypeInfo { // Fallback to Object array if we can't create the specific array type } } - - TypeInfo arr = new TypeInfo(simpleName, fullName, pkg, elementType.kind, arrayClass, true, null, null, null, null, elementType); + TypeInfo arr = new TypeInfo(simpleName, fullName, pkg, elementType.kind, arrayClass, elementType.isResolved(), + null, null, null, null, elementType); arr.setPrimitive(elementType.isPrimitive()); arr.syntheticFields.add(FieldInfo.external("length", TypeInfo.fromPrimitive("int"), null, Modifier.PUBLIC | Modifier.FINAL)); arr.syntheticMethods.add(MethodInfo.external("clone", arr, null, Collections.emptyList(), Modifier.PUBLIC, null)); - return arr; + + arr.typeParameterName = elementType.typeParameterName; + if (elementType.boundType != null) + arr.boundType = arrayOf(elementType.boundType); + return arr; } /** @@ -541,6 +581,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. @@ -645,6 +693,8 @@ public class TypeInfo { public TokenType getTokenType() { if (!resolved) return TokenType.UNDEFINED_VAR; + if (isTypeParameter()) + return TokenType.GENERIC_TYPE_PARAM; if(isPrimitive()) return TokenType.KEYWORD; @@ -1215,19 +1265,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 + "}"; @@ -1236,13 +1273,36 @@ public class TypeInfo { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TypeInfo typeInfo = (TypeInfo) o; - return fullName.equals(typeInfo.fullName); + if (!(o instanceof TypeInfo)) + return false; + TypeInfo other = (TypeInfo) o; + + if (!fullName.equals(other.fullName)) + return false; + + if (typeParameterName != null || other.typeParameterName != null) { + if (!Objects.equals(typeParameterName, other.typeParameterName)) + return false; + if (!Objects.equals(boundType, other.boundType)) + return false; + } + + if(!appliedTypeArgs.equals(other.appliedTypeArgs)) + return false; + + if (!Objects.equals(elementType, other.elementType)) + return false; + + return true; } @Override public int hashCode() { - return fullName.hashCode(); + int result = fullName.hashCode(); + if (typeParameterName != null) + result = 31 * result + typeParameterName.hashCode(); + if (elementType != null) + result = 31 * result + elementType.hashCode(); + return result; } } 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 514174743..2554867d0 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,14 @@ 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. + if (!type.isResolved() || type.isTypeParameter()) { + String lookupKey = type.isTypeParameter() ? type.getTypeParameterName() : typeName; + TypeInfo substitution = bindings.get(lookupKey); if (substitution != null) { return substitution; } @@ -138,60 +143,4 @@ public class TypeSubstitutor { return type; } - /** - * Substitute type variables in a type string using the given bindings. - * Used when we have a raw type string (like "T" or "List") that needs substitution. - * - * @param typeString The raw type string - * @param bindings The map from type variable names to concrete types - * @param resolver The type resolver to use for parsing - * @return The substituted TypeInfo - */ - public static TypeInfo substituteString(String typeString, Map bindings, TypeResolver resolver) { - if (typeString == null || typeString.isEmpty()) { - return null; - } - - // First check if the whole string is a type variable - String trimmed = TypeStringNormalizer.stripImportTypeSyntax(typeString); - trimmed = TypeStringNormalizer.pickPreferredUnionBranch(trimmed); - trimmed = TypeStringNormalizer.stripNullableSuffix(trimmed); - - TypeStringNormalizer.ArraySplit arraySplit = TypeStringNormalizer.splitArraySuffixes(trimmed); - String baseName = arraySplit.base; - int arrayDims = arraySplit.dimensions; - - // Strip generic args to check the base name - String bareBaseName = GenericTypeParser.stripGenerics(baseName); - - // If bare base name is a type variable, substitute - TypeInfo directSubstitution = bindings.get(bareBaseName); - if (directSubstitution != null) { - // It's a type variable, return the substitution - TypeInfo result = directSubstitution; - for (int i = 0; i < arrayDims; i++) { - result = TypeInfo.arrayOf(result); - } - return result; - } - - // Parse and resolve the type, then substitute within it - TypeInfo resolved = resolver.resolveJSType(typeString); - return substitute(resolved, bindings); - } - - /** - * Get the return type for a method on a parameterized receiver, with type variable substitution. - * - * @param rawReturnType The method's raw return type (may contain type variables) - * @param rawReturnTypeString The method's raw return type as a string (for unresolved types) - * @param receiverType The parameterized receiver type - * @param resolver The type resolver - * @return The substituted return type - */ - public static TypeInfo getSubstitutedReturnType(TypeInfo rawReturnType, String rawReturnTypeString, - TypeInfo receiverType, TypeResolver resolver) { - GenericContext ctx = GenericContext.forReceiver(receiverType); - return ctx.substituteType(rawReturnType, rawReturnTypeString, resolver); - } } diff --git a/tools/standalone-JaninoLoader-1.0.1-ALPHA.jar b/tools/standalone-JaninoLoader-1.0.1-ALPHA.jar deleted file mode 100644 index cc5b1c843..000000000 Binary files a/tools/standalone-JaninoLoader-1.0.1-ALPHA.jar and /dev/null differ diff --git a/tools/standalone-JaninoLoader-1.0.2.jar b/tools/standalone-JaninoLoader-1.0.2.jar new file mode 100644 index 000000000..7ff8f23e7 Binary files /dev/null and b/tools/standalone-JaninoLoader-1.0.2.jar differ