diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java index 80c452317..6bcbd86b8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -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 arguments = parseMethodArguments(openParen + 1, closeParen); + // Parse the arguments (first pass - without expected types for overload resolution) + List 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 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 parseMethodArguments(int start, int end, MethodInfo methodInfo) { List 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 arguments = parseMethodArguments(openParen + 1, closeParen); + List arguments = parseMethodArguments(openParen + 1, closeParen, + null); int argCount = arguments.size(); // Try to find matching constructor (may be null if not found) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java index 2a1e2bcf4..86aaacec9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java @@ -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; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java index 2e6e71503..ad1428088 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java @@ -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 ("".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);