diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/LoopVariableParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/LoopVariableParser.java new file mode 100644 index 000000000..bee8ee70f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/LoopVariableParser.java @@ -0,0 +1,157 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.gui.util.script.ScopeInfo; +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.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class LoopVariableParser { + + private static final Pattern ENHANCED_FOR = Pattern.compile( + "\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*:\\s*([^)]+)\\)"); + private static final Pattern CLASSIC_FOR = Pattern.compile( + "\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*="); + private static final Pattern FOR_IN = Pattern.compile( + "\\bfor\\s*\\(\\s*(?:var|let|const)\\s+(\\w+)\\s+in\\s+([^)]+)\\)"); + private static final Pattern FOR_OF = Pattern.compile( + "\\bfor\\s*\\(\\s*(?:var|let|const)\\s+(\\w+)\\s+of\\s+([^)]+)\\)"); + private static final Pattern FOR_IN_OF_LOOKAHEAD = Pattern.compile("^\\s+(?:in|of)\\s"); + + private static final Set SKIP_TYPES = new HashSet<>(Arrays.asList( + "return", "if", "while", "for", "switch", "catch", "new", "throw")); + + private final ScriptDocument doc; + private final String text; + + LoopVariableParser(ScriptDocument doc, String text) { + this.doc = doc; + this.text = text; + } + + boolean isForInOrOf(String textAfterVarName) { + return FOR_IN_OF_LOOKAHEAD.matcher(textAfterVarName).find(); + } + + boolean isInsideForHeader(int bodyStart, int absPos) { + int enclosingParen = doc.findEnclosingParenStart(bodyStart, absPos); + return enclosingParen >= 0 && "for".equals(doc.readKeywordBefore(enclosingParen)); + } + + void parse(Map>> methodLocals) { + boolean js = doc.isJavaScript(); + + for (MethodInfo method : doc.getAllMethods()) { + int bodyStart = method.getBodyStart(); + int bodyEnd = method.getBodyEnd(); + if (bodyStart < 0 || bodyEnd <= bodyStart) continue; + String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length())); + Map> locals = methodLocals.computeIfAbsent(method.getDeclarationOffset(), k -> new HashMap<>()); + scanRange(bodyText, bodyStart, bodyEnd, method, locals, js); + } + + for (InnerCallableScope scope : doc.getInnerScopes()) { + int bodyStart = scope.getBodyStart(); + int bodyEnd = scope.getBodyEnd(); + if (bodyStart < 0 || bodyEnd <= bodyStart) continue; + String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length())); + MethodInfo ownerMethod = doc.findMethodAtPosition(bodyStart); + if (ownerMethod == null) continue; + Map> locals = methodLocals.computeIfAbsent(ownerMethod.getDeclarationOffset(), k -> new HashMap<>()); + scanRange(bodyText, bodyStart, bodyEnd, ownerMethod, locals, js); + } + } + + private void scanRange(String bodyText, int bodyStart, int bodyEnd, + MethodInfo method, Map> locals, boolean js) { + if (!js) { + Matcher m = ENHANCED_FOR.matcher(bodyText); + while (m.find()) { + int absForPos = bodyStart + m.start(); + if (doc.isExcluded(absForPos)) continue; + String typeName = m.group(1).trim(); + String varName = m.group(2); + int absVarPos = bodyStart + m.start(2); + TypeInfo typeInfo = doc.resolveType(typeName); + ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd); + FieldInfo fi = FieldInfo.localField(varName, typeInfo, absVarPos, method, -1, -1, 0); + fi.setScopeInfo(scope); + locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi); + } + + m = CLASSIC_FOR.matcher(bodyText); + while (m.find()) { + int absForPos = bodyStart + m.start(); + if (doc.isExcluded(absForPos)) continue; + String typeName = m.group(1).trim(); + String varName = m.group(2); + if (SKIP_TYPES.contains(typeName)) continue; + int absVarPos = bodyStart + m.start(2); + TypeInfo typeInfo = doc.resolveType(typeName); + ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd); + FieldInfo fi = FieldInfo.localField(varName, typeInfo, absVarPos, method, -1, -1, 0); + fi.setScopeInfo(scope); + locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi); + } + } else { + Matcher m = FOR_IN.matcher(bodyText); + while (m.find()) { + int absForPos = bodyStart + m.start(); + if (doc.isExcluded(absForPos)) continue; + String varName = m.group(1); + int absVarPos = bodyStart + m.start(1); + ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd); + FieldInfo fi = FieldInfo.localField(varName, TypeInfo.fromPrimitive("int"), absVarPos, method, -1, -1, 0); + fi.setScopeInfo(scope); + locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi); + } + + m = FOR_OF.matcher(bodyText); + while (m.find()) { + int absForPos = bodyStart + m.start(); + if (doc.isExcluded(absForPos)) continue; + String varName = m.group(1); + String iterableExpr = m.group(2).trim(); + int absVarPos = bodyStart + m.start(1); + TypeInfo iterableType = doc.resolveExpressionType(iterableExpr, absForPos); + TypeInfo elementType = (iterableType != null) ? iterableType.getElementType() : null; + if (elementType == null) elementType = TypeInfo.ANY; + ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd); + FieldInfo fi = FieldInfo.localField(varName, elementType, absVarPos, method, -1, -1, 0); + fi.setScopeInfo(scope); + locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi); + } + } + } + + private ScopeInfo computeForBodyScope(int forKeywordPos, int bodyStart, int bodyEnd) { + int max = Math.min(bodyEnd, text.length()); + int openParen = -1; + for (int i = forKeywordPos; i < max; i++) { + if (doc.isExcluded(i)) continue; + char c = text.charAt(i); + if (c == '(') { openParen = i; break; } + if (!Character.isWhitespace(c) && c != 'f' && c != 'o' && c != 'r') break; + } + if (openParen < 0) return new ScopeInfo(forKeywordPos, bodyEnd, false, "block"); + + int closeParen = doc.findMatchingParen(openParen, max); + if (closeParen < 0) return new ScopeInfo(forKeywordPos, bodyEnd, false, "block"); + + int after = doc.skipWhitespaceAndExcluded(closeParen + 1, max); + if (after < 0 || after >= max) return new ScopeInfo(forKeywordPos, bodyEnd, false, "block"); + + if (text.charAt(after) == '{') { + int closeBrace = doc.findMatchingBrace(after); + if (closeBrace > 0) return new ScopeInfo(forKeywordPos, closeBrace + 1, false, "block"); + return new ScopeInfo(forKeywordPos, bodyEnd, false, "block"); + } + + int stmtEnd = doc.findStatementEnd(after, max); + if (stmtEnd > after) return new ScopeInfo(forKeywordPos, stmtEnd, false, "block"); + return new ScopeInfo(forKeywordPos, bodyEnd, false, "block"); + } +} 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 96ba06dd2..8010e55ca 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 @@ -1312,7 +1312,7 @@ public class ScriptDocument { return null; } - private int skipWhitespaceAndExcluded(int pos, int limit) { + int skipWhitespaceAndExcluded(int pos, int limit) { int i = Math.max(pos, 0); int max = Math.min(limit, text.length()); while (i < max) { @@ -1328,7 +1328,7 @@ public class ScriptDocument { return -1; } - private int findStatementEnd(int start, int limit) { + int findStatementEnd(int start, int limit) { int max = Math.min(limit, text.length()); int parenDepth = 0; int bracketDepth = 0; @@ -1351,7 +1351,7 @@ public class ScriptDocument { return -1; } - private int findEnclosingParenStart(int min, int position) { + int findEnclosingParenStart(int min, int position) { int depth = 0; for (int i = Math.min(position - 1, text.length() - 1); i >= min; i--) { if (isExcluded(i)) { @@ -1370,7 +1370,7 @@ public class ScriptDocument { return -1; } - private int findMatchingParen(int openParenIndex, int limit) { + int findMatchingParen(int openParenIndex, int limit) { if (openParenIndex < 0 || openParenIndex >= text.length()) { return -1; } @@ -1392,7 +1392,7 @@ public class ScriptDocument { return -1; } - private String readKeywordBefore(int position) { + String readKeywordBefore(int position) { int i = position - 1; while (i >= 0) { if (isExcluded(i)) { @@ -1464,6 +1464,12 @@ public class ScriptDocument { int absPos = bodyStart + m.start(2); if (isExcluded(absPos)) continue; + int afterVar = m.end(2); + if (afterVar < bodyText.length()) { + String rest = bodyText.substring(afterVar); + if (rest.length() > 0 && java.util.regex.Pattern.compile("^\\s+(?:in|of)\\s").matcher(rest).find()) continue; + } + boolean insideInner = false; for (InnerCallableScope scope : innerScopes) { if (scope.containsPosition(absPos)) { @@ -1583,6 +1589,9 @@ public class ScriptDocument { continue; } + int enclosingParen = findEnclosingParenStart(bodyStart, absPos); + if (enclosingParen >= 0 && "for".equals(readKeywordBefore(enclosingParen))) continue; + int modifiers = parseModifiers(typeName); TypeInfo typeInfo; @@ -1655,11 +1664,10 @@ public class ScriptDocument { for (InnerCallableScope scope : innerScopes) { parseLocalVariablesInScope(scope); } + + new LoopVariableParser(this, text).parse(methodLocals); } - - /** - * 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(); @@ -1749,6 +1757,12 @@ public class ScriptDocument { int declPos = start + m.start(); if (declPos < start || declPos >= end) continue; if (isExcluded(declPos)) continue; + + int afterVar = m.end(1); + if (afterVar < rangeText.length()) { + String rest = rangeText.substring(afterVar); + if (rest.length() > 0 && java.util.regex.Pattern.compile("^\\s+(?:in|of)\\s").matcher(rest).find()) continue; + } String varName = m.group(1); String initializer = null; @@ -7696,7 +7710,7 @@ public class ScriptDocument { return pos; } - private MethodInfo findMethodAtPosition(int position) { + MethodInfo findMethodAtPosition(int position) { for (MethodInfo method : getAllMethods()) { if (method.containsPosition(position)) { return method; @@ -7728,7 +7742,7 @@ public class ScriptDocument { return findMethodAtPosition(position); } - private int findMatchingBrace(int openBraceIndex) { + int findMatchingBrace(int openBraceIndex) { if (openBraceIndex < 0 || openBraceIndex >= text.length()) return -1;