Captured all assignments of global/local fields

This commit is contained in:
bigguy345 committed 2025-12-27 17:58:42 +02:00
1 parent a9cd2ec7ef
commit 9402ff5ec5
5 files changed
+938 -15

No files matched your search

@@ -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 +
'}';
}
}
@@ -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<AssignmentInfo> 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<AssignmentInfo> 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<AssignmentInfo> getErroredAssignments() {
List<AssignmentInfo> 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
@@ -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<String, FieldInfo> 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<ScriptLine.Mark> marks) {
// Find uppercase identifiers followed by dot (static method calls, field access)
@@ -3071,6 +3372,28 @@ public class ScriptDocument {
public Map<String, FieldInfo> getGlobalFields() {
return Collections.unmodifiableMap(globalFields);
}
/**
* Get all errored assignments across all fields (global and local).
* Used by ScriptLine to draw error underlines.
*/
public List<AssignmentInfo> getAllErroredAssignments() {
List<AssignmentInfo> errored = new ArrayList<>();
// Check global fields
for (FieldInfo field : globalFields.values()) {
errored.addAll(field.getErroredAssignments());
}
// Check method locals
for (Map<String, FieldInfo> locals : methodLocals.values()) {
for (FieldInfo field : locals.values()) {
errored.addAll(field.getErroredAssignments());
}
}
return errored;
}
public TypeResolver getTypeResolver() {
return typeResolver;
@@ -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);
}
}
}
/**
@@ -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.
*/