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.
This commit is contained in:
bigguy345 committed 2026-02-03 07:03:33 +02:00
1 parent 4e78ffb024
commit 074c780d50
7 files changed
+1208 -27

No files matched your search

@@ -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<int[]> 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<ScriptLine.Mark> 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<ScriptLine.Mark> 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<ScriptLine.Mark> 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<ScriptLine.Mark> 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<ScriptLine.Mark> 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 <expr>; - 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<String, FieldInfo> 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<String, FieldInfo> 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<FieldInfo> getAvailableVariablesAt(int position) {
List<FieldInfo> 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<String, FieldInfo> 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.
@@ -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<String> parameterNames;
private final ExpressionNode body;
private final boolean isBlock;
private noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scopeRef;
public LambdaNode(List<String> 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<String> 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<String> 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<String> 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<String> 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);
@@ -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<String> 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("<block>", 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<String> 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<ExpressionNode> args = parseArgumentList();
int end = current().getStart();
@@ -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);
}
}
@@ -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;
@@ -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<FieldInfo> samParams = sam.getParameters();
List<FieldInfo> 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<String> 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<FieldInfo> samParams = sam.getParameters();
List<FieldInfo> 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<String> 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) {
@@ -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; }