From 102c4fc398008b9a48cd88167440f7bb726801d7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 11 Mar 2026 14:38:49 +0200 Subject: [PATCH] Expanded scope of patterns/markers to handle nested generics more robustly --- .../script/interpreter/ScriptDocument.java | 112 +++++++++++++++--- 1 file changed, 96 insertions(+), 16 deletions(-) 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 b05f71f0f..838e5856f 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 @@ -82,13 +82,13 @@ public class ScriptDocument { private static final Pattern CLASS_DECL_PATTERN = Pattern.compile( "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); private static final Pattern METHOD_DECL_PATTERN = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); + "\\b([A-Za-z_][a-zA-Z0-9_.<>,[ \\t]\\[\\]]*)[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); private static final Pattern METHOD_CALL_PATTERN = Pattern.compile( "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); // Also matches ',' as a delimiter so that "int x, y, z;" is recognized // (the first declarator "int x," is captured; continuations are scanned manually). private static final Pattern FIELD_DECL_PATTERN = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_.<>,[ \\t\\n\\r]\\[\\]]*)[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*)[ \\t]*(=|;|,)"); + "\\b([A-Za-z_][a-zA-Z0-9_.<>,[ \\t]\\[\\]]*)[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*)[ \\t]*(=|;|,)"); private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:<([^>]*)>)?"); // Cast expression pattern: captures type name inside cast parentheses @@ -524,6 +524,20 @@ public class ScriptDocument { return false; } + /** + * Check if any position in [start, end) falls inside an excluded range. + * Uses range-overlap logic (O(excludedRanges.size())) instead of per-character checks. + * Two ranges [a,b) and [c,d) overlap iff a < d && c < b. + */ + public boolean hasExcludedRange(int start, int end) { + for (int[] range : excludedRanges) { + if (start < range[1] && range[0] < end) { + return true; + } + } + return false; + } + /** * Check if a position falls inside one of the type's inner class bodies. * Used to prevent field/constructor declarations inside inner classes @@ -1145,6 +1159,36 @@ public class ScriptDocument { return result; } + /** + * Replace newline characters ({@code \n}, {@code \r}) with spaces when they + * occur inside angle brackets (generic type arguments). This lets patterns + * like FIELD_DECL_PATTERN and METHOD_DECL_PATTERN match multi-line generics + * (e.g. {@code HashMap}) without allowing a match to + * span across {@code //} comment lines. + * + *

