Added super() error validation

This commit is contained in:
bigguy345 committed 2025-12-31 23:36:44 +02:00
1 parent 861cdcd533
commit 7b77c1935f
4 files changed
+515 -19

No files matched your search

@@ -72,18 +72,21 @@ public class ErrorUnderlineRenderer {
int methodEnd = methodStart + call.getMethodName().length();
drawUnderlineForSpan(methodStart, methodEnd, lineStartX, baselineY,
lineText, lineStart, lineEnd, ERROR_COLOR);
}
} else if (call.hasArgTypeError()) {
// Handle return type mismatch (underline the method name) - currently commented out
// if (call.hasReturnTypeMismatch()) { ... }
// Handle return type mismatch (underline the method name) - currently commented out
// if (call.hasReturnTypeMismatch()) { ... }
// Handle arg type errors (underline specific arguments)
if (call.hasArgTypeError()) {
// Handle arg type errors (underline specific arguments)
for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) {
MethodCallInfo.Argument arg = error.getArg();
drawUnderlineForSpan(arg.getStartOffset(), arg.getEndOffset(),
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
}
} else if(call.hasError()){
// Handle other call errors (underline the whole call)
drawUnderlineForSpan(call.getMethodNameStart(), call.getCloseParenOffset() + 1,
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
}
}
@@ -56,7 +56,7 @@ public class ScriptDocument {
private static final Pattern MODIFIER_PATTERN = Pattern.compile(
"\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b");
private static final Pattern KEYWORD_PATTERN = Pattern.compile(
"\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b");
"\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|super|new|typeof|instanceof|import)\\b");
// Declarations - Updated to capture method parameters
private static final Pattern IMPORT_PATTERN = Pattern.compile(
@@ -1996,6 +1996,12 @@ public class ScriptDocument {
continue;
}
// Handle super() constructor calls
if (methodName.equals("super")) {
handleSuperConstructorCall(marks, nameStart, nameEnd, openParen, closeParen);
continue;
}
// Parse the arguments (first pass - without expected types for overload resolution)
List<MethodCallInfo.Argument> arguments = parseMethodArguments(openParen + 1, closeParen, null);
@@ -2007,7 +2013,15 @@ public class ScriptDocument {
MethodInfo resolvedMethod = null;
if (receiverType != null) {
if (receiverType.hasMethod(methodName)) {
// Check for method existence using hierarchy search if it's a ScriptTypeInfo
boolean hasMethod = false;
if (receiverType instanceof ScriptTypeInfo) {
hasMethod = ((ScriptTypeInfo) receiverType).hasMethodInHierarchy(methodName);
} else {
hasMethod = receiverType.hasMethod(methodName);
}
if (hasMethod) {
TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new);
// Get best method overload based on argument types
@@ -2504,6 +2518,14 @@ public class ScriptDocument {
return currentType;
}
}
} else if (first.name.equals("super")) {
// Resolve super to parent class
ScriptTypeInfo enclosingType = findEnclosingScriptType(position);
if (enclosingType != null && enclosingType.hasSuperClass()) {
currentType = enclosingType.getSuperClass();
} else {
return null; // No parent class
}
} else {
// Check if first segment is a type name for static access
TypeInfo typeCheck = resolveType(first.name);
@@ -2782,19 +2804,42 @@ public class ScriptDocument {
if (segment.isMethodCall) {
// Method call - get return type with argument-based overload resolution
if (currentType.hasMethod(segment.name)) {
// Check for method existence using hierarchy search if it's a ScriptTypeInfo
boolean hasMethod = false;
if (currentType instanceof ScriptTypeInfo) {
hasMethod = ((ScriptTypeInfo) currentType).hasMethodInHierarchy(segment.name);
} else {
hasMethod = currentType.hasMethod(segment.name);
}
if (hasMethod) {
// Parse argument types from the method call
TypeInfo[] argTypes = parseArgumentTypes(segment.arguments, segment.start);
// Get the best matching overload based on argument types
// getBestMethodOverload is now overridden in ScriptTypeInfo to search hierarchy
MethodInfo methodInfo = currentType.getBestMethodOverload(segment.name, argTypes);
return (methodInfo != null) ? methodInfo.getReturnType() : null;
}
return null;
} else {
// Field access
if (currentType.hasField(segment.name)) {
FieldInfo fieldInfo = currentType.getFieldInfo(segment.name);
// Field access - use hierarchy search for ScriptTypeInfo
boolean hasField = false;
FieldInfo fieldInfo = null;
if (currentType instanceof ScriptTypeInfo) {
hasField = ((ScriptTypeInfo) currentType).hasFieldInHierarchy(segment.name);
if (hasField) {
fieldInfo = ((ScriptTypeInfo) currentType).getFieldInfoInHierarchy(segment.name);
}
} else {
hasField = currentType.hasField(segment.name);
if (hasField) {
fieldInfo = currentType.getFieldInfo(segment.name);
}
}
if (hasField) {
return (fieldInfo != null) ? fieldInfo.getTypeInfo() : null;
}
return null;
@@ -3322,6 +3367,76 @@ public class ScriptDocument {
return null;
}
/**
* Find the constructor that contains the given position.
* Used for validating super() calls.
*/
private MethodInfo findEnclosingConstructor(int position) {
ScriptTypeInfo enclosingType = findEnclosingScriptType(position);
if (enclosingType == null)
return null;
for (MethodInfo constructor : enclosingType.getConstructors()) {
int bodyStart = constructor.getBodyStart();
int bodyEnd = constructor.getBodyEnd();
if (bodyStart >= 0 && bodyEnd > bodyStart && position >= bodyStart && position < bodyEnd) {
return constructor;
}
}
return null;
}
/**
* Handle super() constructor calls with validation and argument matching.
*/
private void handleSuperConstructorCall(List<ScriptLine.Mark> marks, int nameStart, int nameEnd, int openParen,
int closeParen) {
// Validate that we're in a constructor
TokenErrorMessage errorMsg = null;
//Parent class
ScriptTypeInfo enclosingType = findEnclosingScriptType(nameStart);
MethodInfo enclosingConstructor = findEnclosingConstructor(nameStart);
TypeInfo superClass = enclosingType != null ? enclosingType.getSuperClass() : null;
// Parse arguments and find matching parent constructor
List<MethodCallInfo.Argument> arguments = parseMethodArguments(openParen + 1, closeParen, null);
TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new);
// Find matching constructor in parent class
MethodInfo parentConstructor = superClass != null ? superClass.findConstructor(argTypes) : null;
if (enclosingType == null || !enclosingType.hasSuperClass()) {
// No parent class - error
errorMsg = TokenErrorMessage
.from(enclosingType == null ? "'super()' can only be used within a class" : "Class '" + enclosingType.getSimpleName() + "' does not have a parent class")
.clearOtherErrors();
} else if (enclosingConstructor == null) {
// Not in a constructor - error
errorMsg = TokenErrorMessage.from("Call to 'super()' only allowed in constructor body").clearOtherErrors();
} else if (superClass == null || !superClass.isResolved()) {
// Parent class not resolved - error
errorMsg = TokenErrorMessage.from(
"Cannot resolve parent class '" + enclosingType.getSuperClassName() + "'");
} else if (parentConstructor == null) {
// No matching constructor found
String argTypeStr = java.util.Arrays.stream(argTypes)
.map(t -> t != null ? t.getSimpleName() : "unknown")
.collect(java.util.stream.Collectors.joining(", "));
errorMsg = TokenErrorMessage
.from("No constructor found in '" + superClass.getSimpleName() + "' matching super(" + argTypeStr + ")")
.clearOtherErrors();
}
// Successfully resolved - mark as a valid method call (constructor call)
MethodCallInfo callInfo = new MethodCallInfo("super", nameStart, nameEnd, openParen, closeParen, arguments,
superClass, parentConstructor, false).setConstructor(true);
callInfo.validate();
methodCalls.add(callInfo);
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.IMPORT_KEYWORD,
errorMsg != null ? errorMsg : callInfo));
}
/**
* Check if a method name is defined in this script.
*/
@@ -3540,9 +3655,10 @@ public class ScriptDocument {
* Does NOT mark method calls (identifiers followed by parentheses) - those are handled by markMethodCalls.
*/
private void markChainedFieldAccesses(List<ScriptLine.Mark> marks) {
// Pattern to find identifier chains: identifier.identifier, this.identifier, etc.
// Start with an identifier or 'this' followed by at least one dot and another identifier
Pattern chainPattern = Pattern.compile("\\b(this|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
// Pattern to find identifier chains: identifier.identifier, this.identifier, super.identifier, etc.
// Start with an identifier, 'this', or 'super' followed by at least one dot and another identifier
Pattern chainPattern = Pattern.compile(
"\\b(this|super|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
Matcher m = chainPattern.matcher(text);
while (m.find()) {
@@ -3618,6 +3734,7 @@ public class ScriptDocument {
// Start with resolving the first segment
TypeInfo currentType = null;
boolean firstIsThis = firstSegment.equals("this");
boolean firstIsSuper = firstSegment.equals("super");
boolean firstIsPrecededByDot = isPrecededByDot(chainStart);
// Determine the starting index for marking
@@ -3628,6 +3745,28 @@ public class ScriptDocument {
// For 'this', we don't have a class context to resolve, but we can mark subsequent fields
// as global fields if they exist in globalFields
currentType = null; // We'll use globalFields for the next segment
} else if (firstIsSuper) {
// For 'super', resolve to parent class type
ScriptTypeInfo enclosingType = findEnclosingScriptType(chainStart);
if (enclosingType != null && enclosingType.hasSuperClass()) {
currentType = enclosingType.getSuperClass();
if (currentType == null || !currentType.isResolved()) {
// Mark 'super' as error - parent class not resolved
marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1],
TokenType.UNDEFINED_VAR, TokenErrorMessage
.from("Cannot resolve parent class '" + enclosingType.getSuperClassName() + "'")
.clearOtherErrors()));
return; // Can't continue
}
} else {
// Mark 'super' as error - no parent class
String errorMsg = enclosingType == null ? "'super' can only be used within a class" : "Class '" + enclosingType.getSimpleName() + "' does not have a parent class";
marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1],
TokenType.UNDEFINED_VAR, TokenErrorMessage.from(errorMsg).clearOtherErrors()));
return; // Can't continue
}
marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1],
TokenType.IMPORT_KEYWORD, currentType));
} else {
// Try to resolve as a type first (class/interface/enum name)
TypeInfo typeCheck = resolveType(firstSegment);
@@ -3698,6 +3837,38 @@ public class ScriptDocument {
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR));
currentType = null;
}
} else if (i == 1 && firstIsSuper) {
// For "super.field", currentType is already the parent class
// Search recursively through the inheritance hierarchy
boolean found = false;
FieldInfo fieldInfo = null;
if (currentType != null) {
if (currentType instanceof ScriptTypeInfo) {
found = ((ScriptTypeInfo) currentType).hasFieldInHierarchy(segment);
if (found) {
fieldInfo = ((ScriptTypeInfo) currentType).getFieldInfoInHierarchy(segment);
}
} else {
// For Java classes, hasField already checks inheritance
found = currentType.hasField(segment);
if (found) {
fieldInfo = currentType.getFieldInfo(segment);
}
}
}
if (found) {
FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], currentType,
fieldInfo, isLastSegment, false);
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo));
currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null;
} else {
String errorMsg = currentType != null ? "Field '" + segment + "' not found in parent class hierarchy starting from '" + currentType.getSimpleName() + "'" : "Cannot resolve field '" + segment + "'";
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR,
TokenErrorMessage.from(errorMsg).clearOtherErrors()));
currentType = null;
}
} else if (currentType != null && currentType.isResolved()) {
// Check if this type has this field
if (currentType.hasField(segment)) {
@@ -4556,9 +4727,12 @@ public class ScriptDocument {
List<MethodCallInfo.Argument> arguments = parseMethodArguments(openParen + 1, closeParen,
null);
int argCount = arguments.size();
TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType)
.toArray(TypeInfo[]::new);
// Try to find matching constructor (may be null if not found)
MethodInfo constructor = info.hasConstructors() ? info.findConstructor(argCount) : null;
MethodInfo constructor = info.hasConstructors() ? info.findConstructor(argTypes) : null;
// Create MethodCallInfo for constructor (even if null, so errors are tracked)
MethodCallInfo ctorCall = MethodCallInfo.constructor(
@@ -288,6 +288,297 @@ public class ScriptTypeInfo extends TypeInfo {
}
return null;
}
public MethodInfo findConstructor(TypeInfo[] argTypes) {
for (MethodInfo constructor : constructors) {
if (constructor.getParameterCount() == argTypes.length) {
boolean match = true;
List<FieldInfo> params = constructor.getParameters();
for (int i = 0; i < argTypes.length; i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
if (!TypeChecker.isTypeCompatible(paramType, argTypes[i])) {
match = false;
break;
}
}
if (match)
return constructor;
}
}
return null;
}
// ==================== INHERITANCE HIERARCHY SEARCH ====================
/**
* Check if this type or any of its parent classes has a field with the given name.
* This recursively searches up the inheritance tree.
*/
public boolean hasFieldInHierarchy(String fieldName) {
// Check this class first
if (hasField(fieldName)) {
return true;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).hasFieldInHierarchy(fieldName);
} else {
// For Java classes, hasField already checks inheritance via reflection
return superClass.hasField(fieldName);
}
}
return false;
}
/**
* Get field info from this type or any of its parent classes.
* This recursively searches up the inheritance tree.
*/
public FieldInfo getFieldInfoInHierarchy(String fieldName) {
// Check this class first
FieldInfo field = getFieldInfo(fieldName);
if (field != null) {
return field;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).getFieldInfoInHierarchy(fieldName);
} else {
// For Java classes, getFieldInfo already checks inheritance via reflection
return superClass.getFieldInfo(fieldName);
}
}
return null;
}
/**
* Check if this type or any of its parent classes has a method with the given name.
* This recursively searches up the inheritance tree.
*/
public boolean hasMethodInHierarchy(String methodName) {
// Check this class first
if (hasMethod(methodName)) {
return true;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).hasMethodInHierarchy(methodName);
} else {
// For Java classes, hasMethod already checks inheritance via reflection
return superClass.hasMethod(methodName);
}
}
return false;
}
/**
* Check if this type or any of its parent classes has a method with the given name and parameter count.
* This recursively searches up the inheritance tree.
*/
public boolean hasMethodInHierarchy(String methodName, int paramCount) {
// Check this class first
if (hasMethod(methodName, paramCount)) {
return true;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).hasMethodInHierarchy(methodName, paramCount);
} else {
// For Java classes, hasMethod already checks inheritance via reflection
return superClass.hasMethod(methodName, paramCount);
}
}
return false;
}
/**
* Get method info from this type or any of its parent classes.
* This recursively searches up the inheritance tree.
*/
public MethodInfo getMethodInfoInHierarchy(String methodName) {
// Check this class first
MethodInfo method = getMethodInfo(methodName);
if (method != null) {
return method;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).getMethodInfoInHierarchy(methodName);
} else {
// For Java classes, getMethodInfo already checks inheritance via reflection
return superClass.getMethodInfo(methodName);
}
}
return null;
}
/**
* Get method info with specific parameter count from this type or any of its parent classes.
* This recursively searches up the inheritance tree.
*/
public MethodInfo getMethodWithParamCountInHierarchy(String methodName, int paramCount) {
// Check this class first
MethodInfo method = getMethodWithParamCount(methodName, paramCount);
if (method != null) {
return method;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).getMethodWithParamCountInHierarchy(methodName, paramCount);
} else {
// For Java classes, reflection already handles inheritance
// We need to manually search for matching method
List<MethodInfo> overloads = superClass.getAllMethodOverloads(methodName);
for (MethodInfo m : overloads) {
if (m.getParameterCount() == paramCount) {
return m;
}
}
}
}
return null;
}
/**
* Get all method overloads with the given name from this type or any of its parent classes.
* This recursively searches up the inheritance tree and returns all matching overloads.
*/
public List<MethodInfo> getAllMethodOverloadsInHierarchy(String methodName) {
List<MethodInfo> result = new ArrayList<>();
// Get overloads from this class
result.addAll(getAllMethodOverloads(methodName));
// Get overloads from parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
result.addAll(((ScriptTypeInfo) superClass).getAllMethodOverloadsInHierarchy(methodName));
} else {
// For Java classes, getAllMethodOverloads already checks inheritance via reflection
result.addAll(superClass.getAllMethodOverloads(methodName));
}
}
return result;
}
/**
* Override getBestMethodOverload to search the inheritance hierarchy.
* This ensures that method overload resolution considers parent class methods.
*/
@Override
public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) {
// Get all overloads from this class and parent classes
List<MethodInfo> allOverloads = getAllMethodOverloadsInHierarchy(methodName);
if (allOverloads.isEmpty())
return null;
// If no arguments provided, try to find zero-arg method
if (argTypes == null || argTypes.length == 0) {
for (MethodInfo method : allOverloads) {
if (method.getParameterCount() == 0) {
return method;
}
}
// Fall back to first overload if no zero-arg found
return allOverloads.get(0);
}
// Phase 1: Try exact match
for (MethodInfo method : allOverloads) {
if (method.getParameterCount() == argTypes.length) {
boolean exactMatch = true;
List<FieldInfo> params = method.getParameters();
for (int i = 0; i < argTypes.length; i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
TypeInfo argType = argTypes[i];
if (paramType == null || argType == null || !paramType.equals(argType)) {
exactMatch = false;
break;
}
}
if (exactMatch)
return method;
}
}
// Phase 2: Try compatible type match (assignability)
for (MethodInfo method : allOverloads) {
if (method.getParameterCount() == argTypes.length) {
boolean compatible = true;
List<FieldInfo> params = method.getParameters();
for (int i = 0; i < argTypes.length; i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
TypeInfo argType = argTypes[i];
if (paramType == null || argType == null) {
continue; // Allow null types (unresolved)
}
// Check if argType can be assigned to paramType
if (!noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker.isTypeCompatible(paramType,
argType)) {
compatible = false;
break;
}
}
if (compatible)
return method;
}
}
// Phase 3: Fall back to first overload with matching parameter count
for (MethodInfo method : allOverloads) {
if (method.getParameterCount() == argTypes.length) {
return method;
}
}
// No match found, return first overload
return allOverloads.get(0);
}
/**
* Override getBestMethodOverload with return type expectation to search the inheritance hierarchy.
*/
@Override
public MethodInfo getBestMethodOverload(String methodName, TypeInfo expectedReturnType) {
List<MethodInfo> allOverloads = getAllMethodOverloadsInHierarchy(methodName);
if (allOverloads.isEmpty())
return null;
// If no expected return type, return first overload
if (expectedReturnType == null) {
return allOverloads.get(0);
}
// First pass: look for return type compatible overload
for (MethodInfo method : allOverloads) {
TypeInfo returnType = method.getReturnType();
if (returnType != null && noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker.isTypeCompatible(
expectedReturnType, returnType)) {
return method;
}
}
// Second pass: return any overload (first one)
return allOverloads.get(0);
}
// ==================== INNER CLASS LOOKUP ====================
@@ -4,6 +4,7 @@ import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
@@ -226,8 +227,8 @@ public class TypeInfo {
if (javaClass == null) return null;
try {
java.lang.reflect.Constructor<?>[] constructors = javaClass.getConstructors();
for (java.lang.reflect.Constructor<?> ctor : constructors) {
Constructor<?>[] constructors = javaClass.getConstructors();
for (Constructor<?> ctor : constructors) {
if (ctor.getParameterCount() == argCount) {
return MethodInfo.fromReflectionConstructor(ctor, this);
}
@@ -237,6 +238,33 @@ public class TypeInfo {
}
return null;
}
public MethodInfo findConstructor(TypeInfo[] argTypes) {
if (javaClass == null) return null;
try {
Constructor<?>[] constructors = javaClass.getConstructors();
for (Constructor<?> ctor : constructors) {
if (ctor.getParameterCount() == argTypes.length) {
Class<?>[] paramTypes = ctor.getParameterTypes();
boolean match = true;
for (int i = 0; i < argTypes.length; i++) {
TypeInfo paramTypeInfo = TypeInfo.fromClass(paramTypes[i]);
if (!TypeChecker.isTypeCompatible(paramTypeInfo, argTypes[i])) {
match = false;
break;
}
}
if (match) {
return MethodInfo.fromReflectionConstructor(ctor, this);
}
}
}
} catch (Exception e) {
// Security or linkage error
}
return null;
}
/**
* Check if this type has a field with the given name.