mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 05:27:37 +00:00
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:
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.
|
||||
|
||||
+127
@@ -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);
|
||||
|
||||
+229
-2
@@ -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();
|
||||
|
||||
+6
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
|
||||
+281
@@ -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; }
|
||||
|
||||
Reference in new issue
Block a user