mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 20:07:32 +00:00
A lot of ScriptDocument processing improvements
This commit is contained in:
1 parent
74fe7cb892
commit
457a8aae37
1 file changed
+228
-66
@@ -51,7 +51,7 @@ public class ScriptDocument {
|
||||
private static final Pattern METHOD_CALL_PATTERN = Pattern.compile(
|
||||
"([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(");
|
||||
private static final Pattern FIELD_DECL_PATTERN = Pattern.compile(
|
||||
"\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)");
|
||||
"\\b([A-Za-z_][a-zA-Z0-9_<>,\\s\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)");
|
||||
private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)");
|
||||
|
||||
// Function parameters (for JS-style scripts)
|
||||
@@ -386,8 +386,9 @@ public class ScriptDocument {
|
||||
String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length()));
|
||||
|
||||
// Pattern for local variable declarations: Type varName = or Type varName;
|
||||
// Allows capital var names like "Minecraft Capital = new Minecraft();"
|
||||
Pattern localDecl = Pattern.compile(
|
||||
"\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;|,)");
|
||||
"\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(=|;|,)");
|
||||
Matcher m = localDecl.matcher(bodyText);
|
||||
while (m.find()) {
|
||||
int absPos = bodyStart + m.start();
|
||||
@@ -876,72 +877,213 @@ public class ScriptDocument {
|
||||
if (isExcluded(start))
|
||||
continue;
|
||||
|
||||
// Check if this is preceded by a dot (method call on an object)
|
||||
boolean hasReceiver = false;
|
||||
String receiverName = null;
|
||||
int scanPos = start - 1;
|
||||
while (scanPos >= 0 && Character.isWhitespace(text.charAt(scanPos)))
|
||||
scanPos--;
|
||||
// Skip if in import/package statement
|
||||
if (isInImportOrPackage(start))
|
||||
continue;
|
||||
|
||||
if (scanPos >= 0 && text.charAt(scanPos) == '.') {
|
||||
hasReceiver = true;
|
||||
// Find receiver identifier
|
||||
int recvEnd = scanPos - 1;
|
||||
while (recvEnd >= 0 && Character.isWhitespace(text.charAt(recvEnd)))
|
||||
recvEnd--;
|
||||
int recvStart = recvEnd;
|
||||
while (recvStart >= 0 && Character.isJavaIdentifierPart(text.charAt(recvStart)))
|
||||
recvStart--;
|
||||
recvStart++;
|
||||
if (recvStart <= recvEnd && recvStart >= 0) {
|
||||
receiverName = text.substring(recvStart, recvEnd + 1);
|
||||
}
|
||||
}
|
||||
// Resolve the receiver chain and get the final type
|
||||
TypeInfo receiverType = resolveReceiverChain(start);
|
||||
Object wrongArg;
|
||||
|
||||
if (hasReceiver && receiverName != null && !receiverName.isEmpty()) {
|
||||
// Check if receiver is an uppercase class reference (static call)
|
||||
if (Character.isUpperCase(receiverName.charAt(0))) {
|
||||
TypeInfo recvType = resolveType(receiverName);
|
||||
if (recvType == null || !recvType.isResolved()) {
|
||||
// Method call on unresolved type - mark as undefined
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR));
|
||||
continue;
|
||||
}
|
||||
// Check if method exists on resolved type
|
||||
if (!recvType.hasMethod(methodName) ) {
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR));
|
||||
continue;
|
||||
}
|
||||
// Valid method call on static type
|
||||
MethodInfo methodInfo = recvType.getMethodInfo(methodName);
|
||||
if (receiverType != null) {
|
||||
// We have a resolved receiver type - check if method exists
|
||||
if (receiverType.hasMethod(methodName)) {
|
||||
MethodInfo methodInfo = receiverType.getMethodInfo(methodName);
|
||||
// TODO: Add argument validation here in future
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL, methodInfo));
|
||||
} else {
|
||||
// Lower-case receiver - this is a method call on an instance
|
||||
// Try to resolve the receiver's type
|
||||
|
||||
//TODO IMPROVE HERE: currently only resolves global fields, not chained fields or 'this'
|
||||
FieldInfo receiverField = resolveVariable(receiverName, start);
|
||||
if (receiverField != null && receiverField.getTypeInfo() != null) {
|
||||
TypeInfo recvType = receiverField.getTypeInfo();
|
||||
if (recvType.hasMethod(methodName)) {
|
||||
MethodInfo methodInfo = recvType.getMethodInfo(methodName);
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL, methodInfo));
|
||||
} else {
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR));
|
||||
}
|
||||
} else {
|
||||
// TODO weatherEffects.gest() is marked valid here, despite List having no gest() method
|
||||
// Can't resolve receiver - mark method call anyway (could be valid at runtime)
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL));
|
||||
}
|
||||
// Method doesn't exist on this type
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR));
|
||||
}
|
||||
} else {
|
||||
// No receiver - standalone method call (local function or global)
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL));
|
||||
// No receiver OR receiver couldn't be resolved
|
||||
// Check if there's a dot before this (meaning there WAS a receiver, we just couldn't resolve it)
|
||||
boolean hasDot = isPrecededByDot(start);
|
||||
|
||||
if (hasDot) {
|
||||
// There was a receiver but we couldn't resolve it - mark as undefined
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR));
|
||||
} else {
|
||||
// No receiver - standalone method call
|
||||
// Check if this method is defined in the script itself
|
||||
if (isScriptMethod(methodName)) {
|
||||
// TODO: Add argument validation here in future
|
||||
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL));
|
||||
} else {
|
||||
// Unknown standalone method - mark as undefined
|
||||
marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
int x = 5;
|
||||
int y = 10;
|
||||
Minecraft mc = null;
|
||||
|
||||
//Type 1:
|
||||
// foo(x,y);
|
||||
|
||||
//Type 2:
|
||||
// foo(x,y,mc);
|
||||
}
|
||||
|
||||
public static int foo(int x, int y, Boolean z) {
|
||||
return x + 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the full receiver chain before a method call position.
|
||||
* For example, for "mc.thePlayer.worldObj.weatherEffects.get()",
|
||||
* this would resolve mc -> Minecraft -> thePlayer -> EntityPlayer -> worldObj -> World -> weatherEffects -> List
|
||||
* and return the final TypeInfo (List).
|
||||
*
|
||||
* @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) {
|
||||
// First check if preceded by a dot
|
||||
int scanPos = methodNameStart - 1;
|
||||
while (scanPos >= 0 && Character.isWhitespace(text.charAt(scanPos)))
|
||||
scanPos--;
|
||||
|
||||
if (scanPos < 0 || text.charAt(scanPos) != '.') {
|
||||
return null; // No receiver
|
||||
}
|
||||
|
||||
// Walk backward to collect all identifiers in the chain
|
||||
List<String> chainSegments = new ArrayList<>();
|
||||
List<int[]> segmentPositions = new ArrayList<>();
|
||||
|
||||
int pos = scanPos; // Currently at the dot
|
||||
|
||||
while (pos >= 0) {
|
||||
// Skip the dot
|
||||
pos--;
|
||||
|
||||
// Skip whitespace
|
||||
while (pos >= 0 && Character.isWhitespace(text.charAt(pos)))
|
||||
pos--;
|
||||
|
||||
if (pos < 0)
|
||||
break;
|
||||
|
||||
// Check if this is the end of an identifier
|
||||
if (!Character.isJavaIdentifierPart(text.charAt(pos))) {
|
||||
// Could be ')' from a method call - we need to skip that
|
||||
if (text.charAt(pos) == ')') {
|
||||
// This is a method call result, e.g., getList().get()
|
||||
// For now, we can't resolve method return types in chains
|
||||
// So we return null to indicate unresolvable
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Read the identifier backward
|
||||
int identEnd = pos + 1;
|
||||
while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos)))
|
||||
pos--;
|
||||
int identStart = pos + 1;
|
||||
|
||||
String ident = text.substring(identStart, identEnd);
|
||||
chainSegments.add(0, ident); // Add to front (we're going backward)
|
||||
segmentPositions.add(0, new int[]{identStart, identEnd});
|
||||
|
||||
// Skip whitespace
|
||||
while (pos >= 0 && Character.isWhitespace(text.charAt(pos)))
|
||||
pos--;
|
||||
|
||||
// Check if there's another dot (continuing the chain)
|
||||
if (pos >= 0 && text.charAt(pos) == '.') {
|
||||
// Continue the loop to get the next segment
|
||||
continue;
|
||||
} else {
|
||||
// No more dots - end of chain
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (chainSegments.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now resolve the chain from left to right
|
||||
String firstSegment = chainSegments.get(0);
|
||||
TypeInfo currentType = null;
|
||||
|
||||
if (firstSegment.equals("this")) {
|
||||
// For 'this', we use globalFields for resolution
|
||||
// Can't fully resolve 'this' without class context, but we can try
|
||||
currentType = null;
|
||||
|
||||
if (chainSegments.size() > 1) {
|
||||
// this.something - check globalFields
|
||||
String nextField = chainSegments.get(1);
|
||||
if (globalFields.containsKey(nextField)) {
|
||||
currentType = globalFields.get(nextField).getTypeInfo();
|
||||
// Continue resolving from index 2
|
||||
for (int i = 2; i < chainSegments.size(); i++) {
|
||||
if (currentType == null || !currentType.isResolved())
|
||||
return null;
|
||||
String field = chainSegments.get(i);
|
||||
if (currentType.hasField(field)) {
|
||||
FieldInfo fieldInfo = currentType.getFieldInfo(field);
|
||||
currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return currentType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} else if (Character.isUpperCase(firstSegment.charAt(0))) {
|
||||
// Static access like Minecraft.getMinecraft() - the type is the class itself
|
||||
currentType = resolveType(firstSegment);
|
||||
} else {
|
||||
// Variable like mc.thePlayer.worldObj
|
||||
int[] firstPos = segmentPositions.get(0);
|
||||
FieldInfo varInfo = resolveVariable(firstSegment, firstPos[0]);
|
||||
if (varInfo != null) {
|
||||
currentType = varInfo.getTypeInfo();
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve remaining segments
|
||||
for (int i = 1; i < chainSegments.size(); i++) {
|
||||
if (currentType == null || !currentType.isResolved()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String field = chainSegments.get(i);
|
||||
if (currentType.hasField(field)) {
|
||||
FieldInfo fieldInfo = currentType.getFieldInfo(field);
|
||||
currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null;
|
||||
} else {
|
||||
// Field doesn't exist
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return currentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a method name is defined in this script.
|
||||
*/
|
||||
private boolean isScriptMethod(String methodName) {
|
||||
for (MethodInfo method : methods) {
|
||||
if (method.getName().equals(methodName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void markVariables(List<ScriptLine.Mark> marks) {
|
||||
Set<String> knownKeywords = new HashSet<>(Arrays.asList(
|
||||
"boolean", "int", "float", "double", "long", "char", "byte", "short", "void",
|
||||
@@ -982,10 +1124,6 @@ public class ScriptDocument {
|
||||
if (isInImportOrPackage(position))
|
||||
continue;
|
||||
|
||||
// Skip uppercase (type references handled elsewhere)
|
||||
if (Character.isUpperCase(name.charAt(0)))
|
||||
continue;
|
||||
|
||||
// Skip method calls (followed by paren)
|
||||
if (isFollowedByParen(m.end(1)))
|
||||
continue;
|
||||
@@ -993,6 +1131,10 @@ public class ScriptDocument {
|
||||
// Find containing method
|
||||
MethodInfo containingMethod = findMethodAtPosition(position);
|
||||
|
||||
// For uppercase identifiers, only process if it's a known field
|
||||
// Otherwise, let type handling (markImportedClassUsages) handle it
|
||||
boolean isUppercase = Character.isUpperCase(name.charAt(0));
|
||||
|
||||
if (containingMethod != null) {
|
||||
// Check parameters
|
||||
if (containingMethod.hasParameter(name)) {
|
||||
@@ -1019,6 +1161,10 @@ public class ScriptDocument {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip uppercase if not a known field - type handling will deal with it
|
||||
if (isUppercase)
|
||||
continue;
|
||||
|
||||
// Unknown variable inside method - mark as undefined
|
||||
marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR));
|
||||
} else {
|
||||
@@ -1030,6 +1176,10 @@ public class ScriptDocument {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip uppercase if not a known field - type handling will deal with it
|
||||
if (isUppercase)
|
||||
continue;
|
||||
|
||||
// Unknown variable outside method - mark as undefined
|
||||
marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR));
|
||||
}
|
||||
@@ -1039,6 +1189,7 @@ public class ScriptDocument {
|
||||
/**
|
||||
* Mark chained field accesses like: mc.player.world, array.length, this.field, etc.
|
||||
* This handles dot-separated access chains and colors each segment appropriately.
|
||||
* 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, etc.
|
||||
@@ -1056,6 +1207,17 @@ public class ScriptDocument {
|
||||
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
|
||||
@@ -1093,10 +1255,10 @@ public class ScriptDocument {
|
||||
int identEnd = pos;
|
||||
|
||||
// Check if this is followed by parentheses (method call - stop)
|
||||
int checkPos = pos;
|
||||
while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos)))
|
||||
checkPos++;
|
||||
if (checkPos < text.length() && text.charAt(checkPos) == '(') {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user