mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 04:17:36 +00:00
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:
1 parent
8b00733ddf
commit
44dfec6283
2 files changed
+866
-17
No files matched your search
+405
@@ -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 =)
|
||||
|
||||
Reference in new issue
Block a user