From 86c327707532ca2875ebbc1ddc702e588cfa5c71 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 8 Mar 2026 12:09:16 +0200 Subject: [PATCH] Added JS arrow function => and shorthand methods handling --- .../autocomplete/JSAutocompleteProvider.java | 2 +- .../interpreter/InnerCallableScope.java | 4 +- .../script/interpreter/ScriptDocument.java | 162 +++++++++++++++++- .../expression/ExpressionNode.java | 42 +++++ .../expression/ExpressionParser.java | 11 +- .../expression/ExpressionToken.java | 2 +- .../expression/ExpressionTokenizer.java | 9 +- .../expression/ExpressionTypeResolver.java | 83 +++++++++ .../script/interpreter/type/TypeChecker.java | 11 +- 9 files changed, 318 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index d9d280720..e7798f529 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -346,6 +346,6 @@ public class JSAutocompleteProvider extends JavaAutocompleteProvider { } public String[] getKeywords() { - return TypeChecker.getJaveScriptKeywords(); + return TypeChecker.getJavaScriptKeywords(); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/InnerCallableScope.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/InnerCallableScope.java index 5e05028d1..20d273c21 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/InnerCallableScope.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/InnerCallableScope.java @@ -15,7 +15,9 @@ public class InnerCallableScope { public enum Kind { JAVA_LAMBDA, - JS_FUNCTION_EXPR + JS_FUNCTION_EXPR, + JS_ARROW_FUNC, + JS_SHORTHAND_METHOD } private final Kind kind; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java index b489f8107..995a6641e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -2282,6 +2282,8 @@ public class ScriptDocument { if (isJavaScript()) { parseJSFunctionExpressions(); + parseJSArrowFunctions(); + parseJSShorthandMethods(); } else { parseJavaLambdas(); } @@ -2542,6 +2544,160 @@ public class ScriptDocument { } } + private void parseJSArrowFunctions() { + String arrowToken = "=>"; + int pos = 0; + while ((pos = text.indexOf(arrowToken, pos)) >= 0) { + if (isExcluded(pos)) { + pos += 2; + continue; + } + + int arrowPos = pos; + int headerStart = findArrowFunctionHeaderStart(arrowPos); + if (headerStart < 0) { + pos += 2; + continue; + } + + int bodyStart = arrowPos + 2; + while (bodyStart < text.length() && Character.isWhitespace(text.charAt(bodyStart))) { + bodyStart++; + } + + if (bodyStart >= text.length()) { + pos += 2; + continue; + } + + int bodyEnd; + if (text.charAt(bodyStart) == '{') { + bodyEnd = findMatchingBrace(bodyStart); + if (bodyEnd < 0) bodyEnd = text.length(); + else bodyEnd++; + } else { + bodyEnd = findLambdaExpressionEnd(bodyStart); + } + + InnerCallableScope scope = new InnerCallableScope( + InnerCallableScope.Kind.JS_ARROW_FUNC, + headerStart, + arrowPos + 2, + bodyStart, + bodyEnd + ); + + parseArrowFunctionParameters(scope, headerStart, arrowPos); + innerScopes.add(scope); + pos = bodyEnd; + } + } + + private int findArrowFunctionHeaderStart(int arrowPos) { + int pos = arrowPos - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } + if (pos < 0) return -1; + + if (text.charAt(pos) == ')') { + int depth = 1; + pos--; + while (pos >= 0 && depth > 0) { + if (isExcluded(pos)) { pos--; continue; } + char c = text.charAt(pos); + if (c == ')') depth++; + else if (c == '(') depth--; + pos--; + } + return pos + 1; + } else if (Character.isJavaIdentifierPart(text.charAt(pos))) { + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) { + pos--; + } + return pos + 1; + } + return -1; + } + + private void parseArrowFunctionParameters(InnerCallableScope scope, int headerStart, int arrowPos) { + String headerText = text.substring(headerStart, arrowPos).trim(); + + if (headerText.startsWith("(") && headerText.endsWith(")")) { + String paramsText = headerText.substring(1, headerText.length() - 1).trim(); + if (!paramsText.isEmpty()) { + String[] params = paramsText.split(","); + int offset = headerStart + 1; + for (String param : params) { + param = param.trim(); + if (param.isEmpty()) continue; + + int nameOffset = text.indexOf(param, offset); + if (nameOffset < 0) nameOffset = offset; + + FieldInfo paramInfo = FieldInfo.parameter(param, TypeInfo.ANY, nameOffset, null); + scope.addParameter(paramInfo); + offset = nameOffset + param.length(); + } + } + } else { + if (ObjectLiteralParser.isSimpleIdentifier(headerText)) { + int nameOffset = text.indexOf(headerText, headerStart); + if (nameOffset < 0) nameOffset = headerStart; + FieldInfo paramInfo = FieldInfo.parameter(headerText, TypeInfo.ANY, nameOffset, null); + scope.addParameter(paramInfo); + } + } + } + + private void parseJSShorthandMethods() { + Pattern shorthandPattern = Pattern.compile("(\\w+)\\s*\\(([^)]*)\\)\\s*\\{"); + Matcher m = shorthandPattern.matcher(text); + + while (m.find()) { + int start = m.start(); + if (isExcluded(start)) continue; + + if (!isInsideObjectLiteral(start)) continue; + + String methodName = m.group(1); + if (TypeChecker.isJavaScriptKeyword(methodName)) continue; + + int headerStart = m.start(1); + int bodyStart = m.end() - 1; + int bodyEnd = findMatchingBrace(bodyStart); + if (bodyEnd < 0) bodyEnd = text.length(); + else bodyEnd++; + + InnerCallableScope scope = new InnerCallableScope( + InnerCallableScope.Kind.JS_SHORTHAND_METHOD, + headerStart, + bodyStart, + bodyStart, + bodyEnd + ); + + String paramsText = m.group(2).trim(); + if (!paramsText.isEmpty()) { + String[] params = paramsText.split(","); + int offset = m.start(2); + for (String param : params) { + param = param.trim(); + if (param.isEmpty()) continue; + + int nameOffset = text.indexOf(param, offset); + if (nameOffset < 0) nameOffset = offset; + + FieldInfo paramInfo = FieldInfo.parameter(param, TypeInfo.ANY, nameOffset, null); + scope.addParameter(paramInfo); + offset = nameOffset + param.length(); + } + } + + innerScopes.add(scope); + } + } + private boolean isFunctionDeclaration(int funcStart) { // A function declaration is a statement, check context: // - If preceded by = or ( or , or : or [ it's an expression @@ -3572,7 +3728,9 @@ public class ScriptDocument { private void validateLambdaReturnTypes(List marks) { for (InnerCallableScope scope : innerScopes) { if (scope.getKind() == InnerCallableScope.Kind.JAVA_LAMBDA || - scope.getKind() == InnerCallableScope.Kind.JS_FUNCTION_EXPR) { + scope.getKind() == InnerCallableScope.Kind.JS_FUNCTION_EXPR || + scope.getKind() == InnerCallableScope.Kind.JS_ARROW_FUNC || + scope.getKind() == InnerCallableScope.Kind.JS_SHORTHAND_METHOD) { validateLambdaReturnType(scope, marks); } } @@ -5160,7 +5318,7 @@ public class ScriptDocument { // Check if expression contains operators - if so, use the full expression resolver // Handle cast expressions: (Type)expr, ((Type)expr).method(), etc // Also route JS function expressions and arrow lambdas through the parser - if (containsOperators(expr) || expr.contains("::") || expr.contains("->") || expr.startsWith("(") || looksLikeFunctionOrLambda(expr)) { + if (containsOperators(expr) || expr.contains("::") || expr.contains("->") || expr.contains("=>") || expr.startsWith("(") || looksLikeFunctionOrLambda(expr)) { return resolveExpressionWithParserAPI(expr, position); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java index 2d03773a8..93e85156f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter.expression; +import noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.util.ArrayList; import java.util.Collections; @@ -322,6 +323,47 @@ public abstract class ExpressionNode { } } + public static class JSArrowNode extends ExpressionNode { + private final List parameterNames; + private final ExpressionNode body; + private final boolean isBlock; + private InnerCallableScope scopeRef; + + public JSArrowNode(List parameterNames, ExpressionNode body, boolean isBlock, int start, int end) { + super(start, end); + this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>(); + this.body = body; + this.isBlock = isBlock; + } + + public List getParameterNames() { return Collections.unmodifiableList(parameterNames); } + public ExpressionNode getBody() { return body; } + public boolean isBlock() { return isBlock; } + public InnerCallableScope getScopeRef() { return scopeRef; } + public void setScopeRef(InnerCallableScope scope) { + this.scopeRef = scope; + } + + @Override + public TypeInfo resolveType(TypeResolverContext resolver) { + return TypeInfo.fromClass(Object.class); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("("); + sb.append(String.join(", ", parameterNames)); + sb.append(") => "); + if (isBlock) { + sb.append("{ ... }"); + } else if (body != null) { + sb.append(body); + } + return sb.toString(); + } + } + public static class MethodReferenceNode extends ExpressionNode { private final ExpressionNode target; // Object or class name private final String methodName; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java index eaf97f3a8..204fe80c2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java @@ -24,6 +24,7 @@ public class ExpressionParser { int checkpoint = pos; List paramNames = null; int lambdaStart = current().getStart(); + boolean isJSArrow = false; // Try to parse parameter list if (check(ExpressionToken.TokenKind.IDENTIFIER)) { @@ -33,6 +34,10 @@ public class ExpressionParser { if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) { paramNames = new ArrayList<>(); paramNames.add(paramName); + } else if (check(ExpressionToken.TokenKind.JS_ARROW)) { + paramNames = new ArrayList<>(); + paramNames.add(paramName); + isJSArrow = true; } else { // Not a lambda, rewind pos = checkpoint; @@ -72,6 +77,8 @@ public class ExpressionParser { advance(); // consume ')' if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) { // Valid lambda! + } else if (check(ExpressionToken.TokenKind.JS_ARROW)) { + isJSArrow = true; } else { // Not a lambda, rewind pos = checkpoint; @@ -87,7 +94,7 @@ public class ExpressionParser { if (paramNames != null) { // This is a lambda! Parse it - advance(); // consume '->' + advance(); // consume '->' or '=>' // Parse body ExpressionNode body; @@ -122,6 +129,8 @@ public class ExpressionParser { } // Create lambda node (scopeRef will be set during type resolution) + if (isJSArrow) + return new ExpressionNode.JSArrowNode(paramNames, body, isBlock, lambdaStart, bodyEnd); return new ExpressionNode.LambdaNode(paramNames, body, isBlock, lambdaStart, bodyEnd); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java index 37e2c4ae1..2d482ed2a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java @@ -10,7 +10,7 @@ public class ExpressionToken { LEFT_PAREN, RIGHT_PAREN, LEFT_BRACKET, RIGHT_BRACKET, LEFT_BRACE, RIGHT_BRACE, DOT, COMMA, QUESTION, COLON, SEMICOLON, - LAMBDA_ARROW, METHOD_REFERENCE, + LAMBDA_ARROW, JS_ARROW, METHOD_REFERENCE, EOF } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java index 795344856..5c0aa8d62 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java @@ -106,13 +106,20 @@ public class ExpressionTokenizer { continue; } - // Check for lambda arrow + // Check for lambda arrow (Java ->) if (pos + 1 < len && c == '-' && expr.charAt(pos + 1) == '>') { tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LAMBDA_ARROW, "->", pos, pos + 2)); pos += 2; continue; } + // Check for JS arrow (=>) + if (pos + 1 < len && c == '=' && expr.charAt(pos + 1) == '>') { + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.JS_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; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java index 35db70ea7..f773d6e84 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java @@ -182,6 +182,10 @@ public class ExpressionTypeResolver { return resolveJSFunctionType((ExpressionNode.JSFunctionNode) node); } + if (node instanceof ExpressionNode.JSArrowNode) { + return resolveJSArrowType((ExpressionNode.JSArrowNode) node); + } + if (node instanceof ExpressionNode.MethodReferenceNode) { return resolveMethodReferenceType((ExpressionNode.MethodReferenceNode) node); } @@ -395,6 +399,85 @@ public class ExpressionTypeResolver { return null; } + private TypeInfo resolveJSArrowType(ExpressionNode.JSArrowNode arrowNode) { + TypeInfo expectedType = CURRENT_EXPECTED_TYPE; + + if (expectedType == null) { + TypeInfo functionType = context.resolveTypeName("Function"); + if (functionType != null && functionType.isResolved()) { + return functionType; + } + return TypeInfo.fromClass(Object.class); + } + + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam != null && document != null) { + InnerCallableScope scope = findJSArrowScopeByPosition(arrowNode.getStart(), arrowNode.getParameterNames()); + + if (scope != null) { + scope.setExpectedType(expectedType); + + List samParams = sam.getParameters(); + List arrowParams = scope.getParameters(); + + if (samParams.size() == arrowParams.size()) { + for (int i = 0; i < arrowParams.size(); i++) { + TypeInfo inferredType = samParams.get(i).getTypeInfo(); + if (inferredType != null) { + arrowParams.get(i).setInferredType(inferredType); + } + } + } + } + + return expectedType; + } + + String typeName = expectedType.getFullName(); + if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") || + typeName.contains("Predicate") || typeName.contains("Supplier") || + typeName.equals("java.lang.Runnable"))) { + if (arrowNode.getScopeRef() != null) { + arrowNode.getScopeRef().setExpectedType(expectedType); + } + return expectedType; + } + + if (typeName != null && typeName.equals("Function")) { + return expectedType; + } + + return TypeInfo.fromClass(Object.class); + } + + private InnerCallableScope findJSArrowScopeByPosition(int expressionRelativePos, List paramNames) { + if (document == null) { + return null; + } + + int absolutePos = basePosition + expressionRelativePos; + + for (InnerCallableScope scope : document.getInnerScopes()) { + if (scope.getKind() == InnerCallableScope.Kind.JS_ARROW_FUNC) { + if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) { + 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; + } + // ==================== Method Reference Resolution ==================== /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index ea3966382..d98ba7460 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -534,7 +534,7 @@ public final class TypeChecker { return keywords; } - public static String[] getJaveScriptKeywords() { + public static String[] getJavaScriptKeywords() { String[] keywords = { "function", "var", "let", "const", "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "return", "try", "catch", "finally", @@ -546,4 +546,13 @@ public final class TypeChecker { return keywords; } + + public static boolean isJavaScriptKeyword(String keyword) { + for (String k : getJavaScriptKeywords()) { + if (k.equals(keyword)) { + return true; + } + } + return false; + } }