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
This commit is contained in:
bigguy345 committed 2026-02-18 05:21:42 +02:00
1 parent ba23b9f650
commit e2b35d6d54
6 files changed
+899 -60

No files matched your search

@@ -121,6 +121,14 @@ public class ScriptDocument {
// Excluded regions (strings/comments) - positions where other patterns shouldn't match
private final List<int[]> excludedRanges = new ArrayList<>();
// SAM context tracking for named function references
// Maps method name -> first SAM signature applied (for conflict detection)
private final Map<String, MethodInfo> 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<String> 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<ScriptLine.Mark> 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<ScriptLine.Mark> 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<MethodInfo> 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<MethodInfo> 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<ScriptLine.Mark> 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<ScriptLine.Mark> 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>", "__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", "<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<MethodInfo> 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<FieldInfo> scriptParams = scriptMethod.getParameters();
List<FieldInfo> 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<FieldInfo> params1 = sam1.getParameters();
List<FieldInfo> 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<FieldInfo> 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));
@@ -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)) {
@@ -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<MethodInfo> 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<MethodInfo> 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<FieldInfo> 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<FieldInfo> methodParams = method.getParameters();
List<FieldInfo> 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<FieldInfo> 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<Type[]> or similar single-int-param functional interface.
*/
private TypeInfo resolveArrayConstructorReference(TypeInfo arrayType, MethodInfo sam,
TypeInfo expectedType) {
List<FieldInfo> 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() {
@@ -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();
@@ -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.
@@ -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;
}
}