diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java index 00557fa6a..d3fe10820 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java @@ -1,8 +1,12 @@ package noppes.npcs.client.gui.util.script.interpreter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Represents a single import statement with all its metadata. - * Tracks the full import path, resolution status, and source positions. + * Tracks the full import path, resolution status, source positions, and references. */ public final class ImportData { @@ -17,6 +21,12 @@ public final class ImportData { private TypeInfo resolvedType; // null if wildcard or unresolved private boolean resolved; // whether resolution was attempted and succeeded + + // Track usage count (simpler than tracking individual tokens) + private int usageCount = 0; + + // Track all tokens that reference this import (optional, for detailed analysis) + private final List referencingTokens = new ArrayList<>(); public ImportData(String fullPath, String simpleName, boolean isWildcard, boolean isStatic, int startOffset, int endOffset, int pathStartOffset, int pathEndOffset) { @@ -57,6 +67,62 @@ public final class ImportData { public void markResolved(boolean resolved) { this.resolved = resolved; } + + // ==================== REFERENCE TRACKING ==================== + + /** + * Increment the usage count for this import. + * Called during type resolution when this import is used. + */ + public void incrementUsage() { + usageCount++; + } + + /** + * Add a token that references this import. + * Called when a token is created that uses this import's type. + */ + public void addReference(Token token) { + if (token != null && !referencingTokens.contains(token)) { + referencingTokens.add(token); + } + } + + /** + * Get all tokens that reference this import. + */ + public List getReferencingTokens() { + return Collections.unmodifiableList(referencingTokens); + } + + /** + * Check if this import is used (has any references or usages). + */ + public boolean isUsed() { + return usageCount > 0 || !referencingTokens.isEmpty(); + } + + /** + * Get the usage count for this import. + */ + public int getUsageCount() { + return usageCount; + } + + /** + * Get the number of references to this import. + */ + public int getReferenceCount() { + return referencingTokens.size(); + } + + /** + * Clear all references and reset usage count (used when re-tokenizing). + */ + public void clearReferences() { + referencingTokens.clear(); + usageCount = 0; + } /** * Get the package portion of the import path. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java new file mode 100644 index 000000000..79b7198a5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java @@ -0,0 +1,395 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import scala.annotation.meta.param; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Stores information about a method call for argument validation. + * This includes the method name, the arguments passed, and validation results. + */ +public class MethodCallInfo { + + /** + * Represents a single argument in a method call. + */ + public static class Argument { + private final String text; // The text of the argument expression + private final int startOffset; // Start position in source + private final int endOffset; // End position in source + private final TypeInfo resolvedType; // The resolved type of the argument (null if unresolved) + private final boolean valid; // Whether this arg matches the expected parameter type + private final String errorMessage; // Error message if invalid + + public Argument(String text, int startOffset, int endOffset, TypeInfo resolvedType, + boolean valid, String errorMessage) { + this.text = text; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.resolvedType = resolvedType; + this.valid = valid; + this.errorMessage = errorMessage; + } + + public String getText() { + return text; + } + + public int getStartOffset() { + return startOffset; + } + + public int getEndOffset() { + return endOffset; + } + + public TypeInfo getResolvedType() { + return resolvedType; + } + + public boolean isValid() { + return valid; + } + + public String getErrorMessage() { + return errorMessage; + } + + public boolean equals(Token t) { + return text.equals(t.getText()) && + startOffset == t.getGlobalStart() && + endOffset == t.getGlobalEnd(); + } + + @Override + public String toString() { + return "Arg{" + text + " [" + startOffset + "-" + endOffset + "]" + + (resolvedType != null ? " :" + resolvedType.getSimpleName() : "") + + (valid ? "" : " INVALID: " + errorMessage) + "}"; + } + } + + /** + * Validation error type. + */ + public enum ErrorType { + NONE, + WRONG_ARG_COUNT, // Number of args doesn't match any overload + WRONG_ARG_TYPE, // Specific argument has wrong type + STATIC_ACCESS_ERROR, // Trying to call instance method statically or vice versa + UNRESOLVED_METHOD, // Method doesn't exist + UNRESOLVED_RECEIVER // Can't resolve the receiver type + } + + private final String methodName; + private final int methodNameStart; // Position of method name + private final int methodNameEnd; + private final int openParenOffset; // Position of '(' + private final int closeParenOffset; // Position of ')' + private final List arguments; + private final TypeInfo receiverType; // The type on which this method is called (null for standalone) + private final MethodInfo resolvedMethod; // The resolved method (null if unresolved) + private final boolean isStaticAccess; // True if this is Class.method() style access + + private ErrorType errorType = ErrorType.NONE; + private String errorMessage; + private int errorArgIndex = -1; // Index of the problematic argument (for WRONG_ARG_TYPE) + private List argumentTypeErrors = new ArrayList<>(); + + public MethodCallInfo(String methodName, int methodNameStart, int methodNameEnd, + int openParenOffset, int closeParenOffset, + List arguments, TypeInfo receiverType, + MethodInfo resolvedMethod) { + this(methodName, methodNameStart, methodNameEnd, openParenOffset, closeParenOffset, + arguments, receiverType, resolvedMethod, false); + } + + public MethodCallInfo(String methodName, int methodNameStart, int methodNameEnd, + int openParenOffset, int closeParenOffset, + List arguments, TypeInfo receiverType, + MethodInfo resolvedMethod, boolean isStaticAccess) { + this.methodName = methodName; + this.methodNameStart = methodNameStart; + this.methodNameEnd = methodNameEnd; + this.openParenOffset = openParenOffset; + this.closeParenOffset = closeParenOffset; + this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>(); + this.receiverType = receiverType; + this.resolvedMethod = resolvedMethod; + this.isStaticAccess = isStaticAccess; + } + + // Getters + public String getMethodName() { + return methodName; + } + + public int getMethodNameStart() { + return methodNameStart; + } + + public int getMethodNameEnd() { + return methodNameEnd; + } + + public int getOpenParenOffset() { + return openParenOffset; + } + + public int getCloseParenOffset() { + return closeParenOffset; + } + + public List getArguments() { + return Collections.unmodifiableList(arguments); + } + + public int getArgumentCount() { + return arguments.size(); + } + + public TypeInfo getReceiverType() { + return receiverType; + } + + public MethodInfo getResolvedMethod() { + return resolvedMethod; + } + + public boolean isStaticAccess() { + return isStaticAccess; + } + + public ErrorType getErrorType() { + return errorType; + } + + public String getErrorMessage() { + return errorMessage; + } + + /** + * Get the full span of the method call including parentheses. + * Used for underlining the entire call on arg count errors. + */ + public int getFullCallStart() { + return methodNameStart; + } + + public int getFullCallEnd() { + return closeParenOffset + 1; + } + + /** + * Check if this method call has any validation errors. + */ + public boolean hasError() { + return errorType != ErrorType.NONE; + } + + /** + * Check if this is an arg count error (underline whole call). + */ + public boolean hasArgCountError() { + return errorType == ErrorType.WRONG_ARG_COUNT; + } + + /** + * Check if this is an arg type error (underline specific arg). + */ + public boolean hasArgTypeError() { + return !this.argumentTypeErrors.isEmpty(); + } + public boolean hasArgTypeError(Token t) { + for (ArgumentTypeError ate : this.argumentTypeErrors) { + if (ate.getArg().equals(t)) { + return true; + } + } + return false; + } + + /** + * Check if this is a static access error (underline method name). + */ + public boolean hasStaticAccessError() { + return errorType == ErrorType.STATIC_ACCESS_ERROR; + } + + // Setters for validation results + public void setError(ErrorType type, String message) { + this.errorType = type; + this.errorMessage = message; + } + + public void setArgTypeError(int argIndex, String message) { + this.argumentTypeErrors.add(new ArgumentTypeError(arguments.get(argIndex), argIndex, message)); + } + + + public class ArgumentTypeError { + private ErrorType type = ErrorType.WRONG_ARG_TYPE; + private final Argument arg; + private final int argIndex; + private final String message; + + public ArgumentTypeError(Argument arg, int argIndex, String message) { + this.arg = arg; + this.argIndex = argIndex; + this.message = message; + } + + public int getArgIndex() { + return argIndex; + } + + public String getMessage() { + return message; + } + + public Argument getArg() { + return arg; + } + } + + /** + * Validate this method call against the resolved method signature. + * Sets error information if validation fails. + */ + public void validate() { + if (resolvedMethod == null) { + setError(ErrorType.UNRESOLVED_METHOD, "Cannot resolve method '" + methodName + "'"); + return; + } + + // Check static/instance access + if (isStaticAccess && !resolvedMethod.isStatic()) { + setError(ErrorType.STATIC_ACCESS_ERROR, + "Cannot call instance method '" + methodName + "' on a class type"); + return; + } + + List params = resolvedMethod.getParameters(); + int expectedCount = params.size(); + int actualCount = arguments.size(); + + // Check arg count + if (actualCount != expectedCount) { + setError(ErrorType.WRONG_ARG_COUNT, + "Expected " + expectedCount + " argument(s) but got " + actualCount); + return; + } + + // Check each argument type + for (int i = 0; i < actualCount; i++) { + Argument arg = arguments.get(i); + FieldInfo para = params.get(i); + + TypeInfo argType = arg.getResolvedType(); + TypeInfo paramType = para.getDeclaredType(); + if (argType != null && paramType != null) { + if (!isTypeCompatible(argType, paramType)) { + setArgTypeError(i, "Expected " + paramType.getSimpleName() + + " but got " + argType.getSimpleName()); + return; + } + } else if (paramType == null) { + setArgTypeError(i, "Parameter type of '" + para.getName() + "' is unresolved"); + return; + } else if (argType == null) { + setArgTypeError(i, "Cannot resolve type of argument '" + arg.getText() + "'"); + return; + } + } + } + + /** + * Check if sourceType can be assigned to targetType. + * Handles primitive widening, inheritance, etc. + */ + private boolean isTypeCompatible(TypeInfo sourceType, TypeInfo targetType) { + if (sourceType == null || targetType == null) { + return true; // Can't verify, assume compatible + } + + if (sourceType.equals(targetType)) { + return true; + } + + Class sourceClass = sourceType.getJavaClass(); + Class targetClass = targetType.getJavaClass(); + + if (sourceClass == null || targetClass == null) { + return true; // Can't verify, assume compatible + } + + // Check inheritance + if (targetClass.isAssignableFrom(sourceClass)) { + return true; + } + + // Check primitive widening conversions + if (isPrimitiveWidening(sourceClass, targetClass)) { + return true; + } + + return false; + } + + /** + * Check if widening conversion is valid between primitives. + */ + private boolean isPrimitiveWidening(Class source, Class target) { + // byte -> short -> int -> long -> float -> double + // char -> int -> long -> float -> double + if (target == double.class || target == Double.class) { + return source == float.class || source == Float.class || + source == long.class || source == Long.class || + source == int.class || source == Integer.class || + source == short.class || source == Short.class || + source == byte.class || source == Byte.class || + source == char.class || source == Character.class; + } + if (target == float.class || target == Float.class) { + return source == long.class || source == Long.class || + source == int.class || source == Integer.class || + source == short.class || source == Short.class || + source == byte.class || source == Byte.class || + source == char.class || source == Character.class; + } + if (target == long.class || target == Long.class) { + return source == int.class || source == Integer.class || + source == short.class || source == Short.class || + source == byte.class || source == Byte.class || + source == char.class || source == Character.class; + } + if (target == int.class || target == Integer.class) { + return source == short.class || source == Short.class || + source == byte.class || source == Byte.class || + source == char.class || source == Character.class; + } + if (target == short.class || target == Short.class) { + return source == byte.class || source == Byte.class; + } + return false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MethodCallInfo{"); + sb.append(methodName).append("("); + for (int i = 0; i < arguments.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(arguments.get(i).getText()); + } + sb.append(")"); + if (hasError()) { + sb.append(" ERROR: ").append(errorType).append(" - ").append(errorMessage); + } + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index 58f229797..93f393c40 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -19,10 +20,12 @@ public final class MethodInfo { private final int bodyEnd; // End of method body (before }) private final boolean resolved; private final boolean isDeclaration; // true if this is a declaration, false if it's a call + private final boolean isStatic; // true if this is a static method private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, List parameters, int declarationOffset, - int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration) { + int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration, + boolean isStatic) { this.name = name; this.returnType = returnType; this.containingType = containingType; @@ -32,12 +35,18 @@ public final class MethodInfo { this.bodyEnd = bodyEnd; this.resolved = resolved; this.isDeclaration = isDeclaration; + this.isStatic = isStatic; } // Factory methods public static MethodInfo declaration(String name, TypeInfo returnType, List params, int declOffset, int bodyStart, int bodyEnd) { - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true); + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, false); + } + + public static MethodInfo declaration(String name, TypeInfo returnType, List params, + int declOffset, int bodyStart, int bodyEnd, boolean isStatic) { + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, isStatic); } public static MethodInfo call(String name, TypeInfo containingType, int paramCount) { @@ -47,7 +56,7 @@ public final class MethodInfo { for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false); + return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false, false); } public static MethodInfo unresolvedCall(String name, int paramCount) { @@ -55,7 +64,7 @@ public final class MethodInfo { for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, null, params, -1, -1, -1, false, false); + return new MethodInfo(name, null, null, params, -1, -1, -1, false, false, false); } /** @@ -65,6 +74,7 @@ public final class MethodInfo { public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInfo containingType) { String name = method.getName(); TypeInfo returnType = TypeInfo.fromClass(method.getReturnType()); + boolean isStatic = Modifier.isStatic(method.getModifiers()); List params = new ArrayList<>(); Class[] paramTypes = method.getParameterTypes(); @@ -73,7 +83,7 @@ public final class MethodInfo { params.add(FieldInfo.reflectionParam("arg" + i, paramType)); } - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false); + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false, isStatic); } // Getters @@ -88,6 +98,7 @@ public final class MethodInfo { public boolean isResolved() { return resolved; } public boolean isDeclaration() { return isDeclaration; } public boolean isCall() { return !isDeclaration; } + public boolean isStatic() { return isStatic; } /** * Check if a position is inside this method's body. 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 8cbe9b96c..e5ecc117a 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 @@ -68,6 +68,9 @@ public class ScriptDocument { private final Set wildcardPackages = new HashSet<>(); private Map importsBySimpleName = new HashMap<>(); + // Script-defined types (classes, interfaces, enums defined in the script) + private final Map scriptTypes = new HashMap<>(); + // Local variables per method (methodStartOffset -> {varName -> FieldInfo}) private final Map> methodLocals = new HashMap<>(); @@ -176,6 +179,7 @@ public class ScriptDocument { wildcardPackages.clear(); excludedRanges.clear(); methodLocals.clear(); + scriptTypes.clear(); // Phase 1: Find excluded regions (strings/comments) findExcludedRanges(); @@ -183,7 +187,7 @@ public class ScriptDocument { // Phase 2: Parse imports parseImports(); - // Phase 3: Parse structure (methods, fields, locals) + // Phase 3: Parse structure (script types, methods, fields, locals) parseStructure(); // Phase 4: Build marks and assign to lines @@ -291,6 +295,14 @@ public class ScriptDocument { // ==================== PHASE 3: STRUCTURE ==================== private void parseStructure() { + // Clear import references before re-parsing + for (ImportData imp : imports) { + imp.clearReferences(); + } + + // Parse script-defined types (classes, interfaces, enums) + parseScriptTypes(); + // Parse methods parseMethodDeclarations(); @@ -301,6 +313,141 @@ public class ScriptDocument { parseGlobalFields(); } + /** + * Parse class, interface, and enum declarations defined in the script. + * Creates ScriptTypeInfo instances and stores them for later resolution. + */ + private void parseScriptTypes() { + // Pattern: (class|interface|enum) ClassName { ... } + Pattern typeDecl = Pattern.compile( + "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:extends\\s+[A-Za-z_][a-zA-Z0-9_.]*)?\\s*(?:implements\\s+[A-Za-z_][a-zA-Z0-9_.,\\s]*)?\\s*\\{"); + + Matcher m = typeDecl.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String kindStr = m.group(1); + String typeName = m.group(2); + int bodyStart = text.indexOf('{', m.start()); + int bodyEnd = findMatchingBrace(bodyStart); + + if (bodyEnd < 0) { + bodyEnd = text.length(); + } + + TypeInfo.Kind kind; + switch (kindStr) { + case "interface": kind = TypeInfo.Kind.INTERFACE; break; + case "enum": kind = TypeInfo.Kind.ENUM; break; + default: kind = TypeInfo.Kind.CLASS; break; + } + + ScriptTypeInfo scriptType = ScriptTypeInfo.create( + typeName, kind, m.start(), bodyStart, bodyEnd); + + // Parse fields and methods inside this type + parseScriptTypeMembers(scriptType); + + scriptTypes.put(typeName, scriptType); + } + } + + /** + * Parse fields and methods inside a script-defined type. + */ + private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { + int bodyStart = scriptType.getBodyStart(); + int bodyEnd = scriptType.getBodyEnd(); + + if (bodyStart < 0 || bodyEnd <= bodyStart) return; + + String bodyText = text.substring(bodyStart + 1, Math.min(bodyEnd, text.length())); + + // Parse field declarations + Pattern fieldPattern = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>,\\s\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + Matcher fm = fieldPattern.matcher(bodyText); + while (fm.find()) { + int absPos = bodyStart + 1 + fm.start(); + if (isExcluded(absPos)) continue; + + String typeName = fm.group(1).trim(); + String fieldName = fm.group(2); + + // Skip if this looks like a method declaration + if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || + typeName.equals("for") || typeName.equals("switch") || typeName.equals("catch") || + typeName.equals("new") || typeName.equals("throw")) { + continue; + } + + // Skip if inside a nested method body + if (isInsideNestedMethod(absPos, bodyStart, bodyEnd)) { + continue; + } + + TypeInfo fieldType = resolveType(typeName); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, fieldType, absPos); + scriptType.addField(fieldInfo); + } + + // Parse method declarations + Pattern methodPattern = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*\\{"); + Matcher mm = methodPattern.matcher(bodyText); + while (mm.find()) { + int absPos = bodyStart + 1 + mm.start(); + if (isExcluded(absPos)) continue; + + String returnTypeName = mm.group(1); + String methodName = mm.group(2); + String paramList = mm.group(3); + + // Skip class/interface/enum keywords + if (returnTypeName.equals("class") || returnTypeName.equals("interface") || + returnTypeName.equals("enum")) { + continue; + } + + int methodBodyStart = bodyStart + 1 + mm.end() - 1; + int methodBodyEnd = findMatchingBrace(methodBodyStart); + if (methodBodyEnd < 0) methodBodyEnd = bodyEnd; + + TypeInfo returnType = resolveType(returnTypeName); + List params = parseParametersWithPositions(paramList, + bodyStart + 1 + mm.start(3)); + + MethodInfo methodInfo = MethodInfo.declaration( + methodName, returnType, params, absPos, methodBodyStart, methodBodyEnd); + scriptType.addMethod(methodInfo); + } + } + + /** + * Check if a position is inside a nested method body within a class. + * This prevents field declarations inside methods from being treated as class fields. + */ + private boolean isInsideNestedMethod(int position, int classBodyStart, int classBodyEnd) { + String bodyText = text.substring(classBodyStart + 1, Math.min(classBodyEnd, text.length())); + int relativePos = position - classBodyStart - 1; + + Pattern methodPattern = Pattern.compile( + "\\b[A-Za-z_][a-zA-Z0-9_<>\\[\\]]*\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*\\([^)]*\\)\\s*\\{"); + Matcher m = methodPattern.matcher(bodyText); + + while (m.find()) { + int methodBodyStart = m.end() - 1; + int absMethodBodyStart = classBodyStart + 1 + methodBodyStart; + int absMethodBodyEnd = findMatchingBrace(absMethodBodyStart); + + if (absMethodBodyEnd > 0 && position > absMethodBodyStart && position < absMethodBodyEnd) { + return true; + } + } + return false; + } + private void parseMethodDeclarations() { Pattern methodWithBody = Pattern.compile( "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*\\{"); @@ -396,6 +543,7 @@ public class ScriptDocument { String typeName = m.group(1); String varName = m.group(2); + String delimiter = m.group(3); // Skip if it looks like a method call or control flow if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || @@ -404,13 +552,205 @@ public class ScriptDocument { continue; } - TypeInfo typeInfo = resolveType(typeName); + TypeInfo typeInfo; + + // For var/let/const, infer type from the right-hand side expression + if ((typeName.equals("var") || typeName.equals("let") || typeName.equals("const")) + && delimiter.equals("=")) { + // Find the right-hand side expression + int rhsStart = bodyStart + m.end(); + typeInfo = inferTypeFromExpression(rhsStart); + } else { + typeInfo = resolveType(typeName); + } + int declPos = bodyStart + m.start(2); FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method); locals.put(varName, fieldInfo); } } } + + /** + * Infer the type of an expression starting at the given position. + * Handles patterns like: + * - "new TypeName()" - returns TypeName + * - "receiver.fieldOrMethod" - resolves chain and returns result type + * - "variable" - looks up variable type + * - Literals like numbers, strings, booleans + */ + private TypeInfo inferTypeFromExpression(int position) { + // Skip whitespace + while (position < text.length() && Character.isWhitespace(text.charAt(position))) + position++; + + if (position >= text.length()) + return null; + + // Check for 'new' keyword + if (text.startsWith("new ", position)) { + position += 4; + while (position < text.length() && Character.isWhitespace(text.charAt(position))) + position++; + + // Read the type name + int typeStart = position; + while (position < text.length() && Character.isJavaIdentifierPart(text.charAt(position))) + position++; + + if (position > typeStart) { + String typeName = text.substring(typeStart, position); + return resolveType(typeName); + } + return null; + } + + // Check for string literal + if (position < text.length() && (text.charAt(position) == '"' || text.charAt(position) == '\'')) { + return resolveType("String"); + } + + // Check for numeric literal + if (position < text.length() && Character.isDigit(text.charAt(position))) { + // Check for float/double (has decimal point or f/d suffix) + int numEnd = position; + boolean hasDecimal = false; + while (numEnd < text.length() && (Character.isDigit(text.charAt(numEnd)) || + text.charAt(numEnd) == '.' || text.charAt(numEnd) == 'f' || + text.charAt(numEnd) == 'd' || text.charAt(numEnd) == 'F' || + text.charAt(numEnd) == 'D' || text.charAt(numEnd) == 'L' || + text.charAt(numEnd) == 'l')) { + if (text.charAt(numEnd) == '.') hasDecimal = true; + numEnd++; + } + String num = text.substring(position, numEnd).toLowerCase(); + if (num.endsWith("f")) return resolveType("float"); + if (num.endsWith("d") || hasDecimal) return resolveType("double"); + if (num.endsWith("l")) return resolveType("long"); + return resolveType("int"); + } + + // Check for boolean literals + if (text.startsWith("true", position) || text.startsWith("false", position)) { + return resolveType("boolean"); + } + + // Check for null + if (text.startsWith("null", position)) { + return null; // Can't infer type from null + } + + // Must be an identifier or chain - read the first identifier + if (Character.isJavaIdentifierStart(text.charAt(position))) { + int identStart = position; + while (position < text.length() && Character.isJavaIdentifierPart(text.charAt(position))) + position++; + String ident = text.substring(identStart, position); + + // Skip whitespace + while (position < text.length() && Character.isWhitespace(text.charAt(position))) + position++; + + // Check if this is the start of a chain (followed by .) + if (position < text.length() && text.charAt(position) == '.') { + // This is a chain like "event.player" or "Minecraft.getMinecraft()" + // We need to resolve the entire chain to get the final type + return inferChainType(ident, identStart, position); + } + + // Check if this is a method call (followed by ()) + if (position < text.length() && text.charAt(position) == '(') { + // Method call - check if it's a script method + if (isScriptMethod(ident)) { + MethodInfo methodInfo = getScriptMethodInfo(ident); + return (methodInfo != null) ? methodInfo.getReturnType() : null; + } + return null; + } + + // Just a variable - resolve its type + FieldInfo varInfo = resolveVariable(ident, identStart); + return (varInfo != null) ? varInfo.getTypeInfo() : null; + } + + return null; + } + + /** + * Infer the type from a chain expression starting with the given identifier. + */ + private TypeInfo inferChainType(String firstIdent, int identStart, int dotPosition) { + TypeInfo currentType = null; + + // Resolve the first segment + if (Character.isUpperCase(firstIdent.charAt(0))) { + // Static access like Event.player + currentType = resolveType(firstIdent); + } else { + // Variable access + FieldInfo varInfo = resolveVariable(firstIdent, identStart); + currentType = (varInfo != null) ? varInfo.getTypeInfo() : null; + } + + if (currentType == null || !currentType.isResolved()) + return null; + + // Now resolve the rest of the chain + int pos = dotPosition; + while (pos < text.length() && text.charAt(pos) == '.') { + pos++; // Skip the dot + + // Skip whitespace + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + + if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) + break; + + // Read the next identifier + int segStart = pos; + while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) + pos++; + String segment = text.substring(segStart, pos); + + // Skip whitespace + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + + // Check if this is a method call + if (pos < text.length() && text.charAt(pos) == '(') { + // Method call + if (currentType.hasMethod(segment)) { + MethodInfo methodInfo = currentType.getMethodInfo(segment); + currentType = (methodInfo != null) ? methodInfo.getReturnType() : null; + } else { + return null; + } + + // Skip to after the closing paren + int closeParen = findMatchingParen(pos); + if (closeParen < 0) return null; + pos = closeParen + 1; + } else { + // Field access + if (currentType.hasField(segment)) { + FieldInfo fieldInfo = currentType.getFieldInfo(segment); + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + } else { + return null; + } + } + + if (currentType == null || !currentType.isResolved()) + return null; + + // Skip whitespace for next iteration + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + } + + return currentType; + } private void parseGlobalFields() { Matcher m = FIELD_DECL_PATTERN.matcher(text); @@ -440,6 +780,14 @@ public class ScriptDocument { } private TypeInfo resolveType(String typeName) { + return resolveTypeAndTrackUsage(typeName); + } + + /** + * Resolve a type and track the import usage for unused import detection. + * Checks script-defined types first, then falls back to imported types. + */ + private TypeInfo resolveTypeAndTrackUsage(String typeName) { if (typeName == null || typeName.isEmpty()) return null; @@ -450,8 +798,35 @@ public class ScriptDocument { // Strip array brackets baseName = baseName.replace("[]", "").trim(); - // Try to resolve - return typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); + // Check script-defined types FIRST + if (scriptTypes.containsKey(baseName)) { + return scriptTypes.get(baseName); + } + + // Check if there's an explicit import for this type + ImportData usedImport = importsBySimpleName.get(baseName); + + // Try to resolve through imports/classpath + TypeInfo result = typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); + + // Track the import usage if we found one and the resolution succeeded + if (result != null && usedImport != null) { + usedImport.incrementUsage(); + } + + // Also check wildcard imports if the type was resolved through one + if (result != null && usedImport == null && wildcardPackages != null) { + String resultPkg = result.getPackageName(); + // Check if resolved through a wildcard + for (ImportData imp : imports) { + if (imp.isWildcard() && resultPkg != null && resultPkg.equals(imp.getFullPath())) { + imp.incrementUsage(); + break; + } + } + } + + return result; } // ==================== PHASE 4: BUILD MARKS ==================== @@ -493,9 +868,27 @@ public class ScriptDocument { // Imported class usages markImportedClassUsages(marks); + + // Mark unused imports (after all other marks are built) + markUnusedImports(marks); return marks; } + + /** + * Find and mark unused imports as UNUSED_IMPORT type. + * This must be called after all other mark building is complete. + */ + private void markUnusedImports(List marks) { + for (ImportData imp : imports) { + if (!imp.isUsed() && imp.isResolved() && !imp.isWildcard()) { + // This import is not used - find and update its marks + // Mark the entire import line as unused (gray color) + marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getEndOffset(), + TokenType.UNUSED_IMPORT, imp)); + } + } + } private void addPatternMarks(List marks, Pattern pattern, TokenType type) { addPatternMarks(marks, pattern, type, 0); @@ -860,6 +1253,13 @@ public class ScriptDocument { if (isExcluded(m.start())) continue; + // Skip class/interface/enum declarations - these look like method declarations + // but "class Foo(" is not a method, it's a class declaration + String returnType = m.group(1); + if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum")) { + continue; + } + // Return type marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); // Method name @@ -870,69 +1270,345 @@ public class ScriptDocument { private void markMethodCalls(List marks) { Matcher m = METHOD_CALL_PATTERN.matcher(text); while (m.find()) { - int start = m.start(1); - int end = m.end(1); + int nameStart = m.start(1); + int nameEnd = m.end(1); String methodName = m.group(1); - if (isExcluded(start)) + if (isExcluded(nameStart)) continue; // Skip if in import/package statement - if (isInImportOrPackage(start)) + if (isInImportOrPackage(nameStart)) continue; + // Find the opening parenthesis by scanning forward from the method name end + // The regex includes \( but we scan manually to be safe + int openParen = nameEnd; + while (openParen < text.length() && Character.isWhitespace(text.charAt(openParen))) { + openParen++; + } + + if (openParen >= text.length() || text.charAt(openParen) != '(') { + // Not actually a method call + continue; + } + + // Find the matching closing parenthesis + int closeParen = findMatchingParen(openParen); + if (closeParen < 0) { + // Malformed - no closing paren + continue; + } + + // Parse the arguments + List arguments = parseMethodArguments(openParen + 1, closeParen); + + // Check if this is a static access (Class.method() style) + boolean isStaticAccess = isStaticAccessCall(nameStart); + // Resolve the receiver chain and get the final type - TypeInfo receiverType = resolveReceiverChain(start); - Object wrongArg; + TypeInfo receiverType = resolveReceiverChain(nameStart); + MethodInfo resolvedMethod = null; if (receiverType != null) { // We have a resolved receiver type - check if method exists if (receiverType.hasMethod(methodName)) { - MethodInfo methodInfo = receiverType.getMethodInfo(methodName); - // TODO: Add argument validation here in future - marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL, methodInfo)); + resolvedMethod = receiverType.getMethodInfo(methodName); + + // Create MethodCallInfo for validation (with static access flag) + MethodCallInfo callInfo = new MethodCallInfo( + methodName, nameStart, nameEnd, openParen, closeParen, + arguments, receiverType, resolvedMethod, isStaticAccess + ); + callInfo.validate(); + + // Always mark method call as green (METHOD_CALL), with error info attached for underline + // The MethodCallInfo is attached so rendering can draw curly underline if there's an error + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } else { // Method doesn't exist on this type - marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } } else { // No receiver OR receiver couldn't be resolved // Check if there's a dot before this (meaning there WAS a receiver, we just couldn't resolve it) - boolean hasDot = isPrecededByDot(start); + boolean hasDot = isPrecededByDot(nameStart); if (hasDot) { // There was a receiver but we couldn't resolve it - mark as undefined - marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } else { // No receiver - standalone method call // Check if this method is defined in the script itself if (isScriptMethod(methodName)) { - // TODO: Add argument validation here in future - - marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); + resolvedMethod = getScriptMethodInfo(methodName); + + // Create MethodCallInfo for validation + MethodCallInfo callInfo = new MethodCallInfo( + methodName, nameStart, nameEnd, openParen, closeParen, + arguments, null, resolvedMethod + ); + callInfo.validate(); + + // Always mark method call as green (METHOD_CALL), with error info attached for underline + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } else { // Unknown standalone method - mark as undefined - marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } } } } } - - static { - int x = 5; - int y = 10; - Minecraft mc = null; - - //Type 1: - // foo(x,y); - - //Type 2: - // foo(x,y,mc); + + /** + * Check if a method call is a static access (Class.method() style). + * Returns true if the immediate receiver before the dot is a class name (uppercase). + */ + private boolean isStaticAccessCall(int methodNameStart) { + // Walk backward to find the dot + int pos = methodNameStart - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) + pos--; + + if (pos < 0 || text.charAt(pos) != '.') + return false; + + // Skip the dot and any whitespace + pos--; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) + pos--; + + if (pos < 0) + return false; + + // Check what's before the dot - could be: + // 1. An identifier ending (field or class name) + // 2. A closing paren (method call result) + // 3. A closing bracket (array access) + + char c = text.charAt(pos); + if (c == ')' || c == ']') { + // Method call or array - this is instance access + return false; + } + + if (!Character.isJavaIdentifierPart(c)) + return false; + + // Read the identifier backward + int identEnd = pos + 1; + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) + pos--; + int identStart = pos + 1; + + String ident = text.substring(identStart, identEnd); + + // Check if this is a class name (starts with uppercase) + // AND check if there's no dot before it (making it a direct class reference) + // If there's a chain like "obj.SomeClass.method()", we need to check further + + // Skip whitespace before the identifier + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) + pos--; + + // If preceded by a dot, this might be part of a longer chain + // In that case, it's not a direct static access + if (pos >= 0 && text.charAt(pos) == '.') { + return false; + } + + // It's static access if the identifier starts with uppercase + return !ident.isEmpty() && Character.isUpperCase(ident.charAt(0)); } - public static int foo(int x, int y, Boolean z) { - return x + 10; + /** + * Find the matching closing parenthesis for an opening parenthesis. + * Handles nested parentheses, strings, and comments. + */ + private int findMatchingParen(int openPos) { + if (openPos < 0 || openPos >= text.length() || text.charAt(openPos) != '(') { + return -1; + } + + int depth = 1; + boolean inString = false; + boolean inChar = false; + char stringChar = 0; + + for (int i = openPos + 1; i < text.length(); i++) { + char c = text.charAt(i); + char prev = (i > 0) ? text.charAt(i - 1) : 0; + + // Handle string literals + if (!inChar && (c == '"' || c == '\'') && prev != '\\') { + if (!inString) { + inString = true; + stringChar = c; + } else if (c == stringChar) { + inString = false; + } + continue; + } + + if (inString) continue; + + // Skip excluded regions (comments) + if (isExcluded(i)) continue; + + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + return i; + } + } + } + + return -1; // No matching paren found + } + + /** + * Parse method arguments from the text between opening and closing parentheses. + * Handles nested expressions, strings, and complex argument types. + */ + private List parseMethodArguments(int start, int end) { + List args = new ArrayList<>(); + + if (start >= end) { + return args; // No arguments + } + + int depth = 0; + int argStart = start; + boolean inString = false; + char stringChar = 0; + + for (int i = start; i <= end; i++) { + if (i == end || (depth == 0 && !inString && text.charAt(i) == ',')) { + // End of an argument + String argText = text.substring(argStart, i).trim(); + if (!argText.isEmpty()) { + // Find the actual start/end positions (excluding leading/trailing whitespace) + int actualStart = argStart; + while (actualStart < i && Character.isWhitespace(text.charAt(actualStart))) { + actualStart++; + } + int actualEnd = i; + while (actualEnd > actualStart && Character.isWhitespace(text.charAt(actualEnd - 1))) { + actualEnd--; + } + + // Try to resolve the argument type + TypeInfo argType = resolveExpressionType(argText, actualStart); + + args.add(new MethodCallInfo.Argument( + argText, actualStart, actualEnd, argType, true, null + )); + } + argStart = i + 1; + continue; + } + + char c = text.charAt(i); + char prev = (i > start) ? text.charAt(i - 1) : 0; + + // Handle string literals + if (!inString && (c == '"' || c == '\'') && prev != '\\') { + inString = true; + stringChar = c; + } else if (inString && c == stringChar && prev != '\\') { + inString = false; + } + + if (!inString) { + if (c == '(' || c == '[' || c == '{' || c == '<') { + depth++; + } else if (c == ')' || c == ']' || c == '}' || c == '>') { + depth--; + } + } + } + + return args; + } + + /** + * Try to resolve the type of an expression (for argument type checking). + * This is a simplified version that handles common cases. + */ + private TypeInfo resolveExpressionType(String expr, int position) { + expr = expr.trim(); + + if (expr.isEmpty()) { + return null; + } + + // String literals + if (expr.startsWith("\"") && expr.endsWith("\"")) { + return resolveType("String"); + } + + // Character literals + if (expr.startsWith("'") && expr.endsWith("'")) { + return TypeInfo.forPrimitive("char"); + } + + // Boolean literals + if (expr.equals("true") || expr.equals("false")) { + return TypeInfo.forPrimitive("boolean"); + } + + // Null literal + if (expr.equals("null")) { + return null; // null is compatible with any reference type + } + + // Numeric literals + if (expr.matches("-?\\d+[lL]?")) { + if (expr.toLowerCase().endsWith("l")) { + return TypeInfo.forPrimitive("long"); + } + return TypeInfo.forPrimitive("int"); + } + if (expr.matches("-?\\d+\\.\\d*[fF]?")) { + if (expr.toLowerCase().endsWith("f")) { + return TypeInfo.forPrimitive("float"); + } + return TypeInfo.forPrimitive("double"); + } + if (expr.matches("-?\\d+\\.\\d*[dD]")) { + return TypeInfo.forPrimitive("double"); + } + + // Simple variable reference + if (expr.matches("[a-zA-Z_][a-zA-Z0-9_]*")) { + FieldInfo varInfo = resolveVariable(expr, position); + if (varInfo != null) { + return varInfo.getTypeInfo(); + } + } + + // "this" keyword + if (expr.equals("this")) { + // Could return the enclosing class type if we had that info + return null; + } + + // "new Type()" expressions + if (expr.startsWith("new ")) { + Matcher newMatcher = NEW_TYPE_PATTERN.matcher(expr); + if (newMatcher.find()) { + return resolveType(newMatcher.group(1)); + } + } + + // Chain expressions like "obj.field" or "obj.method()" + // This is complex - for now, return null to indicate unknown + // The type compatibility check will be lenient with null types + + return null; } /** @@ -940,6 +1616,9 @@ public class ScriptDocument { * For example, for "mc.thePlayer.worldObj.weatherEffects.get()", * this would resolve mc -> Minecraft -> thePlayer -> EntityPlayer -> worldObj -> World -> weatherEffects -> List * and return the final TypeInfo (List). + * + * Also handles method calls in chains like "Minecraft.getMinecraft().thePlayer" by resolving + * the return type of getMinecraft() and continuing resolution. * * @param methodNameStart The start position of the method name * @return The TypeInfo of the final receiver, or null if no receiver or couldn't resolve @@ -954,9 +1633,9 @@ public class ScriptDocument { return null; // No receiver } - // Walk backward to collect all identifiers in the chain - List chainSegments = new ArrayList<>(); - List segmentPositions = new ArrayList<>(); + // Walk backward to collect all segments in the chain + // Segments can be identifiers OR method calls (identifier followed by parentheses) + List chainSegments = new ArrayList<>(); int pos = scanPos; // Currently at the dot @@ -971,28 +1650,50 @@ public class ScriptDocument { if (pos < 0) break; - // Check if this is the end of an identifier - if (!Character.isJavaIdentifierPart(text.charAt(pos))) { - // Could be ')' from a method call - we need to skip that - if (text.charAt(pos) == ')') { - // This is a method call result, e.g., getList().get() - // For now, we can't resolve method return types in chains - // So we return null to indicate unresolvable - return null; + char c = text.charAt(pos); + + // Check if this is the end of a method call (closing paren) + if (c == ')') { + // Find the matching opening paren + int closeParen = pos; + int openParen = findMatchingParenBackward(pos); + if (openParen < 0) { + return null; // Malformed } + + // Now find the method name before the open paren + int beforeParen = openParen - 1; + while (beforeParen >= 0 && Character.isWhitespace(text.charAt(beforeParen))) + beforeParen--; + + if (beforeParen < 0 || !Character.isJavaIdentifierPart(text.charAt(beforeParen))) { + return null; // No method name + } + + // Read the method name backward + int methodNameEnd = beforeParen + 1; + while (beforeParen >= 0 && Character.isJavaIdentifierPart(text.charAt(beforeParen))) + beforeParen--; + int methodNameBegin = beforeParen + 1; + + String methodName = text.substring(methodNameBegin, methodNameEnd); + chainSegments.add(0, new ChainSegment(methodName, methodNameBegin, methodNameEnd, true)); + + pos = beforeParen; + } else if (Character.isJavaIdentifierPart(c)) { + // Regular identifier (field or variable) + int identEnd = pos + 1; + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) + pos--; + int identStart = pos + 1; + + String ident = text.substring(identStart, identEnd); + chainSegments.add(0, new ChainSegment(ident, identStart, identEnd, false)); + } else { + // Unexpected character - end of chain break; } - // Read the identifier backward - int identEnd = pos + 1; - while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) - pos--; - int identStart = pos + 1; - - String ident = text.substring(identStart, identEnd); - chainSegments.add(0, ident); // Add to front (we're going backward) - segmentPositions.add(0, new int[]{identStart, identEnd}); - // Skip whitespace while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) pos--; @@ -1012,66 +1713,174 @@ public class ScriptDocument { } // Now resolve the chain from left to right - String firstSegment = chainSegments.get(0); + ChainSegment firstSeg = chainSegments.get(0); + String firstSegment = firstSeg.name; TypeInfo currentType = null; if (firstSegment.equals("this")) { - // For 'this', we use globalFields for resolution - // Can't fully resolve 'this' without class context, but we can try - currentType = null; - - if (chainSegments.size() > 1) { - // this.something - check globalFields - String nextField = chainSegments.get(1); - if (globalFields.containsKey(nextField)) { - currentType = globalFields.get(nextField).getTypeInfo(); + // For 'this', check if we're inside a script-defined class + currentType = findEnclosingScriptType(methodNameStart); + + if (currentType == null && chainSegments.size() > 1) { + // Fallback: this.something - check globalFields + ChainSegment nextSeg = chainSegments.get(1); + if (!nextSeg.isMethodCall && globalFields.containsKey(nextSeg.name)) { + currentType = globalFields.get(nextSeg.name).getTypeInfo(); // Continue resolving from index 2 for (int i = 2; i < chainSegments.size(); i++) { - if (currentType == null || !currentType.isResolved()) - return null; - String field = chainSegments.get(i); - if (currentType.hasField(field)) { - FieldInfo fieldInfo = currentType.getFieldInfo(field); - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; - } else { - return null; - } + currentType = resolveChainSegment(currentType, chainSegments.get(i)); + if (currentType == null) return null; } return currentType; } } + // If we have a script type, continue from index 1 + if (currentType != null) { + for (int i = 1; i < chainSegments.size(); i++) { + currentType = resolveChainSegment(currentType, chainSegments.get(i)); + if (currentType == null) return null; + } + return currentType; + } return null; } else if (Character.isUpperCase(firstSegment.charAt(0))) { // Static access like Minecraft.getMinecraft() - the type is the class itself currentType = resolveType(firstSegment); - } else { + } else if (!firstSeg.isMethodCall) { // Variable like mc.thePlayer.worldObj - int[] firstPos = segmentPositions.get(0); - FieldInfo varInfo = resolveVariable(firstSegment, firstPos[0]); + FieldInfo varInfo = resolveVariable(firstSegment, firstSeg.start); if (varInfo != null) { currentType = varInfo.getTypeInfo(); } + } else { + // First segment is a method call without a receiver - check script methods + if (isScriptMethod(firstSegment)) { + MethodInfo scriptMethod = getScriptMethodInfo(firstSegment); + if (scriptMethod != null) { + currentType = scriptMethod.getReturnType(); + } + } } // Resolve remaining segments for (int i = 1; i < chainSegments.size(); i++) { - if (currentType == null || !currentType.isResolved()) { - return null; - } - - String field = chainSegments.get(i); - if (currentType.hasField(field)) { - FieldInfo fieldInfo = currentType.getFieldInfo(field); - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; - } else { - // Field doesn't exist - return null; - } + currentType = resolveChainSegment(currentType, chainSegments.get(i)); + if (currentType == null) return null; } return currentType; } + /** + * Helper class for chain segments (can be field access or method call). + */ + private static class ChainSegment { + final String name; + final int start; + final int end; + final boolean isMethodCall; + + ChainSegment(String name, int start, int end, boolean isMethodCall) { + this.name = name; + this.start = start; + this.end = end; + this.isMethodCall = isMethodCall; + } + } + + /** + * Resolve a single segment of a chain given the current type context. + */ + private TypeInfo resolveChainSegment(TypeInfo currentType, ChainSegment segment) { + if (currentType == null || !currentType.isResolved()) { + return null; + } + + if (segment.isMethodCall) { + // Method call - get return type + if (currentType.hasMethod(segment.name)) { + MethodInfo methodInfo = currentType.getMethodInfo(segment.name); + return (methodInfo != null) ? methodInfo.getReturnType() : null; + } + return null; + } else { + // Field access + if (currentType.hasField(segment.name)) { + FieldInfo fieldInfo = currentType.getFieldInfo(segment.name); + return (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + } + return null; + } + } + + /** + * Find the matching opening parenthesis when given a closing paren position. + */ + private int findMatchingParenBackward(int closeParenPos) { + if (closeParenPos < 0 || closeParenPos >= text.length() || text.charAt(closeParenPos) != ')') { + return -1; + } + + int depth = 1; + boolean inString = false; + char stringChar = 0; + + for (int i = closeParenPos - 1; i >= 0; i--) { + char c = text.charAt(i); + char next = (i < text.length() - 1) ? text.charAt(i + 1) : 0; + + // Handle string literals (going backward, check for escapes) + if (!inString && (c == '"' || c == '\'')) { + // Check if this quote is escaped (look back for backslash) + int backslashCount = 0; + for (int j = i - 1; j >= 0 && text.charAt(j) == '\\'; j--) { + backslashCount++; + } + if (backslashCount % 2 == 0) { + inString = true; + stringChar = c; + } + } else if (inString && c == stringChar) { + int backslashCount = 0; + for (int j = i - 1; j >= 0 && text.charAt(j) == '\\'; j--) { + backslashCount++; + } + if (backslashCount % 2 == 0) { + inString = false; + } + } + + if (inString) continue; + + // Skip excluded regions (comments) + if (isExcluded(i)) continue; + + if (c == ')') { + depth++; + } else if (c == '(') { + depth--; + if (depth == 0) { + return i; + } + } + } + + return -1; // No matching paren found + } + + /** + * Find the script-defined type that contains the given position. + * Used for resolving 'this' references. + */ + private ScriptTypeInfo findEnclosingScriptType(int position) { + for (ScriptTypeInfo type : scriptTypes.values()) { + if (type.containsPosition(position)) { + return type; + } + } + return null; + } + /** * Check if a method name is defined in this script. */ @@ -1084,6 +1893,18 @@ public class ScriptDocument { return false; } + /** + * Get the MethodInfo for a script-defined method by name. + */ + private MethodInfo getScriptMethodInfo(String methodName) { + for (MethodInfo method : methods) { + if (method.getName().equals(methodName)) { + return method; + } + } + return null; + } + private void markVariables(List marks) { Set knownKeywords = new HashSet<>(Arrays.asList( "boolean", "int", "float", "double", "long", "char", "byte", "short", "void", 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 819c27965..f2268e242 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 @@ -1,6 +1,8 @@ package noppes.npcs.client.gui.util.script.interpreter; +import net.minecraft.client.gui.Gui; import noppes.npcs.client.ClientProxy; +import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.Collections; @@ -146,6 +148,13 @@ public class ScriptLine { token.setFieldInfo((FieldInfo) mark.metadata); } else if (mark.metadata instanceof MethodInfo) { token.setMethodInfo((MethodInfo) mark.metadata); + } else if (mark.metadata instanceof MethodCallInfo) { + MethodCallInfo callInfo = (MethodCallInfo) mark.metadata; + token.setMethodCallInfo(callInfo); + if (callInfo.getErrorType() != MethodCallInfo.ErrorType.NONE) { + // For other errors (arg count, static access), underline the method name + token.setUnderline(true, 0xFF5555); // Red wavy underline + } } else if (mark.metadata instanceof ImportData) { token.setImportData((ImportData) mark.metadata); } @@ -190,19 +199,41 @@ public class ScriptLine { /** * Draw this line with syntax highlighting using Minecraft color codes. * Compatible with the existing rendering system. + * Also draws curly underlines for tokens with errors (method call validation failures). */ public void drawString(int x, int y, int defaultColor) { StringBuilder builder = new StringBuilder(); int lastIndex = 0; + // Track positions for underlines + List underlines = new ArrayList<>(); // [startX, endX, color] + int currentX = x; + for (Token t : tokens) { int tokenStart = t.getGlobalStart() - globalStart; // relative position in line - - // Append any text before this token (spaces, punctuation, etc.) + + // Calculate gap width before this token if (tokenStart > lastIndex && tokenStart <= text.length()) { - builder.append(text, lastIndex, tokenStart); + String gapText = text.substring(lastIndex, tokenStart); + builder.append(gapText); + currentX += ClientProxy.Font.width(gapText); } + // Track underline position if token has one + if (t.hasUnderline()) { + int tokenWidth = ClientProxy.Font.width(t.getText()); + underlines.add(new int[]{currentX, currentX + tokenWidth, t.getUnderlineColor()}); + } + + if (t.getMethodCallInfo() != null) { + MethodCallInfo callInfo = t.getMethodCallInfo(); + if (callInfo.hasArgTypeError(t)) { + underlines.add( + new int[]{currentX, currentX + ClientProxy.Font.width(t.getText()), t.getUnderlineColor()}); + } + } + + // Append the colored token builder.append(COLOR_CHAR) .append(t.getColorCode()) @@ -210,6 +241,7 @@ public class ScriptLine { .append(COLOR_CHAR) .append('f'); // Reset to white + currentX += ClientProxy.Font.width(t.getText()); lastIndex = tokenStart + t.getText().length(); } @@ -218,16 +250,67 @@ public class ScriptLine { builder.append(text.substring(lastIndex)); } + // Draw the text ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor); + + // Draw curly underlines for error tokens + for (int[] ul : underlines) { + drawCurlyUnderline(ul[0], y + ClientProxy.Font.height() - 1, ul[1] - ul[0], ul[2]); + } + } + + /** + * Draw a curly/wavy underline (like IDE error highlighting). + * @param x Start X position + * @param y Y position (bottom of text) + * @param width Width of the underline + * @param color Color in ARGB format (e.g., 0xFFFF5555 for red) + */ + private void drawCurlyUnderline(int x, int y, int width, int color) { + if (width <= 0) + return; + + float a = ((color >> 24) & 0xFF) / 255f; + float r = ((color >> 16) & 0xFF) / 255f; + float g = ((color >> 8) & 0xFF) / 255f; + float b = (color & 0xFF) / 255f; + + // If alpha is 0, assume full opacity + if (a == 0) + a = 1.0f; + + GL11.glPushMatrix(); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r, g, b, a); + GL11.glLineWidth(1.0f); + + GL11.glBegin(GL11.GL_LINE_STRIP); + // Wave parameters: 2 pixels amplitude, 4 pixels wavelength + int waveHeight = 2; + int waveLength = 4; + for (int i = 0; i <= width; i++) { + // Create a sine-like wave pattern + double phase = (double) i / waveLength * Math.PI * 2; + int yOffset = (int) (Math.sin(phase) * waveHeight); + GL11.glVertex2f(x + i, y + yOffset); + } + GL11.glEnd(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); } /** * Draw this line with hex colors instead of Minecraft color codes. * More flexible but requires custom font rendering. + * Also draws wavy underlines for tokens with errors. * * @param x X position * @param y Y position - * @param renderer A functional interface for drawing colored text segments + * @param renderer A renderer for drawing colored text and error underlines */ public void drawStringHex(int x, int y, HexColorRenderer renderer) { int currentX = x; @@ -239,31 +322,85 @@ public class ScriptLine { // Draw any text before this token in default color if (tokenStart > lastIndex && tokenStart <= text.length()) { String gap = text.substring(lastIndex, tokenStart); - currentX = renderer.draw(gap, currentX, y, 0xFFFFFF); + currentX = renderer.draw(gap, currentX, y, 0xFFFFFF, false, 0); } - // Draw the colored token - currentX = renderer.draw(t.getText(), currentX, y, t.getHexColor()); + // Draw the colored token (with underline if flagged) + currentX = renderer.draw(t.getText(), currentX, y, t.getHexColor(), t.hasUnderline(), + t.getUnderlineColor()); lastIndex = tokenStart + t.getText().length(); } // Draw any remaining text in default color if (lastIndex < text.length()) { - renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF); + renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF, false, 0); } } /** - * Functional interface for rendering text with hex colors. + * Functional interface for rendering text with hex colors and optional underlines. */ @FunctionalInterface public interface HexColorRenderer { /** * Draw text at the specified position with the given color. + * @param text The text to draw + * @param x X position + * @param y Y position + * @param hexColor The text color in hex format + * @param underline Whether to draw an underline (wavy for errors) + * @param underlineColor The underline color (if underline is true) * @return The X position after drawing (for continuation) */ - int draw(String text, int x, int y, int hexColor); + int draw(String text, int x, int y, int hexColor, boolean underline, int underlineColor); + } + + /** + * Creates a default HexColorRenderer implementation using ClientProxy.Font. + * This renderer draws text with hex colors and curly underlines for errors. + * @return A HexColorRenderer that can be passed to drawStringHex + */ + public static HexColorRenderer createDefaultHexRenderer() { + return (text, x, y, hexColor, underline, underlineColor) -> { + // Draw the text with hex color + // Minecraft's font renderer uses ARGB, so add full alpha if not present + int color = (hexColor & 0xFF000000) == 0 ? (0xFF000000 | hexColor) : hexColor; + ClientProxy.Font.drawString(text, x, y, color); + int textWidth = ClientProxy.Font.width(text); + + // Draw curly underline if needed + if (underline && textWidth > 0) { + int ulColor = (underlineColor & 0xFF000000) == 0 ? (0xFF000000 | underlineColor) : underlineColor; + + float a = ((ulColor >> 24) & 0xFF) / 255f; + float r = ((ulColor >> 16) & 0xFF) / 255f; + float g = ((ulColor >> 8) & 0xFF) / 255f; + float b = (ulColor & 0xFF) / 255f; + + GL11.glPushMatrix(); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r, g, b, a); + GL11.glLineWidth(1.0f); + + int underlineY = y + ClientProxy.Font.height() - 1; + GL11.glBegin(GL11.GL_LINE_STRIP); + for (int i = 0; i <= textWidth; i++) { + double phase = (double) i / 4 * Math.PI * 2; + int yOffset = (int) (Math.sin(phase) * 2); + GL11.glVertex2f(x + i, underlineY + yOffset); + } + GL11.glEnd(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); + } + + return x + textWidth; + }; } // ==================== UTILITIES ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java new file mode 100644 index 000000000..b0ab251a8 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java @@ -0,0 +1,187 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a type defined within the script itself (not from Java classpath). + * This handles classes, interfaces, and enums declared in the user's script. + * + * Unlike TypeInfo which wraps a Java Class, ScriptTypeInfo stores all metadata + * about fields, methods, and other type information parsed from the script. + */ +public class ScriptTypeInfo extends TypeInfo { + + private final String scriptClassName; + private final int declarationOffset; + private final int bodyStart; + private final int bodyEnd; + + // Script-defined members + private final Map fields = new HashMap<>(); + private final Map> methods = new HashMap<>(); // name -> list of overloads + private final List innerClasses = new ArrayList<>(); + + // Parent class reference (for inner class resolution) + private ScriptTypeInfo outerClass; + + private ScriptTypeInfo(String simpleName, String fullName, Kind kind, + int declarationOffset, int bodyStart, int bodyEnd) { + super(simpleName, fullName, "", kind, null, true, null, true); + this.scriptClassName = simpleName; + this.declarationOffset = declarationOffset; + this.bodyStart = bodyStart; + this.bodyEnd = bodyEnd; + } + + // Factory method + public static ScriptTypeInfo create(String simpleName, Kind kind, + int declarationOffset, int bodyStart, int bodyEnd) { + return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd); + } + + public static ScriptTypeInfo createInner(String simpleName, Kind kind, ScriptTypeInfo outer, + int declarationOffset, int bodyStart, int bodyEnd) { + String fullName = outer.getFullName() + "$" + simpleName; + ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd); + inner.outerClass = outer; + outer.innerClasses.add(inner); + return inner; + } + + // Getters + public String getScriptClassName() { return scriptClassName; } + public int getDeclarationOffset() { return declarationOffset; } + public int getBodyStart() { return bodyStart; } + public int getBodyEnd() { return bodyEnd; } + public ScriptTypeInfo getOuterClass() { return outerClass; } + public List getInnerClasses() { return innerClasses; } + + /** + * Check if a position is inside this type's body. + */ + public boolean containsPosition(int position) { + return position >= bodyStart && position < bodyEnd; + } + + // ==================== FIELD MANAGEMENT ==================== + + public void addField(FieldInfo field) { + fields.put(field.getName(), field); + } + + @Override + public boolean hasField(String fieldName) { + return fields.containsKey(fieldName); + } + + @Override + public FieldInfo getFieldInfo(String fieldName) { + return fields.get(fieldName); + } + + public Map getFields() { + return new HashMap<>(fields); + } + + // ==================== METHOD MANAGEMENT ==================== + + public void addMethod(MethodInfo method) { + methods.computeIfAbsent(method.getName(), k -> new ArrayList<>()).add(method); + } + + @Override + public boolean hasMethod(String methodName) { + return methods.containsKey(methodName); + } + + @Override + public boolean hasMethod(String methodName, int paramCount) { + List overloads = methods.get(methodName); + if (overloads == null) return false; + for (MethodInfo m : overloads) { + if (m.getParameterCount() == paramCount) { + return true; + } + } + return false; + } + + @Override + public MethodInfo getMethodInfo(String methodName) { + List overloads = methods.get(methodName); + return (overloads != null && !overloads.isEmpty()) ? overloads.get(0) : null; + } + + /** + * Get all method overloads with the given name. + */ + public List getMethodOverloads(String methodName) { + return methods.getOrDefault(methodName, new ArrayList<>()); + } + + /** + * Get a method with specific parameter count. + */ + public MethodInfo getMethodWithParamCount(String methodName, int paramCount) { + List overloads = methods.get(methodName); + if (overloads == null) return null; + for (MethodInfo m : overloads) { + if (m.getParameterCount() == paramCount) { + return m; + } + } + return null; + } + + public Map> getMethods() { + return new HashMap<>(methods); + } + + // ==================== INNER CLASS LOOKUP ==================== + + /** + * Find an inner class by name. + */ + public ScriptTypeInfo getInnerClass(String name) { + for (ScriptTypeInfo inner : innerClasses) { + if (inner.getSimpleName().equals(name)) { + return inner; + } + } + return null; + } + + @Override + public TokenType getTokenType() { + switch (getKind()) { + case INTERFACE: + return TokenType.INTERFACE_DECL; + case ENUM: + return TokenType.ENUM_DECL; + case CLASS: + default: + return TokenType.CLASS_DECL; + } + } + + // Script types are always considered resolved since they're defined in the script + @Override + public boolean isResolved() { + return true; + } + + // Script types don't have a Java class + @Override + public Class getJavaClass() { + return null; + } + + @Override + public String toString() { + return "ScriptTypeInfo{" + scriptClassName + ", " + getKind() + + ", fields=" + fields.size() + ", methods=" + methods.size() + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index cbd8cfc4f..de8ed2a66 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -21,7 +21,12 @@ public class Token { private TypeInfo typeInfo; // For class references, type declarations private FieldInfo fieldInfo; // For field references private MethodInfo methodInfo; // For method calls/declarations + private MethodCallInfo methodCallInfo; // For method calls with argument info private ImportData importData; // For import statements + + // Rendering flags + private boolean hasUnderline; // True if this token should be underlined (for errors) + private int underlineColor; // Color of the underline (if any) // Navigation - set by ScriptLine private Token prev; @@ -111,8 +116,11 @@ public class Token { public TypeInfo getTypeInfo() { return typeInfo; } public FieldInfo getFieldInfo() { return fieldInfo; } public MethodInfo getMethodInfo() { return methodInfo; } + public MethodCallInfo getMethodCallInfo() { return methodCallInfo; } public ImportData getImportData() { return importData; } public ScriptLine getParentLine() { return parentLine; } + public boolean hasUnderline() { return hasUnderline; } + public int getUnderlineColor() { return underlineColor; } // ==================== SETTERS ==================== @@ -120,7 +128,13 @@ public class Token { public void setTypeInfo(TypeInfo info) { this.typeInfo = info; } public void setFieldInfo(FieldInfo info) { this.fieldInfo = info; } public void setMethodInfo(MethodInfo info) { this.methodInfo = info; } + public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; } public void setImportData(ImportData data) { this.importData = data; } + + public void setUnderline(boolean hasUnderline, int color) { + this.hasUnderline = hasUnderline; + this.underlineColor = color; + } void setParentLine(ScriptLine line) { this.parentLine = line; } void setPrev(Token prev) { this.prev = prev; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java index 50a74679b..e5a0b0bdc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java @@ -23,10 +23,12 @@ public enum TokenType { CLASS_DECL(0x00AAAA, 85), // class names in declarations IMPORTED_CLASS(0x00AAAA, 75), // imported class usages TYPE_DECL(0x00AAAA, 70), // package paths, type references + UNUSED_IMPORT(0x666666, 65), // unused import statements (gray) // Methods METHOD_DECL(0x00AA00, 60), // method declarations (green) METHOD_CALL(0x55FF55, 50), // method calls (bright green) + METHOD_CALL_ERROR(0xFF5555, 51), // method call with validation error (red underline) // Variables and fields UNDEFINED_VAR(0xAA0000, 20), // unresolved variables (dark red) - high priority @@ -65,11 +67,13 @@ public enum TokenType { switch (this) { case COMMENT: case NUMBER: + case UNUSED_IMPORT: return '7'; // gray case STRING: return '5'; // purple case CLASS_KEYWORD: case KEYWORD: + case METHOD_CALL_ERROR: return 'c'; // red case IMPORT_KEYWORD: case MODIFIER: diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java index e2b0099af..3967ed535 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java @@ -2,9 +2,9 @@ package noppes.npcs.client.gui.util.script.interpreter; /** * Represents resolved type information for a class/interface/enum. - * Immutable data class holding all type metadata. + * Base class for type metadata. Extended by ScriptTypeInfo for script-defined types. */ -public final class TypeInfo { +public class TypeInfo { public enum Kind { CLASS, @@ -31,6 +31,19 @@ public final class TypeInfo { this.resolved = resolved; this.enclosingType = enclosingType; } + + // Protected constructor for subclasses (like ScriptTypeInfo) + protected TypeInfo(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType, + @SuppressWarnings("unused") boolean subclass) { + this.simpleName = simpleName; + this.fullName = fullName; + this.packageName = packageName; + this.kind = kind; + this.javaClass = javaClass; + this.resolved = resolved; + this.enclosingType = enclosingType; + } // Factory methods public static TypeInfo resolved(String simpleName, String fullName, String packageName, @@ -74,6 +87,25 @@ public final class TypeInfo { return new TypeInfo(simpleName, fullName, packageName, kind, clazz, true, enclosing); } + /** + * Create a TypeInfo for a primitive type. + */ + public static TypeInfo forPrimitive(String typeName) { + Class primitiveClass = null; + switch (typeName) { + case "boolean": primitiveClass = boolean.class; break; + case "byte": primitiveClass = byte.class; break; + case "char": primitiveClass = char.class; break; + case "short": primitiveClass = short.class; break; + case "int": primitiveClass = int.class; break; + case "long": primitiveClass = long.class; break; + case "float": primitiveClass = float.class; break; + case "double": primitiveClass = double.class; break; + case "void": primitiveClass = void.class; break; + } + return new TypeInfo(typeName, typeName, "", Kind.CLASS, primitiveClass, true, null); + } + // Getters public String getSimpleName() { return simpleName; } public String getFullName() { return fullName; }