From e2b35d6d549771a93095312794707f5e00a1aa2e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 18 Feb 2026 05:21:42 +0200 Subject: [PATCH] Method reference and SAM type resolution - add full parser/type-resolver support for Java method and constructor references - infer SAM callback types for named JS script methods with conflict reporting - surface argument-level SAM/type mismatch errors in method validation --- .../script/interpreter/ScriptDocument.java | 459 +++++++++++++++++- .../expression/ExpressionParser.java | 17 +- .../expression/ExpressionTypeResolver.java | 456 +++++++++++++++-- .../interpreter/method/MethodCallInfo.java | 7 + .../script/interpreter/method/MethodInfo.java | 13 +- .../script/interpreter/type/TypeChecker.java | 7 +- 6 files changed, 899 insertions(+), 60 deletions(-) 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 e25a0ccdd..54c7a6d37 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 @@ -121,6 +121,14 @@ public class ScriptDocument { // Excluded regions (strings/comments) - positions where other patterns shouldn't match private final List excludedRanges = new ArrayList<>(); + + // SAM context tracking for named function references + // Maps method name -> first SAM signature applied (for conflict detection) + private final Map scriptMethodSamContexts = new HashMap<>(); + + // Thread-local to communicate SAM conflict errors from resolveIdentifier back to parseMethodArguments + // Set when injectSamParameterTypes detects a conflict, cleared after argument is created + private static final ThreadLocal CURRENT_SAM_CONFLICT_ERROR = new ThreadLocal<>(); // Type resolver private final TypeResolver typeResolver; @@ -338,6 +346,7 @@ public class ScriptDocument { methodCalls.clear(); externalFieldAssignments.clear(); declarationErrors.clear(); + scriptMethodSamContexts.clear(); // Unified pipeline for both languages List marks = formatUnified(); @@ -942,7 +951,7 @@ public class ScriptDocument { int paramStart = paramOffset + paramList.indexOf(pn); // Check if JSDoc has a @param tag for this parameter - TypeInfo paramType = TypeInfo.fromClass(Object.class); + TypeInfo paramType = TypeInfo.unresolved("any", "any"); if (jsDoc != null) { JSDocParamTag paramTag = jsDoc.getParamTag(pn); if (paramTag != null && paramTag.getTypeInfo() != null) { @@ -2574,6 +2583,8 @@ public class ScriptDocument { if (!isJavaScript()) { markCastTypes(marks); markUnusedImports(marks); + // Mark method reference expressions (target::methodName) + markMethodReferences(marks); } // Mark lambda and method reference operators (-> and ::) @@ -2608,12 +2619,233 @@ public class ScriptDocument { for (int i = 0; i < text.length() - 1; i++) { if (text.charAt(i) == ':' && text.charAt(i + 1) == ':') { if (!isExcluded(i)) { - marks.add(new ScriptLine.Mark(i, i + 2, TokenType.KEYWORD)); + marks.add(new ScriptLine.Mark(i, i + 2, TokenType.DEFAULT)); } } } } + /** + * Pattern for method reference: target::methodName + * target can be: identifier, qualified name (a.b.c), or 'this'/'super' + */ + private static final java.util.regex.Pattern METHOD_REF_PATTERN = + java.util.regex.Pattern.compile("([a-zA-Z_$][a-zA-Z0-9_$]*(?:\\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\\s*::\\s*([a-zA-Z_$][a-zA-Z0-9_$]*|new)"); + + /** + * Mark method reference expressions with appropriate token types. + * Target gets its resolved type's token, method name gets METHOD_CALL if valid. + * Validates that the method exists on the target type. + */ + private void markMethodReferences(List marks) { + java.util.regex.Matcher m = METHOD_REF_PATTERN.matcher(text); + while (m.find()) { + int targetStart = m.start(1); + int targetEnd = m.end(1); + int methodStart = m.start(2); + int methodEnd = m.end(2); + + // Skip if in excluded region (string/comment) + if (isExcluded(targetStart) || isExcluded(methodStart)) { + continue; + } + + String target = m.group(1); + String methodName = m.group(2); + + // Check if there are parentheses after the method name (invalid for method references) + if (methodEnd < text.length() && text.charAt(methodEnd) == '(') { + // Mark the :: as error + int doubleColonPos = targetEnd; + while (doubleColonPos < methodStart && text.charAt(doubleColonPos) != ':') { + doubleColonPos++; + } + if (doubleColonPos < methodStart) { + marks.add(new ScriptLine.Mark(doubleColonPos, doubleColonPos + 2, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("Method references cannot have parentheses. Use '" + target + "::" + methodName + "' instead of '" + target + "::" + methodName + "()'"))); + } + // Also mark the method name as error + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("Method references cannot have parentheses"))); + // Mark opening paren as error too + marks.add(new ScriptLine.Mark(methodEnd, methodEnd + 1, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("Remove parentheses from method reference"))); + continue; // Skip normal processing for this invalid reference + } + + // Mark the target based on what it resolves to + markMethodRefTarget(marks, target, targetStart, targetEnd); + + // Resolve the target type to validate method existence + TypeInfo targetType = resolveMethodRefTargetType(target, targetStart); + + if (targetType != null && targetType.isResolved()) { + // Handle constructor references (::new) + if ("new".equals(methodName)) { + if (targetType.hasConstructors()) { + MethodInfo ctorInfo = targetType.findConstructor(0); + if (ctorInfo == null) { + List ctors = targetType.getConstructors(); + if (!ctors.isEmpty()) { + ctorInfo = ctors.get(0); + } + } + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.METHOD_CALL, ctorInfo)); + } else { + // No constructors found + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("No constructor found for '" + targetType.getSimpleName() + "'"))); + } + } else if (targetType.hasMethod(methodName)) { + // Method exists - get the MethodInfo for metadata + MethodInfo methodInfo = targetType.getMethodInfo(methodName); + if (methodInfo == null) { + // Try getting from overloads if single getMethodInfo fails + List overloads = targetType.getAllMethodOverloads(methodName); + if (!overloads.isEmpty()) { + methodInfo = overloads.get(0); + } + } + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.METHOD_CALL, methodInfo)); + } else { + // Method does not exist on target type + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("Method '" + methodName + "' not found in '" + targetType.getSimpleName() + "'"))); + } + } else { + // Could not resolve target type - mark method as potentially valid (no error) + // This allows for cases where the target type cannot be resolved but might be valid at runtime + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.UNDEFINED_VAR)); + } + } + } + + /** + * Resolve the target type for a method reference expression. + * @param target The target expression (e.g., "this", "String", "java.util.Arrays") + * @param position The position in the text for scope resolution + * @return The resolved TypeInfo, or null if it cannot be resolved + */ + private TypeInfo resolveMethodRefTargetType(String target, int position) { + // Handle keywords + if ("this".equals(target)) { + // Resolve 'this' to the enclosing type + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null) { + return enclosingType; + } + // For hook methods, resolve to the implied 'this' type + MethodInfo containingMethod = findContainingMethod(position); + if (containingMethod != null && containingMethod.getContainingType() != null) { + return containingMethod.getContainingType(); + } + return null; + } + + if ("super".equals(target)) { + // Resolve 'super' to the parent type of the enclosing class + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null && enclosingType.hasSuperClass()) { + return enclosingType.getSuperClass(); + } + return null; + } + + // Handle qualified names (a.b.ClassName) + if (target.contains(".")) { + TypeInfo typeInfo = resolveType(target); + if (typeInfo != null && typeInfo.isResolved()) { + return typeInfo; + } + // Could not resolve as type - leave unresolved + return null; + } + + // Simple identifier - try as variable first (instance reference like myList::add) + FieldInfo varInfo = resolveVariable(target, position); + if (varInfo != null && varInfo.getTypeInfo() != null) { + return varInfo.getTypeInfo(); + } + + // Try as type name (class reference like String::valueOf) + TypeInfo typeInfo = resolveType(target); + if (typeInfo != null && typeInfo.isResolved()) { + return typeInfo; + } + + return null; + } + + /** + * Mark the target of a method reference with the appropriate token type. + */ + private void markMethodRefTarget(List marks, String target, int start, int end) { + // Handle keywords + if ("this".equals(target)) { + marks.add(new ScriptLine.Mark(start, end, TokenType.KEYWORD)); + return; + } + if ("super".equals(target)) { + marks.add(new ScriptLine.Mark(start, end, TokenType.KEYWORD)); + return; + } + + // Handle qualified names (a.b.ClassName) - mark the whole thing + if (target.contains(".")) { + // Try to resolve as a type + TypeInfo typeInfo = resolveType(target); + if (typeInfo != null && typeInfo.isResolved()) { + marks.add(new ScriptLine.Mark(start, end, TokenType.IMPORTED_CLASS, typeInfo)); + return; + } + // Otherwise mark the parts separately + markQualifiedTargetParts(marks, target, start); + return; + } + + // Simple identifier - determine its token type + // Check for local variable + FieldInfo varInfo = resolveVariable(target, start); + if (varInfo != null) { + TokenType tokenType = varInfo.isParameter() ? TokenType.PARAMETER + : varInfo.isGlobal() ? TokenType.GLOBAL_FIELD + : TokenType.LOCAL_FIELD; + marks.add(new ScriptLine.Mark(start, end, tokenType, varInfo)); + return; + } + + // Check for type name (imported class) + TypeInfo typeInfo = resolveType(target); + if (typeInfo != null && typeInfo.isResolved()) { + marks.add(new ScriptLine.Mark(start, end, TokenType.IMPORTED_CLASS, typeInfo)); + return; + } + + // Unknown - leave as default + } + + /** + * Mark parts of a qualified name like java.util.Arrays::asList + */ + private void markQualifiedTargetParts(List marks, String qualifiedName, int baseStart) { + String[] parts = qualifiedName.split("\\."); + int offset = baseStart; + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + int partEnd = offset + part.length(); + + if (i == parts.length - 1) { + // Last part is typically the class name + marks.add(new ScriptLine.Mark(offset, partEnd, TokenType.IMPORTED_CLASS)); + } else { + // Package parts + marks.add(new ScriptLine.Mark(offset, partEnd, TokenType.TYPE_DECL)); + } + + offset = partEnd + 1; // +1 for the dot + } + } + /** * Mark parameters in all inner callable scopes (lambdas and JS function expressions). */ @@ -3738,8 +3970,9 @@ public class ScriptDocument { } if (hasMethod) { - + TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); + // Get best method overload based on argument types resolvedMethod = receiverType.getBestMethodOverload(methodName, argTypes); @@ -3995,14 +4228,22 @@ public class ScriptDocument { TypeInfo expectedParamType = null; if (methodInfo != null && args.size() < methodInfo.getParameters().size()) { FieldInfo parameter = methodInfo.getParameters().get(args.size()); - expectedParamType = parameter.getDeclaredType(); + expectedParamType = parameter.getTypeInfo(); } TypeInfo argType = resolveArgumentType(argText, actualStart, expectedParamType); - args.add(new MethodCallInfo.Argument( - argText, actualStart, actualEnd, argType, true, null - )); + String samConflictError = CURRENT_SAM_CONFLICT_ERROR.get(); + if (samConflictError != null) { + CURRENT_SAM_CONFLICT_ERROR.remove(); + args.add(new MethodCallInfo.Argument( + argText, actualStart, actualEnd, argType, false, samConflictError + )); + } else { + args.add(new MethodCallInfo.Argument( + argText, actualStart, actualEnd, argType, true, null + )); + } } argStart = i + 1; continue; @@ -4141,9 +4382,9 @@ public class ScriptDocument { // Check if expression contains operators - if so, use the full expression resolver // Handle cast expressions: (Type)expr, ((Type)expr).method(), etc. // Also route JS function expressions and arrow lambdas through the parser - if (containsOperators(expr) || expr.startsWith("(") || looksLikeFunctionOrLambda(expr)) { - return resolveExpressionWithParserAPI(expr, position); - } + if (containsOperators(expr) || expr.contains("::") || expr.contains("->") || expr.startsWith("(") || looksLikeFunctionOrLambda(expr)) { + return resolveExpressionWithParserAPI(expr, position); + } // Invalid expressions starting with brackets if (expr.startsWith("[") || expr.startsWith("]")) { @@ -4270,6 +4511,17 @@ public class ScriptDocument { FieldInfo varInfo = resolveVariable(first.name, position); if (varInfo != null) { currentType = varInfo.getTypeInfo(); + } else if (isScriptMethod(first.name)) { + // Script method reference used as an expression. + // In JS, this is commonly used as a SAM callback (e.g., schedule("id", actionFunction)). + TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE; + TypeInfo samType = resolveScriptMethodAsSam(first.name, expectedType); + if (samType != null) { + currentType = samType; + } else if (isJavaScript()) { + // Provide a non-null placeholder type so overload selection can prefer functional-interface params. + currentType = TypeInfo.unresolved("", "__script_method_ref__"); + } } } else { // First segment is a method call - check script methods @@ -4807,10 +5059,17 @@ public class ScriptDocument { return new ExpressionNode.TypeResolverContext() { @Override public TypeInfo resolveIdentifier(String name) { - // Check for special keywords first if ("this".equals(name)) { return findEnclosingScriptType(position); } + if ("super".equals(name)) { + // Resolve super to parent class type + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null && enclosingType.hasSuperClass()) { + return enclosingType.getSuperClass(); + } + return null; + } if ("true".equals(name) || "false".equals(name)) { return TypeInfo.fromPrimitive("boolean"); } @@ -4818,13 +5077,12 @@ public class ScriptDocument { return TypeInfo.unresolved("null", ""); } - // Try to resolve as a variable + // Variable shadowing: variables take precedence over script methods FieldInfo varInfo = resolveVariable(name, position); if (varInfo != null) { return varInfo.getTypeInfo(); } - // Try as a class name (for static access) if (name.length() > 0) { TypeInfo typeCheck = resolveType(name); if (typeCheck != null && typeCheck.isResolved()) { @@ -4832,6 +5090,13 @@ public class ScriptDocument { } } + // Named script function as SAM callback: schedule("id", actionFunction) + TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE; + TypeInfo samType = resolveScriptMethodAsSam(name, expectedType); + if (samType != null) { + return samType; + } + return null; } @@ -5268,6 +5533,34 @@ public class ScriptDocument { return false; } + private TypeInfo resolveScriptMethodAsSam(String methodName, TypeInfo expectedType) { + if (methodName == null || expectedType == null || !expectedType.isFunctionalInterface() || !isScriptMethod(methodName)) { + return null; + } + + // Java mode: bare method names are invalid as SAM callbacks + if (!isJavaScript()) { + CURRENT_SAM_CONFLICT_ERROR.set( + "Bare method name '" + methodName + "' cannot be used as callback in Java. Use this::" + methodName + ); + return expectedType; + } + + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam == null) { + return null; + } + + int samArity = sam.getParameters().size(); + MethodInfo bestMatch = getScriptMethodBySamArity(methodName, samArity); + if (bestMatch == null) { + return null; + } + + injectSamParameterTypes(bestMatch, sam); + return expectedType; + } + /** * Get the MethodInfo for a script-defined method by name. */ @@ -5353,6 +5646,137 @@ public class ScriptDocument { return candidates.get(0); } + /** + * Get the best matching script method for use as a SAM callback by arity. + * Returns null if no match found, or if multiple overloads match by arity (ambiguous). + * + * @param methodName Name of the script method + * @param samArity Number of parameters in the SAM interface method + * @return The matching MethodInfo, or null if no unambiguous match + */ + private MethodInfo getScriptMethodBySamArity(String methodName, int samArity) { + List matchingByArity = new ArrayList<>(); + + for (MethodInfo method : methods) { + if (method.getName().equals(methodName)) { + if (method.getParameters().size() == samArity) { + matchingByArity.add(method); + } + } + } + + if (matchingByArity.size() == 1) { + return matchingByArity.get(0); + } + + if (matchingByArity.isEmpty()) { + return getScriptMethodInfo(methodName); + } + + CURRENT_SAM_CONFLICT_ERROR.set( + "Ambiguous overload for '" + methodName + "' with " + samArity + " parameter(s): " + + matchingByArity.size() + " overloads match" + ); + return null; + } + + private void injectSamParameterTypes(MethodInfo scriptMethod, MethodInfo sam) { + String methodName = scriptMethod.getName(); + + MethodInfo previousSam = scriptMethodSamContexts.get(methodName); + if (previousSam != null) { + if (!areSamSignaturesCompatible(previousSam, sam)) { + String existingSig = formatSamSignature(previousSam); + String newSig = formatSamSignature(sam); + CURRENT_SAM_CONFLICT_ERROR.set( + "Function '" + methodName + "' used in incompatible SAM contexts: " + + existingSig + " vs " + newSig + ); + return; + } + } else { + scriptMethodSamContexts.put(methodName, sam); + } + + List scriptParams = scriptMethod.getParameters(); + List samParams = sam.getParameters(); + + if (scriptParams.size() != samParams.size()) { + return; + } + + for (int i = 0; i < scriptParams.size(); i++) { + FieldInfo scriptParam = scriptParams.get(i); + TypeInfo samParamType = samParams.get(i).getTypeInfo(); + + if (samParamType == null) { + continue; + } + + TypeInfo declaredType = scriptParam.getDeclaredType(); + + boolean hasExplicitType = declaredType != null + && declaredType.isResolved() + && !"any".equals(declaredType.getFullName()); + + if (hasExplicitType) { + if (!TypeChecker.isTypeCompatible(declaredType, samParamType)) { + scriptMethod.addSamTypeError(i, samParamType, declaredType); + } + } else { + scriptParam.setInferredType(samParamType); + } + } + } + + private boolean areSamSignaturesCompatible(MethodInfo sam1, MethodInfo sam2) { + List params1 = sam1.getParameters(); + List params2 = sam2.getParameters(); + + if (params1.size() != params2.size()) { + return false; + } + + for (int i = 0; i < params1.size(); i++) { + TypeInfo type1 = params1.get(i).getTypeInfo(); + TypeInfo type2 = params2.get(i).getTypeInfo(); + + if (type1 == null || type2 == null) { + continue; + } + + if (!TypeChecker.isTypeCompatible(type1, type2) && !TypeChecker.isTypeCompatible(type2, type1)) { + return false; + } + } + + TypeInfo return1 = sam1.getReturnType(); + TypeInfo return2 = sam2.getReturnType(); + + if (return1 != null && return2 != null) { + if (!TypeChecker.isTypeCompatible(return1, return2) && !TypeChecker.isTypeCompatible(return2, return1)) { + return false; + } + } + + return true; + } + + private String formatSamSignature(MethodInfo sam) { + StringBuilder sb = new StringBuilder(); + sb.append("("); + List params = sam.getParameters(); + for (int i = 0; i < params.size(); i++) { + if (i > 0) sb.append(", "); + TypeInfo type = params.get(i).getTypeInfo(); + sb.append(type != null ? type.getSimpleName() : "?"); + } + sb.append(") -> "); + TypeInfo returnType = sam.getReturnType(); + sb.append(returnType != null ? returnType.getSimpleName() : "void"); + return sb.toString(); + } + /** * Check if a method belongs to a script-defined type (class/interface/enum). * Returns true if the method is defined inside a script type. @@ -5549,6 +5973,15 @@ public class ScriptDocument { if (isUppercase) continue; + // Check if it's a script method used as a value (e.g., in schedule("action", methodName)) + if (isScriptMethod(name)) { + MethodInfo scriptMethod = getScriptMethodInfo(name); + if (scriptMethod != null) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.METHOD_CALL, scriptMethod)); + continue; + } + } + // Unknown variable - mark as undefined if (containingMethod != null) { marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR, callInfo)); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java index dacd60618..cc61f1f5a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java @@ -462,19 +462,10 @@ public class ExpressionParser { int end = current().getEnd(); advance(); - // Determine if this is a static or instance method reference - boolean isStatic = false; - if (base instanceof ExpressionNode.IdentifierNode) { - // Could be Class::method or obj::method - // We'll determine this during type resolution - String targetName = ((ExpressionNode.IdentifierNode) base).getName(); - // If targetName starts with uppercase, likely a class - if (targetName.length() > 0) { - isStatic = Character.isUpperCase(targetName.charAt(0)); - } - } - - base = new ExpressionNode.MethodReferenceNode(base, methodName, isStatic, base.getStart(), end); + // isStatic is determined during type resolution, not parsing + // We pass false as a placeholder - ExpressionTypeResolver will compute + // the real value based on whether target resolves to a ClassTypeInfo + base = new ExpressionNode.MethodReferenceNode(base, methodName, false, base.getStart(), end); // Method references don't chain further (can't do obj::method.something) break; } else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { 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 70c744199..b3944e9e7 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 @@ -4,7 +4,12 @@ import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; import noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope; 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.type.ClassTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.OverloadSelector; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +import java.util.ArrayList; import java.util.List; public class ExpressionTypeResolver { @@ -372,49 +377,436 @@ public class ExpressionTypeResolver { return null; } + // ==================== Method Reference Resolution ==================== + + /** + * Resolve the type of a method reference expression (target::methodName). + * + * Supports all Java method reference forms: + * - this::method - Instance method on current object + * - super::method - Instance method on superclass + * - variable::method - Instance method on a variable's value + * - ClassName::method - Static method OR unbound instance method + * - pkg.ClassName::method - Fully qualified class reference + * - ClassName::new - Constructor reference + * - Type[]::new - Array constructor reference + * + * @param methodRef The method reference AST node + * @return The resolved type (functional interface type if valid, Object if invalid) + */ private TypeInfo resolveMethodReferenceType(ExpressionNode.MethodReferenceNode methodRef) { - // Method reference type is determined by the expected functional interface TypeInfo expectedType = CURRENT_EXPECTED_TYPE; - if (expectedType == null) { - // No expected functional interface context - return TypeInfo.fromClass(Object.class); // Fallback - } - - // Check if expected type is a functional interface - if (!isFunctionalInterface(expectedType)) { - // Not a functional interface + // Must have expected functional interface context + if (expectedType == null || !expectedType.isFunctionalInterface()) { return TypeInfo.fromClass(Object.class); } - // For now, we'll do basic validation and return the expected type - // In a full implementation, we would: - // 1. Resolve the target type (left side of ::) - // 2. Find the referenced method on the target type - // 3. Extract SAM signature from expected type - // 4. Validate parameter/return type compatibility + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam == null) { + return TypeInfo.fromClass(Object.class); + } + + // Resolve target and determine if it's a class reference (static context) + MethodReferenceTarget resolvedTarget = resolveMethodReferenceTarget(methodRef); + if (resolvedTarget == null || resolvedTarget.type == null) { + // Unresolved target - return expected type to allow forward references + return expectedType; + } + + String methodName = methodRef.getMethodName(); + + // Handle constructor references (ClassName::new or Type[]::new) + if ("new".equals(methodName)) { + return resolveConstructorReference(resolvedTarget, sam, expectedType); + } + + // Handle method references + return resolveMethodReference(resolvedTarget, methodName, sam, expectedType); + } + + /** + * Encapsulates the resolved target of a method reference. + */ + private static class MethodReferenceTarget { + final TypeInfo type; // The resolved type + final boolean isClassRef; // True if target is a class reference (for static access) + + MethodReferenceTarget(TypeInfo type, boolean isClassRef) { + this.type = type; + this.isClassRef = isClassRef; + } + } + + /** + * Resolve the target (left side of ::) for a method reference. + * Returns both the type and whether it's a class reference. + */ + private MethodReferenceTarget resolveMethodReferenceTarget(ExpressionNode.MethodReferenceNode methodRef) { + ExpressionNode target = methodRef.getTarget(); + if (target == null) { + return null; + } + + // Handle simple identifiers: this, super, variable, ClassName + if (target instanceof ExpressionNode.IdentifierNode) { + String name = ((ExpressionNode.IdentifierNode) target).getName(); + return resolveSimpleTarget(name); + } + + // Handle qualified names: pkg.ClassName or outer.Inner + if (target instanceof ExpressionNode.MemberAccessNode) { + return resolveQualifiedTarget((ExpressionNode.MemberAccessNode) target); + } + + // Handle array type targets: Type[] + if (target instanceof ExpressionNode.ArrayAccessNode) { + TypeInfo arrayType = resolveNodeType(target); + if (arrayType != null && arrayType.isResolved()) { + // Array type is always a class reference (for Type[]::new) + return new MethodReferenceTarget(arrayType, true); + } + } + + // Fallback: resolve as expression (e.g., (expr)::method) + TypeInfo exprType = resolveNodeType(target); + if (exprType != null && exprType.isResolved()) { + // Expression results are always instance references + return new MethodReferenceTarget(exprType, false); + } + + return null; + } + + /** + * Resolve a simple identifier as method reference target. + */ + private MethodReferenceTarget resolveSimpleTarget(String name) { + // this::method - instance method on current object + if ("this".equals(name)) { + TypeInfo thisType = context.resolveIdentifier("this"); + return thisType != null ? new MethodReferenceTarget(thisType, false) : null; + } + + // super::method - instance method on superclass + if ("super".equals(name)) { + TypeInfo superType = context.resolveIdentifier("super"); + return superType != null ? new MethodReferenceTarget(superType, false) : null; + } + + // Try as type name first (ClassName::method) + TypeInfo typeInfo = context.resolveTypeName(name); + if (typeInfo != null && typeInfo.isResolved()) { + // Wrap as ClassTypeInfo to indicate class reference + TypeInfo classRef = new ClassTypeInfo(typeInfo); + return new MethodReferenceTarget(classRef, true); + } + + // Try as variable (instance::method) + TypeInfo varType = context.resolveIdentifier(name); + if (varType != null && varType.isResolved()) { + // Check if variable holds a Class reference (e.g., var File = Java.type("java.io.File")) + boolean isClassRef = varType.isClassReference(); + return new MethodReferenceTarget(varType, isClassRef); + } + + return null; + } + + /** + * Resolve a qualified name (a.b.c) as method reference target. + * Handles both package-qualified class names and nested class access. + */ + private MethodReferenceTarget resolveQualifiedTarget(ExpressionNode.MemberAccessNode memberAccess) { + // Try to build qualified name and resolve as type + String qualifiedName = buildQualifiedName(memberAccess); + if (qualifiedName != null) { + TypeInfo typeInfo = context.resolveTypeName(qualifiedName); + if (typeInfo != null && typeInfo.isResolved()) { + TypeInfo classRef = new ClassTypeInfo(typeInfo); + return new MethodReferenceTarget(classRef, true); + } + } + + // Try resolving as expression chain (object.field::method) + TypeInfo exprType = resolveNodeType(memberAccess); + if (exprType != null && exprType.isResolved()) { + boolean isClassRef = exprType.isClassReference(); + return new MethodReferenceTarget(exprType, isClassRef); + } + + return null; + } + + /** + * Build a qualified name string from a MemberAccessNode chain. + * Example: a.b.c.ClassName -> "a.b.c.ClassName" + */ + private String buildQualifiedName(ExpressionNode node) { + if (node instanceof ExpressionNode.IdentifierNode) { + return ((ExpressionNode.IdentifierNode) node).getName(); + } + + if (node instanceof ExpressionNode.MemberAccessNode) { + ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node; + String baseName = buildQualifiedName(ma.getTarget()); + if (baseName != null) { + return baseName + "." + ma.getMemberName(); + } + } + + return null; + } + + /** + * Resolve a method reference (target::methodName). + */ + private TypeInfo resolveMethodReference(MethodReferenceTarget target, String methodName, + MethodInfo sam, TypeInfo expectedType) { + TypeInfo targetType = target.isClassRef && target.type instanceof ClassTypeInfo + ? ((ClassTypeInfo) target.type).getInstanceType() + : target.type; + + if (targetType == null || !targetType.hasMethod(methodName)) { + return TypeInfo.fromClass(Object.class); + } + + // Find best matching method using OverloadSelector + MethodInfo method = findBestMethodForReference(targetType, methodName, sam, target.isClassRef); + if (method == null) { + return TypeInfo.fromClass(Object.class); + } + + // Validate signature compatibility + String error = validateMethodSignature(method, sam, target.isClassRef, targetType); + if (error != null) { + return TypeInfo.fromClass(Object.class); + } - // Basic implementation: assume valid and return expected type return expectedType; } - private boolean isFunctionalInterface(TypeInfo type) { - // Check if the type has exactly one abstract method (SAM type) - // Common functional interfaces: Runnable, Supplier, Consumer, Function, Predicate, etc. + /** + * Find the best matching method for a method reference using OverloadSelector logic. + */ + private MethodInfo findBestMethodForReference(TypeInfo targetType, String methodName, + MethodInfo sam, boolean isClassRef) { + List allOverloads = targetType.getAllMethodOverloads(methodName); + if (allOverloads.isEmpty()) { + return null; + } + + // Extract SAM parameter types for overload matching + TypeInfo[] samParamTypes = extractSamParamTypes(sam, isClassRef, targetType); + + // Filter candidates by arity first, then use OverloadSelector + List candidates = new ArrayList<>(); + int expectedArity = samParamTypes.length; + + for (MethodInfo method : allOverloads) { + int methodArity = method.getParameters().size(); + + // Direct match: instance::method or ClassName::staticMethod + if (methodArity == expectedArity) { + candidates.add(method); + } + // Unbound instance method: ClassName::instanceMethod (first SAM param is receiver) + else if (isClassRef && methodArity == sam.getParameters().size() - 1 && !isStaticMethod(method)) { + candidates.add(method); + } + } + + if (candidates.isEmpty()) { + return null; + } + + if (candidates.size() == 1) { + return candidates.get(0); + } + + // Use OverloadSelector for multi-candidate selection + return OverloadSelector.selectBestOverload(candidates, samParamTypes); + } + + /** + * Extract the effective parameter types from SAM for method matching. + * For unbound instance methods, excludes the first receiver parameter. + */ + private TypeInfo[] extractSamParamTypes(MethodInfo sam, boolean isClassRef, TypeInfo targetType) { + List samParams = sam.getParameters(); + + // For class references, we might be matching an unbound instance method + // where the first SAM param is the receiver - handled in findBestMethodForReference + TypeInfo[] types = new TypeInfo[samParams.size()]; + for (int i = 0; i < samParams.size(); i++) { + types[i] = samParams.get(i).getTypeInfo(); + } + return types; + } + + /** + * Check if a method is static (Java reflection). + */ + private boolean isStaticMethod(MethodInfo method) { + java.lang.reflect.Method javaMethod = method.getJavaMethod(); + if (javaMethod != null) { + return java.lang.reflect.Modifier.isStatic(javaMethod.getModifiers()); + } + // For script-defined methods, assume non-static unless marked + return method.isStatic(); + } + + /** + * Validate method signature compatibility with SAM. + */ + private String validateMethodSignature(MethodInfo method, MethodInfo sam, + boolean isClassRef, TypeInfo targetType) { + List methodParams = method.getParameters(); + List samParams = sam.getParameters(); + + int methodArity = methodParams.size(); + int samArity = samParams.size(); + + // Check for unbound instance method reference (ClassName::instanceMethod) + boolean isUnbound = isClassRef && methodArity == samArity - 1 && !isStaticMethod(method); + + // Validate parameter count + if (methodArity != samArity && !isUnbound) { + return "Parameter count mismatch"; + } + + // Validate parameter types + int offset = isUnbound ? 1 : 0; + for (int i = 0; i < methodArity; i++) { + TypeInfo methodParamType = methodParams.get(i).getTypeInfo(); + TypeInfo samParamType = samParams.get(i + offset).getTypeInfo(); + + if (methodParamType != null && samParamType != null) { + if (!TypeChecker.isTypeCompatible(methodParamType, samParamType)) { + return "Parameter type mismatch at position " + (i + 1); + } + } + } + + // For unbound reference, validate receiver compatibility + if (isUnbound && samArity > 0) { + TypeInfo firstSamParam = samParams.get(0).getTypeInfo(); + if (firstSamParam != null && !TypeChecker.isTypeCompatible(targetType, firstSamParam)) { + return "Receiver type mismatch"; + } + } + + // Validate return type (covariant) + TypeInfo methodReturn = method.getReturnType(); + TypeInfo samReturn = sam.getReturnType(); + + if (samReturn != null && methodReturn != null) { + boolean samIsVoid = "void".equals(samReturn.getFullName()) || samReturn.getJavaClass() == void.class; + if (!samIsVoid && !TypeChecker.isTypeCompatible(samReturn, methodReturn)) { + return "Return type mismatch"; + } + } + + return null; + } + + /** + * Resolve a constructor reference (ClassName::new or Type[]::new). + */ + private TypeInfo resolveConstructorReference(MethodReferenceTarget target, MethodInfo sam, + TypeInfo expectedType) { + TypeInfo targetType = target.type; + + // Handle array constructor reference: Type[]::new + if (isArrayType(targetType)) { + return resolveArrayConstructorReference(targetType, sam, expectedType); + } + + // Get the actual class type for constructor lookup + TypeInfo classType = target.type instanceof ClassTypeInfo + ? ((ClassTypeInfo) target.type).getInstanceType() + : target.type; + + if (classType == null) { + return TypeInfo.fromClass(Object.class); + } + + // Cannot construct interfaces or abstract classes + if (classType.getKind() == TypeInfo.Kind.INTERFACE) { + return TypeInfo.fromClass(Object.class); + } + + // Extract SAM parameter types for constructor matching + List samParams = sam.getParameters(); + TypeInfo[] paramTypes = new TypeInfo[samParams.size()]; + for (int i = 0; i < samParams.size(); i++) { + paramTypes[i] = samParams.get(i).getTypeInfo(); + } + + // Find matching constructor + MethodInfo constructor = classType.findConstructor(paramTypes); + if (constructor == null && samParams.size() > 0) { + // Try by arity if exact type match fails + constructor = classType.findConstructor(samParams.size()); + } + + if (constructor == null) { + return TypeInfo.fromClass(Object.class); + } + + // Validate return type compatibility + TypeInfo samReturn = sam.getReturnType(); + if (samReturn != null && !TypeChecker.isTypeCompatible(samReturn, classType)) { + return TypeInfo.fromClass(Object.class); + } + + return expectedType; + } + + /** + * Resolve an array constructor reference: Type[]::new + * Must match IntFunction or similar single-int-param functional interface. + */ + private TypeInfo resolveArrayConstructorReference(TypeInfo arrayType, MethodInfo sam, + TypeInfo expectedType) { + List samParams = sam.getParameters(); + + // Array constructor takes exactly one int parameter (the size) + if (samParams.size() != 1) { + return TypeInfo.fromClass(Object.class); + } + + TypeInfo sizeParam = samParams.get(0).getTypeInfo(); + if (sizeParam == null || !isIntLike(sizeParam)) { + return TypeInfo.fromClass(Object.class); + } + + // Return type must be compatible with array type + TypeInfo samReturn = sam.getReturnType(); + if (samReturn != null && !TypeChecker.isTypeCompatible(samReturn, arrayType)) { + return TypeInfo.fromClass(Object.class); + } + + return expectedType; + } + + /** + * Check if a type is an array type. + */ + private boolean isArrayType(TypeInfo type) { if (type == null) return false; - - String typeName = type.getFullName(); - if (typeName == null) return false; - - return typeName.equals("java.lang.Runnable") || - typeName.contains("Supplier") || - typeName.contains("Consumer") || - typeName.contains("Function") || - typeName.contains("Predicate") || - typeName.contains("BiFunction") || - typeName.contains("BiConsumer") || - typeName.contains("UnaryOperator") || - typeName.contains("BinaryOperator"); + String name = type.getFullName(); + return name != null && name.endsWith("[]"); + } + + /** + * Check if a type is int-like (int, Integer, or numeric that can represent array size). + */ + private boolean isIntLike(TypeInfo type) { + if (type == null) return false; + String name = type.getFullName(); + return "int".equals(name) || "java.lang.Integer".equals(name) + || "long".equals(name) || "java.lang.Long".equals(name); } public static ExpressionNode.TypeResolverContext createBasicContext() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index ec396b7bb..7480ad466 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -382,6 +382,13 @@ public class MethodCallInfo { // Check each argument type for (int i = 0; i < arguments.size(); i++) { Argument arg = arguments.get(i); + + // Surface argument-level errors (e.g., SAM conflict, ambiguous overload, bare method in Java) + if (!arg.isValid() && arg.getErrorMessage() != null) { + setArgTypeError(i, arg.getErrorMessage()); + continue; // Skip remaining checks for this argument + } + FieldInfo para = resolvedMethod.getParameters().get(i); TypeInfo argType = arg.getResolvedType(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 886b1bb54..d780859d8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -43,7 +43,8 @@ public final class MethodInfo { VOID_METHOD_RETURNS_VALUE, // Void method returns a value DUPLICATE_METHOD, // Method with same signature already defined in scope DUPLICATE_PARAMETER, // Two parameters have the same name - PARAMETER_UNDEFINED // Parameter type cannot be resolved + PARAMETER_UNDEFINED, // Parameter type cannot be resolved + SAM_TYPE_INCOMPATIBLE // Explicit param type incompatible with SAM context } private final String name; @@ -549,6 +550,16 @@ public final class MethodInfo { public void addParameterError(FieldInfo param, int index, ErrorType type, String message) { parameterErrors.add(new ParameterError(param, index, type, message)); } + + public void addSamTypeError(int paramIndex, TypeInfo expectedSamType, TypeInfo actualDeclaredType) { + if (paramIndex < 0 || paramIndex >= parameters.size()) { + return; + } + FieldInfo param = parameters.get(paramIndex); + String message = "Explicit type '" + actualDeclaredType.getSimpleName() + + "' is incompatible with SAM parameter type '" + expectedSamType.getSimpleName() + "'"; + parameterErrors.add(new ParameterError(param, paramIndex, ErrorType.SAM_TYPE_INCOMPATIBLE, message)); + } /** * Add a return statement error. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index e9164f20f..70c6b73f4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -17,6 +17,12 @@ public final class TypeChecker { public static boolean isTypeCompatible(TypeInfo expected, TypeInfo actual) { if (expected == null) return true; // void can accept anything (shouldn't happen) if (actual == null) return true; // Can't verify, assume compatible + + // Script method reference placeholder: only compatible with functional interface params. + // Used to help overload selection choose SAM overloads in JavaScript. + if ("__script_method_ref__".equals(actual.getFullName())) { + return expected.isFunctionalInterface(); + } // Handle "any" type - universally compatible (JavaScript/TypeScript) if ("any".equals(expected.getFullName()) || "any".equals(actual.getFullName())) { @@ -343,4 +349,3 @@ public final class TypeChecker { return keywords; } } -