diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MultiDeclaratorParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MultiDeclaratorParser.java
new file mode 100644
index 000000000..7c5806a03
--- /dev/null
+++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MultiDeclaratorParser.java
@@ -0,0 +1,405 @@
+package noppes.npcs.client.gui.util.script.interpreter;
+
+// ==================== MULTI-DECLARATOR HELPERS ====================
+//
+// JavaScript and Java both allow declaring multiple variables in a single
+// statement, separated by commas:
+//
+// JS: var a = 1, b = "x", c = player;
+// Java: int x = 1, y, z = 3;
+//
+// The existing regex patterns only match the FIRST declarator because:
+// - The JS pattern `(var|let|const)\s+(\w+)` requires a keyword prefix.
+// - The Java FIELD_DECL_PATTERN requires a type name prefix.
+//
+// Continuation declarators after the first comma have no keyword/type prefix,
+// so they must be scanned manually. The two helpers below handle this by
+// scanning forward from the end of the first declarator for commas at
+// nesting depth 0, then extracting the `name [= initializer]` pattern
+// for each continuation declarator.
+
+/**
+ * Static helpers for parsing comma-separated multi-variable declarations.
+ *
+ *
Extracted from {@link ScriptDocument} to keep the main document class
+ * focused on orchestration while this class owns the scanning algorithms.
+ */
+public final class MultiDeclaratorParser {
+
+ private MultiDeclaratorParser() {}
+
+ // ======================== CALLBACKS ========================
+
+ /** Callback for {@link #scanJSContinuationDeclarators}. */
+ @FunctionalInterface
+ public interface JSContinuationCallback {
+ void accept(String varName, int absNamePos, String initializer, int absInitStart, int absInitEnd);
+ }
+
+ /** Callback for {@link #scanJavaContinuationDeclarators}. */
+ @FunctionalInterface
+ public interface JavaContinuationCallback {
+ void accept(String varName, int absNamePos, int absInitStart, int absInitEnd);
+ }
+
+ // ======================== JS SCANNING ========================
+
+ /**
+ * Scan forward from {@code scanStart} in {@code source} for comma-separated
+ * continuation declarators in a JS {@code var/let/const} statement.
+ *
+ * Each continuation declarator follows the pattern: {@code name [= initializer]}.
+ * The initializer (if present) is terminated using comma-aware scanning via
+ * {@link ScriptDocument#findJsInitializerEnd(String, int, boolean)}, which stops at
+ * top-level commas so that each declarator's RHS is correctly bounded.
+ *
+ * The callback receives (varName, absNamePos, initializer, absInitStart, absInitEnd)
+ * for each continuation declarator found.
+ *
+ * @param doc the owning ScriptDocument (provides {@code findJsInitializerEnd}
+ * and {@code skipSegmentWhitespace})
+ * @param source the source text to scan within (e.g. method body or global text)
+ * @param scanStart the position in {@code source} to begin scanning (typically
+ * right after the first declarator's initializer end or name end)
+ * @param absBase the absolute offset of {@code source[0]} in the full document text
+ * (used to compute absolute positions for the callback)
+ * @param callback receives each continuation declarator
+ */
+ public static void scanJSContinuationDeclarators(
+ ScriptDocument doc, String source, int scanStart, int absBase,
+ JSContinuationCallback callback) {
+
+ int pos = scanStart;
+ while (pos < source.length()) {
+ // Skip whitespace
+ while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
+ if (pos >= source.length()) break;
+
+ // Expect a comma separating the next declarator
+ if (source.charAt(pos) != ',') break;
+ pos++; // skip the comma
+
+ // Skip whitespace after comma
+ while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
+ if (pos >= source.length()) break;
+
+ // Read the variable name (must be an identifier)
+ int nameStart = pos;
+ while (pos < source.length() && Character.isJavaIdentifierPart(source.charAt(pos))) pos++;
+ if (pos == nameStart) break; // no identifier found; malformed
+ String varName = source.substring(nameStart, pos);
+
+ // Skip whitespace after name
+ while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
+
+ String initializer = null;
+ int initStart = -1;
+ int initEnd = -1;
+
+ // Check for optional '=' initializer
+ if (pos < source.length() && source.charAt(pos) == '=') {
+ // Make sure it's not ==
+ if (pos + 1 < source.length() && source.charAt(pos + 1) == '=') break;
+ pos++; // skip '='
+ int rhsStart = doc.skipSegmentWhitespace(source, pos);
+ // Use comma-aware initializer termination so that the RHS stops at
+ // the next top-level comma (which starts the next declarator).
+ int rhsEnd = doc.findJsInitializerEnd(source, rhsStart, true);
+ if (rhsEnd > rhsStart) {
+ initializer = source.substring(rhsStart, rhsEnd).trim();
+ if (initializer.isEmpty()) initializer = null;
+ }
+ initStart = rhsStart;
+ initEnd = rhsEnd;
+ pos = rhsEnd;
+ }
+
+ int absNamePos = absBase + nameStart;
+ int absInitStart = initStart >= 0 ? absBase + initStart : -1;
+ int absInitEnd = initEnd >= 0 ? absBase + initEnd : -1;
+
+ callback.accept(varName, absNamePos, initializer, absInitStart, absInitEnd);
+ }
+ }
+
+ // ======================== JAVA SCANNING ========================
+
+ /**
+ * Scan forward from {@code scanStart} in {@code source} for comma-separated
+ * continuation declarators in a Java multi-variable declaration.
+ *
+ * Java allows {@code int x = 1, y, z = 3;} where all declarators share
+ * the same declared type. The {@code FIELD_DECL_PATTERN} regex only matches
+ * the first declarator ({@code int x =}); subsequent names like {@code y}
+ * and {@code z} lack a type prefix. This method manually scans the remainder
+ * of the statement for those continuation names.
+ *
+ * Each continuation declarator follows the pattern: {@code name [= initializer]}.
+ * The initializer is terminated at {@code ;} or {@code ,} at depth 0.
+ *
+ * @param source the source text to scan within
+ * @param scanStart the position in {@code source} to begin scanning (typically
+ * right after the semicolon/delimiter of the first declarator
+ * match, or right after the first declarator's initializer)
+ * @param absBase the absolute offset of {@code source[0]} in the full document text
+ * @param callback receives each continuation declarator
+ */
+ public static void scanJavaContinuationDeclarators(
+ String source, int scanStart, int absBase,
+ JavaContinuationCallback callback) {
+
+ int pos = scanStart;
+ while (pos < source.length()) {
+ // Skip whitespace
+ while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
+ if (pos >= source.length()) break;
+
+ // Expect a comma separating the next declarator
+ if (source.charAt(pos) != ',') break;
+ pos++; // skip the comma
+
+ // Skip whitespace after comma
+ while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
+ if (pos >= source.length()) break;
+
+ // Read the variable name (must be an identifier)
+ int nameStart = pos;
+ while (pos < source.length() && Character.isJavaIdentifierPart(source.charAt(pos))) pos++;
+ if (pos == nameStart) break; // no identifier found; malformed
+ String varName = source.substring(nameStart, pos);
+
+ // Skip whitespace after name
+ while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
+
+ int initStart = -1;
+ int initEnd = -1;
+
+ // Check for optional '=' initializer
+ if (pos < source.length() && source.charAt(pos) == '=') {
+ // Make sure it's not ==
+ if (pos + 1 < source.length() && source.charAt(pos + 1) == '=') break;
+
+ initStart = pos; // position of '='
+ pos++; // skip '='
+ int rhsStart = pos;
+ // Skip whitespace after '='
+ while (rhsStart < source.length() && Character.isWhitespace(source.charAt(rhsStart))) rhsStart++;
+
+ // Scan forward for ';' or ',' at depth 0 to terminate this declarator's initializer
+ int depth = 0;
+ int angleDepth = 0;
+ int scanPos = rhsStart;
+ while (scanPos < source.length()) {
+ char c = source.charAt(scanPos);
+ if (c == '(' || c == '[' || c == '{') depth++;
+ else if (c == ')' || c == ']' || c == '}') depth--;
+ else if (c == '<') angleDepth++;
+ else if (c == '>') angleDepth--;
+ else if ((c == ';' || c == ',') && depth == 0 && angleDepth == 0) {
+ initEnd = scanPos;
+ break;
+ }
+ scanPos++;
+ }
+ pos = (initEnd >= 0) ? initEnd : scanPos;
+ } else if (pos < source.length() && (source.charAt(pos) == ';' || source.charAt(pos) == ',')) {
+ // No initializer — name only (e.g., the `y` in `int x, y, z;`)
+ // Don't advance past ';' here; let the outer loop handle it.
+ // If it's a comma, we'll pick it up on the next iteration.
+ } else {
+ // Unexpected character; stop scanning
+ break;
+ }
+
+ int absNamePos = absBase + nameStart;
+ int absInitStartAbs = initStart >= 0 ? absBase + initStart : -1;
+ int absInitEndAbs = initEnd >= 0 ? absBase + initEnd : -1;
+
+ callback.accept(varName, absNamePos, absInitStartAbs, absInitEndAbs);
+ }
+ }
+
+ // ======================== SCAN-START POSITION HELPERS ========================
+
+ /**
+ * Determine the scan position for continuation declarators after a JS
+ * declarator match. If the first declarator has an initializer, we scan
+ * from the initializer end; otherwise we scan from after the variable name.
+ *
+ * @param bodyText the body text being parsed
+ * @param matchEnd the end position of the regex match (end of var name or '=')
+ * @param hasEquals whether the first declarator has '='
+ * @param initEnd the initializer end position (in bodyText coords), or -1
+ * @return the position in bodyText to start scanning for continuation commas
+ */
+ public static int jsDeclaratorScanStart(String bodyText, int matchEnd, boolean hasEquals, int initEnd) {
+ if (hasEquals && initEnd >= 0) {
+ return initEnd;
+ }
+ // No initializer: scan from after the variable name
+ return matchEnd;
+ }
+
+ /**
+ * Determine the scan position for continuation declarators after a Java
+ * FIELD_DECL_PATTERN match. The pattern's group(3) matches either '=', ';',
+ * or ','. If it matched '=', the scan starts after the initializer expression;
+ * if ';', there are no continuations (semicolon ends the statement).
+ *
+ * Note: when the regex greedily consumed commas into the type name
+ * (e.g. {@code "int x, y,"} as the type group), the caller must detect this
+ * via {@link #findFirstDepthZeroComma(String)} on the matched type string
+ * and use the returned comma position directly as the scan start, bypassing
+ * this method. See the call sites in {@link ScriptDocument} for details.
+ *
+ * @param source the source text
+ * @param delimiter the delimiter matched by group(3): "=", ";", or ","
+ * @param delimEnd the end position of the delimiter in source coords
+ * @param initEnd the initializer end position, or -1 if no initializer
+ * @return the position in source to start scanning for continuation commas,
+ * or -1 if the statement ended at a semicolon
+ */
+ public static int javaDeclaratorScanStart(String source, String delimiter, int delimEnd, int initEnd) {
+ if (";".equals(delimiter)) {
+ return -1; // semicolon terminates the statement; no continuations possible
+ }
+ // delimiter is '=' or ',' — scan from after the initializer (which stops at ',' or ';')
+ if (initEnd >= 0) {
+ return initEnd;
+ }
+ // When delimiter is ',' with no initializer (e.g. the 'x' in "int x,y,z;"),
+ // m.end(3) lands AFTER the comma. The scanner expects to START at the comma
+ // so it can consume it as the continuation separator — so back up by one.
+ if (",".equals(delimiter)) {
+ return delimEnd - 1;
+ }
+ return delimEnd;
+ }
+
+ // ======================== GREEDY-REGEX COMMA HELPERS ========================
+
+ /**
+ * Find the first comma at bracket-depth 0 in the given string.
+ *
+ * The Java {@code FIELD_DECL_PATTERN} regex allows commas inside the
+ * type-name character class (to support generics like {@code Map}).
+ * This causes the regex to greedily consume multi-declarator commas into
+ * group(1). For example, {@code "int x, y, z;"} matches with
+ * group(1)={@code "int x, y,"} and group(2)={@code "z"}.
+ *
+ * This helper scans for the first comma that is NOT inside angle brackets
+ * ({@code <>}), parentheses, square brackets, or braces, i.e. at depth 0.
+ * A depth-0 comma indicates a multi-declarator separator that was consumed
+ * by the greedy regex.
+ *
+ * @param s the string to search (typically the matched type group)
+ * @return the index of the first depth-0 comma, or -1 if none found
+ */
+ public static int findFirstDepthZeroComma(String s) {
+ int depth = 0;
+ int angleDepth = 0;
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == '(' || c == '[' || c == '{') depth++;
+ else if (c == ')' || c == ']' || c == '}') depth--;
+ else if (c == '<') angleDepth++;
+ else if (c == '>') angleDepth--;
+ else if (c == ',' && depth == 0 && angleDepth == 0) return i;
+ }
+ return -1;
+ }
+
+ /**
+ * Extract the real type name from a regex group(1) that greedily consumed
+ * extra variable names and commas.
+ *
+ * When the regex matches {@code "int x, y, z;"} as group(1)={@code "int x, y,"},
+ * the real type name is {@code "int"} — everything before the first identifier
+ * that precedes the first depth-0 comma.
+ *
+ * For {@code "final int x, y,"}, returns {@code "final int"}.
+ * For {@code "Map x, y,"}, returns {@code "Map"}.
+ *
+ * @param typeGroup the full group(1) string from the regex match
+ * @param commaIndex the index of the first depth-0 comma in {@code typeGroup}
+ * (as returned by {@link #findFirstDepthZeroComma(String)})
+ * @return the real type name (may include modifiers like "final")
+ */
+ public static String extractRealTypeFromGreedyMatch(String typeGroup, int commaIndex) {
+ // The part before the first depth-0 comma contains "Type firstVar"
+ // (possibly with modifiers). We need to split off the last identifier
+ // token before the comma, as that's the first variable name, not the type.
+ String beforeComma = typeGroup.substring(0, commaIndex).trim();
+ int lastSpace = beforeComma.lastIndexOf(' ');
+ int lastTab = beforeComma.lastIndexOf('\t');
+ int lastWS = Math.max(lastSpace, lastTab);
+ if (lastWS < 0) {
+ // No whitespace found — the "type" is actually just a variable name
+ // This shouldn't happen for valid declarations but handle gracefully
+ return beforeComma;
+ }
+ return beforeComma.substring(0, lastWS).trim();
+ }
+
+ /**
+ * Extract the first variable name from a regex group(1) that greedily consumed
+ * extra variable names and commas.
+ *
+ * When the regex matches {@code "int x, y, z;"} as group(1)={@code "int x, y,"},
+ * the first variable name is {@code "x"} — the last identifier token before
+ * the first depth-0 comma.
+ *
+ * @param typeGroup the full group(1) string from the regex match
+ * @param commaIndex the index of the first depth-0 comma in {@code typeGroup}
+ * (as returned by {@link #findFirstDepthZeroComma(String)})
+ * @return the first variable name
+ */
+ public static String extractFirstVarFromGreedyMatch(String typeGroup, int commaIndex) {
+ String beforeComma = typeGroup.substring(0, commaIndex).trim();
+ int lastSpace = beforeComma.lastIndexOf(' ');
+ int lastTab = beforeComma.lastIndexOf('\t');
+ int lastWS = Math.max(lastSpace, lastTab);
+ if (lastWS < 0) {
+ return beforeComma;
+ }
+ return beforeComma.substring(lastWS + 1).trim();
+ }
+
+ /**
+ * Find the absolute position of the first variable name within a greedily-consumed
+ * type group. Searches for the first variable name (the last token before the
+ * first depth-0 comma) within the source text starting from the type group start.
+ *
+ * @param source the full source text
+ * @param typeGroupStart the absolute start of group(1) in the source
+ * @param typeGroup the group(1) string
+ * @param commaIndex the index of the first depth-0 comma in typeGroup
+ * @return the absolute position of the first variable name in source
+ */
+ public static int findFirstVarPosition(String source, int typeGroupStart, String typeGroup, int commaIndex) {
+ String firstVar = extractFirstVarFromGreedyMatch(typeGroup, commaIndex);
+ // The first variable appears in typeGroup just before the comma.
+ // Search backwards from commaIndex to find it.
+ String beforeComma = typeGroup.substring(0, commaIndex).trim();
+ int varStartInGroup = beforeComma.lastIndexOf(firstVar);
+ return typeGroupStart + varStartInGroup;
+ }
+
+ // ======================== MISC HELPERS ========================
+
+ /**
+ * Check whether the statement starting at {@code stmtStart} begins with
+ * a JS declaration keyword ({@code var}, {@code let}, or {@code const}).
+ * Used by {@link ScriptDocument}'s {@code parseAssignments()} to decide
+ * whether the RHS of an {@code =} should stop at a top-level comma
+ * (multi-declarator context).
+ */
+ public static boolean startsWithJSKeyword(String source, int stmtStart, int equalsPos) {
+ int i = stmtStart;
+ while (i < equalsPos && Character.isWhitespace(source.charAt(i))) i++;
+ String prefix = source.substring(i, Math.min(i + 6, equalsPos)).trim();
+ return prefix.startsWith("var ") || prefix.startsWith("var\t")
+ || prefix.startsWith("let ") || prefix.startsWith("let\t")
+ || prefix.startsWith("const ") || prefix.startsWith("const\t");
+ }
+}
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 cc54eb690..b1a75f719 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
@@ -84,8 +84,10 @@ public class ScriptDocument {
"\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([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]\\[\\]]*)[ \\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_]*)");
// Cast expression pattern: captures type name inside cast parentheses
@@ -1534,7 +1536,7 @@ public class ScriptDocument {
int initializerEnd = -1;
if (m.group(3) != null) {
initializerStart = skipSegmentWhitespace(bodyText, m.end(3));
- initializerEnd = findJsInitializerEnd(bodyText, initializerStart);
+ initializerEnd = findJsInitializerEnd(bodyText, initializerStart, true);
if (initializerEnd > initializerStart) {
initializer = bodyText.substring(initializerStart, initializerEnd).trim();
if (initializer.isEmpty()) {
@@ -1618,6 +1620,53 @@ public class ScriptDocument {
}
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fieldInfo);
+
+ // --- Multi-declarator continuation for JS ---
+ // For "var a=1, b='x', c=player;" the regex only matched 'a'.
+ // Scan forward from the end of this declarator for comma-separated
+ // continuation names (b, c, ...) that share the same var/let/const keyword.
+ final String fKind = kind;
+ final MethodInfo fMethod = method;
+ final int fBodyStart = bodyStart;
+ final int fBodyEnd = bodyEnd;
+ final Map> fLocals = locals;
+ int contScanStart = MultiDeclaratorParser.jsDeclaratorScanStart(bodyText, m.end(2), m.group(3) != null, initializerEnd);
+ MultiDeclaratorParser.scanJSContinuationDeclarators(this, bodyText, contScanStart, bodyStart,
+ (cVarName, cAbsNamePos, cInit, cAbsInitStart, cAbsInitEnd) -> {
+ if (isExcluded(cAbsNamePos)) return;
+ boolean cInsideInner = false;
+ for (InnerCallableScope sc : innerScopes) {
+ if (sc.containsPosition(cAbsNamePos)) { cInsideInner = true; break; }
+ }
+ if (cInsideInner) return;
+
+ TypeInfo cType = null;
+ if (cInit != null && !cInit.isEmpty()) {
+ TypeInfo inferred = resolveExpressionType(cInit, cAbsInitStart);
+ if (!TypeInfo.NULL.equals(inferred)) cType = inferred;
+ }
+ if (cType == null) cType = TypeInfo.ANY;
+
+ FieldInfo cField = FieldInfo.localField(cVarName, cType, cAbsNamePos, fMethod,
+ cAbsInitStart, cAbsInitEnd, 0);
+ ScopeInfo cScope;
+ if ("var".equals(fKind)) {
+ cScope = new ScopeInfo(fBodyStart, fBodyEnd, false, "method");
+ } else {
+ cScope = computeBlockScope(fBodyStart, fBodyEnd, cAbsNamePos);
+ }
+ cField.setScopeInfo(cScope);
+
+ if (fLocals.containsKey(cVarName) || globalFields.containsKey(cVarName)) {
+ AssignmentInfo dupErr = AssignmentInfo.duplicateDeclaration(
+ cVarName, cAbsNamePos, cAbsNamePos + cVarName.length(),
+ "Variable '" + cVarName + "' is already defined in the scope");
+ dupErr.setScopeInfo(cScope);
+ declarationErrors.add(dupErr);
+ return;
+ }
+ fLocals.computeIfAbsent(cVarName, k -> new ArrayList<>()).add(cField);
+ });
}
} else {
// Java: Type varName = expr; or Type varName;
@@ -1629,6 +1678,96 @@ public class ScriptDocument {
int absPos = bodyStart + m.start(2);
if (isExcluded(absPos)) continue;
+
+ // --- FIX: Detect greedy regex consuming commas into the type name ---
+ // FIELD_DECL_PATTERN allows commas in its type character class (for generics).
+ // This causes "int x, y, z = 10;" to match as type="int x, y," name="z" delim="=".
+ // When a depth-0 comma exists in typeName, the regex swallowed extra declarators.
+ // We extract the real type and first variable, then let the continuation scanner
+ // handle ALL variables (including the regex-matched one).
+ int commaInType = MultiDeclaratorParser.findFirstDepthZeroComma(typeName);
+ if (commaInType >= 0) {
+ String realTypeName = MultiDeclaratorParser.extractRealTypeFromGreedyMatch(typeName, commaInType);
+ String firstVar = MultiDeclaratorParser.extractFirstVarFromGreedyMatch(typeName, commaInType);
+ int firstVarAbsPos = MultiDeclaratorParser.findFirstVarPosition(
+ bodyText, m.start(1), typeName, commaInType) + bodyStart;
+
+ // Skip control flow keywords on the real type
+ if (realTypeName.equals("return") || realTypeName.equals("if") || realTypeName.equals("while") ||
+ realTypeName.equals("for") || realTypeName.equals("switch") || realTypeName.equals("catch") ||
+ realTypeName.equals("new") || realTypeName.equals("throw")) {
+ continue;
+ }
+
+ int enclosingParen1 = findEnclosingParenStart(bodyStart, firstVarAbsPos);
+ if (enclosingParen1 >= 0 && "for".equals(readKeywordBefore(enclosingParen1))) continue;
+
+ int greedyModifiers = parseModifiers(realTypeName);
+ TypeInfo greedyTypeInfo = resolveType(realTypeName);
+
+ // Register the first variable (extracted from the greedy type group)
+ FieldInfo firstField = FieldInfo.localField(firstVar, greedyTypeInfo, firstVarAbsPos,
+ method, -1, -1, greedyModifiers);
+ ScopeInfo firstScope = computeBlockScope(bodyStart, bodyEnd, firstVarAbsPos);
+ firstField.setScopeInfo(firstScope);
+
+ List existingFirst = locals.get(firstVar);
+ if (existingFirst != null) {
+ boolean dup = false;
+ for (FieldInfo prev : existingFirst) {
+ ScopeInfo prevScope = prev.getScopeInfo();
+ if (prevScope != null && prevScope.containsPosition(firstVarAbsPos)) {
+ dup = true;
+ break;
+ }
+ }
+ if (dup || globalFields.containsKey(firstVar)) {
+ AssignmentInfo dupErr = AssignmentInfo.duplicateDeclaration(
+ firstVar, firstVarAbsPos, firstVarAbsPos + firstVar.length(),
+ "Variable '" + firstVar + "' is already defined in the scope");
+ dupErr.setScopeInfo(firstScope);
+ declarationErrors.add(dupErr);
+ } else {
+ locals.computeIfAbsent(firstVar, k -> new ArrayList<>()).add(firstField);
+ }
+ } else if (globalFields.containsKey(firstVar)) {
+ AssignmentInfo dupErr = AssignmentInfo.duplicateDeclaration(
+ firstVar, firstVarAbsPos, firstVarAbsPos + firstVar.length(),
+ "Variable '" + firstVar + "' is already defined in the scope");
+ dupErr.setScopeInfo(firstScope);
+ declarationErrors.add(dupErr);
+ } else {
+ locals.computeIfAbsent(firstVar, k -> new ArrayList<>()).add(firstField);
+ }
+
+ // Scan continuations from the first depth-0 comma in the type group.
+ // This will pick up all remaining variables (y, z, etc.) including the
+ // regex-matched varName which was the last one consumed by the greedy match.
+ final TypeInfo greedySharedType = greedyTypeInfo;
+ final int greedySharedMod = greedyModifiers;
+ final MethodInfo fMethod3 = method;
+ final Map> fLocals3 = locals;
+ int greedyScanStart = m.start(1) + commaInType;
+ MultiDeclaratorParser.scanJavaContinuationDeclarators(bodyText, greedyScanStart, bodyStart,
+ (cVarName, cAbsNamePos, cAbsInitStart, cAbsInitEnd) -> {
+ if (isExcluded(cAbsNamePos)) return;
+ FieldInfo cField = FieldInfo.localField(cVarName, greedySharedType, cAbsNamePos,
+ fMethod3, cAbsInitStart, cAbsInitEnd, greedySharedMod);
+ ScopeInfo cScope = computeBlockScope(bodyStart, bodyEnd, cAbsNamePos);
+ cField.setScopeInfo(cScope);
+
+ if (fLocals3.containsKey(cVarName) || globalFields.containsKey(cVarName)) {
+ AssignmentInfo dupErr = AssignmentInfo.duplicateDeclaration(
+ cVarName, cAbsNamePos, cAbsNamePos + cVarName.length(),
+ "Variable '" + cVarName + "' is already defined in the scope");
+ dupErr.setScopeInfo(cScope);
+ declarationErrors.add(dupErr);
+ return;
+ }
+ fLocals3.computeIfAbsent(cVarName, k -> new ArrayList<>()).add(cField);
+ });
+ continue;
+ }
// Skip control flow keywords
if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") ||
@@ -1704,6 +1843,37 @@ public class ScriptDocument {
}
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fieldInfo);
+
+ // --- Multi-declarator continuation for Java ---
+ // For "int x = 1, y, z = 3;" FIELD_DECL_PATTERN matched "int x =" (or "int x,").
+ // Scan forward for comma-separated continuation declarators (y, z)
+ // that share the same declared type from the first match.
+ final TypeInfo sharedType = typeInfo;
+ final int sharedModifiers = modifiers;
+ final MethodInfo fMethod2 = method;
+ final Map> fLocals2 = locals;
+ int javaScanStart = MultiDeclaratorParser.javaDeclaratorScanStart(bodyText, delimiter, m.end(3),
+ initEnd >= 0 ? initEnd - bodyStart : -1);
+ if (javaScanStart >= 0) {
+ MultiDeclaratorParser.scanJavaContinuationDeclarators(bodyText, javaScanStart, bodyStart,
+ (cVarName, cAbsNamePos, cAbsInitStart, cAbsInitEnd) -> {
+ if (isExcluded(cAbsNamePos)) return;
+ FieldInfo cField = FieldInfo.localField(cVarName, sharedType, cAbsNamePos,
+ fMethod2, cAbsInitStart, cAbsInitEnd, sharedModifiers);
+ ScopeInfo cScope = computeBlockScope(bodyStart, bodyEnd, cAbsNamePos);
+ cField.setScopeInfo(cScope);
+
+ if (fLocals2.containsKey(cVarName) || globalFields.containsKey(cVarName)) {
+ AssignmentInfo dupErr = AssignmentInfo.duplicateDeclaration(
+ cVarName, cAbsNamePos, cAbsNamePos + cVarName.length(),
+ "Variable '" + cVarName + "' is already defined in the scope");
+ dupErr.setScopeInfo(cScope);
+ declarationErrors.add(dupErr);
+ return;
+ }
+ fLocals2.computeIfAbsent(cVarName, k -> new ArrayList<>()).add(cField);
+ });
+ }
}
}
}
@@ -1745,6 +1915,48 @@ public class ScriptDocument {
String typeNameRaw = m.group(1);
String varName = m.group(2);
String delimiter = m.group(3);
+
+ // --- FIX: Detect greedy regex consuming commas into the type name ---
+ // Same issue as the other call sites: FIELD_DECL_PATTERN greedily consumes
+ // multi-declarator commas into group(1). Handle by extracting the real type
+ // and first variable, then scanning continuations from the first comma.
+ int commaInType = MultiDeclaratorParser.findFirstDepthZeroComma(typeNameRaw);
+ if (commaInType >= 0) {
+ String realTypeNameRaw = MultiDeclaratorParser.extractRealTypeFromGreedyMatch(typeNameRaw, commaInType);
+ String firstVar = MultiDeclaratorParser.extractFirstVarFromGreedyMatch(typeNameRaw, commaInType);
+ int firstVarAbsPos = MultiDeclaratorParser.findFirstVarPosition(
+ rangeText, m.start(1), typeNameRaw, commaInType) + start;
+
+ // Skip control flow keywords on the real type
+ if (realTypeNameRaw.equals("return") || realTypeNameRaw.equals("if") || realTypeNameRaw.equals("while") ||
+ realTypeNameRaw.equals("for") || realTypeNameRaw.equals("switch") || realTypeNameRaw.equals("catch") ||
+ realTypeNameRaw.equals("new") || realTypeNameRaw.equals("throw")) {
+ continue;
+ }
+
+ String greedyTypeName = stripModifiers(realTypeNameRaw);
+ int greedyModifiers = parseModifiers(realTypeNameRaw);
+ TypeInfo greedyVarType = resolveType(greedyTypeName);
+
+ // Register the first variable
+ FieldInfo firstField = FieldInfo.localField(firstVar, greedyVarType, firstVarAbsPos,
+ null, -1, -1, greedyModifiers);
+ scope.addLocal(firstVar, firstField);
+
+ // Scan continuations from the first depth-0 comma
+ final TypeInfo greedySharedType = greedyVarType;
+ final int greedySharedMod = greedyModifiers;
+ final InnerCallableScope fScope3 = scope;
+ int greedyScanStart = m.start(1) + commaInType;
+ MultiDeclaratorParser.scanJavaContinuationDeclarators(rangeText, greedyScanStart, start,
+ (cVarName, cAbsNamePos, cAbsInitStart, cAbsInitEnd) -> {
+ if (isExcluded(cAbsNamePos)) return;
+ FieldInfo cField = FieldInfo.localField(cVarName, greedySharedType, cAbsNamePos,
+ null, cAbsInitStart, cAbsInitEnd, greedySharedMod);
+ fScope3.addLocal(cVarName, cField);
+ });
+ continue;
+ }
// Skip control flow keywords
if (typeNameRaw.equals("return") || typeNameRaw.equals("if") || typeNameRaw.equals("while") ||
@@ -1788,6 +2000,22 @@ public class ScriptDocument {
int absPos = start + m.start(2);
FieldInfo localVar = FieldInfo.localField(varName, varType, absPos, null, initStart, initEnd, modifiers);
scope.addLocal(varName, localVar);
+
+ // Multi-declarator continuation for Java: "int x, y = 2, z;"
+ final TypeInfo sharedType = varType;
+ final int sharedModifiers = modifiers;
+ final InnerCallableScope fScope2 = scope;
+ int javaScanStart = MultiDeclaratorParser.javaDeclaratorScanStart(rangeText, delimiter, m.end(3),
+ initEnd >= 0 ? initEnd - start : -1);
+ if (javaScanStart >= 0) {
+ MultiDeclaratorParser.scanJavaContinuationDeclarators(rangeText, javaScanStart, start,
+ (cVarName, cAbsNamePos, cAbsInitStart, cAbsInitEnd) -> {
+ if (isExcluded(cAbsNamePos)) return;
+ FieldInfo cField = FieldInfo.localField(cVarName, sharedType, cAbsNamePos,
+ null, cAbsInitStart, cAbsInitEnd, sharedModifiers);
+ fScope2.addLocal(cVarName, cField);
+ });
+ }
}
}
@@ -1818,7 +2046,8 @@ public class ScriptDocument {
int initializerEnd = -1;
if (m.group(2) != null) {
initializerStart = skipSegmentWhitespace(rangeText, m.end(2));
- initializerEnd = findJsInitializerEnd(rangeText, initializerStart);
+ // Use comma-aware termination so multi-declarator RHS is bounded correctly
+ initializerEnd = findJsInitializerEnd(rangeText, initializerStart, true);
if (initializerEnd > initializerStart) {
initializer = rangeText.substring(initializerStart, initializerEnd).trim();
if (initializer.isEmpty()) {
@@ -1864,10 +2093,27 @@ public class ScriptDocument {
}
scope.addLocal(varName, localVar);
+
+ // Multi-declarator continuation: scan for "var a=1, b, c=3"
+ final InnerCallableScope fScope = scope;
+ int contScanStart = MultiDeclaratorParser.jsDeclaratorScanStart(rangeText, m.end(1), m.group(2) != null, initializerEnd);
+ MultiDeclaratorParser.scanJSContinuationDeclarators(this, rangeText, contScanStart, start,
+ (cVarName, cAbsNamePos, cInit, cAbsInitStart, cAbsInitEnd) -> {
+ if (isExcluded(cAbsNamePos)) return;
+ TypeInfo cType = null;
+ if (cInit != null && !cInit.isEmpty()) {
+ TypeInfo inferred = resolveExpressionType(cInit, cAbsInitStart);
+ if (!TypeInfo.NULL.equals(inferred)) cType = inferred;
+ }
+ if (cType == null) cType = TypeInfo.ANY;
+ FieldInfo cField = FieldInfo.localField(cVarName, cType, cAbsNamePos, null,
+ cAbsInitStart, cAbsInitEnd, 0);
+ fScope.addLocal(cVarName, cField);
+ });
}
}
- private int skipSegmentWhitespace(String source, int pos) {
+ int skipSegmentWhitespace(String source, int pos) {
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) {
pos++;
}
@@ -1899,7 +2145,11 @@ public class ScriptDocument {
* {@code source.length()} if the source ends before a terminator is found.
* Returns {@code rhsStart} unchanged if {@code rhsStart} is out of bounds.
*/
- private int findJsInitializerEnd(String source, int rhsStart) {
+ int findJsInitializerEnd(String source, int rhsStart) {
+ return findJsInitializerEnd(source, rhsStart, false);
+ }
+
+ int findJsInitializerEnd(String source, int rhsStart, boolean stopAtTopLevelComma) {
if (rhsStart < 0 || rhsStart >= source.length()) {
return rhsStart;
}
@@ -1987,6 +2237,10 @@ public class ScriptDocument {
return pos;
}
+ if (stopAtTopLevelComma && c == ',' && parenDepth == 0 && bracketDepth == 0 && braceDepth == 0) {
+ return pos;
+ }
+
if (c == '\n' || c == '\r') {
int lineBreakPos = pos;
int nextPos = pos + 1;
@@ -2807,7 +3061,8 @@ public class ScriptDocument {
int initializerEnd = -1;
if (m.group(2) != null) {
initializerStart = skipSegmentWhitespace(text, m.end(2));
- initializerEnd = findJsInitializerEnd(text, initializerStart);
+ // Use comma-aware termination for multi-declarator support
+ initializerEnd = findJsInitializerEnd(text, initializerStart, true);
if (initializerEnd > initializerStart) {
initializer = text.substring(initializerStart, initializerEnd).trim();
if (initializer.isEmpty()) {
@@ -2859,6 +3114,41 @@ public class ScriptDocument {
} else {
globalFields.put(varName, fieldInfo);
}
+
+ // Multi-declarator continuation for global JS: "var a=1, b='x', c;"
+ int contScanStart = MultiDeclaratorParser.jsDeclaratorScanStart(text, m.end(1), m.group(2) != null, initializerEnd);
+ MultiDeclaratorParser.scanJSContinuationDeclarators(this, text, contScanStart, 0,
+ (cVarName, cAbsNamePos, cInit, cAbsInitStart, cAbsInitEnd) -> {
+ if (isExcluded(cAbsNamePos)) return;
+ boolean cInsideMethod = false;
+ for (MethodInfo method : getAllMethods()) {
+ if (method.containsPosition(cAbsNamePos)) { cInsideMethod = true; break; }
+ }
+ if (cInsideMethod) return;
+ boolean cInsideInner = false;
+ for (InnerCallableScope sc : innerScopes) {
+ if (sc.containsPosition(cAbsNamePos)) { cInsideInner = true; break; }
+ }
+ if (cInsideInner) return;
+
+ TypeInfo cType = null;
+ if (cInit != null && !cInit.isEmpty()) {
+ TypeInfo inferred = resolveExpressionType(cInit, cAbsInitStart);
+ if (!TypeInfo.NULL.equals(inferred)) cType = inferred;
+ }
+ if (cType == null) cType = TypeInfo.ANY;
+
+ FieldInfo cField = FieldInfo.globalField(cVarName, cType, cAbsNamePos, null,
+ cAbsInitStart, cAbsInitEnd, 0);
+ if (globalFields.containsKey(cVarName)) {
+ AssignmentInfo dupErr = AssignmentInfo.duplicateDeclaration(
+ cVarName, cAbsNamePos, cAbsNamePos + cVarName.length(),
+ "Variable '" + cVarName + "' is already defined in the scope");
+ declarationErrors.add(dupErr);
+ } else {
+ globalFields.put(cVarName, cField);
+ }
+ });
}
} else {
// Java: [modifiers] Type fieldName [= expr];
@@ -2872,6 +3162,96 @@ public class ScriptDocument {
if (isExcluded(position))
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
+ // typeNameRaw, extract the real type and first variable, then let the
+ // continuation scanner handle ALL variables.
+ int commaInType = MultiDeclaratorParser.findFirstDepthZeroComma(typeNameRaw);
+ if (commaInType >= 0) {
+ String realTypeNameRaw = MultiDeclaratorParser.extractRealTypeFromGreedyMatch(typeNameRaw, commaInType);
+ String firstVar = MultiDeclaratorParser.extractFirstVarFromGreedyMatch(typeNameRaw, commaInType);
+ int firstVarAbsPos = MultiDeclaratorParser.findFirstVarPosition(
+ text, m.start(1), typeNameRaw, commaInType);
+
+ if (isExcluded(firstVarAbsPos)) continue;
+
+ int enclosingParen1 = findEnclosingParenStart(0, firstVarAbsPos);
+ if (enclosingParen1 >= 0 && "for".equals(readKeywordBefore(enclosingParen1))) continue;
+
+ int greedyModifiers = parseModifiers(realTypeNameRaw);
+ String greedyTypeName = stripModifiers(realTypeNameRaw);
+
+ ScriptTypeInfo greedyContainingType = null;
+ for (ScriptTypeInfo scriptType : scriptTypes.values())
+ if (scriptType.containsPosition(firstVarAbsPos)) {
+ greedyContainingType = scriptType;
+ break;
+ }
+
+ boolean greedyInsideMethod = false;
+ if (greedyContainingType != null && isInsideNestedMethod(firstVarAbsPos,
+ greedyContainingType.getBodyStart(), greedyContainingType.getBodyEnd()))
+ greedyInsideMethod = true;
+ else {
+ for (MethodInfo method : getAllMethods()) {
+ if (method.containsPosition(firstVarAbsPos)) { greedyInsideMethod = true; break; }
+ }
+ }
+ if (greedyInsideMethod) continue;
+
+ String greedyDocumentation = extractDocumentationBefore(m.start());
+ TypeInfo greedyTypeInfo = resolveType(greedyTypeName);
+
+ // Register the first variable (no initializer since the regex consumed it into the type group)
+ FieldInfo firstField = FieldInfo.globalField(firstVar, greedyTypeInfo, firstVarAbsPos,
+ greedyDocumentation, -1, -1, greedyModifiers);
+ if (greedyContainingType != null) {
+ greedyContainingType.addField(firstField);
+ } else if (globalFields.containsKey(firstVar)) {
+ AssignmentInfo dupErr = AssignmentInfo.duplicateDeclaration(
+ firstVar, firstVarAbsPos, firstVarAbsPos + firstVar.length(),
+ "Variable '" + firstVar + "' is already defined in the scope");
+ declarationErrors.add(dupErr);
+ } else {
+ globalFields.put(firstVar, firstField);
+ }
+
+ // Scan continuations from the first depth-0 comma in the type group
+ final TypeInfo greedySharedType = greedyTypeInfo;
+ final int greedySharedMod = greedyModifiers;
+ final ScriptTypeInfo fGreedyContType = greedyContainingType;
+ int greedyScanStart = m.start(1) + commaInType;
+ MultiDeclaratorParser.scanJavaContinuationDeclarators(text, greedyScanStart, 0,
+ (cVarName, cAbsNamePos, cAbsInitStart, cAbsInitEnd) -> {
+ if (isExcluded(cAbsNamePos)) return;
+ boolean cInsideMethod = false;
+ if (fGreedyContType != null && isInsideNestedMethod(cAbsNamePos,
+ fGreedyContType.getBodyStart(), fGreedyContType.getBodyEnd()))
+ cInsideMethod = true;
+ else {
+ for (MethodInfo method : getAllMethods()) {
+ if (method.containsPosition(cAbsNamePos)) { cInsideMethod = true; break; }
+ }
+ }
+ if (cInsideMethod) return;
+
+ FieldInfo cField = FieldInfo.globalField(cVarName, greedySharedType, cAbsNamePos,
+ null, cAbsInitStart, cAbsInitEnd, greedySharedMod);
+ if (fGreedyContType != null) {
+ fGreedyContType.addField(cField);
+ } else if (globalFields.containsKey(cVarName)) {
+ AssignmentInfo dupErr = AssignmentInfo.duplicateDeclaration(
+ cVarName, cAbsNamePos, cAbsNamePos + cVarName.length(),
+ "Variable '" + cVarName + "' is already defined in the scope");
+ declarationErrors.add(dupErr);
+ } else {
+ globalFields.put(cVarName, cField);
+ }
+ });
+ continue;
+ }
+
int enclosingParen = findEnclosingParenStart(0, position);
if (enclosingParen >= 0 && "for".equals(readKeywordBefore(enclosingParen)))
continue;
@@ -2911,11 +3291,14 @@ public class ScriptDocument {
initStart = m.start(3);
int searchPos = m.end(3);
int depth = 0;
+ // Stop at ',' or ';' at depth 0 so that multi-declarator
+ // statements like "int x = 1, y, z = 3;" correctly bound
+ // the initializer of x to just "1" (not "1, y, z = 3").
while (searchPos < text.length()) {
char c = text.charAt(searchPos);
if (c == '(' || c == '[' || c == '{') depth++;
else if (c == ')' || c == ']' || c == '}') depth--;
- else if (c == ';' && depth == 0) {
+ else if ((c == ';' || c == ',') && depth == 0) {
initEnd = searchPos;
break;
}
@@ -2936,6 +3319,46 @@ public class ScriptDocument {
} else {
globalFields.put(fieldName, fieldInfo);
}
+
+ // --- Multi-declarator continuation for global Java fields ---
+ // For "int x = 1, y, z = 3;" FIELD_DECL_PATTERN matched "int x ="
+ // (or "int x,"). Scan forward for comma-separated continuation
+ // declarators (y, z) that share the same declared type.
+ final TypeInfo sharedType = typeInfo;
+ final int sharedModifiers = modifiers;
+ final ScriptTypeInfo fContainingType = containingScriptType;
+ int javaScanStart = MultiDeclaratorParser.javaDeclaratorScanStart(text, delimiter, m.end(3),
+ initEnd >= 0 ? initEnd : -1);
+ if (javaScanStart >= 0) {
+ MultiDeclaratorParser.scanJavaContinuationDeclarators(text, javaScanStart, 0,
+ (cVarName, cAbsNamePos, cAbsInitStart, cAbsInitEnd) -> {
+ if (isExcluded(cAbsNamePos)) return;
+ // Check if inside a method — if so, skip (it's a local, not global)
+ boolean cInsideMethod = false;
+ if (fContainingType != null && isInsideNestedMethod(cAbsNamePos,
+ fContainingType.getBodyStart(), fContainingType.getBodyEnd()))
+ cInsideMethod = true;
+ else {
+ for (MethodInfo method : getAllMethods()) {
+ if (method.containsPosition(cAbsNamePos)) { cInsideMethod = true; break; }
+ }
+ }
+ if (cInsideMethod) return;
+
+ FieldInfo cField = FieldInfo.globalField(cVarName, sharedType, cAbsNamePos,
+ null, cAbsInitStart, cAbsInitEnd, sharedModifiers);
+ if (fContainingType != null) {
+ fContainingType.addField(cField);
+ } else if (globalFields.containsKey(cVarName)) {
+ AssignmentInfo dupErr = AssignmentInfo.duplicateDeclaration(
+ cVarName, cAbsNamePos, cAbsNamePos + cVarName.length(),
+ "Variable '" + cVarName + "' is already defined in the scope");
+ declarationErrors.add(dupErr);
+ } else {
+ globalFields.put(cVarName, cField);
+ }
+ });
+ }
}
}
}
@@ -7247,15 +7670,29 @@ public class ScriptDocument {
continue;
}
- // Find the start of this statement (previous ; or { or } or start of text)
+ // Find the start of this statement: walk backward to ';', '{', '}', or a
+ // depth-0 ',' (multi-declarator separator like "int x=1, y=2").
+ // Track '()'/`[]` depth so commas inside argument lists are not treated
+ // as declarator separators.
int stmtStart = equalsPos - 1;
- while (stmtStart >= 0) {
- char c = text.charAt(stmtStart);
- if (c == ';' || c == '{' || c == '}') {
- stmtStart++;
- break;
+ {
+ int backDepth = 0;
+ while (stmtStart >= 0) {
+ char c = text.charAt(stmtStart);
+ if (c == ')' || c == ']') {
+ backDepth++;
+ } else if (c == '(' || c == '[') {
+ if (backDepth == 0) { stmtStart++; break; }
+ backDepth--;
+ } else if (c == ';' || c == '{' || c == '}') {
+ stmtStart++;
+ break;
+ } else if (c == ',' && backDepth == 0) {
+ stmtStart++;
+ break;
+ }
+ stmtStart--;
}
- stmtStart--;
}
if (stmtStart < 0) stmtStart = 0;
@@ -7270,13 +7707,20 @@ public class ScriptDocument {
while (rhsBoundaryStart < text.length() && Character.isWhitespace(text.charAt(rhsBoundaryStart))) {
rhsBoundaryStart++;
}
- stmtEnd = findJsInitializerEnd(text, rhsBoundaryStart);
+ // For multi-declarator JS statements (var a=1, b=2), the RHS of
+ // each declarator must stop at the top-level comma. We detect this
+ // by checking if the statement begins with var/let/const.
+ boolean isMultiDecl = MultiDeclaratorParser.startsWithJSKeyword(text, stmtStart, equalsPos);
+ stmtEnd = findJsInitializerEnd(text, rhsBoundaryStart, isMultiDecl);
if (stmtEnd < equalsPos + 1) {
stmtEnd = equalsPos + 1;
}
} else {
- stmtEnd = text.indexOf(';', equalsPos);
- if (stmtEnd < 0) stmtEnd = text.length();
+ // For Java, stop at depth-0 ',' or ';' so that each declarator in
+ // "int x='str', y=20, z='str'" is bounded independently.
+ // findJsInitializerEnd handles string literals and bracket depth correctly.
+ stmtEnd = findJsInitializerEnd(text, equalsPos + 1, true);
+ if (stmtEnd < equalsPos + 1) stmtEnd = equalsPos + 1;
}
// Extract LHS (before =) and RHS (after =)