Single line multi var decalartion support i.e var x = 10, y = "faah", z = player; OR int x,y,z;

This commit is contained in:
bigguy345 committed 2026-03-09 03:00:42 +02:00
1 parent 8b00733ddf
commit 44dfec6283
2 files changed
+866 -17

No files matched your search

@@ -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.
*
* <p>Extracted from {@link ScriptDocument} to keep the main document class
* focused on orchestration while this class owns the scanning algorithms.</p>
*/
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.
*
* <p>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.</p>
*
* <p>The callback receives (varName, absNamePos, initializer, absInitStart, absInitEnd)
* for each continuation declarator found.</p>
*
* @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.
*
* <p>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.</p>
*
* <p>Each continuation declarator follows the pattern: {@code name [= initializer]}.
* The initializer is terminated at {@code ;} or {@code ,} at depth 0.</p>
*
* @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).
*
* <p><b>Note:</b> 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.</p>
*
* @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.
*
* <p>The Java {@code FIELD_DECL_PATTERN} regex allows commas inside the
* type-name character class (to support generics like {@code Map<K, V>}).
* 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"}.</p>
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* <p>For {@code "final int x, y,"}, returns {@code "final int"}.
* For {@code "Map<String, Integer> x, y,"}, returns {@code "Map<String, Integer>"}.</p>
*
* @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.
*
* <p>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.</p>
*
* @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");
}
}
@@ -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<String, List<FieldInfo>> 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<FieldInfo> 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<String, List<FieldInfo>> 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<String, List<FieldInfo>> 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 =)