From c00ad8e0a7a0a758f579a0d1492f8a2640df4e20 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 7 Mar 2026 15:48:10 +0200 Subject: [PATCH] Fixed loop vars like for(var x in list) declared in top level outside of methods being declared duplicate --- .../interpreter/LoopVariableParser.java | 69 +++++++++++++++++- .../script/interpreter/ScriptDocument.java | 71 ++++++++++++++++++- 2 files changed, 136 insertions(+), 4 deletions(-) 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 index bee8ee70f..b71a8438a 100644 --- 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 @@ -19,7 +19,7 @@ class LoopVariableParser { "\\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"); + 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")); @@ -63,6 +63,73 @@ class LoopVariableParser { Map> locals = methodLocals.computeIfAbsent(ownerMethod.getDeclarationOffset(), k -> new HashMap<>()); scanRange(bodyText, bodyStart, bodyEnd, ownerMethod, locals, js); } + + scanTopLevelLoops(); + } + + private void scanTopLevelLoops() { + boolean js = doc.isJavaScript(); + + if (!js) { + for (Matcher m = ENHANCED_FOR.matcher(text); m.find(); ) { + int absForPos = m.start(); + if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue; + String typeName = m.group(1).trim(); + String varName = m.group(2); + TypeInfo typeInfo = doc.resolveType(typeName); + ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length()); + FieldInfo fi = FieldInfo.localField(varName, typeInfo, m.start(2), null, -1, -1, 0); + fi.setScopeInfo(scope); + doc.addTopLevelLocal(fi); + } + + for (Matcher m = CLASSIC_FOR.matcher(text); m.find(); ) { + int absForPos = m.start(); + if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue; + String typeName = m.group(1).trim(); + String varName = m.group(2); + if (SKIP_TYPES.contains(typeName)) continue; + TypeInfo typeInfo = doc.resolveType(typeName); + ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length()); + FieldInfo fi = FieldInfo.localField(varName, typeInfo, m.start(2), null, -1, -1, 0); + fi.setScopeInfo(scope); + doc.addTopLevelLocal(fi); + } + } else { + for (Matcher m = FOR_IN.matcher(text); m.find(); ) { + int absForPos = m.start(); + if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue; + String varName = m.group(1); + ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length()); + FieldInfo fi = FieldInfo.localField(varName, TypeInfo.fromPrimitive("int"), m.start(1), null, -1, -1, 0); + fi.setScopeInfo(scope); + doc.addTopLevelLocal(fi); + } + + for (Matcher m = FOR_OF.matcher(text); m.find(); ) { + int absForPos = m.start(); + if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue; + String varName = m.group(1); + String iterableExpr = m.group(2).trim(); + TypeInfo iterableType = doc.resolveExpressionType(iterableExpr, absForPos); + TypeInfo elementType = (iterableType != null) ? iterableType.getElementType() : null; + if (elementType == null) elementType = TypeInfo.ANY; + ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length()); + FieldInfo fi = FieldInfo.localField(varName, elementType, m.start(1), null, -1, -1, 0); + fi.setScopeInfo(scope); + doc.addTopLevelLocal(fi); + } + } + } + + private boolean isInsideAnyScope(int pos) { + for (MethodInfo method : doc.getAllMethods()) { + if (method.containsPosition(pos)) return true; + } + for (InnerCallableScope scope : doc.getInnerScopes()) { + if (scope.containsPosition(pos)) return true; + } + return false; } private void scanRange(String bodyText, int bodyStart, int bodyEnd, 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 a2d670e83..e6d715710 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 @@ -113,6 +113,9 @@ public class ScriptDocument { private final Map globalFields = new HashMap<>(); // Local variables per method (methodStartOffset -> {varName -> FieldInfo}) private final Map>> methodLocals = new HashMap<>(); + // Top-level variables (outside methods) - for JavaScript, these can be treated as globals, but we keep them separate for clarity + // Mainly top level loops like for (var x : collection) { } where x is a local variable but not inside a method + private final Map> topLevelLocals = new HashMap<>(); // Inner callable scopes (lambdas, JS function expressions) - NOT in methods list private final List innerScopes = new ArrayList<>(); @@ -349,6 +352,7 @@ public class ScriptDocument { wildcardPackages.clear(); excludedRanges.clear(); methodLocals.clear(); + topLevelLocals.clear(); scriptTypes.clear(); innerScopes.clear(); methodCalls.clear(); @@ -1442,6 +1446,36 @@ public class ScriptDocument { return best; } + private FieldInfo pickVisibleTopLevelLocal(String name, int position) { + return pickVisibleLocal(topLevelLocals.get(name), position); + } + + void addTopLevelLocal(FieldInfo fieldInfo) { + if (fieldInfo == null) { + return; + } + topLevelLocals.computeIfAbsent(fieldInfo.getName(), k -> new ArrayList<>()).add(fieldInfo); + } + + private Collection getVisibleTopLevelLocals(int position) { + List visible = new ArrayList<>(); + for (List candidates : topLevelLocals.values()) { + FieldInfo best = pickVisibleLocal(candidates, position); + if (best != null) { + visible.add(best); + } + } + return visible; + } + + Collection getTopLevelLocalCandidates() { + List all = new ArrayList<>(); + for (List fields : topLevelLocals.values()) { + all.addAll(fields); + } + return all; + } + /** * Parse local variables inside methods/functions - UNIFIED for both Java and JavaScript. * Stores results in the shared 'methodLocals' map (methodOffset -> varName -> FieldInfo). @@ -2488,7 +2522,10 @@ public class ScriptDocument { } } if (insideInner) continue; - + + int afterVar = m.end(1); + if (afterVar < text.length() && LoopVariableParser.FOR_IN_OF_LOOKAHEAD.matcher(text.substring(afterVar)).find()) continue; + String varName = m.group(1); String initializer = null; int initializerStart = -1; @@ -2560,6 +2597,10 @@ public class ScriptDocument { if (isExcluded(position)) continue; + int enclosingParen = findEnclosingParenStart(0, position); + if (enclosingParen >= 0 && "for".equals(readKeywordBefore(enclosingParen))) + continue; + int modifiers = parseModifiers(typeNameRaw); String typeName = stripModifiers(typeNameRaw); @@ -6900,7 +6941,8 @@ public class ScriptDocument { FieldInfo finalTargetField = targetField; boolean isScriptField = targetField != null && (globalFields.containsValue(targetField) || - methodLocals.values().stream().anyMatch(m -> m.values().stream().anyMatch(list -> list.contains(finalTargetField)))); + methodLocals.values().stream().anyMatch(m -> m.values().stream().anyMatch(list -> list.contains(finalTargetField))) || + topLevelLocals.values().stream().anyMatch(list -> list.contains(finalTargetField))); // Determine if the target field is final // For script fields, use the modifiers; for external fields, use reflection @@ -7653,7 +7695,10 @@ public class ScriptDocument { } } - // 3. For Java only: Check if we're inside a script type and look for fields there + FieldInfo topLocal = pickVisibleTopLevelLocal(name, position); + if (topLocal != null) return topLocal; + + // 4. 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)) { @@ -7882,6 +7927,14 @@ public class ScriptDocument { } } } + for (List fields : topLevelLocals.values()) { + for (FieldInfo field : fields) { + AssignmentInfo assign = field.findAssignmentAtPosition(position); + if (assign != null) { + return assign; + } + } + } for (AssignmentInfo assign : declarationErrors) { if (assign.containsLhsPosition(position)) { @@ -7974,6 +8027,8 @@ public class ScriptDocument { } } + variables.addAll(getVisibleTopLevelLocals(position)); + // Add global fields (only those visible at position) for (FieldInfo globalField : globalFields.values()) { if (globalField.isVisibleAt(position)) { @@ -8037,6 +8092,11 @@ public class ScriptDocument { } } } + for (List fields : topLevelLocals.values()) { + for (FieldInfo field : fields) { + errored.addAll(field.getErroredAssignments()); + } + } // Check external field assignments for (AssignmentInfo assign : externalFieldAssignments) { @@ -8232,6 +8292,11 @@ public class ScriptDocument { } } + FieldInfo topLevelLocal = pickVisibleTopLevelLocal(varName, position); + if (topLevelLocal != null) { + return topLevelLocal.getTypeInfo(); + } + // Check global fields if (globalFields.containsKey(varName)) { return globalFields.get(varName).getTypeInfo();