mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 23:04:08 +00:00
Method declaration error validation + helper classes needed for that
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
|
||||
+215
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+74
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user