mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 23:04:08 +00:00
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:
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));
|
||||
|
||||
+4
-13
@@ -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)) {
|
||||
|
||||
+424
-32
@@ -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() {
|
||||
|
||||
+7
@@ -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();
|
||||
|
||||
+12
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user