mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 21:17:32 +00:00
Captured all assignments of global/local fields
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+39
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user