From 176a3b6d50bf648cf486ba79555dd87ea4a4ae99 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 06:56:41 +0200 Subject: [PATCH] - Added Java.type("") for JS API - Added synthetic types (non-existant in java classloader and JS API, but custom defined like Nashorn's Java.type/extends/etc.. --- .../script/autocomplete/AutocompleteItem.java | 76 +++++ .../autocomplete/JSAutocompleteProvider.java | 29 +- .../script/interpreter/ScriptDocument.java | 83 +++++- .../script/interpreter/field/FieldInfo.java | 8 + .../interpreter/method/MethodCallInfo.java | 23 ++ .../script/interpreter/method/MethodInfo.java | 9 + .../interpreter/type/ClassTypeInfo.java | 73 +++++ .../interpreter/type/NashornBuiltins.java | 282 ++++++++++++++++++ .../script/interpreter/type/TypeInfo.java | 7 + .../script/interpreter/type/TypeResolver.java | 96 +++++- .../type/synthetic/SyntheticField.java | 33 ++ .../type/synthetic/SyntheticMethod.java | 136 +++++++++ .../type/synthetic/SyntheticParameter.java | 13 + .../type/synthetic/SyntheticType.java | 104 +++++++ .../type/synthetic/SyntheticTypeBuilder.java | 165 ++++++++++ 15 files changed, 1131 insertions(+), 6 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassTypeInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticTypeBuilder.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 8db4ea073..94b43f025 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -5,6 +5,9 @@ import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticField; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticMethod; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticParameter; /** * Represents a single autocomplete suggestion. @@ -321,6 +324,79 @@ public class AutocompleteItem implements Comparable { ); } + /** + * Create from a synthetic method (e.g., Nashorn built-in like Java.type). + */ + public static AutocompleteItem fromSyntheticMethod( + SyntheticMethod method, + TypeInfo containingType) { + String name = method.name; + StringBuilder insertText = new StringBuilder(name); + insertText.append("("); + insertText.append(")"); + + // Build display name with parameters - use simple names for types + StringBuilder displayName = new StringBuilder(name); + displayName.append("("); + for (int i = 0; i < method.parameters.size(); i++) { + if (i > 0) displayName.append(", "); + SyntheticParameter param = + method.parameters.get(i); + // Use simple name for display (e.g., "Class" instead of "java.lang.Class") + String simpleTypeName = getSimpleTypeName(param.typeName); + displayName.append(simpleTypeName).append(" ").append(param.name); + } + displayName.append(")"); + + // Use simple name for return type display too + String simpleReturnType = getSimpleTypeName(method.returnType); + + return new AutocompleteItem( + displayName.toString(), + name, // Search against just the method name + insertText.toString(), + Kind.METHOD, + simpleReturnType, + method.getSignature(), + method.documentation, + method, + false, + false, + null, + 0 // Synthetic types are at depth 0 + ); + } + + /** + * Get simple name from a fully-qualified type name. + */ + private static String getSimpleTypeName(String typeName) { + if (typeName == null) return "void"; + int lastDot = typeName.lastIndexOf('.'); + return lastDot >= 0 ? typeName.substring(lastDot + 1) : typeName; + } + + /** + * Create from a synthetic field (e.g., Nashorn built-in). + */ + public static AutocompleteItem fromSyntheticField( + SyntheticField field) { + return new AutocompleteItem( + field.name, + field.name, // searchName same as display name + field.name, + Kind.FIELD, + field.typeName, + field.typeName + " " + field.name, + field.documentation, + field, + false, + false, + null, + 0 // Synthetic types are at depth 0 + ); + } + // ==================== HELPER METHODS ==================== private static String buildMethodSignature(MethodInfo method) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index d0c85a3fe..641add7c0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -1,9 +1,7 @@ package noppes.npcs.client.gui.util.script.autocomplete; -import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; -import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.*; -import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.*; import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; @@ -46,6 +44,14 @@ public class JSAutocompleteProvider extends JavaAutocompleteProvider { return; } + // Check for synthetic types first (e.g., Nashorn Java object, custom types) + SyntheticType syntheticType = + document.getTypeResolver().getSyntheticType(receiverType.getSimpleName()); + if (syntheticType != null) { + addSyntheticTypeSuggestions(syntheticType, items); + return; + } + // If this is a Java type (has a Java class), delegate to parent's Java handling // This preserves static/instance context, modifiers, and all Java-specific behavior Class javaClass = receiverType.getJavaClass(); @@ -63,6 +69,23 @@ public class JSAutocompleteProvider extends JavaAutocompleteProvider { } } + /** + * Add autocomplete suggestions for a synthetic type's methods and fields. + */ + private void addSyntheticTypeSuggestions(SyntheticType syntheticType, List items) { + // Add methods + for (SyntheticMethod method : syntheticType.getMethods()) { + AutocompleteItem item = AutocompleteItem.fromSyntheticMethod(method, syntheticType.getTypeInfo()); + items.add(item); + } + + // Add fields + for (SyntheticField field : syntheticType.getFields()) { + AutocompleteItem item = AutocompleteItem.fromSyntheticField(field); + items.add(item); + } + } + /** * Recursively add methods from a type and its parents. * @param type The current JS type to get methods from (changes as we walk up inheritance) 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 a96c0195c..6a1682f6b 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 @@ -20,6 +20,9 @@ import noppes.npcs.client.gui.util.script.interpreter.token.Token; import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.*; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticField; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticMethod; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticType; import java.util.*; import java.util.regex.Matcher; @@ -2661,7 +2664,39 @@ public class ScriptDocument { MethodInfo resolvedMethod = null; if (receiverType != null) { - // Check for method existence using hierarchy search if it's a ScriptTypeInfo + // Check for synthetic types first (JavaScript only) + SyntheticType syntheticType = null; + if (isJavaScript()) { + syntheticType = typeResolver.getSyntheticType(receiverType.getSimpleName()); + } + + if (syntheticType != null && syntheticType.hasMethod(methodName)) { + // Synthetic type method call (e.g., Java.type()) + resolvedMethod = syntheticType.getMethodInfo(methodName); + + // Check for dynamic return type resolution (like Java.type("className")) + SyntheticMethod synMethod = syntheticType.getMethod(methodName); + TypeInfo dynamicReturnType = null; + if (synMethod != null && synMethod.returnTypeResolver != null) { + String argsText = text.substring(openParen + 1, closeParen); + String[] strArgs = TypeResolver.parseStringArguments(argsText); + dynamicReturnType = synMethod.returnTypeResolver.resolve(strArgs); + } + + MethodCallInfo callInfo = new MethodCallInfo(methodName, nameStart, nameEnd, openParen, + closeParen, arguments, receiverType, resolvedMethod, false); + + // Set the actual resolved return type for downstream type resolution + if (dynamicReturnType != null) { + callInfo.setResolvedReturnType(dynamicReturnType); + } + + callInfo.validate(); + methodCalls.add(callInfo); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); + continue; + } + // Regular type - check for method existence using hierarchy search if it's a ScriptTypeInfo boolean hasMethod = false; if (receiverType instanceof ScriptTypeInfo) { hasMethod = ((ScriptTypeInfo) receiverType).hasMethodInHierarchy(methodName); @@ -3496,6 +3531,42 @@ public class ScriptDocument { } } + /** + * Resolve a chain segment on a synthetic type (like Nashorn's Java object). + */ + /** + * Resolve a chain segment for a synthetic type (method call or field access). + * Handles dynamic return type resolution for methods like Java.type(). + */ + private TypeInfo resolveSyntheticChainSegment(SyntheticType syntheticType, ChainSegment segment) { + if (segment.isMethodCall) { + SyntheticMethod method = syntheticType.getMethod(segment.name); + if (method != null) { + // For methods with dynamic return type resolvers (like Java.type), + // extract string arguments and resolve + if (method.returnTypeResolver != null && segment.arguments != null) { + String[] args = TypeResolver.parseStringArguments(segment.arguments); + TypeInfo resolved = method.returnTypeResolver.resolve(args); + if (resolved != null) { + return resolved; + } + } + // Fall back to static return type + TypeInfo returnType = typeResolver.resolve(method.returnType); + return returnType != null ? returnType : TypeInfo.unresolved(method.returnType, method.returnType); + } + } else { + SyntheticField field = syntheticType.getField(segment.name); + if (field != null) { + TypeInfo fieldType = typeResolver.resolve(field.typeName); + return fieldType != null ? fieldType : TypeInfo.unresolved(field.typeName, field.typeName); + } + } + return null; + } + + + // ==================== OPERATOR EXPRESSION RESOLUTION ==================== /** @@ -5023,6 +5094,16 @@ public class ScriptDocument { if (isInImportOrPackage(start)) continue; + // For JavaScript, first check synthetic types (Nashorn built-ins like Java, print, etc.) + if (isJavaScript() && typeResolver.isSyntheticType(className)) { + SyntheticType syntheticType = typeResolver.getSyntheticType(className); + // Mark as IMPORTED_CLASS since it's a type reference + // Pass the TypeInfo (which the hover system can display) + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.IMPORTED_CLASS, + syntheticType.getTypeInfo())); + continue; + } + // Try to resolve the class TypeInfo info = resolveType(className); if (info != null && info.isResolved()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 9182cc267..ec3958f02 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -135,6 +135,14 @@ public final class FieldInfo { return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null, -1, -1, 0, null); } + /** + * Create a FieldInfo for a synthetic/external field. + * Used for built-in types like Nashorn's Java object. + */ + public static FieldInfo external(String name, TypeInfo type, String documentation, int modifiers) { + return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, modifiers, null); + } + /** * Create a FieldInfo from reflection data for a class field. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index dfafad691..c6362d701 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -197,6 +197,29 @@ public class MethodCallInfo { this.expectedType = expectedType; } + // Dynamically resolved return type (e.g., for Java.type() which returns ClassTypeInfo) + private TypeInfo resolvedReturnType; + + /** + * Get the resolved return type of this method call. + * This may differ from resolvedMethod.getReturnType() for methods with dynamic return types. + * @return The resolved return type, or the method's declared return type if not dynamically resolved + */ + public TypeInfo getResolvedReturnType() { + if (resolvedReturnType != null) { + return resolvedReturnType; + } + return resolvedMethod != null ? resolvedMethod.getReturnType() : null; + } + + /** + * Set a dynamically resolved return type. + * Used for methods like Java.type() where the return type depends on the arguments. + */ + public void setResolvedReturnType(TypeInfo returnType) { + this.resolvedReturnType = returnType; + } + public boolean isConstructor() { return isConstructor; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 039127ea5..ababd9816 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -127,6 +127,15 @@ public final class MethodInfo { return new MethodInfo(name, null, null, params, -1, -1, -1, -1, -1, false, false, 0, null, null); } + /** + * Create a MethodInfo for a synthetic/external method. + * Used for built-in types like Nashorn's Java object. + */ + public static MethodInfo external(String name, TypeInfo returnType, TypeInfo containingType, + List params, int modifiers, String documentation) { + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, null); + } + /** * Create a MethodInfo from reflection data. * Used when resolving method calls on known types. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassTypeInfo.java new file mode 100644 index 000000000..c19faeb2c --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassTypeInfo.java @@ -0,0 +1,73 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +/** + * Represents a reference to a Java Class itself (not an instance). + * Used for patterns like: var File = Java.type("java.io.File"); + * + * When you have a ClassTypeInfo: + * - Accessing members should show ONLY static members (File.listRoots()) + * - Using 'new' should return an instance of the wrapped class (new File("path")) + * + * This is distinct from a regular TypeInfo which represents an instance of that type. + */ +public class ClassTypeInfo extends TypeInfo { + + private final TypeInfo instanceType; // The type you get when you do 'new' on this class + + /** + * Create a ClassTypeInfo wrapping a Java class. + * + * @param javaClass The Java class this represents (e.g., java.io.File.class) + */ + public ClassTypeInfo(Class javaClass) { + super( + javaClass.getSimpleName(), + javaClass.getName(), + javaClass.getPackage() != null ? javaClass.getPackage().getName() : "", + javaClass.isInterface() ? Kind.INTERFACE : (javaClass.isEnum() ? Kind.ENUM : Kind.CLASS), + javaClass, + true, + null, + true // subclass marker + ); + this.instanceType = TypeInfo.fromClass(javaClass); + } + + /** + * Create a ClassTypeInfo from an existing TypeInfo. + */ + public ClassTypeInfo(TypeInfo instanceType) { + super( + instanceType.getSimpleName(), + instanceType.getFullName(), + instanceType.getPackageName(), + instanceType.getKind(), + instanceType.getJavaClass(), + instanceType.isResolved(), + instanceType.getEnclosingType(), + true // subclass marker + ); + this.instanceType = instanceType; + } + + /** + * Returns true - this TypeInfo represents a class reference, not an instance. + */ + @Override + public boolean isClassReference() { + return true; + } + + /** + * Get the TypeInfo for instances of this class. + * This is what you get when you call 'new' on this class. + */ + public TypeInfo getInstanceType() { + return instanceType; + } + + @Override + public String toString() { + return "Class<" + getSimpleName() + ">"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java new file mode 100644 index 000000000..3b425b5bb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java @@ -0,0 +1,282 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticMethod; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticParameter; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticTypeBuilder; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticType; + +import java.util.*; + +/** + * Registry for Nashorn built-in types and global functions. + * + *

Nashorn provides several built-in objects and functions:

+ *
    + *
  • Java - Object for Java interop: type(), extend(), from(), to()
  • + *
  • Packages - Root of package hierarchy for accessing Java packages
  • + *
  • JavaImporter - Creates scope with imported Java packages
  • + *
  • print() - Global print function
  • + *
  • load() - Load and execute a script
  • + *
+ */ +public class NashornBuiltins { + + private static NashornBuiltins instance; + + private final Map builtinTypes = new LinkedHashMap<>(); + private final Map globalFunctions = new LinkedHashMap<>(); + + private NashornBuiltins() { + initializeBuiltins(); + } + + public static NashornBuiltins getInstance() { + if (instance == null) { + instance = new NashornBuiltins(); + } + return instance; + } + + private void initializeBuiltins() { + // ==================== Java object ==================== + SyntheticType javaType = new SyntheticTypeBuilder("Java") + .documentation("Nashorn's Java interop object for accessing Java types and utilities.") + + .addMethod("type") + .parameter("className", "String", "Fully-qualified Java class name") + .returns("java.lang.Class") + .returnsResolved(args -> { + if (args != null && args.length > 0 && args[0] != null) { + String className = args[0]; + // Remove quotes if present + if (className.startsWith("\"") && className.endsWith("\"")) { + className = className.substring(1, className.length() - 1); + } else if (className.startsWith("'") && className.endsWith("'")) { + className = className.substring(1, className.length() - 1); + } + // Try to load the class + TypeInfo resolved = TypeResolver.getInstance().resolve(className); + if (resolved != null && resolved.isResolved()) { + return new ClassTypeInfo(resolved); + } + } + return null; + }) + .documentation("Loads a Java class by its fully-qualified name.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var File = Java.type(\"java.io.File\");\n" + + "var file = new File(\"path/to/file\");\n" + + "```\n\n" + + "@param className The fully-qualified Java class name\n" + + "@returns The Java class reference (use with 'new' to create instances)") + .done() + + .addMethod("extend") + .parameter("type", "java.lang.Class", "Java class or interface to extend/implement") + .returns("java.lang.Class") + .documentation("Creates a subclass or implementation of a Java class or interface.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var MyRunnable = Java.extend(Java.type(\"java.lang.Runnable\"), {\n" + + " run: function() {\n" + + " print(\"Running!\");\n" + + " }\n" + + "});\n" + + "```\n\n" + + "@param type The Java class or interface to extend\n" + + "@returns A new class that extends/implements the given type") + .done() + + .addMethod("from") + .parameter("javaArray", "Object", "A Java array or Collection") + .returns("Array") + .documentation("Converts a Java array or Collection to a JavaScript array.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var jsList = Java.from(javaArrayList);\n" + + "jsList.forEach(function(item) { print(item); });\n" + + "```\n\n" + + "@param javaArray A Java array or java.util.Collection\n" + + "@returns A JavaScript array containing the elements") + .done() + + .addMethod("to") + .parameter("jsArray", "Array", "A JavaScript array") + .parameter("javaType", "java.lang.Class", "The target Java array type") + .returns("Object") + .documentation("Converts a JavaScript array to a Java array of the specified type.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var jsArray = [1, 2, 3];\n" + + "var intArray = Java.to(jsArray, \"int[]\");\n" + + "```\n\n" + + "@param jsArray A JavaScript array\n" + + "@param javaType The target Java array type (e.g., \"int[]\", \"java.lang.String[]\")\n" + + "@returns A Java array of the specified type") + .done() + + .addMethod("super") + .parameter("object", "Object", "A Java object created via Java.extend()") + .returns("Object") + .documentation("Gets a reference to the super class for calling super methods.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var MyList = Java.extend(Java.type(\"java.util.ArrayList\"), {\n" + + " add: function(e) {\n" + + " print(\"Adding: \" + e);\n" + + " return Java.super(this).add(e);\n" + + " }\n" + + "});\n" + + "```\n\n" + + "@param object An extended Java object\n" + + "@returns A reference to call super methods") + .done() + + .addMethod("synchronized") + .parameter("func", "Function", "The function to synchronize") + .parameter("lock", "Object", "The object to synchronize on") + .returns("Function") + .documentation("Wraps a function to execute synchronized on a given object.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var syncFunc = Java.synchronized(function() {\n" + + " // Thread-safe code here\n" + + "}, lockObject);\n" + + "```\n\n" + + "@param func The function to synchronize\n" + + "@param lock The object to use as the monitor\n" + + "@returns A synchronized version of the function") + .done() + + .build(); + builtinTypes.put("Java", javaType); + + // ==================== Global print function ==================== + SyntheticType printFunc = new SyntheticTypeBuilder("print") + .addMethod("print") + .parameter("message", "Object", "The message to print") + .returns("void") + .documentation("Prints a message to standard output.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "print(\"Hello, world!\");\n" + + "print(myObject);\n" + + "```") + .done() + .build(); + globalFunctions.put("print", printFunc.getMethod("print")); + + // ==================== Global load function ==================== + SyntheticType loadFunc = new SyntheticTypeBuilder("load") + .addMethod("load") + .parameter("script", "String", "Path or URL to the script") + .returns("Object") + .documentation("Loads and executes a script file or URL.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "load(\"./myScript.js\");\n" + + "load(\"http://example.com/script.js\");\n" + + "```\n\n" + + "@param script Path to a local script or URL\n" + + "@returns The result of the script execution") + .done() + .build(); + globalFunctions.put("load", loadFunc.getMethod("load")); + } + + /** + * Get a built-in type by name. + * @param name The type name (e.g., "Java") + * @return The SyntheticType or null if not found + */ + public SyntheticType getBuiltinType(String name) { + return builtinTypes.get(name); + } + + /** + * Check if a name is a built-in type. + */ + public boolean isBuiltinType(String name) { + return builtinTypes.containsKey(name); + } + + /** + * Get a global function by name. + */ + public SyntheticMethod getGlobalFunction(String name) { + return globalFunctions.get(name); + } + + /** + * Check if a name is a global function. + */ + public boolean isGlobalFunction(String name) { + return globalFunctions.containsKey(name); + } + + /** + * Get all built-in type names. + */ + public Set getBuiltinTypeNames() { + return Collections.unmodifiableSet(builtinTypes.keySet()); + } + + /** + * Get all global function names. + */ + public Set getGlobalFunctionNames() { + return Collections.unmodifiableSet(globalFunctions.keySet()); + } + + /** + * Get all built-in types. + */ + public Collection getAllBuiltinTypes() { + return Collections.unmodifiableCollection(builtinTypes.values()); + } + + /** + * Get all global functions as SyntheticTypes. + * Each function is wrapped in its own SyntheticType for the registry. + */ + public Map getAllGlobalFunctions() { + Map result = new LinkedHashMap<>(); + + // Wrap each global function in a minimal SyntheticType + for (Map.Entry entry : globalFunctions.entrySet()) { + String name = entry.getKey(); + SyntheticMethod method = entry.getValue(); + + // Create a synthetic type that just contains this one global function + SyntheticTypeBuilder builder = new SyntheticTypeBuilder(name); + builder.documentation("Global " + name + " function"); + + // Add the method + SyntheticTypeBuilder.MethodBuilder methodBuilder = builder.addMethod(name); + for (SyntheticParameter param : method.parameters) { + methodBuilder.parameter(param.name, param.typeName, param.documentation); + } + methodBuilder.returns(method.returnType); + if (method.documentation != null && !method.documentation.isEmpty()) { + methodBuilder.documentation(method.documentation); + } + methodBuilder.done(); + + result.put(name, builder.build()); + } + + return result; + } + + /** + * Resolve the return type of Java.type() given the class name argument. + */ + public TypeInfo resolveJavaType(String className) { + SyntheticType javaBuiltin = builtinTypes.get("Java"); + if (javaBuiltin != null) { + return javaBuiltin.resolveMethodReturnType("type", new String[]{className}); + } + return null; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 5983012de..14f4c6242 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -240,6 +240,13 @@ public class TypeInfo { public boolean isEnum() {return kind == Kind.ENUM;} public boolean isPrimitive() {return javaClass != null && javaClass.isPrimitive();} + /** + * Returns true if this TypeInfo represents a Class reference (not an instance). + * For example, Java.type("java.io.File") returns a Class reference. + * Override in ClassTypeInfo to return true. + */ + public boolean isClassReference() { return false; } + /** * Get the appropriate TokenType for highlighting this type. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java index 81349ef52..4b930c934 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java @@ -1,11 +1,12 @@ package noppes.npcs.client.gui.util.script.interpreter.type; import noppes.npcs.client.gui.util.script.PackageFinder; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticType; import java.util.*; @@ -29,6 +30,9 @@ public class TypeResolver { // JavaScript type registry (lazily initialized) private JSTypeRegistry jsTypeRegistry; + // Synthetic type registry (e.g., Nashorn built-ins) + private final Map syntheticTypes = new LinkedHashMap<>(); + // Auto-imported java.lang classes public static final Set JAVA_LANG_CLASSES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", @@ -70,6 +74,60 @@ public class TypeResolver { validPackages.add("java.lang"); validPackages.add("java.util"); validPackages.add("java.io"); + + // Initialize built-in synthetic types + initializeSyntheticTypes(); + } + + /** + * Initialize built-in synthetic types (e.g., Nashorn built-ins). + */ + private void initializeSyntheticTypes() { + // Register all Nashorn built-in types (Java, etc.) + for (SyntheticType type : NashornBuiltins.getInstance().getAllBuiltinTypes()) { + syntheticTypes.put(type.getName(), type); + } + + // Register all Nashorn global functions (print, load, etc.) + Map nashornGlobals = NashornBuiltins.getInstance().getAllGlobalFunctions(); + syntheticTypes.putAll(nashornGlobals); + } + + // ==================== SYNTHETIC TYPE REGISTRY ==================== + + /** + * Register a synthetic type for use in scripts. + * @param name The type name (e.g., "Java", "MyCustomType") + * @param type The synthetic type definition + */ + public void registerSyntheticType(String name, SyntheticType type) { + syntheticTypes.put(name, type); + } + + /** + * Check if a name is a known synthetic type. + * @param name The type name to check + * @return true if the name is a registered synthetic type + */ + public boolean isSyntheticType(String name) { + return syntheticTypes.containsKey(name); + } + + /** + * Get a synthetic type by name. + * @param name The type name + * @return The synthetic type, or null if not found + */ + public SyntheticType getSyntheticType(String name) { + return syntheticTypes.get(name); + } + + /** + * Get all registered synthetic types. + * @return Collection of all synthetic types + */ + public Collection getAllSyntheticTypes() { + return syntheticTypes.values(); } // ==================== JS TYPE REGISTRY ACCESS ==================== @@ -124,6 +182,34 @@ public class TypeResolver { } // ==================== UNIFIED TYPE RESOLUTION ==================== + + /** + * General-purpose type resolution. + * Tries JS types first, then falls back to Java types. + * + * @param typeName The type name (can be simple or fully-qualified) + * @return TypeInfo for the resolved type, or null if not found + */ + public TypeInfo resolve(String typeName) { + if (typeName == null || typeName.isEmpty()) { + return null; + } + + // Try JS resolution first (handles primitives, .d.ts types) + TypeInfo jsType = resolveJSType(typeName); + if (jsType != null && jsType.isResolved()) { + return jsType; + } + + // Try full name resolution + TypeInfo fullType = resolveFullName(typeName); + if (fullType != null && fullType.isResolved()) { + return fullType; + } + + // Return unresolved or null + return jsType; // Will be unresolved TypeInfo or null + } /** * Resolve a type name for JavaScript context. @@ -148,6 +234,12 @@ public class TypeResolver { // Strip array brackets for base type resolution boolean isArray = jsTypeName.endsWith("[]"); String baseName = isArray ? jsTypeName.substring(0, jsTypeName.length() - 2) : jsTypeName; + + // Check synthetic types (Nashorn built-ins, custom types, etc.) + if (isSyntheticType(baseName)) { + SyntheticType syntheticType = getSyntheticType(baseName); + return syntheticType.getTypeInfo(); + } // Handle "Java." prefix - convert to actual Java type // e.g., "Java.java.io.File" -> "java.io.File" diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java new file mode 100644 index 000000000..1f798e4f6 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java @@ -0,0 +1,33 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +import java.lang.reflect.Modifier; + +public class SyntheticField { + public final String name; + public final String typeName; + public final String documentation; + public final boolean isStatic; + + SyntheticField(String name, String typeName, String documentation, boolean isStatic) { + this.name = name; + this.typeName = typeName; + this.documentation = documentation; + this.isStatic = isStatic; + } + + public FieldInfo toFieldInfo() { + TypeInfo type = TypeResolver.getInstance().resolve(typeName); + if (type == null) { + type = TypeInfo.unresolved(typeName, typeName); + } + int modifiers = Modifier.PUBLIC; + if (isStatic) { + modifiers |= Modifier.STATIC; + } + return FieldInfo.external(name, type, documentation, modifiers); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java new file mode 100644 index 000000000..6bd554904 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java @@ -0,0 +1,136 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SyntheticMethod { + public final String name; + public final String returnType; + public final List parameters; + public final String documentation; + public final boolean isStatic; + public final SyntheticTypeBuilder.ReturnTypeResolver returnTypeResolver; + + SyntheticMethod(String name, String returnType, List parameters, + String documentation, boolean isStatic, + SyntheticTypeBuilder.ReturnTypeResolver returnTypeResolver) { + this.name = name; + this.returnType = returnType; + this.parameters = Collections.unmodifiableList(new ArrayList<>(parameters)); + this.documentation = documentation; + this.isStatic = isStatic; + this.returnTypeResolver = returnTypeResolver; + } + + /** + * Create a MethodInfo from this synthetic method. + */ + public MethodInfo toMethodInfo(TypeInfo containingType) { + List paramInfos = new ArrayList<>(); + for (SyntheticParameter param : parameters) { + TypeInfo paramType = TypeResolver.getInstance().resolve(param.typeName); + if (paramType == null) { + paramType = TypeInfo.unresolved(param.typeName, param.typeName); + } + paramInfos.add(FieldInfo.parameter(param.name, paramType, -1, null)); + } + + TypeInfo returnTypeInfo = TypeResolver.getInstance().resolve(returnType); + if (returnTypeInfo == null) { + returnTypeInfo = TypeInfo.unresolved(returnType, returnType); + } + + int modifiers = Modifier.PUBLIC; + if (isStatic) { + modifiers |= Modifier.STATIC; + } + + MethodInfo methodInfo = MethodInfo.external(name, returnTypeInfo, containingType, paramInfos, modifiers, null); + + // Create JSDocInfo from documentation + if (documentation != null && !documentation.isEmpty()) { + JSDocInfo jsDocInfo = createJSDocInfo(returnTypeInfo); + methodInfo.setJSDocInfo(jsDocInfo); + } + + return methodInfo; + } + + /** + * Create JSDocInfo from the documentation string. + */ + private JSDocInfo createJSDocInfo(TypeInfo returnTypeInfo) { + // Parse the documentation to extract description and separate sections + String[] lines = documentation.split("\\n"); + StringBuilder descBuilder = new StringBuilder(); + + // Extract description (everything before @param or @returns) + for (String line : lines) { + line = line.trim(); + if (line.startsWith("@param") || line.startsWith("@returns") || line.startsWith("@return")) { + break; + } + if (!line.isEmpty()) { + if (descBuilder.length() > 0) + descBuilder.append("\n"); + descBuilder.append(line); + } + } + + JSDocInfo jsDocInfo = new JSDocInfo(documentation, -1, -1); + jsDocInfo.setDescription(descBuilder.toString()); + + // Add @param tags for each parameter + for (int i = 0; i < parameters.size(); i++) { + SyntheticParameter param = parameters.get(i); + TypeInfo paramType = TypeResolver.getInstance().resolve(param.typeName); + if (paramType == null) { + paramType = TypeInfo.unresolved(param.typeName, param.typeName); + } + + JSDocParamTag paramTag = JSDocParamTag.create( + -1, -1, -1, // offsets + param.typeName, paramType, -1, -1, // type info + param.name, -1, -1, // param name + param.documentation // description + ); + jsDocInfo.addParamTag(paramTag); + } + + // Add @returns tag + JSDocReturnTag returnTag = JSDocReturnTag.create( + "returns", -1, -1, -1, // offsets + returnTypeInfo.getSimpleName(), returnTypeInfo, -1, -1, // type info + null // description extracted from main documentation + ); + jsDocInfo.setReturnTag(returnTag); + + return jsDocInfo; + } + + /** + * Get signature string for display. + */ + public String getSignature() { + StringBuilder sb = new StringBuilder(); + sb.append(name).append("("); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) + sb.append(", "); + SyntheticParameter p = parameters.get(i); + sb.append(p.name).append(": ").append(p.typeName); + } + sb.append("): ").append(returnType); + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java new file mode 100644 index 000000000..9b58bebf3 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java @@ -0,0 +1,13 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +public class SyntheticParameter { + public final String name; + public final String typeName; + public final String documentation; + + SyntheticParameter(String name, String typeName, String documentation) { + this.name = name; + this.typeName = typeName; + this.documentation = documentation; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java new file mode 100644 index 000000000..356891caf --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java @@ -0,0 +1,104 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a fully-built synthetic type. + */ +public class SyntheticType { + private final String name; + private final String documentation; + private final Map methods; + private final Map fields; + private TypeInfo typeInfo; + + SyntheticType(String name, String documentation, List methods, + List fields) { + this.name = name; + this.documentation = documentation; + this.methods = new LinkedHashMap<>(); + for (SyntheticMethod m : methods) { + this.methods.put(m.name, m); + } + this.fields = new LinkedHashMap<>(); + for (SyntheticField f : fields) { + this.fields.put(f.name, f); + } + } + + public String getName() { + return name; + } + + public String getDocumentation() { + return documentation; + } + + public SyntheticMethod getMethod(String methodName) { + return methods.get(methodName); + } + + public Collection getMethods() { + return methods.values(); + } + + public SyntheticField getField(String fieldName) { + return fields.get(fieldName); + } + + public Collection getFields() { + return fields.values(); + } + + public boolean hasMethod(String methodName) { + return methods.containsKey(methodName); + } + + public boolean hasField(String fieldName) { + return fields.containsKey(fieldName); + } + + /** + * Get or create the TypeInfo for this synthetic type. + */ + public TypeInfo getTypeInfo() { + if (typeInfo == null) { + typeInfo = TypeInfo.resolved(name, name, "", TypeInfo.Kind.CLASS, null); + } + return typeInfo; + } + + /** + * Create a MethodInfo for a method in this type. + */ + public MethodInfo getMethodInfo(String methodName) { + SyntheticMethod method = methods.get(methodName); + if (method == null) + return null; + return method.toMethodInfo(getTypeInfo()); + } + + /** + * Resolve the return type of a method given arguments. + * Used for special methods like Java.type(). + */ + public TypeInfo resolveMethodReturnType(String methodName, String[] arguments) { + SyntheticMethod method = methods.get(methodName); + if (method == null) + return null; + + if (method.returnTypeResolver != null) { + return method.returnTypeResolver.resolve(arguments); + } + + // Fall back to static return type + return TypeResolver.getInstance().resolve(method.returnType); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticTypeBuilder.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticTypeBuilder.java new file mode 100644 index 000000000..02fc4d586 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticTypeBuilder.java @@ -0,0 +1,165 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +import java.util.*; + +/** + * Builder for creating synthetic (non-reflection-based) types. + * Used for built-in Nashorn objects like 'Java' that have no corresponding Java class. + * + *

Usage:

+ *
+ * SyntheticType javaType = new SyntheticTypeBuilder("Java")
+ *     .addMethod("type")
+ *         .parameter("className", "string")
+ *         .returns("Class")
+ *         .documentation("Loads a Java class by fully-qualified name.")
+ *         .done()
+ *     .build();
+ * 
+ */ +public class SyntheticTypeBuilder { + + private final String name; + private String documentation; + private final List methods = new ArrayList<>(); + private final List fields = new ArrayList<>(); + + public SyntheticTypeBuilder(String name) { + this.name = name; + } + + public SyntheticTypeBuilder documentation(String doc) { + this.documentation = doc; + return this; + } + + /** + * Start building a method for this type. + */ + public MethodBuilder addMethod(String methodName) { + return new MethodBuilder(this, methodName); + } + + /** + * Add a field to this type. + */ + public SyntheticTypeBuilder addField(String fieldName, String typeName, String doc) { + fields.add(new SyntheticField(fieldName, typeName, doc, false)); + return this; + } + + /** + * Add a static field to this type. + */ + public SyntheticTypeBuilder addStaticField(String fieldName, String typeName, String doc) { + fields.add(new SyntheticField(fieldName, typeName, doc, true)); + return this; + } + + void addBuiltMethod(SyntheticMethod method) { + methods.add(method); + } + + /** + * Build the synthetic type. + */ + public SyntheticType build() { + return new SyntheticType(name, documentation, methods, fields); + } + + // ==================== Inner classes ==================== + + /** + * Builder for methods within a synthetic type. + */ + public static class MethodBuilder { + private final SyntheticTypeBuilder parent; + private final String name; + private final List parameters = new ArrayList<>(); + private String returnType = "void"; + private String documentation; + private boolean isStatic = false; + private ReturnTypeResolver returnTypeResolver; + + MethodBuilder(SyntheticTypeBuilder parent, String name) { + this.parent = parent; + this.name = name; + } + + /** + * Add a parameter to this method. + */ + public MethodBuilder parameter(String paramName, String typeName) { + parameters.add(new SyntheticParameter(paramName, typeName, null)); + return this; + } + + /** + * Add a parameter with documentation. + */ + public MethodBuilder parameter(String paramName, String typeName, String doc) { + parameters.add(new SyntheticParameter(paramName, typeName, doc)); + return this; + } + + /** + * Set the return type. + */ + public MethodBuilder returns(String typeName) { + this.returnType = typeName; + return this; + } + + /** + * Set a dynamic return type resolver. + * Used for methods like Java.type() where return type depends on arguments. + */ + public MethodBuilder returnsResolved(ReturnTypeResolver resolver) { + this.returnTypeResolver = resolver; + return this; + } + + /** + * Set method documentation. + */ + public MethodBuilder documentation(String doc) { + this.documentation = doc; + return this; + } + + /** + * Mark this method as static. + */ + public MethodBuilder asStatic() { + this.isStatic = true; + return this; + } + + /** + * Finish building this method and return to the type builder. + */ + public SyntheticTypeBuilder done() { + parent.addBuiltMethod(new SyntheticMethod(name, returnType, parameters, documentation, isStatic, returnTypeResolver)); + return parent; + } + } + + /** + * Functional interface for resolving return types dynamically. + */ + @FunctionalInterface + public interface ReturnTypeResolver { + /** + * Resolve the return type given the arguments passed to the method. + * @param arguments The string arguments (for methods like Java.type("className")) + * @return The resolved TypeInfo, or null if cannot be resolved + */ + TypeInfo resolve(String[] arguments); + } + + // ==================== Data classes ==================== + + // ==================== SyntheticType (the result) ==================== +}