mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 00:47:50 +00:00
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:
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)
|
||||
|
||||
+8
@@ -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;
|
||||
}
|
||||
|
||||
+131
-6
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user