The returned string has the same length as the input (characters + * are replaced, never inserted or removed), so all positional indices remain + * valid for the original text. + */ + private static String normalizeGenericNewlines(String src) { + StringBuilder sb = new StringBuilder(src.length()); + int depth = 0; + for (int i = 0; i < src.length(); i++) { + char c = src.charAt(i); + if (c == '<') + depth++; + else if (c == '>') { + if (depth > 0) + depth--; + } + if ((c == '\n' || c == '\r') && depth > 0) + sb.append(' '); + else + sb.append(c); + } + return sb.toString(); + } + /** * Find the index of a keyword in a string, but only at angle-bracket depth 0. * The keyword must be preceded and followed by non-identifier characters (word boundary). @@ -1591,15 +1635,21 @@ public class ScriptDocument { } else { // Java: ReturnType methodName(params) { ... } or { ; } Pattern methodWithBody = Pattern.compile( - "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*(\\{|;)"); + "\\b([a-zA-Z_][a-zA-Z0-9_.<>,[ \\t]\\[\\]]*)[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*(\\{|;)"); - Matcher m = methodWithBody.matcher(text); + Matcher m = methodWithBody.matcher(normalizeGenericNewlines(text)); while (m.find()) { String methodName = m.group(2); if (isExcluded(m.start()) || isKeyword(methodName)) continue; + if (isExcluded(m.start(2))) + continue; + + if (hasExcludedRange(m.start(1), m.end(1))) + continue; + String returnType = m.group(1); if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum") || returnType.equals("new") @@ -2280,7 +2330,7 @@ public class ScriptDocument { } } else { // Java: Type varName = expr; or Type varName; - Matcher m = FIELD_DECL_PATTERN.matcher(bodyText); + Matcher m = FIELD_DECL_PATTERN.matcher(normalizeGenericNewlines(bodyText)); while (m.find()) { String typeName = m.group(1); String varName = m.group(2); @@ -2294,6 +2344,8 @@ public class ScriptDocument { // where group(2) lands on the next line outside the comment. int absTypeStart = bodyStart + m.start(1); if (isExcluded(absTypeStart)) continue; + if (hasExcludedRange(absTypeStart, bodyStart + m.end(1))) + continue; // --- FIX: Detect greedy regex consuming commas into the type name --- // FIELD_DECL_PATTERN allows commas in its type character class (for generics). @@ -2523,12 +2575,14 @@ public class ScriptDocument { */ 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); + + Matcher m = FIELD_DECL_PATTERN.matcher(normalizeGenericNewlines(rangeText)); while (m.find()) { int declPos = start + m.start(); if (declPos < start || declPos >= end) continue; if (isExcluded(declPos)) continue; + if (hasExcludedRange(start + m.start(1), start + m.end(1))) + continue; String typeNameRaw = m.group(1); String varName = m.group(2); @@ -3334,8 +3388,20 @@ public class ScriptDocument { if (param.isEmpty()) continue; // Find position in original text + // Search within a reasonable window to avoid matching shadowed names from outer scopes int paramOffset = text.indexOf(param, offset); if (paramOffset < 0) paramOffset = offset; + // Validate that we found it within expected range (before next comma or closing paren) + int nextComma = text.indexOf(',', offset); + int nextClose = text.indexOf(')', offset); + int endBound = Math.min( + nextComma >= 0 ? nextComma : Integer.MAX_VALUE, + nextClose >= 0 ? nextClose : Integer.MAX_VALUE + ); + if (paramOffset >= endBound) { + // Found match is outside the parameter list, use direct offset calculation instead + paramOffset = offset; + } String[] parts = param.split("\\s+"); String paramName; @@ -3348,28 +3414,33 @@ public class ScriptDocument { boolean isVarArg = typeName.endsWith("..."); if (isVarArg) typeName = typeName.substring(0, typeName.length() - 3); paramType = resolveType(typeName); - int nameOffset = text.indexOf(paramName, paramOffset); - if (nameOffset < 0) nameOffset = paramOffset; + // For typed params, the name is after the type, so calculate position more carefully + int nameOffset = paramOffset + (param.lastIndexOf(paramName)); + if (nameOffset < paramOffset || nameOffset >= endBound) + nameOffset = paramOffset + param.length() - paramName.length(); FieldInfo paramInfo = FieldInfo.parameter(paramName, paramType, nameOffset, null); paramInfo.setVarArg(isVarArg); scope.addParameter(paramInfo); } else { // Untyped: just name (type will be inferred from expected FI) paramName = parts[0]; - int nameOffset = text.indexOf(paramName, paramOffset); - if (nameOffset < 0) nameOffset = paramOffset; + // For untyped params, the entire param string is the name, so use paramOffset directly + int nameOffset = paramOffset; FieldInfo paramInfo = FieldInfo.parameter(paramName, paramType, nameOffset, null); scope.addParameter(paramInfo); } - - offset = paramOffset + param.length(); + + offset = Math.max(paramOffset + param.length(), offset + 1); } } } else { // Single identifier: x -> String paramName = headerText; int nameOffset = text.indexOf(paramName, headerStart); - if (nameOffset < 0) nameOffset = headerStart; + // Validate found position is before the arrow (not a shadowed name after) + if (nameOffset < 0 || nameOffset >= arrowPos) { + nameOffset = headerStart; + } FieldInfo paramInfo = FieldInfo.parameter(paramName, null, nameOffset, null); scope.addParameter(paramInfo); @@ -3783,7 +3854,7 @@ public class ScriptDocument { } } else { // Java: [modifiers] Type fieldName [= expr]; - Matcher m = FIELD_DECL_PATTERN.matcher(text); + Matcher m = FIELD_DECL_PATTERN.matcher(normalizeGenericNewlines(text)); while (m.find()) { String typeNameRaw = m.group(1); String fieldName = m.group(2); @@ -3797,6 +3868,9 @@ public class ScriptDocument { if (isExcluded(m.start(1))) continue; + if (hasExcludedRange(m.start(1), m.end(1))) + continue; + // --- FIX: Detect greedy regex consuming commas into the type name --- // Same issue as parseLocalVariables: FIELD_DECL_PATTERN greedily consumes // multi-declarator commas into group(1). When a depth-0 comma exists in @@ -6074,11 +6148,17 @@ public class ScriptDocument { } private void markMethodDeclarations(List marks) { - Matcher m = METHOD_DECL_PATTERN.matcher(text); + Matcher m = METHOD_DECL_PATTERN.matcher(normalizeGenericNewlines(text)); while (m.find()) { if (isExcluded(m.start())) continue; + if (isExcluded(m.start(2))) + continue; + + if (hasExcludedRange(m.start(1), m.end(1))) + continue; + // Skip class/interface/enum declarations - these look like method declarations // but "class Foo(" is not a method, it's a class declaration3 String returnType = m.group(1);