Handled inner classes i.e. Outer.Inner.Innermost

This commit is contained in:
bigguy345 committed 2026-03-09 09:41:18 +02:00
1 parent 18c15b13bc
commit f015eef645
6 files changed
+443 -123

No files matched your search

@@ -332,6 +332,31 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
}
}
// Add inner classes of the enclosing type as type suggestions
for (ScriptTypeInfo inner : enclosingType.getInnerClasses()) {
items.add(AutocompleteItem.fromType(inner));
}
// Walk up the outerClass chain to include outer class members and sibling types.
// Inside a nested class, users can access outer class fields/methods/sibling types.
ScriptTypeInfo outer = enclosingType.getOuterClass();
while (outer != null) {
for (ScriptTypeInfo sibling : outer.getInnerClasses()) {
if (sibling != enclosingType) {
items.add(AutocompleteItem.fromType(sibling));
}
}
for (FieldInfo field : outer.getFields().values()) {
items.add(AutocompleteItem.fromField(field));
}
for (List<MethodInfo> overloads : outer.getMethods().values()) {
for (MethodInfo method : overloads) {
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
}
}
outer = outer.getOuterClass();
}
}
// Add script-defined methods
@@ -435,15 +460,11 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
/**
* Find the enclosing script type at a position.
* This is a workaround since findEnclosingScriptType is package-private.
* Delegates to ScriptDocument's recursive descent implementation
* which correctly returns the innermost enclosing type for nested classes.
*/
protected ScriptTypeInfo findEnclosingType(int position) {
for (ScriptTypeInfo type : document.getScriptTypesMap().values()) {
if (type.containsPosition(position)) {
return type;
}
}
return null;
return document.findEnclosingScriptType(position);
}
/**
@@ -368,6 +368,33 @@ public class FieldChainMarker {
TypeInfo currentType = ctx.currentType;
if (!currentType.hasField(segment)) {
// Field not found — check if the segment names an inner class.
// This handles chains like Outer.Inner where Inner is a nested type, not a field.
if (currentType instanceof ScriptTypeInfo) {
ScriptTypeInfo innerClass = ((ScriptTypeInfo) currentType).getInnerClass(segment);
if (innerClass != null) {
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], innerClass.getTokenType(), innerClass),
innerClass
);
}
}
// Also check Java inner classes via reflection (e.g., Map.Entry)
if (currentType.getJavaClass() != null) {
try {
for (Class<?> nested : currentType.getJavaClass().getDeclaredClasses()) {
if (java.lang.reflect.Modifier.isPublic(nested.getModifiers())
&& nested.getSimpleName().equals(segment)) {
TypeInfo nestedType = TypeInfo.fromClass(nested);
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], nestedType.getTokenType(), nestedType),
nestedType
);
}
}
} catch (SecurityException ignored) { }
}
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
null
@@ -87,7 +87,7 @@ public class ScriptDocument {
// 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
@@ -109,7 +109,14 @@ public class ScriptDocument {
private Map<String, ImportData> importsBySimpleName = new HashMap<>();
// Script-defined types (classes, interfaces, enums defined in the script)
// PRIMARY: simple name → ScriptTypeInfo (backward-compatible, top-level only)
private final Map<String, ScriptTypeInfo> scriptTypes = new HashMap<>();
// SECONDARY: dollar-separated full name → ScriptTypeInfo (all types including nested)
// Enables O(1) lookup by qualified name for any nesting depth (e.g., "Outer$Middle$Inner")
private final Map<String, ScriptTypeInfo> scriptTypesByFullName = new HashMap<>();
// TERTIARY: dot-separated display name → ScriptTypeInfo (for resolveType("Outer.Inner"))
// Populated alongside scriptTypesByFullName during parsing; matches user-written syntax
private final Map<String, ScriptTypeInfo> scriptTypesByDotName = new HashMap<>();
private final List<MethodInfo> methods = new ArrayList<>();
private final Map<String, FieldInfo> globalFields = new HashMap<>();
@@ -361,6 +368,8 @@ public class ScriptDocument {
methodLocals.clear();
topLevelLocals.clear();
scriptTypes.clear();
scriptTypesByFullName.clear();
scriptTypesByDotName.clear();
innerScopes.clear();
methodCalls.clear();
externalFieldAssignments.clear();
@@ -488,8 +497,10 @@ public class ScriptDocument {
return false;
}
/** Inclusive version for precision with end offset
Check GuiScriptTextArea#handleCharacterInput */
/**
* Like isExcluded but uses inclusive end bound (position <= range end).
* Used by auto-pair logic where the cursor sits right at a string boundary.
*/
public boolean isExcludedInclusive(int position) {
for (int[] range : excludedRanges) {
if (position >= range[0] && position <= range[1]) {
@@ -498,7 +509,21 @@ public class ScriptDocument {
}
return false;
}
/**
* Check if a position falls inside one of the type's inner class bodies.
* Used to prevent field/constructor declarations inside inner classes
* from being misattributed to the outer class during member parsing.
*/
private boolean isInsideNestedType(int position, ScriptTypeInfo type) {
for (ScriptTypeInfo inner : type.getInnerClasses()) {
if (inner.containsPosition(position)) {
return true;
}
}
return false;
}
/**
* Get the list of excluded ranges (comments, strings, etc.).
* Used by AutocompleteManager to skip over excluded regions.
@@ -719,89 +744,147 @@ public class ScriptDocument {
}
/**
* Parse class, interface, and enum declarations defined in the script.
* Creates ScriptTypeInfo instances and stores them for later resolution.
* Java only - JavaScript doesn't use class declarations in the same way.
* Intermediate record for type declarations discovered in Pass 1 of parseScriptTypes().
* Holds raw data before hierarchy relationships are established in Pass 2.
*/
private static class RawTypeDeclaration {
final String name;
final TypeInfo.Kind kind;
final int declOffset;
final int bodyStart;
final int bodyEnd;
final int modifiers;
final String extendsClause;
final String implementsClause;
final JSDocInfo jsDoc;
RawTypeDeclaration(String name, TypeInfo.Kind kind, int declOffset, int bodyStart, int bodyEnd,
int modifiers, String extendsClause, String implementsClause, JSDocInfo jsDoc) {
this.name = name;
this.kind = kind;
this.declOffset = declOffset;
this.bodyStart = bodyStart;
this.bodyEnd = bodyEnd;
this.modifiers = modifiers;
this.extendsClause = extendsClause;
this.implementsClause = implementsClause;
this.jsDoc = jsDoc;
}
}
/**
* Parse class, interface, and enum declarations using a two-pass strategy.
*
* Pass 1: Regex scan discovers all type declarations with their body spans.
* Pass 2: Sort by bodyStart, then use a stack to determine parent-child nesting.
* The stack invariant is: top = innermost enclosing type for the current position.
* This correctly handles arbitrary nesting depth in O(n) time.
*/
private void parseScriptTypes() {
// Pattern: [modifiers] (class|interface|enum) ClassName [optional ()] [extends Parent] [implements I1, I2...] { ... }
// Matches optional modifiers (public, private, static, final, abstract) before class/interface/enum
// Also allows optional () after the class name (common mistake)
// Groups: 1=class|interface|enum, 2=TypeName, 3=extends clause (optional), 4=implements clause (optional)
// ========== PASS 1: Discover all type declarations (flat list) ==========
Pattern typeDecl = Pattern.compile(
"(?:(?:public|private|protected|static|final|abstract)\\s+)*(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:\\(\\))?\\s*(?:extends\\s+([A-Za-z_][a-zA-Z0-9_.]*))?\\s*(?:implements\\s+([A-Za-z_][a-zA-Z0-9_.,\\s]*))?\\s*\\{");
List<RawTypeDeclaration> rawDeclarations = new ArrayList<>();
Matcher m = typeDecl.matcher(text);
while (m.find()) {
if (isExcluded(m.start()))
continue;
String kindStr = m.group(1);
String typeName = m.group(2);
String extendsClause = m.group(3); // e.g., "ParentClass" or null
String implementsClause = m.group(4); // e.g., "Interface1, Interface2" or null
String extendsClause = m.group(3);
String implementsClause = m.group(4);
int bodyStart = text.indexOf('{', m.start());
int bodyEnd = findMatchingBrace(bodyStart);
if (bodyEnd < 0) {
bodyEnd = text.length();
}
TypeInfo.Kind kind;
switch (kindStr) {
case "interface": kind = TypeInfo.Kind.INTERFACE; break;
case "enum": kind = TypeInfo.Kind.ENUM; break;
default: kind = TypeInfo.Kind.CLASS; break;
}
// Extract modifiers from the matched text
String fullMatch = text.substring(m.start(), m.end());
int modifiers = parseModifiers(fullMatch);
JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, m.start());
ScriptTypeInfo scriptType = ScriptTypeInfo.create(
typeName, kind, m.start(), bodyStart, bodyEnd, modifiers);
if (jsDoc != null) {
scriptType.setJSDocInfo(jsDoc);
rawDeclarations.add(new RawTypeDeclaration(
typeName, kind, m.start(), bodyStart, bodyEnd,
modifiers, extendsClause, implementsClause, jsDoc));
}
// ========== PASS 2: Build hierarchy using stack-based containment ==========
// Sort by bodyStart ascending so outer types are processed before inner types
rawDeclarations.sort(Comparator.comparingInt(r -> r.bodyStart));
// Stack tracks the nesting chain: top = current innermost enclosing type
Deque<ScriptTypeInfo> parentStack = new ArrayDeque<>();
for (RawTypeDeclaration raw : rawDeclarations) {
// Pop any parent whose body has ended before this declaration starts.
// This handles sibling types at the same level: when we reach InnerB,
// InnerA (whose bodyEnd <= InnerB.bodyStart) gets popped, leaving Outer on top.
while (!parentStack.isEmpty() && parentStack.peek().getBodyEnd() <= raw.bodyStart) {
parentStack.pop();
}
// Store the script type first so it can be resolved by other types
scriptTypes.put(typeName, scriptType);
ScriptTypeInfo scriptType;
if (parentStack.isEmpty()) {
// Top-level type: no enclosing parent
scriptType = ScriptTypeInfo.create(
raw.name, raw.kind, raw.declOffset, raw.bodyStart, raw.bodyEnd, raw.modifiers);
scriptTypes.put(raw.name, scriptType);
} else {
// Inner type: parent is top of stack. createInner sets outerClass link
// and adds this type to the parent's innerClasses list.
ScriptTypeInfo parent = parentStack.peek();
scriptType = ScriptTypeInfo.createInner(
raw.name, raw.kind, parent, raw.declOffset, raw.bodyStart, raw.bodyEnd, raw.modifiers);
}
// Process extends clause (parent class)
if (extendsClause != null && !extendsClause.trim().isEmpty()) {
String parentName = extendsClause.trim();
TypeInfo parentType = resolveType(parentName);
if (raw.jsDoc != null) {
scriptType.setJSDocInfo(raw.jsDoc);
}
// Register in all lookup maps for O(1) access by any naming convention
scriptTypesByFullName.put(scriptType.getFullName(), scriptType);
scriptTypesByDotName.put(scriptType.getDotSeparatedName(), scriptType);
// Push onto stack — this type can contain further nested types
parentStack.push(scriptType);
// Resolve extends clause (parent class inheritance)
if (raw.extendsClause != null && !raw.extendsClause.trim().isEmpty()) {
String parentName = raw.extendsClause.trim();
TypeInfo parentType = resolveType(parentName, raw.declOffset);
if (parentType == null) {
// Create unresolved type info for display purposes
parentType = TypeInfo.unresolved(parentName, parentName);
}
scriptType.setSuperClass(parentType, parentName);
}
// Process implements clause (interfaces)
if (implementsClause != null && !implementsClause.trim().isEmpty()) {
String[] interfaceNames = implementsClause.split(",");
// Resolve implements clause (interfaces)
if (raw.implementsClause != null && !raw.implementsClause.trim().isEmpty()) {
String[] interfaceNames = raw.implementsClause.split(",");
for (String ifaceName : interfaceNames) {
String trimmedName = ifaceName.trim();
if (trimmedName.isEmpty())
continue;
TypeInfo ifaceType = resolveType(trimmedName);
TypeInfo ifaceType = resolveType(trimmedName, raw.declOffset);
if (ifaceType == null) {
// Create unresolved type info for display purposes
ifaceType = TypeInfo.unresolved(trimmedName, trimmedName);
}
scriptType.addImplementedInterface(ifaceType, trimmedName);
}
}
// Parse fields and methods inside this type AFTER adding the scriptType globally, so its members can reference it
// Parse fields and methods inside this type
parseScriptTypeMembers(scriptType);
}
}
@@ -825,6 +908,8 @@ public class ScriptDocument {
while (cm.find()) {
int absPos = bodyStart + 1 + cm.start();
if (isExcluded(absPos)) continue;
// Skip constructors found inside a nested type's body — they belong to the inner class
if (isInsideNestedType(absPos, scriptType)) continue;
String paramList = cm.group(1);
@@ -1039,7 +1124,8 @@ public class ScriptDocument {
String returnType = m.group(1);
if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum") || returnType.equals("new")) {
if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum") || returnType.equals("new")
|| TypeResolver.isModifier(returnType)) {
continue;
}
@@ -1063,12 +1149,9 @@ public class ScriptDocument {
int nameOffset = m.start(2);
int fullDeclOffset = findFullDeclarationStart(m.start(1), text);
ScriptTypeInfo scriptType = null;
for (ScriptTypeInfo type : scriptTypes.values())
if (type.containsPosition(bodyStart)) {
scriptType = type;
break;
}
// Use findEnclosingScriptType (recursive descent) so methods declared
// inside inner classes are attributed to the innermost containing type.
ScriptTypeInfo scriptType = findEnclosingScriptType(bodyStart);
// Parse parameters with their actual positions
List<FieldInfo> params = parseParametersWithPositions(paramList, m.start(3));
@@ -1076,7 +1159,7 @@ public class ScriptDocument {
MethodInfo methodInfo = MethodInfo.declaration(
methodName,
scriptType,
resolveType(returnType),
resolveType(returnType, m.start(1)),
params,
fullDeclOffset,
typeOffset,
@@ -1252,7 +1335,7 @@ public class ScriptDocument {
String paramName = m.group(2);
boolean isVarArg = typeName.endsWith("...");
if (isVarArg) typeName = typeName.substring(0, typeName.length() - 3);
TypeInfo typeInfo = resolveType(typeName);
TypeInfo typeInfo = resolveType(typeName, paramListStart);
// Store the absolute position of the parameter name
int paramNameStart = paramListStart + m.start(2);
FieldInfo fieldInfo = FieldInfo.parameter(paramName, typeInfo, paramNameStart, null);
@@ -1704,7 +1787,7 @@ public class ScriptDocument {
if (enclosingParen1 >= 0) continue;
int greedyModifiers = parseModifiers(realTypeName);
TypeInfo greedyTypeInfo = resolveType(realTypeName);
TypeInfo greedyTypeInfo = resolveType(realTypeName, firstVarAbsPos);
// Register the first variable (extracted from the greedy type group)
FieldInfo firstField = FieldInfo.localField(firstVar, greedyTypeInfo, firstVarAbsPos,
@@ -1789,7 +1872,8 @@ public class ScriptDocument {
int rhsStart = bodyStart + m.end();
typeInfo = inferTypeFromExpression(rhsStart);
} else {
typeInfo = resolveType(typeName);
// Use position-aware resolution so inner class type names resolve in scope
typeInfo = resolveType(typeName, bodyStart + m.start(2));
}
int initStart = -1, initEnd = -1;
@@ -1938,7 +2022,7 @@ public class ScriptDocument {
String greedyTypeName = stripModifiers(realTypeNameRaw);
int greedyModifiers = parseModifiers(realTypeNameRaw);
TypeInfo greedyVarType = resolveType(greedyTypeName);
TypeInfo greedyVarType = resolveType(greedyTypeName, firstVarAbsPos);
// Register the first variable
FieldInfo firstField = FieldInfo.localField(firstVar, greedyVarType, firstVarAbsPos,
@@ -2359,7 +2443,7 @@ public class ScriptDocument {
if (position > typeStart) {
String typeName = text.substring(typeStart, position);
return resolveType(typeName);
return resolveType(typeName, typeStart);
}
return null;
}
@@ -2446,7 +2530,7 @@ public class ScriptDocument {
currentType = resolveThisType(identStart);
}
TypeInfo typeCheck = resolveType(firstIdent);
TypeInfo typeCheck = resolveType(firstIdent, identStart);
if (currentType == null && typeCheck != null && typeCheck.isResolved()) {
// Static access like Event.player or scriptType.field
currentType = typeCheck;
@@ -3184,12 +3268,10 @@ public class ScriptDocument {
int greedyModifiers = parseModifiers(realTypeNameRaw);
String greedyTypeName = stripModifiers(realTypeNameRaw);
ScriptTypeInfo greedyContainingType = null;
for (ScriptTypeInfo scriptType : scriptTypes.values())
if (scriptType.containsPosition(firstVarAbsPos)) {
greedyContainingType = scriptType;
break;
}
// Use findEnclosingScriptType (recursive descent) so inner class fields
// are attributed to the innermost containing type, not just the first
// HashMap match (which could be the outer class).
ScriptTypeInfo greedyContainingType = findEnclosingScriptType(firstVarAbsPos);
boolean greedyInsideMethod = false;
if (greedyContainingType != null && isInsideNestedMethod(firstVarAbsPos,
@@ -3203,7 +3285,7 @@ public class ScriptDocument {
if (greedyInsideMethod) continue;
String greedyDocumentation = extractDocumentationBefore(m.start());
TypeInfo greedyTypeInfo = resolveType(greedyTypeName);
TypeInfo greedyTypeInfo = resolveType(greedyTypeName, firstVarAbsPos);
// Register the first variable (no initializer since the regex consumed it into the type group)
FieldInfo firstField = FieldInfo.globalField(firstVar, greedyTypeInfo, firstVarAbsPos,
@@ -3261,12 +3343,10 @@ public class ScriptDocument {
int modifiers = parseModifiers(typeNameRaw);
String typeName = stripModifiers(typeNameRaw);
ScriptTypeInfo containingScriptType = null;
for (ScriptTypeInfo scriptType : scriptTypes.values())
if (scriptType.containsPosition(position)) {
containingScriptType = scriptType;
break;
}
// Use findEnclosingScriptType (recursive descent) so inner class fields
// are attributed to the innermost containing type, not just the first
// HashMap match (which could be the outer class).
ScriptTypeInfo containingScriptType = findEnclosingScriptType(position);
// Check if inside a method - if so, it's a local, not global
boolean insideMethod = false;
@@ -3308,7 +3388,8 @@ public class ScriptDocument {
}
}
TypeInfo typeInfo = resolveType(typeName);
// Use position-aware resolution so inner class type names resolve correctly
TypeInfo typeInfo = resolveType(typeName, position);
FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position, documentation, initStart, initEnd, modifiers);
if (containingScriptType != null) {
@@ -3595,6 +3676,53 @@ public class ScriptDocument {
return resolveTypeAndTrackUsage(typeName);
}
/**
* Position-aware type resolution overload.
* Resolves a type name with positional context so that an unqualified inner class name
* (e.g., just "Inner") resolves correctly when the cursor/position is inside the body
* of an enclosing type that declares that inner class.
*
* Resolution order:
* 1. Standard resolveType(typeName) — handles simple names, dot-names, imports, primitives
* 2. If unresolved and the name is a simple identifier (no dots), check the enclosing
* type at 'position' for an inner class with that name.
* This walks the nesting hierarchy upward so "Inner" resolves even from deeply nested code.
*
* @param typeName The type name to resolve (simple or dot-separated)
* @param position The absolute offset in the script text for scope context
* @return The resolved TypeInfo, or an unresolved TypeInfo if not found
*/
public TypeInfo resolveType(String typeName, int position) {
// First: delegate to the standard (positionless) resolver
TypeInfo resolved = resolveType(typeName);
// If already resolved, no need for positional fallback
if (resolved != null && resolved.isResolved()) {
return resolved;
}
// Positional fallback: only applies to simple names (no dots) that could be
// inner classes visible from the current scope. Dot-separated names like
// "Outer.Inner" are already handled by scriptTypesByDotName in resolveType().
if (typeName != null && !typeName.contains(".")) {
// Walk the enclosing type hierarchy upward from the innermost type at this position.
// At each level, check if there's an inner class matching the name.
// This mirrors Java's scoping rules where inner classes shadow outer-scope types.
ScriptTypeInfo enclosing = findEnclosingScriptType(position);
while (enclosing != null) {
ScriptTypeInfo inner = enclosing.getInnerClass(typeName);
if (inner != null) {
return inner;
}
// Move up to the parent type — inner classes of outer types are also in scope
enclosing = enclosing.getOuterClass();
}
}
// Return whatever resolveType() returned (possibly unresolved)
return resolved;
}
/**
* Resolve a type and track the import usage for unused import detection.
@@ -3629,7 +3757,12 @@ public class ScriptDocument {
else if (!baseName.contains(".") && scriptTypes.containsKey(baseName)) {
resolved = scriptTypes.get(baseName);
}
// Fully-qualified
// Script-defined inner types (dot-separated, e.g., "Outer.Inner")
// Checked before Java full-name resolution so script types shadow Java types
else if (baseName.contains(".") && scriptTypesByDotName.containsKey(baseName)) {
resolved = scriptTypesByDotName.get(baseName);
}
// Fully-qualified Java types
else if (baseName.contains(".")) {
resolved = typeResolver.resolveFullName(baseName);
}
@@ -4058,7 +4191,7 @@ public class ScriptDocument {
// Handle qualified names (a.b.ClassName)
if (target.contains(".")) {
TypeInfo typeInfo = resolveType(target);
TypeInfo typeInfo = resolveType(target, position);
if (typeInfo != null && typeInfo.isResolved()) {
return typeInfo;
}
@@ -4073,7 +4206,7 @@ public class ScriptDocument {
}
// Try as type name (class reference like String::valueOf)
TypeInfo typeInfo = resolveType(target);
TypeInfo typeInfo = resolveType(target, position);
if (typeInfo != null && typeInfo.isResolved()) {
return typeInfo;
}
@@ -4627,7 +4760,22 @@ public class ScriptDocument {
nameType = TokenType.CLASS_DECL;
}
String typeName = m.group(2);
marks.add(new ScriptLine.Mark(m.start(2), m.end(2), nameType, scriptTypes.get(typeName)));
// Look up the ScriptTypeInfo: try top-level first, then search all types
// by simple name + declaration range (declarationOffset..bodyEnd) to handle inner classes.
// Cannot use containsPosition() here because the class NAME sits before the opening '{',
// i.e., in [declarationOffset, bodyStart) — outside the body range checked by containsPosition.
ScriptTypeInfo typeInfo = scriptTypes.get(typeName);
if (typeInfo == null) {
for (ScriptTypeInfo candidate : scriptTypesByFullName.values()) {
if (candidate.getSimpleName().equals(typeName)
&& m.start(2) >= candidate.getDeclarationOffset()
&& m.start(2) < candidate.getBodyEnd()) {
typeInfo = candidate;
break;
}
}
}
marks.add(new ScriptLine.Mark(m.start(2), m.end(2), nameType, typeInfo));
// Mark extends clause
if (m.group(3) != null) {
@@ -4650,7 +4798,8 @@ public class ScriptDocument {
* Adds marks for all enum constants in script-defined enums.
*/
private void markEnumConstants(List<ScriptLine.Mark> marks) {
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
// Iterate all types (including inner) so enum constants in nested enums are marked
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
if (!scriptType.isEnum())
continue;
@@ -4677,8 +4826,8 @@ public class ScriptDocument {
return;
// Resolve the parent type
TypeInfo parentType = resolveType(trimmedName);
// Resolve the parent type with position context for inner class names
TypeInfo parentType = resolveType(trimmedName, clauseStart);
TokenType tokenType;
if (parentType != null && parentType.isResolved()) {
@@ -4725,8 +4874,8 @@ public class ScriptDocument {
}
int actualStart = currentPos + leadingSpaces;
// Resolve the interface type
TypeInfo ifaceType = resolveType(trimmedName);
// Resolve the interface type with position context for inner class names
TypeInfo ifaceType = resolveType(trimmedName, actualStart);
TokenType tokenType;
if (ifaceType != null && ifaceType.isResolved()) {
@@ -5111,8 +5260,8 @@ public class ScriptDocument {
}
}
// Also check methods inside script types
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
// Also check methods inside script types (including inner classes)
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
for (MethodInfo method : scriptType.getAllMethodsFlat()) {
if (!method.isDeclaration())
continue;
@@ -5898,7 +6047,7 @@ public class ScriptDocument {
: "";
return TypeInfo.arrayOf(unifyJsArrayElementType(argsText, position));
}
TypeInfo baseType = resolveType(typeName);
TypeInfo baseType = resolveType(typeName, position);
if (baseType == null) return null;
String rest = expr.substring(newMatcher.end());
int dims = 0;
@@ -5949,7 +6098,7 @@ public class ScriptDocument {
}
} else {
// Check if first segment is a type name for static access
TypeInfo typeCheck = resolveType(first.name);
TypeInfo typeCheck = resolveType(first.name, position);
if (typeCheck != null && typeCheck.isResolved()) {
currentType = typeCheck;
}
@@ -6397,6 +6546,27 @@ public class ScriptDocument {
if (hasField) {
return (fieldInfo != null) ? fieldInfo.getTypeInfo() : null;
}
// Field not found — check if the segment names an inner class.
// This handles chains like Outer.Inner where Inner is a nested type, not a field.
if (currentType instanceof ScriptTypeInfo) {
ScriptTypeInfo innerClass = ((ScriptTypeInfo) currentType).getInnerClass(segment.name);
if (innerClass != null) {
return innerClass;
}
}
// Also check Java inner classes via reflection (e.g., Map.Entry)
if (currentType.getJavaClass() != null) {
try {
for (Class<?> nested : currentType.getJavaClass().getDeclaredClasses()) {
if (java.lang.reflect.Modifier.isPublic(nested.getModifiers())
&& nested.getSimpleName().equals(segment.name)) {
return TypeInfo.fromClass(nested);
}
}
} catch (SecurityException ignored) { }
}
return null;
}
}
@@ -6676,7 +6846,7 @@ public class ScriptDocument {
}
if (name.length() > 0) {
TypeInfo typeCheck = resolveType(name);
TypeInfo typeCheck = resolveType(name, position);
if (typeCheck != null && typeCheck.isResolved()) {
return typeCheck;
}
@@ -6845,7 +7015,7 @@ public class ScriptDocument {
currentType = resolveThisType(position);
} else {
// Check if first segment is a type name
TypeInfo typeCheck = resolveType(first.name);
TypeInfo typeCheck = resolveType(first.name, position);
if (typeCheck != null && typeCheck.isResolved()) {
currentType = typeCheck;
}
@@ -7048,16 +7218,31 @@ public class ScriptDocument {
}
/**
* Find the script-defined type that contains the given position.
* Used for resolving 'this' references.
* Find the innermost script-defined type that contains the given position.
* Uses recursive descent: starts from top-level types in scriptTypes, then drills
* into inner classes. This is O(depth) rather than O(total_types).
*/
ScriptTypeInfo findEnclosingScriptType(int position) {
for (ScriptTypeInfo type : scriptTypes.values()) {
if (type.containsPosition(position)) {
return type;
public ScriptTypeInfo findEnclosingScriptType(int position) {
for (ScriptTypeInfo type:scriptTypes.values()) {
if (type.containsPosition(position)) {
return findInnermostType(type,position);
}
}
return null;
}
/**
* Recursively descend into inner classes to find the innermost type containing the position.
* At each level, if an inner class contains the position, recurse into it.
* If no inner class matches, the current parent is the innermost enclosing type.
*/
private ScriptTypeInfo findInnermostType(ScriptTypeInfo parent, int position) {
for (ScriptTypeInfo inner : parent.getInnerClasses()) {
if (inner.containsPosition(position)) {
return findInnermostType(inner, position);
}
}
return null;
return parent;
}
/**
@@ -7416,7 +7601,7 @@ public class ScriptDocument {
}
// Check if the method's declaration position is inside any script type's body
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
if (scriptType.containsPosition(declPos)) {
return true;
}
@@ -7456,6 +7641,7 @@ public class ScriptDocument {
Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b");
Matcher m = identifier.matcher(text);
identifierLoop:
while (m.find()) {
String name = m.group(1);
int position = m.start(1);
@@ -7569,8 +7755,11 @@ public class ScriptDocument {
}
}
// Check other script type fields (only if position is within that type's boundaries)
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
// Check other script type fields (only if position is within that type's boundaries).
// Use continue identifierLoop to exit the outer while when a match is found —
// a plain 'continue' would only skip to the next scriptType, causing fall-through
// to the UNDEFINED_VAR mark even after a GLOBAL_FIELD mark was already added.
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
if (scriptType.hasField(name)) {
// Only check if position is within this script type's class body
if (position >= scriptType.getBodyStart() && position <= scriptType.getBodyEnd()) {
@@ -7581,7 +7770,7 @@ public class ScriptDocument {
if (fieldInfo.isVisibleAt(position)) {
Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, callInfo) : fieldInfo;
marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata));
continue;
continue identifierLoop;
}
}
}
@@ -7675,8 +7864,8 @@ public class ScriptDocument {
field.clearAssignments();
}
}
// Also clear assignments in script type fields
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
// Also clear assignments in script type fields (including inner classes)
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
for (FieldInfo field : scriptType.getFields().values()) {
field.clearAssignments();
}
@@ -8121,7 +8310,7 @@ public class ScriptDocument {
* Also validates that all interface methods are implemented and constructors match.
*/
private void detectMethodInheritance() {
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
scriptType.clearErrors(); // Clear previous errors
detectMethodInheritanceForType(scriptType);
scriptType.validate(); // Validate after detecting inheritance
@@ -8353,8 +8542,8 @@ public class ScriptDocument {
// Skip in import/package statements
if (isInImportOrPackage(typeStart)) continue;
// Resolve the type
TypeInfo info = resolveType(typeName);
// Resolve the type with position context for inner class names
TypeInfo info = resolveType(typeName, typeStart);
if (info != null && info.isResolved()) {
// Mark the type with its appropriate color
marks.add(new ScriptLine.Mark(typeStart, typeEnd, info.getTokenType(), info));
@@ -8420,8 +8609,9 @@ public class ScriptDocument {
}
}
// Try to resolve the class
TypeInfo info = resolveType(className);
// Use position-aware resolution so inner class names (e.g., OrbBuilder inside Outer)
// resolve via the positional fallback that walks enclosing.getInnerClass(name).
TypeInfo info = resolveType(className, start);
if (info != null && info.isResolved()) {
marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), info));
} else {
@@ -8447,8 +8637,30 @@ public class ScriptDocument {
if (isInImportOrPackage(start))
continue;
// Try to resolve the class
TypeInfo info = resolveType(className);
// When the class name is preceded by a dot (e.g., OrbBuilder in AbilityLine.OrbBuilder),
// walk backwards to reconstruct the full qualified name and resolve via scriptTypesByDotName.
// resolveType(simpleNameOnly) fails outside the enclosing class body; the qualified name always works.
// We also track outerQualStart so we can detect 'new' before the full qualified name.
String resolveKey = className;
int outerQualStart = start;
if (isPrecededByDot(start)) {
int pos = start - 1;
while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) pos--;
while (pos >= 0 && text.charAt(pos) == '.') {
pos--;
while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) pos--;
int identEnd = pos + 1;
while (pos > 0 && (Character.isLetterOrDigit(text.charAt(pos - 1)) || text.charAt(pos - 1) == '_')) pos--;
resolveKey = text.substring(pos, identEnd) + "." + resolveKey;
outerQualStart = pos;
pos--;
while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) pos--;
}
}
TypeInfo info = resolveType(resolveKey, start);
if (info == null || !info.isResolved()) {
info = resolveType(className, start);
}
boolean isClassTypeInfo = false;
ClassTypeInfo classRef = null;
FieldInfo varInfo = null;
@@ -8465,6 +8677,19 @@ public class ScriptDocument {
if (info != null && info.isResolved()) {
boolean isNewCreation = newKeyword != null && newKeyword.trim().equals("new");
// For dot-qualified types like 'new AbilityLine.OrbBuilder(...)', the regex never
// captures 'new' in group 1 because 'new' precedes 'AbilityLine', not 'OrbBuilder'.
// Detect it by checking whether outerQualStart is preceded by the 'new' keyword.
if (!isNewCreation && outerQualStart < start) {
int checkPos = outerQualStart - 1;
while (checkPos >= 0 && Character.isWhitespace(text.charAt(checkPos))) checkPos--;
if (checkPos >= 2 && "new".equals(text.substring(checkPos - 2, checkPos + 1))) {
char beforeNew = checkPos >= 3 ? text.charAt(checkPos - 3) : ' ';
if (!Character.isLetterOrDigit(beforeNew) && beforeNew != '_') {
isNewCreation = true;
}
}
}
boolean isConstructorDecl = info instanceof ScriptTypeInfo && className.equals(info.getSimpleName());
// Check if this is a "new" expression or constructor declaration
@@ -8500,8 +8725,17 @@ public class ScriptDocument {
.toArray(TypeInfo[]::new);
// Try to find matching constructor (may be null if not found)
MethodInfo constructor = info.hasConstructors() ? info.findConstructor(argTypes) : null;
// Try to find matching constructor (may be null if not found).
// First try exact type-match; if that fails but constructors exist, fall back to
// arg-count match so validate() can fire a WRONG_ARG_TYPE error instead of
// incorrectly treating the call as "no constructor matches N arguments".
MethodInfo constructor = null;
if (info.hasConstructors()) {
constructor = info.findConstructor(argTypes);
if (constructor == null) {
constructor = info.findConstructor(argCount);
}
}
// Create MethodCallInfo for constructor
// Use the actual variable name (className) for variables, not the class's simple name
@@ -8889,15 +9123,16 @@ public class ScriptDocument {
}
public List<ScriptTypeInfo> getScriptTypes() {
return scriptTypes.values().stream().collect(Collectors.toList());
// Return all types including inner classes — needed by error underline rendering,
// validation loops, and mark building so inner classes get full treatment
return new ArrayList<>(scriptTypesByFullName.values());
}
public List<MethodInfo> getAllMethods() {
List<MethodInfo> allMethods = new ArrayList<>(methods);
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
// Iterate all types (including inner) so nested class methods are included
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
allMethods.addAll(scriptType.getAllMethodsFlat());
// Include constructors so their parameters are recognized
// allMethods.addAll(scriptType.getConstructors());
}
return allMethods;
@@ -8905,7 +9140,8 @@ public class ScriptDocument {
public List<MethodInfo> getAllConstructors() {
List<MethodInfo> allConstructors = new ArrayList<>();
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
// Iterate all types (including inner) so nested class constructors are included
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
allConstructors.addAll(scriptType.getConstructors());
}
return allConstructors;
@@ -8929,7 +9165,7 @@ public class ScriptDocument {
public List<EnumConstantInfo> getAllEnumConstants() {
List<EnumConstantInfo> enums = new ArrayList<>();
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
if (!scriptType.hasEnumConstants())
continue;
@@ -8989,7 +9225,7 @@ public class ScriptDocument {
}
}
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
for (FieldInfo field : scriptType.getFields().values()) {
AssignmentInfo assign = field.findAssignmentAtPosition(position);
if (assign != null)
@@ -9140,7 +9376,7 @@ public class ScriptDocument {
}
for (ScriptTypeInfo scriptType : scriptTypes.values()) {
for (ScriptTypeInfo scriptType : scriptTypesByFullName.values()) {
for (FieldInfo field : scriptType.getFields().values()) {
errored.addAll(field.getErroredAssignments());
}
@@ -9398,8 +9634,17 @@ public class ScriptDocument {
/**
* Get script types as a Map (needed by autocomplete).
* Returns only top-level types keyed by simple name for backward compatibility.
*/
public Map<String, ScriptTypeInfo> getScriptTypesMap() {
return Collections.unmodifiableMap(scriptTypes);
}
/**
* Get all script types including inner classes, keyed by full ($-separated) name.
* Use this when you need to iterate or look up inner classes.
*/
public Map<String, ScriptTypeInfo> getAllScriptTypes() {
return Collections.unmodifiableMap(scriptTypesByFullName);
}
}
@@ -991,6 +991,9 @@ public class TokenHoverInfo {
* - JS types with namespace: "IPlayerEvent.InteractEvent"
*/
public String getName(TypeInfo type){
if (type instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) type).getDotSeparatedName();
}
return type.getDisplayName();
}
@@ -641,6 +641,23 @@ public class ScriptTypeInfo extends TypeInfo {
// ==================== INNER CLASS LOOKUP ====================
/**
* Get the dot-separated display name for this type.
* Top-level: "MyClass"
* Inner: "Outer.Inner"
* Deeply nested: "Outer.Middle.Inner"
*
* This is what users write in type annotations and what resolveType() should accept.
* Recursively walks the outerClass chain to build the full user-facing qualified name.
*/
public String getDotSeparatedName() {
if (outerClass == null) {
return getSimpleName();
}
// Recurse up the nesting hierarchy: each level prepends "Parent." to the name
return outerClass.getDotSeparatedName() + "." + getSimpleName();
}
/**
* Find an inner class by name.
*/
@@ -653,7 +670,6 @@ public class ScriptTypeInfo extends TypeInfo {
return null;
}
// Script types are always considered resolved since they're defined in the script
@Override
public boolean isResolved() {
@@ -932,7 +932,15 @@ public class TypeResolver {
// For complex expressions, resolve as expression and check the result type
TypeInfo exprType = document.resolveExpressionType(identifier, position);
// Only static if the expression result is itself a ClassTypeInfo (e.g., Java.type("File"))
return isStaticAccess(exprType, false);
if (isStaticAccess(exprType, false))
return true;
// Dotted script type name like "Outer.Inner" is always static access
if (!identifier.contains("(") && !identifier.contains("[") && !document.containsOperators(identifier)) {
TypeInfo dotResolved = document.resolveType(identifier);
if (dotResolved instanceof ScriptTypeInfo)
return true;
}
return false;
}
// Simple identifier - check if it's a variable/field (instance access)