Added JS arrow function => and shorthand methods handling

This commit is contained in:
bigguy345 committed 2026-03-08 12:09:16 +02:00
1 parent 959717fafd
commit 86c3277075
9 files changed
+318 -8

No files matched your search

@@ -346,6 +346,6 @@ public class JSAutocompleteProvider extends JavaAutocompleteProvider {
}
public String[] getKeywords() {
return TypeChecker.getJaveScriptKeywords();
return TypeChecker.getJavaScriptKeywords();
}
}
@@ -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;
@@ -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<ScriptLine.Mark> 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);
}
@@ -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<String> parameterNames;
private final ExpressionNode body;
private final boolean isBlock;
private InnerCallableScope scopeRef;
public JSArrowNode(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;
}
public List<String> 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;
@@ -24,6 +24,7 @@ public class ExpressionParser {
int checkpoint = pos;
List<String> 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);
}
@@ -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
}
@@ -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;
@@ -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<FieldInfo> samParams = sam.getParameters();
List<FieldInfo> 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<String> 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 ====================
/**
@@ -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;
}
}