From 78a6a29b1c9ae82f6b71ca4e5bc7ee27134ed617 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 25 Dec 2025 23:12:36 +0200 Subject: [PATCH] Validated right hand expression type is the expected type at variable declaration, and underlined the mismatched type's token if not. --- .../client/gui/util/GuiScriptTextArea.java | 1 + .../script/interpreter/FieldAccessInfo.java | 172 +++++++++++++ .../script/interpreter/MethodCallInfo.java | 26 ++ .../script/interpreter/ScriptDocument.java | 243 +++++++++++++++++- .../util/script/interpreter/ScriptLine.java | 52 +++- 5 files changed, 486 insertions(+), 8 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 34b49d075..14f07e7eb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -839,6 +839,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); scriptLine.drawString(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + // scriptLine.drawStringHex(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, ScriptLine.createDefaultHexRenderer()); // Draw cursor: pause blinking while user is active recently boolean recentInput = selection.hadRecentInput(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java new file mode 100644 index 000000000..4f169fd7c --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java @@ -0,0 +1,172 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Metadata for field access validation. + * Parallel to MethodCallInfo but for field accesses like `obj.field` or `Class.staticField`. + */ +public class FieldAccessInfo { + + /** + * Validation error type for field accesses. + */ + public enum ErrorType { + NONE, + TYPE_MISMATCH, // Field type doesn't match expected type (e.g., assignment LHS) + UNRESOLVED_FIELD, // Field doesn't exist on the receiver type + STATIC_ACCESS_ERROR // Trying to access instance field statically or vice versa + } + + private final String fieldName; + private final int fieldNameStart; + private final int fieldNameEnd; + private final TypeInfo receiverType; // The type on which this field is accessed + private final FieldInfo resolvedField; // The resolved field (null if unresolved) + private final boolean isStaticAccess; // True if this is Class.field style access + private TypeInfo expectedType; // Expected field type (from assignment LHS, etc.) + + private ErrorType errorType = ErrorType.NONE; + private String errorMessage; + + public FieldAccessInfo(String fieldName, int fieldNameStart, int fieldNameEnd, + TypeInfo receiverType, FieldInfo resolvedField, boolean isStaticAccess) { + this.fieldName = fieldName; + this.fieldNameStart = fieldNameStart; + this.fieldNameEnd = fieldNameEnd; + this.receiverType = receiverType; + this.resolvedField = resolvedField; + this.isStaticAccess = isStaticAccess; + } + + // Getters + public String getFieldName() { + return fieldName; + } + + public int getFieldNameStart() { + return fieldNameStart; + } + + public int getFieldNameEnd() { + return fieldNameEnd; + } + + public TypeInfo getReceiverType() { + return receiverType; + } + + public FieldInfo getResolvedField() { + return resolvedField; + } + + public boolean isStaticAccess() { + return isStaticAccess; + } + + public TypeInfo getExpectedType() { + return expectedType; + } + + public void setExpectedType(TypeInfo expectedType) { + this.expectedType = expectedType; + } + + public ErrorType getErrorType() { + return errorType; + } + + public String getErrorMessage() { + return errorMessage; + } + + /** + * Validate this field access. + * Checks type compatibility with expected type. + */ + public void validate() { + // Check if field was resolved + if (resolvedField == null) { + setError(ErrorType.UNRESOLVED_FIELD, "Cannot resolve field '" + fieldName + "'"); + return; + } + + // Check return type compatibility with expected type (e.g., assignment LHS) + if (expectedType != null && resolvedField != null) { + TypeInfo fieldType = resolvedField.getDeclaredType(); + if (fieldType != null && !isTypeCompatible(fieldType, expectedType)) { + setError(ErrorType.TYPE_MISMATCH, + "Required type: " + expectedType.getSimpleName() + ", Provided: " + fieldType.getSimpleName()); + } + } + } + + /** + * Check if sourceType can be assigned to targetType. + */ + private boolean isTypeCompatible(TypeInfo sourceType, TypeInfo targetType) { + if (sourceType == null || targetType == null) { + return true; // Can't validate, assume compatible + } + + // Same type + if (sourceType.getFullName().equals(targetType.getFullName())) { + return true; + } + + // Check if sourceType is a subtype of targetType + if (sourceType.isResolved() && targetType.isResolved()) { + Class sourceClass = sourceType.getJavaClass(); + Class targetClass = targetType.getJavaClass(); + + if (sourceClass != null && targetClass != null) { + return targetClass.isAssignableFrom(sourceClass); + } + } + + // Primitive widening/boxing conversions would go here + // For now, just check direct equality + return false; + } + + private void setError(ErrorType type, String message) { + this.errorType = type; + this.errorMessage = message; + } + + /** + * Check if this field access has any validation error. + */ + public boolean hasError() { + return errorType != ErrorType.NONE; + } + + /** + * Check if this is a type mismatch error. + */ + public boolean hasTypeMismatch() { + return errorType == ErrorType.TYPE_MISMATCH; + } + + /** + * Check if this is an unresolved field error. + */ + public boolean hasUnresolvedField() { + return errorType == ErrorType.UNRESOLVED_FIELD; + } + + /** + * Check if this is a static access error. + */ + public boolean hasStaticAccessError() { + return errorType == ErrorType.STATIC_ACCESS_ERROR; + } + + @Override + public String toString() { + return "FieldAccessInfo{" + + "fieldName='" + fieldName + "', " + + "receiverType=" + receiverType + ", " + + "resolvedField=" + resolvedField + ", " + + "errorType=" + errorType + + '}'; + } +} 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 index f9440a492..2d7d0afae 100644 --- 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 @@ -79,6 +79,7 @@ public class MethodCallInfo { 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 + RETURN_TYPE_MISMATCH, // Return type doesn't match expected type (e.g., assignment LHS) UNRESOLVED_METHOD, // Method doesn't exist UNRESOLVED_RECEIVER // Can't resolve the receiver type } @@ -92,6 +93,7 @@ public class MethodCallInfo { 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 TypeInfo expectedType; // Expected return type (from assignment LHS, etc.) private ErrorType errorType = ErrorType.NONE; private String errorMessage; @@ -162,6 +164,14 @@ public class MethodCallInfo { return isStaticAccess; } + public TypeInfo getExpectedType() { + return expectedType; + } + + public void setExpectedType(TypeInfo expectedType) { + this.expectedType = expectedType; + } + public ErrorType getErrorType() { return errorType; } @@ -211,6 +221,13 @@ public class MethodCallInfo { return errorType == ErrorType.STATIC_ACCESS_ERROR; } + /** + * Check if this is a return type mismatch error. + */ + public boolean hasReturnTypeMismatch() { + return errorType == ErrorType.RETURN_TYPE_MISMATCH; + } + // Setters for validation results public void setError(ErrorType type, String message) { this.errorType = type; @@ -296,6 +313,15 @@ public class MethodCallInfo { setArgTypeError(i, "Cannot resolve type of argument '" + arg.getText() + "'"); } } + + // Check return type compatibility with expected type (e.g., assignment LHS) + if (expectedType != null && resolvedMethod != null) { + TypeInfo returnType = resolvedMethod.getReturnType(); + if (returnType != null && !isTypeCompatible(returnType, expectedType)) { + setError(ErrorType.RETURN_TYPE_MISMATCH, + "Required type: " + expectedType.getSimpleName() + ", Provided: " + returnType.getSimpleName()); + } + } } /** 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 78209f711..8b6c08c0c 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 @@ -76,6 +76,9 @@ public class ScriptDocument { // Method calls - stores all parsed method call information private final List methodCalls = new ArrayList<>(); + // Field accesses - stores all parsed field access information + private final List fieldAccesses = new ArrayList<>(); + // Excluded regions (strings/comments) - positions where other patterns shouldn't match private final List excludedRanges = new ArrayList<>(); @@ -1322,6 +1325,15 @@ public class ScriptDocument { methodName, nameStart, nameEnd, openParen, closeParen, arguments, receiverType, resolvedMethod, computedStaticAccess ); + + // Only set expected type if this is the final expression (not followed by .field or .method) + if (!isFollowedByDot(closeParen)) { + TypeInfo expectedType = findExpectedTypeAtPosition(nameStart); + if (expectedType != null) { + callInfo.setExpectedType(expectedType); + } + } + callInfo.validate(); methodCalls.add(callInfo); marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); @@ -1339,6 +1351,15 @@ public class ScriptDocument { methodName, nameStart, nameEnd, openParen, closeParen, arguments, null, resolvedMethod ); + + // Only set expected type if this is the final expression (not followed by .field or .method) + if (!isFollowedByDot(closeParen)) { + TypeInfo expectedType = findExpectedTypeAtPosition(nameStart); + if (expectedType != null) { + callInfo.setExpectedType(expectedType); + } + } + callInfo.validate(); methodCalls.add(callInfo); marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); @@ -2247,7 +2268,29 @@ public class ScriptDocument { //TODO: THIS MAY NEED TO HANDLE STATIC VS INSTANCE FIELDS WITH DIFFERENT RENDERING if (currentType.hasField(segment)) { FieldInfo fieldInfo = currentType.getFieldInfo(segment); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); + + // Check if this is the last segment and if it's in an assignment context + boolean isLastSegment = (i == chainSegments.size() - 1); + if (isLastSegment) { + // Create FieldAccessInfo for validation + boolean isStaticAccess = Character.isUpperCase(chainSegments.get(i-1).charAt(0)); + FieldAccessInfo accessInfo = new FieldAccessInfo( + segment, segPos[0], segPos[1], currentType, fieldInfo, isStaticAccess + ); + + // Set expected type from assignment context + TypeInfo expectedType = findExpectedTypeAtPosition(segPos[0]); + if (expectedType != null) { + accessInfo.setExpectedType(expectedType); + } + + accessInfo.validate(); + fieldAccesses.add(accessInfo); + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); + } else { + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); + } + // Update currentType for next segment currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; @@ -2315,7 +2358,35 @@ public class ScriptDocument { FieldInfo fInfo = null; if (receiverType != null && receiverType.hasField(firstField)) { fInfo = receiverType.getFieldInfo(firstField); - marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, fInfo)); + + // Check if there are more segments after this one + boolean hasMoreSegments = false; + int checkPos = identEnd; + while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos))) + checkPos++; + if (checkPos < text.length() && text.charAt(checkPos) == '.') { + hasMoreSegments = true; + } + + if (!hasMoreSegments) { + // This is the final field - create FieldAccessInfo for validation + boolean isStaticAccess = false; // Determine based on receiver + FieldAccessInfo accessInfo = new FieldAccessInfo( + firstField, identStart, identEnd, receiverType, fInfo, isStaticAccess + ); + + // Set expected type from assignment context + TypeInfo expectedType = findExpectedTypeAtPosition(identStart); + if (expectedType != null) { + accessInfo.setExpectedType(expectedType); + } + + accessInfo.validate(); + fieldAccesses.add(accessInfo); + marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, accessInfo)); + } else { + marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, fInfo)); + } } else { marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR)); } @@ -2352,9 +2423,38 @@ public class ScriptDocument { if (isExcluded(nStart)) break; + // Check if this is the last segment (no more dots after) + boolean isLastSegment = true; + int checkNext = nEnd; + while (checkNext < text.length() && Character.isWhitespace(text.charAt(checkNext))) + checkNext++; + if (checkNext < text.length() && text.charAt(checkNext) == '.') { + isLastSegment = false; + } + if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) { FieldInfo segInfo = currentType.getFieldInfo(seg); - marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, segInfo)); + + if (isLastSegment) { + // Create FieldAccessInfo for validation + boolean isStaticAccess = false; + FieldAccessInfo accessInfo = new FieldAccessInfo( + seg, nStart, nEnd, currentType, segInfo, isStaticAccess + ); + + // Set expected type from assignment context + TypeInfo expectedType = findExpectedTypeAtPosition(nStart); + if (expectedType != null) { + accessInfo.setExpectedType(expectedType); + } + + accessInfo.validate(); + fieldAccesses.add(accessInfo); + marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, accessInfo)); + } else { + marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, segInfo)); + } + currentType = (segInfo != null) ? segInfo.getTypeInfo() : null; } else { marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR)); @@ -2570,6 +2670,14 @@ public class ScriptDocument { return i < text.length() && text.charAt(i) == '('; } + private boolean isFollowedByDot(int position) { + // Start checking after the given position (e.g. after a closing paren) + int i = position + 1; + while (i < text.length() && Character.isWhitespace(text.charAt(i))) + i++; + return i < text.length() && text.charAt(i) == '.'; + } + private boolean isInImportOrPackage(int position) { if (position < 0 || position >= text.length()) return false; @@ -2635,6 +2743,10 @@ public class ScriptDocument { return Collections.unmodifiableList(methodCalls); } + public List getFieldAccesses() { + return Collections.unmodifiableList(fieldAccesses); + } + public Map getGlobalFields() { return Collections.unmodifiableMap(globalFields); } @@ -2669,4 +2781,129 @@ public class ScriptDocument { } return -1; } + + /** + * Find the expected type for an expression at the given position by looking for assignment context. + * Returns the type of the variable being assigned to, or null if not in an assignment. + * + * Examples: + * - "Type varName = expr;" -> returns Type + * - "varName = expr;" -> returns type of varName + */ + public TypeInfo findExpectedTypeAtPosition(int position) { + // Walk backward from position to find '=' + int pos = position - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } + + // Look for '=' that precedes this position + int equalsPos = -1; + int depth = 0; // Track parentheses/brackets depth + for (int i = pos; i >= 0; i--) { + if (isExcluded(i)) continue; + + char c = text.charAt(i); + if (c == ')' || c == ']') depth++; + else if (c == '(' || c == '[') depth--; + else if (c == '=' && depth == 0) { + // Make sure it's not ==, !=, <=, >= + if (i > 0 && "!<>=".indexOf(text.charAt(i - 1)) >= 0) continue; + if (i < text.length() - 1 && text.charAt(i + 1) == '=') continue; + equalsPos = i; + break; + } + else if (c == ';' || c == '{' || c == '}') { + // Reached statement boundary without finding assignment + break; + } + } + + if (equalsPos < 0) { + return null; // Not in an assignment + } + + // Now parse the left-hand side of the assignment + // Could be: "Type varName = expr" or "varName = expr" + pos = equalsPos - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } + + if (pos < 0) return null; + + // Find the variable name (work backward to find identifier) + int varNameEnd = pos + 1; + int varNameStart = pos; + while (varNameStart >= 0 && (Character.isJavaIdentifierPart(text.charAt(varNameStart)))) { + varNameStart--; + } + varNameStart++; // Move to first char of identifier + + if (varNameStart >= varNameEnd) return null; + + String varName = text.substring(varNameStart, varNameEnd); + + // Now check if there's a type declaration before the variable name + pos = varNameStart - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } + + if (pos < 0) { + // Just "varName = expr" - look up varName in scope + return lookupVariableType(varName, position); + } + + // Check if this could be a type declaration (look for identifier before varName) + int typeEnd = pos + 1; + int typeStart = pos; + while (typeStart >= 0 && (Character.isJavaIdentifierPart(text.charAt(typeStart)) || + text.charAt(typeStart) == '<' || text.charAt(typeStart) == '>' || + text.charAt(typeStart) == '[' || text.charAt(typeStart) == ']' || + text.charAt(typeStart) == ',')) { + typeStart--; + } + typeStart++; + + if (typeStart >= typeEnd) { + // Just "varName = expr" - look up varName + return lookupVariableType(varName, position); + } + + String typeStr = text.substring(typeStart, typeEnd).trim(); + + // Check if this is a var/let/const keyword (type inference) + if (typeStr.equals("var") || typeStr.equals("let") || typeStr.equals("const")) { + // Can't determine expected type from var/let/const + return null; + } + + // Resolve the type + return resolveType(typeStr); + } + + /** + * Look up the type of a variable by name at the given position. + */ + private TypeInfo lookupVariableType(String varName, int position) { + // Check method locals + MethodInfo containingMethod = findMethodAtPosition(position); + if (containingMethod != null) { + Map locals = methodLocals.get(containingMethod.getDeclarationOffset()); + if (locals != null && locals.containsKey(varName)) { + FieldInfo field = locals.get(varName); + if (field.isVisibleAt(position)) { + return field.getDeclaredType(); + } + } + } + + // Check global fields + if (globalFields.containsKey(varName)) { + return globalFields.get(varName).getDeclaredType(); + } + + return null; + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 950ab8a9e..1383891bd 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 @@ -236,17 +236,19 @@ public class ScriptLine { // Draw the text ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor); +// Minecraft.getMinecraft().fontRenderer.drawString(builder.toString(), x, y, defaultColor); - // Draw underlines for method call arguments that intersect this line - drawMethodCallUnderlines(x, y + ClientProxy.Font.height() - 1); + + // Draw underlines for validation errors (method calls and field accesses) + drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1); } /** - * Draw underlines for all method call arguments that intersect this line. + * Draw underlines for all validation errors (method calls and field accesses) that intersect this line. * Uses the same token-walking logic as Token.drawUnderline but applies it - * to ALL method calls in the document, enabling multi-line underlines. + * to ALL validatable items in the document, enabling multi-line underlines. */ - private void drawMethodCallUnderlines(int lineStartX, int baselineY) { + private void drawErrorUnderlines(int lineStartX, int baselineY) { if (parent == null) return; @@ -287,6 +289,20 @@ public class ScriptLine { } } + // Handle return type mismatch (underline the method name) + if (call.hasReturnTypeMismatch()) { + // Check if method name is on this line + if (methodStart >= lineStart && methodStart < lineEnd) { + int lineLocalStart = methodStart - lineStart; + if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { + String beforeMethod = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeMethod); + int methodWidth = ClientProxy.Font.width(call.getMethodName()); + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555); + } + } + } + // Handle arg type errors (underline specific arguments) if (call.hasArgTypeError()) { for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) { @@ -330,6 +346,30 @@ public class ScriptLine { } } } + + // Check all field accesses in the document + for (FieldAccessInfo access : doc.getFieldAccesses()) { + // Skip if this access doesn't intersect this line + int fieldStart = access.getFieldNameStart(); + int fieldEnd = access.getFieldNameEnd(); + + if (fieldEnd < lineStart || fieldStart > lineEnd) + continue; + + // Handle type mismatch errors (underline the field name) + if (access.hasTypeMismatch()) { + // Check if field name is on this line + if (fieldStart >= lineStart && fieldStart < lineEnd) { + int lineLocalStart = fieldStart - lineStart; + if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { + String beforeField = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeField); + int fieldWidth = ClientProxy.Font.width(access.getFieldName()); + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, fieldWidth, 0xFF5555); + } + } + } + } } /** @@ -408,6 +448,8 @@ public class ScriptLine { if (lastIndex < text.length()) { renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF); } + drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1); + } /**