From 9402ff5ec5eb193c47d000f9b54e5d9c55596014 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 17:58:42 +0200 Subject: [PATCH] Captured all assignments of global/local fields --- .../script/interpreter/AssignmentInfo.java | 309 +++++++++++++++++ .../util/script/interpreter/FieldInfo.java | 219 +++++++++++- .../script/interpreter/ScriptDocument.java | 323 ++++++++++++++++++ .../util/script/interpreter/ScriptLine.java | 63 +++- .../interpreter/hover/TokenHoverInfo.java | 39 +++ 5 files changed, 938 insertions(+), 15 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java new file mode 100644 index 000000000..e748834c2 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java @@ -0,0 +1,309 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +/** + * Metadata for assignment validation. + * Tracks assignment statements like "varName = expr" or "obj.field = expr" + * and validates type compatibility, access modifiers, and final status. + * + * Each AssignmentInfo represents a single assignment to a field. + * The positions are: + * - lhsStart/lhsEnd: The target variable/field position (left-hand side) + * - rhsStart/rhsEnd: From first token of RHS to the semicolon (inclusive) + */ +public class AssignmentInfo { + + /** + * Error type for assignment validation. + */ + public enum ErrorType { + NONE, + TYPE_MISMATCH, // Assigned value type doesn't match target type + FINAL_REASSIGNMENT, // Attempting to reassign a final field + PRIVATE_ACCESS, // Attempting to access private field + PROTECTED_ACCESS, // Attempting to access protected field from invalid context + UNRESOLVED_TARGET, // Target variable/field doesn't exist + STATIC_CONTEXT_ERROR // Accessing instance field from static context + } + + // Target/LHS info + private final String targetName; + private final int lhsStart; // Position of first char of target variable/field + private final int lhsEnd; // Position after last char of target + private final TypeInfo targetType; // Declared type of target + + // Source/RHS info (from first token of RHS expression to semicolon) + private final int rhsStart; // Position of first non-whitespace char after '=' + private final int rhsEnd; // Position of ';' (inclusive in the range for underlining) + private final TypeInfo sourceType; // Resolved type of RHS expression + private final String sourceExpr; // The RHS expression text (for display) + + // For chained field access (e.g., obj.field = value) + private final TypeInfo receiverType; // Type of receiver (null for simple variables) + private final Field reflectionField; // Java reflection field (for modifier checks) + + // Flag for script-defined final variables + private final boolean isFinal; + + // Validation + private ErrorType errorType = ErrorType.NONE; + private String errorMessage; + private String requiredType; + private String providedType; + + public AssignmentInfo(String targetName, int lhsStart, int lhsEnd, + TypeInfo targetType, int rhsStart, int rhsEnd, + TypeInfo sourceType, String sourceExpr, + TypeInfo receiverType, java.lang.reflect.Field reflectionField, + boolean isFinal) { + this.targetName = targetName; + this.lhsStart = lhsStart; + this.lhsEnd = lhsEnd; + this.targetType = targetType; + this.rhsStart = rhsStart; + this.rhsEnd = rhsEnd; + this.sourceType = sourceType; + this.sourceExpr = sourceExpr; + this.receiverType = receiverType; + this.reflectionField = reflectionField; + this.isFinal = isFinal; + } + + // ==================== VALIDATION ==================== + + /** + * Validate this assignment. + * Checks type compatibility, final status, and access modifiers. + */ + public void validate() { + // Check for final field reassignment from reflection + if (reflectionField != null && Modifier.isFinal(reflectionField.getModifiers())) { + setError(ErrorType.FINAL_REASSIGNMENT, + "Cannot assign a value to final variable '" + targetName + "'"); + return; + } + + // Check for final field reassignment from FieldInfo (script-defined finals) + if (isFinal) { + setError(ErrorType.FINAL_REASSIGNMENT, + "Cannot assign a value to final variable '" + targetName + "'"); + return; + } + + // Check access modifiers for chained field access + if (reflectionField != null && receiverType != null) { + int mods = reflectionField.getModifiers(); + if (Modifier.isPrivate(mods)) { + setError(ErrorType.PRIVATE_ACCESS, + "'" + targetName + "' has private access in '" + receiverType.getFullName() + "'"); + return; + } + } + + // Check type compatibility + if (targetType != null && sourceType != null) { + if (!isTypeCompatible(sourceType, targetType)) { + this.requiredType = targetType.getSimpleName(); + this.providedType = sourceType.getSimpleName(); + setError(ErrorType.TYPE_MISMATCH, buildTypeMismatchMessage()); + } + } + } + + /** + * Build a formatted type mismatch error message (IntelliJ style). + */ + private String buildTypeMismatchMessage() { + return "Required type: " + requiredType + "\nProvided: " + providedType; + } + + /** + * 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 name + if (sourceType.getFullName().equals(targetType.getFullName())) { + return true; + } + + // Same simple name (for primitives and common types) + if (sourceType.getSimpleName().equals(targetType.getSimpleName())) { + return true; + } + + // Check if sourceType is a subtype of targetType via reflection + if (sourceType.isResolved() && targetType.isResolved()) { + Class sourceClass = sourceType.getJavaClass(); + Class targetClass = targetType.getJavaClass(); + + if (sourceClass != null && targetClass != null) { + // Direct assignability + if (targetClass.isAssignableFrom(sourceClass)) { + return true; + } + + // Primitive widening conversions + if (isPrimitiveWidening(sourceClass, targetClass)) { + return true; + } + + // Boxing/unboxing + if (isBoxingCompatible(sourceClass, targetClass)) { + return true; + } + } + } + + return false; + } + + /** + * Check for primitive widening conversions. + * byte -> short -> int -> long -> float -> double + * char -> int -> long -> float -> double + */ + private boolean isPrimitiveWidening(Class from, Class to) { + if (!from.isPrimitive() || !to.isPrimitive()) { + return false; + } + + if (from == byte.class) { + return to == short.class || to == int.class || to == long.class || + to == float.class || to == double.class; + } + if (from == short.class || from == char.class) { + return to == int.class || to == long.class || to == float.class || to == double.class; + } + if (from == int.class) { + return to == long.class || to == float.class || to == double.class; + } + if (from == long.class) { + return to == float.class || to == double.class; + } + if (from == float.class) { + return to == double.class; + } + return false; + } + + /** + * Check for boxing/unboxing compatibility. + */ + private boolean isBoxingCompatible(Class from, Class to) { + if (from.isPrimitive()) { + Class wrapper = getWrapperClass(from); + if (wrapper != null && to.isAssignableFrom(wrapper)) { + return true; + } + } + if (to.isPrimitive()) { + Class wrapper = getWrapperClass(to); + if (wrapper != null && wrapper.isAssignableFrom(from)) { + return true; + } + } + return false; + } + + private Class getWrapperClass(Class primitive) { + if (primitive == boolean.class) return Boolean.class; + if (primitive == byte.class) return Byte.class; + if (primitive == char.class) return Character.class; + if (primitive == short.class) return Short.class; + if (primitive == int.class) return Integer.class; + if (primitive == long.class) return Long.class; + if (primitive == float.class) return Float.class; + if (primitive == double.class) return Double.class; + return null; + } + + private void setError(ErrorType type, String message) { + this.errorType = type; + this.errorMessage = message; + } + + // ==================== POSITION CHECKS ==================== + + /** + * Check if the given global position falls within the LHS (target) of this assignment. + */ + public boolean containsLhsPosition(int position) { + return position >= lhsStart && position < lhsEnd; + } + + /** + * Check if the given global position falls within the RHS (source) of this assignment. + */ + public boolean containsRhsPosition(int position) { + return position >= rhsStart && position <= rhsEnd; + } + + /** + * Check if the given global position falls anywhere in this assignment (LHS or RHS). + */ + public boolean containsPosition(int position) { + return position >= lhsStart && position <= rhsEnd; + } + + // ==================== ERROR TYPE CHECKS ==================== + + /** + * Check if this is an LHS error (final reassignment, access errors). + * These errors should underline the LHS. + */ + public boolean isLhsError() { + return errorType == ErrorType.FINAL_REASSIGNMENT || + errorType == ErrorType.PRIVATE_ACCESS || + errorType == ErrorType.PROTECTED_ACCESS || + errorType == ErrorType.STATIC_CONTEXT_ERROR; + } + + /** + * Check if this is an RHS error (type mismatch). + * These errors should underline the RHS. + */ + public boolean isRhsError() { + return errorType == ErrorType.TYPE_MISMATCH; + } + + // ==================== GETTERS ==================== + + public String getTargetName() { return targetName; } + public int getLhsStart() { return lhsStart; } + public int getLhsEnd() { return lhsEnd; } + public TypeInfo getTargetType() { return targetType; } + public int getRhsStart() { return rhsStart; } + public int getRhsEnd() { return rhsEnd; } + public TypeInfo getSourceType() { return sourceType; } + public String getSourceExpr() { return sourceExpr; } + public TypeInfo getReceiverType() { return receiverType; } + public java.lang.reflect.Field getReflectionField() { return reflectionField; } + + public ErrorType getErrorType() { return errorType; } + public String getErrorMessage() { return errorMessage; } + public String getRequiredType() { return requiredType; } + public String getProvidedType() { return providedType; } + + public boolean hasError() { return errorType != ErrorType.NONE; } + public boolean hasTypeMismatch() { return errorType == ErrorType.TYPE_MISMATCH; } + public boolean hasFinalReassignment() { return errorType == ErrorType.FINAL_REASSIGNMENT; } + public boolean hasAccessError() { return errorType == ErrorType.PRIVATE_ACCESS || errorType == ErrorType.PROTECTED_ACCESS; } + + @Override + public String toString() { + return "AssignmentInfo{" + + "target='" + targetName + "', " + + "lhs=[" + lhsStart + "-" + lhsEnd + "], " + + "rhs=[" + rhsStart + "-" + rhsEnd + "], " + + "targetType=" + targetType + ", " + + "sourceType=" + sourceType + ", " + + "error=" + errorType + + '}'; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java index c123af759..47eb3191a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java @@ -1,8 +1,17 @@ package noppes.npcs.client.gui.util.script.interpreter; +import scala.annotation.meta.field; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Metadata for a field (variable) declaration or reference. * Tracks the field name, type, scope, and declaration location. + * Also tracks all assignments made to this field throughout the script. */ public final class FieldInfo { @@ -26,9 +35,22 @@ public final class FieldInfo { // For local/parameter fields, track the containing method private final MethodInfo containingMethod; + // Modifiers for script-defined fields + private final int modifiers; // Java Modifier flags (e.g., Modifier.FINAL) + + // Reflection field for external types + private final Field reflectionField; + + // Assignments to this field (populated after parsing) + private final List assignments = new ArrayList<>(); + + // Declaration assignment (for initial value validation) + private AssignmentInfo declarationAssignment; + private FieldInfo(String name, Scope scope, TypeInfo declaredType, - int declarationOffset, boolean resolved, MethodInfo containingMethod, - String documentation, int initStart, int initEnd) { + int declarationOffset, boolean resolved, MethodInfo containingMethod, + String documentation, int initStart, int initEnd, int modifiers, + Field reflectionField) { this.name = name; this.scope = scope; this.declaredType = declaredType; @@ -38,54 +60,223 @@ public final class FieldInfo { this.documentation = documentation; this.initStart = initStart; this.initEnd = initEnd; + this.modifiers = modifiers; + this.reflectionField = reflectionField; } // Factory methods public static FieldInfo globalField(String name, TypeInfo type, int declOffset) { - return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, null, -1, -1); + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, null, -1, + -1, 0, null); } public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation) { - return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, documentation, -1, -1); + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, + documentation, -1, -1, 0, null); } public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation, int initStart, int initEnd) { - return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, documentation, initStart, initEnd); + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, + documentation, initStart, initEnd, 0, null); + } + + public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation, int initStart, + int initEnd, int modifiers) { + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, + documentation, initStart, initEnd, modifiers, null); } public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method) { - return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, -1, -1); + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, -1, + -1, 0, null); } public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method, int initStart, int initEnd) { - return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, initStart, initEnd); + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, + initStart, initEnd, 0, null); + } + + public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method, int initStart, + int initEnd, int modifiers) { + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, + initStart, initEnd, modifiers, null); } public static FieldInfo parameter(String name, TypeInfo type, int declOffset, MethodInfo method) { - return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method, null, -1, -1); + return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method, null, + -1, -1, 0, null); } public static FieldInfo unresolved(String name, Scope scope) { - return new FieldInfo(name, scope, null, -1, false, null, null, -1, -1); + return new FieldInfo(name, scope, null, -1, false, null, null, -1, -1, 0, null); } /** * Create a FieldInfo from reflection data for method parameters. */ public static FieldInfo reflectionParam(String name, TypeInfo type) { - return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null, -1, -1); + return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null, -1, -1, 0, null); } /** * Create a FieldInfo from reflection data for a class field. */ - public static FieldInfo fromReflection(java.lang.reflect.Field field, TypeInfo containingType) { + public static FieldInfo fromReflection(Field field, TypeInfo containingType) { String name = field.getName(); TypeInfo type = TypeInfo.fromClass(field.getType()); - return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null, -1, -1); + return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null, -1, -1, field.getModifiers(), field); } - // Getters + // ==================== ASSIGNMENT MANAGEMENT ==================== + + /** + * Add an assignment to this field. + */ + public void addAssignment(AssignmentInfo assignment) { + assignments.add(assignment); + } + + /** + * Get all assignments to this field (unmodifiable). + */ + public List getAssignments() { + return Collections.unmodifiableList(assignments); + } + + /** + * Set the declaration assignment (initial value assignment). + */ + public void setDeclarationAssignment(AssignmentInfo assignment) { + this.declarationAssignment = assignment; + } + + /** + * Get the declaration assignment (initial value assignment). + * Returns null if there was no initializer or it hasn't been validated yet. + */ + public AssignmentInfo getDeclarationAssignment() { + return declarationAssignment; + } + + /** + * Find an assignment that contains the given position. + * Returns null if no assignment contains this position. + */ + public AssignmentInfo findAssignmentAtPosition(int position) { + // Check declaration assignment first + if (declarationAssignment != null && declarationAssignment.containsPosition(position)) { + return declarationAssignment; + } + + // Check other assignments + for (AssignmentInfo assign : assignments) { + if (assign.containsPosition(position)) { + return assign; + } + } + return null; + } + + /** + * Get all errored assignments for this field. + */ + public List getErroredAssignments() { + List errored = new ArrayList<>(); + + // Include declaration assignment if it has an error + if (declarationAssignment != null && declarationAssignment.hasError()) { + errored.add(declarationAssignment); + } + + // Include all other assignments with errors + for (AssignmentInfo assign : assignments) { + if (assign.hasError()) { + errored.add(assign); + } + } + return errored; + } + + /** + * Clear all assignments (for re-parsing). + */ + public void clearAssignments() { + assignments.clear(); + declarationAssignment = null; + } + + // ==================== MODIFIER CHECKS ==================== + + /** + * Check if this field is declared as final. + * Works for both script-defined fields (via modifiers) and reflection fields. + */ + public boolean isFinal() { + if (reflectionField != null) + return Modifier.isFinal(reflectionField.getModifiers()); + + return Modifier.isFinal(modifiers); + } + + /** + * Check if this field is declared as static. + */ + public boolean isStatic() { + if (reflectionField != null) + return Modifier.isStatic(reflectionField.getModifiers()); + + return Modifier.isStatic(modifiers); + } + + /** + * Check if this field is declared as private. + */ + public boolean isPrivate() { + if (reflectionField != null) + return Modifier.isPrivate(reflectionField.getModifiers()); + + return Modifier.isPrivate(modifiers); + } + + /** + * Check if this field is declared as protected. + */ + public boolean isProtected() { + if (reflectionField != null) + return Modifier.isProtected(reflectionField.getModifiers()); + + return Modifier.isProtected(modifiers); + } + + /** + * Check if this field is declared as public. + */ + public boolean isPublic() { + if (reflectionField != null) + return Modifier.isPublic(reflectionField.getModifiers()); + + return Modifier.isPublic(modifiers); + } + + /** + * Get the raw modifiers value. + */ + public int getModifiers() { + if (reflectionField != null) + return reflectionField.getModifiers(); + + return modifiers; + } + + /** + * Get the reflection field, if available. + */ + public java.lang.reflect.Field getReflectionField() { + return reflectionField; + } + + // ==================== BASIC GETTERS ==================== + public String getName() { return name; } public Scope getScope() { return scope; } public TypeInfo getDeclaredType() { return declaredType; } @@ -135,7 +326,7 @@ public final class FieldInfo { @Override public String toString() { - return "FieldInfo{" + name + ", " + scope + ", type=" + declaredType + "}"; + return "FieldInfo{" + name + ", " + scope + ", type=" + declaredType + ", final=" + isFinal() + "}"; } @Override 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 8acc8532d..fdd9b2bf5 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 @@ -317,6 +317,9 @@ public class ScriptDocument { // Parse global fields (outside methods) parseGlobalFields(); + + // Parse and validate assignments (reassignments) - stores in FieldInfo + parseAssignments(); } /** @@ -2766,6 +2769,304 @@ public class ScriptDocument { } } + + /** + * Parse assignment statements (reassignments, not declarations) and validate type compatibility. + * This detects patterns like: varName = expr; or obj.field = expr; + * Assignments are stored in the corresponding FieldInfo, not as marks. + */ + private void parseAssignments() { + // First clear any existing assignments in all FieldInfo objects + for (FieldInfo field : globalFields.values()) { + field.clearAssignments(); + } + for (Map locals : methodLocals.values()) { + for (FieldInfo field : locals.values()) { + field.clearAssignments(); + } + } + + // Pattern to find assignments: identifier = expression; + // But NOT declarations (Type varName = expr) or compound assignments (+=, -=, etc.) + // We need to find '=' that is: + // 1. Not preceded by another operator (!, <, >, =) + // 2. Not followed by '=' + // 3. The LHS must be a valid l-value (variable or field access) + + int pos = 0; + while (pos < text.length()) { + // Find the next '=' character + int equalsPos = text.indexOf('=', pos); + if (equalsPos < 0) break; + + // Skip if in excluded region (string/comment) + if (isExcluded(equalsPos)) { + pos = equalsPos + 1; + continue; + } + + // Check it's not part of ==, !=, <=, >=, +=, -=, *=, /=, %=, &=, |=, ^= + if (equalsPos > 0 && "!<>=+-*/%&|^".indexOf(text.charAt(equalsPos - 1)) >= 0) { + pos = equalsPos + 1; + continue; + } + if (equalsPos < text.length() - 1 && text.charAt(equalsPos + 1) == '=') { + pos = equalsPos + 2; + continue; + } + + // Find the start of this statement (previous ; or { or } or start of text) + int stmtStart = equalsPos - 1; + while (stmtStart >= 0) { + char c = text.charAt(stmtStart); + if (c == ';' || c == '{' || c == '}') { + stmtStart++; + break; + } + stmtStart--; + } + if (stmtStart < 0) stmtStart = 0; + + // Find the end of this statement (next ;) + int stmtEnd = text.indexOf(';', equalsPos); + if (stmtEnd < 0) stmtEnd = text.length(); + + // Extract LHS (before =) and RHS (after =) + String lhsRaw = text.substring(stmtStart, equalsPos).trim(); + String rhsRaw = text.substring(equalsPos + 1, stmtEnd).trim(); + + // Skip empty assignments + if (lhsRaw.isEmpty() || rhsRaw.isEmpty()) { + pos = equalsPos + 1; + continue; + } + + // Check if this is a declaration (has type before variable name) + // Declarations have: Type varName = expr + // Reassignments have: varName = expr or obj.field = expr + boolean isDeclaration = isVariableDeclaration(lhsRaw); + + if (isDeclaration) { + // This is a declaration with initializer - validate the initial value + createAndAttachDeclarationAssignment(lhsRaw, rhsRaw, stmtStart, equalsPos, stmtEnd); + } else { + // This is a reassignment - create AssignmentInfo and attach to FieldInfo + createAndAttachAssignment(lhsRaw, rhsRaw, stmtStart, equalsPos, stmtEnd); + } + + pos = stmtEnd + 1; + } + } + + /** + * Check if the LHS represents a variable declaration (has a type before the variable name). + */ + private boolean isVariableDeclaration(String lhs) { + // Split by whitespace + String[] parts = lhs.trim().split("\\s+"); + + // Single word = reassignment (e.g., "x") + if (parts.length == 1) { + // Could also be a.b = expr (field access) + return false; + } + + // Check if it matches pattern: [modifiers] Type varName + // Last part is the variable name, second-to-last should be a type + String potentialType = parts[parts.length - 2]; + String potentialVar = parts[parts.length - 1]; + + // Type patterns: primitives, or capitalized class names, or generic types + if (isPrimitiveType(potentialType) || + (Character.isUpperCase(potentialType.charAt(0)) && potentialType.matches("[A-Za-z_][A-Za-z0-9_<>\\[\\],\\s]*")) || + potentialType.equals("var") || potentialType.equals("let") || potentialType.equals("const")) { + + // Make sure the variable name is a valid identifier (not a field access chain) + if (potentialVar.matches("[a-zA-Z_][a-zA-Z0-9_]*") && !potentialVar.contains(".")) { + return true; + } + } + + return false; + } + + private boolean isPrimitiveType(String type) { + return type.equals("int") || type.equals("long") || type.equals("short") || type.equals("byte") || + type.equals("float") || type.equals("double") || type.equals("boolean") || type.equals("char") || + type.equals("void"); + } + + /** + * Create an AssignmentInfo for a reassignment statement and attach it to the appropriate FieldInfo. + */ + private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, int equalsPos, int stmtEnd) { + lhs = lhs.trim(); + rhs = rhs.trim(); + + // Parse the target (LHS) + // Could be: varName or obj.field or this.field or array[index] + String targetName = lhs; + FieldInfo targetField = null; + TypeInfo targetType = null; + TypeInfo receiverType = null; + java.lang.reflect.Field reflectionField = null; + + // Find the position where the LHS starts in the actual text (skip leading whitespace) + int lhsStart = stmtStart; + while (lhsStart < equalsPos && Character.isWhitespace(text.charAt(lhsStart))) { + lhsStart++; + } + + // Calculate the actual LHS end (before '=' minus trailing whitespace) + int lhsEnd = equalsPos; + while (lhsEnd > lhsStart && Character.isWhitespace(text.charAt(lhsEnd - 1))) { + lhsEnd--; + } + + // Calculate RHS start (first non-whitespace after '=') + int rhsStart = equalsPos + 1; + while (rhsStart < stmtEnd && Character.isWhitespace(text.charAt(rhsStart))) { + rhsStart++; + } + + // RHS end is at the semicolon position (inclusive for error underline) + int rhsEnd = stmtEnd; + + // Handle chained field access (obj.field) + if (lhs.contains(".")) { + // Split into segments + String[] segments = lhs.split("\\."); + targetName = segments[segments.length - 1].trim(); + + // Resolve the chain to get the target type + String receiverExpr = lhs.substring(0, lhs.lastIndexOf('.')).trim(); + receiverType = resolveExpressionType(receiverExpr, lhsStart); + + if (receiverType != null && receiverType.isResolved()) { + // Get the field from the receiver type + if (receiverType.hasField(targetName)) { + targetField = receiverType.getFieldInfo(targetName); + targetType = targetField.getTypeInfo(); + reflectionField = targetField.getReflectionField(); + } + } + } else { + // Simple variable + targetField = resolveVariable(targetName, lhsStart); + if (targetField != null) { + targetType = targetField.getDeclaredType(); + reflectionField = targetField.getReflectionField(); + } + } + + // If we couldn't resolve the target field, we can't attach the assignment + if (targetField == null) { + return; + } + + // Resolve the source type (RHS) + TypeInfo sourceType = resolveExpressionType(rhs, equalsPos + 1); + + // Create the assignment info using the new constructor + AssignmentInfo info = new AssignmentInfo( + targetName, + lhsStart, + lhsEnd, + targetType, + rhsStart, + rhsEnd, + sourceType, + rhs, + receiverType, + reflectionField, + targetField.isFinal() + ); + + // Validate the assignment + info.validate(); + + // Attach to the FieldInfo + targetField.addAssignment(info); + } + + /** + * Create an AssignmentInfo for a declaration with initializer and attach it to the appropriate FieldInfo. + * Example: String str = 20; or final int x = "test"; + */ + private void createAndAttachDeclarationAssignment(String lhs, String rhs, int stmtStart, int equalsPos, int stmtEnd) { + lhs = lhs.trim(); + rhs = rhs.trim(); + + // Parse the declaration: [modifiers] Type varName + String[] parts = lhs.split("\\s+"); + if (parts.length < 2) { + return; // Invalid declaration format + } + + // Last part is the variable name + String varName = parts[parts.length - 1].trim(); + + // Find the actual position of the variable name in the text + int varNameStart = stmtStart; + int searchStart = stmtStart; + while (searchStart < equalsPos) { + int found = text.indexOf(varName, searchStart); + if (found >= 0 && found < equalsPos) { + // Verify it's the actual variable name (not part of type name) + // Check that it's either at start or preceded by whitespace + if (found == stmtStart || Character.isWhitespace(text.charAt(found - 1))) { + // Check that it's followed by whitespace or '=' + int afterVar = found + varName.length(); + if (afterVar >= text.length() || Character.isWhitespace(text.charAt(afterVar)) || text.charAt(afterVar) == '=') { + varNameStart = found; + break; + } + } + searchStart = found + 1; + } else { + break; + } + } + + int varNameEnd = varNameStart + varName.length(); + + // Calculate RHS positions + int rhsStart = equalsPos + 1; + while (rhsStart < stmtEnd && Character.isWhitespace(text.charAt(rhsStart))) { + rhsStart++; + } + int rhsEnd = stmtEnd; + + // Resolve the target field (should already exist from parseGlobalFields or parseLocalVariables) + FieldInfo targetField = resolveVariable(varName, varNameStart); + if (targetField == null) { + return; // Field doesn't exist, can't attach assignment + } + + TypeInfo targetType = targetField.getDeclaredType(); + TypeInfo sourceType = resolveExpressionType(rhs, rhsStart); + + // Create the assignment info + AssignmentInfo info = new AssignmentInfo( + varName, + varNameStart, + varNameEnd, + targetType, + rhsStart, + rhsEnd, + sourceType, + rhs, + null, // No receiver for simple declarations + targetField.getReflectionField(), + targetField.isFinal() + ); + + // Validate the assignment + info.validate(); + + // Attach as the declaration assignment + targetField.setDeclarationAssignment(info); + } private void markImportedClassUsages(List marks) { // Find uppercase identifiers followed by dot (static method calls, field access) @@ -3071,6 +3372,28 @@ public class ScriptDocument { public Map getGlobalFields() { return Collections.unmodifiableMap(globalFields); } + + /** + * Get all errored assignments across all fields (global and local). + * Used by ScriptLine to draw error underlines. + */ + public List getAllErroredAssignments() { + List errored = new ArrayList<>(); + + // Check global fields + for (FieldInfo field : globalFields.values()) { + errored.addAll(field.getErroredAssignments()); + } + + // Check method locals + for (Map locals : methodLocals.values()) { + for (FieldInfo field : locals.values()) { + errored.addAll(field.getErroredAssignments()); + } + } + + return errored; + } public TypeResolver getTypeResolver() { return typeResolver; 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 d08751a68..09a6159fe 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 @@ -164,7 +164,7 @@ public class ScriptLine { token.setMethodCallInfo((MethodCallInfo) mark.metadata); } else if (mark.metadata instanceof ImportData) { token.setImportData((ImportData) mark.metadata); - }else if (mark.metadata instanceof FieldAccessInfo) { + } else if (mark.metadata instanceof FieldAccessInfo) { token.setFieldAccessInfo((FieldAccessInfo) mark.metadata); } } @@ -381,6 +381,67 @@ public class ScriptLine { } } } + + // Check all errored assignments in the document (stored in FieldInfo objects) + for (AssignmentInfo assign : doc.getAllErroredAssignments()) { + // Determine what to underline based on error type + // LHS errors (final reassignment, access errors) underline the LHS + // RHS errors (type mismatch) underline the RHS + + int underlineStart, underlineEnd; + String underlineText; + + if (assign.isLhsError()) { + // Underline the LHS (target variable) + underlineStart = assign.getLhsStart(); + underlineEnd = assign.getLhsEnd(); + underlineText = assign.getTargetName(); + } else if (assign.isRhsError()) { + // Underline the RHS (source expression) + underlineStart = assign.getRhsStart(); + underlineEnd = assign.getRhsEnd(); + underlineText = assign.getSourceExpr(); + } else { + // Skip assignments with no displayable error + continue; + } + + // Skip if this doesn't intersect this line + if (underlineEnd < lineStart || underlineStart > lineEnd) + continue; + + // Clip to line boundaries + int clipStart = Math.max(underlineStart, lineStart); + int clipEnd = Math.min(underlineEnd, 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 beforeUnderline = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeUnderline); + + int underlineWidth; + if (lineLocalEnd > lineText.length()) { + // Extends past line end + underlineWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); + } else { + String underlineOnLine = lineText.substring(lineLocalStart, lineLocalEnd); + underlineWidth = ClientProxy.Font.width(underlineOnLine); + } + + if (underlineWidth > 0) { + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, underlineWidth, 0xFF5555); + } + } } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 6facd698f..05c3344fe 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -196,6 +196,13 @@ public class TokenHoverInfo { errors.add(fieldAccessInfo.getErrorMessage()); } + // Show assignment errors (type mismatch, final reassignment, etc.) + // Search through FieldInfo's assignments by position + AssignmentInfo assignmentInfo = findAssignmentContainingPosition(token); + if (assignmentInfo != null && assignmentInfo.hasError()) { + errors.add(assignmentInfo.getErrorMessage()); + } + // Show unresolved field errors FieldInfo fieldInfo = token.getFieldInfo(); if (fieldInfo != null && !fieldInfo.isResolved()) { @@ -203,6 +210,38 @@ public class TokenHoverInfo { } } + /** + * Find an assignment that contains this token's position. + * Searches through all FieldInfo's assignments in the document. + */ + private AssignmentInfo findAssignmentContainingPosition(Token token) { + ScriptLine line = token.getParentLine(); + if (line == null || line.getParent() == null) { + return null; + } + + ScriptDocument doc = line.getParent(); + int tokenStart = token.getGlobalStart(); + + // Search global fields + for (FieldInfo field : doc.getGlobalFields().values()) { + AssignmentInfo assign = field.findAssignmentAtPosition(tokenStart); + if (assign != null) { + return assign; + } + } + + // Search method locals (need access to method locals map) + // For now, use the getAllErroredAssignments approach and filter by position + for (AssignmentInfo assign : doc.getAllErroredAssignments()) { + if (assign.containsPosition(tokenStart)) { + return assign; + } + } + + return null; + } + /** * Find the method call that contains this token's position within its argument list. */