ExpressionTypeResolver API now checks resolved type against expected type.

i.e. in:
String s =  true? true : "";

Both true and "" try to match s type String, and if any are incompatible it returns that so the error can be produced
This commit is contained in:
bigguy345 committed 2025-12-30 21:09:39 +02:00
1 parent 61fb092c82
commit f0b4513105
3 files changed
+207 -17

No files matched your search

@@ -18,6 +18,7 @@ import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
import scala.annotation.meta.param;
import java.util.*;
import java.util.regex.Matcher;
@@ -1930,8 +1931,8 @@ public class ScriptDocument {
continue;
}
// Parse the arguments
List<MethodCallInfo.Argument> arguments = parseMethodArguments(openParen + 1, closeParen);
// Parse the arguments (first pass - without expected types for overload resolution)
List<MethodCallInfo.Argument> arguments = parseMethodArguments(openParen + 1, closeParen, null);
// Check if this is a static access (Class.method() style)
boolean isStaticAccess = isStaticAccessCall(nameStart);
@@ -1946,12 +1947,18 @@ public class ScriptDocument {
TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new);
// Get best method overload based on argument types
resolvedMethod = receiverType.getBestMethodOverload(methodName, argTypes);
if (isStaticAccess && resolvedMethod != null && !resolvedMethod.isStatic()) {
TokenErrorMessage errorMsg = TokenErrorMessage
.from("Cannot call non-static method '" + methodName + "' from static context '" + receiverType.getSimpleName() + "'")
.clearOtherErrors();
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR, errorMsg));
} else {
// Second pass: Re-resolve arguments with expected parameter types if method was found
if (resolvedMethod != null && resolvedMethod.getParameters().size() == arguments.size())
arguments = parseMethodArguments(openParen + 1, closeParen, resolvedMethod);
MethodCallInfo callInfo = new MethodCallInfo(methodName, nameStart, nameEnd, openParen,
closeParen, arguments, receiverType, resolvedMethod, isStaticAccess);
// Set expected type for validation if this is the final expression
@@ -1979,6 +1986,11 @@ public class ScriptDocument {
TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new);
resolvedMethod = getScriptMethodInfo(methodName, argTypes);
// Second pass: Re-resolve arguments with expected parameter types if method was found
if (resolvedMethod != null && resolvedMethod.getParameters().size() == arguments.size()) {
arguments = parseMethodArguments(openParen + 1, closeParen, resolvedMethod);
}
// Check if this is a method from a script type
// Instance methods from script types cannot be called without a receiver
@@ -2144,7 +2156,15 @@ public class ScriptDocument {
* Parse method arguments from the text between opening and closing parentheses.
* Handles nested expressions, strings, and complex argument types.
*/
private List<MethodCallInfo.Argument> parseMethodArguments(int start, int end) {
/**
* Parse method call arguments.
*
* @param start Start position of arguments (after opening paren)
* @param end End position of arguments (at closing paren)
* @param methodInfo Optional MethodInfo to provide expected parameter types for validation
* @return List of parsed arguments with resolved types
*/
private List<MethodCallInfo.Argument> parseMethodArguments(int start, int end, MethodInfo methodInfo) {
List<MethodCallInfo.Argument> args = new ArrayList<>();
if (start >= end) {
@@ -2173,7 +2193,14 @@ public class ScriptDocument {
// Try to resolve the argument type
// First check if this looks like a parameter declaration (Type varName)
TypeInfo argType = resolveArgumentType(argText, actualStart);
// Get expected parameter type if available
TypeInfo expectedParamType = null;
if (methodInfo != null && args.size() < methodInfo.getParameters().size()) {
FieldInfo parameter = methodInfo.getParameters().get(args.size());
expectedParamType = parameter.getDeclaredType();
}
TypeInfo argType = resolveArgumentType(argText, actualStart, expectedParamType);
args.add(new MethodCallInfo.Argument(
argText, actualStart, actualEnd, argType, true, null
@@ -2209,8 +2236,13 @@ public class ScriptDocument {
/**
* Resolve the type of a method call argument.
* Handles both parameter declarations (Type varName) and expressions (variable.field()).
*
* @param argText The argument text
* @param position The position in the document
* @param expectedType Optional expected parameter type for validation
* @return The resolved type of the argument
*/
private TypeInfo resolveArgumentType(String argText, int position) {
private TypeInfo resolveArgumentType(String argText, int position, TypeInfo expectedType) {
argText = argText.trim();
// Check if this looks like a parameter declaration: "Type varName"
@@ -2230,8 +2262,18 @@ public class ScriptDocument {
return resolveType(typeName);
}
}
// Otherwise, treat it as an expression
// Otherwise, treat it as an expression with expected type context
if (expectedType != null) {
ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = expectedType;
try {
return resolveExpressionType(argText, position);
} finally {
ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = null;
}
}
// No expected type - resolve normally
return resolveExpressionType(argText, position);
}
@@ -3968,8 +4010,14 @@ public class ScriptDocument {
}
}
// Resolve the source type (RHS)
TypeInfo sourceType = resolveExpressionType(rhs, equalsPos + 1);
// Resolve the source type (RHS) with expected type context
TypeInfo sourceType;
ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = targetType;
try {
sourceType = resolveExpressionType(rhs, equalsPos + 1);
} finally {
ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = null;
}
// Determine if this is a script-defined field or external field
FieldInfo finalTargetField = targetField;
@@ -4068,7 +4116,15 @@ public class ScriptDocument {
}
TypeInfo targetType = targetField.getDeclaredType();
TypeInfo sourceType = resolveExpressionType(rhs, rhsStart);
// Resolve the source type with expected type context
TypeInfo sourceType;
ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = targetType;
try {
sourceType = resolveExpressionType(rhs, rhsStart);
} finally {
ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = null;
}
// Create the assignment info
// Declaration assignments should NOT check final status - this is the one place where final fields can be assigned
@@ -4219,7 +4275,8 @@ public class ScriptDocument {
}
// Parse arguments to find matching constructor
List<MethodCallInfo.Argument> arguments = parseMethodArguments(openParen + 1, closeParen);
List<MethodCallInfo.Argument> arguments = parseMethodArguments(openParen + 1, closeParen,
null);
int argCount = arguments.size();
// Try to find matching constructor (may be null if not found)
@@ -6,6 +6,14 @@ import java.util.List;
public class ExpressionTypeResolver {
private final ExpressionNode.TypeResolverContext context;
/**
* Static field to track the expected/desired type for the current expression being resolved.
* This allows TypeRules to validate type compatibility in context (e.g., ternary operator branches
* must be compatible with the assignment target type).
* Should be set before calling resolve() and cleared after.
*/
public static TypeInfo CURRENT_EXPECTED_TYPE = null;
public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context) {
this.context = context;
}
@@ -64,10 +64,11 @@ public class TypeRules {
switch (op.getCategory()) {
case ARITHMETIC:
if (op == OperatorType.ADD && (isString(left) || isString(right))) {
return TypeInfo.fromClass(String.class);
return validateAgainstExpectedType(TypeInfo.fromClass(String.class));
}
if (isNumeric(left) && isNumeric(right)) {
return binaryNumericPromotion(left, right);
TypeInfo promoted = binaryNumericPromotion(left, right);
return validateAgainstExpectedType(promoted);
}
return null;
@@ -90,11 +91,13 @@ public class TypeRules {
if (op == OperatorType.LEFT_SHIFT || op == OperatorType.RIGHT_SHIFT ||
op == OperatorType.UNSIGNED_RIGHT_SHIFT) {
if (isIntegral(left)) {
return unaryNumericPromotion(left);
TypeInfo promoted = unaryNumericPromotion(left);
return validateAgainstExpectedType(promoted);
}
}
if (isIntegral(left) && isIntegral(right)) {
return binaryNumericPromotion(left, right);
TypeInfo promoted = binaryNumericPromotion(left, right);
return validateAgainstExpectedType(promoted);
}
if (isBoolean(left) && isBoolean(right)) {
return TypeInfo.fromPrimitive("boolean");
@@ -117,11 +120,17 @@ public class TypeRules {
switch (op) {
case UNARY_PLUS:
case UNARY_MINUS:
if (isNumeric(operand)) return unaryNumericPromotion(operand);
if (isNumeric(operand)) {
TypeInfo promoted = unaryNumericPromotion(operand);
return validateAgainstExpectedType(promoted);
}
return null;
case BITWISE_NOT:
if (isIntegral(operand)) return unaryNumericPromotion(operand);
if (isIntegral(operand)) {
TypeInfo promoted = unaryNumericPromotion(operand);
return validateAgainstExpectedType(promoted);
}
return null;
case LOGICAL_NOT:
@@ -139,6 +148,103 @@ public class TypeRules {
return null;
}
}
/**
* Check if sourceType can be assigned to targetType.
* Handles primitives, numeric conversions, null compatibility, and reference types.
*/
public static boolean isAssignmentCompatible(TypeInfo sourceType, TypeInfo targetType) {
if (sourceType == null || targetType == null)
return false;
if (!targetType.isResolved())
return true; // Can't validate against unresolved type
// Null literal can be assigned to any reference type
if ("<null>".equals(sourceType.getFullName())) {
return !isPrimitive(targetType);
}
if (!sourceType.isResolved())
return false;
// Exact match
if (sourceType.equals(targetType))
return true;
if (sourceType.getFullName().equals(targetType.getFullName()))
return true;
// Numeric conversions (widening primitive conversions)
String source = sourceType.getSimpleName();
String target = targetType.getSimpleName();
// byte -> short, int, long, float, double
if ("byte".equals(source)) {
return "short".equals(target) || "int".equals(target) || "long".equals(target) ||
"float".equals(target) || "double".equals(target);
}
// short -> int, long, float, double
if ("short".equals(source)) {
return "int".equals(target) || "long".equals(target) ||
"float".equals(target) || "double".equals(target);
}
// char -> int, long, float, double
if ("char".equals(source)) {
return "int".equals(target) || "long".equals(target) ||
"float".equals(target) || "double".equals(target);
}
// int -> long, float, double
if ("int".equals(source)) {
return "long".equals(target) || "float".equals(target) || "double".equals(target);
}
// long -> float, double
if ("long".equals(source)) {
return "float".equals(target) || "double".equals(target);
}
// float -> double
if ("float".equals(source)) {
return "double".equals(target);
}
// For reference types, we'd need inheritance/interface checking
// For now, return false for incompatible types
return false;
}
/**
* Check if a type is a primitive type.
*/
public static boolean isPrimitive(TypeInfo type) {
if (type == null || !type.isResolved())
return false;
String name = type.getSimpleName();
return "int".equals(name) || "long".equals(name) || "float".equals(name) ||
"double".equals(name) || "byte".equals(name) || "short".equals(name) ||
"char".equals(name) || "boolean".equals(name) || "void".equals(name);
}
/**
* Validate a computed type against the current expected type context.
* If there's an expected type and the computed type is compatible, returns the expected type.
* If incompatible, returns the computed type so the error can be properly reported.
* If no expected type context, returns the computed type unchanged.
*
* @param computedType The type computed by normal type rules
* @return The type to use (either expectedType if compatible, or computedType)
*/
public static TypeInfo validateAgainstExpectedType(TypeInfo computedType) {
if (computedType == null) return null;
TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE;
if (expectedType != null && expectedType.isResolved() && computedType.isResolved()) {
if (isAssignmentCompatible(computedType, expectedType)) {
return expectedType;
}
// Return computed type so error can be detected (incompatible with expected)
return computedType;
}
return computedType;
}
public static TypeInfo resolveTernaryType(TypeInfo thenType, TypeInfo elseType) {
if (thenType == null && elseType == null) return null;
@@ -155,7 +261,26 @@ public class TypeRules {
if (!thenType.isResolved()) return elseType.isResolved() ? elseType : null;
if (!elseType.isResolved()) return thenType;
// If there's an expected type context, validate both branches against it
TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE;
if (expectedType != null && expectedType.isResolved()) {
boolean thenCompatible = isAssignmentCompatible(thenType, expectedType);
boolean elseCompatible = isAssignmentCompatible(elseType, expectedType);
// If both branches are compatible with expected type, return expected type
if (thenCompatible && elseCompatible) {
return expectedType;
} else if (!elseCompatible) {
// Return the incompatible type so error can be detected
return elseType;
} else {
// thenType is incompatible
return thenType;
}
}
// Normal ternary type resolution without expected type context
if (thenType.equals(elseType)) return thenType;
if (isNumeric(thenType) && isNumeric(elseType)) {
return binaryNumericPromotion(thenType, elseType);