Cleaned up markChainedAccessFields heavily

This commit is contained in:
bigguy345 committed 2026-01-02 21:24:45 +02:00
1 parent fbbd53d5ff
commit 4e0c5e91da
2 files changed
+461 -403

No files matched your search

@@ -0,0 +1,443 @@
package noppes.npcs.client.gui.util.script.interpreter;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Helper class for marking chained field accesses in script documents.
* Handles patterns like: identifier.field, this.field, super.field, method().field
*/
public class FieldChainMarker {
private final ScriptDocument document;
private final String text;
public FieldChainMarker(ScriptDocument document, String text) {
this.document = document;
this.text = text;
}
/**
* Mark all chained field accesses in the document.
* Uses two passes: one for standard chains, one for method/array result chains.
*/
public void markChainedFieldAccesses(List<ScriptLine.Mark> marks) {
// ==================== PASS 1: Standard identifier chains ====================
// Pattern: identifier.identifier, this.identifier, super.identifier
Pattern chainPattern = Pattern.compile(
"\\b(this|super|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
Matcher m = chainPattern.matcher(text);
while (m.find()) {
int chainStart = m.start(1);
// Skip excluded ranges and imports
if (document.isExcluded(chainStart) || document.isInImportOrPackage(chainStart)) continue;
// Skip if second segment is a method call - markMethodCalls handles it
if (document.isFollowedByParen(m.end(2))) continue;
// Build the full chain
ChainData chain = buildChain(m);
if (chain == null) continue;
// Resolve the starting type
ChainContext ctx = resolveChainStart(chain, chainStart);
// Mark each segment
for (int i = ctx.startIndex; i < chain.segments.size(); i++) {
String segment = chain.segments.get(i);
int[] pos = chain.positions.get(i);
if (document.isExcluded(pos[0])) continue;
boolean isLast = (i == chain.segments.size() - 1);
boolean isStatic = (i > 0) && isStaticContext(chain.segments.get(i - 1));
// Determine what type of marking this segment needs
MarkResult result = resolveSegmentMark(ctx, segment, pos, i, isLast, isStatic, chainStart);
// Apply the mark
if (result.mark != null) {
marks.add(result.mark);
}
// Update context for next segment
ctx.currentType = result.nextType;
}
}
// ==================== PASS 2: Method/array result chains ====================
// Handle: getMinecraft().thePlayer, array[0].length
Pattern dotIdent = Pattern.compile("\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
Matcher md = dotIdent.matcher(text);
while (md.find()) {
int identStart = md.start(1);
int identEnd = md.end(1);
int dotPos = md.start();
// Skip excluded ranges and imports
if (document.isExcluded(identStart) || document.isInImportOrPackage(identStart)) continue;
// Only handle when preceded by ')' or ']' (method call or array access result)
Character precedingChar = getNonWhitespaceBefore(dotPos);
if (precedingChar == null || (precedingChar != ')' && precedingChar != ']')) continue;
// Get receiver type from the expression before the dot
int[] bounds = document.findReceiverBoundsBefore(dotPos);
if (bounds == null) continue;
if (document.isExcluded(bounds[0]) || document.isInImportOrPackage(bounds[0])) continue;
String receiverExpr = text.substring(bounds[0], bounds[1]).trim();
if (receiverExpr.isEmpty()) continue;
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, bounds[0]);
// Mark the first field and continue the chain
markReceiverChainSegments(marks, md.group(1), identStart, identEnd, receiverType);
}
}
// ==================== CHAIN BUILDING HELPERS ====================
/** Data class for a parsed chain */
private static class ChainData {
final List<String> segments = new ArrayList<>();
final List<int[]> positions = new ArrayList<>();
}
/** Context for chain resolution */
private static class ChainContext {
TypeInfo currentType;
int startIndex;
boolean firstIsThis;
boolean firstIsSuper;
boolean firstIsPrecededByDot;
}
/** Result of resolving a segment's mark */
private static class MarkResult {
final ScriptLine.Mark mark;
final TypeInfo nextType;
MarkResult(ScriptLine.Mark mark, TypeInfo nextType) {
this.mark = mark;
this.nextType = nextType;
}
}
/** Build a complete chain from a regex match, continuing to read more segments */
private ChainData buildChain(Matcher m) {
ChainData chain = new ChainData();
// Add first two segments from the match
chain.segments.add(m.group(1));
chain.positions.add(new int[]{m.start(1), m.end(1)});
chain.segments.add(m.group(2));
chain.positions.add(new int[]{m.start(2), m.end(2)});
// Continue reading subsequent .identifier segments
int pos = m.end(2);
while (pos < text.length()) {
pos = document.skipWhitespace(pos);
if (pos >= text.length() || text.charAt(pos) != '.') break;
pos++; // Skip dot
pos = document.skipWhitespace(pos);
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
// Read identifier
int identStart = pos;
while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) pos++;
int identEnd = pos;
// Stop if this is a method call
if (document.isFollowedByParen(identEnd)) break;
chain.segments.add(text.substring(identStart, identEnd));
chain.positions.add(new int[]{identStart, identEnd});
}
return chain;
}
/** Resolve the starting context for a chain */
private ChainContext resolveChainStart(ChainData chain, int chainStart) {
ChainContext ctx = new ChainContext();
String first = chain.segments.get(0);
ctx.firstIsThis = first.equals("this");
ctx.firstIsSuper = first.equals("super");
ctx.firstIsPrecededByDot = document.isPrecededByDot(chainStart);
ctx.startIndex = ctx.firstIsPrecededByDot ? 0 : 1;
ctx.currentType = null;
if (ctx.firstIsThis) {
// 'this' - will use globalFields for resolution
ctx.currentType = null;
} else if (ctx.firstIsSuper) {
// 'super' - resolve to parent class
ScriptTypeInfo enclosingType = document.findEnclosingScriptType(chainStart);
if (enclosingType != null && enclosingType.hasSuperClass()) {
ctx.currentType = enclosingType.getSuperClass();
}
} else if (ctx.firstIsPrecededByDot) {
// Field access on a receiver (e.g., getMinecraft().thePlayer)
TypeInfo receiverType = document.resolveReceiverChain(chainStart);
if (receiverType != null && receiverType.hasField(first)) {
FieldInfo varInfo = receiverType.getFieldInfo(first);
ctx.currentType = (varInfo != null) ? varInfo.getTypeInfo() : null;
}
} else {
// Try as type first (static access), then as variable
TypeInfo typeCheck = document.resolveType(first);
if (typeCheck != null && typeCheck.isResolved()) {
ctx.currentType = typeCheck;
} else {
FieldInfo varInfo = document.resolveVariable(first, chainStart);
ctx.currentType = (varInfo != null) ? varInfo.getTypeInfo() : null;
}
}
return ctx;
}
/** Resolve what mark a segment should get */
private MarkResult resolveSegmentMark(ChainContext ctx, String segment, int[] pos,
int index, boolean isLast, boolean isStatic, int chainStart) {
// Case 1: First segment preceded by dot (field on receiver)
if (index == 0 && ctx.firstIsPrecededByDot) {
return resolveReceiverFieldMark(segment, pos, chainStart, isLast, isStatic);
}
// Case 2: this.field
if (index == 1 && ctx.firstIsThis) {
return resolveThisFieldMark(segment, pos);
}
// Case 3: super.field
if (index == 1 && ctx.firstIsSuper) {
return resolveSuperFieldMark(ctx.currentType, segment, pos, isLast);
}
// Case 4: Resolved type with field
if (ctx.currentType != null && ctx.currentType.isResolved()) {
return resolveTypedFieldMark(ctx.currentType, segment, pos, isLast, isStatic);
}
// Case 5: Unresolved - mark as undefined
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
null
);
}
// ==================== SEGMENT RESOLUTION HELPERS ====================
/** Resolve mark for field access on a receiver (preceded by dot) */
private MarkResult resolveReceiverFieldMark(String segment, int[] pos, int chainStart,
boolean isLast, boolean isStatic) {
TypeInfo receiverType = document.resolveReceiverChain(chainStart);
if (receiverType != null && receiverType.hasField(segment)) {
FieldInfo fieldInfo = receiverType.getFieldInfo(segment);
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1],
receiverType, fieldInfo, isLast, isStatic);
TokenType tokenType = (fieldInfo != null && fieldInfo.isEnumConstant())
? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD;
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], tokenType, accessInfo),
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
);
}
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
null
);
}
/** Resolve mark for this.field access */
private MarkResult resolveThisFieldMark(String segment, int[] pos) {
if (document.getGlobalFields().containsKey(segment)) {
FieldInfo fieldInfo = document.getGlobalFields().get(segment);
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.GLOBAL_FIELD, fieldInfo),
fieldInfo.getTypeInfo()
);
}
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
null
);
}
/** Resolve mark for super.field access */
private MarkResult resolveSuperFieldMark(TypeInfo superType, String segment, int[] pos, boolean isLast) {
if (superType == null) {
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR,
TokenErrorMessage.from("Cannot resolve field '" + segment + "'").clearOtherErrors()),
null
);
}
// Search through inheritance hierarchy
boolean found = false;
FieldInfo fieldInfo = null;
if (superType instanceof ScriptTypeInfo) {
ScriptTypeInfo scriptSuper = (ScriptTypeInfo) superType;
found = scriptSuper.hasFieldInHierarchy(segment);
if (found) fieldInfo = scriptSuper.getFieldInfoInHierarchy(segment);
} else {
found = superType.hasField(segment);
if (found) fieldInfo = superType.getFieldInfo(segment);
}
if (found) {
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1],
superType, fieldInfo, isLast, false);
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.GLOBAL_FIELD, accessInfo),
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
);
}
String errorMsg = "Field '" + segment + "' not found in parent class hierarchy starting from '"
+ superType.getSimpleName() + "'";
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR,
TokenErrorMessage.from(errorMsg).clearOtherErrors()),
null
);
}
/** Resolve mark for typed field access */
private MarkResult resolveTypedFieldMark(TypeInfo currentType, String segment, int[] pos,
boolean isLast, boolean isStatic) {
if (!currentType.hasField(segment)) {
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
null
);
}
FieldInfo fieldInfo = currentType.getFieldInfo(segment);
// Check for static access error
if (isStatic && fieldInfo != null && !fieldInfo.isStatic()) {
TokenErrorMessage errorMsg = TokenErrorMessage
.from("Cannot access non-static field '" + segment + "' from static context '"
+ currentType.getSimpleName() + "'")
.clearOtherErrors();
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR, errorMsg),
null
);
}
// Valid field access
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1],
currentType, fieldInfo, isLast, isStatic);
TokenType tokenType = (fieldInfo != null && fieldInfo.isEnumConstant())
? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD;
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], tokenType, accessInfo),
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
);
}
/** Mark segments following a method call or array access result */
private void markReceiverChainSegments(List<ScriptLine.Mark> marks, String firstField,
int identStart, int identEnd, TypeInfo receiverType) {
// Mark the first field
FieldInfo fInfo = null;
TypeInfo currentType = null;
if (receiverType != null && receiverType.hasField(firstField)) {
fInfo = receiverType.getFieldInfo(firstField);
boolean hasMore = document.isFollowedByDot(identEnd);
boolean isStatic = isStaticContext(receiverType.getSimpleName());
FieldAccessInfo accessInfo = document.createFieldAccessInfo(firstField, identStart, identEnd,
receiverType, fInfo, !hasMore, isStatic);
TokenType tokenType = (fInfo != null && fInfo.isEnumConstant())
? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD;
marks.add(new ScriptLine.Mark(identStart, identEnd, tokenType, accessInfo));
currentType = (fInfo != null) ? fInfo.getTypeInfo() : null;
} else {
marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR));
return;
}
// Continue the chain
int pos = identEnd;
while (pos < text.length()) {
pos = document.skipWhitespace(pos);
if (pos >= text.length() || text.charAt(pos) != '.') break;
pos++; // Skip dot
pos = document.skipWhitespace(pos);
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
// Read identifier
int nStart = pos;
while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) pos++;
int nEnd = pos;
String seg = text.substring(nStart, nEnd);
// Stop if method call
if (document.isFollowedByParen(nEnd)) break;
if (document.isExcluded(nStart)) break;
boolean isLast = !document.isFollowedByDot(nEnd);
if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) {
FieldInfo segInfo = currentType.getFieldInfo(seg);
boolean isStatic = isStaticContext(currentType.getSimpleName());
FieldAccessInfo accessInfo = document.createFieldAccessInfo(seg, nStart, nEnd,
currentType, segInfo, isLast, isStatic);
TokenType tokenType = (segInfo != null && segInfo.isEnumConstant())
? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD;
marks.add(new ScriptLine.Mark(nStart, nEnd, tokenType, accessInfo));
currentType = (segInfo != null) ? segInfo.getTypeInfo() : null;
} else {
marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR));
break;
}
}
}
// ==================== UTILITY HELPERS ====================
/** Get the non-whitespace character before a position */
private Character getNonWhitespaceBefore(int pos) {
int before = pos - 1;
while (before >= 0 && Character.isWhitespace(text.charAt(before))) before--;
return (before >= 0) ? text.charAt(before) : null;
}
/** Check if a name indicates static context (starts with uppercase) */
private boolean isStaticContext(String name) {
return name != null && !name.isEmpty() && Character.isUpperCase(name.charAt(0));
}
}
@@ -1371,7 +1371,7 @@ public class ScriptDocument {
return result.isEmpty() ? null : result;
}
private TypeInfo resolveType(String typeName) {
TypeInfo resolveType(String typeName) {
return resolveTypeAndTrackUsage(typeName);
}
@@ -2460,7 +2460,7 @@ public class ScriptDocument {
*
* This is THE method that should be used for all type resolution needs.
*/
private TypeInfo resolveExpressionType(String expr, int position) {
TypeInfo resolveExpressionType(String expr, int position) {
expr = expr.trim();
if (expr.isEmpty()) {
@@ -2742,7 +2742,7 @@ public class ScriptDocument {
* @param methodNameStart The start position of the method name
* @return The TypeInfo of the final receiver, or null if no receiver or couldn't resolve
*/
private TypeInfo resolveReceiverChain(int methodNameStart) {
TypeInfo resolveReceiverChain(int methodNameStart) {
// First check if preceded by a dot
int scanPos = methodNameStart - 1;
while (scanPos >= 0 && Character.isWhitespace(text.charAt(scanPos)))
@@ -3357,7 +3357,7 @@ public class ScriptDocument {
* of the receiver expression immediately to the left of the dot. The end
* returned will be the dot index (exclusive). Returns null if not found or malformed.
*/
private int[] findReceiverBoundsBefore(int dotIndex) {
int[] findReceiverBoundsBefore(int dotIndex) {
if (dotIndex < 0 || dotIndex >= text.length() || text.charAt(dotIndex) != '.') {
return null;
}
@@ -3368,6 +3368,12 @@ public class ScriptDocument {
if (pos < 0) return null;
while (pos >= 0) {
// Check if we're in a comment range (but not string range)
// Skip comments to avoid picking up types from comment text
if (isInCommentRange(pos)) {
return null;
}
char c = text.charAt(pos);
if (Character.isWhitespace(c)) { pos--; continue; }
@@ -3406,7 +3412,7 @@ public class ScriptDocument {
* Find the script-defined type that contains the given position.
* Used for resolving 'this' references.
*/
private ScriptTypeInfo findEnclosingScriptType(int position) {
ScriptTypeInfo findEnclosingScriptType(int position) {
for (ScriptTypeInfo type : scriptTypes.values()) {
if (type.containsPosition(position)) {
return type;
@@ -3703,386 +3709,10 @@ public class ScriptDocument {
* Does NOT mark method calls (identifiers followed by parentheses) - those are handled by markMethodCalls.
*/
private void markChainedFieldAccesses(List<ScriptLine.Mark> marks) {
// Pattern to find identifier chains: identifier.identifier, this.identifier, super.identifier, etc.
// Start with an identifier, 'this', or 'super' followed by at least one dot and another identifier
Pattern chainPattern = Pattern.compile(
"\\b(this|super|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
Matcher m = chainPattern.matcher(text);
while (m.find()) {
int chainStart = m.start(1);
if (isExcluded(chainStart))
continue;
// Skip import/package statements
if (isInImportOrPackage(chainStart))
continue;
// Check if the SECOND segment is a method call (followed by parentheses)
// If so, we skip this match entirely - markMethodCalls will handle it
int afterSecond = m.end(2);
int checkPos = afterSecond;
while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos)))
checkPos++;
if (checkPos < text.length() && text.charAt(checkPos) == '(') {
// This is "something.methodCall()" - skip, handled by markMethodCalls
continue;
}
// Build the full chain by continuing to match subsequent .identifier patterns
List<String> chainSegments = new ArrayList<>();
List<int[]> segmentPositions = new ArrayList<>(); // [start, end] for each segment
String firstSegment = m.group(1);
chainSegments.add(firstSegment);
segmentPositions.add(new int[]{m.start(1), m.end(1)});
String secondSegment = m.group(2);
chainSegments.add(secondSegment);
segmentPositions.add(new int[]{m.start(2), m.end(2)});
// Continue reading more segments
int pos = m.end(2);
while (pos < text.length()) {
// Skip whitespace
while (pos < text.length() && Character.isWhitespace(text.charAt(pos)))
pos++;
if (pos >= text.length() || text.charAt(pos) != '.')
break;
pos++; // Skip the dot
// Skip whitespace after dot
while (pos < text.length() && Character.isWhitespace(text.charAt(pos)))
pos++;
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos)))
break;
// Read next identifier
int identStart = pos;
while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos)))
pos++;
int identEnd = pos;
// Check if this is followed by parentheses (method call - stop)
int checkPosInner = pos;
while (checkPosInner < text.length() && Character.isWhitespace(text.charAt(checkPosInner)))
checkPosInner++;
if (checkPosInner < text.length() && text.charAt(checkPosInner) == '(') {
break; // This is a method call, not a field
}
chainSegments.add(text.substring(identStart, identEnd));
segmentPositions.add(new int[]{identStart, identEnd});
}
// Now resolve the chain type by type
// Start with resolving the first segment
TypeInfo currentType = null;
boolean firstIsThis = firstSegment.equals("this");
boolean firstIsSuper = firstSegment.equals("super");
boolean firstIsPrecededByDot = isPrecededByDot(chainStart);
// Determine the starting index for marking
// If the first segment is preceded by a dot, it's a field access (not a variable), so we should mark it
int startMarkingIndex = firstIsPrecededByDot ? 0 : 1;
if (firstIsThis) {
// For 'this', we don't have a class context to resolve, but we can mark subsequent fields
// as global fields if they exist in globalFields
currentType = null; // We'll use globalFields for the next segment
} else if (firstIsSuper) {
// For 'super', resolve to parent class type
ScriptTypeInfo enclosingType = findEnclosingScriptType(chainStart);
if (enclosingType != null && enclosingType.hasSuperClass()) {
currentType = enclosingType.getSuperClass();
if (currentType == null || !currentType.isResolved()) {
// Mark 'super' as error - parent class not resolved
marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1],
TokenType.UNDEFINED_VAR, TokenErrorMessage
.from("Cannot resolve parent class '" + enclosingType.getSuperClassName() + "'")
.clearOtherErrors()));
return; // Can't continue
}
} else {
// Mark 'super' as error - no parent class
String errorMsg = enclosingType == null ? "'super' can only be used within a class" : "Class '" + enclosingType.getSimpleName() + "' does not have a parent class";
marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1],
TokenType.UNDEFINED_VAR, TokenErrorMessage.from(errorMsg).clearOtherErrors()));
return; // Can't continue
}
marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1],
TokenType.IMPORT_KEYWORD, currentType));
} else {
// Try to resolve as a type first (class/interface/enum name)
TypeInfo typeCheck = resolveType(firstSegment);
if (typeCheck != null && typeCheck.isResolved()) {
// Static field access like SomeClass.field or scriptType.field
currentType = typeCheck;
} else if (firstIsPrecededByDot) {
// This is a field access on a previous result (e.g., getMinecraft().thePlayer)
// Resolve the receiver chain to get the type of what comes before the dot
TypeInfo receiverType = resolveReceiverChain(chainStart);
if (receiverType != null && receiverType.hasField(firstSegment)) {
FieldInfo varInfo = receiverType.getFieldInfo(firstSegment);
currentType = (varInfo != null) ? varInfo.getTypeInfo() : null;
} else {
currentType = null; // Can't resolve
}
} else {
// This is a variable starting the chain
FieldInfo varInfo = resolveVariable(firstSegment, chainStart);
if (varInfo != null) {
currentType = varInfo.getTypeInfo();
} else {
currentType = null;
}
}
}
// Mark the segments - start from index determined above
for (int i = startMarkingIndex; i < chainSegments.size(); i++) {
String segment = chainSegments.get(i);
int[] segPos = segmentPositions.get(i);
if (isExcluded(segPos[0]))
continue;
boolean isLastSegment = (i == chainSegments.size() - 1);
boolean isStaticAccess = false;
if (i - 1 >= 0) {
String prev = chainSegments.get(i - 1);
// Check if previous segment is a class/type name (indicating static access)
TypeInfo prevType = resolveType(prev);
if (prevType != null && prevType.isResolved()) {
isStaticAccess = true;
}
}
// Handle the first segment if we're marking it (i.e., when preceded by dot)
if (i == 0 && firstIsPrecededByDot) {
// This is a field access on a receiver (e.g., the "thePlayer" in "getMinecraft().thePlayer")
TypeInfo receiverType = resolveReceiverChain(chainStart);
if (receiverType != null && receiverType.hasField(segment)) {
FieldInfo fieldInfo = receiverType.getFieldInfo(segment);
FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], receiverType, fieldInfo, isLastSegment, isStaticAccess);
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo));
currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null;
} else {
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR));
currentType = null;
}
} else if (i == 1 && firstIsThis) {
// For "this.field", check if field exists in globalFields
if (globalFields.containsKey(segment)) {
FieldInfo fieldInfo = globalFields.get(segment);
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo));
currentType = fieldInfo.getTypeInfo();
} else {
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR));
currentType = null;
}
} else if (i == 1 && firstIsSuper) {
// For "super.field", currentType is already the parent class
// Search recursively through the inheritance hierarchy
boolean found = false;
FieldInfo fieldInfo = null;
if (currentType != null) {
if (currentType instanceof ScriptTypeInfo) {
found = ((ScriptTypeInfo) currentType).hasFieldInHierarchy(segment);
if (found) {
fieldInfo = ((ScriptTypeInfo) currentType).getFieldInfoInHierarchy(segment);
}
} else {
// For Java classes, hasField already checks inheritance
found = currentType.hasField(segment);
if (found) {
fieldInfo = currentType.getFieldInfo(segment);
}
}
}
if (found) {
FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], currentType,
fieldInfo, isLastSegment, false);
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo));
currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null;
} else {
String errorMsg = currentType != null ? "Field '" + segment + "' not found in parent class hierarchy starting from '" + currentType.getSimpleName() + "'" : "Cannot resolve field '" + segment + "'";
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR,
TokenErrorMessage.from(errorMsg).clearOtherErrors()));
currentType = null;
}
} else if (currentType != null && currentType.isResolved()) {
// Check if this type has this field
if (currentType.hasField(segment)) {
FieldInfo fieldInfo = currentType.getFieldInfo(segment);
// If accessing from a class name (isStaticAccessSeg) and field is not static, that's an error
if (isStaticAccess && fieldInfo != null && !fieldInfo.isStatic()) {
// Mark as error - trying to access non-static field from static context
TokenErrorMessage errorMsg = TokenErrorMessage
.from("Cannot access non-static field '" + segment + "' from static context '" + currentType.getSimpleName() + "'")
.clearOtherErrors();
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR, errorMsg));
currentType = null; // Can't continue resolving
} else {
// Valid access
FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1],
currentType, fieldInfo, isLastSegment, isStaticAccess);
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo));
// Update currentType for next segment
currentType = (fieldInfo != null && fieldInfo.getTypeInfo() != null) ? fieldInfo.getTypeInfo() : null;
}
} else {
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR));
currentType = null; // Can't continue resolving
}
} else {
// Can't resolve - mark as undefined
marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR));
currentType = null;
}
}
}
// Second pass: handle cases where the receiver is a method call or array access
// Example: getMinecraft().thePlayer -> initial chainPattern won't match because
// the left side isn't a simple identifier. Find ".identifier" occurrences where
// the char before the dot ends with ')' or ']' and resolve the receiver expression.
Pattern dotIdent = Pattern.compile("\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
Matcher md = dotIdent.matcher(text);
while (md.find()) {
int identStart = md.start(1);
int identEnd = md.end(1);
int dotPos = md.start();
// Skip if excluded or in import/package
if (isExcluded(identStart) || isInImportOrPackage(identStart))
continue;
// Find non-whitespace char before the dot
int before = dotPos - 1;
while (before >= 0 && Character.isWhitespace(text.charAt(before)))
before--;
if (before < 0)
continue;
char bc = text.charAt(before);
// Only handle when left side ends with ')' or ']' (method call or array access)
if (bc != ')' && bc != ']')
continue;
// Extract receiver bounds and resolve its type
int[] bounds = findReceiverBoundsBefore(dotPos);
if (bounds == null)
continue;
int recvStart = bounds[0];
int recvEnd = bounds[1];
if (isExcluded(recvStart) || isInImportOrPackage(recvStart))
continue;
String receiverExpr = text.substring(recvStart, recvEnd).trim();
if (receiverExpr.isEmpty())
continue;
TypeInfo receiverType = resolveExpressionType(receiverExpr, recvStart);
// Now mark the first field after the dot and continue chaining
String firstField = md.group(1);
FieldInfo fInfo = null;
if (receiverType != null && receiverType.hasField(firstField)) {
fInfo = receiverType.getFieldInfo(firstField);
// Check if there are more segments after this one
boolean hasMoreSegments = false;
int checkPos = identEnd;
while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos)))
checkPos++;
if (checkPos < text.length() && text.charAt(checkPos) == '.') {
hasMoreSegments = true;
}
boolean isStaticAccessFirst = false;
if (receiverType != null) {
String recvName = receiverType.getSimpleName();
if (recvName != null && !recvName.isEmpty() && Character.isUpperCase(recvName.charAt(0)))
isStaticAccessFirst = true;
}
FieldAccessInfo accessInfoFirst = createFieldAccessInfo(firstField, identStart, identEnd, receiverType, fInfo, !hasMoreSegments, isStaticAccessFirst);
marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, accessInfoFirst));
} else {
marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR));
}
// Attempt to continue the chain after this identifier
int pos = identEnd;
TypeInfo currentType = (fInfo != null) ? fInfo.getTypeInfo() : null;
while (pos < text.length()) {
// Skip whitespace
while (pos < text.length() && Character.isWhitespace(text.charAt(pos)))
pos++;
if (pos >= text.length() || text.charAt(pos) != '.')
break;
pos++; // skip dot
// Skip whitespace
while (pos < text.length() && Character.isWhitespace(text.charAt(pos)))
pos++;
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos)))
break;
int nStart = pos;
while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos)))
pos++;
int nEnd = pos;
String seg = text.substring(nStart, nEnd);
// Check if next segment is a method call (followed by '(') -> stop
int checkPosInner = nEnd;
while (checkPosInner < text.length() && Character.isWhitespace(text.charAt(checkPosInner)))
checkPosInner++;
if (checkPosInner < text.length() && text.charAt(checkPosInner) == '(')
break;
if (isExcluded(nStart))
break;
// Check if this is the last segment (no more dots after)
boolean isLastSegment = true;
int checkNext = nEnd;
while (checkNext < text.length() && Character.isWhitespace(text.charAt(checkNext)))
checkNext++;
if (checkNext < text.length() && text.charAt(checkNext) == '.') {
isLastSegment = false;
}
if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) {
FieldInfo segInfo = currentType.getFieldInfo(seg);
boolean isStaticAccessSeg2 = false;
if (currentType != null) {
String tn = currentType.getSimpleName();
if (tn != null && !tn.isEmpty() && Character.isUpperCase(tn.charAt(0)))
isStaticAccessSeg2 = true;
}
FieldAccessInfo accessInfoSeg = createFieldAccessInfo(seg, nStart, nEnd, currentType, segInfo, isLastSegment, isStaticAccessSeg2);
marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, accessInfoSeg));
currentType = (segInfo != null) ? segInfo.getTypeInfo() : null;
} else {
marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR));
currentType = null;
}
}
}
FieldChainMarker marker = new FieldChainMarker(this, text);
marker.markChainedFieldAccesses(marks);
}
/**
* Parse assignment statements (reassignments, not declarations) and validate type compatibility.
@@ -4913,7 +4543,7 @@ public class ScriptDocument {
* Helper to create and validate a FieldAccessInfo. Does NOT add marks or register the info —
* callers should add to `fieldAccesses` and `marks` as appropriate so marking logic remains in-place.
*/
private FieldAccessInfo createFieldAccessInfo(String name, int start, int end,
FieldAccessInfo createFieldAccessInfo(String name, int start, int end,
TypeInfo receiverType, FieldInfo fieldInfo,
boolean isLastSegment, boolean isStaticAccess) {
FieldAccessInfo accessInfo = new FieldAccessInfo(name, start, end, receiverType, fieldInfo, isStaticAccess);
@@ -4928,7 +4558,7 @@ public class ScriptDocument {
return accessInfo;
}
private FieldInfo resolveVariable(String name, int position) {
FieldInfo resolveVariable(String name, int position) {
// Find containing method
MethodInfo containingMethod = findMethodAtPosition(position);
@@ -4962,7 +4592,7 @@ public class ScriptDocument {
return null;
}
private boolean isPrecededByDot(int position) {
boolean isPrecededByDot(int position) {
if (position <= 0)
return false;
int i = position - 1;
@@ -4971,22 +4601,7 @@ public class ScriptDocument {
return i >= 0 && text.charAt(i) == '.';
}
private boolean isFollowedByParen(int position) {
int i = position;
while (i < text.length() && Character.isWhitespace(text.charAt(i)))
i++;
return i < text.length() && text.charAt(i) == '(';
}
private boolean isFollowedByDot(int position) {
// Start checking after the given position (e.g. after a closing paren)
int i = position + 1;
while (i < text.length() && Character.isWhitespace(text.charAt(i)))
i++;
return i < text.length() && text.charAt(i) == '.';
}
private boolean isInImportOrPackage(int position) {
boolean isInImportOrPackage(int position) {
if (position < 0 || position >= text.length())
return false;