Merge branch 'syntax-highlighter-unified' into dev

This commit is contained in:
Kam committed 2026-03-10 21:07:17 -04:00
commit ec152e4171
22 files changed
+903 -254

No files matched your search

+2 -2
View File
@@ -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")
@@ -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),
@@ -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(
@@ -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<Generic>)
@@ -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 <T extends Comparable<T>>.
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<RawTypeDeclaration> 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 <T extends Comparable<T>> 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., <E>, <K, V>, <T extends Entity>)
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<T>" -> "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<T>" -> "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<TypeInfo> 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<ScriptLine.Mark> 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<ScriptLine.Mark> marks, int clauseStart, int clauseEnd, List<TypeParamInfo> typeParams) {
if (clauseStart >= clauseEnd) return;
String clause = text.substring(clauseStart, clauseEnd);
Map<String, TypeParamInfo> 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<int[]> 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<MethodInfo> allMethodsAndConstructors = getAllMethods();
allMethodsAndConstructors.addAll(getAllConstructors());
for (MethodInfo method : allMethodsAndConstructors) {
Map<String, List<FieldInfo>> 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<E>)
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.
*
* <p><b>Phase 1 (structure-only)</b>: parse every object literal with a no-op type
* resolver. This produces {@link ObjectLiteralParser.ObjectLiteralAnalysis} entries
* whose property <em>names</em> 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.</p>
*
* <p><b>Phase 2 (full resolution)</b>: 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}.</p>
*/
private void parseObjectLiterals() {
if (!isJavaScript()) return;
List<int[]> 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<int[]> findObjectLiteralBraces() {
List<int[]> 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., <E>, <K, V>)
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<ScriptLine.Mark> marks, int clauseStart, String parentName) {
private void markExtendsClause(List<ScriptLine.Mark> 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<MethodCallInfo.Argument> arguments = parseMethodArguments(openParen + 1, closeParen, null);
List<MethodCallInfo.Argument> 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<MethodCallInfo.Argument> 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<MethodCallInfo.Argument> parseMethodArguments(int start, int end, MethodInfo methodInfo) {
public List<MethodCallInfo.Argument> parseMethodArguments(int start, int end, MethodInfo methodInfo, TypeInfo receiverType) {
List<MethodCallInfo.Argument> 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>(...))
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<MethodCallInfo.Argument> arguments = parseMethodArguments(openParen + 1, closeParen, null);
List<MethodCallInfo.Argument> 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., <Integer> 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<MethodCallInfo.Argument> 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;
}
@@ -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()) {
@@ -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());
}
}
@@ -160,7 +160,8 @@ public class EnumConstantInfo {
List<MethodCallInfo.Argument> arguments = ScriptDocument.INSTANCE.parseMethodArguments(
openParenPos + 1,
closeParenPos,
null
null,
enumType
);
// Find matching constructor
@@ -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<TypeParamInfo> 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<FieldInfo> 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());
}
@@ -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<String, Integer> entry : domainCounts.entrySet()) {
System.out.println("[JSTypeRegistry] Domain " + entry.getKey() + ": " + entry.getValue() + " files");
// System.out.println("[JSTypeRegistry] Domain " + entry.getKey() + ": " + entry.getValue() + " files");
}
}
@@ -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.
@@ -93,7 +93,7 @@ public class JSTypeRegistry {
if (dtsFiles.isEmpty()) {
Set<String> 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);
}
/**
@@ -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.
@@ -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{");
@@ -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());
}
}
}
@@ -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:
@@ -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;
}
@@ -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<String, EnumConstantInfo> enumConstants = new HashMap<>();
// Generic type parameters declared on this type (e.g., <E>, <K, V>, <T extends Entity>)
private final List<TypeParamInfo> 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<ScriptTypeInfo> getInnerClasses() { return innerClasses; }
// ==================== GENERIC TYPE PARAMETERS ====================
public void addDeclaredTypeParam(TypeParamInfo param) {
declaredTypeParams.add(param);
}
public List<TypeParamInfo> 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<TypeParamInfo> 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<FieldInfo> params = new ArrayList<>(m.getParameters());
boolean changed = subRet != retType;
for (int i = 0; i < params.size(); i++) {
FieldInfo p = params.get(i);
TypeInfo subP = ctx.substitute(p.getTypeInfo());
if (subP != p.getTypeInfo()) {
params.set(i, FieldInfo.reflectionParam(p.getName(), subP));
changed = true;
}
}
return changed ? MethodInfo.external(m.getName(), subRet, m.getContainingType(),
params, m.getModifiers(), m.getDocumentation()) : m;
}
@Override
public TypeInfo parameterize(List<TypeInfo> typeArgs) {
if (typeArgs == null || typeArgs.isEmpty()) return this;
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<String, List<MethodInfo>> entry : this.methods.entrySet()) {
List<MethodInfo> 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<String, FieldInfo> entry : this.fields.entrySet()) {
FieldInfo f = entry.getValue();
TypeInfo subType = ctx.substitute(f.getTypeInfo());
result.fields.put(entry.getKey(), subType != f.getTypeInfo()
? FieldInfo.external(f.getName(), subType, null, f.getModifiers()) : f);
}
return result;
}
// ==================== INHERITANCE ====================
/**
@@ -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<TypeInfo> expectedArgs = expected.getAppliedTypeArgs();
java.util.List<TypeInfo> 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<T>
// 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).
*/
@@ -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., <String, Integer> in Map<String, Integer>)
// These are TypeInfo instances that can themselves be parameterized for nested generics
private final List<TypeInfo> appliedTypeArgs = new ArrayList<>();
private List<TypeInfo> appliedTypeArgs = new ArrayList<>();
// For parameterized types, the raw/base type (e.g., List for List<String>)
// 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<T>}).
* 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<TypeInfo> getAppliedTypeArgs() {
return appliedTypeArgs;
}
protected void setRawType(TypeInfo rawType) {
this.rawType = rawType;
}
protected void setAppliedTypeArgs(List<TypeInfo> 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;
}
}
@@ -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<T>") 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<String, TypeInfo> 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);
}
}
Binary file not shown.
Binary file not shown.