From 074c780d50e92b38e68dffe2ca826399d5e12d5c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 3 Feb 2026 07:03:33 +0200 Subject: [PATCH] Add inner callable scope locals and expression type resolution ScriptDocument now tracks locals inside lambdas/function expressions and resolves identifiers by consulting the innermost callable scope before falling back to method/global scope. Adds expression resolver nodes/type resolver and wires position-aware resolution for lambda bodies, plus basic lambda operator marking and return type validation against expected functional interface SAM. --- .../script/interpreter/ScriptDocument.java | 566 +++++++++++++++++- .../expression/ExpressionNode.java | 127 ++++ .../expression/ExpressionParser.java | 231 ++++++- .../expression/ExpressionToken.java | 7 +- .../expression/ExpressionTokenizer.java | 17 + .../expression/ExpressionTypeResolver.java | 281 +++++++++ .../script/interpreter/field/FieldInfo.java | 6 +- 7 files changed, 1208 insertions(+), 27 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 a12bfff5a..e7dd78a6f 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 @@ -467,6 +467,15 @@ public class ScriptDocument { } return false; } + + /** + * Get the list of excluded ranges (comments, strings, etc.). + * Used by AutocompleteManager to skip over excluded regions. + * @return List of [start, end) ranges to exclude + */ + public List getExcludedRanges() { + return excludedRanges; + } /** * Check if a position is within a comment range (not string). @@ -1330,6 +1339,144 @@ public class ScriptDocument { } } } + + // Also parse locals inside inner callable scopes + for (InnerCallableScope scope : innerScopes) { + parseLocalVariablesInScope(scope); + } + } + + /** + * Parse local variable declarations inside an inner callable scope (lambda or JS function expression). + */ + private void parseLocalVariablesInScope(InnerCallableScope scope) { + int start = scope.getBodyStart(); + int end = scope.getBodyEnd(); + + if (start < 0 || end <= start) return; + + if (isJavaScript()) { + parseJSLocalsInRange(start, end, scope); + } else { + parseJavaLocalsInRange(start, end, scope); + } + } + + /** + * Parse Java local variable declarations in the range [start, end). + * Pattern: Type varName = ... or var varName = ... + */ + private void parseJavaLocalsInRange(int start, int end, InnerCallableScope scope) { + String rangeText = text.substring(start, Math.min(end, text.length())); + + Matcher m = FIELD_DECL_PATTERN.matcher(rangeText); + while (m.find()) { + int declPos = start + m.start(); + if (declPos < start || declPos >= end) continue; + if (isExcluded(declPos)) continue; + + String typeNameRaw = m.group(1); + String varName = m.group(2); + String delimiter = m.group(3); + + // Skip control flow keywords + if (typeNameRaw.equals("return") || typeNameRaw.equals("if") || typeNameRaw.equals("while") || + typeNameRaw.equals("for") || typeNameRaw.equals("switch") || typeNameRaw.equals("catch") || + typeNameRaw.equals("new") || typeNameRaw.equals("throw")) { + continue; + } + + String typeName = stripModifiers(typeNameRaw); + int modifiers = parseModifiers(typeNameRaw); + + TypeInfo varType = null; + if (!typeName.equals("var") && !typeName.equals("let") && !typeName.equals("const")) { + varType = resolveType(typeName); + } else if (delimiter.equals("=")) { + // Infer type from initializer + int rhsStart = start + m.end(); + varType = inferTypeFromExpression(rhsStart); + } + + int initStart = -1, initEnd = -1; + if ("=".equals(delimiter)) { + initStart = start + m.start(3); + int searchPos = start + m.end(3); + int depth = 0; + int angleDepth = 0; + while (searchPos < end) { + char c = text.charAt(searchPos); + if (c == '(' || c == '[' || c == '{') depth++; + else if (c == ')' || c == ']' || c == '}') depth--; + else if (c == '<') angleDepth++; + else if (c == '>') angleDepth--; + else if ((c == ';' || c == ',') && depth == 0 && angleDepth == 0) { + initEnd = searchPos; + break; + } + searchPos++; + } + } + + int absPos = start + m.start(2); + FieldInfo localVar = FieldInfo.localField(varName, varType, absPos, null, initStart, initEnd, modifiers); + scope.addLocal(varName, localVar); + } + } + + /** + * Parse JS local variable declarations in the range [start, end). + * Pattern: var/let/const varName = ... + */ + private void parseJSLocalsInRange(int start, int end, InnerCallableScope scope) { + String rangeText = text.substring(start, Math.min(end, text.length())); + + Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*(=)\\s*([^;\\n]+))?"); + Matcher m = varPattern.matcher(rangeText); + + while (m.find()) { + int declPos = start + m.start(); + if (declPos < start || declPos >= end) continue; + if (isExcluded(declPos)) continue; + + String varName = m.group(1); + String initializer = m.group(3); + + // Check for JSDoc type annotation + JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, declPos); + + TypeInfo varType = null; + + // Priority 1: JSDoc @type + if (jsDoc != null && jsDoc.hasTypeTag()) { + varType = jsDoc.getDeclaredType(); + } + + // Priority 2: Infer from initializer + if (varType == null && initializer != null && !initializer.trim().isEmpty()) { + varType = resolveExpressionType(initializer.trim(), start + m.start(3)); + } + + // Priority 3: Use "any" type + if (varType == null) { + varType = TypeInfo.ANY; + } + + int initStart = -1, initEnd = -1; + if (m.group(2) != null) { + initStart = start + m.start(2); + initEnd = start + m.end(3); + } + + int absPos = start + m.start(1); + FieldInfo localVar = FieldInfo.localField(varName, varType, absPos, null, initStart, initEnd, 0); + + if (jsDoc != null) { + localVar.setJSDocInfo(jsDoc); + } + + scope.addLocal(varName, localVar); + } } /** @@ -2429,12 +2576,226 @@ public class ScriptDocument { markUnusedImports(marks); } + // Mark lambda and method reference operators (-> and ::) + markLambdaOperators(marks); + + // Mark lambda/function parameters with type info + markInnerScopeParameters(marks); + + // Validate lambda return types + validateLambdaReturnTypes(marks); + // Final pass: Mark any remaining unmarked identifiers as undefined markUndefinedIdentifiers(marks); return marks; } + /** + * Mark lambda arrow (->) and method reference (::) operators. + */ + private void markLambdaOperators(List marks) { + // Mark -> operators (lambda arrows) + 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)); + } + } + } + + // Mark :: operators (method references) + 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)); + } + } + } + } + + /** + * Mark parameters in all inner callable scopes (lambdas and JS function expressions). + */ + private void markInnerScopeParameters(List marks) { + for (InnerCallableScope scope : innerScopes) { + for (FieldInfo param : scope.getParameters()) { + int start = param.getDeclarationOffset(); + int end = start + param.getName().length(); + + // Use FieldInfo as metadata - hover system will extract tooltip + marks.add(new ScriptLine.Mark(start, end, TokenType.PARAMETER, param)); + } + } + } + + /** + * Validate lambda return types against expected SAM return type. + */ + private void validateLambdaReturnTypes(List marks) { + for (InnerCallableScope scope : innerScopes) { + if (scope.getKind() == InnerCallableScope.Kind.JAVA_LAMBDA || + scope.getKind() == InnerCallableScope.Kind.JS_FUNCTION_EXPR) { + validateLambdaReturnType(scope, marks); + } + } + } + + /** + * Validate that a lambda's body return type matches the expected SAM return type. + */ + private void validateLambdaReturnType(InnerCallableScope lambda, List marks) { + TypeInfo expectedType = lambda.getExpectedType(); + if (expectedType == null || !expectedType.isFunctionalInterface()) { + return; // No expected type to validate against + } + + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam == null) { + return; + } + + TypeInfo expectedReturnType = sam.getReturnType(); + + int bodyStart = lambda.getBodyStart(); + int bodyEnd = lambda.getBodyEnd(); + if (bodyStart < 0 || bodyEnd <= bodyStart || bodyEnd > text.length()) { + return; + } + + String bodyText = text.substring(bodyStart, bodyEnd).trim(); + + if (bodyText.startsWith("{")) { + // Block lambda - validate return statements + validateBlockLambdaReturns(lambda, bodyStart, bodyEnd, expectedReturnType, marks); + } else { + // Expression lambda - validate expression type + try { + TypeInfo bodyType = resolveExpressionType(bodyText, bodyStart); + + if (bodyType != null && expectedReturnType != null) { + // Check compatibility + if (!isCompatibleType(bodyType, expectedReturnType)) { + // Mark error at body start + String error = "Incompatible return type: expected " + + expectedReturnType.getSimpleName() + + " but was " + bodyType.getSimpleName(); + marks.add(new ScriptLine.Mark(bodyStart, bodyEnd, TokenType.UNDEFINED_VAR, TokenErrorMessage.from(error))); + } + } + } catch (Exception e) { + // Fail soft - don't crash on malformed expressions + } + } + } + + /** + * Validate return statements in a block lambda (one with { }). + * Checks that all return statements return compatible types with the expected SAM return type. + */ + private void validateBlockLambdaReturns(InnerCallableScope lambda, int blockStart, int blockEnd, + TypeInfo expectedReturnType, List marks) { + // Find all return statements in the block + String blockText = text.substring(blockStart, blockEnd); + Pattern returnPattern = Pattern.compile("\\breturn\\b(\\s*;|\\s+[^;]+;)"); + Matcher m = returnPattern.matcher(blockText); + + while (m.find()) { + int returnStart = blockStart + m.start(); + int returnEnd = blockStart + m.end(); + + if (isExcluded(returnStart)) { + continue; + } + + // Get the return expression (if any) + String returnStmt = m.group(1).trim(); + + if (returnStmt.equals(";")) { + // return; - void return + if (expectedReturnType != null && !expectedReturnType.getSimpleName().equals("void")) { + String error = "Cannot return void from lambda expecting " + expectedReturnType.getSimpleName(); + marks.add(new ScriptLine.Mark(returnStart, returnEnd, TokenType.UNDEFINED_VAR, TokenErrorMessage.from(error))); + } + } else { + // return ; - typed return + String expr = returnStmt.substring(0, returnStmt.length() - 1).trim(); + + if (expectedReturnType != null && expectedReturnType.getSimpleName().equals("void")) { + String error = "Cannot return a value from void lambda"; + marks.add(new ScriptLine.Mark(returnStart, returnEnd, TokenType.UNDEFINED_VAR, TokenErrorMessage.from(error))); + } else { + try { + // Validate return expression type + int exprStart = returnStart + m.group(0).indexOf(expr); + TypeInfo returnType = resolveExpressionType(expr, exprStart); + + if (returnType != null && expectedReturnType != null) { + if (!isCompatibleType(returnType, expectedReturnType)) { + String error = "Incompatible return type: expected " + + expectedReturnType.getSimpleName() + + " but returned " + returnType.getSimpleName(); + int exprEnd = exprStart + expr.length(); + marks.add(new ScriptLine.Mark(exprStart, exprEnd, TokenType.UNDEFINED_VAR, TokenErrorMessage.from(error))); + } + } + } catch (Exception e) { + // Fail soft on malformed expressions + } + } + } + } + + // Check for missing returns in non-void lambdas + // This is a warning rather than an error - Java allows it if the lambda throws or has infinite loop + // We skip this validation to avoid false positives + } + + /** + * Check if an actual type is compatible with an expected type. + * Includes boxing, subtype checking, and special cases like void. + */ + private boolean isCompatibleType(TypeInfo actual, TypeInfo expected) { + if (actual == null || expected == null) return false; + if (actual.equals(expected)) return true; + if (actual.getSimpleName().equals(expected.getSimpleName())) return true; + + // void is compatible with anything (statement lambda) + if (expected.getSimpleName().equals("void")) return true; + + // Object is compatible with anything + if (expected.getSimpleName().equals("Object") || expected.getFullName().equals("java.lang.Object")) return true; + + // Check primitive boxing + if (isBoxingCompatible(actual.getSimpleName(), expected.getSimpleName())) return true; + + // Use TypeChecker for more complex compatibility + return TypeChecker.isTypeCompatible(expected, actual); + } + + /** + * Check if types are compatible via boxing/unboxing. + */ + private boolean isBoxingCompatible(String actualName, String expectedName) { + if (actualName.equals("int") && expectedName.equals("Integer")) return true; + if (actualName.equals("Integer") && expectedName.equals("int")) return true; + if (actualName.equals("boolean") && expectedName.equals("Boolean")) return true; + if (actualName.equals("Boolean") && expectedName.equals("boolean")) return true; + if (actualName.equals("long") && expectedName.equals("Long")) return true; + if (actualName.equals("Long") && expectedName.equals("long")) return true; + if (actualName.equals("double") && expectedName.equals("Double")) return true; + if (actualName.equals("Double") && expectedName.equals("double")) return true; + if (actualName.equals("float") && expectedName.equals("Float")) return true; + if (actualName.equals("Float") && expectedName.equals("float")) return true; + if (actualName.equals("byte") && expectedName.equals("Byte")) return true; + if (actualName.equals("Byte") && expectedName.equals("byte")) return true; + if (actualName.equals("short") && expectedName.equals("Short")) return true; + if (actualName.equals("Short") && expectedName.equals("short")) return true; + if (actualName.equals("char") && expectedName.equals("Character")) return true; + if (actualName.equals("Character") && expectedName.equals("char")) return true; + return false; + } + /** * Find and mark unused imports as UNUSED_IMPORT type. * This must be called after all other mark building is complete. @@ -4403,7 +4764,8 @@ public class ScriptDocument { ExpressionNode.TypeResolverContext context = createExpressionResolverContext(position); // Use the expression resolver to parse and resolve the type - ExpressionTypeResolver resolver = new ExpressionTypeResolver(context); + // Pass the position as basePosition so lambda position calculations work correctly + ExpressionTypeResolver resolver = new ExpressionTypeResolver(context, this, position); TypeInfo result = resolver.resolve(expr); // Special case: null literal type is "unresolved" but valid @@ -4744,10 +5106,17 @@ public class ScriptDocument { if (pos < 0) return null; while (pos >= 0) { - // Check if we're in a comment range (but not string range) - // Skip comments to avoid picking up types from comment text - if (isInCommentRange(pos)) { - return null; + // Check if we're in an excluded range (comment, string, etc.) + // Skip excluded regions to avoid picking up types from comment text + if (isExcluded(pos)) { + // Find the excluded range and skip to before it + for (int[] range : excludedRanges) { + if (pos >= range[0] && pos < range[1]) { + pos = range[0] - 1; // Jump to before the excluded range + break; + } + } + continue; // Continue scanning from before the excluded range } char c = text.charAt(pos); @@ -4769,8 +5138,11 @@ public class ScriptDocument { if (Character.isJavaIdentifierPart(c)) { while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) pos--; + // Skip whitespace to check for chained identifier (preceded by a dot) + int checkPos = pos; + while (checkPos >= 0 && Character.isWhitespace(text.charAt(checkPos))) checkPos--; // If it's part of a chained identifier (preceded by a dot), continue - if (pos >= 0 && text.charAt(pos) == '.') { pos--; continue; } + if (checkPos >= 0 && text.charAt(checkPos) == '.') { pos = checkPos - 1; continue; } break; } @@ -5045,13 +5417,32 @@ public class ScriptDocument { boolean isUppercase = Character.isUpperCase(name.charAt(0)); // Scope resolution order: - // 1. Method parameters (if inside method) - // 2. Method local variables (if inside method) - // 3. Enclosing type fields (if inside method) - // 4. Global fields - // 5. Script type fields + // 1. Inner callable scope parameters (lambda/function expressions) + // 2. Inner callable scope locals + // 3. Method parameters (if inside method) + // 4. Method local variables (if inside method) + // 5. Enclosing type fields (if inside method) + // 6. Global fields + // 7. Script type fields - // Check parameters first (method scope) + // Check inner callable scope parameters first (lambda/function expressions) + Object innermostScope = findInnermostScopeAt(m.start(1)); + if (innermostScope instanceof InnerCallableScope) { + InnerCallableScope innerScope = (InnerCallableScope) innermostScope; + FieldInfo innerParam = innerScope.getParameter(name); + if (innerParam != null) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER, innerParam)); + continue; + } + // Also check locals + FieldInfo innerLocal = innerScope.getLocals().get(name); + if (innerLocal != null) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD, innerLocal)); + continue; + } + } + + // Check parameters (method scope) - fallback if not in inner scope if (containingMethod != null && containingMethod.hasParameter(name)) { FieldInfo paramInfo = containingMethod.getParameter(name); marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER, paramInfo)); @@ -6065,19 +6456,49 @@ public class ScriptDocument { /** * Resolve a variable by name at a given position. * Works for BOTH Java and JavaScript using unified data structures. + * Checks innermost scope first (lambda or method), then walks up parent chain. */ public FieldInfo resolveVariable(String name, int position) { - // Find containing method/function - MethodInfo containingMethod = findMethodAtPosition(position); + // 1. Check innermost scope (lambda or method) + Object innermostScope = findInnermostScopeAt(position); - if (containingMethod != null) { - // Check parameters - if (containingMethod.hasParameter(name)) { - return containingMethod.getParameter(name); + if (innermostScope instanceof InnerCallableScope) { + InnerCallableScope scope = (InnerCallableScope) innermostScope; + + // Check lambda parameters and locals, walking up parent chain + InnerCallableScope currentScope = scope; + while (currentScope != null) { + // Check parameters + FieldInfo param = currentScope.getParameter(name); + if (param != null) { + return param; + } + + // Check locals + FieldInfo local = currentScope.getLocals().get(name); + if (local != null) { + return local; + } + + currentScope = currentScope.getParentScope(); } - // Check local variables (methodLocals stores both Java locals and JS var/let/const inside functions) - Map locals = methodLocals.get(containingMethod.getDeclarationOffset()); + // Not found in lambda scopes - fall back to enclosing method + // Find which method this lambda is inside + innermostScope = findMethodAtPosition(position); + } + + // 2. Check method scope (params + locals) + if (innermostScope instanceof MethodInfo) { + MethodInfo method = (MethodInfo) innermostScope; + + // Check method parameters + if (method.hasParameter(name)) { + return method.getParameter(name); + } + + // Check method locals + Map locals = methodLocals.get(method.getDeclarationOffset()); if (locals != null && locals.containsKey(name)) { FieldInfo localInfo = locals.get(name); if (localInfo.isVisibleAt(position)) { @@ -6086,7 +6507,7 @@ public class ScriptDocument { } } - // For Java only: Check if we're inside a script type and look for fields there + // 3. For Java only: Check if we're inside a script type and look for fields there if (!isJavaScript()) { ScriptTypeInfo enclosingType = findEnclosingScriptType(position); if (enclosingType != null && enclosingType.hasField(name)) { @@ -6094,12 +6515,12 @@ public class ScriptDocument { } } - // Check global fields (stores both Java global fields and JS global var/let/const) + // 4. Check global fields (stores both Java global fields and JS global var/let/const) if (globalFields.containsKey(name)) { return globalFields.get(name); } - // Check JS global objects from JSTypeRegistry (like API, DBCAPI) + // 5. Check JS global objects from JSTypeRegistry (like API, DBCAPI) if (isJavaScript()) { JSTypeRegistry registry = JSTypeRegistry.getInstance(); String globalObjectType = registry.getGlobalObjectType(name); @@ -6348,6 +6769,105 @@ public class ScriptDocument { return Collections.unmodifiableMap(globalFields); } + /** + * Get all variables available at a specific position. + * Used by autocomplete to show scope-aware suggestions. + * Returns variables from innermost scope first (parameters, then locals), + * walking up through parent scopes, then method scope, then globals. + * + * @param position The cursor position + * @return List of FieldInfo for all available variables, ordered by priority + */ + public List getAvailableVariablesAt(int position) { + List variables = new ArrayList<>(); + + // Check innermost scope (lambda or method) + Object innermostScope = findInnermostScopeAt(position); + + if (innermostScope instanceof InnerCallableScope) { + InnerCallableScope scope = (InnerCallableScope) innermostScope; + + // Add lambda parameters and locals, walking up parent chain + InnerCallableScope currentScope = scope; + while (currentScope != null) { + // Add parameters from this scope + variables.addAll(currentScope.getParameters()); + + // Add locals from this scope (only those visible at position) + for (FieldInfo local : currentScope.getLocals().values()) { + if (local.isVisibleAt(position)) { + variables.add(local); + } + } + + currentScope = currentScope.getParentScope(); + } + + // Fall back to enclosing method + innermostScope = findMethodAtPosition(position); + } + + // Add method scope variables + if (innermostScope instanceof MethodInfo) { + MethodInfo method = (MethodInfo) innermostScope; + + // Add method parameters + variables.addAll(method.getParameters()); + + // Add method locals (only those visible at position) + Map locals = methodLocals.get(method.getDeclarationOffset()); + if (locals != null) { + for (FieldInfo local : locals.values()) { + if (local.isVisibleAt(position)) { + variables.add(local); + } + } + } + } + + // Add global fields (only those visible at position) + for (FieldInfo globalField : globalFields.values()) { + if (globalField.isVisibleAt(position)) { + variables.add(globalField); + } + } + + // For Java: add fields from enclosing type + if (!isJavaScript()) { + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null) { + for (FieldInfo field : enclosingType.getFields().values()) { + if (field.isVisibleAt(position)) { + variables.add(field); + } + } + } + } + + // For JS: add global engine objects and editor globals + if (isJavaScript()) { + JSTypeRegistry registry = JSTypeRegistry.getInstance(); + + // Add global engine objects (like API, DBCAPI) + for (String globalName : registry.getGlobalEngineObjects().keySet()) { + FieldInfo field = resolveVariable(globalName, position); + if (field != null && field.isResolved()) { + variables.add(field); + } + } + + // Add editor/DataScript global variables + for (String globalName : editorGlobals.keySet()) { + FieldInfo field = resolveVariable(globalName, position); + if (field != null && field.isResolved()) { + variables.add(field); + } + } + } + + return variables; + } + /** * Get all errored assignments across all fields (global, local, and external). * Used by ScriptLine to draw error underlines. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java index 85bcf1f30..f3d588f4d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java @@ -224,6 +224,133 @@ public abstract class ExpressionNode { public TypeInfo resolveType(TypeResolverContext resolver) { return inner.resolveType(resolver); } } + public static class LambdaNode extends ExpressionNode { + private final List parameterNames; + private final ExpressionNode body; + private final boolean isBlock; + private noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scopeRef; + + public LambdaNode(List parameterNames, ExpressionNode body, boolean isBlock, int start, int end) { + super(start, end); + this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>(); + this.body = body; + this.isBlock = isBlock; + this.scopeRef = null; + } + + public List getParameterNames() { return Collections.unmodifiableList(parameterNames); } + public ExpressionNode getBody() { return body; } + public boolean isBlock() { return isBlock; } + public noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope getScopeRef() { return scopeRef; } + public void setScopeRef(noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scope) { + this.scopeRef = scope; + } + + @Override + public TypeInfo resolveType(TypeResolverContext resolver) { + // Lambda type is determined by the expected functional interface type + // See ExpressionTypeResolver.resolveLambdaType() + return TypeInfo.fromClass(Object.class); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (parameterNames.size() == 1 && !isBlock) { + sb.append(parameterNames.get(0)); + } else { + sb.append("("); + sb.append(String.join(", ", parameterNames)); + sb.append(")"); + } + sb.append(" -> "); + if (isBlock) { + sb.append("{ ... }"); + } else { + sb.append(body); + } + return sb.toString(); + } + } + + public static class JSFunctionNode extends ExpressionNode { + private final String name; // Optional name for named function expressions + private final List parameterNames; + private final String bodyText; // Raw body text (for block functions) + private noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scopeRef; + + public JSFunctionNode(String name, List parameterNames, String bodyText, int start, int end) { + super(start, end); + this.name = name; + this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>(); + this.bodyText = bodyText; + this.scopeRef = null; + } + + public String getName() { return name; } + public List getParameterNames() { return Collections.unmodifiableList(parameterNames); } + public String getBodyText() { return bodyText; } + public noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope getScopeRef() { return scopeRef; } + public void setScopeRef(noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scope) { + this.scopeRef = scope; + } + + @Override + public TypeInfo resolveType(TypeResolverContext resolver) { + // JS function type is determined by the expected functional interface type + // See ExpressionTypeResolver.resolveJSFunctionType() + return TypeInfo.fromClass(Object.class); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("function"); + if (name != null && !name.isEmpty()) { + sb.append(" ").append(name); + } + sb.append("("); + sb.append(String.join(", ", parameterNames)); + sb.append(") { "); + if (bodyText.length() > 30) { + sb.append(bodyText.substring(0, 30)).append("..."); + } else { + sb.append(bodyText); + } + sb.append(" }"); + return sb.toString(); + } + } + + public static class MethodReferenceNode extends ExpressionNode { + private final ExpressionNode target; // Object or class name + private final String methodName; + private final boolean isStatic; // Whether it's Class::method vs obj::method + + public MethodReferenceNode(ExpressionNode target, String methodName, boolean isStatic, int start, int end) { + super(start, end); + this.target = target; + this.methodName = methodName; + this.isStatic = isStatic; + } + + public ExpressionNode getTarget() { return target; } + public String getMethodName() { return methodName; } + public boolean isStatic() { return isStatic; } + + @Override + public TypeInfo resolveType(TypeResolverContext resolver) { + // Method reference type is determined by the expected functional interface type + // See ExpressionTypeResolver.resolveMethodReferenceType() + return TypeInfo.fromClass(Object.class); + } + + @Override + public String toString() { + return target + "::" + methodName; + } + } + public interface TypeResolverContext { TypeInfo resolveIdentifier(String name); TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName); 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 5bb2f9fd7..dacd60618 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 @@ -14,7 +14,119 @@ public class ExpressionParser { public ExpressionNode parse() { if (tokens.isEmpty()) return null; - return parseExpression(0); + return parseLambdaOrExpression(); + } + + private ExpressionNode parseLambdaOrExpression() { + // Check if this is a lambda: (params) -> or param -> + // Look ahead to see if there's a -> after the next identifier/parens + + int checkpoint = pos; + List paramNames = null; + int lambdaStart = current().getStart(); + + // Try to parse parameter list + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + // Single param without parens + String paramName = current().getText(); + advance(); + if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) { + paramNames = new ArrayList<>(); + paramNames.add(paramName); + } else { + // Not a lambda, rewind + pos = checkpoint; + } + } else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { + // Parenthesized params: (a, b) or () + advance(); // consume '(' + paramNames = new ArrayList<>(); + + if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + // Parse param list + while (true) { + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) { + // Not a valid lambda param list, rewind + pos = checkpoint; + paramNames = null; + break; + } + paramNames.add(current().getText()); + advance(); + + if (check(ExpressionToken.TokenKind.COMMA)) { + advance(); + } else if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + break; + } else { + // Invalid syntax, rewind + pos = checkpoint; + paramNames = null; + break; + } + } + } + + if (paramNames != null) { + if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + advance(); // consume ')' + if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) { + // Valid lambda! + } else { + // Not a lambda, rewind + pos = checkpoint; + paramNames = null; + } + } else { + // No closing paren, rewind + pos = checkpoint; + paramNames = null; + } + } + } + + if (paramNames != null) { + // This is a lambda! Parse it + advance(); // consume '->' + + // Parse body + ExpressionNode body; + boolean isBlock = false; + int bodyEnd; + + if (check(ExpressionToken.TokenKind.LEFT_BRACE)) { + // Block lambda: { ... } + isBlock = true; + int braceStart = current().getStart(); + advance(); // consume '{' + // Skip to matching '}' + int depth = 1; + while (depth > 0 && pos < tokens.size() && !check(ExpressionToken.TokenKind.EOF)) { + if (check(ExpressionToken.TokenKind.LEFT_BRACE)) depth++; + else if (check(ExpressionToken.TokenKind.RIGHT_BRACE)) depth--; + if (depth > 0) advance(); + } + bodyEnd = current().getEnd(); + if (check(ExpressionToken.TokenKind.RIGHT_BRACE)) { + advance(); // consume '}' + } + body = new ExpressionNode.StringLiteralNode("", braceStart, bodyEnd); // Placeholder + } else { + // Expression lambda + body = parseExpressionInternal(0); + if (body == null) { + // Failed to parse body, return error recovery + return null; + } + bodyEnd = body.getEnd(); + } + + // Create lambda node (scopeRef will be set during type resolution) + return new ExpressionNode.LambdaNode(paramNames, body, isBlock, lambdaStart, bodyEnd); + } + + // Not a lambda, parse as regular expression + return parseExpressionInternal(0); } private ExpressionToken current() { @@ -36,6 +148,15 @@ public class ExpressionParser { } private ExpressionNode parseExpression(int minPrecedence) { + // For top-level expressions (minPrecedence = 0), check for lambda first + if (minPrecedence == 0) { + return parseLambdaOrExpression(); + } + + return parseExpressionInternal(minPrecedence); + } + + private ExpressionNode parseExpressionInternal(int minPrecedence) { ExpressionNode left = parsePrefixExpression(); if (left == null) return null; @@ -108,6 +229,11 @@ public class ExpressionParser { ExpressionToken tok = current(); int start = tok.getStart(); + // Check for function expression (JS only) + if (tok.getKind() == ExpressionToken.TokenKind.FUNCTION) { + return parseFunctionExpression(); + } + if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) { OperatorType op = tok.getOperatorType(); if (op != null && isUnaryOperator(op)) { @@ -126,6 +252,80 @@ public class ExpressionParser { return parsePrimaryExpression(); } + private ExpressionNode parseFunctionExpression() { + int start = current().getStart(); + advance(); // consume 'function' + + // Optional function name + String functionName = null; + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + functionName = current().getText(); + advance(); + } + + // Expect '(' + if (!check(ExpressionToken.TokenKind.LEFT_PAREN)) { + return null; // Invalid syntax + } + advance(); // consume '(' + + // Parse parameters + List params = new ArrayList<>(); + while (!check(ExpressionToken.TokenKind.RIGHT_PAREN) && !check(ExpressionToken.TokenKind.EOF)) { + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) { + return null; // Invalid parameter + } + params.add(current().getText()); + advance(); + + if (check(ExpressionToken.TokenKind.COMMA)) { + advance(); + } else if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + return null; // Expected comma or closing paren + } + } + + if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + return null; // Expected closing paren + } + advance(); // consume ')' + + // Expect '{' + if (!check(ExpressionToken.TokenKind.LEFT_BRACE)) { + return null; // Invalid syntax + } + advance(); // consume '{' + + // Collect body tokens until matching '}' + StringBuilder bodyBuilder = new StringBuilder(); + int depth = 1; + while (depth > 0 && !check(ExpressionToken.TokenKind.EOF)) { + ExpressionToken bodyToken = current(); + if (bodyToken.getKind() == ExpressionToken.TokenKind.LEFT_BRACE) { + depth++; + } else if (bodyToken.getKind() == ExpressionToken.TokenKind.RIGHT_BRACE) { + depth--; + if (depth == 0) { + break; + } + } + bodyBuilder.append(bodyToken.getText()).append(" "); + advance(); + } + + String bodyText = bodyBuilder.toString().trim(); + int end = current().getEnd(); + + if (!check(ExpressionToken.TokenKind.RIGHT_BRACE)) { + return null; // Expected closing brace + } + advance(); // consume final '}' + + // Find the corresponding InnerCallableScope from ScriptDocument + // This will be set during type resolution when we have access to ScriptDocument + return new ExpressionNode.JSFunctionNode(functionName, params, bodyText, start, end); + } + private boolean isUnaryOperator(OperatorType op) { switch (op) { case ADD: case SUBTRACT: case LOGICAL_NOT: case BITWISE_NOT: @@ -250,7 +450,34 @@ public class ExpressionParser { private ExpressionNode parseAccessChain(ExpressionNode base) { while (true) { - if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { + if (check(ExpressionToken.TokenKind.METHOD_REFERENCE)) { + advance(); // consume '::' + + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) { + // Invalid syntax, return what we have + break; + } + + String methodName = current().getText(); + 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); + // Method references don't chain further (can't do obj::method.something) + break; + } else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { advance(); List args = parseArgumentList(); int end = current().getStart(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java index 655adea9a..37e2c4ae1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java @@ -5,10 +5,12 @@ public class ExpressionToken { public enum TokenKind { INT_LITERAL, LONG_LITERAL, FLOAT_LITERAL, DOUBLE_LITERAL, BOOLEAN_LITERAL, CHAR_LITERAL, STRING_LITERAL, NULL_LITERAL, - IDENTIFIER, NEW, INSTANCEOF, + IDENTIFIER, NEW, INSTANCEOF, FUNCTION, OPERATOR, LEFT_PAREN, RIGHT_PAREN, LEFT_BRACKET, RIGHT_BRACKET, + LEFT_BRACE, RIGHT_BRACE, DOT, COMMA, QUESTION, COLON, SEMICOLON, + LAMBDA_ARROW, METHOD_REFERENCE, EOF } @@ -55,6 +57,9 @@ public class ExpressionToken { if ("instanceof".equals(name)) { return new ExpressionToken(TokenKind.INSTANCEOF, name, start, end); } + if ("function".equals(name)) { + return new ExpressionToken(TokenKind.FUNCTION, name, start, end); + } return new ExpressionToken(TokenKind.IDENTIFIER, name, start, end); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java index 25ee10ea5..795344856 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java @@ -98,6 +98,21 @@ public class ExpressionTokenizer { int start = pos; String op = null; + + // Check for method reference :: (before lambda arrow and single colon) + if (pos + 1 < len && c == ':' && expr.charAt(pos + 1) == ':') { + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.METHOD_REFERENCE, "::", pos, pos + 2)); + pos += 2; + continue; + } + + // Check for lambda arrow + if (pos + 1 < len && c == '-' && expr.charAt(pos + 1) == '>') { + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LAMBDA_ARROW, "->", pos, pos + 2)); + pos += 2; + continue; + } + if (pos + 3 <= len) { String s3 = expr.substring(pos, pos + 3); if (">>>".equals(s3) || "<<=".equals(s3) || ">>=".equals(s3)) op = s3; @@ -133,6 +148,8 @@ public class ExpressionTokenizer { case ')': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_PAREN, ")", pos, pos + 1)); break; case '[': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACKET, "[", pos, pos + 1)); break; case ']': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACKET, "]", pos, pos + 1)); break; + case '{': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACE, "{", pos, pos + 1)); break; + case '}': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACE, "}", pos, pos + 1)); break; case '.': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.DOT, ".", pos, pos + 1)); break; case ',': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COMMA, ",", pos, pos + 1)); break; case '?': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.QUESTION, "?", pos, pos + 1)); break; 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 86aaacec9..70c744199 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 @@ -1,10 +1,16 @@ package noppes.npcs.client.gui.util.script.interpreter.expression; +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.TypeInfo; import java.util.List; public class ExpressionTypeResolver { private final ExpressionNode.TypeResolverContext context; + private final ScriptDocument document; // Access to document for inner scope lookups + private final int basePosition; // Document offset for this expression /** * Static field to track the expected/desired type for the current expression being resolved. @@ -16,6 +22,18 @@ public class ExpressionTypeResolver { public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context) { this.context = context; + this.document = null; + this.basePosition = 0; + } + + public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context, ScriptDocument document) { + this(context, document, 0); + } + + public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context, ScriptDocument document, int basePosition) { + this.context = context; + this.document = document; + this.basePosition = basePosition; } public TypeInfo resolve(String expression) { @@ -133,9 +151,272 @@ public class ExpressionTypeResolver { return resolveNodeType(((ExpressionNode.ParenthesizedNode) node).getInner()); } + if (node instanceof ExpressionNode.LambdaNode) { + return resolveLambdaType((ExpressionNode.LambdaNode) node); + } + + if (node instanceof ExpressionNode.JSFunctionNode) { + return resolveJSFunctionType((ExpressionNode.JSFunctionNode) node); + } + + if (node instanceof ExpressionNode.MethodReferenceNode) { + return resolveMethodReferenceType((ExpressionNode.MethodReferenceNode) node); + } + return null; } + private TypeInfo resolveLambdaType(ExpressionNode.LambdaNode lambda) { + // Lambda type is determined by the expected functional interface + TypeInfo expectedType = CURRENT_EXPECTED_TYPE; + + if (expectedType == null) { + // No expected type context + // Return a generic Object type + return TypeInfo.fromClass(Object.class); + } + + // Get SAM method from the expected functional interface + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam != null && document != null) { + // Find the InnerCallableScope for this lambda by matching position and parameters + InnerCallableScope scope = findLambdaScopeByPosition(lambda.getStart(), lambda.getParameterNames()); + + if (scope != null) { + scope.setExpectedType(expectedType); + + // Extract and apply parameter types from SAM to lambda parameters + List samParams = sam.getParameters(); + List lambdaParams = scope.getParameters(); + + if (samParams.size() == lambdaParams.size()) { + for (int i = 0; i < lambdaParams.size(); i++) { + FieldInfo lambdaParam = lambdaParams.get(i); + TypeInfo inferredType = samParams.get(i).getTypeInfo(); + + // Set inferred type on the parameter + if (inferredType != null) { + lambdaParam.setInferredType(inferredType); + } + } + } + } + + // Type check lambda body (if expression lambda) + if (!lambda.isBlock() && lambda.getBody() != null) { + // Resolve body type for validation + TypeInfo bodyType = resolveNodeType(lambda.getBody()); + // The actual compatibility check will be done during marking phase + } + + return expectedType; + } + + // Fallback: check if it's a recognized functional interface by name + String typeName = expectedType.getFullName(); + if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") || + typeName.contains("Predicate") || typeName.contains("Supplier") || + typeName.equals("java.lang.Runnable"))) { + // Update the scope reference if available + if (lambda.getScopeRef() != null) { + lambda.getScopeRef().setExpectedType(expectedType); + } + + return expectedType; + } + + // Not a recognized functional interface, return Object + return TypeInfo.fromClass(Object.class); + } + + /** + * Find the InnerCallableScope for a lambda by matching position and parameter names. + * + * @param expressionRelativePos The start position of the lambda (relative to expression string) + * @param paramNames The parameter names of the lambda + * @return The matching InnerCallableScope, or null if not found + */ + private InnerCallableScope findLambdaScopeByPosition(int expressionRelativePos, List paramNames) { + if (document == null) { + return null; + } + + // Convert expression-relative position to absolute document position + int absolutePos = basePosition + expressionRelativePos; + + for (InnerCallableScope scope : document.getInnerScopes()) { + if (scope.getKind() == InnerCallableScope.Kind.JAVA_LAMBDA) { + // Check if absolute position is in range + if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) { + // Verify parameter names match + if (scope.getParameters().size() == paramNames.size()) { + boolean match = true; + for (int i = 0; i < paramNames.size(); i++) { + if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) { + match = false; + break; + } + } + if (match) { + return scope; + } + } + } + } + } + return null; + } + + private TypeInfo resolveJSFunctionType(ExpressionNode.JSFunctionNode functionNode) { + // JS function type is determined by the expected functional interface (if any) + TypeInfo expectedType = CURRENT_EXPECTED_TYPE; + + if (expectedType == null) { + // No expected type - return generic Function type or Object + // Try to resolve "Function" as a type from the context + TypeInfo functionType = context.resolveTypeName("Function"); + if (functionType != null && functionType.isResolved()) { + return functionType; + } + return TypeInfo.fromClass(Object.class); // Fallback + } + + // Get SAM method from the expected functional interface + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam != null && document != null) { + // Find the InnerCallableScope for this JS function by matching position and parameters + InnerCallableScope scope = findJSFunctionScopeByPosition(functionNode.getStart(), functionNode.getParameterNames()); + + if (scope != null) { + scope.setExpectedType(expectedType); + + // Extract and apply parameter types from SAM to function parameters + List samParams = sam.getParameters(); + List functionParams = scope.getParameters(); + + if (samParams.size() == functionParams.size()) { + for (int i = 0; i < functionParams.size(); i++) { + FieldInfo functionParam = functionParams.get(i); + TypeInfo inferredType = samParams.get(i).getTypeInfo(); + + // Set inferred type on the parameter + if (inferredType != null) { + functionParam.setInferredType(inferredType); + } + } + } + } + + return expectedType; + } + + // Fallback: check if expected type is compatible with a function + String typeName = expectedType.getFullName(); + if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") || + typeName.contains("Predicate") || typeName.contains("Supplier") || + typeName.equals("java.lang.Runnable"))) { + // This is likely a functional interface + + // Update the InnerCallableScope with the expected type + if (functionNode.getScopeRef() != null) { + functionNode.getScopeRef().setExpectedType(expectedType); + } + + return expectedType; + } + + // Expected type is not a recognized functional interface + // Check if it's a generic JS "Function" type expectation + if (typeName != null && typeName.equals("Function")) { + return expectedType; + } + + return TypeInfo.fromClass(Object.class); // Fallback + } + + /** + * Find the InnerCallableScope for a JS function by matching position and parameter names. + * + * @param expressionRelativePos The start position of the function (relative to expression string) + * @param paramNames The parameter names of the function + * @return The matching InnerCallableScope, or null if not found + */ + private InnerCallableScope findJSFunctionScopeByPosition(int expressionRelativePos, List paramNames) { + if (document == null) { + return null; + } + + // Convert expression-relative position to absolute document position + int absolutePos = basePosition + expressionRelativePos; + + for (InnerCallableScope scope : document.getInnerScopes()) { + if (scope.getKind() == InnerCallableScope.Kind.JS_FUNCTION_EXPR) { + // Check if absolute position is in range + if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) { + // Verify parameter names match + if (scope.getParameters().size() == paramNames.size()) { + boolean match = true; + for (int i = 0; i < paramNames.size(); i++) { + if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) { + match = false; + break; + } + } + if (match) { + return scope; + } + } + } + } + } + return null; + } + + 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 + 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 + + // 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. + 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"); + } + public static ExpressionNode.TypeResolverContext createBasicContext() { return new ExpressionNode.TypeResolverContext() { public TypeInfo resolveIdentifier(String name) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 2c021920a..8648d91f9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -509,7 +509,11 @@ public final class FieldInfo { return getEffectiveType(); } public int getDeclarationOffset() { return declarationOffset; } - public boolean isResolved() { return resolved; } + public boolean isResolved() { + // Check if we have any resolved type (declared or inferred) + TypeInfo effectiveType = getEffectiveType(); + return effectiveType != null && effectiveType.isResolved(); + } public MethodInfo getContainingMethod() { return containingMethod; } public String getDocumentation() { return documentation; } public JSDocInfo getJSDocInfo() { return jsDocInfo; }