diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java new file mode 100644 index 000000000..8e28a4021 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java @@ -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 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 segments = new ArrayList<>(); + final List 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 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)); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java index 5c23db663..95b3b099f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -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 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 chainSegments = new ArrayList<>(); - List 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;