Method declaration error validation + helper classes needed for that

This commit is contained in:
bigguy345 committed 2025-12-28 18:54:10 +02:00
1 parent b7333bcf6a
commit 3e21757d12
6 files changed
+1154

No files matched your search

@@ -0,0 +1,323 @@
package noppes.npcs.client.gui.util.script.interpreter;
/**
* Utility class for parsing and analyzing code text.
* Provides methods for finding matching braces/parentheses, removing comments/strings,
* and locating keywords in code.
*/
public final class CodeParser {
private CodeParser() {} // Utility class
// ==================== Matching Delimiters ====================
/**
* Find the closing brace that matches the opening brace at the given position.
* @param text The text to search
* @param openBrace Position of the opening '{'
* @return Position of matching '}' or -1 if not found
*/
public static int findMatchingBrace(String text, int openBrace) {
if (openBrace < 0 || openBrace >= text.length() || text.charAt(openBrace) != '{') {
return -1;
}
int depth = 1;
for (int i = openBrace + 1; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '{') depth++;
else if (c == '}') {
depth--;
if (depth == 0) return i;
}
}
return -1;
}
/**
* Find the closing parenthesis that matches the opening parenthesis at the given position.
* @param text The text to search
* @param openParen Position of the opening '('
* @return Position of matching ')' or -1 if not found
*/
public static int findMatchingParen(String text, int openParen) {
if (openParen < 0 || openParen >= text.length() || text.charAt(openParen) != '(') {
return -1;
}
int depth = 1;
for (int i = openParen + 1; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '(') depth++;
else if (c == ')') {
depth--;
if (depth == 0) return i;
}
}
return -1;
}
// ==================== Comment/String Removal ====================
/**
* Remove string literals and comments from code for structural analysis.
* This is used when analyzing control flow to avoid false positives from
* keywords inside strings or comments.
* @param code The source code
* @return Code with strings and comments removed
*/
public static String removeStringsAndComments(String code) {
StringBuilder result = new StringBuilder();
boolean inString = false;
boolean inChar = false;
boolean inLineComment = false;
boolean inBlockComment = false;
for (int i = 0; i < code.length(); i++) {
char c = code.charAt(i);
char next = (i + 1 < code.length()) ? code.charAt(i + 1) : 0;
if (inLineComment) {
if (c == '\n') {
inLineComment = false;
result.append(c);
}
continue;
}
if (inBlockComment) {
if (c == '*' && next == '/') {
inBlockComment = false;
i++; // skip '/'
}
continue;
}
if (inString || inChar) {
if (c == '\\' && i + 1 < code.length()) {
i++; // skip escaped character
continue;
}
if ((inString && c == '"') || (inChar && c == '\'')) {
inString = false;
inChar = false;
}
continue;
}
// Check for start of comments or strings
if (c == '/' && next == '/') {
inLineComment = true;
i++;
continue;
}
if (c == '/' && next == '*') {
inBlockComment = true;
i++;
continue;
}
if (c == '"') {
inString = true;
continue;
}
if (c == '\'') {
inChar = true;
continue;
}
result.append(c);
}
return result.toString();
}
/**
* Remove only comments from code, keeping strings intact and preserving positions.
* Comments are replaced with spaces to maintain character positions.
* @param code The source code
* @return Code with comments replaced by spaces
*/
public static String removeComments(String code) {
StringBuilder result = new StringBuilder();
boolean inString = false;
boolean inChar = false;
boolean inLineComment = false;
boolean inBlockComment = false;
for (int i = 0; i < code.length(); i++) {
char c = code.charAt(i);
char next = (i + 1 < code.length()) ? code.charAt(i + 1) : 0;
if (inLineComment) {
if (c == '\n') {
inLineComment = false;
result.append(c);
} else {
result.append(' '); // Preserve position with spaces
}
continue;
}
if (inBlockComment) {
if (c == '*' && next == '/') {
inBlockComment = false;
result.append(" "); // Preserve position
i++;
} else {
result.append(c == '\n' ? '\n' : ' '); // Preserve newlines
}
continue;
}
if (inString) {
result.append(c);
if (c == '\\' && i + 1 < code.length()) {
result.append(code.charAt(++i));
continue;
}
if (c == '"') inString = false;
continue;
}
if (inChar) {
result.append(c);
if (c == '\\' && i + 1 < code.length()) {
result.append(code.charAt(++i));
continue;
}
if (c == '\'') inChar = false;
continue;
}
// Check for start of comments
if (c == '/' && next == '/') {
inLineComment = true;
result.append(" "); // Preserve position
i++;
continue;
}
if (c == '/' && next == '*') {
inBlockComment = true;
result.append(" "); // Preserve position
i++;
continue;
}
if (c == '"') { inString = true; }
if (c == '\'') { inChar = true; }
result.append(c);
}
return result.toString();
}
// ==================== Keyword Detection ====================
/**
* Check if a keyword exists at the given position in text.
* Verifies the keyword is not part of a larger identifier.
* @param text The text to check
* @param pos Position to check
* @param keyword The keyword to look for
* @return true if the keyword is at this position as a standalone word
*/
public static boolean isKeywordAt(String text, int pos, String keyword) {
if (pos + keyword.length() > text.length()) return false;
if (!text.substring(pos).startsWith(keyword)) return false;
boolean validBefore = pos == 0 || !Character.isLetterOrDigit(text.charAt(pos - 1));
boolean validAfter = pos + keyword.length() >= text.length()
|| !Character.isLetterOrDigit(text.charAt(pos + keyword.length()));
return validBefore && validAfter;
}
/**
* Find the next "return" keyword that is not inside a string literal.
* @param text The text to search
* @param start Position to start searching from
* @return Position of "return" or -1 if not found
*/
public static int findReturnKeyword(String text, int start) {
boolean inString = false;
boolean inChar = false;
for (int i = start; i < text.length() - 5; i++) {
char c = text.charAt(i);
if (inString) {
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
if (c == '"') inString = false;
continue;
}
if (inChar) {
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
if (c == '\'') inChar = false;
continue;
}
if (c == '"') { inString = true; continue; }
if (c == '\'') { inChar = true; continue; }
// Check for "return" keyword
if (isKeywordAt(text, i, "return")) {
return i;
}
}
return -1;
}
/**
* Find the semicolon that ends a return statement, handling nested structures.
* @param text The text to search
* @param start Position to start searching (after "return" keyword)
* @return Position of ';' or -1 if not found
*/
public static int findReturnSemicolon(String text, int start) {
int parenDepth = 0;
int braceDepth = 0;
boolean inString = false;
boolean inChar = false;
for (int i = start; i < text.length(); i++) {
char c = text.charAt(i);
if (inString) {
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
if (c == '"') inString = false;
continue;
}
if (inChar) {
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
if (c == '\'') inChar = false;
continue;
}
if (c == '"') { inString = true; continue; }
if (c == '\'') { inChar = true; continue; }
if (c == '(') { parenDepth++; continue; }
if (c == ')') { parenDepth--; continue; }
if (c == '{') { braceDepth++; continue; }
if (c == '}') { braceDepth--; continue; }
if (c == ';' && parenDepth == 0 && braceDepth == 0) {
return i;
}
}
return -1;
}
// ==================== Whitespace Utilities ====================
/**
* Skip whitespace characters starting from the given position.
* @param text The text
* @param pos Starting position
* @return Position of first non-whitespace character
*/
public static int skipWhitespace(String text, int pos) {
while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) {
pos++;
}
return pos;
}
}
@@ -0,0 +1,215 @@
package noppes.npcs.client.gui.util.script.interpreter;
/**
* Analyzes control flow in method bodies to determine if all paths return a value.
* Used for detecting "missing return statement" errors.
*/
public final class ControlFlowAnalyzer {
private ControlFlowAnalyzer() {} // Utility class
/**
* Check if the method body has a guaranteed return on all code paths.
*/
public static boolean hasGuaranteedReturn(String body) {
String cleanBody = CodeParser.removeStringsAndComments(body);
return hasTopLevelReturn(cleanBody)
|| hasCompleteIfElseReturn(cleanBody)
|| hasTryFinallyReturn(cleanBody)
|| hasCompleteSwitchReturn(cleanBody);
}
/**
* Check for a bare return statement at the method's top level (brace depth 0).
*/
private static boolean hasTopLevelReturn(String body) {
int braceDepth = 0;
int parenDepth = 0;
for (int i = 0; i < body.length(); i++) {
char c = body.charAt(i);
if (c == '{') braceDepth++;
else if (c == '}') braceDepth--;
else if (c == '(') parenDepth++;
else if (c == ')') parenDepth--;
if (braceDepth == 0 && parenDepth == 0 && isKeywordAt(body, i, "return")) {
return true;
}
}
return false;
}
/**
* Check for complete if-else chains where all branches return.
*/
private static boolean hasCompleteIfElseReturn(String body) {
int braceDepth = 0;
for (int i = 0; i < body.length(); i++) {
char c = body.charAt(i);
if (c == '{') { braceDepth++; continue; }
if (c == '}') { braceDepth--; continue; }
if (braceDepth == 0 && isKeywordAt(body, i, "if")) {
if (checkIfElseChainReturns(body, i)) {
return true;
}
}
}
return false;
}
/**
* Check if an if-else chain starting at the given position guarantees a return.
* Handles: if(true) { return; } - always returns (else unreachable)
* if(false) { } else { return; } - else always executes
*/
private static boolean checkIfElseChainReturns(String body, int ifStart) {
int pos = ifStart;
while (pos < body.length()) {
if (!body.substring(pos).startsWith("if")) return false;
pos += 2;
pos = skipWhitespace(body, pos);
if (pos >= body.length() || body.charAt(pos) != '(') return false;
int condStart = pos;
int condEnd = CodeParser.findMatchingParen(body, pos);
if (condEnd < 0) return false;
// Check for literal true/false conditions
String condition = body.substring(condStart + 1, condEnd).trim();
boolean isAlwaysTrue = condition.equals("true");
boolean isAlwaysFalse = condition.equals("false");
pos = skipWhitespace(body, condEnd + 1);
if (pos >= body.length() || body.charAt(pos) != '{') return false;
int blockEnd = CodeParser.findMatchingBrace(body, pos);
if (blockEnd < 0) return false;
String ifBlock = body.substring(pos + 1, blockEnd);
boolean ifBlockReturns = hasGuaranteedReturn(ifBlock);
// if(true) with return means we always return (else is unreachable)
if (isAlwaysTrue && ifBlockReturns) {
return true;
}
pos = skipWhitespace(body, blockEnd + 1);
// Must have else clause for guaranteed return
if (!body.substring(pos).startsWith("else")) {
return false;
}
pos += 4;
pos = skipWhitespace(body, pos);
// else if - continue the chain
if (isKeywordAt(body, pos, "if")) {
continue;
}
// Plain else block - final branch
if (pos >= body.length() || body.charAt(pos) != '{') return false;
int elseBlockEnd = CodeParser.findMatchingBrace(body, pos);
if (elseBlockEnd < 0) return false;
String elseBlock = body.substring(pos + 1, elseBlockEnd);
boolean elseBlockReturns = hasGuaranteedReturn(elseBlock);
// For if-else to guarantee return: both branches must return
// Special case: if(false) means if-block never executes, so only else matters
if (isAlwaysFalse) {
return elseBlockReturns;
}
// Normal case: both if and else must return
return ifBlockReturns && elseBlockReturns;
}
return false;
}
/**
* Check for try-finally where finally block returns.
*/
private static boolean hasTryFinallyReturn(String body) {
for (int i = 0; i < body.length(); i++) {
if (!isKeywordAt(body, i, "try")) continue;
int tryBlockStart = body.indexOf('{', i);
if (tryBlockStart < 0) continue;
int tryBlockEnd = CodeParser.findMatchingBrace(body, tryBlockStart);
if (tryBlockEnd < 0) continue;
// Skip catch blocks
int searchPos = skipCatchBlocks(body, tryBlockEnd + 1);
searchPos = skipWhitespace(body, searchPos);
// Check for finally
if (body.substring(searchPos).startsWith("finally")) {
int finallyBlockStart = body.indexOf('{', searchPos);
if (finallyBlockStart >= 0) {
int finallyBlockEnd = CodeParser.findMatchingBrace(body, finallyBlockStart);
if (finallyBlockEnd >= 0) {
String finallyBlock = body.substring(finallyBlockStart + 1, finallyBlockEnd);
if (hasGuaranteedReturn(finallyBlock)) {
return true;
}
}
}
}
}
return false;
}
private static int skipCatchBlocks(String body, int start) {
int pos = start;
while (true) {
pos = skipWhitespace(body, pos);
if (pos >= body.length() || !body.substring(pos).startsWith("catch")) break;
int catchBlockStart = body.indexOf('{', pos);
if (catchBlockStart < 0) break;
int catchBlockEnd = CodeParser.findMatchingBrace(body, catchBlockStart);
if (catchBlockEnd < 0) break;
pos = catchBlockEnd + 1;
}
return pos;
}
/**
* Check for switch with default where all cases return.
* Returns false for now - full implementation is complex.
*/
private static boolean hasCompleteSwitchReturn(String body) {
return false;
}
// ==================== Utility Methods ====================
private static boolean isKeywordAt(String text, int pos, String keyword) {
if (pos + keyword.length() > text.length()) return false;
if (!text.substring(pos).startsWith(keyword)) return false;
boolean validBefore = pos == 0 || !Character.isLetterOrDigit(text.charAt(pos - 1));
boolean validAfter = pos + keyword.length() >= text.length()
|| !Character.isLetterOrDigit(text.charAt(pos + keyword.length()));
return validBefore && validAfter;
}
private static int skipWhitespace(String text, int pos) {
while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) {
pos++;
}
return pos;
}
}
@@ -22,6 +22,17 @@ import java.util.Set;
*/
public final class MethodInfo {
/**
* Validation error types for method declarations.
*/
public enum ErrorType {
NONE,
MISSING_RETURN, // Non-void method missing return statement
RETURN_TYPE_MISMATCH, // Return statement type doesn't match method return type
DUPLICATE_PARAMETER, // Two parameters have the same name
PARAMETER_UNDEFINED // Parameter type cannot be resolved
}
private final String name;
private final TypeInfo returnType;
private final TypeInfo containingType; // The class/interface that owns this method
@@ -36,6 +47,12 @@ public final class MethodInfo {
private final int modifiers; // Java Modifier flags (e.g., Modifier.PUBLIC | Modifier.STATIC)
private final String documentation; // Javadoc/comment documentation for this method
// Error tracking for method declarations
private ErrorType errorType = ErrorType.NONE;
private String errorMessage;
private List<ParameterError> parameterErrors = new ArrayList<>();
private List<ReturnStatementError> returnStatementErrors = new ArrayList<>();
private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType,
List<FieldInfo> parameters, int fullDeclarationOffset, int typeOffset, int nameOffset,
int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration,
@@ -237,4 +254,248 @@ public final class MethodInfo {
public int hashCode() {
return name.hashCode() * 31 + parameters.size();
}
// ==================== ERROR HANDLING ====================
/**
* Represents an error with a specific parameter.
*/
public static class ParameterError {
private final FieldInfo parameter;
private final int paramIndex;
private final ErrorType errorType;
private final String message;
public ParameterError(FieldInfo parameter, int paramIndex, ErrorType errorType, String message) {
this.parameter = parameter;
this.paramIndex = paramIndex;
this.errorType = errorType;
this.message = message;
}
public FieldInfo getParameter() { return parameter; }
public int getParamIndex() { return paramIndex; }
public ErrorType getErrorType() { return errorType; }
public String getMessage() { return message; }
}
/**
* Represents an error with a return statement (type mismatch).
*/
public static class ReturnStatementError {
private final int startOffset; // Start of "return" keyword (absolute position)
private final int endOffset; // End of return statement (after semicolon)
private final String message;
private final TypeInfo expectedType;
private final TypeInfo actualType;
public ReturnStatementError(int startOffset, int endOffset, String message, TypeInfo expectedType, TypeInfo actualType) {
this.startOffset = startOffset;
this.endOffset = endOffset;
this.message = message;
this.expectedType = expectedType;
this.actualType = actualType;
}
public int getStartOffset() { return startOffset; }
public int getEndOffset() { return endOffset; }
public String getMessage() { return message; }
public TypeInfo getExpectedType() { return expectedType; }
public TypeInfo getActualType() { return actualType; }
}
/**
* Check if this method declaration has any errors.
*/
public boolean hasError() {
return errorType != ErrorType.NONE || !parameterErrors.isEmpty() || !returnStatementErrors.isEmpty();
}
/**
* Check if this has a missing return error.
*/
public boolean hasMissingReturnError() {
return errorType == ErrorType.MISSING_RETURN;
}
/**
* Check if this has parameter errors.
*/
public boolean hasParameterErrors() {
return !parameterErrors.isEmpty();
}
/**
* Check if this has return statement type errors.
*/
public boolean hasReturnStatementErrors() {
return !returnStatementErrors.isEmpty();
}
public ErrorType getErrorType() { return errorType; }
public String getErrorMessage() { return errorMessage; }
public List<ParameterError> getParameterErrors() { return Collections.unmodifiableList(parameterErrors); }
public List<ReturnStatementError> getReturnStatementErrors() { return Collections.unmodifiableList(returnStatementErrors); }
/**
* Set the main error for this method.
*/
public void setError(ErrorType type, String message) {
this.errorType = type;
this.errorMessage = message;
}
/**
* Add a parameter-specific error.
*/
public void addParameterError(FieldInfo param, int index, ErrorType type, String message) {
parameterErrors.add(new ParameterError(param, index, type, message));
}
/**
* Add a return statement error.
*/
public void addReturnStatementError(int startOffset, int endOffset, String message, TypeInfo expectedType, TypeInfo actualType) {
returnStatementErrors.add(new ReturnStatementError(startOffset, endOffset, message, expectedType, actualType));
}
/**
* Functional interface for type resolution callback.
*/
@FunctionalInterface
public interface TypeResolver {
TypeInfo resolveExpression(String expression, int position);
}
/**
* Validate this method declaration.
* Checks for parameter errors and missing return statements.
*
* @param methodBodyText The text content of the method body (between { and })
*/
public void validate(String methodBodyText) {
validate(methodBodyText, null);
}
/**
* Validate this method declaration with type resolution for return statements.
* Checks for parameter errors, missing return statements, and return type mismatches.
*
* @param methodBodyText The text content of the method body (between { and })
* @param typeResolver Optional callback to resolve expression types (for return type checking)
*/
public void validate(String methodBodyText, TypeResolver typeResolver) {
if (!isDeclaration) return;
// Validate parameters
validateParameters();
// Validate return statement types FIRST if we have a type resolver
// This ensures type errors are shown even if there's also a missing return
if (typeResolver != null) {
validateReturnTypes(methodBodyText, typeResolver);
}
// Validate return statement for non-void methods
// This may set the main error, but return type errors are already recorded
validateReturnStatement(methodBodyText);
}
/**
* Check for duplicate and unresolved parameters.
*/
private void validateParameters() {
Set<String> seenNames = new HashSet<>();
for (int i = 0; i < parameters.size(); i++) {
FieldInfo param = parameters.get(i);
String paramName = param.getName();
// Check for duplicate parameter names
if (seenNames.contains(paramName)) {
addParameterError(param, i, ErrorType.DUPLICATE_PARAMETER,
"Duplicate parameter name '" + paramName + "'");
} else {
seenNames.add(paramName);
}
// Check for unresolved parameter types
TypeInfo paramType = param.getDeclaredType();
if (paramType == null || !paramType.isResolved()) {
String typeName = paramType != null ? paramType.getSimpleName() : "unknown";
addParameterError(param, i, ErrorType.PARAMETER_UNDEFINED,
"Cannot resolve parameter type '" + typeName + "'");
}
}
}
/**
* Validate that a non-void method has a guaranteed return statement.
* Delegates to {@link ControlFlowAnalyzer} for control flow analysis.
*/
private void validateReturnStatement(String bodyText) {
if (bodyText == null || bodyText.isEmpty()) return;
// void methods don't need return statements
if (TypeChecker.isVoidType(returnType)) return;
// Abstract/native methods don't have bodies
if (isAbstract() || isNative()) return;
// Check if the method has a guaranteed return using the control flow analyzer
if (!ControlFlowAnalyzer.hasGuaranteedReturn(bodyText)) {
setError(ErrorType.MISSING_RETURN, "Missing return statement");
}
}
/**
* Validate that all return statements have compatible types.
* Delegates to {@link CodeParser} for parsing and {@link TypeChecker} for type checks.
*/
private void validateReturnTypes(String bodyText, TypeResolver typeResolver) {
if (bodyText == null || bodyText.isEmpty()) return;
if (TypeChecker.isVoidType(returnType)) return;
String expectedTypeName = returnType.getSimpleName();
// Remove comments but keep strings (we need accurate positions)
String cleanBody = CodeParser.removeComments(bodyText);
// Find all return statements
int pos = 0;
while (pos < cleanBody.length()) {
// Look for "return" keyword
int returnPos = CodeParser.findReturnKeyword(cleanBody, pos);
if (returnPos < 0) break;
// Find the semicolon ending this return statement
int semiPos = CodeParser.findReturnSemicolon(cleanBody, returnPos + 6);
if (semiPos < 0) {
pos = returnPos + 6;
continue;
}
// Extract the return expression
String returnExpr = cleanBody.substring(returnPos + 6, semiPos).trim();
// Skip empty returns (handled by missing return check)
if (!returnExpr.isEmpty()) {
// Calculate absolute position (bodyStart + 1 is after the opening brace)
int absoluteReturnStart = bodyStart + 1 + returnPos;
int absoluteSemiEnd = bodyStart + 1 + semiPos + 1; // +1 to include semicolon
// Resolve the expression type
TypeInfo actualType = typeResolver.resolveExpression(returnExpr, absoluteReturnStart);
// Check type compatibility
if (!TypeChecker.isTypeCompatible(returnType, actualType)) {
String actualTypeName = actualType != null ? actualType.getSimpleName() : "null";
String message = "Incompatible types.\nRequired: " + expectedTypeName + "\nFound: " + actualTypeName;
addReturnStatementError(absoluteReturnStart, absoluteSemiEnd, message, returnType, actualType);
}
}
pos = semiPos + 1;
}
}
}
@@ -450,6 +450,126 @@ public class ScriptLine {
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, underlineWidth, 0xFF5555);
}
}
// Check all method declarations for errors (missing return, parameter errors, return type errors)
for (MethodInfo method : doc.getMethods()) {
if (!method.isDeclaration() || !method.hasError())
continue;
// Handle return statement type errors (underline the entire return statement)
if (method.hasReturnStatementErrors()) {
for (MethodInfo.ReturnStatementError returnError : method.getReturnStatementErrors()) {
int returnStart = returnError.getStartOffset();
int returnEnd = returnError.getEndOffset();
//error 247, 258
//line 235, 259
// Skip if return statement doesn't intersect this line
if (returnEnd < lineStart || returnStart > lineEnd)
continue;
// Clip to line boundaries
int clipStart = Math.max(returnStart, lineStart);
int clipEnd = Math.min(returnEnd, lineEnd);
if (clipStart >= clipEnd)
continue;
// Convert to line-local coordinates
int lineLocalStart = clipStart - lineStart;
int lineLocalEnd = clipEnd - lineStart;
// Bounds check
if (lineLocalStart < 0 || lineLocalStart >= lineText.length())
continue;
// Compute pixel position
String beforeReturn = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeReturn);
int returnWidth;
if (lineLocalEnd > lineText.length()) {
returnWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
} else {
String returnTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
returnWidth = ClientProxy.Font.width(returnTextOnLine);
}
if (returnWidth > 0) {
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, returnWidth, 0xFF5555);
}
}
}
// Handle missing return error (underline the method name)
if (method.hasMissingReturnError()) {
int methodNameStart = method.getNameOffset();
int methodNameEnd = methodNameStart + method.getName().length();
// Skip if method name doesn't intersect this line
if (methodNameEnd < lineStart || methodNameStart > lineEnd)
continue;
// Check if method name is on this line
if (methodNameStart >= lineStart && methodNameStart < lineEnd) {
int lineLocalStart = methodNameStart - lineStart;
if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) {
String beforeMethod = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeMethod);
int methodWidth = ClientProxy.Font.width(method.getName());
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555);
}
}
}
// Handle parameter errors (underline specific parameters)
if (method.hasParameterErrors()) {
for (MethodInfo.ParameterError paramError : method.getParameterErrors()) {
FieldInfo param = paramError.getParameter();
if (param == null || param.getDeclarationOffset() < 0)
continue;
int paramStart = param.getDeclarationOffset();
int paramEnd = paramStart + param.getName().length();
// Skip if parameter doesn't intersect this line
if (paramEnd < lineStart || paramStart > lineEnd)
continue;
// Clip to line boundaries
int clipStart = Math.max(paramStart, lineStart);
int clipEnd = Math.min(paramEnd, lineEnd);
if (clipStart >= clipEnd)
continue;
// Convert to line-local coordinates
int lineLocalStart = clipStart - lineStart;
int lineLocalEnd = clipEnd - lineStart;
// Bounds check
if (lineLocalStart < 0 || lineLocalStart >= lineText.length())
continue;
// Compute pixel position
String beforeParam = lineText.substring(0, lineLocalStart);
int beforeWidth = ClientProxy.Font.width(beforeParam);
int paramWidth;
if (lineLocalEnd > lineText.length()) {
paramWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
} else {
String paramTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
paramWidth = ClientProxy.Font.width(paramTextOnLine);
}
if (paramWidth > 0) {
drawCurlyUnderline(lineStartX + beforeWidth, baselineY, paramWidth, 0xFF5555);
}
}
}
}
}
/**
@@ -0,0 +1,161 @@
package noppes.npcs.client.gui.util.script.interpreter;
/**
* Utility class for checking type compatibility between types.
* Handles primitive widening conversions, boxing/unboxing, and inheritance.
*/
public final class TypeChecker {
private TypeChecker() {} // Utility class
/**
* Check if the actual type is compatible with (assignable to) the expected type.
* @param expected The expected/target type
* @param actual The actual/source type
* @return true if actual can be assigned to expected
*/
public static boolean isTypeCompatible(TypeInfo expected, TypeInfo actual) {
if (expected == null) return true; // void can accept anything (shouldn't happen)
if (actual == null) return true; // null is compatible with any reference type
String expectedName = expected.getSimpleName();
String actualName = actual.getSimpleName();
if (expectedName == null || actualName == null) return true;
// Exact match
if (expectedName.equals(actualName)) return true;
// Primitive widening conversions
if (isNumericType(expectedName) && isNumericType(actualName)) {
return canWiden(actualName, expectedName);
}
// Object type compatibility (check inheritance)
if (expected.getJavaClass() != null && actual.getJavaClass() != null) {
return expected.getJavaClass().isAssignableFrom(actual.getJavaClass());
}
// Allow boxed/unboxed conversions
if (isPrimitiveOrWrapper(expectedName) && isPrimitiveOrWrapper(actualName)) {
return getUnboxedName(expectedName).equals(getUnboxedName(actualName));
}
return false;
}
/**
* Check if the type is a numeric type (including wrappers).
*/
public static boolean isNumericType(String typeName) {
switch (typeName) {
case "byte": case "Byte":
case "short": case "Short":
case "int": case "Integer":
case "long": case "Long":
case "float": case "Float":
case "double": case "Double":
case "char": case "Character":
return true;
default:
return false;
}
}
/**
* Check if a primitive type can be widened to another type.
* Follows Java's widening primitive conversion rules.
*/
public static boolean canWiden(String from, String to) {
int fromRank = getNumericRank(from);
int toRank = getNumericRank(to);
return fromRank <= toRank;
}
/**
* Get the numeric rank for primitive widening conversion.
* Higher rank can accept lower ranks.
*/
public static int getNumericRank(String typeName) {
switch (getUnboxedName(typeName)) {
case "byte": return 1;
case "short": case "char": return 2;
case "int": return 3;
case "long": return 4;
case "float": return 5;
case "double": return 6;
default: return 0;
}
}
/**
* Check if the type name is a primitive or its wrapper type.
*/
public static boolean isPrimitiveOrWrapper(String typeName) {
switch (typeName) {
case "byte": case "Byte":
case "short": case "Short":
case "int": case "Integer":
case "long": case "Long":
case "float": case "Float":
case "double": case "Double":
case "char": case "Character":
case "boolean": case "Boolean":
return true;
default:
return false;
}
}
/**
* Get the primitive type name from a wrapper type name.
* Returns the input unchanged if already primitive or not a wrapper.
*/
public static String getUnboxedName(String typeName) {
switch (typeName) {
case "Byte": return "byte";
case "Short": return "short";
case "Integer": return "int";
case "Long": return "long";
case "Float": return "float";
case "Double": return "double";
case "Character": return "char";
case "Boolean": return "boolean";
default: return typeName;
}
}
/**
* Get the wrapper type name from a primitive type name.
* Returns the input unchanged if already a wrapper or not a primitive.
*/
public static String getBoxedName(String typeName) {
switch (typeName) {
case "byte": return "Byte";
case "short": return "Short";
case "int": return "Integer";
case "long": return "Long";
case "float": return "Float";
case "double": return "Double";
case "char": return "Character";
case "boolean": return "Boolean";
default: return typeName;
}
}
/**
* Check if the type name represents a void type.
*/
public static boolean isVoidType(String typeName) {
return typeName == null || typeName.equals("void") || typeName.equals("Void");
}
/**
* Check if the type represents a void type.
*/
public static boolean isVoidType(TypeInfo type) {
if (type == null) return true;
return isVoidType(type.getSimpleName());
}
}
@@ -204,6 +204,50 @@ public class TokenHoverInfo {
if (fieldInfo != null && !fieldInfo.isResolved()) {
errors.add("Cannot resolve symbol '" + token.getText() + "'");
}
// Show method declaration errors (missing return, parameter errors, return type errors)
MethodInfo methodDecl = findMethodDeclarationContainingPosition(token);
if (methodDecl != null && methodDecl.hasError()) {
int tokenStart = token.getGlobalStart();
int tokenEnd = token.getGlobalEnd();
// If hovering over the method name, show missing return error
if (methodDecl.hasMissingReturnError()) {
int methodNameStart = methodDecl.getNameOffset();
int methodNameEnd = methodNameStart + methodDecl.getName().length();
if (tokenStart >= methodNameStart && tokenEnd <= methodNameEnd) {
errors.add(methodDecl.getErrorMessage());
}
}
// If hovering over a parameter with an error, show that error
if (methodDecl.hasParameterErrors()) {
for (MethodInfo.ParameterError paramError : methodDecl.getParameterErrors()) {
FieldInfo param = paramError.getParameter();
if (param != null && param.getDeclarationOffset() >= 0) {
int paramStart = param.getDeclarationOffset();
int paramEnd = paramStart + param.getName().length();
if (tokenStart >= paramStart && tokenEnd <= paramEnd) {
errors.add(paramError.getMessage());
}
}
}
}
// If hovering over a return statement with a type error, show that error
if (methodDecl.hasReturnStatementErrors()) {
for (MethodInfo.ReturnStatementError returnError : methodDecl.getReturnStatementErrors()) {
int returnStart = returnError.getStartOffset();
int returnEnd = returnError.getEndOffset();
if (tokenStart >= returnStart && tokenEnd <= returnEnd) {
errors.add(returnError.getMessage());
}
}
}
}
}
/**
@@ -247,6 +291,36 @@ public class TokenHoverInfo {
}
return null;
}
/**
* Find the method declaration that contains this token's position.
* Returns null if the token is not within a method declaration (header or body).
*/
private MethodInfo findMethodDeclarationContainingPosition(Token token) {
ScriptLine line = token.getParentLine();
if (line == null || line.getParent() == null) {
return null;
}
ScriptDocument doc = line.getParent();
int tokenStart = token.getGlobalStart();
for (MethodInfo method : doc.getMethods()) {
if (!method.isDeclaration())
continue;
// Check if token is within the method declaration header OR body
int methodStart = method.getFullDeclarationOffset();
if (methodStart < 0) methodStart = method.getTypeOffset();
int bodyEnd = method.getBodyEnd();
// Token is within the method (header + body)
if (tokenStart >= methodStart && tokenStart <= bodyEnd) {
return method;
}
}
return null;
}
/**
* Find the argument that contains the given position.