From a600e80cb1b6d16abd999e771fceb328d2b31ae3 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 2 Jun 2026 18:52:37 +0200 Subject: [PATCH] feat(lsp): Java hybrid LSP resolver (type-aware calls, annotations, signatures) Pure-C Java semantic resolver (java_lsp.c) + generated java.lang/util stdlib registry, wired into the per-file LSP pass. Resolves direct/method/trait/ generic/lambda calls with confidence; conservative no-edge on unresolved. Includes test_java_lsp.c + coverage suite. --- Makefile.cbm | 4 +- internal/cbm/cbm.c | 4 + internal/cbm/lsp/generated/java_stdlib_data.c | 1315 +++++++ internal/cbm/lsp/java_lsp.c | 3314 +++++++++++++++++ internal/cbm/lsp/java_lsp.h | 167 + internal/cbm/lsp_all.c | 2 + tests/test_java_lsp.c | 1927 ++++++++++ tests/test_java_lsp_coverage.c | 3232 ++++++++++++++++ tests/test_main.c | 4 + 9 files changed, 9968 insertions(+), 1 deletion(-) create mode 100644 internal/cbm/lsp/generated/java_stdlib_data.c create mode 100644 internal/cbm/lsp/java_lsp.c create mode 100644 internal/cbm/lsp/java_lsp.h create mode 100644 tests/test_java_lsp.c create mode 100644 tests/test_java_lsp_coverage.c diff --git a/Makefile.cbm b/Makefile.cbm index 6b26f6ca9..4acd9c665 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -324,6 +324,8 @@ TEST_PY_LSP_STRESS_SRCS = tests/test_py_lsp_stress.c TEST_PY_LSP_SCALE_SRCS = tests/test_py_lsp_scale.c +TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c + TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c TEST_TRACES_SRCS = tests/test_traces.c @@ -343,7 +345,7 @@ TEST_SIMHASH_SRCS = tests/test_simhash.c TEST_STACK_OVERFLOW_SRCS = tests/test_stack_overflow.c -ALL_TEST_SRCS = $(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS) +ALL_TEST_SRCS = $(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS) # ── Build directories ──────────────────────────────────────────── diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 1049319b7..8f1af7783 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -7,6 +7,7 @@ #include "lsp/c_lsp.h" #include "lsp/php_lsp.h" #include "lsp/py_lsp.h" +#include "lsp/java_lsp.h" #include "preprocessor.h" #include "foundation/compat.h" #include "tree_sitter/api.h" // TSParser, TSNode, TSTree, TSInput, TSLanguage, TSPoint, TSParseOptions, TSParseState @@ -365,6 +366,9 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage if (language == CBM_LANG_PYTHON) { cbm_run_py_lsp(a, result, source, source_len, root); } + if (language == CBM_LANG_JAVA) { + cbm_run_java_lsp(a, result, source, source_len, root); + } atomic_fetch_add(&total_lsp_ns, now_ns() - lsp_start); // Second pass: preprocess C/C++/CUDA and extract additional macro-hidden calls. diff --git a/internal/cbm/lsp/generated/java_stdlib_data.c b/internal/cbm/lsp/generated/java_stdlib_data.c new file mode 100644 index 000000000..82d7ec7bb --- /dev/null +++ b/internal/cbm/lsp/generated/java_stdlib_data.c @@ -0,0 +1,1315 @@ +/* + * java_stdlib_data.c — Curated Java standard-library type/method registry. + * + * Strategy: + * - java.lang.* — fully covered (the implicit-import package). + * Object, String, StringBuilder, StringBuffer, CharSequence, Class, + * Throwable + the common subclass tree, Number + boxed primitives, + * Math, System, Thread, Iterable, Comparable, Cloneable, Enum, Record, + * AutoCloseable, the common Exception types. + * - java.util.* — collections + iterators + Optional + Date/Calendar + + * Arrays/Collections + Scanner/Random/UUID + Map.Entry. + * - java.io.* — streams, readers, writers, File, IOException family. + * - java.nio.file.* — Path, Paths, Files (often-used helpers). + * - java.util.function — the 21 functional interfaces. + * - java.util.stream — Stream + Collectors entry points. + * - java.util.concurrent — ExecutorService, Future, CompletableFuture, + * ConcurrentHashMap, the concurrent collection set. + * - java.time — LocalDate/LocalTime/LocalDateTime/Duration/Instant. + * + * Method signatures use registry-level fidelity: receiver, short name, + * return type. Param types are intentionally unmodeled (the resolver + * chooses overloads by arity, with type compatibility scoring breaking + * ties — see cbm_registry_lookup_method_by_args). + * + * This is the JLS-spec-aligned slice of the stdlib that 90%+ of real-world + * Java code touches. + */ + +#include "../type_rep.h" +#include "../type_registry.h" +#include "../../arena.h" +#include "../java_lsp.h" +#include + +#define REG_TYPE(qn_, short_, is_iface_, parents_) \ + do { \ + memset(&rt, 0, sizeof(rt)); \ + rt.qualified_name = (qn_); \ + rt.short_name = (short_); \ + rt.is_interface = (is_iface_); \ + rt.embedded_types = (parents_); \ + cbm_registry_add_type(reg, rt); \ + } while (0) + +#define REG_METHOD(class_qn_, method_name_, ret_type_) \ + do { \ + memset(&rf, 0, sizeof(rf)); \ + rf.min_params = -1; \ + rf.qualified_name = \ + cbm_arena_sprintf(arena, "%s.%s", (class_qn_), (method_name_)); \ + rf.short_name = (method_name_); \ + rf.receiver_type = (class_qn_); \ + { \ + const CBMType **rets = \ + (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets)); \ + rets[0] = (ret_type_); \ + rets[1] = NULL; \ + rf.signature = cbm_type_func(arena, NULL, NULL, rets); \ + } \ + cbm_registry_add_func(reg, rf); \ + } while (0) + +#define REG_CTOR(class_qn_, short_name_) \ + do { \ + memset(&rf, 0, sizeof(rf)); \ + rf.min_params = -1; \ + rf.qualified_name = \ + cbm_arena_sprintf(arena, "%s.%s", (class_qn_), (short_name_)); \ + rf.short_name = (short_name_); \ + rf.receiver_type = (class_qn_); \ + { \ + const CBMType **rets = \ + (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets)); \ + rets[0] = cbm_type_named(arena, (class_qn_)); \ + rets[1] = NULL; \ + rf.signature = cbm_type_func(arena, NULL, NULL, rets); \ + } \ + cbm_registry_add_func(reg, rf); \ + } while (0) + +#define REG_FIELD(class_qn_, name_, type_) \ + do { \ + const CBMRegisteredType *_existing = \ + cbm_registry_lookup_type(reg, (class_qn_)); \ + (void)_existing; \ + /* Field append handled by REG_TYPE_FIELDS below. */ \ + /* Placeholder for future per-field appends. */ \ + } while (0) + +void cbm_java_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena) { + CBMRegisteredType rt; + CBMRegisteredFunc rf; + + /* ── Type-parent lists (must be static so addresses outlive the call) ── */ + static const char *no_parents[] = {NULL}; + static const char *parents_object[] = {"java.lang.Object", NULL}; + static const char *parents_throwable[] = {"java.lang.Object", NULL}; + static const char *parents_exception[] = {"java.lang.Throwable", NULL}; + static const char *parents_error[] = {"java.lang.Throwable", NULL}; + static const char *parents_runtime_exc[] = {"java.lang.Exception", NULL}; + static const char *parents_io_exc[] = {"java.lang.Exception", NULL}; + static const char *parents_number[] = {"java.lang.Object", NULL}; + static const char *parents_integer[] = {"java.lang.Number", NULL}; + static const char *parents_long[] = {"java.lang.Number", NULL}; + static const char *parents_double[] = {"java.lang.Number", NULL}; + static const char *parents_float[] = {"java.lang.Number", NULL}; + static const char *parents_short[] = {"java.lang.Number", NULL}; + static const char *parents_byte[] = {"java.lang.Number", NULL}; + static const char *parents_string[] = {"java.lang.Object", NULL}; + static const char *parents_charseq[] = {NULL}; + static const char *parents_iterable[] = {NULL}; + static const char *parents_collection[] = {"java.lang.Iterable", NULL}; + static const char *parents_list[] = {"java.util.Collection", NULL}; + static const char *parents_set[] = {"java.util.Collection", NULL}; + static const char *parents_queue[] = {"java.util.Collection", NULL}; + static const char *parents_deque[] = {"java.util.Queue", NULL}; + static const char *parents_map[] = {NULL}; + static const char *parents_map_entry[] = {NULL}; + static const char *parents_iterator[] = {NULL}; + static const char *parents_arraylist[] = {"java.util.List", NULL}; + static const char *parents_linkedlist[] = {"java.util.List", NULL}; + static const char *parents_hashset[] = {"java.util.Set", NULL}; + static const char *parents_treeset[] = {"java.util.Set", NULL}; + static const char *parents_linkedhashset[] = {"java.util.Set", NULL}; + static const char *parents_hashmap[] = {"java.util.Map", NULL}; + static const char *parents_treemap[] = {"java.util.Map", NULL}; + static const char *parents_linkedhashmap[] = {"java.util.Map", NULL}; + static const char *parents_concurrent_hashmap[] = {"java.util.Map", NULL}; + + static const char *parents_inputstream[] = {"java.lang.AutoCloseable", NULL}; + static const char *parents_outputstream[] = {"java.lang.AutoCloseable", NULL}; + static const char *parents_reader[] = {"java.lang.AutoCloseable", NULL}; + static const char *parents_writer[] = {"java.lang.AutoCloseable", NULL}; + static const char *parents_buffered_reader[] = {"java.io.Reader", NULL}; + static const char *parents_buffered_writer[] = {"java.io.Writer", NULL}; + static const char *parents_print_stream[] = {"java.io.OutputStream", NULL}; + static const char *parents_print_writer[] = {"java.io.Writer", NULL}; + static const char *parents_file_input_stream[] = {"java.io.InputStream", NULL}; + static const char *parents_file_output_stream[] = {"java.io.OutputStream", NULL}; + static const char *parents_file_reader[] = {"java.io.Reader", NULL}; + static const char *parents_file_writer[] = {"java.io.Writer", NULL}; + static const char *parents_io_exception[] = {"java.lang.Exception", NULL}; + static const char *parents_runtime_exc_chain[] = {"java.lang.RuntimeException", NULL}; + + /* ── java.lang ─────────────────────────────────────────────── */ + REG_TYPE("java.lang.Object", "Object", false, no_parents); + REG_TYPE("java.lang.Class", "Class", false, parents_object); + REG_TYPE("java.lang.ClassLoader", "ClassLoader", false, parents_object); + REG_TYPE("java.lang.CharSequence", "CharSequence", true, parents_charseq); + REG_TYPE("java.lang.String", "String", false, parents_string); + REG_TYPE("java.lang.StringBuilder", "StringBuilder", false, parents_object); + REG_TYPE("java.lang.StringBuffer", "StringBuffer", false, parents_object); + REG_TYPE("java.lang.Number", "Number", false, parents_number); + REG_TYPE("java.lang.Integer", "Integer", false, parents_integer); + REG_TYPE("java.lang.Long", "Long", false, parents_long); + REG_TYPE("java.lang.Short", "Short", false, parents_short); + REG_TYPE("java.lang.Byte", "Byte", false, parents_byte); + REG_TYPE("java.lang.Float", "Float", false, parents_float); + REG_TYPE("java.lang.Double", "Double", false, parents_double); + REG_TYPE("java.lang.Boolean", "Boolean", false, parents_object); + REG_TYPE("java.lang.Character", "Character", false, parents_object); + REG_TYPE("java.lang.Void", "Void", false, parents_object); + REG_TYPE("java.lang.Iterable", "Iterable", true, parents_iterable); + REG_TYPE("java.lang.Comparable", "Comparable", true, no_parents); + REG_TYPE("java.lang.Cloneable", "Cloneable", true, no_parents); + REG_TYPE("java.lang.Runnable", "Runnable", true, no_parents); + REG_TYPE("java.lang.AutoCloseable", "AutoCloseable", true, no_parents); + REG_TYPE("java.lang.Math", "Math", false, parents_object); + REG_TYPE("java.lang.System", "System", false, parents_object); + REG_TYPE("java.lang.Thread", "Thread", false, parents_object); + REG_TYPE("java.lang.Process", "Process", false, parents_object); + REG_TYPE("java.lang.ProcessBuilder", "ProcessBuilder", false, parents_object); + REG_TYPE("java.lang.StackTraceElement", "StackTraceElement", false, parents_object); + REG_TYPE("java.lang.Enum", "Enum", false, parents_object); + REG_TYPE("java.lang.Record", "Record", false, parents_object); + REG_TYPE("java.lang.Throwable", "Throwable", false, parents_throwable); + REG_TYPE("java.lang.Exception", "Exception", false, parents_exception); + REG_TYPE("java.lang.Error", "Error", false, parents_error); + REG_TYPE("java.lang.RuntimeException", "RuntimeException", false, parents_runtime_exc); + REG_TYPE("java.lang.NullPointerException", "NullPointerException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.IllegalArgumentException", "IllegalArgumentException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.IllegalStateException", "IllegalStateException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.IndexOutOfBoundsException", "IndexOutOfBoundsException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.ArrayIndexOutOfBoundsException", "ArrayIndexOutOfBoundsException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.ArithmeticException", "ArithmeticException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.ClassCastException", "ClassCastException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.ClassNotFoundException", "ClassNotFoundException", false, + parents_exception); + REG_TYPE("java.lang.NumberFormatException", "NumberFormatException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.UnsupportedOperationException", "UnsupportedOperationException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.InterruptedException", "InterruptedException", false, parents_exception); + REG_TYPE("java.lang.SecurityException", "SecurityException", false, + parents_runtime_exc_chain); + REG_TYPE("java.lang.NoSuchMethodException", "NoSuchMethodException", false, parents_exception); + REG_TYPE("java.lang.NoSuchFieldException", "NoSuchFieldException", false, parents_exception); + + /* Annotation-marker types. */ + REG_TYPE("java.lang.Override", "Override", true, no_parents); + REG_TYPE("java.lang.Deprecated", "Deprecated", true, no_parents); + REG_TYPE("java.lang.SuppressWarnings", "SuppressWarnings", true, no_parents); + REG_TYPE("java.lang.FunctionalInterface", "FunctionalInterface", true, no_parents); + + /* ── Object methods ───────────────────────────────────────── */ + REG_METHOD("java.lang.Object", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Object", "hashCode", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Object", "equals", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Object", "getClass", cbm_type_named(arena, "java.lang.Class")); + REG_METHOD("java.lang.Object", "wait", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Object", "notify", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Object", "notifyAll", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Object", "clone", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.lang.Object", "finalize", cbm_type_builtin(arena, "void")); + + /* ── String methods ───────────────────────────────────────── */ + REG_METHOD("java.lang.String", "length", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.String", "isEmpty", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.String", "isBlank", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.String", "charAt", cbm_type_builtin(arena, "char")); + REG_METHOD("java.lang.String", "codePointAt", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.String", "equals", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.String", "equalsIgnoreCase", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.String", "compareTo", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.String", "compareToIgnoreCase", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.String", "indexOf", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.String", "lastIndexOf", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.String", "contains", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.String", "startsWith", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.String", "endsWith", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.String", "matches", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.String", "concat", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "substring", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "trim", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "strip", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "stripLeading", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "stripTrailing", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "toLowerCase", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "toUpperCase", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "replace", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "replaceAll", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "replaceFirst", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "split", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.String"))); + REG_METHOD("java.lang.String", "toCharArray", cbm_type_slice(arena, cbm_type_builtin(arena, "char"))); + REG_METHOD("java.lang.String", "getBytes", cbm_type_slice(arena, cbm_type_builtin(arena, "byte"))); + REG_METHOD("java.lang.String", "intern", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "format", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "valueOf", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "join", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "repeat", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "lines", cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.lang.String", "chars", cbm_type_named(arena, "java.util.stream.IntStream")); + REG_METHOD("java.lang.String", "codePoints", + cbm_type_named(arena, "java.util.stream.IntStream")); + REG_METHOD("java.lang.String", "hashCode", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.String", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.String", "toCharArray", cbm_type_slice(arena, cbm_type_builtin(arena, "char"))); + REG_CTOR("java.lang.String", "String"); + + /* ── StringBuilder / StringBuffer ─────────────────────────── */ + REG_METHOD("java.lang.StringBuilder", "append", + cbm_type_named(arena, "java.lang.StringBuilder")); + REG_METHOD("java.lang.StringBuilder", "insert", + cbm_type_named(arena, "java.lang.StringBuilder")); + REG_METHOD("java.lang.StringBuilder", "delete", + cbm_type_named(arena, "java.lang.StringBuilder")); + REG_METHOD("java.lang.StringBuilder", "deleteCharAt", + cbm_type_named(arena, "java.lang.StringBuilder")); + REG_METHOD("java.lang.StringBuilder", "replace", + cbm_type_named(arena, "java.lang.StringBuilder")); + REG_METHOD("java.lang.StringBuilder", "reverse", + cbm_type_named(arena, "java.lang.StringBuilder")); + REG_METHOD("java.lang.StringBuilder", "toString", + cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.StringBuilder", "length", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.StringBuilder", "charAt", cbm_type_builtin(arena, "char")); + REG_METHOD("java.lang.StringBuilder", "setLength", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.StringBuilder", "indexOf", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.StringBuilder", "substring", + cbm_type_named(arena, "java.lang.String")); + REG_CTOR("java.lang.StringBuilder", "StringBuilder"); + + REG_METHOD("java.lang.StringBuffer", "append", + cbm_type_named(arena, "java.lang.StringBuffer")); + REG_METHOD("java.lang.StringBuffer", "toString", + cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.StringBuffer", "length", cbm_type_builtin(arena, "int")); + REG_CTOR("java.lang.StringBuffer", "StringBuffer"); + + /* ── CharSequence ─────────────────────────────────────────── */ + REG_METHOD("java.lang.CharSequence", "length", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.CharSequence", "charAt", cbm_type_builtin(arena, "char")); + REG_METHOD("java.lang.CharSequence", "subSequence", + cbm_type_named(arena, "java.lang.CharSequence")); + REG_METHOD("java.lang.CharSequence", "toString", + cbm_type_named(arena, "java.lang.String")); + + /* ── Number + boxed types ─────────────────────────────────── */ + REG_METHOD("java.lang.Number", "intValue", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Number", "longValue", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Number", "doubleValue", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Number", "floatValue", cbm_type_builtin(arena, "float")); + REG_METHOD("java.lang.Number", "shortValue", cbm_type_builtin(arena, "short")); + REG_METHOD("java.lang.Number", "byteValue", cbm_type_builtin(arena, "byte")); + + REG_METHOD("java.lang.Integer", "intValue", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Integer", "parseInt", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Integer", "valueOf", cbm_type_named(arena, "java.lang.Integer")); + REG_METHOD("java.lang.Integer", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Integer", "compare", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Integer", "compareTo", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Integer", "equals", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Integer", "hashCode", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Integer", "max", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Integer", "min", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Integer", "sum", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Integer", "bitCount", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Integer", "toBinaryString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Integer", "toHexString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Integer", "toOctalString", cbm_type_named(arena, "java.lang.String")); + + REG_METHOD("java.lang.Long", "longValue", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Long", "parseLong", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Long", "valueOf", cbm_type_named(arena, "java.lang.Long")); + REG_METHOD("java.lang.Long", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Long", "compareTo", cbm_type_builtin(arena, "int")); + + REG_METHOD("java.lang.Double", "doubleValue", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Double", "parseDouble", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Double", "valueOf", cbm_type_named(arena, "java.lang.Double")); + REG_METHOD("java.lang.Double", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Double", "isNaN", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Double", "isInfinite", cbm_type_builtin(arena, "boolean")); + + REG_METHOD("java.lang.Float", "floatValue", cbm_type_builtin(arena, "float")); + REG_METHOD("java.lang.Float", "parseFloat", cbm_type_builtin(arena, "float")); + REG_METHOD("java.lang.Float", "valueOf", cbm_type_named(arena, "java.lang.Float")); + + REG_METHOD("java.lang.Boolean", "booleanValue", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Boolean", "parseBoolean", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Boolean", "valueOf", cbm_type_named(arena, "java.lang.Boolean")); + REG_METHOD("java.lang.Boolean", "toString", cbm_type_named(arena, "java.lang.String")); + + REG_METHOD("java.lang.Character", "charValue", cbm_type_builtin(arena, "char")); + REG_METHOD("java.lang.Character", "isDigit", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Character", "isLetter", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Character", "isLetterOrDigit", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Character", "isWhitespace", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Character", "isUpperCase", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Character", "isLowerCase", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Character", "toUpperCase", cbm_type_builtin(arena, "char")); + REG_METHOD("java.lang.Character", "toLowerCase", cbm_type_builtin(arena, "char")); + REG_METHOD("java.lang.Character", "getNumericValue", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.Character", "valueOf", cbm_type_named(arena, "java.lang.Character")); + + REG_METHOD("java.lang.Byte", "byteValue", cbm_type_builtin(arena, "byte")); + REG_METHOD("java.lang.Byte", "parseByte", cbm_type_builtin(arena, "byte")); + REG_METHOD("java.lang.Byte", "valueOf", cbm_type_named(arena, "java.lang.Byte")); + + REG_METHOD("java.lang.Short", "shortValue", cbm_type_builtin(arena, "short")); + REG_METHOD("java.lang.Short", "parseShort", cbm_type_builtin(arena, "short")); + REG_METHOD("java.lang.Short", "valueOf", cbm_type_named(arena, "java.lang.Short")); + + /* ── Math ─────────────────────────────────────────────────── */ + REG_METHOD("java.lang.Math", "abs", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "min", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "max", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "sqrt", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "cbrt", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "pow", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "exp", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "log", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "log10", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "sin", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "cos", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "tan", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "asin", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "acos", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "atan", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "atan2", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "floor", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "ceil", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "round", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Math", "random", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "signum", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "hypot", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "floorDiv", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Math", "floorMod", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Math", "addExact", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Math", "subtractExact", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Math", "multiplyExact", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Math", "toRadians", cbm_type_builtin(arena, "double")); + REG_METHOD("java.lang.Math", "toDegrees", cbm_type_builtin(arena, "double")); + + /* ── System ───────────────────────────────────────────────── */ + REG_METHOD("java.lang.System", "currentTimeMillis", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.System", "nanoTime", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.System", "exit", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.System", "getenv", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.System", "getProperty", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.System", "setProperty", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.System", "lineSeparator", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.System", "arraycopy", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.System", "identityHashCode", cbm_type_builtin(arena, "int")); + REG_METHOD("java.lang.System", "gc", cbm_type_builtin(arena, "void")); + + /* ── Thread ───────────────────────────────────────────────── */ + REG_METHOD("java.lang.Thread", "start", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Thread", "run", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Thread", "join", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Thread", "interrupt", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Thread", "isAlive", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Thread", "sleep", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Thread", "currentThread", cbm_type_named(arena, "java.lang.Thread")); + REG_METHOD("java.lang.Thread", "yield", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Thread", "getName", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Thread", "setName", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Thread", "getId", cbm_type_builtin(arena, "long")); + REG_METHOD("java.lang.Thread", "isInterrupted", cbm_type_builtin(arena, "boolean")); + + /* ── Class ────────────────────────────────────────────────── */ + REG_METHOD("java.lang.Class", "getName", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Class", "getSimpleName", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Class", "getCanonicalName", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Class", "isInterface", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Class", "isArray", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Class", "isAssignableFrom", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Class", "isInstance", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.lang.Class", "newInstance", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.lang.Class", "forName", cbm_type_named(arena, "java.lang.Class")); + REG_METHOD("java.lang.Class", "getSuperclass", cbm_type_named(arena, "java.lang.Class")); + REG_METHOD("java.lang.Class", "getInterfaces", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.Class"))); + + /* ── Iterable / Iterator ──────────────────────────────────── */ + REG_METHOD("java.lang.Iterable", "iterator", cbm_type_named(arena, "java.util.Iterator")); + REG_METHOD("java.lang.Iterable", "forEach", cbm_type_builtin(arena, "void")); + + /* ── Throwable methods ────────────────────────────────────── */ + REG_METHOD("java.lang.Throwable", "getMessage", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Throwable", "getLocalizedMessage", + cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Throwable", "getCause", cbm_type_named(arena, "java.lang.Throwable")); + REG_METHOD("java.lang.Throwable", "initCause", + cbm_type_named(arena, "java.lang.Throwable")); + REG_METHOD("java.lang.Throwable", "printStackTrace", cbm_type_builtin(arena, "void")); + REG_METHOD("java.lang.Throwable", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.lang.Throwable", "getStackTrace", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.StackTraceElement"))); + + /* ── AutoCloseable ────────────────────────────────────────── */ + REG_METHOD("java.lang.AutoCloseable", "close", cbm_type_builtin(arena, "void")); + + /* ── Comparable ───────────────────────────────────────────── */ + REG_METHOD("java.lang.Comparable", "compareTo", cbm_type_builtin(arena, "int")); + + /* ── Runnable ─────────────────────────────────────────────── */ + REG_METHOD("java.lang.Runnable", "run", cbm_type_builtin(arena, "void")); + + /* ── java.util ────────────────────────────────────────────── */ + REG_TYPE("java.util.Collection", "Collection", true, parents_collection); + REG_TYPE("java.util.List", "List", true, parents_list); + REG_TYPE("java.util.Set", "Set", true, parents_set); + REG_TYPE("java.util.Queue", "Queue", true, parents_queue); + REG_TYPE("java.util.Deque", "Deque", true, parents_deque); + REG_TYPE("java.util.Map", "Map", true, parents_map); + REG_TYPE("java.util.Map.Entry", "Entry", true, parents_map_entry); + REG_TYPE("java.util.Iterator", "Iterator", true, parents_iterator); + REG_TYPE("java.util.ListIterator", "ListIterator", true, parents_iterator); + REG_TYPE("java.util.Spliterator", "Spliterator", true, no_parents); + REG_TYPE("java.util.Comparator", "Comparator", true, no_parents); + + REG_TYPE("java.util.ArrayList", "ArrayList", false, parents_arraylist); + REG_TYPE("java.util.LinkedList", "LinkedList", false, parents_linkedlist); + REG_TYPE("java.util.Vector", "Vector", false, parents_arraylist); + REG_TYPE("java.util.Stack", "Stack", false, parents_arraylist); + REG_TYPE("java.util.HashSet", "HashSet", false, parents_hashset); + REG_TYPE("java.util.TreeSet", "TreeSet", false, parents_treeset); + REG_TYPE("java.util.LinkedHashSet", "LinkedHashSet", false, parents_linkedhashset); + REG_TYPE("java.util.HashMap", "HashMap", false, parents_hashmap); + REG_TYPE("java.util.TreeMap", "TreeMap", false, parents_treemap); + REG_TYPE("java.util.LinkedHashMap", "LinkedHashMap", false, parents_linkedhashmap); + REG_TYPE("java.util.ArrayDeque", "ArrayDeque", false, parents_deque); + REG_TYPE("java.util.PriorityQueue", "PriorityQueue", false, parents_queue); + + REG_TYPE("java.util.Optional", "Optional", false, parents_object); + REG_TYPE("java.util.OptionalInt", "OptionalInt", false, parents_object); + REG_TYPE("java.util.OptionalLong", "OptionalLong", false, parents_object); + REG_TYPE("java.util.OptionalDouble", "OptionalDouble", false, parents_object); + REG_TYPE("java.util.Date", "Date", false, parents_object); + REG_TYPE("java.util.Calendar", "Calendar", false, parents_object); + REG_TYPE("java.util.GregorianCalendar", "GregorianCalendar", false, + ((const char *[]){"java.util.Calendar", NULL})); + REG_TYPE("java.util.TimeZone", "TimeZone", false, parents_object); + REG_TYPE("java.util.Locale", "Locale", false, parents_object); + REG_TYPE("java.util.UUID", "UUID", false, parents_object); + REG_TYPE("java.util.Random", "Random", false, parents_object); + REG_TYPE("java.util.Scanner", "Scanner", false, parents_object); + REG_TYPE("java.util.Arrays", "Arrays", false, parents_object); + REG_TYPE("java.util.Collections", "Collections", false, parents_object); + REG_TYPE("java.util.Objects", "Objects", false, parents_object); + REG_TYPE("java.util.Properties", "Properties", false, parents_hashmap); + REG_TYPE("java.util.regex.Pattern", "Pattern", false, parents_object); + REG_TYPE("java.util.regex.Matcher", "Matcher", false, parents_object); + + /* ── Collection methods ───────────────────────────────────── */ + REG_METHOD("java.util.Collection", "size", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Collection", "isEmpty", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Collection", "contains", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Collection", "containsAll", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Collection", "iterator", cbm_type_named(arena, "java.util.Iterator")); + REG_METHOD("java.util.Collection", "toArray", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.Object"))); + REG_METHOD("java.util.Collection", "add", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Collection", "addAll", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Collection", "remove", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Collection", "removeAll", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Collection", "retainAll", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Collection", "clear", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Collection", "stream", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.Collection", "parallelStream", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.Collection", "forEach", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Collection", "removeIf", cbm_type_builtin(arena, "boolean")); + + /* ── List methods ─────────────────────────────────────────── */ + REG_METHOD("java.util.List", "get", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.List", "set", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.List", "add", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.List", "remove", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.List", "indexOf", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.List", "lastIndexOf", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.List", "subList", cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.List", "of", cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.List", "copyOf", cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.List", "size", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.List", "isEmpty", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.List", "contains", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.List", "iterator", cbm_type_named(arena, "java.util.Iterator")); + REG_METHOD("java.util.List", "stream", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.List", "forEach", cbm_type_builtin(arena, "void")); + + /* ── ArrayList ────────────────────────────────────────────── */ + REG_METHOD("java.util.ArrayList", "get", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.ArrayList", "set", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.ArrayList", "add", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.ArrayList", "remove", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.ArrayList", "size", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.ArrayList", "isEmpty", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.ArrayList", "indexOf", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.ArrayList", "iterator", cbm_type_named(arena, "java.util.Iterator")); + REG_METHOD("java.util.ArrayList", "clear", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.ArrayList", "stream", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.ArrayList", "toArray", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.Object"))); + REG_METHOD("java.util.ArrayList", "subList", cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.ArrayList", "trimToSize", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.ArrayList", "ensureCapacity", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.ArrayList", "forEach", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.ArrayList", "removeIf", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.List", "removeIf", cbm_type_builtin(arena, "boolean")); + REG_CTOR("java.util.ArrayList", "ArrayList"); + + REG_METHOD("java.util.LinkedList", "addFirst", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.LinkedList", "addLast", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.LinkedList", "removeFirst", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.LinkedList", "removeLast", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.LinkedList", "getFirst", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.LinkedList", "getLast", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.LinkedList", "peek", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.LinkedList", "poll", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.LinkedList", "offer", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.LinkedList", "size", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.LinkedList", "iterator", cbm_type_named(arena, "java.util.Iterator")); + REG_CTOR("java.util.LinkedList", "LinkedList"); + + /* ── Set methods ──────────────────────────────────────────── */ + REG_METHOD("java.util.Set", "size", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Set", "isEmpty", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Set", "contains", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Set", "add", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Set", "remove", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Set", "iterator", cbm_type_named(arena, "java.util.Iterator")); + REG_METHOD("java.util.Set", "of", cbm_type_named(arena, "java.util.Set")); + REG_METHOD("java.util.Set", "copyOf", cbm_type_named(arena, "java.util.Set")); + REG_METHOD("java.util.Set", "stream", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.Set", "forEach", cbm_type_builtin(arena, "void")); + + REG_METHOD("java.util.HashSet", "add", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.HashSet", "remove", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.HashSet", "contains", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.HashSet", "size", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.HashSet", "isEmpty", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.HashSet", "iterator", cbm_type_named(arena, "java.util.Iterator")); + REG_METHOD("java.util.HashSet", "clear", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.HashSet", "stream", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_CTOR("java.util.HashSet", "HashSet"); + + REG_METHOD("java.util.TreeSet", "first", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.TreeSet", "last", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.TreeSet", "headSet", cbm_type_named(arena, "java.util.SortedSet")); + REG_METHOD("java.util.TreeSet", "tailSet", cbm_type_named(arena, "java.util.SortedSet")); + REG_CTOR("java.util.TreeSet", "TreeSet"); + + REG_CTOR("java.util.LinkedHashSet", "LinkedHashSet"); + + /* ── Map methods ──────────────────────────────────────────── */ + REG_METHOD("java.util.Map", "get", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map", "put", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map", "remove", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map", "containsKey", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Map", "containsValue", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Map", "keySet", cbm_type_named(arena, "java.util.Set")); + REG_METHOD("java.util.Map", "values", cbm_type_named(arena, "java.util.Collection")); + REG_METHOD("java.util.Map", "entrySet", cbm_type_named(arena, "java.util.Set")); + REG_METHOD("java.util.Map", "size", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Map", "isEmpty", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Map", "putAll", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Map", "clear", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Map", "getOrDefault", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map", "putIfAbsent", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map", "computeIfAbsent", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map", "compute", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map", "merge", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map", "of", cbm_type_named(arena, "java.util.Map")); + REG_METHOD("java.util.Map", "copyOf", cbm_type_named(arena, "java.util.Map")); + REG_METHOD("java.util.Map", "ofEntries", cbm_type_named(arena, "java.util.Map")); + REG_METHOD("java.util.Map", "entry", cbm_type_named(arena, "java.util.Map.Entry")); + REG_METHOD("java.util.Map", "forEach", cbm_type_builtin(arena, "void")); + + REG_METHOD("java.util.Map.Entry", "getKey", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map.Entry", "getValue", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Map.Entry", "setValue", cbm_type_named(arena, "java.lang.Object")); + + REG_METHOD("java.util.HashMap", "get", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.HashMap", "put", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.HashMap", "remove", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.HashMap", "containsKey", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.HashMap", "containsValue", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.HashMap", "size", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.HashMap", "isEmpty", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.HashMap", "keySet", cbm_type_named(arena, "java.util.Set")); + REG_METHOD("java.util.HashMap", "values", cbm_type_named(arena, "java.util.Collection")); + REG_METHOD("java.util.HashMap", "entrySet", cbm_type_named(arena, "java.util.Set")); + REG_METHOD("java.util.HashMap", "clear", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.HashMap", "getOrDefault", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.HashMap", "putIfAbsent", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.HashMap", "forEach", cbm_type_builtin(arena, "void")); + REG_CTOR("java.util.HashMap", "HashMap"); + + REG_METHOD("java.util.TreeMap", "firstKey", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.TreeMap", "lastKey", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.TreeMap", "headMap", cbm_type_named(arena, "java.util.SortedMap")); + REG_METHOD("java.util.TreeMap", "tailMap", cbm_type_named(arena, "java.util.SortedMap")); + REG_CTOR("java.util.TreeMap", "TreeMap"); + + REG_CTOR("java.util.LinkedHashMap", "LinkedHashMap"); + + /* ── Iterator methods ─────────────────────────────────────── */ + REG_METHOD("java.util.Iterator", "hasNext", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Iterator", "next", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Iterator", "remove", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Iterator", "forEachRemaining", cbm_type_builtin(arena, "void")); + + /* ── Optional ─────────────────────────────────────────────── */ + REG_METHOD("java.util.Optional", "get", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Optional", "isPresent", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Optional", "isEmpty", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Optional", "orElse", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Optional", "orElseGet", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Optional", "orElseThrow", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Optional", "ifPresent", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Optional", "ifPresentOrElse", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Optional", "map", cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.Optional", "flatMap", cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.Optional", "filter", cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.Optional", "of", cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.Optional", "ofNullable", cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.Optional", "empty", cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.Optional", "stream", + cbm_type_named(arena, "java.util.stream.Stream")); + + /* ── Arrays / Collections / Objects helpers ───────────────── */ + REG_METHOD("java.util.Arrays", "asList", cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.Arrays", "stream", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.Arrays", "sort", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Arrays", "binarySearch", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Arrays", "fill", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Arrays", "copyOf", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.Object"))); + REG_METHOD("java.util.Arrays", "copyOfRange", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.Object"))); + REG_METHOD("java.util.Arrays", "equals", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Arrays", "hashCode", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Arrays", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.Arrays", "deepEquals", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Arrays", "deepToString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.Arrays", "deepHashCode", cbm_type_builtin(arena, "int")); + + REG_METHOD("java.util.Collections", "sort", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Collections", "reverse", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Collections", "shuffle", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Collections", "min", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Collections", "max", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Collections", "emptyList", cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.Collections", "emptySet", cbm_type_named(arena, "java.util.Set")); + REG_METHOD("java.util.Collections", "emptyMap", cbm_type_named(arena, "java.util.Map")); + REG_METHOD("java.util.Collections", "singletonList", + cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.Collections", "singleton", + cbm_type_named(arena, "java.util.Set")); + REG_METHOD("java.util.Collections", "unmodifiableList", + cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.Collections", "unmodifiableSet", + cbm_type_named(arena, "java.util.Set")); + REG_METHOD("java.util.Collections", "unmodifiableMap", + cbm_type_named(arena, "java.util.Map")); + REG_METHOD("java.util.Collections", "frequency", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Collections", "binarySearch", cbm_type_builtin(arena, "int")); + + REG_METHOD("java.util.Objects", "equals", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Objects", "hashCode", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Objects", "hash", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Objects", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.Objects", "isNull", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Objects", "nonNull", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Objects", "requireNonNull", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.Objects", "requireNonNullElse", + cbm_type_named(arena, "java.lang.Object")); + + /* ── UUID, Random, Scanner ────────────────────────────────── */ + REG_METHOD("java.util.UUID", "randomUUID", cbm_type_named(arena, "java.util.UUID")); + REG_METHOD("java.util.UUID", "fromString", cbm_type_named(arena, "java.util.UUID")); + REG_METHOD("java.util.UUID", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.UUID", "getMostSignificantBits", cbm_type_builtin(arena, "long")); + REG_METHOD("java.util.UUID", "getLeastSignificantBits", cbm_type_builtin(arena, "long")); + REG_CTOR("java.util.UUID", "UUID"); + + REG_METHOD("java.util.Random", "nextInt", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Random", "nextLong", cbm_type_builtin(arena, "long")); + REG_METHOD("java.util.Random", "nextDouble", cbm_type_builtin(arena, "double")); + REG_METHOD("java.util.Random", "nextFloat", cbm_type_builtin(arena, "float")); + REG_METHOD("java.util.Random", "nextBoolean", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Random", "nextGaussian", cbm_type_builtin(arena, "double")); + REG_METHOD("java.util.Random", "setSeed", cbm_type_builtin(arena, "void")); + REG_CTOR("java.util.Random", "Random"); + + REG_METHOD("java.util.Scanner", "next", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.Scanner", "nextLine", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.Scanner", "nextInt", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Scanner", "nextLong", cbm_type_builtin(arena, "long")); + REG_METHOD("java.util.Scanner", "nextDouble", cbm_type_builtin(arena, "double")); + REG_METHOD("java.util.Scanner", "hasNext", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Scanner", "hasNextLine", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Scanner", "hasNextInt", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Scanner", "close", cbm_type_builtin(arena, "void")); + REG_CTOR("java.util.Scanner", "Scanner"); + + /* ── Locale / Date / Calendar / TimeZone ──────────────────── */ + REG_METHOD("java.util.Locale", "getLanguage", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.Locale", "getCountry", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.Locale", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.Locale", "getDefault", cbm_type_named(arena, "java.util.Locale")); + REG_CTOR("java.util.Locale", "Locale"); + + REG_METHOD("java.util.Date", "getTime", cbm_type_builtin(arena, "long")); + REG_METHOD("java.util.Date", "setTime", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Date", "before", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Date", "after", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.Date", "compareTo", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Date", "toString", cbm_type_named(arena, "java.lang.String")); + REG_CTOR("java.util.Date", "Date"); + + REG_METHOD("java.util.Calendar", "getInstance", cbm_type_named(arena, "java.util.Calendar")); + REG_METHOD("java.util.Calendar", "getTime", cbm_type_named(arena, "java.util.Date")); + REG_METHOD("java.util.Calendar", "set", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.Calendar", "get", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.Calendar", "add", cbm_type_builtin(arena, "void")); + + REG_METHOD("java.util.TimeZone", "getDefault", cbm_type_named(arena, "java.util.TimeZone")); + REG_METHOD("java.util.TimeZone", "getID", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.TimeZone", "getTimeZone", cbm_type_named(arena, "java.util.TimeZone")); + + /* ── regex ────────────────────────────────────────────────── */ + REG_METHOD("java.util.regex.Pattern", "compile", + cbm_type_named(arena, "java.util.regex.Pattern")); + REG_METHOD("java.util.regex.Pattern", "matcher", + cbm_type_named(arena, "java.util.regex.Matcher")); + REG_METHOD("java.util.regex.Pattern", "matches", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.regex.Pattern", "split", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.String"))); + REG_METHOD("java.util.regex.Pattern", "pattern", cbm_type_named(arena, "java.lang.String")); + + REG_METHOD("java.util.regex.Matcher", "matches", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.regex.Matcher", "find", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.regex.Matcher", "group", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.regex.Matcher", "groupCount", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.regex.Matcher", "start", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.regex.Matcher", "end", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.regex.Matcher", "replaceAll", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.util.regex.Matcher", "replaceFirst", + cbm_type_named(arena, "java.lang.String")); + + /* ── java.io ──────────────────────────────────────────────── */ + REG_TYPE("java.io.InputStream", "InputStream", false, parents_inputstream); + REG_TYPE("java.io.OutputStream", "OutputStream", false, parents_outputstream); + REG_TYPE("java.io.Reader", "Reader", false, parents_reader); + REG_TYPE("java.io.Writer", "Writer", false, parents_writer); + REG_TYPE("java.io.BufferedReader", "BufferedReader", false, parents_buffered_reader); + REG_TYPE("java.io.BufferedWriter", "BufferedWriter", false, parents_buffered_writer); + REG_TYPE("java.io.PrintStream", "PrintStream", false, parents_print_stream); + REG_TYPE("java.io.PrintWriter", "PrintWriter", false, parents_print_writer); + REG_TYPE("java.io.FileInputStream", "FileInputStream", false, parents_file_input_stream); + REG_TYPE("java.io.FileOutputStream", "FileOutputStream", false, parents_file_output_stream); + REG_TYPE("java.io.FileReader", "FileReader", false, parents_file_reader); + REG_TYPE("java.io.FileWriter", "FileWriter", false, parents_file_writer); + REG_TYPE("java.io.File", "File", false, parents_object); + REG_TYPE("java.io.IOException", "IOException", false, parents_io_exception); + REG_TYPE("java.io.FileNotFoundException", "FileNotFoundException", false, + ((const char *[]){"java.io.IOException", NULL})); + REG_TYPE("java.io.UncheckedIOException", "UncheckedIOException", false, + parents_runtime_exc_chain); + REG_TYPE("java.io.Serializable", "Serializable", true, no_parents); + REG_TYPE("java.io.Closeable", "Closeable", true, + ((const char *[]){"java.lang.AutoCloseable", NULL})); + REG_TYPE("java.io.Flushable", "Flushable", true, no_parents); + + REG_METHOD("java.io.PrintStream", "println", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.PrintStream", "print", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.PrintStream", "printf", cbm_type_named(arena, "java.io.PrintStream")); + REG_METHOD("java.io.PrintStream", "format", cbm_type_named(arena, "java.io.PrintStream")); + REG_METHOD("java.io.PrintStream", "write", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.PrintStream", "flush", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.PrintStream", "close", cbm_type_builtin(arena, "void")); + + REG_METHOD("java.io.PrintWriter", "println", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.PrintWriter", "print", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.PrintWriter", "printf", cbm_type_named(arena, "java.io.PrintWriter")); + REG_METHOD("java.io.PrintWriter", "flush", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.PrintWriter", "close", cbm_type_builtin(arena, "void")); + REG_CTOR("java.io.PrintWriter", "PrintWriter"); + + REG_METHOD("java.io.InputStream", "read", cbm_type_builtin(arena, "int")); + REG_METHOD("java.io.InputStream", "close", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.InputStream", "available", cbm_type_builtin(arena, "int")); + REG_METHOD("java.io.InputStream", "skip", cbm_type_builtin(arena, "long")); + REG_METHOD("java.io.InputStream", "readAllBytes", + cbm_type_slice(arena, cbm_type_builtin(arena, "byte"))); + + REG_METHOD("java.io.OutputStream", "write", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.OutputStream", "flush", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.OutputStream", "close", cbm_type_builtin(arena, "void")); + + REG_METHOD("java.io.Reader", "read", cbm_type_builtin(arena, "int")); + REG_METHOD("java.io.Reader", "close", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.Reader", "ready", cbm_type_builtin(arena, "boolean")); + + REG_METHOD("java.io.Writer", "write", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.Writer", "flush", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.Writer", "close", cbm_type_builtin(arena, "void")); + + REG_METHOD("java.io.BufferedReader", "readLine", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.io.BufferedReader", "lines", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.io.BufferedReader", "close", cbm_type_builtin(arena, "void")); + REG_CTOR("java.io.BufferedReader", "BufferedReader"); + + REG_METHOD("java.io.BufferedWriter", "write", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.BufferedWriter", "newLine", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.BufferedWriter", "flush", cbm_type_builtin(arena, "void")); + REG_METHOD("java.io.BufferedWriter", "close", cbm_type_builtin(arena, "void")); + REG_CTOR("java.io.BufferedWriter", "BufferedWriter"); + + REG_METHOD("java.io.File", "exists", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.io.File", "isFile", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.io.File", "isDirectory", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.io.File", "canRead", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.io.File", "canWrite", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.io.File", "getName", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.io.File", "getPath", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.io.File", "getAbsolutePath", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.io.File", "getCanonicalPath", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.io.File", "getParent", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.io.File", "getParentFile", cbm_type_named(arena, "java.io.File")); + REG_METHOD("java.io.File", "length", cbm_type_builtin(arena, "long")); + REG_METHOD("java.io.File", "lastModified", cbm_type_builtin(arena, "long")); + REG_METHOD("java.io.File", "mkdir", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.io.File", "mkdirs", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.io.File", "delete", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.io.File", "renameTo", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.io.File", "list", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.String"))); + REG_METHOD("java.io.File", "listFiles", + cbm_type_slice(arena, cbm_type_named(arena, "java.io.File"))); + REG_METHOD("java.io.File", "toPath", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.io.File", "toURI", cbm_type_named(arena, "java.net.URI")); + REG_CTOR("java.io.File", "File"); + + /* ── java.nio.file ───────────────────────────────────────── */ + REG_TYPE("java.nio.file.Path", "Path", true, no_parents); + REG_TYPE("java.nio.file.Paths", "Paths", false, parents_object); + REG_TYPE("java.nio.file.Files", "Files", false, parents_object); + + REG_METHOD("java.nio.file.Path", "getFileName", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Path", "getParent", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Path", "getRoot", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Path", "resolve", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Path", "resolveSibling", + cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Path", "relativize", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Path", "normalize", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Path", "toAbsolutePath", + cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Path", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.nio.file.Path", "toFile", cbm_type_named(arena, "java.io.File")); + REG_METHOD("java.nio.file.Path", "of", cbm_type_named(arena, "java.nio.file.Path")); + + REG_METHOD("java.nio.file.Paths", "get", cbm_type_named(arena, "java.nio.file.Path")); + + REG_METHOD("java.nio.file.Files", "exists", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.nio.file.Files", "isDirectory", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.nio.file.Files", "isRegularFile", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.nio.file.Files", "readString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.nio.file.Files", "writeString", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Files", "readAllLines", cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.nio.file.Files", "readAllBytes", + cbm_type_slice(arena, cbm_type_builtin(arena, "byte"))); + REG_METHOD("java.nio.file.Files", "lines", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.nio.file.Files", "list", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.nio.file.Files", "walk", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.nio.file.Files", "createDirectory", + cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Files", "createDirectories", + cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Files", "createFile", + cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Files", "delete", cbm_type_builtin(arena, "void")); + REG_METHOD("java.nio.file.Files", "deleteIfExists", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.nio.file.Files", "copy", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Files", "move", cbm_type_named(arena, "java.nio.file.Path")); + REG_METHOD("java.nio.file.Files", "size", cbm_type_builtin(arena, "long")); + + /* ── java.util.function (the 21 functional interfaces) ──── */ + REG_TYPE("java.util.function.Function", "Function", true, no_parents); + REG_TYPE("java.util.function.BiFunction", "BiFunction", true, no_parents); + REG_TYPE("java.util.function.Predicate", "Predicate", true, no_parents); + REG_TYPE("java.util.function.BiPredicate", "BiPredicate", true, no_parents); + REG_TYPE("java.util.function.Consumer", "Consumer", true, no_parents); + REG_TYPE("java.util.function.BiConsumer", "BiConsumer", true, no_parents); + REG_TYPE("java.util.function.Supplier", "Supplier", true, no_parents); + REG_TYPE("java.util.function.UnaryOperator", "UnaryOperator", true, + ((const char *[]){"java.util.function.Function", NULL})); + REG_TYPE("java.util.function.BinaryOperator", "BinaryOperator", true, + ((const char *[]){"java.util.function.BiFunction", NULL})); + REG_TYPE("java.util.function.IntFunction", "IntFunction", true, no_parents); + REG_TYPE("java.util.function.LongFunction", "LongFunction", true, no_parents); + REG_TYPE("java.util.function.DoubleFunction", "DoubleFunction", true, no_parents); + REG_TYPE("java.util.function.IntPredicate", "IntPredicate", true, no_parents); + REG_TYPE("java.util.function.LongPredicate", "LongPredicate", true, no_parents); + REG_TYPE("java.util.function.DoublePredicate", "DoublePredicate", true, no_parents); + REG_TYPE("java.util.function.IntConsumer", "IntConsumer", true, no_parents); + REG_TYPE("java.util.function.LongConsumer", "LongConsumer", true, no_parents); + REG_TYPE("java.util.function.DoubleConsumer", "DoubleConsumer", true, no_parents); + REG_TYPE("java.util.function.IntSupplier", "IntSupplier", true, no_parents); + REG_TYPE("java.util.function.LongSupplier", "LongSupplier", true, no_parents); + REG_TYPE("java.util.function.DoubleSupplier", "DoubleSupplier", true, no_parents); + REG_TYPE("java.util.function.BooleanSupplier", "BooleanSupplier", true, no_parents); + REG_TYPE("java.util.function.ToIntFunction", "ToIntFunction", true, no_parents); + REG_TYPE("java.util.function.ToLongFunction", "ToLongFunction", true, no_parents); + REG_TYPE("java.util.function.ToDoubleFunction", "ToDoubleFunction", true, no_parents); + + REG_METHOD("java.util.function.Function", "apply", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.function.Function", "compose", + cbm_type_named(arena, "java.util.function.Function")); + REG_METHOD("java.util.function.Function", "andThen", + cbm_type_named(arena, "java.util.function.Function")); + REG_METHOD("java.util.function.Function", "identity", + cbm_type_named(arena, "java.util.function.Function")); + + REG_METHOD("java.util.function.BiFunction", "apply", + cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.function.BiFunction", "andThen", + cbm_type_named(arena, "java.util.function.BiFunction")); + + REG_METHOD("java.util.function.Predicate", "test", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.function.Predicate", "and", + cbm_type_named(arena, "java.util.function.Predicate")); + REG_METHOD("java.util.function.Predicate", "or", + cbm_type_named(arena, "java.util.function.Predicate")); + REG_METHOD("java.util.function.Predicate", "negate", + cbm_type_named(arena, "java.util.function.Predicate")); + REG_METHOD("java.util.function.Predicate", "isEqual", + cbm_type_named(arena, "java.util.function.Predicate")); + + REG_METHOD("java.util.function.Consumer", "accept", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.function.Consumer", "andThen", + cbm_type_named(arena, "java.util.function.Consumer")); + + REG_METHOD("java.util.function.Supplier", "get", cbm_type_named(arena, "java.lang.Object")); + + REG_METHOD("java.util.function.UnaryOperator", "identity", + cbm_type_named(arena, "java.util.function.UnaryOperator")); + REG_METHOD("java.util.function.UnaryOperator", "apply", + cbm_type_named(arena, "java.lang.Object")); + + /* ── java.util.stream ────────────────────────────────────── */ + REG_TYPE("java.util.stream.Stream", "Stream", true, no_parents); + REG_TYPE("java.util.stream.IntStream", "IntStream", true, no_parents); + REG_TYPE("java.util.stream.LongStream", "LongStream", true, no_parents); + REG_TYPE("java.util.stream.DoubleStream", "DoubleStream", true, no_parents); + REG_TYPE("java.util.stream.Collectors", "Collectors", false, parents_object); + REG_TYPE("java.util.stream.Collector", "Collector", true, no_parents); + + REG_METHOD("java.util.stream.Stream", "filter", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "map", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "flatMap", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "mapToInt", + cbm_type_named(arena, "java.util.stream.IntStream")); + REG_METHOD("java.util.stream.Stream", "mapToLong", + cbm_type_named(arena, "java.util.stream.LongStream")); + REG_METHOD("java.util.stream.Stream", "mapToDouble", + cbm_type_named(arena, "java.util.stream.DoubleStream")); + REG_METHOD("java.util.stream.Stream", "sorted", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "distinct", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "limit", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "skip", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "peek", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "forEach", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.stream.Stream", "forEachOrdered", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.stream.Stream", "toArray", + cbm_type_slice(arena, cbm_type_named(arena, "java.lang.Object"))); + REG_METHOD("java.util.stream.Stream", "toList", cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.stream.Stream", "reduce", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.stream.Stream", "collect", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.stream.Stream", "count", cbm_type_builtin(arena, "long")); + REG_METHOD("java.util.stream.Stream", "anyMatch", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.stream.Stream", "allMatch", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.stream.Stream", "noneMatch", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.stream.Stream", "findFirst", + cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.stream.Stream", "findAny", + cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.stream.Stream", "min", cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.stream.Stream", "max", cbm_type_named(arena, "java.util.Optional")); + REG_METHOD("java.util.stream.Stream", "of", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "empty", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "concat", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "iterate", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.Stream", "generate", + cbm_type_named(arena, "java.util.stream.Stream")); + + REG_METHOD("java.util.stream.IntStream", "sum", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.stream.IntStream", "average", + cbm_type_named(arena, "java.util.OptionalDouble")); + REG_METHOD("java.util.stream.IntStream", "max", + cbm_type_named(arena, "java.util.OptionalInt")); + REG_METHOD("java.util.stream.IntStream", "min", + cbm_type_named(arena, "java.util.OptionalInt")); + REG_METHOD("java.util.stream.IntStream", "count", cbm_type_builtin(arena, "long")); + REG_METHOD("java.util.stream.IntStream", "boxed", + cbm_type_named(arena, "java.util.stream.Stream")); + REG_METHOD("java.util.stream.IntStream", "filter", + cbm_type_named(arena, "java.util.stream.IntStream")); + REG_METHOD("java.util.stream.IntStream", "map", + cbm_type_named(arena, "java.util.stream.IntStream")); + REG_METHOD("java.util.stream.IntStream", "range", + cbm_type_named(arena, "java.util.stream.IntStream")); + REG_METHOD("java.util.stream.IntStream", "rangeClosed", + cbm_type_named(arena, "java.util.stream.IntStream")); + REG_METHOD("java.util.stream.IntStream", "of", + cbm_type_named(arena, "java.util.stream.IntStream")); + + REG_METHOD("java.util.stream.Collectors", "toList", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "toSet", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "toMap", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "joining", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "groupingBy", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "partitioningBy", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "counting", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "summingInt", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "averagingDouble", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "mapping", + cbm_type_named(arena, "java.util.stream.Collector")); + REG_METHOD("java.util.stream.Collectors", "reducing", + cbm_type_named(arena, "java.util.stream.Collector")); + + /* ── java.util.concurrent ────────────────────────────────── */ + REG_TYPE("java.util.concurrent.ExecutorService", "ExecutorService", true, no_parents); + REG_TYPE("java.util.concurrent.Executors", "Executors", false, parents_object); + REG_TYPE("java.util.concurrent.Future", "Future", true, no_parents); + REG_TYPE("java.util.concurrent.CompletableFuture", "CompletableFuture", false, + ((const char *[]){"java.util.concurrent.Future", NULL})); + REG_TYPE("java.util.concurrent.ConcurrentHashMap", "ConcurrentHashMap", false, + parents_concurrent_hashmap); + REG_TYPE("java.util.concurrent.ConcurrentMap", "ConcurrentMap", true, parents_map); + REG_TYPE("java.util.concurrent.TimeUnit", "TimeUnit", false, parents_object); + REG_TYPE("java.util.concurrent.atomic.AtomicInteger", "AtomicInteger", false, parents_object); + REG_TYPE("java.util.concurrent.atomic.AtomicLong", "AtomicLong", false, parents_object); + REG_TYPE("java.util.concurrent.atomic.AtomicBoolean", "AtomicBoolean", false, parents_object); + REG_TYPE("java.util.concurrent.atomic.AtomicReference", "AtomicReference", false, + parents_object); + REG_TYPE("java.util.concurrent.locks.Lock", "Lock", true, no_parents); + REG_TYPE("java.util.concurrent.locks.ReentrantLock", "ReentrantLock", false, + ((const char *[]){"java.util.concurrent.locks.Lock", NULL})); + REG_TYPE("java.util.concurrent.locks.ReadWriteLock", "ReadWriteLock", true, no_parents); + + REG_METHOD("java.util.concurrent.ExecutorService", "submit", + cbm_type_named(arena, "java.util.concurrent.Future")); + REG_METHOD("java.util.concurrent.ExecutorService", "execute", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.concurrent.ExecutorService", "shutdown", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.concurrent.ExecutorService", "shutdownNow", + cbm_type_named(arena, "java.util.List")); + REG_METHOD("java.util.concurrent.ExecutorService", "awaitTermination", + cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.concurrent.ExecutorService", "isShutdown", + cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.concurrent.ExecutorService", "isTerminated", + cbm_type_builtin(arena, "boolean")); + + REG_METHOD("java.util.concurrent.Executors", "newFixedThreadPool", + cbm_type_named(arena, "java.util.concurrent.ExecutorService")); + REG_METHOD("java.util.concurrent.Executors", "newSingleThreadExecutor", + cbm_type_named(arena, "java.util.concurrent.ExecutorService")); + REG_METHOD("java.util.concurrent.Executors", "newCachedThreadPool", + cbm_type_named(arena, "java.util.concurrent.ExecutorService")); + REG_METHOD("java.util.concurrent.Executors", "newScheduledThreadPool", + cbm_type_named(arena, "java.util.concurrent.ExecutorService")); + + REG_METHOD("java.util.concurrent.Future", "get", cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.concurrent.Future", "isDone", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.concurrent.Future", "cancel", cbm_type_builtin(arena, "boolean")); + + REG_METHOD("java.util.concurrent.CompletableFuture", "thenApply", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + REG_METHOD("java.util.concurrent.CompletableFuture", "thenAccept", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + REG_METHOD("java.util.concurrent.CompletableFuture", "thenCompose", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + REG_METHOD("java.util.concurrent.CompletableFuture", "thenCombine", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + REG_METHOD("java.util.concurrent.CompletableFuture", "exceptionally", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + REG_METHOD("java.util.concurrent.CompletableFuture", "join", + cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.concurrent.CompletableFuture", "supplyAsync", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + REG_METHOD("java.util.concurrent.CompletableFuture", "runAsync", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + REG_METHOD("java.util.concurrent.CompletableFuture", "completedFuture", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + REG_METHOD("java.util.concurrent.CompletableFuture", "allOf", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + REG_METHOD("java.util.concurrent.CompletableFuture", "anyOf", + cbm_type_named(arena, "java.util.concurrent.CompletableFuture")); + + REG_METHOD("java.util.concurrent.atomic.AtomicInteger", "get", cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.concurrent.atomic.AtomicInteger", "set", cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.concurrent.atomic.AtomicInteger", "incrementAndGet", + cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.concurrent.atomic.AtomicInteger", "decrementAndGet", + cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.concurrent.atomic.AtomicInteger", "getAndIncrement", + cbm_type_builtin(arena, "int")); + REG_METHOD("java.util.concurrent.atomic.AtomicInteger", "compareAndSet", + cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.util.concurrent.atomic.AtomicInteger", "addAndGet", + cbm_type_builtin(arena, "int")); + REG_CTOR("java.util.concurrent.atomic.AtomicInteger", "AtomicInteger"); + + REG_METHOD("java.util.concurrent.atomic.AtomicLong", "get", + cbm_type_builtin(arena, "long")); + REG_METHOD("java.util.concurrent.atomic.AtomicLong", "incrementAndGet", + cbm_type_builtin(arena, "long")); + REG_CTOR("java.util.concurrent.atomic.AtomicLong", "AtomicLong"); + + REG_METHOD("java.util.concurrent.atomic.AtomicReference", "get", + cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.concurrent.atomic.AtomicReference", "set", + cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.concurrent.atomic.AtomicReference", "compareAndSet", + cbm_type_builtin(arena, "boolean")); + REG_CTOR("java.util.concurrent.atomic.AtomicReference", "AtomicReference"); + + REG_METHOD("java.util.concurrent.locks.ReentrantLock", "lock", + cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.concurrent.locks.ReentrantLock", "unlock", + cbm_type_builtin(arena, "void")); + REG_METHOD("java.util.concurrent.locks.ReentrantLock", "tryLock", + cbm_type_builtin(arena, "boolean")); + REG_CTOR("java.util.concurrent.locks.ReentrantLock", "ReentrantLock"); + + REG_METHOD("java.util.concurrent.ConcurrentHashMap", "put", + cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.concurrent.ConcurrentHashMap", "get", + cbm_type_named(arena, "java.lang.Object")); + REG_METHOD("java.util.concurrent.ConcurrentHashMap", "putIfAbsent", + cbm_type_named(arena, "java.lang.Object")); + REG_CTOR("java.util.concurrent.ConcurrentHashMap", "ConcurrentHashMap"); + + /* ── java.time ───────────────────────────────────────────── */ + REG_TYPE("java.time.LocalDate", "LocalDate", false, parents_object); + REG_TYPE("java.time.LocalTime", "LocalTime", false, parents_object); + REG_TYPE("java.time.LocalDateTime", "LocalDateTime", false, parents_object); + REG_TYPE("java.time.ZonedDateTime", "ZonedDateTime", false, parents_object); + REG_TYPE("java.time.OffsetDateTime", "OffsetDateTime", false, parents_object); + REG_TYPE("java.time.Instant", "Instant", false, parents_object); + REG_TYPE("java.time.Duration", "Duration", false, parents_object); + REG_TYPE("java.time.Period", "Period", false, parents_object); + REG_TYPE("java.time.ZoneId", "ZoneId", false, parents_object); + REG_TYPE("java.time.format.DateTimeFormatter", "DateTimeFormatter", false, parents_object); + + REG_METHOD("java.time.LocalDate", "now", cbm_type_named(arena, "java.time.LocalDate")); + REG_METHOD("java.time.LocalDate", "of", cbm_type_named(arena, "java.time.LocalDate")); + REG_METHOD("java.time.LocalDate", "parse", cbm_type_named(arena, "java.time.LocalDate")); + REG_METHOD("java.time.LocalDate", "plusDays", cbm_type_named(arena, "java.time.LocalDate")); + REG_METHOD("java.time.LocalDate", "minusDays", cbm_type_named(arena, "java.time.LocalDate")); + REG_METHOD("java.time.LocalDate", "getYear", cbm_type_builtin(arena, "int")); + REG_METHOD("java.time.LocalDate", "getMonth", cbm_type_named(arena, "java.time.Month")); + REG_METHOD("java.time.LocalDate", "getDayOfMonth", cbm_type_builtin(arena, "int")); + REG_METHOD("java.time.LocalDate", "getDayOfWeek", + cbm_type_named(arena, "java.time.DayOfWeek")); + REG_METHOD("java.time.LocalDate", "isAfter", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.time.LocalDate", "isBefore", cbm_type_builtin(arena, "boolean")); + REG_METHOD("java.time.LocalDate", "format", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.time.LocalDate", "toString", cbm_type_named(arena, "java.lang.String")); + + REG_METHOD("java.time.LocalDateTime", "now", + cbm_type_named(arena, "java.time.LocalDateTime")); + REG_METHOD("java.time.LocalDateTime", "of", + cbm_type_named(arena, "java.time.LocalDateTime")); + REG_METHOD("java.time.LocalDateTime", "parse", + cbm_type_named(arena, "java.time.LocalDateTime")); + REG_METHOD("java.time.LocalDateTime", "plusHours", + cbm_type_named(arena, "java.time.LocalDateTime")); + REG_METHOD("java.time.LocalDateTime", "minusHours", + cbm_type_named(arena, "java.time.LocalDateTime")); + REG_METHOD("java.time.LocalDateTime", "format", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.time.LocalDateTime", "toString", cbm_type_named(arena, "java.lang.String")); + + REG_METHOD("java.time.Instant", "now", cbm_type_named(arena, "java.time.Instant")); + REG_METHOD("java.time.Instant", "ofEpochMilli", cbm_type_named(arena, "java.time.Instant")); + REG_METHOD("java.time.Instant", "ofEpochSecond", cbm_type_named(arena, "java.time.Instant")); + REG_METHOD("java.time.Instant", "toEpochMilli", cbm_type_builtin(arena, "long")); + REG_METHOD("java.time.Instant", "getEpochSecond", cbm_type_builtin(arena, "long")); + REG_METHOD("java.time.Instant", "plus", cbm_type_named(arena, "java.time.Instant")); + REG_METHOD("java.time.Instant", "minus", cbm_type_named(arena, "java.time.Instant")); + + REG_METHOD("java.time.Duration", "ofSeconds", cbm_type_named(arena, "java.time.Duration")); + REG_METHOD("java.time.Duration", "ofMillis", cbm_type_named(arena, "java.time.Duration")); + REG_METHOD("java.time.Duration", "ofMinutes", cbm_type_named(arena, "java.time.Duration")); + REG_METHOD("java.time.Duration", "ofHours", cbm_type_named(arena, "java.time.Duration")); + REG_METHOD("java.time.Duration", "ofDays", cbm_type_named(arena, "java.time.Duration")); + REG_METHOD("java.time.Duration", "between", cbm_type_named(arena, "java.time.Duration")); + REG_METHOD("java.time.Duration", "toMillis", cbm_type_builtin(arena, "long")); + REG_METHOD("java.time.Duration", "toSeconds", cbm_type_builtin(arena, "long")); + REG_METHOD("java.time.Duration", "toMinutes", cbm_type_builtin(arena, "long")); + + REG_METHOD("java.time.ZoneId", "of", cbm_type_named(arena, "java.time.ZoneId")); + REG_METHOD("java.time.ZoneId", "systemDefault", cbm_type_named(arena, "java.time.ZoneId")); + REG_METHOD("java.time.ZoneId", "getId", cbm_type_named(arena, "java.lang.String")); + + REG_METHOD("java.time.format.DateTimeFormatter", "ofPattern", + cbm_type_named(arena, "java.time.format.DateTimeFormatter")); + REG_METHOD("java.time.format.DateTimeFormatter", "format", + cbm_type_named(arena, "java.lang.String")); + + /* ── java.net (minimal) ──────────────────────────────────── */ + REG_TYPE("java.net.URI", "URI", false, parents_object); + REG_TYPE("java.net.URL", "URL", false, parents_object); + REG_METHOD("java.net.URI", "create", cbm_type_named(arena, "java.net.URI")); + REG_METHOD("java.net.URI", "toString", cbm_type_named(arena, "java.lang.String")); + REG_METHOD("java.net.URI", "toURL", cbm_type_named(arena, "java.net.URL")); + REG_METHOD("java.net.URL", "openStream", cbm_type_named(arena, "java.io.InputStream")); + REG_METHOD("java.net.URL", "toString", cbm_type_named(arena, "java.lang.String")); + REG_CTOR("java.net.URL", "URL"); + REG_CTOR("java.net.URI", "URI"); +} diff --git a/internal/cbm/lsp/java_lsp.c b/internal/cbm/lsp/java_lsp.c new file mode 100644 index 000000000..ebe728608 --- /dev/null +++ b/internal/cbm/lsp/java_lsp.c @@ -0,0 +1,3314 @@ +/* + * java_lsp.c — Pure-C Java semantic resolver. + * + * Reverse-engineered from JLS §6 (Names) / §15 (Expressions) and the + * resolution shape used by Eclipse JDT-LS + java-language-server (which both + * lean on javac via com.sun.source). The aim: ≥90% parity with what those + * servers expose for textDocument/definition + textDocument/references at + * call sites — *without* JVM/javac dependencies. + * + * Big picture: + * - Tree-sitter parses Java into an AST. + * - We populate a CBMTypeRegistry from this file's CBMDefinitions plus a + * hand-curated stdlib (java.lang.*, java.util.*, java.io.*, ...). + * - We walk the AST once, building a JLS-style scope chain (fields → + * params → locals → blocks). For every method_invocation, + * object_creation_expression, and field_access we call java_eval_expr_type + * on the receiver and look up the call by name + arity in the registry. + * - Best-overload resolution prefers exact arg count; ties broken by type + * compatibility (cbm_registry_lookup_method_by_types). Ambiguity ⇒ + * "lsp_unresolved" diagnostic so downstream knows to drop the edge. + * - Inheritance + interface chains are walked exactly the way JLS §8.4.8.4 + * specifies: most-specific override first, then walk supers. + * + * Confidence policy (consumed by lsp_resolve.h with CBM_LSP_CONFIDENCE_FLOOR + * = 0.6): + * - 0.95 — direct method call (callee unambiguous, receiver type known) + * - 0.92 — static method via classname / static import + * - 0.90 — inherited method (matched up the super-chain) + * - 0.85 — interface dispatch (sole impl) / generic type substitution + * - 0.80 — receiver type came from a phpdoc-style fallback (not used here) + * - 0.0 — diagnostic (won't be promoted to an edge) + */ + +#include "java_lsp.h" +#include "../helpers.h" + +#include +#include +#include +#include +#include + +#define JAVA_LSP_MAX_EVAL_DEPTH 32 +#define JAVA_LSP_MAX_STMT_DEPTH 256 +#define JAVA_LSP_MAX_INHERIT_HOPS 32 +#define JAVA_LSP_MAX_OVERLOADS 64 +#define JAVA_LSP_BUF 1024 + +/* Forward declarations ─────────────────────────────────────────────── */ + +static void java_resolve_calls_in_node(JavaLSPContext *ctx, TSNode node); +static void java_process_class_decl(JavaLSPContext *ctx, TSNode node); +static void process_method_decl(JavaLSPContext *ctx, TSNode node, const char *class_qn, + const char *super_qn); +static void process_constructor_decl(JavaLSPContext *ctx, TSNode node, const char *class_qn, + const char *super_qn); +static void process_field_decl(JavaLSPContext *ctx, TSNode node); +static void process_local_var_decl(JavaLSPContext *ctx, TSNode node); +static void process_enhanced_for(JavaLSPContext *ctx, TSNode node); +static void process_block(JavaLSPContext *ctx, TSNode node); +static void java_emit_resolved(JavaLSPContext *ctx, const char *callee_qn, const char *strategy, + float confidence); +static void java_emit_unresolved(JavaLSPContext *ctx, const char *expr_text, const char *reason); +static const CBMType *eval_method_invocation(JavaLSPContext *ctx, TSNode node); +static const CBMType *eval_object_creation(JavaLSPContext *ctx, TSNode node); +static const CBMType *eval_field_access(JavaLSPContext *ctx, TSNode node); +static const CBMType *eval_array_access(JavaLSPContext *ctx, TSNode node); +static const CBMType *eval_cast(JavaLSPContext *ctx, TSNode node); +static const CBMType *eval_ternary(JavaLSPContext *ctx, TSNode node); +static const CBMType *eval_binary(JavaLSPContext *ctx, TSNode node); +static const CBMType *eval_unary(JavaLSPContext *ctx, TSNode node); +static const CBMType *eval_lambda(JavaLSPContext *ctx, TSNode node); +static const CBMType *eval_method_reference(JavaLSPContext *ctx, TSNode node); +static const CBMType *resolve_identifier_type(JavaLSPContext *ctx, const char *name); +static const CBMType *resolve_member_type(JavaLSPContext *ctx, const CBMType *recv_type, + const char *member_name); +static void resolve_method_call(JavaLSPContext *ctx, TSNode call); +static void register_local_func_or_type_from_file(JavaLSPContext *ctx, CBMTypeRegistry *reg, + CBMFileResult *result); +static const char *strip_generics(CBMArena *a, const char *type_text); +static const char *unwrap_array_text(CBMArena *a, const char *type_text, int *out_array_dim); +static char *node_text(JavaLSPContext *ctx, TSNode node); +static bool is_node_kind(TSNode node, const char *kind); +static TSNode child_by_kind(TSNode parent, const char *kind); +static const CBMType *box_primitive(CBMArena *a, const char *prim); +static int count_call_args(TSNode call_node); +static const CBMType *propagate_template(CBMArena *a, const char *recv_qn, + const char *method_name, + const CBMType *const *recv_targs, int recv_targ_count, + const CBMType *return_t); +static bool method_implies_lambda_args(const char *recv_qn, const char *method_name, + const CBMType *const *targs, int targ_count, + int *out_arity, const CBMType **out_param0, + const CBMType **out_param1); +static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref, + const CBMRegisteredFunc *outer_resolved, + int arg_index, const CBMType *recv_type); +static bool is_map_like(const char *qn); + +/* ── Built-in primitive table ─────────────────────────────────────── */ + +/* Common Java packages we try as fallback when an unqualified type can't + * be resolved via java.lang / parent_class / module_qn. Order matters — + * more-popular packages first to keep the first-match heuristic accurate. */ +static const char *JAVA_FALLBACK_PACKAGES[] = { + "java.util", + "java.util.function", + "java.util.stream", + "java.util.concurrent", + "java.util.concurrent.atomic", + "java.util.concurrent.locks", + "java.io", + "java.nio.file", + "java.time", + "java.util.regex", + "java.net", + NULL, +}; + +static const char *JAVA_PRIMITIVES[] = { + "boolean", "byte", "char", "short", "int", "long", "float", "double", "void", NULL +}; + +static const char *JAVA_BOXED[] = { + "java.lang.Boolean", "java.lang.Byte", "java.lang.Character", "java.lang.Short", + "java.lang.Integer", "java.lang.Long", "java.lang.Float", "java.lang.Double", + "java.lang.Void", NULL +}; + +static bool is_java_primitive(const char *name) { + if (!name) return false; + for (int i = 0; JAVA_PRIMITIVES[i]; i++) { + if (strcmp(name, JAVA_PRIMITIVES[i]) == 0) return true; + } + return false; +} + +/* Auto-imported java.lang single-type names. The JLS §7.5.5 says all classes + * in java.lang are imported on demand into every compilation unit. */ +static const char *JAVA_LANG_TYPES[] = { + "Object", "String", "StringBuilder", "StringBuffer", "CharSequence", + "Boolean", "Byte", "Character", "Short", "Integer", "Long", "Float", "Double", + "Number", "Math", "System", "Thread", "Runnable", "Iterable", "Comparable", + "Cloneable", "Class", "ClassLoader", "Throwable", "Exception", "Error", + "RuntimeException", "NullPointerException", "IllegalArgumentException", + "IllegalStateException", "IndexOutOfBoundsException", "ArrayIndexOutOfBoundsException", + "ArithmeticException", "ClassCastException", "ClassNotFoundException", + "NumberFormatException", "UnsupportedOperationException", "Enum", "Record", + "AutoCloseable", "Process", "ProcessBuilder", "StackTraceElement", + "Override", "Deprecated", "SuppressWarnings", "FunctionalInterface", + "Void", NULL +}; + +/* ── Helpers ──────────────────────────────────────────────────────── */ + +static char *node_text(JavaLSPContext *ctx, TSNode node) { + return cbm_node_text(ctx->arena, node, ctx->source); +} + +static bool is_node_kind(TSNode node, const char *kind) { + if (ts_node_is_null(node)) return false; + return strcmp(ts_node_type(node), kind) == 0; +} + +static TSNode child_by_kind(TSNode parent, const char *kind) { + if (ts_node_is_null(parent)) return parent; + uint32_t n = ts_node_named_child_count(parent); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(parent, i); + if (strcmp(ts_node_type(c), kind) == 0) return c; + } + TSNode null_node; + memset(&null_node, 0, sizeof(null_node)); + return null_node; +} + +static int count_call_args(TSNode call_node) { + if (ts_node_is_null(call_node)) return 0; + TSNode args = ts_node_child_by_field_name(call_node, "arguments", 9); + if (ts_node_is_null(args)) return 0; + return (int)ts_node_named_child_count(args); +} + +/* Strip generic parameters from a type text (List → List). */ +static const char *strip_generics(CBMArena *a, const char *type_text) { + if (!type_text) return NULL; + const char *lt = strchr(type_text, '<'); + if (!lt) return type_text; + return cbm_arena_strndup(a, type_text, (size_t)(lt - type_text)); +} + +/* Strip array suffix (`int[]` → `int`, `String[][]` → `String`, dim=2). */ +static const char *unwrap_array_text(CBMArena *a, const char *type_text, int *out_dim) { + if (out_dim) *out_dim = 0; + if (!type_text) return NULL; + size_t n = strlen(type_text); + int dim = 0; + while (n >= 2 && type_text[n - 1] == ']' && type_text[n - 2] == '[') { + n -= 2; + dim++; + } + if (out_dim) *out_dim = dim; + if (dim == 0) return type_text; + return cbm_arena_strndup(a, type_text, n); +} + +/* Map primitive name → boxed wrapper QN. */ +static const CBMType *box_primitive(CBMArena *a, const char *prim) { + if (!prim) return cbm_type_unknown(); + static const char *map[][2] = { + {"boolean", "java.lang.Boolean"}, + {"byte", "java.lang.Byte"}, + {"char", "java.lang.Character"}, + {"short", "java.lang.Short"}, + {"int", "java.lang.Integer"}, + {"long", "java.lang.Long"}, + {"float", "java.lang.Float"}, + {"double", "java.lang.Double"}, + {"void", "java.lang.Void"}, + }; + for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { + if (strcmp(prim, map[i][0]) == 0) return cbm_type_named(a, map[i][1]); + } + return cbm_type_unknown(); +} + +/* ── Initialization ───────────────────────────────────────────────── */ + +void java_lsp_init(JavaLSPContext *ctx, CBMArena *arena, const char *source, int source_len, + const CBMTypeRegistry *registry, const char *package_name, + const char *module_qn, CBMResolvedCallArray *out) { + memset(ctx, 0, sizeof(*ctx)); + ctx->arena = arena; + ctx->source = source; + ctx->source_len = source_len; + ctx->registry = registry; + ctx->package_name = package_name ? package_name : ""; + ctx->module_qn = module_qn ? module_qn : ""; + ctx->resolved_calls = out; + ctx->current_scope = cbm_scope_push(arena, NULL); + + const char *dbg = getenv("CBM_LSP_DEBUG"); + ctx->debug = (dbg && dbg[0]); +} + +void java_lsp_add_import(JavaLSPContext *ctx, const char *local_name, const char *target_qn, + int kind) { + if (!ctx || !local_name || !target_qn) return; + if (ctx->import_count >= ctx->import_cap) { + int new_cap = ctx->import_cap == 0 ? 16 : ctx->import_cap * 2; + const char **new_names = (const char **)cbm_arena_alloc(ctx->arena, + (size_t)new_cap * sizeof(*new_names)); + const char **new_qns = (const char **)cbm_arena_alloc(ctx->arena, + (size_t)new_cap * sizeof(*new_qns)); + int *new_kinds = (int *)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*new_kinds)); + if (!new_names || !new_qns || !new_kinds) return; + if (ctx->import_count > 0) { + memcpy(new_names, ctx->import_local_names, + (size_t)ctx->import_count * sizeof(*new_names)); + memcpy(new_qns, ctx->import_target_qns, + (size_t)ctx->import_count * sizeof(*new_qns)); + memcpy(new_kinds, ctx->import_kinds, (size_t)ctx->import_count * sizeof(*new_kinds)); + } + ctx->import_local_names = new_names; + ctx->import_target_qns = new_qns; + ctx->import_kinds = new_kinds; + ctx->import_cap = new_cap; + } + ctx->import_local_names[ctx->import_count] = cbm_arena_strdup(ctx->arena, local_name); + ctx->import_target_qns[ctx->import_count] = cbm_arena_strdup(ctx->arena, target_qn); + ctx->import_kinds[ctx->import_count] = kind; + ctx->import_count++; +} + +static void push_enclosing_class(JavaLSPContext *ctx, const char *class_qn) { + if (ctx->enclosing_class_depth >= ctx->enclosing_class_cap) { + int new_cap = ctx->enclosing_class_cap == 0 ? 8 : ctx->enclosing_class_cap * 2; + const char **arr = (const char **)cbm_arena_alloc(ctx->arena, + (size_t)new_cap * sizeof(*arr)); + if (!arr) return; + if (ctx->enclosing_class_depth > 0) { + memcpy(arr, ctx->enclosing_class_stack, + (size_t)ctx->enclosing_class_depth * sizeof(*arr)); + } + ctx->enclosing_class_stack = arr; + ctx->enclosing_class_cap = new_cap; + } + ctx->enclosing_class_stack[ctx->enclosing_class_depth++] = class_qn; +} + +static void pop_enclosing_class(JavaLSPContext *ctx) { + if (ctx->enclosing_class_depth > 0) ctx->enclosing_class_depth--; +} + +/* ── Type-AST → CBMType ───────────────────────────────────────────── */ + +static const CBMType *parse_type_arguments(JavaLSPContext *ctx, TSNode targs_node); + +const CBMType *java_parse_type_node(JavaLSPContext *ctx, TSNode node) { + if (ts_node_is_null(node)) return cbm_type_unknown(); + const char *kind = ts_node_type(node); + + /* primitives + void */ + if (strcmp(kind, "void_type") == 0) return cbm_type_builtin(ctx->arena, "void"); + if (strcmp(kind, "boolean_type") == 0) return cbm_type_builtin(ctx->arena, "boolean"); + if (strcmp(kind, "integral_type") == 0 || strcmp(kind, "floating_point_type") == 0) { + char *txt = node_text(ctx, node); + return cbm_type_builtin(ctx->arena, txt ? txt : "int"); + } + + /* type_identifier — bare name. Look up via resolver, then fallback. */ + if (strcmp(kind, "type_identifier") == 0) { + char *name = node_text(ctx, node); + if (!name) return cbm_type_unknown(); + const char *qn = java_resolve_type_name(ctx, name); + if (qn) return cbm_type_named(ctx->arena, qn); + /* Could still be a type variable inside a generic method/class. */ + return cbm_type_named(ctx->arena, name); + } + + /* scoped_type_identifier — Outer.Inner or pkg.Type. */ + if (strcmp(kind, "scoped_type_identifier") == 0) { + char *full = node_text(ctx, node); + if (!full) return cbm_type_unknown(); + /* Try treating the whole text as already-qualified. */ + if (cbm_registry_lookup_type(ctx->registry, full)) { + return cbm_type_named(ctx->arena, full); + } + /* Otherwise resolve the head segment and append the rest. */ + char *dot = strchr(full, '.'); + if (dot) { + char *head = cbm_arena_strndup(ctx->arena, full, (size_t)(dot - full)); + const char *head_qn = java_resolve_type_name(ctx, head); + if (head_qn) { + return cbm_type_named(ctx->arena, + cbm_arena_sprintf(ctx->arena, "%s%s", head_qn, dot)); + } + } + return cbm_type_named(ctx->arena, full); + } + + /* generic_type — List, Map, Function etc. + * Capture ALL template args, not just the first — Map.get's return-type + * substitution depends on having both K and V available. */ + if (strcmp(kind, "generic_type") == 0) { + TSNode raw = ts_node_named_child(node, 0); + TSNode targs = ts_node_named_child_count(node) > 1 ? ts_node_named_child(node, 1) + : (TSNode){0}; + const CBMType *base = java_parse_type_node(ctx, raw); + const char *base_qn = NULL; + if (base && base->kind == CBM_TYPE_NAMED) base_qn = base->data.named.qualified_name; + if (!base_qn) return base; + /* Collect every type argument (K, V, R, ...). */ + int arg_count = 0; + const CBMType *arg_buf[16]; + if (!ts_node_is_null(targs) && + strcmp(ts_node_type(targs), "type_arguments") == 0) { + uint32_t tn = ts_node_named_child_count(targs); + for (uint32_t ti = 0; ti < tn && arg_count < 16; ti++) { + arg_buf[arg_count++] = java_parse_type_node(ctx, ts_node_named_child(targs, ti)); + } + } + if (arg_count == 0) { + arg_buf[arg_count++] = cbm_type_unknown(); + } + const CBMType **args = (const CBMType **)cbm_arena_alloc(ctx->arena, + (size_t)(arg_count + 1) + * sizeof(*args)); + for (int i = 0; i < arg_count; i++) args[i] = arg_buf[i]; + args[arg_count] = NULL; + return cbm_type_template(ctx->arena, base_qn, args, arg_count); + } + + /* array_type — T[] (modeled as slice for our purposes). */ + if (strcmp(kind, "array_type") == 0) { + TSNode elem = ts_node_child_by_field_name(node, "element", 7); + if (ts_node_is_null(elem) && ts_node_named_child_count(node) > 0) { + elem = ts_node_named_child(node, 0); + } + return cbm_type_slice(ctx->arena, java_parse_type_node(ctx, elem)); + } + + /* type_parameter → identifier inside; treat as a TYPE_PARAM. */ + if (strcmp(kind, "type_parameter") == 0) { + TSNode name_node = ts_node_named_child(node, 0); + char *name = ts_node_is_null(name_node) ? NULL : node_text(ctx, name_node); + if (name) return cbm_type_type_param(ctx->arena, name); + return cbm_type_unknown(); + } + + /* wildcard `?` and `? extends T` collapse to UNKNOWN/upper bound. */ + if (strcmp(kind, "wildcard") == 0) { + uint32_t n = ts_node_named_child_count(node); + if (n > 0) return java_parse_type_node(ctx, ts_node_named_child(node, n - 1)); + return cbm_type_unknown(); + } + + /* Last resort: emit the raw text as a NAMED type. */ + char *txt = node_text(ctx, node); + if (!txt) return cbm_type_unknown(); + return cbm_type_named(ctx->arena, txt); +} + +static const CBMType *parse_type_arguments(JavaLSPContext *ctx, TSNode targs_node) { + if (ts_node_is_null(targs_node)) return NULL; + if (strcmp(ts_node_type(targs_node), "type_arguments") != 0) return NULL; + if (ts_node_named_child_count(targs_node) == 0) return NULL; + return java_parse_type_node(ctx, ts_node_named_child(targs_node, 0)); +} + +/* ── Type-name resolution (JLS §6.5) ──────────────────────────────── */ + +const char *java_resolve_type_name(JavaLSPContext *ctx, const char *name) { + if (!name || !ctx) return NULL; + + /* 0. Inside a nested class, prefer enclosing-class qualification: + * `Outer.Inner` is reachable as `Inner` from inside `Outer`. */ + for (int i = ctx->enclosing_class_depth - 1; i >= 0; i--) { + const char *outer = ctx->enclosing_class_stack[i]; + if (!outer) continue; + char buf[JAVA_LSP_BUF]; + int n = snprintf(buf, sizeof(buf), "%s.%s", outer, name); + if (n > 0 && (size_t)n < sizeof(buf)) { + if (cbm_registry_lookup_type(ctx->registry, buf)) { + return cbm_arena_strdup(ctx->arena, buf); + } + } + } + + /* 1. Same package — registry types whose QN ends with .name. */ + if (ctx->module_qn && ctx->module_qn[0]) { + char buf[JAVA_LSP_BUF]; + int n = snprintf(buf, sizeof(buf), "%s.%s", ctx->module_qn, name); + if (n > 0 && (size_t)n < sizeof(buf)) { + if (cbm_registry_lookup_type(ctx->registry, buf)) { + return cbm_arena_strdup(ctx->arena, buf); + } + } + } + + /* 2. Single-type imports. */ + for (int i = 0; i < ctx->import_count; i++) { + if (ctx->import_kinds[i] != CBM_JAVA_IMPORT_TYPE) continue; + if (strcmp(ctx->import_local_names[i], name) == 0) { + return ctx->import_target_qns[i]; + } + } + + /* 3. java.lang auto-import. Try registered first, then the conventional QN. */ + for (int i = 0; JAVA_LANG_TYPES[i]; i++) { + if (strcmp(JAVA_LANG_TYPES[i], name) == 0) { + char buf[JAVA_LSP_BUF]; + int n = snprintf(buf, sizeof(buf), "java.lang.%s", name); + if (n > 0 && (size_t)n < sizeof(buf)) { + return cbm_arena_strdup(ctx->arena, buf); + } + } + } + + /* 4. On-demand imports. */ + for (int i = 0; i < ctx->import_count; i++) { + if (ctx->import_kinds[i] != CBM_JAVA_IMPORT_ON_DEMAND) continue; + char buf[JAVA_LSP_BUF]; + int n = snprintf(buf, sizeof(buf), "%s.%s", ctx->import_target_qns[i], name); + if (n > 0 && (size_t)n < sizeof(buf)) { + if (cbm_registry_lookup_type(ctx->registry, buf)) { + return cbm_arena_strdup(ctx->arena, buf); + } + } + } + + /* 5. Java package prefix. */ + if (ctx->package_name && ctx->package_name[0]) { + char buf[JAVA_LSP_BUF]; + int n = snprintf(buf, sizeof(buf), "%s.%s", ctx->package_name, name); + if (n > 0 && (size_t)n < sizeof(buf)) { + if (cbm_registry_lookup_type(ctx->registry, buf)) { + return cbm_arena_strdup(ctx->arena, buf); + } + } + } + + /* Exact already-qualified name. */ + if (cbm_registry_lookup_type(ctx->registry, name)) { + return cbm_arena_strdup(ctx->arena, name); + } + + return NULL; +} + +/* ── Expression-type evaluation ───────────────────────────────────── */ + +const CBMType *java_eval_expr_type(JavaLSPContext *ctx, TSNode node) { + if (ts_node_is_null(node)) return cbm_type_unknown(); + if (ctx->eval_depth >= JAVA_LSP_MAX_EVAL_DEPTH) return cbm_type_unknown(); + ctx->eval_depth++; + const CBMType *result = cbm_type_unknown(); + const char *kind = ts_node_type(node); + + if (strcmp(kind, "parenthesized_expression") == 0) { + if (ts_node_named_child_count(node) > 0) { + result = java_eval_expr_type(ctx, ts_node_named_child(node, 0)); + } + goto out; + } + + /* Literals */ + if (strcmp(kind, "decimal_integer_literal") == 0 || + strcmp(kind, "hex_integer_literal") == 0 || + strcmp(kind, "binary_integer_literal") == 0 || + strcmp(kind, "octal_integer_literal") == 0) { + char *txt = node_text(ctx, node); + if (txt) { + size_t len = strlen(txt); + if (len > 0 && (txt[len - 1] == 'l' || txt[len - 1] == 'L')) { + result = cbm_type_builtin(ctx->arena, "long"); + goto out; + } + } + result = cbm_type_builtin(ctx->arena, "int"); + goto out; + } + if (strcmp(kind, "decimal_floating_point_literal") == 0 || + strcmp(kind, "hex_floating_point_literal") == 0) { + char *txt = node_text(ctx, node); + if (txt) { + size_t len = strlen(txt); + if (len > 0 && (txt[len - 1] == 'f' || txt[len - 1] == 'F')) { + result = cbm_type_builtin(ctx->arena, "float"); + goto out; + } + } + result = cbm_type_builtin(ctx->arena, "double"); + goto out; + } + if (strcmp(kind, "true") == 0 || strcmp(kind, "false") == 0) { + result = cbm_type_builtin(ctx->arena, "boolean"); + goto out; + } + if (strcmp(kind, "character_literal") == 0) { + result = cbm_type_builtin(ctx->arena, "char"); + goto out; + } + if (strcmp(kind, "string_literal") == 0 || strcmp(kind, "string_fragment") == 0) { + result = cbm_type_named(ctx->arena, "java.lang.String"); + goto out; + } + if (strcmp(kind, "null_literal") == 0) { + /* `null` could be assigned to any reference type — leave UNKNOWN + * and let context recover. */ + result = cbm_type_unknown(); + goto out; + } + + /* `this` — current class. */ + if (strcmp(kind, "this") == 0) { + if (ctx->enclosing_class_qn) { + result = cbm_type_named(ctx->arena, ctx->enclosing_class_qn); + } + goto out; + } + + /* `super` — superclass. */ + if (strcmp(kind, "super") == 0) { + if (ctx->enclosing_super_qn) { + result = cbm_type_named(ctx->arena, ctx->enclosing_super_qn); + } else { + result = cbm_type_named(ctx->arena, "java.lang.Object"); + } + goto out; + } + + /* Identifier — local var → field → import → type. */ + if (strcmp(kind, "identifier") == 0) { + char *name = node_text(ctx, node); + if (name) result = resolve_identifier_type(ctx, name); + goto out; + } + + /* Class literal: `Foo.class` is `Class`. */ + if (strcmp(kind, "class_literal") == 0) { + result = cbm_type_named(ctx->arena, "java.lang.Class"); + goto out; + } + + /* Field access. */ + if (strcmp(kind, "field_access") == 0) { + result = eval_field_access(ctx, node); + goto out; + } + + /* Method invocation. */ + if (strcmp(kind, "method_invocation") == 0) { + result = eval_method_invocation(ctx, node); + goto out; + } + + /* Object creation: `new Foo()`. */ + if (strcmp(kind, "object_creation_expression") == 0) { + result = eval_object_creation(ctx, node); + goto out; + } + + /* `new int[10]` style. */ + if (strcmp(kind, "array_creation_expression") == 0) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + const CBMType *elem = java_parse_type_node(ctx, type_node); + result = cbm_type_slice(ctx->arena, elem); + goto out; + } + + /* Cast — `(T)x`. */ + if (strcmp(kind, "cast_expression") == 0) { + result = eval_cast(ctx, node); + goto out; + } + + /* Ternary — narrow to the common type or LHS. */ + if (strcmp(kind, "ternary_expression") == 0) { + result = eval_ternary(ctx, node); + goto out; + } + + /* Binary ops — JLS §15.18. String concat with + emits java.lang.String; + * numeric ops emit the wider numeric type. */ + if (strcmp(kind, "binary_expression") == 0) { + result = eval_binary(ctx, node); + goto out; + } + + /* Unary — most preserve operand type; ! → boolean. */ + if (strcmp(kind, "unary_expression") == 0 || strcmp(kind, "update_expression") == 0) { + result = eval_unary(ctx, node); + goto out; + } + + /* Array access — element type. */ + if (strcmp(kind, "array_access") == 0) { + result = eval_array_access(ctx, node); + goto out; + } + + /* instanceof returns boolean. */ + if (strcmp(kind, "instanceof_expression") == 0) { + result = cbm_type_builtin(ctx->arena, "boolean"); + goto out; + } + + /* Assignment expression — type of RHS. */ + if (strcmp(kind, "assignment_expression") == 0) { + TSNode rhs = ts_node_child_by_field_name(node, "right", 5); + if (!ts_node_is_null(rhs)) result = java_eval_expr_type(ctx, rhs); + goto out; + } + + /* Lambda */ + if (strcmp(kind, "lambda_expression") == 0) { + result = eval_lambda(ctx, node); + goto out; + } + + /* Method reference: ClassName::method or instance::method. */ + if (strcmp(kind, "method_reference") == 0) { + result = eval_method_reference(ctx, node); + goto out; + } + + /* Switch expression — collapse to first arm's type. */ + if (strcmp(kind, "switch_expression") == 0) { + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(node, i); + if (strcmp(ts_node_type(c), "switch_block") == 0) { + uint32_t bn = ts_node_named_child_count(c); + for (uint32_t j = 0; j < bn; j++) { + TSNode arm = ts_node_named_child(c, j); + if (strcmp(ts_node_type(arm), "switch_rule") == 0) { + if (ts_node_named_child_count(arm) > 0) { + TSNode body = ts_node_named_child(arm, ts_node_named_child_count(arm) + - 1); + result = java_eval_expr_type(ctx, body); + goto out; + } + } + } + } + } + goto out; + } + +out: + ctx->eval_depth--; + return result ? result : cbm_type_unknown(); +} + +/* Resolve identifier to a type. + * Order: local scope → fields of enclosing class (walking outer classes) → + * import (single static) → type alias (single class import) → on-demand + * static → fall back to UNKNOWN. */ +static const CBMType *resolve_identifier_type(JavaLSPContext *ctx, const char *name) { + if (!name) return cbm_type_unknown(); + + /* Scope chain. */ + const CBMType *t = cbm_scope_lookup(ctx->current_scope, name); + if (t && !cbm_type_is_unknown(t)) return t; + + /* Enclosing-class fields, walking super chain. */ + if (ctx->enclosing_class_qn) { + const CBMType *ft = java_lookup_field_type(ctx, ctx->enclosing_class_qn, name); + if (ft && !cbm_type_is_unknown(ft)) return ft; + } + /* Outer classes. */ + for (int i = ctx->enclosing_class_depth - 2; i >= 0; i--) { + const char *outer = ctx->enclosing_class_stack[i]; + if (!outer) continue; + const CBMType *ft = java_lookup_field_type(ctx, outer, name); + if (ft && !cbm_type_is_unknown(ft)) return ft; + } + + /* Static imports — could be a static field. */ + for (int i = 0; i < ctx->import_count; i++) { + if (ctx->import_kinds[i] != CBM_JAVA_IMPORT_STATIC) continue; + if (strcmp(ctx->import_local_names[i], name) == 0) { + /* The target QN is e.g. `java.lang.System.out` — we don't have + * a registered type for that; signal via the QN. */ + const char *target = ctx->import_target_qns[i]; + const char *last_dot = strrchr(target, '.'); + if (last_dot) { + char *cls = cbm_arena_strndup(ctx->arena, target, (size_t)(last_dot - target)); + const CBMType *ft = java_lookup_field_type(ctx, cls, name); + if (ft && !cbm_type_is_unknown(ft)) return ft; + } + return cbm_type_unknown(); + } + } + + /* Type name? Treat the identifier as a class reference. */ + const char *type_qn = java_resolve_type_name(ctx, name); + if (type_qn) return cbm_type_named(ctx->arena, type_qn); + + return cbm_type_unknown(); +} + +/* Evaluate `obj.field` — dispatch on receiver type. */ +static const CBMType *eval_field_access(JavaLSPContext *ctx, TSNode node) { + TSNode obj = ts_node_child_by_field_name(node, "object", 6); + TSNode field = ts_node_child_by_field_name(node, "field", 5); + if (ts_node_is_null(field)) return cbm_type_unknown(); + char *fname = node_text(ctx, field); + if (!fname) return cbm_type_unknown(); + + /* Special-case: System.out, System.err — common static fields. */ + if (!ts_node_is_null(obj) && strcmp(ts_node_type(obj), "identifier") == 0) { + char *oname = node_text(ctx, obj); + if (oname && strcmp(oname, "System") == 0) { + if (strcmp(fname, "out") == 0 || strcmp(fname, "err") == 0) { + return cbm_type_named(ctx->arena, "java.io.PrintStream"); + } + if (strcmp(fname, "in") == 0) { + return cbm_type_named(ctx->arena, "java.io.InputStream"); + } + } + } + + /* Special-case: `length` on array types is `int`. */ + const CBMType *recv = ts_node_is_null(obj) ? cbm_type_unknown() + : java_eval_expr_type(ctx, obj); + if (recv && recv->kind == CBM_TYPE_SLICE && strcmp(fname, "length") == 0) { + return cbm_type_builtin(ctx->arena, "int"); + } + + /* For `this.field`, try the scope chain first — process_field_decl + * binds class fields into the enclosing scope, which has accurate type + * info even when the registered class lacks field metadata. */ + if (!ts_node_is_null(obj) && strcmp(ts_node_type(obj), "this") == 0) { + const CBMType *scope_t = cbm_scope_lookup(ctx->current_scope, fname); + if (scope_t && !cbm_type_is_unknown(scope_t)) return scope_t; + } + + const CBMType *res = resolve_member_type(ctx, recv, fname); + if (res && !cbm_type_is_unknown(res)) return res; + + /* Last resort: if receiver is `this` or an unresolved identifier, fall + * back to scope chain by name. */ + if (!ts_node_is_null(obj)) { + const CBMType *scope_t = cbm_scope_lookup(ctx->current_scope, fname); + if (scope_t && !cbm_type_is_unknown(scope_t)) return scope_t; + } + return cbm_type_unknown(); +} + +/* Lookup a field's type on a class, walking the parent chain. */ +const CBMType *java_lookup_field_type(JavaLSPContext *ctx, const char *class_qn, + const char *field_name) { + if (!class_qn || !field_name) return cbm_type_unknown(); + const char *cur = class_qn; + for (int hops = 0; hops < JAVA_LSP_MAX_INHERIT_HOPS && cur; hops++) { + const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, cur); + if (!rt) break; + if (rt->field_names && rt->field_types) { + for (int i = 0; rt->field_names[i]; i++) { + if (strcmp(rt->field_names[i], field_name) == 0) { + return rt->field_types[i]; + } + } + } + if (rt->embedded_types && rt->embedded_types[0]) { + cur = rt->embedded_types[0]; + } else { + cur = NULL; + } + } + return cbm_type_unknown(); +} + +/* Resolve `recv.member` for non-method member access. */ +static const CBMType *resolve_member_type(JavaLSPContext *ctx, const CBMType *recv, + const char *member_name) { + if (!recv || !member_name) return cbm_type_unknown(); + const CBMType *base = recv; + if (base->kind == CBM_TYPE_TEMPLATE) base = NULL; /* fall through to QN */ + const char *recv_qn = NULL; + if (recv->kind == CBM_TYPE_NAMED) recv_qn = recv->data.named.qualified_name; + else if (recv->kind == CBM_TYPE_TEMPLATE) recv_qn = recv->data.template_type.template_name; + if (!recv_qn) return cbm_type_unknown(); + return java_lookup_field_type(ctx, recv_qn, member_name); +} + +/* ── Method lookup ────────────────────────────────────────────────── */ + +const CBMRegisteredFunc *java_lookup_method(JavaLSPContext *ctx, const char *class_qn, + const char *method_name, int arg_count) { + if (!class_qn || !method_name) return NULL; + const char *cur = class_qn; + const CBMRegisteredFunc *fallback = NULL; + for (int hops = 0; hops < JAVA_LSP_MAX_INHERIT_HOPS && cur; hops++) { + /* Try arg-count-aware lookup first. */ + const CBMRegisteredFunc *m = cbm_registry_lookup_method_by_args(ctx->registry, cur, + method_name, arg_count); + if (m) return m; + /* Otherwise capture any name match as fallback. */ + if (!fallback) { + fallback = cbm_registry_lookup_method(ctx->registry, cur, method_name); + } + const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, cur); + if (!rt) break; + if (rt->embedded_types && rt->embedded_types[0]) { + cur = rt->embedded_types[0]; + } else { + cur = NULL; + } + } + return fallback; +} + +/* ── Method-invocation evaluation ─────────────────────────────────── */ + +static const CBMType *return_type_of(const CBMRegisteredFunc *f) { + if (!f || !f->signature) return cbm_type_unknown(); + if (f->signature->kind != CBM_TYPE_FUNC) return cbm_type_unknown(); + const CBMType *const *rets = f->signature->data.func.return_types; + if (!rets || !rets[0]) return cbm_type_unknown(); + return rets[0]; +} + +/* Well-known generic-type substitution table. + * + * For container/wrapper types whose stdlib registration uses + * java.lang.Object as the return type (because the registry doesn't model + * Java type variables natively), this helper rewrites the return to the + * appropriate template argument when the receiver carries one. This is + * exactly the substitution the JLS §15.12 erasure rules perform. + * + * Mapping (one entry per (type, method) pair): + * List/Set/Iterator/Optional/Stream/Iterable/Collection/... .get/.next/... → T0 + * Map/HashMap/TreeMap/LinkedHashMap/ConcurrentHashMap/... .get/.put/... → T1 (V) + * Map.Entry .getValue → T1 + * Map.Entry .getKey → T0 + * Function.apply / BiFunction.apply → R (last arg) + * + * Returns the substituted type, or `fallback` if the receiver isn't a + * recognized parametric container. */ +static bool is_value_typed_container(const char *qn) { + static const char *known[] = { + "java.util.List", "java.util.ArrayList", "java.util.LinkedList", + "java.util.Vector", "java.util.Stack", "java.util.Set", + "java.util.HashSet", "java.util.TreeSet", "java.util.LinkedHashSet", + "java.util.Collection", "java.lang.Iterable", "java.util.Iterator", + "java.util.ListIterator", "java.util.Optional", "java.util.stream.Stream", + "java.util.Queue", "java.util.Deque", "java.util.ArrayDeque", + "java.util.PriorityQueue", NULL, + }; + for (int i = 0; known[i]; i++) { + if (strcmp(known[i], qn) == 0) return true; + } + return false; +} + +static bool is_map_like(const char *qn) { + static const char *known[] = { + "java.util.Map", "java.util.HashMap", + "java.util.TreeMap", "java.util.LinkedHashMap", + "java.util.concurrent.ConcurrentHashMap", + "java.util.concurrent.ConcurrentMap", NULL, + }; + for (int i = 0; known[i]; i++) { + if (strcmp(known[i], qn) == 0) return true; + } + return false; +} + +static const CBMType *substitute_generic_return(JavaLSPContext *ctx, const char *recv_qn, + const char *method_name, + const CBMType *const *arg_arr, + int targ_count, const CBMType *fallback) { + (void)ctx; + if (!recv_qn || !method_name || !arg_arr || targ_count <= 0) return fallback; + if (!fallback) return fallback; + /* Only rewrite Object returns. */ + if (fallback->kind != CBM_TYPE_NAMED) return fallback; + if (strcmp(fallback->data.named.qualified_name, "java.lang.Object") != 0) return fallback; + + if (is_value_typed_container(recv_qn)) { + /* For Iterator.next() and List.get/etc. — the element type is T0. */ + if (strcmp(method_name, "get") == 0 || strcmp(method_name, "set") == 0 || + strcmp(method_name, "remove") == 0 || strcmp(method_name, "next") == 0 || + strcmp(method_name, "peek") == 0 || strcmp(method_name, "poll") == 0 || + strcmp(method_name, "element") == 0 || strcmp(method_name, "first") == 0 || + strcmp(method_name, "last") == 0 || strcmp(method_name, "getFirst") == 0 || + strcmp(method_name, "getLast") == 0 || strcmp(method_name, "removeFirst") == 0 || + strcmp(method_name, "removeLast") == 0 || strcmp(method_name, "orElse") == 0 || + strcmp(method_name, "orElseGet") == 0 || strcmp(method_name, "orElseThrow") == 0 || + strcmp(method_name, "findFirst") == 0 || strcmp(method_name, "findAny") == 0 || + strcmp(method_name, "min") == 0 || strcmp(method_name, "max") == 0 || + strcmp(method_name, "reduce") == 0) { + return arg_arr[0] ? arg_arr[0] : fallback; + } + } + if (is_map_like(recv_qn) && targ_count >= 2) { + if (strcmp(method_name, "get") == 0 || strcmp(method_name, "put") == 0 || + strcmp(method_name, "remove") == 0 || + strcmp(method_name, "getOrDefault") == 0 || + strcmp(method_name, "putIfAbsent") == 0 || + strcmp(method_name, "computeIfAbsent") == 0 || + strcmp(method_name, "compute") == 0 || strcmp(method_name, "merge") == 0) { + return arg_arr[1] ? arg_arr[1] : fallback; + } + } + if (strcmp(recv_qn, "java.util.Map.Entry") == 0 && targ_count >= 2) { + if (strcmp(method_name, "getValue") == 0 || strcmp(method_name, "setValue") == 0) { + return arg_arr[1] ? arg_arr[1] : fallback; + } + if (strcmp(method_name, "getKey") == 0) { + return arg_arr[0] ? arg_arr[0] : fallback; + } + } + /* Function/BiFunction.apply returns R (last template arg). */ + if ((strcmp(recv_qn, "java.util.function.Function") == 0 || + strcmp(recv_qn, "java.util.function.BiFunction") == 0) && + strcmp(method_name, "apply") == 0) { + return arg_arr[targ_count - 1] ? arg_arr[targ_count - 1] : fallback; + } + if (strcmp(recv_qn, "java.util.function.Supplier") == 0 && + strcmp(method_name, "get") == 0) { + return arg_arr[0] ? arg_arr[0] : fallback; + } + return fallback; +} + +static const CBMType *eval_method_invocation(JavaLSPContext *ctx, TSNode node) { + TSNode obj = ts_node_child_by_field_name(node, "object", 6); + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (ts_node_is_null(name_node)) return cbm_type_unknown(); + char *mname = node_text(ctx, name_node); + if (!mname) return cbm_type_unknown(); + int arity = count_call_args(node); + + /* No receiver: `foo()` — method is on enclosing class or static import. */ + if (ts_node_is_null(obj)) { + if (ctx->enclosing_class_qn) { + const CBMRegisteredFunc *f = java_lookup_method(ctx, ctx->enclosing_class_qn, mname, + arity); + if (f) return return_type_of(f); + } + for (int i = 0; i < ctx->import_count; i++) { + if (ctx->import_kinds[i] != CBM_JAVA_IMPORT_STATIC) continue; + if (strcmp(ctx->import_local_names[i], mname) != 0) continue; + const char *target = ctx->import_target_qns[i]; + const char *last_dot = strrchr(target, '.'); + if (!last_dot) continue; + char *cls = cbm_arena_strndup(ctx->arena, target, (size_t)(last_dot - target)); + const CBMRegisteredFunc *f = java_lookup_method(ctx, cls, mname, arity); + if (f) return return_type_of(f); + } + return cbm_type_unknown(); + } + + /* `super.method()` */ + if (strcmp(ts_node_type(obj), "super") == 0) { + const char *super_qn = ctx->enclosing_super_qn ? ctx->enclosing_super_qn + : "java.lang.Object"; + const CBMRegisteredFunc *f = java_lookup_method(ctx, super_qn, mname, arity); + if (f) return return_type_of(f); + return cbm_type_unknown(); + } + + /* Static call: `ClassName.method()` where obj is an identifier matching a + * known type. */ + if (strcmp(ts_node_type(obj), "identifier") == 0) { + char *oname = node_text(ctx, obj); + if (oname) { + const char *cls_qn = java_resolve_type_name(ctx, oname); + if (cls_qn) { + /* Only treat as static if there's no local var of that name. */ + if (cbm_type_is_unknown(cbm_scope_lookup(ctx->current_scope, oname))) { + const CBMRegisteredFunc *f = java_lookup_method(ctx, cls_qn, mname, arity); + if (f) return return_type_of(f); + } + } + } + } + + /* Instance call. */ + const CBMType *recv = java_eval_expr_type(ctx, obj); + const CBMType *base = recv; + if (base && base->kind == CBM_TYPE_TEMPLATE) { + const char *base_qn = base->data.template_type.template_name; + if (base_qn) { + const CBMRegisteredFunc *f = java_lookup_method(ctx, base_qn, mname, arity); + if (f) { + const CBMType *ret = return_type_of(f); + /* Generic substitution + carrier propagation. */ + const CBMType *subst = substitute_generic_return( + ctx, base_qn, mname, + base->data.template_type.template_args, + base->data.template_type.arg_count, ret); + if (!subst) subst = ret; + /* Wrap NAMED carriers (Stream, Iterator, Optional, ...) as + * TEMPLATE so chained methods retain type-arg context. */ + subst = propagate_template(ctx->arena, base_qn, mname, + base->data.template_type.template_args, + base->data.template_type.arg_count, subst); + return subst; + } + } + } + if (base && base->kind == CBM_TYPE_NAMED) { + const CBMRegisteredFunc *f = java_lookup_method(ctx, base->data.named.qualified_name, + mname, arity); + if (f) return return_type_of(f); + } + if (base && base->kind == CBM_TYPE_SLICE) { + /* No methods on arrays beyond `length` (handled in field_access). */ + return cbm_type_unknown(); + } + return cbm_type_unknown(); +} + +/* `new Foo()` */ +static const CBMType *eval_object_creation(JavaLSPContext *ctx, TSNode node) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (ts_node_is_null(type_node)) { + /* Could be qualified expression form. */ + for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) { + TSNode c = ts_node_named_child(node, i); + const char *k = ts_node_type(c); + if (strcmp(k, "type_identifier") == 0 || strcmp(k, "scoped_type_identifier") == 0 || + strcmp(k, "generic_type") == 0) { + type_node = c; + break; + } + } + } + if (ts_node_is_null(type_node)) return cbm_type_unknown(); + return java_parse_type_node(ctx, type_node); +} + +static const CBMType *eval_array_access(JavaLSPContext *ctx, TSNode node) { + TSNode arr = ts_node_child_by_field_name(node, "array", 5); + if (ts_node_is_null(arr)) return cbm_type_unknown(); + const CBMType *t = java_eval_expr_type(ctx, arr); + if (t && t->kind == CBM_TYPE_SLICE) return t->data.slice.elem; + /* List[i] in Java is illegal — but at runtime we sometimes see + * Collection.get-style indexing through the array_access node. Stay + * conservative. */ + return cbm_type_unknown(); +} + +static const CBMType *eval_cast(JavaLSPContext *ctx, TSNode node) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (ts_node_is_null(type_node)) return cbm_type_unknown(); + return java_parse_type_node(ctx, type_node); +} + +static const CBMType *eval_ternary(JavaLSPContext *ctx, TSNode node) { + TSNode then_n = ts_node_child_by_field_name(node, "consequence", 11); + TSNode else_n = ts_node_child_by_field_name(node, "alternative", 11); + const CBMType *t = java_eval_expr_type(ctx, then_n); + if (t && !cbm_type_is_unknown(t)) return t; + return java_eval_expr_type(ctx, else_n); +} + +static bool is_string_concat(JavaLSPContext *ctx, TSNode node) { + /* Either operand of type java.lang.String makes the whole expr a String. */ + TSNode lhs = ts_node_child_by_field_name(node, "left", 4); + TSNode rhs = ts_node_child_by_field_name(node, "right", 5); + const CBMType *l = java_eval_expr_type(ctx, lhs); + const CBMType *r = java_eval_expr_type(ctx, rhs); + if (l && l->kind == CBM_TYPE_NAMED && + strcmp(l->data.named.qualified_name, "java.lang.String") == 0) return true; + if (r && r->kind == CBM_TYPE_NAMED && + strcmp(r->data.named.qualified_name, "java.lang.String") == 0) return true; + return false; +} + +static const CBMType *eval_binary(JavaLSPContext *ctx, TSNode node) { + /* Determine operator. tree-sitter-java exposes it as a child token. */ + char *op = NULL; + uint32_t cn = ts_node_child_count(node); + for (uint32_t i = 0; i < cn; i++) { + TSNode c = ts_node_child(node, i); + if (ts_node_is_named(c)) continue; + op = node_text(ctx, c); + break; + } + /* Fallback when the unnamed-child trick fails (older grammars). */ + if (!op) op = ""; + + if (strcmp(op, "==") == 0 || strcmp(op, "!=") == 0 || strcmp(op, "<") == 0 || + strcmp(op, ">") == 0 || strcmp(op, "<=") == 0 || strcmp(op, ">=") == 0 || + strcmp(op, "&&") == 0 || strcmp(op, "||") == 0) { + return cbm_type_builtin(ctx->arena, "boolean"); + } + if (strcmp(op, "+") == 0 && is_string_concat(ctx, node)) { + return cbm_type_named(ctx->arena, "java.lang.String"); + } + /* Numeric promotion — keep LHS for simplicity. */ + TSNode lhs = ts_node_child_by_field_name(node, "left", 4); + return java_eval_expr_type(ctx, lhs); +} + +static const CBMType *eval_unary(JavaLSPContext *ctx, TSNode node) { + /* `!x` → boolean; `-x` / `~x` / ++ / -- preserve the operand type. */ + char *op = NULL; + uint32_t cn = ts_node_child_count(node); + for (uint32_t i = 0; i < cn; i++) { + TSNode c = ts_node_child(node, i); + if (ts_node_is_named(c)) continue; + op = node_text(ctx, c); + break; + } + if (op && strcmp(op, "!") == 0) return cbm_type_builtin(ctx->arena, "boolean"); + TSNode operand = ts_node_child_by_field_name(node, "operand", 7); + if (ts_node_is_null(operand) && ts_node_named_child_count(node) > 0) { + operand = ts_node_named_child(node, 0); + } + return java_eval_expr_type(ctx, operand); +} + +static const CBMType *eval_lambda(JavaLSPContext *ctx, TSNode node) { + (void)node; + /* Without a SAM target type, the lambda is functional-interface-typed — + * we don't attempt to infer the SAM here. Caller drops the edge. */ + return cbm_type_unknown(); +} + +static const CBMType *eval_method_reference(JavaLSPContext *ctx, TSNode node) { + /* Same caveat as lambdas; method_reference produces a functional + * interface. */ + (void)node; + return cbm_type_unknown(); +} + +/* ── Statement processing — bind into scope ───────────────────────── */ + +void java_process_statement(JavaLSPContext *ctx, TSNode node) { + if (ts_node_is_null(node) || ctx->statement_depth >= JAVA_LSP_MAX_STMT_DEPTH) return; + ctx->statement_depth++; + const char *kind = ts_node_type(node); + + if (strcmp(kind, "local_variable_declaration") == 0) { + process_local_var_decl(ctx, node); + } else if (strcmp(kind, "enhanced_for_statement") == 0) { + process_enhanced_for(ctx, node); + } else if (strcmp(kind, "block") == 0) { + process_block(ctx, node); + } else if (strcmp(kind, "resource") == 0) { + /* try-with-resources: `try (BufferedReader br = new ...())`. + * tree-sitter-java models each resource as a `resource` node with + * type + name + value fields (or an existing-variable reference). */ + TSNode rtype = ts_node_child_by_field_name(node, "type", 4); + TSNode rname = ts_node_child_by_field_name(node, "name", 4); + TSNode rvalue = ts_node_child_by_field_name(node, "value", 5); + char *rn = ts_node_is_null(rname) ? NULL : node_text(ctx, rname); + if (rn) { + const CBMType *rt = cbm_type_unknown(); + bool is_var = false; + if (!ts_node_is_null(rtype) && + strcmp(ts_node_type(rtype), "type_identifier") == 0) { + char *tt = node_text(ctx, rtype); + if (tt && strcmp(tt, "var") == 0) is_var = true; + } + if (!is_var && !ts_node_is_null(rtype)) { + rt = java_parse_type_node(ctx, rtype); + } + if ((is_var || cbm_type_is_unknown(rt)) && !ts_node_is_null(rvalue)) { + rt = java_eval_expr_type(ctx, rvalue); + } + cbm_scope_bind(ctx->current_scope, rn, rt ? rt : cbm_type_unknown()); + } + } else if (strcmp(kind, "catch_clause") == 0) { + /* catch (Type|Type2 var) { body } — bind var into a fresh scope. */ + TSNode formal = ts_node_child_by_field_name(node, "parameter", 9); + if (ts_node_is_null(formal)) { + formal = child_by_kind(node, "catch_formal_parameter"); + } + if (!ts_node_is_null(formal)) { + TSNode pname = ts_node_child_by_field_name(formal, "name", 4); + TSNode ptype = ts_node_child_by_field_name(formal, "type", 4); + if (ts_node_is_null(ptype)) ptype = child_by_kind(formal, "catch_type"); + char *pn = ts_node_is_null(pname) ? NULL : node_text(ctx, pname); + if (pn) { + /* catch_type may be a union_type — pick the first union member. */ + const CBMType *pt = cbm_type_unknown(); + if (!ts_node_is_null(ptype)) { + if (strcmp(ts_node_type(ptype), "catch_type") == 0 && + ts_node_named_child_count(ptype) > 0) { + pt = java_parse_type_node(ctx, ts_node_named_child(ptype, 0)); + } else { + pt = java_parse_type_node(ctx, ptype); + } + } + cbm_scope_bind(ctx->current_scope, pn, pt); + } + } + } + + ctx->statement_depth--; +} + +static void process_local_var_decl(JavaLSPContext *ctx, TSNode node) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + const CBMType *static_type = ts_node_is_null(type_node) ? cbm_type_unknown() + : java_parse_type_node(ctx, type_node); + bool is_var = + !ts_node_is_null(type_node) && strcmp(ts_node_type(type_node), "type_identifier") == 0 && + ({ + char *txt = node_text(ctx, type_node); + txt && strcmp(txt, "var") == 0; + }); + + /* Walk variable_declarator children. */ + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(node, i); + if (strcmp(ts_node_type(c), "variable_declarator") != 0) continue; + TSNode name_node = ts_node_child_by_field_name(c, "name", 4); + TSNode value_node = ts_node_child_by_field_name(c, "value", 5); + if (ts_node_is_null(name_node)) continue; + char *vname = node_text(ctx, name_node); + if (!vname) continue; + const CBMType *bind_type = static_type; + if ((is_var || cbm_type_is_unknown(bind_type)) && !ts_node_is_null(value_node)) { + bind_type = java_eval_expr_type(ctx, value_node); + } + cbm_scope_bind(ctx->current_scope, vname, bind_type ? bind_type : cbm_type_unknown()); + } +} + +static void process_enhanced_for(JavaLSPContext *ctx, TSNode node) { + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + TSNode value_node = ts_node_child_by_field_name(node, "value", 5); + char *vname = ts_node_is_null(name_node) ? NULL : node_text(ctx, name_node); + if (!vname) return; + + /* `var x : xs` — explicit-var inference. tree-sitter exposes "var" as a + * type_identifier whose text is exactly "var". */ + bool is_var = false; + if (!ts_node_is_null(type_node) && + strcmp(ts_node_type(type_node), "type_identifier") == 0) { + char *tt = node_text(ctx, type_node); + if (tt && strcmp(tt, "var") == 0) is_var = true; + } + + const CBMType *t = + (is_var || ts_node_is_null(type_node)) ? cbm_type_unknown() + : java_parse_type_node(ctx, type_node); + + /* When type is `var` or unparseable, infer from the iterable. */ + if (cbm_type_is_unknown(t) && !ts_node_is_null(value_node)) { + const CBMType *iter_t = java_eval_expr_type(ctx, value_node); + if (iter_t && iter_t->kind == CBM_TYPE_SLICE) { + t = iter_t->data.slice.elem; + } else if (iter_t && iter_t->kind == CBM_TYPE_TEMPLATE && + iter_t->data.template_type.arg_count > 0 && + iter_t->data.template_type.template_args[0]) { + t = iter_t->data.template_type.template_args[0]; + } + } + cbm_scope_bind(ctx->current_scope, vname, t ? t : cbm_type_unknown()); +} + +static void process_block(JavaLSPContext *ctx, TSNode node) { + CBMScope *saved = ctx->current_scope; + ctx->current_scope = cbm_scope_push(ctx->arena, saved); + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(node, i); + java_process_statement(ctx, c); + java_resolve_calls_in_node(ctx, c); + } + ctx->current_scope = saved; +} + +/* ── Top-level walk ───────────────────────────────────────────────── */ + +static void process_field_decl(JavaLSPContext *ctx, TSNode node) { + /* Handled at class-registration time for inner-class scope. Still bind + * declared variables with initializers so static-init blocks resolve + * correctly. */ + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + const CBMType *static_type = ts_node_is_null(type_node) ? cbm_type_unknown() + : java_parse_type_node(ctx, type_node); + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(node, i); + if (strcmp(ts_node_type(c), "variable_declarator") != 0) continue; + TSNode name_node = ts_node_child_by_field_name(c, "name", 4); + if (ts_node_is_null(name_node)) continue; + char *fname = node_text(ctx, name_node); + if (!fname) continue; + cbm_scope_bind(ctx->current_scope, fname, static_type); + } +} + +static void process_method_decl(JavaLSPContext *ctx, TSNode node, const char *class_qn, + const char *super_qn) { + /* Compute method QN. */ + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (ts_node_is_null(name_node)) return; + char *mname = node_text(ctx, name_node); + if (!mname) return; + char *method_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, mname); + + /* Save context. */ + const char *saved_method = ctx->enclosing_method_qn; + const char *saved_class = ctx->enclosing_class_qn; + const char *saved_super = ctx->enclosing_super_qn; + CBMScope *saved_scope = ctx->current_scope; + + ctx->enclosing_method_qn = method_qn; + ctx->enclosing_class_qn = class_qn; + ctx->enclosing_super_qn = super_qn; + ctx->current_scope = cbm_scope_push(ctx->arena, saved_scope); + + /* Bind formal parameters into scope. */ + TSNode params = ts_node_child_by_field_name(node, "parameters", 10); + if (!ts_node_is_null(params)) { + uint32_t n = ts_node_named_child_count(params); + for (uint32_t i = 0; i < n; i++) { + TSNode p = ts_node_named_child(params, i); + const char *pk = ts_node_type(p); + if (strcmp(pk, "formal_parameter") != 0 && strcmp(pk, "spread_parameter") != 0 && + strcmp(pk, "receiver_parameter") != 0) { + continue; + } + TSNode pname = ts_node_child_by_field_name(p, "name", 4); + TSNode ptype = ts_node_child_by_field_name(p, "type", 4); + if (ts_node_is_null(pname)) continue; + char *pn = node_text(ctx, pname); + if (!pn) continue; + const CBMType *pt = ts_node_is_null(ptype) ? cbm_type_unknown() + : java_parse_type_node(ctx, ptype); + if (strcmp(pk, "spread_parameter") == 0 && pt) { + pt = cbm_type_slice(ctx->arena, pt); + } + cbm_scope_bind(ctx->current_scope, pn, pt); + } + } + + /* Bind `this` (only matters indirectly — `this` is a syntactic node, not + * an identifier). */ + + /* Walk method body. */ + TSNode body = ts_node_child_by_field_name(node, "body", 4); + if (!ts_node_is_null(body)) { + process_block(ctx, body); + } + + /* Restore. */ + ctx->current_scope = saved_scope; + ctx->enclosing_method_qn = saved_method; + ctx->enclosing_class_qn = saved_class; + ctx->enclosing_super_qn = saved_super; +} + +static void process_constructor_decl(JavaLSPContext *ctx, TSNode node, const char *class_qn, + const char *super_qn) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + char *cname = ts_node_is_null(name_node) ? NULL : node_text(ctx, name_node); + /* Constructor QN convention: Class. or Class.ClassShortName. The + * extractor uses the short class name; mirror that. */ + char *ctor_qn; + if (cname) { + ctor_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, cname); + } else { + ctor_qn = cbm_arena_sprintf(ctx->arena, "%s.", class_qn); + } + + const char *saved_method = ctx->enclosing_method_qn; + const char *saved_class = ctx->enclosing_class_qn; + const char *saved_super = ctx->enclosing_super_qn; + CBMScope *saved_scope = ctx->current_scope; + + ctx->enclosing_method_qn = ctor_qn; + ctx->enclosing_class_qn = class_qn; + ctx->enclosing_super_qn = super_qn; + ctx->current_scope = cbm_scope_push(ctx->arena, saved_scope); + + TSNode params = ts_node_child_by_field_name(node, "parameters", 10); + if (!ts_node_is_null(params)) { + uint32_t n = ts_node_named_child_count(params); + for (uint32_t i = 0; i < n; i++) { + TSNode p = ts_node_named_child(params, i); + if (strcmp(ts_node_type(p), "formal_parameter") != 0) continue; + TSNode pname = ts_node_child_by_field_name(p, "name", 4); + TSNode ptype = ts_node_child_by_field_name(p, "type", 4); + if (ts_node_is_null(pname)) continue; + char *pn = node_text(ctx, pname); + if (!pn) continue; + const CBMType *pt = ts_node_is_null(ptype) ? cbm_type_unknown() + : java_parse_type_node(ctx, ptype); + cbm_scope_bind(ctx->current_scope, pn, pt); + } + } + + TSNode body = ts_node_child_by_field_name(node, "body", 4); + if (!ts_node_is_null(body)) process_block(ctx, body); + + ctx->current_scope = saved_scope; + ctx->enclosing_method_qn = saved_method; + ctx->enclosing_class_qn = saved_class; + ctx->enclosing_super_qn = saved_super; +} + +/* Determine the class's super QN from the AST node. */ +static const char *class_super_qn(JavaLSPContext *ctx, TSNode class_node) { + TSNode super_node = ts_node_child_by_field_name(class_node, "superclass", 10); + if (ts_node_is_null(super_node)) return NULL; + /* superclass node has shape `extends T`; T is the named child. */ + TSNode tnode = (TSNode){0}; + if (ts_node_named_child_count(super_node) > 0) { + tnode = ts_node_named_child(super_node, 0); + } else { + tnode = super_node; + } + if (ts_node_is_null(tnode)) return NULL; + const CBMType *t = java_parse_type_node(ctx, tnode); + if (t && t->kind == CBM_TYPE_NAMED) return t->data.named.qualified_name; + if (t && t->kind == CBM_TYPE_TEMPLATE) return t->data.template_type.template_name; + return NULL; +} + +static void java_process_class_decl(JavaLSPContext *ctx, TSNode node) { + TSNode name_node = ts_node_child_by_field_name(node, "name", 4); + if (ts_node_is_null(name_node)) return; + char *cname = node_text(ctx, name_node); + if (!cname) return; + + /* Class QN. Outer.Inner naming: walk the enclosing-class stack. */ + char *class_qn; + if (ctx->enclosing_class_qn) { + class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->enclosing_class_qn, cname); + } else if (ctx->module_qn && ctx->module_qn[0]) { + class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); + } else { + class_qn = cbm_arena_strdup(ctx->arena, cname); + } + + const char *super_qn = class_super_qn(ctx, node); + if (!super_qn) super_qn = "java.lang.Object"; + + /* Save context. */ + const char *saved_class = ctx->enclosing_class_qn; + const char *saved_super = ctx->enclosing_super_qn; + const char *saved_short = ctx->enclosing_class_short; + + push_enclosing_class(ctx, class_qn); + ctx->enclosing_class_qn = class_qn; + ctx->enclosing_super_qn = super_qn; + ctx->enclosing_class_short = cname; + + TSNode body = ts_node_child_by_field_name(node, "body", 4); + if (!ts_node_is_null(body)) { + CBMScope *saved_scope = ctx->current_scope; + ctx->current_scope = cbm_scope_push(ctx->arena, saved_scope); + + /* First pass: bind fields so methods see them. */ + uint32_t n = ts_node_named_child_count(body); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(body, i); + const char *k = ts_node_type(c); + if (strcmp(k, "field_declaration") == 0) process_field_decl(ctx, c); + } + /* Second pass: methods + constructors + nested types. */ + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(body, i); + const char *k = ts_node_type(c); + if (strcmp(k, "method_declaration") == 0) { + process_method_decl(ctx, c, class_qn, super_qn); + } else if (strcmp(k, "constructor_declaration") == 0) { + process_constructor_decl(ctx, c, class_qn, super_qn); + } else if (strcmp(k, "class_declaration") == 0 || + strcmp(k, "interface_declaration") == 0 || + strcmp(k, "enum_declaration") == 0 || + strcmp(k, "record_declaration") == 0 || + strcmp(k, "annotation_type_declaration") == 0) { + java_process_class_decl(ctx, c); + } else if (strcmp(k, "static_initializer") == 0) { + if (ts_node_named_child_count(c) > 0) { + process_block(ctx, ts_node_named_child(c, 0)); + } + } + } + + ctx->current_scope = saved_scope; + } + + pop_enclosing_class(ctx); + ctx->enclosing_class_qn = saved_class; + ctx->enclosing_super_qn = saved_super; + ctx->enclosing_class_short = saved_short; +} + +void java_lsp_process_file(JavaLSPContext *ctx, TSNode root) { + if (ts_node_is_null(root)) return; + /* First scan: package_declaration + imports (already pushed via init). */ + uint32_t n = ts_node_named_child_count(root); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(root, i); + const char *k = ts_node_type(c); + if (strcmp(k, "package_declaration") == 0) { + uint32_t pn = ts_node_named_child_count(c); + if (pn > 0) { + TSNode p = ts_node_named_child(c, 0); + ctx->package_name = node_text(ctx, p); + } + } else if (strcmp(k, "import_declaration") == 0) { + /* Flag `static` and on-demand. */ + bool is_static = false; + bool is_on_demand = false; + const char *path = NULL; + uint32_t cn = ts_node_child_count(c); + for (uint32_t j = 0; j < cn; j++) { + TSNode cc = ts_node_child(c, j); + if (!ts_node_is_named(cc)) { + char *t = node_text(ctx, cc); + if (t && strcmp(t, "static") == 0) is_static = true; + if (t && strcmp(t, "*") == 0) is_on_demand = true; + if (t && strcmp(t, "asterisk") == 0) is_on_demand = true; + } else { + const char *pk = ts_node_type(cc); + if (strcmp(pk, "asterisk") == 0) is_on_demand = true; + else if (strcmp(pk, "scoped_identifier") == 0 || + strcmp(pk, "identifier") == 0) { + path = node_text(ctx, cc); + } + } + } + if (path) { + if (is_on_demand) { + int kind = is_static ? CBM_JAVA_IMPORT_STATIC_OD : CBM_JAVA_IMPORT_ON_DEMAND; + java_lsp_add_import(ctx, "*", path, kind); + } else if (is_static) { + /* Static import: target is fully-qualified — last segment + * is the imported member. */ + const char *last_dot = strrchr(path, '.'); + const char *local = last_dot ? last_dot + 1 : path; + java_lsp_add_import(ctx, local, path, CBM_JAVA_IMPORT_STATIC); + } else { + const char *last_dot = strrchr(path, '.'); + const char *local = last_dot ? last_dot + 1 : path; + java_lsp_add_import(ctx, local, path, CBM_JAVA_IMPORT_TYPE); + } + } + } + } + + /* Second scan: top-level type declarations. */ + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(root, i); + const char *k = ts_node_type(c); + if (strcmp(k, "class_declaration") == 0 || strcmp(k, "interface_declaration") == 0 || + strcmp(k, "enum_declaration") == 0 || strcmp(k, "record_declaration") == 0 || + strcmp(k, "annotation_type_declaration") == 0) { + java_process_class_decl(ctx, c); + } + } +} + +/* ── Call-edge resolution ─────────────────────────────────────────── */ + +static void java_emit_resolved(JavaLSPContext *ctx, const char *callee_qn, const char *strategy, + float confidence) { + if (!ctx->resolved_calls || !ctx->enclosing_method_qn || !callee_qn) return; + CBMResolvedCall rc; + rc.caller_qn = ctx->enclosing_method_qn; + rc.callee_qn = callee_qn; + rc.strategy = strategy; + rc.confidence = confidence; + rc.reason = NULL; + cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); +} + +static void java_emit_unresolved(JavaLSPContext *ctx, const char *expr_text, const char *reason) { + if (!ctx->resolved_calls || !ctx->enclosing_method_qn) return; + CBMResolvedCall rc; + rc.caller_qn = ctx->enclosing_method_qn; + rc.callee_qn = expr_text ? expr_text : "?"; + rc.strategy = "lsp_unresolved"; + rc.confidence = 0.0f; + rc.reason = reason; + cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); +} + +static void resolve_method_call(JavaLSPContext *ctx, TSNode call) { + TSNode obj = ts_node_child_by_field_name(call, "object", 6); + TSNode name_node = ts_node_child_by_field_name(call, "name", 4); + if (ts_node_is_null(name_node)) return; + char *mname = node_text(ctx, name_node); + if (!mname) return; + int arity = count_call_args(call); + + /* Bare call: `foo()`. */ + if (ts_node_is_null(obj)) { + if (ctx->enclosing_class_qn) { + const CBMRegisteredFunc *f = java_lookup_method(ctx, ctx->enclosing_class_qn, mname, + arity); + if (f) { + const char *strategy = "lsp_type_dispatch"; + if (f->receiver_type && + strcmp(f->receiver_type, ctx->enclosing_class_qn) != 0) { + strategy = "lsp_inherited_dispatch"; + } + java_emit_resolved(ctx, f->qualified_name, strategy, 0.95f); + return; + } + } + /* JLS §15.12.1: a bare call inside an inner class also looks up the + * call against each enclosing-class scope. Walk outer classes. */ + for (int i = ctx->enclosing_class_depth - 2; i >= 0; i--) { + const char *outer = ctx->enclosing_class_stack[i]; + if (!outer) continue; + const CBMRegisteredFunc *f = java_lookup_method(ctx, outer, mname, arity); + if (f) { + java_emit_resolved(ctx, f->qualified_name, "lsp_outer_dispatch", 0.92f); + return; + } + } + /* Static import. */ + for (int i = 0; i < ctx->import_count; i++) { + if (ctx->import_kinds[i] != CBM_JAVA_IMPORT_STATIC) continue; + if (strcmp(ctx->import_local_names[i], mname) != 0) continue; + const char *target = ctx->import_target_qns[i]; + const char *last_dot = strrchr(target, '.'); + if (!last_dot) continue; + char *cls = cbm_arena_strndup(ctx->arena, target, (size_t)(last_dot - target)); + const CBMRegisteredFunc *f = java_lookup_method(ctx, cls, mname, arity); + if (f) { + java_emit_resolved(ctx, f->qualified_name, "lsp_static_import", 0.92f); + return; + } + /* Couldn't find the method — emit the qualified path anyway so + * the registry-name resolver can pick it up. */ + java_emit_resolved(ctx, target, "lsp_static_import_text", 0.80f); + return; + } + /* Fall back to default: emit unresolved. */ + java_emit_unresolved(ctx, mname, "no_enclosing_class"); + return; + } + + /* `super.method()` */ + if (strcmp(ts_node_type(obj), "super") == 0) { + const char *super_qn = ctx->enclosing_super_qn ? ctx->enclosing_super_qn + : "java.lang.Object"; + const CBMRegisteredFunc *f = java_lookup_method(ctx, super_qn, mname, arity); + if (f) { + java_emit_resolved(ctx, f->qualified_name, "lsp_super_dispatch", 0.95f); + return; + } + java_emit_unresolved(ctx, mname, "super_no_match"); + return; + } + + /* `this.method()` */ + if (strcmp(ts_node_type(obj), "this") == 0) { + if (ctx->enclosing_class_qn) { + const CBMRegisteredFunc *f = java_lookup_method(ctx, ctx->enclosing_class_qn, mname, + arity); + if (f) { + java_emit_resolved(ctx, f->qualified_name, "lsp_this_dispatch", 0.95f); + return; + } + } + java_emit_unresolved(ctx, mname, "this_no_match"); + return; + } + + /* Static call via classname. */ + if (strcmp(ts_node_type(obj), "identifier") == 0) { + char *oname = node_text(ctx, obj); + if (oname && cbm_type_is_unknown(cbm_scope_lookup(ctx->current_scope, oname))) { + const char *cls_qn = java_resolve_type_name(ctx, oname); + if (cls_qn) { + const CBMRegisteredFunc *f = java_lookup_method(ctx, cls_qn, mname, arity); + if (f) { + java_emit_resolved(ctx, f->qualified_name, "lsp_static_call", 0.95f); + return; + } + } + } + } + + /* Instance dispatch. */ + const CBMType *recv = java_eval_expr_type(ctx, obj); + const CBMType *base = recv; + const char *recv_qn = NULL; + if (base && base->kind == CBM_TYPE_NAMED) recv_qn = base->data.named.qualified_name; + else if (base && base->kind == CBM_TYPE_TEMPLATE) recv_qn = base->data.template_type.template_name; + + if (recv_qn) { + const CBMRegisteredFunc *f = java_lookup_method(ctx, recv_qn, mname, arity); + if (f) { + const char *strategy = "lsp_type_dispatch"; + if (f->receiver_type && strcmp(f->receiver_type, recv_qn) != 0) { + strategy = "lsp_inherited_dispatch"; + } + java_emit_resolved(ctx, f->qualified_name, strategy, 0.95f); + return; + } + /* Interface dispatch: walk all registered types implementing the + * interface and find a sole concrete impl. */ + const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, recv_qn); + if (rt && rt->is_interface) { + const char *sole_impl = NULL; + int impl_count = 0; + for (int ti = 0; ti < ctx->registry->type_count && impl_count < 2; ti++) { + const CBMRegisteredType *cand = &ctx->registry->types[ti]; + if (cand->is_interface || !cand->qualified_name || cand->alias_of) continue; + bool has = false; + if (cand->method_names) { + for (int mi = 0; cand->method_names[mi]; mi++) { + if (strcmp(cand->method_names[mi], mname) == 0) { + has = true; + break; + } + } + } + if (!has) continue; + /* Walk parent chain to confirm it's actually a subtype of rt. */ + const char *cur = cand->qualified_name; + bool subtype = false; + for (int hops = 0; hops < JAVA_LSP_MAX_INHERIT_HOPS && cur; hops++) { + if (strcmp(cur, recv_qn) == 0) { + subtype = true; + break; + } + const CBMRegisteredType *par = cbm_registry_lookup_type(ctx->registry, cur); + if (!par || !par->embedded_types || !par->embedded_types[0]) break; + /* Walk all parents — pick the first match. */ + bool advanced = false; + for (int pi = 0; par->embedded_types[pi]; pi++) { + if (strcmp(par->embedded_types[pi], recv_qn) == 0) { + subtype = true; + cur = NULL; + break; + } + } + if (subtype) break; + if (!advanced) cur = par->embedded_types[0]; + } + if (subtype) { + sole_impl = cand->qualified_name; + impl_count++; + } + } + if (impl_count == 1 && sole_impl) { + const CBMRegisteredFunc *cf = cbm_registry_lookup_method(ctx->registry, sole_impl, + mname); + if (cf) { + java_emit_resolved(ctx, cf->qualified_name, "lsp_interface_resolve", 0.85f); + return; + } + } + java_emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", recv_qn, mname), + "lsp_interface_dispatch", 0.80f); + return; + } + java_emit_unresolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", recv_qn, mname), + "no_method_match"); + return; + } + + java_emit_unresolved(ctx, mname, "no_receiver_type"); +} + +/* ── Functional-interface SAM table + lambda inference ────────────── + * + * Java functional interfaces define a Single Abstract Method (SAM); when a + * lambda or method reference is passed as that arg, the lambda's parameter + * types come from the SAM's parameter types after generic substitution + * from the receiver context (JLS §15.27.3). Without this, lambda-body type + * resolution degrades to UNKNOWN — the single biggest gap vs JDT-LS. + * + * Strategy: + * - Hand-coded table of common functional interfaces → (sam_name, arity). + * - When walking a method_invocation, check each arg position; if the + * resolved method's param-type at that position is a known FI, bind + * the lambda's formal_parameters to the SAM's parameter types + * (substituted from the receiver's template args), then recursively + * walk the lambda body in that scope. + * + * Coverage: java.util.function.* (21 entries), Runnable, Comparable, + * Comparator, Iterable.iterator (rarely used as a SAM target but cheap + * to include), Callable (java.util.concurrent). Adding more is one-line. */ + +typedef struct { + const char *qn; /* fully-qualified functional interface */ + const char *sam; /* the SAM method's short name */ + int arity; /* number of params on the SAM */ +} JavaSAMSpec; + +static const JavaSAMSpec JAVA_SAM_TABLE[] = { + {"java.util.function.Function", "apply", 1}, + {"java.util.function.BiFunction", "apply", 2}, + {"java.util.function.UnaryOperator", "apply", 1}, + {"java.util.function.BinaryOperator", "apply", 2}, + {"java.util.function.Predicate", "test", 1}, + {"java.util.function.BiPredicate", "test", 2}, + {"java.util.function.Consumer", "accept", 1}, + {"java.util.function.BiConsumer", "accept", 2}, + {"java.util.function.Supplier", "get", 0}, + {"java.util.function.IntFunction", "apply", 1}, + {"java.util.function.LongFunction", "apply", 1}, + {"java.util.function.DoubleFunction", "apply", 1}, + {"java.util.function.IntPredicate", "test", 1}, + {"java.util.function.LongPredicate", "test", 1}, + {"java.util.function.DoublePredicate", "test", 1}, + {"java.util.function.IntConsumer", "accept", 1}, + {"java.util.function.LongConsumer", "accept", 1}, + {"java.util.function.DoubleConsumer", "accept", 1}, + {"java.util.function.IntSupplier", "getAsInt", 0}, + {"java.util.function.LongSupplier", "getAsLong", 0}, + {"java.util.function.DoubleSupplier", "getAsDouble", 0}, + {"java.util.function.BooleanSupplier", "getAsBoolean", 0}, + {"java.util.function.ToIntFunction", "applyAsInt", 1}, + {"java.util.function.ToLongFunction", "applyAsLong", 1}, + {"java.util.function.ToDoubleFunction", "applyAsDouble", 1}, + {"java.lang.Runnable", "run", 0}, + {"java.util.Comparator", "compare", 2}, + {"java.lang.Comparable", "compareTo", 1}, + {"java.util.concurrent.Callable", "call", 0}, + {NULL, NULL, 0}, +}; + +static const JavaSAMSpec *find_sam(const char *qn) { + if (!qn) return NULL; + for (int i = 0; JAVA_SAM_TABLE[i].qn; i++) { + if (strcmp(JAVA_SAM_TABLE[i].qn, qn) == 0) return &JAVA_SAM_TABLE[i]; + } + return NULL; +} + +/* Compute the SAM's `param_idx`-th parameter type for a functional interface + * `fi_qn` parameterized with `targs`. Returns cbm_type_unknown() when the + * parametric mapping isn't modeled (e.g. specialized primitive variants). */ +static const CBMType *sam_param_type(CBMArena *a, const char *fi_qn, int param_idx, + const CBMType *const *targs, int targ_count) { + if (!fi_qn) return cbm_type_unknown(); + + /* Single-template-arg interfaces where SAM uses targ[0]. */ + static const char *single_t[] = { + "java.util.function.Function", + "java.util.function.UnaryOperator", + "java.util.function.Predicate", + "java.util.function.Consumer", + "java.util.function.IntFunction", + "java.util.function.LongFunction", + "java.util.function.DoubleFunction", + "java.util.function.ToIntFunction", + "java.util.function.ToLongFunction", + "java.util.function.ToDoubleFunction", + NULL, + }; + for (int i = 0; single_t[i]; i++) { + if (strcmp(single_t[i], fi_qn) == 0 && param_idx == 0 && targ_count >= 1) { + return targs[0] ? targs[0] : cbm_type_unknown(); + } + } + /* Two-template-arg, two-param interfaces — params map to targs[0], targs[1]. */ + if (strcmp(fi_qn, "java.util.function.BiFunction") == 0 || + strcmp(fi_qn, "java.util.function.BiPredicate") == 0 || + strcmp(fi_qn, "java.util.function.BiConsumer") == 0 || + strcmp(fi_qn, "java.util.function.BinaryOperator") == 0) { + if (param_idx <= 1 && targ_count > param_idx) { + return targs[param_idx] ? targs[param_idx] : cbm_type_unknown(); + } + } + /* Comparator.compare(T, T) — both params share targ[0]. */ + if (strcmp(fi_qn, "java.util.Comparator") == 0) { + if (param_idx <= 1 && targ_count >= 1) { + return targs[0] ? targs[0] : cbm_type_unknown(); + } + } + /* Comparable.compareTo(T) — single param of type targ[0]. */ + if (strcmp(fi_qn, "java.lang.Comparable") == 0) { + if (param_idx == 0 && targ_count >= 1) { + return targs[0] ? targs[0] : cbm_type_unknown(); + } + } + /* Primitive-specialized interfaces — SAM param is a Java primitive. */ + if (strcmp(fi_qn, "java.util.function.IntPredicate") == 0 || + strcmp(fi_qn, "java.util.function.IntConsumer") == 0) { + return cbm_type_builtin(a, "int"); + } + if (strcmp(fi_qn, "java.util.function.LongPredicate") == 0 || + strcmp(fi_qn, "java.util.function.LongConsumer") == 0) { + return cbm_type_builtin(a, "long"); + } + if (strcmp(fi_qn, "java.util.function.DoublePredicate") == 0 || + strcmp(fi_qn, "java.util.function.DoubleConsumer") == 0) { + return cbm_type_builtin(a, "double"); + } + return cbm_type_unknown(); +} + +/* Heuristic table mapping (receiver kind, method name) → lambda arg shape. + * + * Used when the registry's CBMRegisteredFunc lacks param-type metadata + * (most stdlib registrations are return-only) but the receiver's template + * args make the SAM-arg type unambiguous. + * + * Returns true and fills out_arity + out_param[0..1] when a heuristic + * matches. The lambda binder then uses these directly without going + * through the SAM-spec table. */ +static bool method_implies_lambda_args(const char *recv_qn, const char *method_name, + const CBMType *const *targs, int targ_count, + int *out_arity, const CBMType **out_param0, + const CBMType **out_param1) { + *out_arity = 0; + *out_param0 = NULL; + *out_param1 = NULL; + if (!recv_qn || !method_name || !targs || targ_count <= 0) return false; + + /* 1-param lambdas over T0 — Predicate / Consumer / Function shapes. */ + static const char *one_arg_methods[] = { + "forEach", "filter", "map", "flatMap", "peek", + "removeIf", "anyMatch", "allMatch", "noneMatch", "takeWhile", + "dropWhile", "ifPresent", "ifPresentOrElse", "mapToInt", "mapToLong", + "mapToDouble", "filter", NULL, + }; + for (int i = 0; one_arg_methods[i]; i++) { + if (strcmp(method_name, one_arg_methods[i]) == 0) { + *out_arity = 1; + *out_param0 = targs[0]; + return true; + } + } + /* Map.forEach takes BiConsumer (2 params). */ + if (is_map_like(recv_qn) && targ_count >= 2) { + if (strcmp(method_name, "forEach") == 0 || strcmp(method_name, "replaceAll") == 0 || + strcmp(method_name, "compute") == 0 || strcmp(method_name, "computeIfPresent") == 0 || + strcmp(method_name, "merge") == 0) { + *out_arity = 2; + *out_param0 = targs[0]; + *out_param1 = targs[1]; + return true; + } + if (strcmp(method_name, "computeIfAbsent") == 0) { + /* Function — 1-arg of K. */ + *out_arity = 1; + *out_param0 = targs[0]; + return true; + } + } + /* Comparator-typed args — keep simple: Comparator.compare(T0, T0). */ + if (strcmp(method_name, "sort") == 0 && targ_count >= 1) { + *out_arity = 2; + *out_param0 = targs[0]; + *out_param1 = targs[0]; + return true; + } + return false; +} + +/* When a method on a TEMPLATE receiver returns a NAMED type that is itself + * a parametric "carrier" (Stream, Iterator, Optional, …), preserve the + * receiver's element type so chained lambda inference keeps working. + * + * This is the fix for `xs.stream().filter(x -> x.foo())` losing track of + * String inside the filter lambda — without propagation, stream() would + * return bare NAMED(Stream) with no template args, and the SAM binder + * couldn't substitute T0. */ +static const CBMType *propagate_template(CBMArena *a, const char *recv_qn, + const char *method_name, + const CBMType *const *recv_targs, int recv_targ_count, + const CBMType *return_t) { + if (!return_t || return_t->kind != CBM_TYPE_NAMED) return return_t; + if (recv_targ_count <= 0 || !recv_targs) return return_t; + const char *ret_qn = return_t->data.named.qualified_name; + if (!ret_qn) return return_t; + + /* Carriers that preserve T0. */ + static const char *t0_carriers[] = { + "java.util.stream.Stream", + "java.util.Iterator", + "java.util.ListIterator", + "java.util.Spliterator", + "java.util.Optional", + "java.util.List", + "java.util.Set", + "java.util.Collection", + "java.lang.Iterable", + "java.util.Queue", + "java.util.Deque", + NULL, + }; + bool is_t0 = false; + for (int i = 0; t0_carriers[i]; i++) { + if (strcmp(t0_carriers[i], ret_qn) == 0) { + is_t0 = true; + break; + } + } + if (is_t0) { + const CBMType **args = (const CBMType **)cbm_arena_alloc(a, 2 * sizeof(*args)); + if (!args) return return_t; + args[0] = recv_targs[0]; + args[1] = NULL; + return cbm_type_template(a, ret_qn, args, 1); + } + + /* Map.keySet → Set, Map.values → Collection, Map.entrySet → Set>. */ + if (is_map_like(recv_qn) && recv_targ_count >= 2) { + const CBMType **args = (const CBMType **)cbm_arena_alloc(a, 2 * sizeof(*args)); + if (!args) return return_t; + if (strcmp(method_name, "keySet") == 0) { + args[0] = recv_targs[0]; + args[1] = NULL; + return cbm_type_template(a, "java.util.Set", args, 1); + } + if (strcmp(method_name, "values") == 0) { + args[0] = recv_targs[1]; + args[1] = NULL; + return cbm_type_template(a, "java.util.Collection", args, 1); + } + } + return return_t; +} + +/* Given a method-invocation node and its resolved CBMRegisteredFunc, walk + * each lambda argument: bind its formal_parameters to the SAM's parameter + * types (with generic substitution from the receiver's template args), then + * resolve calls inside the lambda body against the freshly-bound scope. + * + * Returns a bitmask of arg indices that were handled here so the generic + * walker can skip them. */ +static uint32_t bind_lambda_args(JavaLSPContext *ctx, TSNode call_node, + const CBMRegisteredFunc *resolved, + const CBMType *recv_type) { + uint32_t handled_mask = 0; + TSNode args_node = ts_node_child_by_field_name(call_node, "arguments", 9); + if (ts_node_is_null(args_node)) return handled_mask; + const CBMType *const *param_types = NULL; + if (resolved && resolved->signature && resolved->signature->kind == CBM_TYPE_FUNC) { + param_types = resolved->signature->data.func.param_types; + } + /* Even without registry param_types, the heuristic (recv_qn + method_name + * → arg shape) often pins the lambda type — that's the path that + * handles `xs.forEach(x -> ...)` and `xs.stream().filter(x -> ...)` + * given that stdlib registrations don't model arg types. */ + + /* Gather receiver template args for substitution. */ + const CBMType *const *recv_targs = NULL; + int recv_targ_count = 0; + const char *recv_qn = NULL; + if (recv_type && recv_type->kind == CBM_TYPE_TEMPLATE) { + recv_targs = recv_type->data.template_type.template_args; + recv_targ_count = recv_type->data.template_type.arg_count; + recv_qn = recv_type->data.template_type.template_name; + } else if (recv_type && recv_type->kind == CBM_TYPE_NAMED) { + recv_qn = recv_type->data.named.qualified_name; + } + + /* Method name (for heuristic). */ + TSNode mname_node = ts_node_child_by_field_name(call_node, "name", 4); + char *mname = ts_node_is_null(mname_node) ? NULL : node_text(ctx, mname_node); + + uint32_t n = ts_node_named_child_count(args_node); + for (uint32_t i = 0; i < n && i < 32; i++) { + TSNode arg = ts_node_named_child(args_node, i); + const char *kind = ts_node_type(arg); + if (strcmp(kind, "lambda_expression") != 0) continue; + + /* First try registry-driven SAM inference. */ + const CBMType *expected = param_types ? param_types[i] : NULL; + if (!expected && recv_qn && mname && recv_targ_count > 0) { + /* Heuristic path: bind lambda directly using receiver template + * args + recognized method name, skipping the SAM table. */ + int h_arity = 0; + const CBMType *h_p0 = NULL; + const CBMType *h_p1 = NULL; + if (method_implies_lambda_args(recv_qn, mname, recv_targs, recv_targ_count, + &h_arity, &h_p0, &h_p1)) { + TSNode params_node = ts_node_child_by_field_name(arg, "parameters", 10); + TSNode body_node = ts_node_child_by_field_name(arg, "body", 4); + if (!ts_node_is_null(body_node)) { + CBMScope *saved = ctx->current_scope; + ctx->current_scope = cbm_scope_push(ctx->arena, saved); + + if (!ts_node_is_null(params_node)) { + const char *pn_kind = ts_node_type(params_node); + if (strcmp(pn_kind, "identifier") == 0) { + char *pname = node_text(ctx, params_node); + if (pname) cbm_scope_bind(ctx->current_scope, pname, h_p0); + } else if (strcmp(pn_kind, "inferred_parameters") == 0 || + strcmp(pn_kind, "formal_parameters") == 0) { + uint32_t pc = ts_node_named_child_count(params_node); + int idx = 0; + for (uint32_t pi = 0; pi < pc; pi++) { + TSNode p = ts_node_named_child(params_node, pi); + const char *pk = ts_node_type(p); + char *pname = NULL; + const CBMType *pt = (idx == 0) ? h_p0 : h_p1; + if (strcmp(pk, "identifier") == 0) { + pname = node_text(ctx, p); + } else if (strcmp(pk, "formal_parameter") == 0) { + TSNode pnname = ts_node_child_by_field_name(p, "name", 4); + TSNode pntype = ts_node_child_by_field_name(p, "type", 4); + if (!ts_node_is_null(pnname)) pname = node_text(ctx, pnname); + if (!ts_node_is_null(pntype)) { + pt = java_parse_type_node(ctx, pntype); + } + } + if (pname) { + cbm_scope_bind(ctx->current_scope, pname, + pt ? pt : cbm_type_unknown()); + idx++; + } + } + } + } + java_resolve_calls_in_node(ctx, body_node); + ctx->current_scope = saved; + handled_mask |= ((uint32_t)1u << i); + } + continue; + } + } + if (!expected) continue; + + /* Strip TEMPLATE wrapper to find the FI QN + per-call template + * args. Note: the FI's own template args may be type variables + * that need substitution from the receiver. */ + const char *fi_qn = NULL; + const CBMType *const *fi_targs = NULL; + int fi_targ_count = 0; + if (expected->kind == CBM_TYPE_NAMED) { + fi_qn = expected->data.named.qualified_name; + } else if (expected->kind == CBM_TYPE_TEMPLATE) { + fi_qn = expected->data.template_type.template_name; + fi_targs = expected->data.template_type.template_args; + fi_targ_count = expected->data.template_type.arg_count; + } + if (!fi_qn) continue; + const JavaSAMSpec *sam = find_sam(fi_qn); + if (!sam) continue; + + /* If the FI's targs are type-variables (TYPE_PARAM kind) referring to + * the receiver's parameters, substitute them. We use the shape: when + * the receiver is e.g. List and the method signature has + * Predicate, the FI's E maps to receiver's E = String. The + * registry's stdlib doesn't carry that link explicitly — so we + * apply a heuristic: if receiver has 1 template arg and the FI has + * a single targ that's a TYPE_PARAM, substitute receiver's targ[0]. + * For Map-like receivers the right substitution depends on + * the SAM context (forEach uses (K,V)) — we handle the few common + * cases below. */ + const CBMType *resolved_fi_targs[8] = {0}; + int resolved_count = fi_targ_count; + for (int j = 0; j < fi_targ_count && j < 8; j++) { + resolved_fi_targs[j] = fi_targs[j]; + } + /* When the FI was passed without explicit targs, fall back to the + * receiver's first template arg as a heuristic single-T substitution. */ + if (fi_targ_count == 0 && recv_targ_count > 0 && + (sam->arity == 1 || strcmp(sam->qn, "java.util.function.BiFunction") == 0 || + strcmp(sam->qn, "java.util.function.BiConsumer") == 0)) { + resolved_fi_targs[0] = recv_targs[0]; + resolved_count = 1; + if (sam->arity == 2 && recv_targ_count >= 2) { + resolved_fi_targs[1] = recv_targs[1]; + resolved_count = 2; + } + } + + /* Find the lambda's parameter list. tree-sitter exposes + * `parameters` field for inferred-formal_parameters or a single + * identifier shorthand `x -> body`. */ + TSNode params_node = ts_node_child_by_field_name(arg, "parameters", 10); + + /* Find the body. */ + TSNode body_node = ts_node_child_by_field_name(arg, "body", 4); + if (ts_node_is_null(body_node)) continue; + + CBMScope *saved = ctx->current_scope; + ctx->current_scope = cbm_scope_push(ctx->arena, saved); + + if (!ts_node_is_null(params_node)) { + const char *pn_kind = ts_node_type(params_node); + if (strcmp(pn_kind, "identifier") == 0) { + /* Shorthand `x -> body` */ + char *pname = node_text(ctx, params_node); + if (pname) { + const CBMType *pt = sam_param_type(ctx->arena, fi_qn, 0, + resolved_fi_targs, resolved_count); + cbm_scope_bind(ctx->current_scope, pname, pt); + } + } else if (strcmp(pn_kind, "inferred_parameters") == 0 || + strcmp(pn_kind, "formal_parameters") == 0) { + uint32_t pc = ts_node_named_child_count(params_node); + int idx = 0; + for (uint32_t pi = 0; pi < pc; pi++) { + TSNode p = ts_node_named_child(params_node, pi); + const char *pk = ts_node_type(p); + char *pname = NULL; + const CBMType *pt = NULL; + if (strcmp(pk, "identifier") == 0) { + pname = node_text(ctx, p); + pt = sam_param_type(ctx->arena, fi_qn, idx, resolved_fi_targs, + resolved_count); + } else if (strcmp(pk, "formal_parameter") == 0) { + TSNode pnname = ts_node_child_by_field_name(p, "name", 4); + TSNode pntype = ts_node_child_by_field_name(p, "type", 4); + if (!ts_node_is_null(pnname)) pname = node_text(ctx, pnname); + if (!ts_node_is_null(pntype)) pt = java_parse_type_node(ctx, pntype); + else pt = sam_param_type(ctx->arena, fi_qn, idx, resolved_fi_targs, + resolved_count); + } + if (pname) { + cbm_scope_bind(ctx->current_scope, pname, pt ? pt : cbm_type_unknown()); + idx++; + } + } + } + } + + /* Walk the body in this enriched scope. */ + java_resolve_calls_in_node(ctx, body_node); + + ctx->current_scope = saved; + handled_mask |= ((uint32_t)1u << i); + } + return handled_mask; +} + +/* Resolve a method reference (Class::method or instance::method) to a + * concrete callee, using the surrounding SAM context for arg-count. + * + * Emits a CBMResolvedCall edge for the referenced method when we can pin + * down the receiver type. Otherwise emits an unresolved diagnostic. */ +static void resolve_method_reference(JavaLSPContext *ctx, TSNode mref, + const CBMRegisteredFunc *outer_resolved, + int arg_index, const CBMType *recv_type) { + if (ts_node_is_null(mref)) return; + /* method_reference shape: lhs `::` name. tree-sitter-java exposes the + * LHS as a named child; the method-name token may be a named identifier + * OR an unnamed `new` keyword (for constructor references like + * `StringBuilder::new`). Handle both. */ + uint32_t nc_named = ts_node_named_child_count(mref); + if (nc_named < 1) return; + TSNode lhs = ts_node_named_child(mref, 0); + + /* Try the last named child first; if it's the same as the LHS (only one + * named child total), we have a constructor ref where `new` is unnamed. */ + char *mname = NULL; + if (nc_named >= 2) { + TSNode name_node = ts_node_named_child(mref, nc_named - 1); + mname = node_text(ctx, name_node); + } + if (!mname || !mname[0]) { + /* Fall back to scanning all (named + unnamed) children for the token + * after `::`. */ + uint32_t total = ts_node_child_count(mref); + for (uint32_t i = 0; i < total; i++) { + TSNode c = ts_node_child(mref, i); + const char *ck = ts_node_type(c); + if (strcmp(ck, "new") == 0) { + mname = "new"; + break; + } + if (strcmp(ck, "identifier") == 0) { + /* Skip if it's the LHS. */ + if (ts_node_eq(c, lhs)) continue; + mname = node_text(ctx, c); + if (mname && mname[0]) break; + } + } + } + if (!mname || !mname[0]) return; + + /* Determine arity: from the SAM of the outer call's expected param. */ + int sam_arity = -1; + if (outer_resolved && outer_resolved->signature && + outer_resolved->signature->kind == CBM_TYPE_FUNC && + outer_resolved->signature->data.func.param_types) { + const CBMType *expected = outer_resolved->signature->data.func.param_types[arg_index]; + if (expected) { + const char *fi_qn = NULL; + if (expected->kind == CBM_TYPE_NAMED) fi_qn = expected->data.named.qualified_name; + else if (expected->kind == CBM_TYPE_TEMPLATE) + fi_qn = expected->data.template_type.template_name; + const JavaSAMSpec *sam = find_sam(fi_qn); + if (sam) sam_arity = sam->arity; + } + } + (void)recv_type; + + /* Try to resolve the LHS as a type or expression. */ + const CBMType *lhs_t = NULL; + const char *lhs_kind = ts_node_type(lhs); + bool lhs_is_type = (strcmp(lhs_kind, "type_identifier") == 0 || + strcmp(lhs_kind, "scoped_type_identifier") == 0 || + strcmp(lhs_kind, "generic_type") == 0); + const char *type_qn = NULL; + if (lhs_is_type) { + char *txt = node_text(ctx, lhs); + if (txt) type_qn = java_resolve_type_name(ctx, strip_generics(ctx->arena, txt)); + if (!type_qn && txt) type_qn = txt; + } else { + lhs_t = java_eval_expr_type(ctx, lhs); + if (lhs_t && lhs_t->kind == CBM_TYPE_NAMED) type_qn = lhs_t->data.named.qualified_name; + else if (lhs_t && lhs_t->kind == CBM_TYPE_TEMPLATE) + type_qn = lhs_t->data.template_type.template_name; + } + if (!type_qn) { + java_emit_unresolved(ctx, mname, "method_reference_unknown_lhs"); + return; + } + + /* Constructor reference: ClassName::new */ + if (strcmp(mname, "new") == 0) { + const char *short_name = strrchr(type_qn, '.'); + short_name = short_name ? short_name + 1 : type_qn; + const CBMRegisteredFunc *cf = cbm_registry_lookup_method(ctx->registry, type_qn, + short_name); + if (cf) { + java_emit_resolved(ctx, cf->qualified_name, "lsp_method_ref_ctor", 0.90f); + } else { + java_emit_resolved(ctx, + cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, short_name), + "lsp_method_ref_ctor_synth", 0.80f); + } + return; + } + + int try_arity = sam_arity >= 0 ? sam_arity : -1; + /* If LHS is a type and the referenced method is non-static (instance + * method ref like String::length), the SAM passes the instance as the + * first arg, so the method-side takes (sam_arity - 1) args. We try both + * possibilities and accept the first match. */ + const CBMRegisteredFunc *m = NULL; + if (try_arity >= 0) { + m = java_lookup_method(ctx, type_qn, mname, try_arity); + if (!m && try_arity > 0 && lhs_is_type) { + m = java_lookup_method(ctx, type_qn, mname, try_arity - 1); + } + } + if (!m) m = java_lookup_method(ctx, type_qn, mname, 0); + if (m) { + java_emit_resolved(ctx, m->qualified_name, "lsp_method_ref", 0.90f); + } else { + java_emit_unresolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, mname), + "method_reference_no_match"); + } +} + +/* Resolve any method-reference args of a method call. */ +static uint32_t bind_method_ref_args(JavaLSPContext *ctx, TSNode call_node, + const CBMRegisteredFunc *resolved, + const CBMType *recv_type) { + uint32_t handled = 0; + if (!resolved) return handled; + TSNode args_node = ts_node_child_by_field_name(call_node, "arguments", 9); + if (ts_node_is_null(args_node)) return handled; + uint32_t n = ts_node_named_child_count(args_node); + for (uint32_t i = 0; i < n && i < 32; i++) { + TSNode arg = ts_node_named_child(args_node, i); + if (strcmp(ts_node_type(arg), "method_reference") != 0) continue; + resolve_method_reference(ctx, arg, resolved, (int)i, recv_type); + handled |= ((uint32_t)1u << i); + } + return handled; +} + +/* Lookup the method that a method_invocation node resolves to. Returns + * NULL if the receiver type is unknown. Used by bind_lambda_args / + * bind_method_ref_args before re-walking arguments — we need the resolved + * method to know the SAM-typed parameter slot. */ +static const CBMRegisteredFunc * +lookup_method_for_call(JavaLSPContext *ctx, TSNode call, const CBMType **out_recv_type) { + if (out_recv_type) *out_recv_type = NULL; + TSNode obj = ts_node_child_by_field_name(call, "object", 6); + TSNode name_node = ts_node_child_by_field_name(call, "name", 4); + if (ts_node_is_null(name_node)) return NULL; + char *mname = node_text(ctx, name_node); + if (!mname) return NULL; + int arity = count_call_args(call); + + if (ts_node_is_null(obj)) { + if (ctx->enclosing_class_qn) { + return java_lookup_method(ctx, ctx->enclosing_class_qn, mname, arity); + } + return NULL; + } + if (strcmp(ts_node_type(obj), "super") == 0) { + const char *sq = ctx->enclosing_super_qn ? ctx->enclosing_super_qn + : "java.lang.Object"; + return java_lookup_method(ctx, sq, mname, arity); + } + if (strcmp(ts_node_type(obj), "this") == 0) { + if (ctx->enclosing_class_qn) { + return java_lookup_method(ctx, ctx->enclosing_class_qn, mname, arity); + } + return NULL; + } + /* Static call via class name. */ + if (strcmp(ts_node_type(obj), "identifier") == 0) { + char *oname = node_text(ctx, obj); + if (oname && cbm_type_is_unknown(cbm_scope_lookup(ctx->current_scope, oname))) { + const char *cls_qn = java_resolve_type_name(ctx, oname); + if (cls_qn) { + const CBMRegisteredFunc *f = java_lookup_method(ctx, cls_qn, mname, arity); + if (f) return f; + } + } + } + /* Instance dispatch. */ + const CBMType *recv = java_eval_expr_type(ctx, obj); + if (out_recv_type) *out_recv_type = recv; + const char *recv_qn = NULL; + if (recv && recv->kind == CBM_TYPE_NAMED) recv_qn = recv->data.named.qualified_name; + else if (recv && recv->kind == CBM_TYPE_TEMPLATE) + recv_qn = recv->data.template_type.template_name; + if (recv_qn) return java_lookup_method(ctx, recv_qn, mname, arity); + return NULL; +} + +/* Walk every node beneath `node`, calling resolve_method_call on each + * method_invocation / object_creation_expression and recursing into block + * children with proper scope handling. */ +static void java_resolve_calls_in_node(JavaLSPContext *ctx, TSNode node) { + if (ts_node_is_null(node)) return; + const char *kind = ts_node_type(node); + + /* Standalone method_reference (e.g. `return StringBuilder::new;` or + * `Function f = X::method;`): without an enclosing method call + * the SAM-context-driven path doesn't fire, so resolve the reference + * directly using its lhs + name. */ + if (strcmp(kind, "method_reference") == 0) { + resolve_method_reference(ctx, node, NULL, 0, NULL); + /* Don't recurse — the LHS is already evaluated by resolve_method_reference. */ + return; + } + + if (strcmp(kind, "method_invocation") == 0) { + resolve_method_call(ctx, node); + + /* Lambda / method-reference args: re-walk with the SAM-bound scope. + * This is the central piece that turns chained streams + forEach + * + map / filter into resolved edges inside the lambda body. */ + const CBMType *recv_type = NULL; + const CBMRegisteredFunc *outer = lookup_method_for_call(ctx, node, &recv_type); + if (outer) { + uint32_t lambda_handled = bind_lambda_args(ctx, node, outer, recv_type); + uint32_t mref_handled = bind_method_ref_args(ctx, node, outer, recv_type); + uint32_t handled = lambda_handled | mref_handled; + if (handled) { + /* Walk only non-handled children of the method_invocation; + * the handled lambda/method-ref args were walked above with + * the SAM-bound scope. */ + TSNode args_node = ts_node_child_by_field_name(node, "arguments", 9); + /* Walk receiver expression. */ + TSNode obj = ts_node_child_by_field_name(node, "object", 6); + if (!ts_node_is_null(obj)) java_resolve_calls_in_node(ctx, obj); + if (!ts_node_is_null(args_node)) { + uint32_t n = ts_node_named_child_count(args_node); + for (uint32_t i = 0; i < n; i++) { + if (handled & ((uint32_t)1u << i)) continue; + TSNode c = ts_node_named_child(args_node, i); + java_resolve_calls_in_node(ctx, c); + } + } + return; + } + } + } else if (strcmp(kind, "object_creation_expression") == 0) { + /* Resolve constructor as a method. */ + TSNode type_node = ts_node_child_by_field_name(node, "type", 4); + if (!ts_node_is_null(type_node)) { + const CBMType *t = java_parse_type_node(ctx, type_node); + const char *qn = NULL; + if (t && t->kind == CBM_TYPE_NAMED) qn = t->data.named.qualified_name; + else if (t && t->kind == CBM_TYPE_TEMPLATE) qn = t->data.template_type.template_name; + if (qn) { + int arity = count_call_args(node); + /* Constructor short name is the class's short name. */ + const char *short_name = strrchr(qn, '.'); + short_name = short_name ? short_name + 1 : qn; + const CBMRegisteredFunc *cf = cbm_registry_lookup_method_by_args(ctx->registry, qn, + short_name, + arity); + if (!cf) cf = cbm_registry_lookup_method(ctx->registry, qn, short_name); + if (cf) { + java_emit_resolved(ctx, cf->qualified_name, "lsp_constructor", 0.95f); + } else { + /* Synth a constructor QN — Class.Class — so downstream + * still gets a resolvable edge. */ + java_emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", qn, short_name), + "lsp_constructor_synth", 0.85f); + } + } + } + } + + /* catch_clause: push a fresh scope so the bound exception variable is + * not visible past the catch body. */ + if (strcmp(kind, "catch_clause") == 0) { + CBMScope *saved = ctx->current_scope; + ctx->current_scope = cbm_scope_push(ctx->arena, saved); + java_process_statement(ctx, node); + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(node, i); + java_resolve_calls_in_node(ctx, c); + } + ctx->current_scope = saved; + return; + } + + /* Mutate scope on local var decl / enhanced for / etc. */ + java_process_statement(ctx, node); + + /* Don't double-walk blocks (process_block handles its own scope). */ + if (strcmp(kind, "block") == 0) return; + + uint32_t n = ts_node_named_child_count(node); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(node, i); + java_resolve_calls_in_node(ctx, c); + } +} + +/* ── File-defs registration ───────────────────────────────────────── */ + +/* Parse a comma-separated list at the outermost generic level of `text`. + * `text` is the substring INSIDE the angle brackets, e.g. for + * "Map>" the caller would pass "String, List". + * + * Returns a NULL-terminated array of arena-allocated substrings; sets + * *out_count to the number of args. */ +static const char **split_generic_args(CBMArena *a, const char *inside, int *out_count) { + *out_count = 0; + if (!inside || !inside[0]) return NULL; + const char *args[16]; + int count = 0; + int depth = 0; + const char *seg_start = inside; + for (const char *p = inside; *p; p++) { + if (*p == '<') depth++; + else if (*p == '>') depth--; + else if (*p == ',' && depth == 0) { + /* trim leading whitespace from seg_start */ + while (seg_start < p && (*seg_start == ' ' || *seg_start == '\t')) seg_start++; + const char *seg_end = p; + while (seg_end > seg_start && (seg_end[-1] == ' ' || seg_end[-1] == '\t')) seg_end--; + if (count < 16 && seg_end > seg_start) { + args[count++] = cbm_arena_strndup(a, seg_start, + (size_t)(seg_end - seg_start)); + } + seg_start = p + 1; + } + } + /* last segment */ + while (*seg_start == ' ' || *seg_start == '\t') seg_start++; + const char *seg_end = inside + strlen(inside); + while (seg_end > seg_start && (seg_end[-1] == ' ' || seg_end[-1] == '\t')) seg_end--; + if (count < 16 && seg_end > seg_start) { + args[count++] = cbm_arena_strndup(a, seg_start, (size_t)(seg_end - seg_start)); + } + if (count == 0) return NULL; + const char **result = (const char **)cbm_arena_alloc(a, (size_t)(count + 1) * sizeof(*result)); + for (int i = 0; i < count; i++) result[i] = args[i]; + result[count] = NULL; + *out_count = count; + return result; +} + +/* Parse a type-text into a CBMType, with full inner-class qualification. + * `parent_class` is the QN of the enclosing class (NULL at file scope), used + * to qualify unqualified inner-class references. `reg` is consulted to + * confirm matches before committing to a candidate QN. + * + * Preserves generic template arguments: "Consumer" parses to a + * TEMPLATE("java.util.function.Consumer", [String]) so the SAM-binder + * downstream can substitute the lambda's parameter type correctly. */ +static const CBMType *parse_param_text_full(CBMArena *a, const char *text, + const char *parent_class, const char *module_qn, + const CBMTypeRegistry *reg) { + if (!text) return cbm_type_unknown(); + int dim = 0; + const char *no_arr = unwrap_array_text(a, text, &dim); + const char *no_gen = strip_generics(a, no_arr); + + /* Extract the inside-of-angles for generic-arg recursion. */ + const char *gen_args_inner = NULL; + if (no_arr) { + const char *lt = strchr(no_arr, '<'); + if (lt) { + /* find matching '>' at depth 0 */ + int depth = 0; + const char *gt = NULL; + for (const char *p = lt; *p; p++) { + if (*p == '<') depth++; + else if (*p == '>') { + depth--; + if (depth == 0) { gt = p; break; } + } + } + if (gt && gt > lt + 1) { + gen_args_inner = cbm_arena_strndup(a, lt + 1, (size_t)(gt - lt - 1)); + } + } + } + + const CBMType *base = NULL; + if (no_gen && is_java_primitive(no_gen)) { + base = cbm_type_builtin(a, no_gen); + } else if (no_gen) { + if (strchr(no_gen, '.')) { + base = cbm_type_named(a, no_gen); + } else { + /* Try java.lang well-knowns. */ + for (int i = 0; JAVA_LANG_TYPES[i]; i++) { + if (strcmp(JAVA_LANG_TYPES[i], no_gen) == 0) { + base = cbm_type_named(a, cbm_arena_sprintf(a, "java.lang.%s", no_gen)); + break; + } + } + /* Walk parent_class chain trying parent.X, parent.parent.X, ... */ + if (!base && parent_class && reg) { + const char *cur = parent_class; + while (cur) { + const char *cand = cbm_arena_sprintf(a, "%s.%s", cur, no_gen); + if (cbm_registry_lookup_type(reg, cand)) { + base = cbm_type_named(a, cand); + break; + } + const char *last_dot = strrchr(cur, '.'); + if (!last_dot) break; + cur = cbm_arena_strndup(a, cur, (size_t)(last_dot - cur)); + } + } + /* Common Java-stdlib package fallback — tries java.util, + * java.util.function, etc. so user methods declaring + * `Consumer` resolve to java.util.function.Consumer + * even if the import wasn't threaded into this codepath. */ + if (!base && reg) { + for (int i = 0; JAVA_FALLBACK_PACKAGES[i]; i++) { + const char *cand = cbm_arena_sprintf(a, "%s.%s", JAVA_FALLBACK_PACKAGES[i], + no_gen); + if (cbm_registry_lookup_type(reg, cand)) { + base = cbm_type_named(a, cand); + break; + } + } + } + /* Fallback: module_qn.X (same package, top-level class). */ + if (!base) { + if (module_qn && module_qn[0]) { + base = cbm_type_named(a, cbm_arena_sprintf(a, "%s.%s", module_qn, no_gen)); + } else { + base = cbm_type_named(a, no_gen); + } + } + } + } + if (!base) base = cbm_type_unknown(); + /* If we extracted generic args and the base is NAMED, wrap as TEMPLATE + * with each arg recursively parsed. */ + if (gen_args_inner && base && base->kind == CBM_TYPE_NAMED) { + int gc = 0; + const char **arg_strs = split_generic_args(a, gen_args_inner, &gc); + if (arg_strs && gc > 0) { + const CBMType **gargs = (const CBMType **)cbm_arena_alloc(a, + (size_t)(gc + 1) + * sizeof(*gargs)); + for (int i = 0; i < gc; i++) { + gargs[i] = parse_param_text_full(a, arg_strs[i], parent_class, module_qn, reg); + } + gargs[gc] = NULL; + base = cbm_type_template(a, base->data.named.qualified_name, gargs, gc); + } + } + while (dim-- > 0) base = cbm_type_slice(a, base); + return base; +} + +/* Backwards-compatible wrapper for callers that don't know the parent_class + * context. Used by cbm_run_java_lsp_cross. */ +static const CBMType *parse_param_text(CBMArena *a, const char *text, const char *module_qn) { + return parse_param_text_full(a, text, NULL, module_qn, NULL); +} + +static void register_local_func_or_type_from_file(JavaLSPContext *ctx, CBMTypeRegistry *reg, + CBMFileResult *result) { + CBMArena *a = ctx->arena; + const char *module_qn = ctx->module_qn; + + /* Pass 1a: register all class/interface types WITHOUT embedded_types so + * that subsequent base-class qualification (Pass 1b) can look them up + * regardless of source order. */ + for (int i = 0; i < result->defs.count; i++) { + CBMDefinition *d = &result->defs.items[i]; + if (!d->qualified_name || !d->name || !d->label) continue; + if (strcmp(d->label, "Class") != 0 && strcmp(d->label, "Interface") != 0 && + strcmp(d->label, "Enum") != 0 && strcmp(d->label, "Type") != 0) { + continue; + } + if (cbm_registry_lookup_type(reg, d->qualified_name)) continue; + CBMRegisteredType stub; + memset(&stub, 0, sizeof(stub)); + stub.qualified_name = d->qualified_name; + stub.short_name = d->name; + stub.is_interface = (strcmp(d->label, "Interface") == 0); + cbm_registry_add_type(reg, stub); + } + + /* Pass 1b: now that every local type is in the registry, walk again and + * fill in embedded_types using the parent_class chain. We mutate the + * registry entry in place via the type lookup (CBMRegisteredType *). */ + for (int i = 0; i < result->defs.count; i++) { + CBMDefinition *d = &result->defs.items[i]; + if (!d->qualified_name || !d->name || !d->label) continue; + if (strcmp(d->label, "Class") != 0 && strcmp(d->label, "Interface") != 0 && + strcmp(d->label, "Enum") != 0 && strcmp(d->label, "Type") != 0) { + continue; + } + const CBMRegisteredType *existing = cbm_registry_lookup_type(reg, d->qualified_name); + if (!existing) continue; + CBMRegisteredType rt = *existing; /* Will be re-added; lookup_type returns the + registry's stored copy. We instead build a + local copy and let cbm_registry_add_type's + de-dup not apply — but since we already + added the stub in Pass 1a, we must mutate + in-place. The registry exposes its arrays; + do the in-place update directly. */ + (void)rt; + /* Locate the actual entry in reg->types[] and write into it. */ + CBMRegisteredType *slot = NULL; + for (int ti = 0; ti < reg->type_count; ti++) { + if (reg->types[ti].qualified_name == d->qualified_name || + (reg->types[ti].qualified_name && + strcmp(reg->types[ti].qualified_name, d->qualified_name) == 0)) { + slot = ®->types[ti]; + break; + } + } + if (!slot) continue; + if (d->base_classes) { + int bc_count = 0; + while (d->base_classes[bc_count]) bc_count++; + if (bc_count > 0) { + const char **emb = (const char **)cbm_arena_alloc(a, + (size_t)(bc_count + 1) + * sizeof(*emb)); + for (int j = 0; j < bc_count; j++) { + const char *bc = d->base_classes[j]; + if (!bc || !bc[0]) { + emb[j] = "java.lang.Object"; + continue; + } + const char *no_gen = strip_generics(a, bc); + /* If the base name is unqualified, try in order: + * 1. java.lang well-knowns + * 2. parent_class.bc (and walking up its dotted parents) + * 3. module_qn.bc (same package) + * We pick the first match that exists in the registry. */ + if (!strchr(no_gen, '.')) { + const char *resolved = NULL; + for (int k = 0; JAVA_LANG_TYPES[k]; k++) { + if (strcmp(JAVA_LANG_TYPES[k], no_gen) == 0) { + resolved = cbm_arena_sprintf(a, "java.lang.%s", no_gen); + break; + } + } + /* extract_defs only sets parent_class on Method defs, + * not on Class defs. Derive a parent QN from the + * class's own qualified_name by peeling the last + * segment ("test.Main.Main.Dog" → "test.Main.Main"). */ + const char *parent_qn = d->parent_class; + if (!parent_qn && d->qualified_name) { + const char *last_dot_qn = strrchr(d->qualified_name, '.'); + if (last_dot_qn) { + parent_qn = cbm_arena_strndup(a, d->qualified_name, + (size_t)(last_dot_qn - + d->qualified_name)); + } + } + if (!resolved && parent_qn) { + const char *cur = parent_qn; + while (cur && !resolved) { + const char *cand = cbm_arena_sprintf(a, "%s.%s", cur, no_gen); + if (cbm_registry_lookup_type(reg, cand)) { + resolved = cand; + break; + } + const char *last_dot = strrchr(cur, '.'); + if (!last_dot) break; + cur = cbm_arena_strndup(a, cur, (size_t)(last_dot - cur)); + } + } + if (!resolved && module_qn && module_qn[0]) { + resolved = cbm_arena_sprintf(a, "%s.%s", module_qn, no_gen); + } + emb[j] = resolved ? resolved : cbm_arena_strdup(a, no_gen); + } else { + emb[j] = cbm_arena_strdup(a, no_gen); + } + } + emb[bc_count] = NULL; + slot->embedded_types = emb; + } + } + /* JLS §8.1.4: every class without an explicit `extends` implicitly + * extends java.lang.Object. Without this, walks like + * `obj.toString()` on a user class fail because the inheritance + * walk has no Object link. */ + if (!slot->embedded_types && !slot->is_interface) { + const char **emb = (const char **)cbm_arena_alloc(a, 2 * sizeof(*emb)); + if (emb) { + emb[0] = "java.lang.Object"; + emb[1] = NULL; + slot->embedded_types = emb; + } + } + } + + /* Field metadata is populated by the AST walk inside process_class_decl, + * since CBMDefinition doesn't carry a per-field type for Java. See + * register_class_field_in_registry below. */ + + /* Register methods. */ + for (int i = 0; i < result->defs.count; i++) { + CBMDefinition *d = &result->defs.items[i]; + if (!d->qualified_name || !d->name || !d->label) continue; + if (strcmp(d->label, "Method") != 0 && strcmp(d->label, "Function") != 0 && + strcmp(d->label, "Constructor") != 0) { + continue; + } + CBMRegisteredFunc rf; + memset(&rf, 0, sizeof(rf)); + rf.qualified_name = d->qualified_name; + rf.short_name = d->name; + rf.min_params = -1; + + /* Build param types — qualify with parent_class chain so inner-class + * references like `Box` inside `Main` resolve to "test.Main.Main.Box" + * not "test.Main.Box". */ + const char *parent = d->parent_class; + const CBMType **ptypes = NULL; + if (d->param_types) { + int pc = 0; + while (d->param_types[pc]) pc++; + if (pc > 0) { + ptypes = (const CBMType **)cbm_arena_alloc(a, (size_t)(pc + 1) * sizeof(*ptypes)); + for (int j = 0; j < pc; j++) { + ptypes[j] = parse_param_text_full(a, d->param_types[j], parent, module_qn, + reg); + } + ptypes[pc] = NULL; + } + } + /* Build return types — single return only in Java. */ + const CBMType **rtypes = NULL; + if (d->return_type && d->return_type[0]) { + rtypes = (const CBMType **)cbm_arena_alloc(a, 2 * sizeof(*rtypes)); + rtypes[0] = parse_param_text_full(a, d->return_type, parent, module_qn, reg); + rtypes[1] = NULL; + } else if (d->return_types && d->return_types[0]) { + rtypes = (const CBMType **)cbm_arena_alloc(a, 2 * sizeof(*rtypes)); + rtypes[0] = parse_param_text_full(a, d->return_types[0], parent, module_qn, reg); + rtypes[1] = NULL; + } + rf.signature = cbm_type_func(a, d->param_names, ptypes, rtypes); + if (d->parent_class) { + rf.receiver_type = d->parent_class; + } + cbm_registry_add_func(reg, rf); + } +} + +/* ── AST-driven param-type patching ────────────────────────────────── + * + * extract_defs.c's clean_type_name() strips generics before storing + * d->param_types — so Consumer arrives as just "Consumer". This + * is fine for graph-storage but loses the arg-type info the LSP needs + * for SAM-binder substitution. + * + * patch_method_signatures_from_ast walks the file's AST, finds each + * method/constructor declaration, builds its QN to match what's already + * in the registry, then re-extracts the formal_parameter type texts + * directly from the source (with generics preserved) and replaces the + * registered func's signature with a template-aware version. */ + +static void patch_method_signatures_from_ast(JavaLSPContext *ctx, CBMTypeRegistry *reg, + TSNode class_node, const char *enclosing_class_qn); + +static void patch_one_method(JavaLSPContext *ctx, CBMTypeRegistry *reg, TSNode method_node, + const char *class_qn, bool is_constructor) { + TSNode name_node = ts_node_child_by_field_name(method_node, "name", 4); + if (ts_node_is_null(name_node)) return; + char *mname = node_text(ctx, name_node); + if (!mname) return; + char *method_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, mname); + + /* Find the registered func by exact QN. */ + CBMRegisteredFunc *slot = NULL; + for (int fi = 0; fi < reg->func_count; fi++) { + if (reg->funcs[fi].qualified_name && + strcmp(reg->funcs[fi].qualified_name, method_qn) == 0) { + slot = ®->funcs[fi]; + break; + } + } + if (!slot) return; + + /* Re-extract param types from the AST. */ + TSNode params = ts_node_child_by_field_name(method_node, "parameters", 10); + if (ts_node_is_null(params)) return; + uint32_t pn = ts_node_named_child_count(params); + if (pn == 0) return; + const CBMType **ptypes = (const CBMType **)cbm_arena_alloc(ctx->arena, + (size_t)(pn + 1) + * sizeof(*ptypes)); + if (!ptypes) return; + int idx = 0; + for (uint32_t i = 0; i < pn; i++) { + TSNode p = ts_node_named_child(params, i); + const char *pk = ts_node_type(p); + if (strcmp(pk, "formal_parameter") != 0 && strcmp(pk, "spread_parameter") != 0) { + continue; + } + TSNode ptype = ts_node_child_by_field_name(p, "type", 4); + const CBMType *pt = cbm_type_unknown(); + if (!ts_node_is_null(ptype)) pt = java_parse_type_node(ctx, ptype); + if (strcmp(pk, "spread_parameter") == 0 && pt) { + pt = cbm_type_slice(ctx->arena, pt); + } + ptypes[idx++] = pt; + } + ptypes[idx] = NULL; + + /* Re-extract return type from AST (only for methods, not constructors). */ + const CBMType **rtypes = NULL; + if (!is_constructor) { + TSNode rtype = ts_node_child_by_field_name(method_node, "type", 4); + if (!ts_node_is_null(rtype)) { + rtypes = (const CBMType **)cbm_arena_alloc(ctx->arena, 2 * sizeof(*rtypes)); + rtypes[0] = java_parse_type_node(ctx, rtype); + rtypes[1] = NULL; + } + } + if (is_constructor) { + rtypes = (const CBMType **)cbm_arena_alloc(ctx->arena, 2 * sizeof(*rtypes)); + rtypes[0] = cbm_type_named(ctx->arena, class_qn); + rtypes[1] = NULL; + } + + /* Rebuild the FUNC type with both param-types and return-types. */ + slot->signature = cbm_type_func(ctx->arena, NULL, ptypes, rtypes); +} + +static void patch_method_signatures_from_ast(JavaLSPContext *ctx, CBMTypeRegistry *reg, + TSNode class_node, const char *enclosing_class_qn) { + if (ts_node_is_null(class_node)) return; + const char *kind = ts_node_type(class_node); + if (strcmp(kind, "class_declaration") != 0 && strcmp(kind, "interface_declaration") != 0 && + strcmp(kind, "enum_declaration") != 0 && strcmp(kind, "record_declaration") != 0) { + return; + } + TSNode name_node = ts_node_child_by_field_name(class_node, "name", 4); + if (ts_node_is_null(name_node)) return; + char *cname = node_text(ctx, name_node); + if (!cname) return; + char *class_qn; + if (enclosing_class_qn) { + class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", enclosing_class_qn, cname); + } else if (ctx->module_qn && ctx->module_qn[0]) { + class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); + } else { + class_qn = cbm_arena_strdup(ctx->arena, cname); + } + + TSNode body = ts_node_child_by_field_name(class_node, "body", 4); + if (ts_node_is_null(body)) return; + + const char *saved = ctx->enclosing_class_qn; + ctx->enclosing_class_qn = class_qn; + push_enclosing_class(ctx, class_qn); + + uint32_t n = ts_node_named_child_count(body); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(body, i); + const char *ck = ts_node_type(c); + if (strcmp(ck, "method_declaration") == 0) { + patch_one_method(ctx, reg, c, class_qn, false); + } else if (strcmp(ck, "constructor_declaration") == 0) { + patch_one_method(ctx, reg, c, class_qn, true); + } else if (strcmp(ck, "class_declaration") == 0 || + strcmp(ck, "interface_declaration") == 0 || + strcmp(ck, "enum_declaration") == 0 || + strcmp(ck, "record_declaration") == 0) { + patch_method_signatures_from_ast(ctx, reg, c, class_qn); + } + } + pop_enclosing_class(ctx); + ctx->enclosing_class_qn = saved; +} + +/* ── AST-driven field metadata population ─────────────────────────── */ + +/* Append (field_name, field_type) to the registry slot for `class_qn`. + * Used by populate_class_fields_from_ast so eval_field_access can resolve + * `obj.field.method()` for arbitrary receivers, not just `this`. */ +static void append_field_to_class(CBMTypeRegistry *reg, CBMArena *a, const char *class_qn, + const char *field_name, const CBMType *ftype) { + CBMRegisteredType *slot = NULL; + for (int ti = 0; ti < reg->type_count; ti++) { + if (reg->types[ti].qualified_name && + strcmp(reg->types[ti].qualified_name, class_qn) == 0) { + slot = ®->types[ti]; + break; + } + } + if (!slot) return; + + int existing = 0; + if (slot->field_names) { + while (slot->field_names[existing]) existing++; + } + const char **new_names = (const char **)cbm_arena_alloc(a, + (size_t)(existing + 2) + * sizeof(*new_names)); + const CBMType **new_types = (const CBMType **)cbm_arena_alloc(a, + (size_t)(existing + 2) + * sizeof(*new_types)); + if (!new_names || !new_types) return; + for (int j = 0; j < existing; j++) { + new_names[j] = slot->field_names[j]; + new_types[j] = slot->field_types[j]; + } + new_names[existing] = cbm_arena_strdup(a, field_name); + new_types[existing] = ftype ? ftype : cbm_type_unknown(); + new_names[existing + 1] = NULL; + new_types[existing + 1] = NULL; + slot->field_names = new_names; + slot->field_types = new_types; +} + +/* Walk a class_declaration / interface_declaration / enum_declaration body + * and append each field_declaration to its containing class's registry + * field arrays. Recurses into nested type declarations. */ +static void populate_class_fields_from_ast(JavaLSPContext *ctx, CBMTypeRegistry *reg, + TSNode class_node, const char *enclosing_class_qn) { + if (ts_node_is_null(class_node)) return; + const char *kind = ts_node_type(class_node); + if (strcmp(kind, "class_declaration") != 0 && strcmp(kind, "interface_declaration") != 0 && + strcmp(kind, "enum_declaration") != 0 && strcmp(kind, "record_declaration") != 0) { + return; + } + TSNode name_node = ts_node_child_by_field_name(class_node, "name", 4); + if (ts_node_is_null(name_node)) return; + char *cname = node_text(ctx, name_node); + if (!cname) return; + + char *class_qn; + if (enclosing_class_qn) { + class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", enclosing_class_qn, cname); + } else if (ctx->module_qn && ctx->module_qn[0]) { + class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); + } else { + class_qn = cbm_arena_strdup(ctx->arena, cname); + } + + TSNode body = ts_node_child_by_field_name(class_node, "body", 4); + if (ts_node_is_null(body)) return; + /* Snapshot enclosing-class stack so java_parse_type_node sees inner-class + * scope when parsing field types like `Greeter` inside class `Main`. */ + const char *saved_enc = ctx->enclosing_class_qn; + ctx->enclosing_class_qn = class_qn; + push_enclosing_class(ctx, class_qn); + + uint32_t n = ts_node_named_child_count(body); + for (uint32_t i = 0; i < n; i++) { + TSNode c = ts_node_named_child(body, i); + const char *ck = ts_node_type(c); + if (strcmp(ck, "field_declaration") == 0) { + TSNode tn = ts_node_child_by_field_name(c, "type", 4); + const CBMType *ftype = ts_node_is_null(tn) ? cbm_type_unknown() + : java_parse_type_node(ctx, tn); + uint32_t fcc = ts_node_named_child_count(c); + for (uint32_t j = 0; j < fcc; j++) { + TSNode dec = ts_node_named_child(c, j); + if (strcmp(ts_node_type(dec), "variable_declarator") != 0) continue; + TSNode dname = ts_node_child_by_field_name(dec, "name", 4); + if (ts_node_is_null(dname)) continue; + char *fname = node_text(ctx, dname); + if (fname) append_field_to_class(reg, ctx->arena, class_qn, fname, ftype); + } + } else if (strcmp(ck, "class_declaration") == 0 || + strcmp(ck, "interface_declaration") == 0 || + strcmp(ck, "enum_declaration") == 0 || + strcmp(ck, "record_declaration") == 0) { + populate_class_fields_from_ast(ctx, reg, c, class_qn); + } + } + pop_enclosing_class(ctx); + ctx->enclosing_class_qn = saved_enc; +} + +/* ── Top-level entry: cbm_run_java_lsp ────────────────────────────── */ + +extern const TSLanguage *tree_sitter_java(void); + +void cbm_run_java_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len, + TSNode root) { + if (!arena || !result || !source) return; + + CBMTypeRegistry reg; + cbm_registry_init(®, arena); + + /* Java stdlib — java.lang/util/io/etc. */ + cbm_java_stdlib_register(®, arena); + + /* Module QN comes from the file result. */ + const char *module_qn = result->module_qn ? result->module_qn : ""; + + JavaLSPContext ctx; + java_lsp_init(&ctx, arena, source, source_len, ®, NULL, module_qn, + &result->resolved_calls); + + /* Add imports collected by extract_imports.c — they are stored as + * (local_name, module_path = full Java path). We translate to the LSP + * import shape here. + * + * Note: extract_imports.c can't tell static / on-demand from the AST in + * a uniform way, so the LSP re-scans the AST for those distinctions in + * java_lsp_process_file. We still re-emit TYPE imports here so that + * resolution works even if process_file's import scan misses + * pre-grammar-version edge cases. */ + for (int i = 0; i < result->imports.count; i++) { + CBMImport *imp = &result->imports.items[i]; + if (!imp->local_name || !imp->module_path) continue; + java_lsp_add_import(&ctx, imp->local_name, imp->module_path, CBM_JAVA_IMPORT_TYPE); + } + + /* Register file-local defs into the registry. */ + register_local_func_or_type_from_file(&ctx, ®, result); + + /* Populate field metadata from the AST so eval_field_access works on + * receivers other than `this`. Also patch method signatures with + * generics-preserving types (extract_defs strips generics from + * d->param_types via clean_type_name). */ + uint32_t rn = ts_node_named_child_count(root); + for (uint32_t i = 0; i < rn; i++) { + TSNode c = ts_node_named_child(root, i); + const char *ck = ts_node_type(c); + if (strcmp(ck, "class_declaration") == 0 || strcmp(ck, "interface_declaration") == 0 || + strcmp(ck, "enum_declaration") == 0 || strcmp(ck, "record_declaration") == 0) { + populate_class_fields_from_ast(&ctx, ®, c, NULL); + patch_method_signatures_from_ast(&ctx, ®, c, NULL); + } + } + + /* Walk the file. */ + java_lsp_process_file(&ctx, root); +} + +/* ── Cross-file LSP ───────────────────────────────────────────────── */ + +void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len, + const char *module_qn, CBMLSPDef *defs, int def_count, + const char **import_names, const char **import_qns, int import_count, + TSTree *cached_tree, CBMResolvedCallArray *out) { + if (!arena || !source) return; + + CBMTypeRegistry reg; + cbm_registry_init(®, arena); + cbm_java_stdlib_register(®, arena); + + /* Register cross-file defs. */ + for (int i = 0; i < def_count; i++) { + CBMLSPDef *d = &defs[i]; + if (!d->qualified_name || !d->short_name || !d->label) continue; + if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0 || + strcmp(d->label, "Enum") == 0 || strcmp(d->label, "Type") == 0) { + CBMRegisteredType rt; + memset(&rt, 0, sizeof(rt)); + rt.qualified_name = d->qualified_name; + rt.short_name = d->short_name; + rt.is_interface = (strcmp(d->label, "Interface") == 0) || d->is_interface; + /* Embedded types from "|"-separated text. */ + if (d->embedded_types && d->embedded_types[0]) { + int n = 1; + for (const char *p = d->embedded_types; *p; p++) if (*p == '|') n++; + const char **emb = (const char **)cbm_arena_alloc(arena, + (size_t)(n + 1) * sizeof(*emb)); + int idx = 0; + const char *start = d->embedded_types; + while (*start) { + const char *end = start; + while (*end && *end != '|') end++; + if (end > start) { + emb[idx++] = cbm_arena_strndup(arena, start, (size_t)(end - start)); + } + if (!*end) break; + start = end + 1; + } + emb[idx] = NULL; + rt.embedded_types = emb; + } + cbm_registry_add_type(®, rt); + } else if (strcmp(d->label, "Method") == 0 || strcmp(d->label, "Function") == 0 || + strcmp(d->label, "Constructor") == 0) { + CBMRegisteredFunc rf; + memset(&rf, 0, sizeof(rf)); + rf.qualified_name = d->qualified_name; + rf.short_name = d->short_name; + rf.min_params = -1; + rf.receiver_type = d->receiver_type; + /* Build single-return signature from return_types text. */ + const CBMType *ret = NULL; + if (d->return_types && d->return_types[0]) { + const char *no_pipe = d->return_types; + const char *bar = strchr(no_pipe, '|'); + const char *first; + if (bar) { + first = cbm_arena_strndup(arena, no_pipe, (size_t)(bar - no_pipe)); + } else { + first = no_pipe; + } + ret = parse_param_text(arena, first, d->def_module_qn); + } + const CBMType **rtypes = NULL; + if (ret) { + rtypes = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rtypes)); + rtypes[0] = ret; + rtypes[1] = NULL; + } + rf.signature = cbm_type_func(arena, NULL, NULL, rtypes); + cbm_registry_add_func(®, rf); + } + } + + /* Parse if needed. */ + TSTree *tree = cached_tree; + bool owns_tree = false; + if (!tree) { + TSParser *parser = ts_parser_new(); + ts_parser_set_language(parser, tree_sitter_java()); + tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)source_len); + ts_parser_delete(parser); + owns_tree = true; + } + if (!tree) return; + TSNode root = ts_tree_root_node(tree); + + JavaLSPContext ctx; + java_lsp_init(&ctx, arena, source, source_len, ®, NULL, module_qn, out); + + /* Apply caller-supplied imports. */ + for (int i = 0; i < import_count; i++) { + if (!import_names[i] || !import_qns[i]) continue; + java_lsp_add_import(&ctx, import_names[i], import_qns[i], CBM_JAVA_IMPORT_TYPE); + } + + java_lsp_process_file(&ctx, root); + + if (owns_tree) ts_tree_delete(tree); +} + +void cbm_batch_java_lsp_cross(CBMArena *arena, CBMBatchJavaLSPFile *files, int file_count, + CBMResolvedCallArray *out) { + if (!files || file_count <= 0) return; + for (int i = 0; i < file_count; i++) { + cbm_run_java_lsp_cross(arena, files[i].source, files[i].source_len, files[i].module_qn, + files[i].defs, files[i].def_count, files[i].import_names, + files[i].import_qns, files[i].import_count, files[i].cached_tree, + &out[i]); + } +} diff --git a/internal/cbm/lsp/java_lsp.h b/internal/cbm/lsp/java_lsp.h new file mode 100644 index 000000000..6bae01932 --- /dev/null +++ b/internal/cbm/lsp/java_lsp.h @@ -0,0 +1,167 @@ +#ifndef CBM_LSP_JAVA_LSP_H +#define CBM_LSP_JAVA_LSP_H + +/* + * java_lsp.h — Pure-C Java semantic resolver. + * + * Reverse-engineered from JLS §6 (Names) and §15 (Expressions) plus the + * algorithm shape used by Eclipse JDT-LS / java-language-server (which both + * delegate to javac.com.sun.source). The goal is parity with what JDT-LS + * exposes through textDocument/definition + textDocument/references for + * call-site resolution, *without* shelling out to javac. + * + * Mirrors the structure of go_lsp.h / c_lsp.h / php_lsp.h / py_lsp.h. + * + * Resolution scheme (single file): + * 1. Tree-sitter parses Java source into AST. + * 2. Build a CBMTypeRegistry from this file's CBMDefinitions + Java + * stdlib (java.lang.*, java.util.*, java.io.*, java.util.function.*, + * java.util.stream.*). + * 3. Walk the AST and for every method_invocation / object_creation / + * field_access expression, evaluate the expression's type using + * JLS-style scope chains (block → method params → class members → + * superclass chain → outer class → import single → import on-demand → + * java.lang → same package). + * 4. Match the textual call (callee name + arity) against the resolved + * receiver type's method set, walking superclasses and interfaces. + * Best-overload resolution falls back to argument count match. + * 5. Emit CBMResolvedCall entries with confidence ≥ 0.6 (the LSP floor + * enforced by lsp_resolve.h). + */ + +#include "type_rep.h" +#include "scope.h" +#include "type_registry.h" +#include "../cbm.h" +#include "go_lsp.h" /* CBMLSPDef, CBMResolvedCallArray reused across languages */ + +/* Java `use`-style import kinds. Mirrors PHP's enum with Java semantics. */ +enum { + CBM_JAVA_IMPORT_TYPE = 0, /* import com.foo.Bar; — type import */ + CBM_JAVA_IMPORT_STATIC = 1, /* import static com.foo.Bar.method; */ + CBM_JAVA_IMPORT_ON_DEMAND = 2, /* import com.foo.*; — package on-demand */ + CBM_JAVA_IMPORT_STATIC_OD = 3, /* import static com.foo.Bar.*; */ +}; + +/* Per-file resolution context. */ +typedef struct { + CBMArena *arena; + const char *source; + int source_len; + const CBMTypeRegistry *registry; + CBMScope *current_scope; + + /* Java package — the package declared in the file ("com.example"), or + * empty string for the unnamed package. Stored in dotted form. */ + const char *package_name; + + /* The path-derived module QN for this file (passed in from the caller), + * used as the QN prefix for types defined here. */ + const char *module_qn; + + /* Import map. Each entry is one of CBM_JAVA_IMPORT_*. + * - TYPE: local_name="Bar", target_qn="com.foo.Bar" + * - STATIC: local_name="sqrt", target_qn="java.lang.Math.sqrt" + * - ON_DEMAND: local_name="*", target_qn="com.foo" + * - STATIC_OD: local_name="*", target_qn="java.lang.Math" + */ + const char **import_local_names; + const char **import_target_qns; + int *import_kinds; + int import_count; + int import_cap; + + /* Current enclosing context. */ + const char *enclosing_method_qn; /* QN of the nearest method/ctor */ + const char *enclosing_class_qn; /* QN of the nearest class/interface/enum */ + const char *enclosing_super_qn; /* QN of the immediate superclass (NULL ⇒ Object) */ + const char *enclosing_class_short; /* short name of enclosing class — for "this" + ctor */ + const char **enclosing_class_stack; /* nested-class stack (enclosing_class_qn at each depth) */ + int enclosing_class_depth; + int enclosing_class_cap; + + /* Output: resolved + diagnostic call edges. */ + CBMResolvedCallArray *resolved_calls; + + /* Recursion guards. */ + int eval_depth; + int statement_depth; + + /* Debug mode (CBM_LSP_DEBUG env). */ + bool debug; +} JavaLSPContext; + +/* ── Initialization / configuration ───────────────────────────────── */ + +void java_lsp_init(JavaLSPContext *ctx, CBMArena *arena, const char *source, int source_len, + const CBMTypeRegistry *registry, const char *package_name, + const char *module_qn, CBMResolvedCallArray *out); + +void java_lsp_add_import(JavaLSPContext *ctx, const char *local_name, const char *target_qn, + int kind); + +/* ── Walking / resolution ─────────────────────────────────────────── */ + +void java_lsp_process_file(JavaLSPContext *ctx, TSNode root); + +/* Evaluate the type of an arbitrary Java expression node. May return + * cbm_type_unknown(); never returns NULL. */ +const CBMType *java_eval_expr_type(JavaLSPContext *ctx, TSNode node); + +/* Convert a Java type AST node (type_identifier, generic_type, array_type, + * scoped_type_identifier, void_type, integral_type, ...) into a CBMType. */ +const CBMType *java_parse_type_node(JavaLSPContext *ctx, TSNode node); + +/* Process a Java statement, binding any declared variables/parameters into + * the current scope. */ +void java_process_statement(JavaLSPContext *ctx, TSNode node); + +/* Resolve a Java type name (bare or qualified) against the current scope + * (imports + java.lang + same package). Returns the registered FQN or NULL. */ +const char *java_resolve_type_name(JavaLSPContext *ctx, const char *name); + +/* Lookup a method on a class, walking the super-chain and implemented + * interfaces. Returns the matched function or NULL. */ +const CBMRegisteredFunc *java_lookup_method(JavaLSPContext *ctx, const char *class_qn, + const char *method_name, int arg_count); + +/* Lookup a field on a class, walking the super-chain. Returns the field's + * type, or cbm_type_unknown() on miss. */ +const CBMType *java_lookup_field_type(JavaLSPContext *ctx, const char *class_qn, + const char *field_name); + +/* ── Top-level entry points ───────────────────────────────────────── */ + +/* Single-file LSP: build registry from file defs + stdlib, walk and resolve. */ +void cbm_run_java_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len, + TSNode root); + +/* Cross-file LSP: build registry from defs + stdlib, re-parse if needed, + * walk and resolve. defs include both local + cross-file definitions. */ +void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len, + const char *module_qn, CBMLSPDef *defs, int def_count, + const char **import_names, const char **import_qns, int import_count, + TSTree *cached_tree, CBMResolvedCallArray *out); + +/* Register the Java standard library (java.lang/util/io/etc.) into reg. + * Implementation lives in generated/java_stdlib_data.c. */ +void cbm_java_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena); + +/* ── Batch cross-file LSP ─────────────────────────────────────────── */ + +typedef struct { + const char *source; + int source_len; + const char *module_qn; + TSTree *cached_tree; + CBMLSPDef *defs; + int def_count; + const char **import_names; + const char **import_qns; + int import_count; +} CBMBatchJavaLSPFile; + +void cbm_batch_java_lsp_cross(CBMArena *arena, CBMBatchJavaLSPFile *files, int file_count, + CBMResolvedCallArray *out); + +#endif /* CBM_LSP_JAVA_LSP_H */ diff --git a/internal/cbm/lsp_all.c b/internal/cbm/lsp_all.c index 7369b11dd..7e4fe961f 100644 --- a/internal/cbm/lsp_all.c +++ b/internal/cbm/lsp_all.c @@ -14,3 +14,5 @@ #include "lsp/generated/php_stdlib_data.c" #include "lsp/generated/python_stdlib_data.c" #include "lsp/py_lsp.c" +#include "lsp/java_lsp.c" +#include "lsp/generated/java_stdlib_data.c" diff --git a/tests/test_java_lsp.c b/tests/test_java_lsp.c new file mode 100644 index 000000000..69fcc3c04 --- /dev/null +++ b/tests/test_java_lsp.c @@ -0,0 +1,1927 @@ +/* + * test_java_lsp.c — Tests for the pure-C Java LSP type-aware call resolver. + * + * Coverage targets parity (≥90%) with what JDT-LS / java-language-server + * report through textDocument/definition + textDocument/references at call + * sites: + * - Single-file resolution: identifiers, fields, methods, generics, + * overloads, this/super, statics, enhanced-for, lambdas, ternaries, + * casts, instance/static dispatch. + * - Inheritance + interface dispatch: method look-ups walking the + * super-chain and implemented interfaces. + * - java.lang implicit-import behaviour (String, Math, System, Objects). + * - java.util collections (ArrayList, HashMap, Set), java.io streams, + * java.util.stream chains, java.util.function functional interfaces, + * java.util.concurrent ExecutorService / CompletableFuture / atomics, + * java.time.LocalDate / Instant / Duration, java.nio.file.Path / Files, + * java.util.regex.Pattern / Matcher, java.lang.Thread. + * - Negative tests: ambiguous receivers, missing types, wrong arity. + * - Diagnostic emission (lsp_unresolved confidence=0). + * + * Each test is a TEST(...) function returning 0 on success. They run via + * RUN_TEST in suite_java_lsp at the bottom of this file. + */ + +#include "test_framework.h" +#include "cbm.h" +#include "lsp/java_lsp.h" + +#include +#include + +/* ── Helpers ─────────────────────────────────────────────────────── */ + +static CBMFileResult *extract_java(const char *source) { + return cbm_extract_file(source, (int)strlen(source), CBM_LANG_JAVA, "test", "Main.java", 0, + NULL, NULL); +} + +static CBMFileResult *extract_java_at(const char *source, const char *rel_path) { + return cbm_extract_file(source, (int)strlen(source), CBM_LANG_JAVA, "test", rel_path, 0, NULL, + NULL); +} + +static int find_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { + for (int i = 0; i < r->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &r->resolved_calls.items[i]; + if (rc->confidence < 0.5f) continue; + if (rc->caller_qn && strstr(rc->caller_qn, callerSub) && rc->callee_qn && + strstr(rc->callee_qn, calleeSub)) { + return i; + } + } + return -1; +} + +static int require_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { + int idx = find_resolved(r, callerSub, calleeSub); + if (idx < 0) { + printf("\n MISSING resolved call: caller~%s -> callee~%s (have %d entries)\n", callerSub, + calleeSub, r->resolved_calls.count); + for (int i = 0; i < r->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &r->resolved_calls.items[i]; + printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)", + rc->callee_qn ? rc->callee_qn : "(null)", + rc->strategy ? rc->strategy : "(null)", rc->confidence); + } + } + return idx; +} + +static int count_resolved(const CBMFileResult *r) { + int n = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + if (r->resolved_calls.items[i].confidence >= 0.5f) n++; + } + return n; +} + +/* ── Category 1: java.lang.String resolution ─────────────────────── */ + +TEST(jlsp_string_length) { + const char *src = + "public class Main {\n" + " public int run(String s) {\n" + " return s.length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_string_concat_method) { + const char *src = + "public class Main {\n" + " public String greet(String name) {\n" + " String hello = \"Hello, \" + name;\n" + " return hello.toUpperCase();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "greet", "String.toUpperCase"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_string_chain) { + const char *src = + "public class Main {\n" + " public String clean(String raw) {\n" + " return raw.trim().toLowerCase();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "clean", "String.trim"), 0); + ASSERT_GTE(require_resolved(r, "clean", "String.toLowerCase"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_string_static) { + const char *src = + "public class Main {\n" + " public String fmt(int x) {\n" + " return String.valueOf(x);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "fmt", "String.valueOf"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_string_format) { + const char *src = + "public class Main {\n" + " public String build(int n) {\n" + " return String.format(\"count=%d\", n);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "build", "String.format"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 2: java.lang.Math + System + Integer ────────────────── */ + +TEST(jlsp_math_static) { + const char *src = + "public class Main {\n" + " public double dist(double a, double b) {\n" + " return Math.sqrt(a * a + b * b);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "dist", "Math.sqrt"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_math_chain) { + const char *src = + "public class Main {\n" + " public double f(double x) {\n" + " return Math.pow(Math.abs(x), 2);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "f", "Math.pow"), 0); + ASSERT_GTE(require_resolved(r, "f", "Math.abs"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_system_currentTimeMillis) { + const char *src = + "public class Main {\n" + " public long now() {\n" + " return System.currentTimeMillis();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "now", "System.currentTimeMillis"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_system_out_println) { + const char *src = + "public class Main {\n" + " public void hi() {\n" + " System.out.println(\"hi\");\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "hi", "PrintStream.println"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_integer_parse) { + const char *src = + "public class Main {\n" + " public int toInt(String s) {\n" + " return Integer.parseInt(s);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "toInt", "Integer.parseInt"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 3: Local class methods (this, super, plain) ─────────── */ + +TEST(jlsp_local_method_call) { + const char *src = + "public class Main {\n" + " String greet() { return \"hi\"; }\n" + " void run() { greet(); }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "greet"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_this_method_call) { + const char *src = + "public class Main {\n" + " String greet() { return \"hi\"; }\n" + " void run() { this.greet(); }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "greet"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_super_method_call) { + const char *src = + "public class Main extends java.lang.Object {\n" + " public String describe() {\n" + " return super.toString();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "describe", "Object.toString"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_local_field_method) { + const char *src = + "public class Main {\n" + " private String name;\n" + " public int run() {\n" + " return this.name.length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_field_no_this_method) { + const char *src = + "public class Main {\n" + " private String name = \"x\";\n" + " public int run() {\n" + " return name.length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 4: Constructors ────────────────────────────────────── */ + +TEST(jlsp_object_creation) { + const char *src = + "public class Main {\n" + " public StringBuilder build() {\n" + " return new StringBuilder();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "build", "StringBuilder"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_arraylist_creation) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public ArrayList empty() {\n" + " return new ArrayList<>();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "empty", "ArrayList"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_constructor_then_method) { + const char *src = + "public class Main {\n" + " public String go() {\n" + " return new StringBuilder().append(\"x\").toString();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "go", "StringBuilder.append"), 0); + ASSERT_GTE(require_resolved(r, "go", "toString"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 5: java.util.ArrayList / List / Map ────────────────── */ + +TEST(jlsp_list_add) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public void run() {\n" + " ArrayList xs = new ArrayList<>();\n" + " xs.add(\"a\");\n" + " xs.size();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "ArrayList.add"), 0); + ASSERT_GTE(require_resolved(r, "run", "ArrayList.size"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_list_get_chain) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public int len(ArrayList xs) {\n" + " return xs.get(0).length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "len", "ArrayList.get"), 0); + /* Generics: get returns String → length resolves on String */ + ASSERT_GTE(require_resolved(r, "len", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_hashmap) { + const char *src = + "import java.util.HashMap;\n" + "public class Main {\n" + " public int run(HashMap m) {\n" + " m.put(\"k\", 1);\n" + " return m.size();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "HashMap.put"), 0); + ASSERT_GTE(require_resolved(r, "run", "HashMap.size"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_list_static_of) { + const char *src = + "import java.util.List;\n" + "public class Main {\n" + " public List ones() {\n" + " return List.of(\"a\", \"b\");\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "ones", "List.of"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_set_contains) { + const char *src = + "import java.util.HashSet;\n" + "public class Main {\n" + " public boolean has(HashSet s, String x) {\n" + " return s.contains(x);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "has", "HashSet.contains"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 6: Inheritance ─────────────────────────────────────── */ + +TEST(jlsp_inherited_method) { + const char *src = + "public class Main {\n" + " static class Animal {\n" + " public String name() { return \"x\"; }\n" + " }\n" + " static class Dog extends Animal {\n" + " public void bark(Dog d) { d.name(); }\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "bark", "name"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_inherited_through_chain) { + const char *src = + "public class Main {\n" + " static class A { public String greet() { return \"a\"; } }\n" + " static class B extends A {}\n" + " static class C extends B {\n" + " public String go(C c) { return c.greet(); }\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "go", "greet"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_super_chain_object_toString) { + const char *src = + "public class Main {\n" + " static class T {\n" + " public String describe() { return super.toString(); }\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "describe", "Object.toString"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 7: Enhanced for ─────────────────────────────────────── */ + +TEST(jlsp_enhanced_for_array) { + const char *src = + "public class Main {\n" + " public int total(String[] xs) {\n" + " int n = 0;\n" + " for (String x : xs) {\n" + " n += x.length();\n" + " }\n" + " return n;\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "total", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_enhanced_for_list) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public int total(ArrayList xs) {\n" + " int n = 0;\n" + " for (String x : xs) {\n" + " n += x.length();\n" + " }\n" + " return n;\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "total", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_enhanced_for_var) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public int total(ArrayList xs) {\n" + " int n = 0;\n" + " for (var x : xs) {\n" + " n += x.length();\n" + " }\n" + " return n;\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "total", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 8: var local-type inference ─────────────────────────── */ + +TEST(jlsp_var_inference) { + const char *src = + "public class Main {\n" + " public int run() {\n" + " var s = \"hello\";\n" + " return s.length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_var_inference_constructor) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public void run() {\n" + " var xs = new ArrayList();\n" + " xs.add(\"a\");\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "ArrayList.add"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 9: Cast / instanceof / ternary / array ─────────────── */ + +TEST(jlsp_cast_method) { + const char *src = + "public class Main {\n" + " public int len(Object o) {\n" + " return ((String) o).length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "len", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_ternary) { + const char *src = + "public class Main {\n" + " public int run(boolean b, String x) {\n" + " return (b ? x : x).length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_array_index) { + const char *src = + "public class Main {\n" + " public int run(String[] xs) {\n" + " return xs[0].length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_array_length_field) { + const char *src = + "public class Main {\n" + " public int run(String[] xs) {\n" + " int n = xs.length;\n" + " return n;\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + /* `xs.length` is a field access, no method call. Just confirms parse + * doesn't break and no spurious diagnostics fired. */ + cbm_free_result(r); + PASS(); +} + +/* ── Category 10: Optional / Streams / lambdas ───────────────────── */ + +TEST(jlsp_optional_chain) { + const char *src = + "import java.util.Optional;\n" + "public class Main {\n" + " public String run(Optional o) {\n" + " return o.orElse(\"\");\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Optional.orElse"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_stream_filter_map) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public long run(ArrayList xs) {\n" + " return xs.stream().filter(x -> x.length() > 0).count();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "stream"), 0); + ASSERT_GTE(require_resolved(r, "run", "filter"), 0); + ASSERT_GTE(require_resolved(r, "run", "count"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_stream_collect_toList) { + const char *src = + "import java.util.ArrayList;\n" + "import java.util.stream.Collectors;\n" + "public class Main {\n" + " public Object run(ArrayList xs) {\n" + " return xs.stream().collect(Collectors.toList());\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Collectors.toList"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 11: Functional interfaces ──────────────────────────── */ + +TEST(jlsp_predicate_test) { + const char *src = + "import java.util.function.Predicate;\n" + "public class Main {\n" + " public boolean ok(Predicate p, String s) {\n" + " return p.test(s);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "ok", "Predicate.test"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_function_apply) { + const char *src = + "import java.util.function.Function;\n" + "public class Main {\n" + " public Object run(Function f, String s) {\n" + " return f.apply(s);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Function.apply"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_supplier_get) { + const char *src = + "import java.util.function.Supplier;\n" + "public class Main {\n" + " public Object run(Supplier s) {\n" + " return s.get();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Supplier.get"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 12: Throwable + try/catch ──────────────────────────── */ + +TEST(jlsp_exception_message) { + const char *src = + "public class Main {\n" + " public String describe() {\n" + " try {\n" + " Integer.parseInt(\"x\");\n" + " } catch (NumberFormatException e) {\n" + " return e.getMessage();\n" + " }\n" + " return \"\";\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "describe", "Throwable.getMessage"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 13: Imports (static + on-demand) ───────────────────── */ + +TEST(jlsp_static_import_method) { + const char *src = + "import static java.lang.Math.sqrt;\n" + "public class Main {\n" + " public double run(double x) {\n" + " return sqrt(x);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Math.sqrt"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_on_demand_import) { + const char *src = + "import java.util.*;\n" + "public class Main {\n" + " public void run() {\n" + " HashMap m = new HashMap<>();\n" + " m.put(\"x\", 1);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "HashMap.put"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 14: Generics + nested templates ────────────────────── */ + +TEST(jlsp_generic_get_method) { + const char *src = + "import java.util.HashMap;\n" + "public class Main {\n" + " public int run(HashMap m) {\n" + " return m.get(\"k\").length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "HashMap.get"), 0); + /* The chained .length() lookup depends on generic-arg substitution into + * the return type. We cover that for List-of-String elsewhere; with + * HashMap.get the registry signature returns Object, so the chained + * String.length call is allowed to be unresolved here. Accept either. */ + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_list_of_string_chain) { + const char *src = + "import java.util.List;\n" + "public class Main {\n" + " public int run(List xs) {\n" + " return xs.get(0).length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "List.get"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 15: Throw + catch types ────────────────────────────── */ + +TEST(jlsp_throw_runtime) { + const char *src = + "public class Main {\n" + " public void run() {\n" + " throw new IllegalArgumentException(\"bad\");\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "IllegalArgumentException"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 16: java.io Streams ────────────────────────────────── */ + +TEST(jlsp_print_stream) { + const char *src = + "import java.io.PrintStream;\n" + "public class Main {\n" + " public void run(PrintStream s) {\n" + " s.println(\"hi\");\n" + " s.flush();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "PrintStream.println"), 0); + ASSERT_GTE(require_resolved(r, "run", "PrintStream.flush"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_buffered_reader) { + const char *src = + "import java.io.BufferedReader;\n" + "public class Main {\n" + " public String run(BufferedReader r) throws java.io.IOException {\n" + " return r.readLine();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "BufferedReader.readLine"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_file_methods) { + const char *src = + "import java.io.File;\n" + "public class Main {\n" + " public boolean run(File f) {\n" + " return f.exists() && f.isFile();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "File.exists"), 0); + ASSERT_GTE(require_resolved(r, "run", "File.isFile"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 17: java.nio.file ──────────────────────────────────── */ + +TEST(jlsp_paths_get) { + const char *src = + "import java.nio.file.Paths;\n" + "public class Main {\n" + " public Object run() {\n" + " return Paths.get(\"a\");\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Paths.get"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_files_readString) { + const char *src = + "import java.nio.file.Files;\n" + "import java.nio.file.Path;\n" + "public class Main {\n" + " public String run(Path p) throws java.io.IOException {\n" + " return Files.readString(p);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Files.readString"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 18: java.util.regex ────────────────────────────────── */ + +TEST(jlsp_regex_pattern_compile) { + const char *src = + "import java.util.regex.Pattern;\n" + "public class Main {\n" + " public Pattern run() {\n" + " return Pattern.compile(\"^x\");\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Pattern.compile"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_matcher_find) { + const char *src = + "import java.util.regex.Matcher;\n" + "import java.util.regex.Pattern;\n" + "public class Main {\n" + " public boolean run(String s) {\n" + " Matcher m = Pattern.compile(\"x\").matcher(s);\n" + " return m.find();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Pattern.compile"), 0); + ASSERT_GTE(require_resolved(r, "run", "Matcher.find"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 19: Concurrent + atomic ───────────────────────────── */ + +TEST(jlsp_atomic_increment) { + const char *src = + "import java.util.concurrent.atomic.AtomicInteger;\n" + "public class Main {\n" + " public int bump(AtomicInteger n) {\n" + " return n.incrementAndGet();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "bump", "AtomicInteger.incrementAndGet"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_executor_submit) { + const char *src = + "import java.util.concurrent.ExecutorService;\n" + "import java.util.concurrent.Executors;\n" + "public class Main {\n" + " public void run() {\n" + " ExecutorService es = Executors.newFixedThreadPool(2);\n" + " es.shutdown();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Executors.newFixedThreadPool"), 0); + ASSERT_GTE(require_resolved(r, "run", "ExecutorService.shutdown"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_completable_future_thenApply) { + const char *src = + "import java.util.concurrent.CompletableFuture;\n" + "public class Main {\n" + " public Object run(CompletableFuture f) {\n" + " return f.thenApply(x -> x.length());\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "CompletableFuture.thenApply"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 20: java.time ──────────────────────────────────────── */ + +TEST(jlsp_localdate_now) { + const char *src = + "import java.time.LocalDate;\n" + "public class Main {\n" + " public LocalDate run() {\n" + " return LocalDate.now();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "LocalDate.now"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_duration_chain) { + const char *src = + "import java.time.Duration;\n" + "public class Main {\n" + " public long run() {\n" + " return Duration.ofSeconds(60).toMillis();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Duration.ofSeconds"), 0); + ASSERT_GTE(require_resolved(r, "run", "Duration.toMillis"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 21: User-defined cross-file like (within one file) ─── */ + +TEST(jlsp_user_class_method) { + const char *src = + "public class Main {\n" + " static class Greeter {\n" + " public String greet(String name) { return \"hi \" + name; }\n" + " }\n" + " public String run() {\n" + " Greeter g = new Greeter();\n" + " return g.greet(\"world\");\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "greet"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_user_static_method) { + const char *src = + "public class Main {\n" + " static class Util {\n" + " public static int twice(int x) { return x + x; }\n" + " }\n" + " public int run() {\n" + " return Util.twice(21);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "twice"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_user_chain) { + const char *src = + "public class Main {\n" + " static class Box {\n" + " public Box self() { return this; }\n" + " public String tag() { return \"x\"; }\n" + " }\n" + " public String run(Box b) {\n" + " return b.self().tag();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "self"), 0); + ASSERT_GTE(require_resolved(r, "run", "tag"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 22: Diagnostics ────────────────────────────────────── */ + +TEST(jlsp_unknown_variable_unresolved) { + const char *src = + "public class Main {\n" + " public int run() {\n" + " return blob.size();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + /* Should appear as an unresolved diagnostic (confidence 0) — confirm at + * least one entry exists. */ + int diag = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + if (r->resolved_calls.items[i].confidence == 0.0f) diag++; + } + ASSERT_GTE(diag, 1); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_no_resolved_calls_for_empty_class) { + const char *src = "public class Main {}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_EQ(count_resolved(r), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 23: Quality-vs-tree-sitter parity check ────────────── */ + +/* Without LSP, tree-sitter alone produces calls but no resolved targets. + * This test confirms: on a real-world-ish snippet, the LSP turns unresolved + * tree-sitter calls into resolved calls with high confidence. */ +TEST(jlsp_lifts_treesitter_calls) { + const char *src = + "import java.util.ArrayList;\n" + "import java.util.HashMap;\n" + "public class Main {\n" + " private ArrayList items = new ArrayList<>();\n" + " private HashMap counts = new HashMap<>();\n" + " public int run() {\n" + " items.add(\"a\");\n" + " items.add(\"b\");\n" + " counts.put(\"a\", 1);\n" + " String first = items.get(0).toUpperCase();\n" + " int n = first.length();\n" + " return items.size() + counts.size() + n;\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + /* Tree-sitter alone gives us only raw call nodes; LSP should resolve at + * least 7 of these to fully-qualified callees. */ + ASSERT_GTE(count_resolved(r), 7); + ASSERT_GTE(require_resolved(r, "run", "ArrayList.add"), 0); + ASSERT_GTE(require_resolved(r, "run", "HashMap.put"), 0); + ASSERT_GTE(require_resolved(r, "run", "ArrayList.get"), 0); + ASSERT_GTE(require_resolved(r, "run", "String.toUpperCase"), 0); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + ASSERT_GTE(require_resolved(r, "run", "ArrayList.size"), 0); + ASSERT_GTE(require_resolved(r, "run", "HashMap.size"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 24: cross-file LSP API (registry-fed) ──────────────── */ + +TEST(jlsp_cross_file_basic) { + const char *src = + "package com.example;\n" + "public class Caller {\n" + " public String run(com.example.Greeter g) { return g.greet(\"x\"); }\n" + "}\n"; + /* Provide a single cross-file def for Greeter.greet returning String. */ + CBMArena arena; + cbm_arena_init(&arena); + + CBMResolvedCallArray out; + memset(&out, 0, sizeof(out)); + + CBMLSPDef defs[2]; + memset(defs, 0, sizeof(defs)); + defs[0].qualified_name = "com.example.Greeter"; + defs[0].short_name = "Greeter"; + defs[0].label = "Class"; + defs[0].def_module_qn = "com.example"; + defs[1].qualified_name = "com.example.Greeter.greet"; + defs[1].short_name = "greet"; + defs[1].label = "Method"; + defs[1].receiver_type = "com.example.Greeter"; + defs[1].def_module_qn = "com.example"; + defs[1].return_types = "java.lang.String"; + + const char *imp_names[] = {"Greeter"}; + const char *imp_qns[] = {"com.example.Greeter"}; + + cbm_run_java_lsp_cross(&arena, src, (int)strlen(src), "test.com.example", defs, 2, imp_names, + imp_qns, 1, NULL, &out); + + int found = 0; + for (int i = 0; i < out.count; i++) { + if (out.items[i].confidence < 0.5f) continue; + if (out.items[i].callee_qn && strstr(out.items[i].callee_qn, "Greeter.greet")) { + found = 1; + break; + } + } + if (!found) { + printf("\n cross_file_basic: have %d entries\n", out.count); + for (int i = 0; i < out.count; i++) { + printf(" %s -> %s [%s %.2f]\n", + out.items[i].caller_qn ? out.items[i].caller_qn : "(null)", + out.items[i].callee_qn ? out.items[i].callee_qn : "(null)", + out.items[i].strategy ? out.items[i].strategy : "(null)", + out.items[i].confidence); + } + } + ASSERT_EQ(found, 1); + cbm_arena_destroy(&arena); + PASS(); +} + +/* ── Category 25: Java-specific edge cases ──────────────────────── */ + +TEST(jlsp_objects_requireNonNull) { + const char *src = + "import java.util.Objects;\n" + "public class Main {\n" + " public Object run(Object x) {\n" + " return Objects.requireNonNull(x);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Objects.requireNonNull"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_thread_currentThread) { + const char *src = + "public class Main {\n" + " public Thread run() {\n" + " return Thread.currentThread();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Thread.currentThread"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_class_class_literal) { + const char *src = + "public class Main {\n" + " public String run() {\n" + " return String.class.getName();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Class.getName"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_string_join) { + const char *src = + "public class Main {\n" + " public String run(String[] xs) {\n" + " return String.join(\",\", xs);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.join"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_uuid_random) { + const char *src = + "import java.util.UUID;\n" + "public class Main {\n" + " public String run() {\n" + " return UUID.randomUUID().toString();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "UUID.randomUUID"), 0); + ASSERT_GTE(require_resolved(r, "run", "toString"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_stringbuilder_append_chain) { + const char *src = + "public class Main {\n" + " public String run() {\n" + " return new StringBuilder().append(\"a\").append(\"b\").append(\"c\").toString();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + /* All three .append() and final .toString() should resolve. */ + int appends = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &r->resolved_calls.items[i]; + if (rc->confidence >= 0.5f && rc->callee_qn && strstr(rc->callee_qn, "StringBuilder.append")) { + appends++; + } + } + ASSERT_GTE(appends, 3); + ASSERT_GTE(require_resolved(r, "run", "StringBuilder.toString"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_arrays_aslist) { + const char *src = + "import java.util.Arrays;\n" + "import java.util.List;\n" + "public class Main {\n" + " public List run() {\n" + " return Arrays.asList(\"a\", \"b\");\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Arrays.asList"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_collections_sort) { + const char *src = + "import java.util.ArrayList;\n" + "import java.util.Collections;\n" + "public class Main {\n" + " public void run(ArrayList xs) {\n" + " Collections.sort(xs);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "Collections.sort"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_deeply_nested_block) { + const char *src = + "public class Main {\n" + " public int run() {\n" + " String s = \"hello\";\n" + " if (true) {\n" + " if (s.length() > 0) {\n" + " return s.length();\n" + " }\n" + " }\n" + " return 0;\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + int length_calls = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &r->resolved_calls.items[i]; + if (rc->confidence >= 0.5f && rc->callee_qn && strstr(rc->callee_qn, "String.length")) { + length_calls++; + } + } + ASSERT_GTE(length_calls, 2); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_method_reference_no_crash) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public void run(ArrayList xs) {\n" + " xs.forEach(System.out::println);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + /* forEach should still resolve even if method-ref operand isn't typed. */ + ASSERT_GTE(require_resolved(r, "run", "forEach"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_lambda_no_crash) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public void run(ArrayList xs) {\n" + " xs.forEach(x -> System.out.println(x));\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "forEach"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_record_call) { + const char *src = + "public class Main {\n" + " public record Point(int x, int y) {}\n" + " public int xCoord(Point p) {\n" + " return p.x();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + /* Records create accessor methods; we don't currently model them + * specially, but the call should at least be registered via the AST + * walk (either resolved or as diagnostic). */ + ASSERT_GTE(r->resolved_calls.count, 1); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_inner_class) { + const char *src = + "public class Main {\n" + " static class Inner {\n" + " public String tag() { return \"x\"; }\n" + " }\n" + " public String run() {\n" + " Inner i = new Inner();\n" + " return i.tag();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "tag"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_interface_default_method) { + const char *src = + "public class Main {\n" + " interface Greet {\n" + " default String hello() { return \"hi\"; }\n" + " }\n" + " static class Impl implements Greet {\n" + " public String run() { return hello(); }\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + /* Some indirection — accept any resolved call. */ + ASSERT_GTE(r->resolved_calls.count, 1); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_file_path) { + const char *src = + "public class Main {\n" + " public int run(String s) {\n" + " return s.length();\n" + " }\n" + "}\n"; + /* Use a deeper rel_path to confirm FQN compute respects the path. */ + CBMFileResult *r = extract_java_at(src, "src/main/java/com/example/Main.java"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_deeply_nested_chain) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public int run(ArrayList xs) {\n" + " return xs.get(0).trim().toLowerCase().length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "ArrayList.get"), 0); + ASSERT_GTE(require_resolved(r, "run", "String.trim"), 0); + ASSERT_GTE(require_resolved(r, "run", "String.toLowerCase"), 0); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 26: Lambda SAM inference ────────────────────────────── */ + +TEST(jlsp_lambda_param_typed_in_filter) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public long run(ArrayList xs) {\n" + " return xs.stream().filter(x -> x.length() > 0).count();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + /* x has type String (from Predicate), so x.length() must + * resolve to String.length. This was previously unresolved. */ + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_lambda_param_typed_in_map) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public Object run(ArrayList xs) {\n" + " return xs.stream().map(x -> x.toUpperCase()).count();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.toUpperCase"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_lambda_in_forEach) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public void run(ArrayList xs) {\n" + " xs.forEach(x -> { x.length(); });\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_lambda_in_consumer) { + const char *src = + "import java.util.function.Consumer;\n" + "public class Main {\n" + " public void run(Consumer c) {\n" + " c.accept(\"x\");\n" + " }\n" + " public void go() {\n" + " run(s -> s.toUpperCase());\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "go", "String.toUpperCase"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_lambda_with_block_body) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public void run(ArrayList xs) {\n" + " xs.stream().filter(x -> {\n" + " String t = x.trim();\n" + " return t.length() > 0;\n" + " }).count();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.trim"), 0); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_lambda_typed_in_chained_stream) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public long run(ArrayList xs) {\n" + " return xs.stream()\n" + " .filter(x -> x.length() > 0)\n" + " .map(s -> s.toUpperCase())\n" + " .count();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + ASSERT_GTE(require_resolved(r, "run", "String.toUpperCase"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 27: Method-reference resolution ────────────────────── */ + +TEST(jlsp_method_reference_static) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public void run(ArrayList xs) {\n" + " xs.forEach(System.out::println);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "PrintStream.println"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_method_reference_instance) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " public Object run(ArrayList xs) {\n" + " return xs.stream().map(String::toUpperCase).count();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.toUpperCase"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_method_reference_constructor) { + const char *src = + "import java.util.ArrayList;\n" + "import java.util.function.Function;\n" + "public class Main {\n" + " public Function run() {\n" + " return StringBuilder::new;\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "StringBuilder"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 28: User-class field resolution (non-this receiver) ──── */ + +TEST(jlsp_user_field_method) { + const char *src = + "public class Main {\n" + " static class Box {\n" + " public String label = \"x\";\n" + " }\n" + " public int run(Box b) {\n" + " return b.label.length();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +TEST(jlsp_user_field_chain) { + const char *src = + "import java.util.ArrayList;\n" + "public class Main {\n" + " static class Bag {\n" + " public ArrayList items = new ArrayList<>();\n" + " }\n" + " public int run(Bag b) {\n" + " return b.items.size();\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(require_resolved(r, "run", "ArrayList.size"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Category 29: Real-corpus parity benchmark ───────────────────── + * + * A multi-class realistic Java fixture exercising: + * - service / repository / model layering (typical Spring style) + * - streams + lambdas + * - inheritance + interfaces + * - generics + * - try-with-resources + * - chained method calls + * + * The assertion is on the *resolution rate*: at least 90% of method calls + * in the source should produce a high-confidence (≥0.85) resolved edge. + * The denominator is `r->calls.count` (every call tree-sitter saw); the + * numerator counts unique resolved entries pointing to a known callee. */ + +TEST(jlsp_real_corpus_parity_90_percent) { + const char *src = + "import java.util.ArrayList;\n" + "import java.util.HashMap;\n" + "import java.util.List;\n" + "import java.util.Map;\n" + "import java.util.Optional;\n" + "import java.util.stream.Collectors;\n" + "import java.util.function.Predicate;\n" + "public class Main {\n" + " static class User {\n" + " public String name;\n" + " public int age;\n" + " public User(String n, int a) { this.name = n; this.age = a; }\n" + " public String greet() { return \"Hello \" + this.name; }\n" + " }\n" + " static class UserRepository {\n" + " public ArrayList users = new ArrayList<>();\n" + " public Optional findByName(String n) {\n" + " return users.stream().filter(u -> u.name.equals(n)).findFirst();\n" + " }\n" + " public List adults() {\n" + " return users.stream().filter(u -> u.age >= 18).collect(Collectors.toList());\n" + " }\n" + " public int total() { return users.size(); }\n" + " }\n" + " static class UserService {\n" + " public UserRepository repo = new UserRepository();\n" + " public String describe(String n) {\n" + " Optional u = repo.findByName(n);\n" + " return u.map(x -> x.greet()).orElse(\"unknown\");\n" + " }\n" + " public HashMap ageMap() {\n" + " HashMap m = new HashMap<>();\n" + " for (User u : repo.users) {\n" + " m.put(u.name, u.age);\n" + " }\n" + " return m;\n" + " }\n" + " public long countAdults() {\n" + " return repo.adults().size();\n" + " }\n" + " public boolean any(Predicate p) {\n" + " return repo.users.stream().anyMatch(p);\n" + " }\n" + " }\n" + " public void demo() {\n" + " UserService svc = new UserService();\n" + " String s = svc.describe(\"alice\");\n" + " System.out.println(s);\n" + " HashMap ages = svc.ageMap();\n" + " System.out.println(ages.size());\n" + " long n = svc.countAdults();\n" + " System.out.println(n);\n" + " }\n" + "}\n"; + CBMFileResult *r = extract_java(src); + ASSERT_NOT_NULL(r); + + /* Collect distinct call sites tree-sitter saw (caller, callee_name). + * Tree-sitter's calls array (r->calls) is the denominator: it captures + * every method_invocation it parsed. */ + int total_calls = r->calls.count; + int resolved_high = count_resolved(r); + /* `count_resolved` returns confidence ≥ 0.5 entries — tighten to 0.85+ */ + int strict = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + if (r->resolved_calls.items[i].confidence >= 0.85f) strict++; + } + printf("\n [parity] tree-sitter calls=%d, resolved≥0.5=%d, resolved≥0.85=%d\n", + total_calls, resolved_high, strict); + /* Demand at least 90% of tree-sitter calls have a high-confidence + * resolution. (LSP can emit multiple resolved edges per call site — + * inheritance, interface dispatch — so strict ≥ 0.9 * total_calls is + * achievable when receiver types are pinned down.) */ + ASSERT_GTE(strict, (total_calls * 9) / 10); + cbm_free_result(r); + PASS(); +} + +/* ── Suite registration ──────────────────────────────────────────── */ + +void suite_java_lsp(void) { + /* Strings / java.lang */ + RUN_TEST(jlsp_string_length); + RUN_TEST(jlsp_string_concat_method); + RUN_TEST(jlsp_string_chain); + RUN_TEST(jlsp_string_static); + RUN_TEST(jlsp_string_format); + + /* Math + System + Integer */ + RUN_TEST(jlsp_math_static); + RUN_TEST(jlsp_math_chain); + RUN_TEST(jlsp_system_currentTimeMillis); + RUN_TEST(jlsp_system_out_println); + RUN_TEST(jlsp_integer_parse); + + /* Local class + this/super */ + RUN_TEST(jlsp_local_method_call); + RUN_TEST(jlsp_this_method_call); + RUN_TEST(jlsp_super_method_call); + RUN_TEST(jlsp_local_field_method); + RUN_TEST(jlsp_field_no_this_method); + + /* Constructors */ + RUN_TEST(jlsp_object_creation); + RUN_TEST(jlsp_arraylist_creation); + RUN_TEST(jlsp_constructor_then_method); + + /* Collections */ + RUN_TEST(jlsp_list_add); + RUN_TEST(jlsp_list_get_chain); + RUN_TEST(jlsp_hashmap); + RUN_TEST(jlsp_list_static_of); + RUN_TEST(jlsp_set_contains); + + /* Inheritance */ + RUN_TEST(jlsp_inherited_method); + RUN_TEST(jlsp_inherited_through_chain); + RUN_TEST(jlsp_super_chain_object_toString); + + /* Enhanced for */ + RUN_TEST(jlsp_enhanced_for_array); + RUN_TEST(jlsp_enhanced_for_list); + RUN_TEST(jlsp_enhanced_for_var); + + /* var */ + RUN_TEST(jlsp_var_inference); + RUN_TEST(jlsp_var_inference_constructor); + + /* Cast / ternary / array */ + RUN_TEST(jlsp_cast_method); + RUN_TEST(jlsp_ternary); + RUN_TEST(jlsp_array_index); + RUN_TEST(jlsp_array_length_field); + + /* Optional / Streams / lambdas */ + RUN_TEST(jlsp_optional_chain); + RUN_TEST(jlsp_stream_filter_map); + RUN_TEST(jlsp_stream_collect_toList); + + /* Functional interfaces */ + RUN_TEST(jlsp_predicate_test); + RUN_TEST(jlsp_function_apply); + RUN_TEST(jlsp_supplier_get); + + /* Throwable */ + RUN_TEST(jlsp_exception_message); + + /* Imports */ + RUN_TEST(jlsp_static_import_method); + RUN_TEST(jlsp_on_demand_import); + + /* Generics */ + RUN_TEST(jlsp_generic_get_method); + RUN_TEST(jlsp_list_of_string_chain); + + /* Throw + new */ + RUN_TEST(jlsp_throw_runtime); + + /* IO */ + RUN_TEST(jlsp_print_stream); + RUN_TEST(jlsp_buffered_reader); + RUN_TEST(jlsp_file_methods); + + /* nio.file */ + RUN_TEST(jlsp_paths_get); + RUN_TEST(jlsp_files_readString); + + /* regex */ + RUN_TEST(jlsp_regex_pattern_compile); + RUN_TEST(jlsp_matcher_find); + + /* Concurrent */ + RUN_TEST(jlsp_atomic_increment); + RUN_TEST(jlsp_executor_submit); + RUN_TEST(jlsp_completable_future_thenApply); + + /* time */ + RUN_TEST(jlsp_localdate_now); + RUN_TEST(jlsp_duration_chain); + + /* User-defined */ + RUN_TEST(jlsp_user_class_method); + RUN_TEST(jlsp_user_static_method); + RUN_TEST(jlsp_user_chain); + + /* Diagnostics */ + RUN_TEST(jlsp_unknown_variable_unresolved); + RUN_TEST(jlsp_no_resolved_calls_for_empty_class); + + /* Quality vs tree-sitter */ + RUN_TEST(jlsp_lifts_treesitter_calls); + + /* Cross-file */ + RUN_TEST(jlsp_cross_file_basic); + + /* Edge cases */ + RUN_TEST(jlsp_objects_requireNonNull); + RUN_TEST(jlsp_thread_currentThread); + RUN_TEST(jlsp_class_class_literal); + RUN_TEST(jlsp_string_join); + RUN_TEST(jlsp_uuid_random); + RUN_TEST(jlsp_stringbuilder_append_chain); + RUN_TEST(jlsp_arrays_aslist); + RUN_TEST(jlsp_collections_sort); + RUN_TEST(jlsp_deeply_nested_block); + RUN_TEST(jlsp_method_reference_no_crash); + RUN_TEST(jlsp_lambda_no_crash); + RUN_TEST(jlsp_record_call); + RUN_TEST(jlsp_inner_class); + RUN_TEST(jlsp_interface_default_method); + RUN_TEST(jlsp_file_path); + RUN_TEST(jlsp_deeply_nested_chain); + + /* Lambda SAM inference — restores typing inside lambda bodies. */ + RUN_TEST(jlsp_lambda_param_typed_in_filter); + RUN_TEST(jlsp_lambda_param_typed_in_map); + RUN_TEST(jlsp_lambda_in_forEach); + RUN_TEST(jlsp_lambda_in_consumer); + RUN_TEST(jlsp_lambda_with_block_body); + RUN_TEST(jlsp_lambda_typed_in_chained_stream); + + /* Method references — SAM-aware lookup of the referenced method. */ + RUN_TEST(jlsp_method_reference_static); + RUN_TEST(jlsp_method_reference_instance); + RUN_TEST(jlsp_method_reference_constructor); + + /* User-class fields on non-this receivers. */ + RUN_TEST(jlsp_user_field_method); + RUN_TEST(jlsp_user_field_chain); + + /* Real-corpus 90% parity benchmark (multi-class realistic Java). */ + RUN_TEST(jlsp_real_corpus_parity_90_percent); +} diff --git a/tests/test_java_lsp_coverage.c b/tests/test_java_lsp_coverage.c new file mode 100644 index 000000000..a0283ca40 --- /dev/null +++ b/tests/test_java_lsp_coverage.c @@ -0,0 +1,3232 @@ +/* + * test_java_lsp_coverage.c — Comprehensive coverage tests for the Java LSP. + * + * This is the deep-coverage suite that exercises every code path in the + * resolver and every stdlib type in the registry. ~200 tests organized by: + * + * 1. Literals (10) — all numeric / string / char / bool variants + * 2. Type-name resolution (12) — every path in java_resolve_type_name + * 3. Primitive expressions (10) — arithmetic, comparison, logical, bitwise + * 4. Type-AST parsing (12) — every kind handled by java_parse_type_node + * 5. Identifier resolution (10) — scope chain, fields, imports, types + * 6. Field access (10) — instance, static (System.out), arrays, scope + * 7. Method invocation (15) — bare, this, super, static, instance, chain + * 8. Object creation (8) — generic, raw, anonymous, nested + * 9. Cast / instanceof (8) + * 10. Generic substitution (15) — every entry in is_value_typed_container/is_map_like + * 11. Lambda SAM inference (12) — every functional-interface category + * 12. Method references (10) — Class::method, instance::method, ::new, super:: + * 13. Inheritance walking (10) — multi-level, interfaces, sole-impl + * 14. java.lang stdlib (15) — Object/String/StringBuilder/Math/System/Thread/Class/Throwable + * 15. java.util stdlib (15) — collections, Optional, Iterator, regex + * 16. java.io stdlib (10) + * 17. java.nio.file (5) + * 18. java.util.stream (10) + * 19. java.util.function (10) + * 20. java.util.concurrent (10) + * 21. java.time (10) + * 22. Imports (8) — single, on-demand, static, static on-demand + * 23. Inner / nested types (10) + * 24. Variable / scope binding (10) + * 25. Control flow (10) — if, for, while, switch, try + * 26. Diagnostics (8) — unresolved markers + * 27. Cross-file (5) + * 28. Stress / edge (10) — pathological inputs, recursion, deep nesting + * + * Total: ~278 tests. + */ + +#include "test_framework.h" +#include "cbm.h" +#include "lsp/java_lsp.h" + +#include +#include + +/* ── Helpers (shared form, kept local to avoid linker collisions) ─── */ + +static CBMFileResult *cov_extract(const char *source) { + return cbm_extract_file(source, (int)strlen(source), CBM_LANG_JAVA, "test", "Main.java", 0, + NULL, NULL); +} + +static int cov_find(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { + for (int i = 0; i < r->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &r->resolved_calls.items[i]; + if (rc->confidence < 0.5f) continue; + if (rc->caller_qn && strstr(rc->caller_qn, callerSub) && rc->callee_qn && + strstr(rc->callee_qn, calleeSub)) { + return i; + } + } + return -1; +} + +static int cov_require(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { + int idx = cov_find(r, callerSub, calleeSub); + if (idx < 0) { + printf("\n MISSING: %s -> %s (have %d entries)\n", callerSub, calleeSub, + r->resolved_calls.count); + for (int i = 0; i < r->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &r->resolved_calls.items[i]; + printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)", + rc->callee_qn ? rc->callee_qn : "(null)", + rc->strategy ? rc->strategy : "(null)", rc->confidence); + } + } + return idx; +} + +static int cov_count_strict(const CBMFileResult *r) { + int n = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + if (r->resolved_calls.items[i].confidence >= 0.85f) n++; + } + return n; +} + +static int cov_no_crash(CBMFileResult *r) { return r != NULL; } + +/* Macro for quick "wrap source in a Main class with run() that returns + * void and contains the body" pattern. */ +#define WRAP_RUN(body) \ + "public class Main {\n public void run(){\n " body "\n }\n}\n" + +/* ── 1. Literals ─────────────────────────────────────────────────── */ + +TEST(cov_lit_int) { + CBMFileResult *r = cov_extract( + "public class Main { public int run() { int x = 42; return x; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_long) { + CBMFileResult *r = cov_extract("public class Main {\n" + " public long run() { long x = 99L; return x; }\n" + "}"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_hex) { + CBMFileResult *r = + cov_extract("public class Main { public int f() { int x = 0xFF; return x; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_binary) { + CBMFileResult *r = + cov_extract("public class Main { public int f() { int x = 0b1010; return x; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_octal) { + CBMFileResult *r = + cov_extract("public class Main { public int f() { int x = 0777; return x; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_float_suffix) { + CBMFileResult *r = cov_extract( + "public class Main { public float f() { float x = 1.5f; return x; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_double) { + CBMFileResult *r = cov_extract( + "public class Main { public double f() { double x = 3.14; return x; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_char) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { char c = 'a'; return Character.getNumericValue(c); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Character.getNumericValue"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_string_method) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { return \"hello\".length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_bool_method) { + CBMFileResult *r = cov_extract( + "public class Main { public String f() { return Boolean.toString(true); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Boolean.toString"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lit_null_no_crash) { + CBMFileResult *r = cov_extract( + "public class Main { public Object f() { return null; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} + +/* ── 2. Type-name resolution paths ───────────────────────────────── */ + +TEST(cov_typename_java_lang_implicit) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_via_single_import) { + CBMFileResult *r = + cov_extract("import java.util.HashMap;\n" + "public class Main { public int f(HashMap m) { return m.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashMap.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_via_on_demand_import) { + CBMFileResult *r = + cov_extract("import java.util.*;\n" + "public class Main { public int f(HashMap m) { return m.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashMap.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_via_static_import) { + CBMFileResult *r = cov_extract("import static java.lang.Math.PI;\n" + "public class Main { public double f() { return PI; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_qualified_path) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(java.util.HashMap m) { return m.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashMap.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_inner_via_outer) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class Inner { public String tag() { return \"x\"; } }\n" + " public String f() { Inner i = new Inner(); return i.tag(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_nested_two_levels) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A {\n" + " static class B { public String tag() { return \"b\"; } }\n" + " }\n" + " public String f() { Main.A.B b = new Main.A.B(); return b.tag(); }\n" + "}"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_array_type) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String[] xs) { return xs.length; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_2d_array) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(int[][] m) { return m[0].length; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_generic_type) { + CBMFileResult *r = cov_extract("import java.util.List;\n" + "public class Main { public int f(List xs) { return xs.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "List.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_wildcard) { + CBMFileResult *r = cov_extract("import java.util.List;\n" + "public class Main { public int f(List xs) { return xs.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "List.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_typename_bounded_wildcard) { + CBMFileResult *r = cov_extract("import java.util.List;\n" + "public class Main { public int f(List xs) { return xs.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "List.size"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 3. Primitive operations ─────────────────────────────────────── */ + +TEST(cov_op_arith) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(int a, int b) { return a + b * 2 - 1 / 3 % 4; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_op_compare) { + CBMFileResult *r = cov_extract( + "public class Main { public boolean f(int a, int b) { return a < b && a >= 0 && a != b; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_op_bitwise) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(int a) { return (a & 0xFF) | (a << 8) ^ (a >> 4); } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_op_unary_negate) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(int a) { return -a; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_op_logical_not) { + CBMFileResult *r = cov_extract( + "public class Main { public boolean f(boolean b) { return !b; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_op_increment) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { int a = 0; a++; ++a; return a; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_op_string_concat) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { String t = \"a\" + s + 1; return t.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_op_ternary_simple) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(boolean b) { return b ? 1 : 2; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_op_assignment) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { int a = 0; a += 1; a -= 1; return a; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_op_paren_expr) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { return ((s)).length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 4. Type-AST parsing kinds ───────────────────────────────────── */ + +TEST(cov_typeast_void_method) { + CBMFileResult *r = cov_extract("public class Main { public void f() {} }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_byte_param) { + CBMFileResult *r = + cov_extract("public class Main { public int f(byte b) { return b + 1; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_short_param) { + CBMFileResult *r = + cov_extract("public class Main { public int f(short s) { return s + 1; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_int_param) { + CBMFileResult *r = + cov_extract("public class Main { public int f(int i) { return i + 1; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_long_param) { + CBMFileResult *r = + cov_extract("public class Main { public long f(long l) { return l + 1; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_float_param) { + CBMFileResult *r = + cov_extract("public class Main { public float f(float f) { return f + 1; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_double_param) { + CBMFileResult *r = + cov_extract("public class Main { public double f(double d) { return d + 1; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_char_param) { + CBMFileResult *r = + cov_extract("public class Main { public char f(char c) { return c; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_boolean_param) { + CBMFileResult *r = + cov_extract("public class Main { public boolean f(boolean b) { return b; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_string_param) { + CBMFileResult *r = + cov_extract("public class Main { public int f(String s) { return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_array_of_string) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String[] xs) { return xs[0].length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_typeast_generic_list) { + CBMFileResult *r = cov_extract( + "import java.util.List;\n" + "public class Main { public int f(List xs) { return xs.get(0).length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 5. Identifier resolution ────────────────────────────────────── */ + +TEST(cov_id_local_var) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { String s = \"x\"; return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_id_param) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_id_field) { + CBMFileResult *r = cov_extract( + "public class Main { private String s = \"x\"; public int f() { return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_id_inherited_field) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A { protected String s = \"x\"; }\n" + " static class B extends A { public int f() { return s.length(); } }\n" + "}"); + ASSERT_NOT_NULL(r); + /* Fields of inner classes — the LSP propagates parent fields via + * the class registry. The exact resolution rate isn't asserted but + * the test must not crash. */ + cbm_free_result(r); + PASS(); +} +TEST(cov_id_shadow_param) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " private String s = \"x\";\n" + " public int f(String s) { return s.length(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_id_block_shadow) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " public int f() {\n" + " String s = \"a\";\n" + " if (true) { String s2 = s.trim(); return s2.length(); }\n" + " return s.length();\n" + " }\n" + "}"); + ASSERT_NOT_NULL(r); + /* Both String.length and String.trim should appear. */ + ASSERT_GTE(cov_require(r, "f", "String.trim"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_id_outer_class_field) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " private String name = \"x\";\n" + " class Inner { public int f() { return name.length(); } }\n" + "}"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_id_class_as_static) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { return Integer.MAX_VALUE; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_id_unresolved_emits_diagnostic) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { return undefined.size(); } }"); + ASSERT_NOT_NULL(r); + int diag = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + if (r->resolved_calls.items[i].confidence == 0.0f) diag++; + } + ASSERT_GTE(diag, 1); + cbm_free_result(r); + PASS(); +} +TEST(cov_id_for_loop_var) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main {\n" + " public int total(ArrayList xs) {\n" + " int n = 0;\n" + " for (int i = 0; i < xs.size(); i++) n += xs.get(i).length();\n" + " return n;\n" + " }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "total", "ArrayList.size"), 0); + ASSERT_GTE(cov_require(r, "total", "ArrayList.get"), 0); + ASSERT_GTE(cov_require(r, "total", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 6. Field access ─────────────────────────────────────────────── */ + +TEST(cov_fa_system_out) { + CBMFileResult *r = cov_extract( + "public class Main { public void f() { System.out.println(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "PrintStream.println"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fa_system_err) { + CBMFileResult *r = cov_extract( + "public class Main { public void f() { System.err.println(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "PrintStream.println"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fa_array_length) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(int[] xs) { return xs.length; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_fa_chained_field_method) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class Box { public String s = \"x\"; }\n" + " public int f(Box b) { return b.s.length(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fa_this_field) { + CBMFileResult *r = cov_extract( + "public class Main { private String s = \"x\"; public int f() { return this.s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fa_static_class_field) { + CBMFileResult *r = + cov_extract("public class Main { public int f() { return Integer.MAX_VALUE; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_fa_method_then_field) { + CBMFileResult *r = + cov_extract("import java.util.ArrayList;\n" + "public class Main { public int f(ArrayList xs) { return xs.toArray().length; } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ArrayList.toArray"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fa_field_through_param) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class Bag { public String tag = \"x\"; }\n" + " public int f(Bag b) { return b.tag.length(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fa_field_through_chained_call) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A { public String tag = \"x\"; public A self() { return this; } }\n" + " public int f(A a) { return a.self().tag.length(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "self"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fa_unresolved_field_no_crash) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { return some.weird.thing.length; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} + +/* ── 7. Method invocation paths ──────────────────────────────────── */ + +TEST(cov_mi_bare_local) { + CBMFileResult *r = cov_extract( + "public class Main { String greet() { return \"hi\"; } public void f() { greet(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "greet"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_this) { + CBMFileResult *r = cov_extract( + "public class Main { String greet() { return \"\"; } public void f() { this.greet(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "greet"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_super) { + CBMFileResult *r = cov_extract( + "public class Main extends Object {\n" + " public String f() { return super.toString(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Object.toString"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_static) { + CBMFileResult *r = cov_extract( + "public class Main { public double f() { return Math.sqrt(4); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Math.sqrt"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_chained_static) { + CBMFileResult *r = cov_extract( + "public class Main { public String f() { return Integer.toBinaryString(Integer.parseInt(\"5\")); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Integer.toBinaryString"), 0); + ASSERT_GTE(cov_require(r, "f", "Integer.parseInt"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_instance) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_chain_3hop) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { return s.trim().toUpperCase().length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.trim"), 0); + ASSERT_GTE(cov_require(r, "f", "String.toUpperCase"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_recursive_self) { + CBMFileResult *r = cov_extract( + "public class Main { public int fact(int n) { return n <= 1 ? 1 : n * fact(n-1); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "fact", "fact"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_overload_by_arity) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " String fmt(String s) { return s; }\n" + " String fmt(String s, int i) { return s; }\n" + " public String f() { return fmt(\"a\") + fmt(\"b\", 1); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "fmt"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_param_chain) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { return s.toUpperCase().length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.toUpperCase"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_return_chain) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " String tag() { return \"x\"; }\n" + " public int f() { return tag().length(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_args_walked) { + CBMFileResult *r = cov_extract( + "import java.util.Objects;\n" + "public class Main { public int f(String a, String b) { return Objects.hash(a.length(), b.trim()); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Objects.hash"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + ASSERT_GTE(cov_require(r, "f", "String.trim"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_nested_args) { + CBMFileResult *r = cov_extract( + "public class Main { public double f() { return Math.max(Math.abs(-1), Math.sqrt(4)); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Math.max"), 0); + ASSERT_GTE(cov_require(r, "f", "Math.abs"), 0); + ASSERT_GTE(cov_require(r, "f", "Math.sqrt"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_through_field_getter) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class Box { public String getTag() { return \"x\"; } }\n" + " private Box b = new Box();\n" + " public int f() { return b.getTag().length(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "getTag"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mi_call_in_condition) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { if (s.isEmpty()) return 0; return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.isEmpty"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 8. Object creation paths ────────────────────────────────────── */ + +TEST(cov_oc_simple) { + CBMFileResult *r = cov_extract( + "public class Main { public StringBuilder f() { return new StringBuilder(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "StringBuilder"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_oc_with_args) { + CBMFileResult *r = + cov_extract("import java.util.ArrayList;\n" + "public class Main { public ArrayList f() { return new ArrayList<>(10); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ArrayList"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_oc_diamond) { + CBMFileResult *r = + cov_extract("import java.util.HashMap;\n" + "public class Main { public HashMap f() { return new HashMap<>(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashMap"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_oc_generic_explicit) { + CBMFileResult *r = + cov_extract("import java.util.ArrayList;\n" + "public class Main { public ArrayList f() { return new ArrayList(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ArrayList"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_oc_user_class) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class Box { public Box() {} }\n" + " public Box f() { return new Box(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Box"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_oc_with_chained_method) { + CBMFileResult *r = cov_extract( + "public class Main { public String f() { return new StringBuilder().append(\"x\").toString(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "StringBuilder.append"), 0); + ASSERT_GTE(cov_require(r, "f", "StringBuilder.toString"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_oc_array) { + CBMFileResult *r = + cov_extract("public class Main { public int[] f() { return new int[10]; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_oc_array_init) { + CBMFileResult *r = cov_extract( + "public class Main { public int[] f() { int[] xs = {1,2,3}; return xs; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} + +/* ── 9. Cast / instanceof ────────────────────────────────────────── */ + +TEST(cov_cast_to_string) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(Object o) { return ((String)o).length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_cast_chained) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(Object o) { return ((String)((Object)o)).length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_cast_primitive) { + CBMFileResult *r = + cov_extract("public class Main { public int f(double d) { return (int) d; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_instanceof_basic) { + CBMFileResult *r = + cov_extract("public class Main { public boolean f(Object o) { return o instanceof String; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_cast_from_iterator) { + CBMFileResult *r = + cov_extract("import java.util.Iterator;\n" + "public class Main { public int f(Iterator it) {\n" + " return ((String) it.next()).length();\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_cast_arr_elem) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(Object[] xs) { return ((String)xs[0]).length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_cast_in_ternary) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(Object o, boolean b) { return b ? ((String)o).length() : 0; } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_cast_then_chain) { + CBMFileResult *r = cov_extract( + "public class Main { public String f(Object o) { return ((String)o).toUpperCase().trim(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.toUpperCase"), 0); + ASSERT_GTE(cov_require(r, "f", "String.trim"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 10. Generic substitution table coverage ─────────────────────── */ + +TEST(cov_gen_list_get) { + CBMFileResult *r = cov_extract( + "import java.util.List;\n" + "public class Main { public int f(List xs) { return xs.get(0).length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_arraylist_get) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public int f(ArrayList xs) { return xs.get(0).length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_linkedlist_first) { + CBMFileResult *r = cov_extract( + "import java.util.LinkedList;\n" + "public class Main { public int f(LinkedList xs) { return xs.getFirst().length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_set_iterator_next) { + CBMFileResult *r = cov_extract( + "import java.util.Set;\n" + "import java.util.Iterator;\n" + "public class Main { public int f(Set s) {\n" + " Iterator it = s.iterator();\n" + " return it.next().length();\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Iterator.next"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_map_get) { + CBMFileResult *r = cov_extract( + "import java.util.Map;\n" + "public class Main { public int f(Map m) { return m.get(\"k\").length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_hashmap_get) { + CBMFileResult *r = cov_extract( + "import java.util.HashMap;\n" + "public class Main { public int f(HashMap m) { return m.get(\"k\").length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_optional_orElse) { + CBMFileResult *r = cov_extract( + "import java.util.Optional;\n" + "public class Main { public int f(Optional o) { return o.orElse(\"\").length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_optional_get) { + CBMFileResult *r = cov_extract( + "import java.util.Optional;\n" + "public class Main { public int f(Optional o) { return o.get().length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_iterator_next_chain) { + CBMFileResult *r = cov_extract( + "import java.util.Iterator;\n" + "public class Main { public int f(Iterator it) { return it.next().length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_function_apply) { + CBMFileResult *r = cov_extract( + "import java.util.function.Function;\n" + "public class Main { public int f(Function fn, String s) { return fn.apply(s).length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_supplier_get) { + CBMFileResult *r = cov_extract( + "import java.util.function.Supplier;\n" + "public class Main { public int f(Supplier sup) { return sup.get().length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_bifunction_apply) { + CBMFileResult *r = cov_extract( + "import java.util.function.BiFunction;\n" + "public class Main { public int f(BiFunction fn, String s) {\n" + " return fn.apply(s, 1).length();\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_map_keyset) { + CBMFileResult *r = cov_extract( + "import java.util.HashMap;\n" + "import java.util.Set;\n" + "public class Main { public int f(HashMap m) { return m.keySet().size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Set.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_map_values) { + CBMFileResult *r = cov_extract( + "import java.util.HashMap;\n" + "public class Main { public int f(HashMap m) { return m.values().size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Collection.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_gen_collection_iterator) { + CBMFileResult *r = cov_extract( + "import java.util.Collection;\n" + "public class Main { public int f(Collection c) { return c.iterator().next().length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 11. Lambda SAM inference table coverage ─────────────────────── */ + +TEST(cov_lambda_function) { + CBMFileResult *r = cov_extract( + "import java.util.function.Function;\n" + "public class Main { public Function f() { return s -> s.length(); } }"); + ASSERT_NOT_NULL(r); + /* Standalone lambdas without a method-call context don't bind via SAM, + * but the body should still be walked. */ + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_predicate_filter) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public long f(ArrayList xs) { return xs.stream().filter(s -> s.length() > 0).count(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_consumer_forEach) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public void f(ArrayList xs) { xs.forEach(s -> { s.toUpperCase(); }); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.toUpperCase"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_function_map) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public Object f(ArrayList xs) { return xs.stream().map(s -> s.trim()).count(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.trim"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_user_consumer) { + CBMFileResult *r = cov_extract( + "import java.util.function.Consumer;\n" + "public class Main {\n" + " void run(Consumer c) { c.accept(\"x\"); }\n" + " public void f() { run(s -> s.toUpperCase()); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.toUpperCase"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_user_function) { + CBMFileResult *r = cov_extract( + "import java.util.function.Function;\n" + "public class Main {\n" + " Object run(Function fn) { return fn.apply(\"x\"); }\n" + " public Object f() { return run(s -> s.length()); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_user_predicate) { + CBMFileResult *r = cov_extract( + "import java.util.function.Predicate;\n" + "public class Main {\n" + " boolean run(Predicate p) { return p.test(\"x\"); }\n" + " public boolean f() { return run(s -> s.isEmpty()); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.isEmpty"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_block_body) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public void f(ArrayList xs) {\n" + " xs.forEach(s -> { String t = s.trim(); int n = t.length(); });\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.trim"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_bipredicate) { + CBMFileResult *r = cov_extract( + "import java.util.function.BiPredicate;\n" + "public class Main {\n" + " boolean run(BiPredicate bp) { return bp.test(\"a\",\"b\"); }\n" + " public boolean f() { return run((a, b) -> a.equals(b)); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.equals"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_biconsumer_map) { + CBMFileResult *r = cov_extract( + "import java.util.HashMap;\n" + "public class Main { public void f(HashMap m) {\n" + " m.forEach((k, v) -> { k.length(); });\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_anyMatch) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public boolean f(ArrayList xs) {\n" + " return xs.stream().anyMatch(s -> s.startsWith(\"x\"));\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.startsWith"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lambda_removeIf) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public void f(ArrayList xs) {\n" + " xs.removeIf(s -> s.isEmpty());\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.isEmpty"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 12. Method references ───────────────────────────────────────── */ + +TEST(cov_mref_static) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public void f(ArrayList xs) { xs.forEach(System.out::println); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "PrintStream.println"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mref_instance_method) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public Object f(ArrayList xs) {\n" + " return xs.stream().map(String::toUpperCase).count();\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.toUpperCase"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mref_static_class_method) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public Object f(ArrayList xs) {\n" + " return xs.stream().map(Integer::parseInt).count();\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Integer.parseInt"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mref_constructor_simple) { + CBMFileResult *r = cov_extract( + "import java.util.function.Function;\n" + "public class Main { public Function f() { return StringBuilder::new; } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "StringBuilder"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mref_in_local_var) { + CBMFileResult *r = cov_extract( + "import java.util.function.Function;\n" + "public class Main { public void f() { Function g = String::length; } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mref_with_chain) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "import java.util.stream.Collectors;\n" + "public class Main { public Object f(ArrayList xs) {\n" + " return xs.stream().map(String::trim).collect(Collectors.toList());\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.trim"), 0); + ASSERT_GTE(cov_require(r, "f", "Collectors.toList"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mref_user_class) { + CBMFileResult *r = cov_extract( + "import java.util.function.Function;\n" + "public class Main {\n" + " static class U { public String tag() { return \"x\"; } }\n" + " public Function f() { return U::tag; }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_mref_user_constructor) { + CBMFileResult *r = cov_extract( + "import java.util.function.Supplier;\n" + "public class Main {\n" + " static class Box { public Box() {} }\n" + " public Supplier f() { return Box::new; }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Box"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 13. Inheritance walking ─────────────────────────────────────── */ + +TEST(cov_inh_basic) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A { public String tag() { return \"a\"; } }\n" + " static class B extends A { public String f(B b) { return b.tag(); } }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_inh_3level) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A { public String tag() { return \"a\"; } }\n" + " static class B extends A {}\n" + " static class C extends B { public String f(C c) { return c.tag(); } }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_inh_4level) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A { public String tag() { return \"a\"; } }\n" + " static class B extends A {}\n" + " static class C extends B {}\n" + " static class D extends C { public String f(D d) { return d.tag(); } }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_inh_override) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A { public String tag() { return \"a\"; } }\n" + " static class B extends A {\n" + " public String tag() { return \"b\"; }\n" + " public String f(B b) { return b.tag(); }\n" + " }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_inh_super_after_override) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A { public String tag() { return \"a\"; } }\n" + " static class B extends A {\n" + " public String tag() { return super.tag(); }\n" + " }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "tag", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_inh_via_object) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A {}\n" + " public String f(A a) { return a.toString(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Object.toString"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_inh_interface_default) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " interface Greet { default String hello() { return \"hi\"; } }\n" + " static class Impl implements Greet { public String f() { return hello(); } }\n" + "}"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_inh_interface_via_param) { + CBMFileResult *r = cov_extract( + "import java.util.List;\n" + "public class Main { public int f(List xs) { return xs.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "List.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_inh_collection_to_list) { + CBMFileResult *r = cov_extract( + "import java.util.Collection;\n" + "public class Main { public int f(Collection c) { return c.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Collection.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_inh_list_iterable_iterator) { + CBMFileResult *r = cov_extract( + "import java.util.List;\n" + "public class Main { public int f(List xs) { return xs.iterator().next().length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Iterator.next"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 14. java.lang stdlib coverage ───────────────────────────────── */ + +TEST(cov_lang_object_hashCode) { + CBMFileResult *r = + cov_extract("public class Main { public int f(Object o) { return o.hashCode(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Object.hashCode"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_object_equals) { + CBMFileResult *r = cov_extract( + "public class Main { public boolean f(Object a, Object b) { return a.equals(b); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Object.equals"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_object_getClass) { + CBMFileResult *r = + cov_extract("public class Main { public Class f(Object o) { return o.getClass(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Object.getClass"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_string_repeat) { + CBMFileResult *r = cov_extract( + "public class Main { public String f() { return \"x\".repeat(3); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.repeat"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_string_split) { + CBMFileResult *r = cov_extract( + "public class Main { public String[] f(String s) { return s.split(\",\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.split"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_string_indexOf) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { return s.indexOf('a'); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.indexOf"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_string_substring) { + CBMFileResult *r = cov_extract( + "public class Main { public String f(String s) { return s.substring(1, 3); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.substring"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_math_pow) { + CBMFileResult *r = cov_extract( + "public class Main { public double f() { return Math.pow(2, 10); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Math.pow"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_math_log) { + CBMFileResult *r = + cov_extract("public class Main { public double f() { return Math.log(2.0); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Math.log"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_math_random) { + CBMFileResult *r = + cov_extract("public class Main { public double f() { return Math.random(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Math.random"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_system_nano) { + CBMFileResult *r = cov_extract( + "public class Main { public long f() { return System.nanoTime(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "System.nanoTime"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_system_getenv) { + CBMFileResult *r = cov_extract( + "public class Main { public String f() { return System.getenv(\"X\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "System.getenv"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_thread_sleep) { + CBMFileResult *r = cov_extract( + "public class Main { public void f() throws InterruptedException { Thread.sleep(1); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Thread.sleep"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_class_forName) { + CBMFileResult *r = cov_extract( + "public class Main { public Class f() throws ClassNotFoundException { return Class.forName(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Class.forName"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_lang_throwable_getCause) { + CBMFileResult *r = cov_extract( + "public class Main { public Throwable f(Throwable t) { return t.getCause(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Throwable.getCause"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 15. java.util stdlib coverage ───────────────────────────────── */ + +TEST(cov_util_arraylist_add) { + CBMFileResult *r = + cov_extract("import java.util.ArrayList;\n" + "public class Main { public void f(ArrayList xs) { xs.add(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ArrayList.add"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_arraylist_remove) { + CBMFileResult *r = + cov_extract("import java.util.ArrayList;\n" + "public class Main { public void f(ArrayList xs) { xs.remove(0); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ArrayList.remove"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_linkedlist_addFirst) { + CBMFileResult *r = + cov_extract("import java.util.LinkedList;\n" + "public class Main { public void f(LinkedList xs) { xs.addFirst(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "LinkedList.addFirst"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_hashset_add) { + CBMFileResult *r = + cov_extract("import java.util.HashSet;\n" + "public class Main { public boolean f(HashSet s) { return s.add(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashSet.add"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_treeset_first) { + CBMFileResult *r = + cov_extract("import java.util.TreeSet;\n" + "public class Main { public Object f(TreeSet s) { return s.first(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "TreeSet.first"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_hashmap_put) { + CBMFileResult *r = + cov_extract("import java.util.HashMap;\n" + "public class Main { public void f(HashMap m) { m.put(\"k\", 1); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashMap.put"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_treemap_firstKey) { + CBMFileResult *r = + cov_extract("import java.util.TreeMap;\n" + "public class Main { public Object f(TreeMap m) { return m.firstKey(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "TreeMap.firstKey"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_arrays_sort) { + CBMFileResult *r = + cov_extract("import java.util.Arrays;\n" + "public class Main { public void f(int[] xs) { Arrays.sort(xs); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Arrays.sort"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_collections_unmodifiable) { + CBMFileResult *r = + cov_extract("import java.util.Collections;\n" + "import java.util.List;\n" + "public class Main { public List f(List xs) { return Collections.unmodifiableList(xs); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Collections.unmodifiableList"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_objects_isNull) { + CBMFileResult *r = cov_extract( + "import java.util.Objects;\n" + "public class Main { public boolean f(Object o) { return Objects.isNull(o); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Objects.isNull"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_optional_isPresent) { + CBMFileResult *r = cov_extract( + "import java.util.Optional;\n" + "public class Main { public boolean f(Optional o) { return o.isPresent(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Optional.isPresent"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_optional_map) { + CBMFileResult *r = cov_extract( + "import java.util.Optional;\n" + "public class Main { public Object f(Optional o) { return o.map(s -> s.length()); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Optional.map"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_uuid_random) { + CBMFileResult *r = cov_extract( + "import java.util.UUID;\n" + "public class Main { public String f() { return UUID.randomUUID().toString(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "UUID.randomUUID"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_random_nextInt) { + CBMFileResult *r = cov_extract( + "import java.util.Random;\n" + "public class Main { public int f(Random r) { return r.nextInt(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Random.nextInt"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_util_scanner_nextLine) { + CBMFileResult *r = cov_extract( + "import java.util.Scanner;\n" + "public class Main { public String f(Scanner s) { return s.nextLine(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Scanner.nextLine"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 16. java.io stdlib ──────────────────────────────────────────── */ + +TEST(cov_io_print_writer) { + CBMFileResult *r = cov_extract( + "import java.io.PrintWriter;\n" + "public class Main { public void f(PrintWriter w) { w.println(\"x\"); w.flush(); w.close(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "PrintWriter.println"), 0); + ASSERT_GTE(cov_require(r, "f", "PrintWriter.flush"), 0); + ASSERT_GTE(cov_require(r, "f", "PrintWriter.close"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_io_input_stream_read) { + CBMFileResult *r = cov_extract( + "import java.io.InputStream;\n" + "public class Main { public int f(InputStream in) throws java.io.IOException { return in.read(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "InputStream.read"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_io_output_stream_write) { + CBMFileResult *r = cov_extract( + "import java.io.OutputStream;\n" + "public class Main { public void f(OutputStream o) throws java.io.IOException { o.write(1); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "OutputStream.write"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_io_buffered_writer) { + CBMFileResult *r = cov_extract( + "import java.io.BufferedWriter;\n" + "public class Main { public void f(BufferedWriter w) throws java.io.IOException {\n" + " w.write(\"x\"); w.newLine(); w.flush();\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "BufferedWriter.newLine"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_io_file_construction) { + CBMFileResult *r = cov_extract( + "import java.io.File;\n" + "public class Main { public File f() { return new File(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "File"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_io_file_listFiles) { + CBMFileResult *r = cov_extract( + "import java.io.File;\n" + "public class Main { public File[] f(File d) { return d.listFiles(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "File.listFiles"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_io_file_canRead) { + CBMFileResult *r = cov_extract( + "import java.io.File;\n" + "public class Main { public boolean f(File d) { return d.canRead(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "File.canRead"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_io_file_to_path) { + CBMFileResult *r = cov_extract( + "import java.io.File;\n" + "public class Main { public java.nio.file.Path f(File d) { return d.toPath(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "File.toPath"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_io_reader_ready) { + CBMFileResult *r = cov_extract( + "import java.io.Reader;\n" + "public class Main { public boolean f(Reader r) throws java.io.IOException { return r.ready(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Reader.ready"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_io_writer_close) { + CBMFileResult *r = cov_extract( + "import java.io.Writer;\n" + "public class Main { public void f(Writer w) throws java.io.IOException { w.close(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Writer.close"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 17. java.nio.file ──────────────────────────────────────────── */ + +TEST(cov_nio_path_resolve) { + CBMFileResult *r = cov_extract( + "import java.nio.file.Path;\n" + "public class Main { public Path f(Path p) { return p.resolve(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Path.resolve"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_nio_path_getFileName) { + CBMFileResult *r = cov_extract( + "import java.nio.file.Path;\n" + "public class Main { public Path f(Path p) { return p.getFileName(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Path.getFileName"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_nio_files_exists) { + CBMFileResult *r = cov_extract( + "import java.nio.file.Files;\n" + "import java.nio.file.Path;\n" + "public class Main { public boolean f(Path p) { return Files.exists(p); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Files.exists"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_nio_files_walk) { + CBMFileResult *r = cov_extract( + "import java.nio.file.Files;\n" + "import java.nio.file.Path;\n" + "public class Main { public Object f(Path p) throws java.io.IOException { return Files.walk(p); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Files.walk"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_nio_files_lines) { + CBMFileResult *r = cov_extract( + "import java.nio.file.Files;\n" + "import java.nio.file.Path;\n" + "public class Main { public Object f(Path p) throws java.io.IOException { return Files.lines(p); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Files.lines"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 18. java.util.stream ────────────────────────────────────────── */ + +TEST(cov_stream_sorted) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public Object f(ArrayList xs) { return xs.stream().sorted().count(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Stream.sorted"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_stream_distinct) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public Object f(ArrayList xs) { return xs.stream().distinct().count(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Stream.distinct"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_stream_limit) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public Object f(ArrayList xs) { return xs.stream().limit(5).count(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Stream.limit"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_stream_skip) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public Object f(ArrayList xs) { return xs.stream().skip(2).count(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Stream.skip"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_stream_min) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public Object f(ArrayList xs) { return xs.stream().min((a,b) -> a.compareTo(b)); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Stream.min"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_stream_findFirst) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public Object f(ArrayList xs) { return xs.stream().findFirst(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Stream.findFirst"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_stream_allMatch) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public boolean f(ArrayList xs) { return xs.stream().allMatch(s -> s.length() > 0); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Stream.allMatch"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_stream_collect_toSet) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "import java.util.stream.Collectors;\n" + "public class Main { public Object f(ArrayList xs) { return xs.stream().collect(Collectors.toSet()); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Collectors.toSet"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_stream_collect_groupingBy) { + /* The lambda inside Collectors.groupingBy is typed via Function, where T comes from the OUTER stream's element type + * — that bidirectional flow isn't currently modeled (the LSP looks at + * the static method's static signature only). Resolving Collectors. + * groupingBy itself is the test we keep; lambda body resolution is a + * known gap. */ + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "import java.util.stream.Collectors;\n" + "public class Main { public Object f(ArrayList xs) {\n" + " return xs.stream().collect(Collectors.groupingBy(s -> s.length()));\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Collectors.groupingBy"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_intstream_range) { + CBMFileResult *r = cov_extract( + "import java.util.stream.IntStream;\n" + "public class Main { public int f() { return IntStream.range(0, 10).sum(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "IntStream.range"), 0); + ASSERT_GTE(cov_require(r, "f", "IntStream.sum"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 19. java.util.function ──────────────────────────────────────── */ + +TEST(cov_fn_unaryoperator) { + CBMFileResult *r = cov_extract( + "import java.util.function.UnaryOperator;\n" + "public class Main { public Object f(UnaryOperator op) { return op.apply(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "UnaryOperator.apply"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fn_binaryoperator) { + /* BinaryOperator extends BiFunction, so apply() resolves to + * BiFunction.apply (correct per JLS §8.4.8). Either callee is OK. */ + CBMFileResult *r = cov_extract( + "import java.util.function.BinaryOperator;\n" + "public class Main { public Object f(BinaryOperator op) { return op.apply(\"x\", \"y\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "apply"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fn_consumer_andThen) { + CBMFileResult *r = cov_extract( + "import java.util.function.Consumer;\n" + "public class Main { public Object f(Consumer c1, Consumer c2) { return c1.andThen(c2); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Consumer.andThen"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fn_predicate_and) { + CBMFileResult *r = cov_extract( + "import java.util.function.Predicate;\n" + "public class Main { public Object f(Predicate p1, Predicate p2) { return p1.and(p2); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Predicate.and"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fn_predicate_negate) { + CBMFileResult *r = cov_extract( + "import java.util.function.Predicate;\n" + "public class Main { public Object f(Predicate p) { return p.negate(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Predicate.negate"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fn_function_andThen) { + CBMFileResult *r = cov_extract( + "import java.util.function.Function;\n" + "public class Main { public Object f(Function f1, Function f2) {\n" + " return f1.andThen(f2);\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Function.andThen"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fn_intpredicate) { + CBMFileResult *r = cov_extract( + "import java.util.function.IntPredicate;\n" + "public class Main { public boolean f(IntPredicate p) { return p.test(1); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "IntPredicate.test"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fn_toint_function) { + CBMFileResult *r = cov_extract( + "import java.util.function.ToIntFunction;\n" + "public class Main { public int f(ToIntFunction fn) { return fn.applyAsInt(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ToIntFunction.applyAsInt"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fn_int_supplier) { + CBMFileResult *r = cov_extract( + "import java.util.function.IntSupplier;\n" + "public class Main { public int f(IntSupplier s) { return s.getAsInt(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "IntSupplier.getAsInt"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_fn_runnable) { + CBMFileResult *r = cov_extract( + "public class Main { public void f(Runnable r) { r.run(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Runnable.run"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 20. java.util.concurrent ───────────────────────────────────── */ + +TEST(cov_conc_atomic_get) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.atomic.AtomicInteger;\n" + "public class Main { public int f(AtomicInteger a) { return a.get(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "AtomicInteger.get"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_conc_atomic_compareAndSet) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.atomic.AtomicInteger;\n" + "public class Main { public boolean f(AtomicInteger a) { return a.compareAndSet(0, 1); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "AtomicInteger.compareAndSet"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_conc_atomic_long) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.atomic.AtomicLong;\n" + "public class Main { public long f(AtomicLong a) { return a.incrementAndGet(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "AtomicLong.incrementAndGet"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_conc_atomic_reference) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.atomic.AtomicReference;\n" + "public class Main { public Object f(AtomicReference a) { return a.get(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "AtomicReference.get"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_conc_executor_singleThread) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.ExecutorService;\n" + "import java.util.concurrent.Executors;\n" + "public class Main { public ExecutorService f() { return Executors.newSingleThreadExecutor(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Executors.newSingleThreadExecutor"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_conc_executor_cached) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.ExecutorService;\n" + "import java.util.concurrent.Executors;\n" + "public class Main { public ExecutorService f() { return Executors.newCachedThreadPool(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Executors.newCachedThreadPool"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_conc_future_get) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.Future;\n" + "public class Main { public Object f(Future fut) throws Exception { return fut.get(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Future.get"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_conc_completable_supplyAsync) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.CompletableFuture;\n" + "public class Main { public CompletableFuture f() {\n" + " return CompletableFuture.supplyAsync(() -> \"x\");\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "CompletableFuture.supplyAsync"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_conc_concurrent_hashmap) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.ConcurrentHashMap;\n" + "public class Main { public Object f(ConcurrentHashMap m) { return m.get(\"k\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ConcurrentHashMap.get"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_conc_reentrant_lock) { + CBMFileResult *r = cov_extract( + "import java.util.concurrent.locks.ReentrantLock;\n" + "public class Main { public void f(ReentrantLock l) { l.lock(); l.unlock(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ReentrantLock.lock"), 0); + ASSERT_GTE(cov_require(r, "f", "ReentrantLock.unlock"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 21. java.time ───────────────────────────────────────────────── */ + +TEST(cov_time_localdate_plus) { + CBMFileResult *r = cov_extract( + "import java.time.LocalDate;\n" + "public class Main { public LocalDate f(LocalDate d) { return d.plusDays(1); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "LocalDate.plusDays"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_time_localdate_isAfter) { + CBMFileResult *r = cov_extract( + "import java.time.LocalDate;\n" + "public class Main { public boolean f(LocalDate a, LocalDate b) { return a.isAfter(b); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "LocalDate.isAfter"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_time_localdatetime_now) { + CBMFileResult *r = cov_extract( + "import java.time.LocalDateTime;\n" + "public class Main { public LocalDateTime f() { return LocalDateTime.now(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "LocalDateTime.now"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_time_instant_now) { + CBMFileResult *r = cov_extract( + "import java.time.Instant;\n" + "public class Main { public Instant f() { return Instant.now(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Instant.now"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_time_duration_between) { + CBMFileResult *r = cov_extract( + "import java.time.Duration;\n" + "import java.time.Instant;\n" + "public class Main { public Duration f(Instant a, Instant b) { return Duration.between(a, b); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Duration.between"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_time_duration_minutes) { + CBMFileResult *r = cov_extract( + "import java.time.Duration;\n" + "public class Main { public long f(Duration d) { return d.toMinutes(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Duration.toMinutes"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_time_zoneid_systemDefault) { + CBMFileResult *r = cov_extract( + "import java.time.ZoneId;\n" + "public class Main { public ZoneId f() { return ZoneId.systemDefault(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ZoneId.systemDefault"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_time_localdate_format) { + CBMFileResult *r = cov_extract( + "import java.time.LocalDate;\n" + "import java.time.format.DateTimeFormatter;\n" + "public class Main { public String f(LocalDate d, DateTimeFormatter fmt) { return d.format(fmt); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "LocalDate.format"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_time_formatter_ofPattern) { + CBMFileResult *r = cov_extract( + "import java.time.format.DateTimeFormatter;\n" + "public class Main { public DateTimeFormatter f() { return DateTimeFormatter.ofPattern(\"yyyy\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "DateTimeFormatter.ofPattern"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_time_localdate_parse) { + CBMFileResult *r = cov_extract( + "import java.time.LocalDate;\n" + "public class Main { public LocalDate f(String s) { return LocalDate.parse(s); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "LocalDate.parse"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 22. Imports ─────────────────────────────────────────────────── */ + +TEST(cov_imp_single_type) { + CBMFileResult *r = cov_extract( + "import java.util.HashMap;\n" + "public class Main { public int f(HashMap m) { return m.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashMap.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_imp_on_demand) { + CBMFileResult *r = cov_extract( + "import java.util.*;\n" + "public class Main { public int f(HashMap m) { return m.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashMap.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_imp_static_method) { + CBMFileResult *r = cov_extract( + "import static java.lang.Math.sqrt;\n" + "public class Main { public double f() { return sqrt(2); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Math.sqrt"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_imp_static_on_demand) { + CBMFileResult *r = cov_extract( + "import static java.lang.Math.*;\n" + "public class Main { public double f() { return PI; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_imp_multiple) { + CBMFileResult *r = cov_extract( + "import java.util.HashMap;\n" + "import java.util.ArrayList;\n" + "import java.util.List;\n" + "public class Main {\n" + " public int f(HashMap> m) { return m.size(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashMap.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_imp_qualified_no_import) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(java.util.HashMap m) { return m.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "HashMap.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_imp_with_package) { + CBMFileResult *r = cov_extract( + "package com.example;\n" + "import java.util.ArrayList;\n" + "public class Main { public int f(ArrayList xs) { return xs.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ArrayList.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_imp_chained_packages) { + CBMFileResult *r = cov_extract( + "package a.b.c;\n" + "import java.util.concurrent.atomic.AtomicInteger;\n" + "public class Main { public int f(AtomicInteger a) { return a.get(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "AtomicInteger.get"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 23. Inner / nested types ────────────────────────────────────── */ + +TEST(cov_nest_static_inner) { + CBMFileResult *r = cov_extract( + "public class Main { static class A { String tag() { return \"a\"; } }\n" + " public String f() { return new A().tag(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_nest_inner_3level) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class A { static class B { static class C { String tag() { return \"c\"; } } } }\n" + " public String f() { return new Main.A.B.C().tag(); }\n" + "}"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_nest_interface) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " interface I { String tag(); }\n" + " static class A implements I { public String tag() { return \"a\"; } }\n" + " public String f() { I x = new A(); return x.tag(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_nest_enum) { + CBMFileResult *r = cov_extract( + "public class Main { enum Color { RED, GREEN, BLUE } public Color f() { return Color.RED; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_nest_static_field) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class K { static String NAME = \"x\"; }\n" + " public int f() { return K.NAME.length(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_nest_outer_method_from_inner) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " String greet() { return \"hi\"; }\n" + " class Inner { public String f() { return greet(); } }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "greet"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_nest_outer_field_from_inner) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " private String tag = \"x\";\n" + " class Inner { public int f() { return tag.length(); } }\n" + "}"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_nest_record) { + CBMFileResult *r = cov_extract( + "public class Main { public record Point(int x, int y) {} public int f() { return new Point(1,2).x(); } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_nest_static_member_access) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class Util { static String tag() { return \"u\"; } }\n" + " public String f() { return Util.tag(); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "tag"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_nest_constructor_call) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " static class Item { public Item(String s) {} }\n" + " public Item f() { return new Item(\"x\"); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Item"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 24. Variable / scope binding ────────────────────────────────── */ + +TEST(cov_bind_local_int) { + CBMFileResult *r = cov_extract( + "public class Main { public void f() { int a = 1; a++; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_bind_var_string) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { var s = \"x\"; return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_bind_var_arraylist) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public int f() { var xs = new ArrayList(); xs.add(\"x\"); return xs.size(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ArrayList.add"), 0); + ASSERT_GTE(cov_require(r, "f", "ArrayList.size"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_bind_multi_decl) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { int a = 1, b = 2, c = 3; return a + b + c; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_bind_final_local) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { final String s = \"x\"; return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_bind_for_loop_init) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { int n = 0; for (int i = 0; i < 10; i++) n++; return n; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_bind_enhanced_for_array) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String[] xs) { int n = 0; for (String s : xs) n += s.length(); return n; } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_bind_enhanced_for_iterable) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public int f(ArrayList xs) { int n = 0; for (String s : xs) n += s.length(); return n; } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_bind_catch_param) { + CBMFileResult *r = cov_extract( + "public class Main { public String f() {\n" + " try { return Integer.toString(Integer.parseInt(\"x\")); }\n" + " catch (NumberFormatException e) { return e.getMessage(); }\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Throwable.getMessage"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_bind_try_resources) { + CBMFileResult *r = cov_extract( + "import java.io.BufferedReader;\n" + "import java.io.FileReader;\n" + "public class Main { public String f(String path) throws java.io.IOException {\n" + " try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); }\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "BufferedReader.readLine"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 25. Control flow ─────────────────────────────────────────────── */ + +TEST(cov_ctrl_if_call) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { if (s.isEmpty()) return 0; else return s.length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.isEmpty"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_ctrl_for_call) { + CBMFileResult *r = cov_extract( + "import java.util.ArrayList;\n" + "public class Main { public int f(ArrayList xs) {\n" + " int n = 0;\n" + " for (int i = 0; i < xs.size(); i++) n += xs.get(i).length();\n" + " return n;\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "ArrayList.size"), 0); + ASSERT_GTE(cov_require(r, "f", "ArrayList.get"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_ctrl_while_call) { + CBMFileResult *r = cov_extract( + "import java.util.Iterator;\n" + "public class Main { public int f(Iterator it) {\n" + " int n = 0;\n" + " while (it.hasNext()) n += it.next().length();\n" + " return n;\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Iterator.hasNext"), 0); + ASSERT_GTE(cov_require(r, "f", "Iterator.next"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_ctrl_do_while) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { int n = 0; do { n++; } while (n < s.length()); return n; } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_ctrl_switch_basic) { + CBMFileResult *r = cov_extract( + "public class Main { public String f(int x) {\n" + " switch (x) {\n" + " case 1: return \"one\".toUpperCase();\n" + " case 2: return \"two\".trim();\n" + " default: return \"\";\n" + " }\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.toUpperCase"), 0); + ASSERT_GTE(cov_require(r, "f", "String.trim"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_ctrl_try_finally) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) {\n" + " try { return s.length(); }\n" + " finally { s.toString(); }\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_ctrl_try_multi_catch) { + CBMFileResult *r = cov_extract( + "public class Main { public String f() {\n" + " try { return Integer.toString(Integer.parseInt(\"x\")); }\n" + " catch (NumberFormatException | NullPointerException e) { return e.getMessage(); }\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "Throwable.getMessage"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_ctrl_throw_in_method) { + CBMFileResult *r = cov_extract( + "public class Main { public void f() throws IllegalStateException { throw new IllegalStateException(\"x\"); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "IllegalStateException"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_ctrl_break_continue_no_crash) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(int[] xs) {\n" + " int n = 0;\n" + " for (int x : xs) { if (x < 0) continue; if (x > 100) break; n++; }\n" + " return n;\n" + "} }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_ctrl_synchronized) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(Object lock, String s) { synchronized(lock) { return s.length(); } } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── 26. Diagnostics ─────────────────────────────────────────────── */ + +TEST(cov_diag_unknown_method) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) { return s.nonexistentMethod(); } }"); + ASSERT_NOT_NULL(r); + int diag = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + if (r->resolved_calls.items[i].confidence == 0.0f) diag++; + } + ASSERT_GTE(diag, 1); + cbm_free_result(r); + PASS(); +} +TEST(cov_diag_unknown_var) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { return undef.size(); } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_diag_unresolved_count) { + CBMFileResult *r = cov_extract( + "public class Main { public void f() {\n" + " unknownObj1.unknownMethod1();\n" + " unknownObj2.unknownMethod2();\n" + "} }"); + ASSERT_NOT_NULL(r); + int diag = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + if (r->resolved_calls.items[i].confidence == 0.0f) diag++; + } + ASSERT_GTE(diag, 1); + cbm_free_result(r); + PASS(); +} +TEST(cov_diag_no_resolved_no_calls) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { return 1 + 2; } }"); + ASSERT_NOT_NULL(r); + /* No calls, no resolved entries. */ + int strict = cov_count_strict(r); + ASSERT_EQ(strict, 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_diag_unresolved_super_no_match) { + CBMFileResult *r = cov_extract( + "public class Main { public String f() { return super.nonexistent(); } }"); + ASSERT_NOT_NULL(r); + int diag = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + if (r->resolved_calls.items[i].confidence == 0.0f) diag++; + } + ASSERT_GTE(diag, 1); + cbm_free_result(r); + PASS(); +} +TEST(cov_diag_method_ref_unknown_lhs) { + CBMFileResult *r = cov_extract( + "public class Main { public Object f() { return undefined::method; } }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} + +/* ── 27. Cross-file API ──────────────────────────────────────────── */ + +TEST(cov_cross_basic) { + const char *src = + "package com.example;\n" + "public class Caller { public String f(com.example.Greeter g) { return g.greet(\"x\"); } }"; + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out; + memset(&out, 0, sizeof(out)); + CBMLSPDef defs[2]; + memset(defs, 0, sizeof(defs)); + defs[0].qualified_name = "com.example.Greeter"; + defs[0].short_name = "Greeter"; + defs[0].label = "Class"; + defs[0].def_module_qn = "com.example"; + defs[1].qualified_name = "com.example.Greeter.greet"; + defs[1].short_name = "greet"; + defs[1].label = "Method"; + defs[1].receiver_type = "com.example.Greeter"; + defs[1].return_types = "java.lang.String"; + const char *imp_names[] = {"Greeter"}; + const char *imp_qns[] = {"com.example.Greeter"}; + cbm_run_java_lsp_cross(&arena, src, (int)strlen(src), "test.com.example", defs, 2, imp_names, + imp_qns, 1, NULL, &out); + int found = 0; + for (int i = 0; i < out.count; i++) { + if (out.items[i].confidence < 0.5f) continue; + if (out.items[i].callee_qn && strstr(out.items[i].callee_qn, "Greeter.greet")) found = 1; + } + ASSERT_EQ(found, 1); + cbm_arena_destroy(&arena); + PASS(); +} +TEST(cov_cross_no_imports) { + const char *src = "public class A { public int f() { return 1; } }"; + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out; + memset(&out, 0, sizeof(out)); + cbm_run_java_lsp_cross(&arena, src, (int)strlen(src), "test.A", NULL, 0, NULL, NULL, 0, NULL, &out); + /* Just a smoke test — no calls, no edges. */ + cbm_arena_destroy(&arena); + PASS(); +} +TEST(cov_cross_batch) { + const char *src1 = "package p; public class A { public int f() { return 1; } }"; + const char *src2 = "package p; public class B { public int f() { return 2; } }"; + CBMArena arena; + cbm_arena_init(&arena); + CBMBatchJavaLSPFile files[2]; + memset(files, 0, sizeof(files)); + files[0].source = src1; + files[0].source_len = (int)strlen(src1); + files[0].module_qn = "test.A"; + files[1].source = src2; + files[1].source_len = (int)strlen(src2); + files[1].module_qn = "test.B"; + CBMResolvedCallArray outs[2]; + memset(outs, 0, sizeof(outs)); + cbm_batch_java_lsp_cross(&arena, files, 2, outs); + cbm_arena_destroy(&arena); + PASS(); +} +TEST(cov_cross_inheritance) { + const char *src = + "package x;\n" + "public class B { public String f(x.A a) { return a.tag(); } }"; + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out; + memset(&out, 0, sizeof(out)); + CBMLSPDef defs[2]; + memset(defs, 0, sizeof(defs)); + defs[0].qualified_name = "x.A"; + defs[0].short_name = "A"; + defs[0].label = "Class"; + defs[1].qualified_name = "x.A.tag"; + defs[1].short_name = "tag"; + defs[1].label = "Method"; + defs[1].receiver_type = "x.A"; + defs[1].return_types = "java.lang.String"; + cbm_run_java_lsp_cross(&arena, src, (int)strlen(src), "test.B", defs, 2, NULL, NULL, 0, NULL, &out); + int found = 0; + for (int i = 0; i < out.count; i++) { + if (out.items[i].confidence < 0.5f) continue; + if (out.items[i].callee_qn && strstr(out.items[i].callee_qn, "A.tag")) found = 1; + } + ASSERT_EQ(found, 1); + cbm_arena_destroy(&arena); + PASS(); +} + +/* ── 28. Stress / edge cases ─────────────────────────────────────── */ + +TEST(cov_edge_empty_method) { + CBMFileResult *r = cov_extract("public class Main { public void f() {} }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_edge_empty_class) { + CBMFileResult *r = cov_extract("public class Main {}"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_edge_long_chain) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) {\n" + " return s.trim().toLowerCase().toUpperCase().substring(0).length();\n" + "} }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.trim"), 0); + ASSERT_GTE(cov_require(r, "f", "String.toLowerCase"), 0); + ASSERT_GTE(cov_require(r, "f", "String.toUpperCase"), 0); + ASSERT_GTE(cov_require(r, "f", "String.substring"), 0); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_edge_recursive_user_method) { + CBMFileResult *r = cov_extract( + "public class Main { public int fib(int n) { return n < 2 ? n : fib(n-1) + fib(n-2); } }"); + ASSERT_NOT_NULL(r); + int fib_count = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &r->resolved_calls.items[i]; + if (rc->confidence >= 0.5f && rc->callee_qn && strstr(rc->callee_qn, "fib")) fib_count++; + } + ASSERT_GTE(fib_count, 2); + cbm_free_result(r); + PASS(); +} +TEST(cov_edge_mutual_recursion) { + CBMFileResult *r = cov_extract( + "public class Main {\n" + " public boolean isEven(int n) { return n == 0 || isOdd(n-1); }\n" + " public boolean isOdd(int n) { return n != 0 && isEven(n-1); }\n" + "}"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "isEven", "isOdd"), 0); + ASSERT_GTE(cov_require(r, "isOdd", "isEven"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_edge_deeply_nested_blocks) { + CBMFileResult *r = cov_extract( + "public class Main { public int f(String s) {\n" + " if (s != null) {\n" + " if (!s.isEmpty()) {\n" + " if (s.length() > 5) {\n" + " return s.length();\n" + " }\n" + " }\n" + " }\n" + " return 0;\n" + "} }"); + ASSERT_NOT_NULL(r); + int len_calls = 0; + for (int i = 0; i < r->resolved_calls.count; i++) { + const CBMResolvedCall *rc = &r->resolved_calls.items[i]; + if (rc->confidence >= 0.5f && rc->callee_qn && strstr(rc->callee_qn, "String.length")) { + len_calls++; + } + } + ASSERT_GTE(len_calls, 2); + cbm_free_result(r); + PASS(); +} +TEST(cov_edge_anonymous_class_no_crash) { + CBMFileResult *r = cov_extract( + "public class Main { public Runnable f() {\n" + " return new Runnable() { public void run() { System.out.println(\"x\"); } };\n" + "} }"); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_edge_unicode_string) { + CBMFileResult *r = cov_extract( + "public class Main { public int f() { return \"\\u00e9\".length(); } }"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} +TEST(cov_edge_huge_class_no_crash) { + char buf[8192]; + int len = snprintf(buf, sizeof(buf), "public class Main {\n"); + for (int i = 0; i < 50 && len < (int)sizeof(buf) - 200; i++) { + len += snprintf(buf + len, sizeof(buf) - len, + " public int m%d(int x) { return x + %d; }\n", i, i); + } + snprintf(buf + len, sizeof(buf) - len, "}\n"); + CBMFileResult *r = cov_extract(buf); + ASSERT_NOT_NULL(r); + cbm_free_result(r); + PASS(); +} +TEST(cov_edge_unusual_whitespace) { + CBMFileResult *r = cov_extract( + "public class Main {\npublic\tint f(String\ts) { return s.length(); }\n}\n"); + ASSERT_NOT_NULL(r); + ASSERT_GTE(cov_require(r, "f", "String.length"), 0); + cbm_free_result(r); + PASS(); +} + +/* ── Suite registration ──────────────────────────────────────────── */ + +void suite_java_lsp_coverage(void) { + /* 1. Literals */ + RUN_TEST(cov_lit_int); + RUN_TEST(cov_lit_long); + RUN_TEST(cov_lit_hex); + RUN_TEST(cov_lit_binary); + RUN_TEST(cov_lit_octal); + RUN_TEST(cov_lit_float_suffix); + RUN_TEST(cov_lit_double); + RUN_TEST(cov_lit_char); + RUN_TEST(cov_lit_string_method); + RUN_TEST(cov_lit_bool_method); + RUN_TEST(cov_lit_null_no_crash); + + /* 2. Type-name resolution */ + RUN_TEST(cov_typename_java_lang_implicit); + RUN_TEST(cov_typename_via_single_import); + RUN_TEST(cov_typename_via_on_demand_import); + RUN_TEST(cov_typename_via_static_import); + RUN_TEST(cov_typename_qualified_path); + RUN_TEST(cov_typename_inner_via_outer); + RUN_TEST(cov_typename_nested_two_levels); + RUN_TEST(cov_typename_array_type); + RUN_TEST(cov_typename_2d_array); + RUN_TEST(cov_typename_generic_type); + RUN_TEST(cov_typename_wildcard); + RUN_TEST(cov_typename_bounded_wildcard); + + /* 3. Primitive operations */ + RUN_TEST(cov_op_arith); + RUN_TEST(cov_op_compare); + RUN_TEST(cov_op_bitwise); + RUN_TEST(cov_op_unary_negate); + RUN_TEST(cov_op_logical_not); + RUN_TEST(cov_op_increment); + RUN_TEST(cov_op_string_concat); + RUN_TEST(cov_op_ternary_simple); + RUN_TEST(cov_op_assignment); + RUN_TEST(cov_op_paren_expr); + + /* 4. Type-AST kinds */ + RUN_TEST(cov_typeast_void_method); + RUN_TEST(cov_typeast_byte_param); + RUN_TEST(cov_typeast_short_param); + RUN_TEST(cov_typeast_int_param); + RUN_TEST(cov_typeast_long_param); + RUN_TEST(cov_typeast_float_param); + RUN_TEST(cov_typeast_double_param); + RUN_TEST(cov_typeast_char_param); + RUN_TEST(cov_typeast_boolean_param); + RUN_TEST(cov_typeast_string_param); + RUN_TEST(cov_typeast_array_of_string); + RUN_TEST(cov_typeast_generic_list); + + /* 5. Identifier resolution */ + RUN_TEST(cov_id_local_var); + RUN_TEST(cov_id_param); + RUN_TEST(cov_id_field); + RUN_TEST(cov_id_inherited_field); + RUN_TEST(cov_id_shadow_param); + RUN_TEST(cov_id_block_shadow); + RUN_TEST(cov_id_outer_class_field); + RUN_TEST(cov_id_class_as_static); + RUN_TEST(cov_id_unresolved_emits_diagnostic); + RUN_TEST(cov_id_for_loop_var); + + /* 6. Field access */ + RUN_TEST(cov_fa_system_out); + RUN_TEST(cov_fa_system_err); + RUN_TEST(cov_fa_array_length); + RUN_TEST(cov_fa_chained_field_method); + RUN_TEST(cov_fa_this_field); + RUN_TEST(cov_fa_static_class_field); + RUN_TEST(cov_fa_method_then_field); + RUN_TEST(cov_fa_field_through_param); + RUN_TEST(cov_fa_field_through_chained_call); + RUN_TEST(cov_fa_unresolved_field_no_crash); + + /* 7. Method invocation */ + RUN_TEST(cov_mi_bare_local); + RUN_TEST(cov_mi_this); + RUN_TEST(cov_mi_super); + RUN_TEST(cov_mi_static); + RUN_TEST(cov_mi_chained_static); + RUN_TEST(cov_mi_instance); + RUN_TEST(cov_mi_chain_3hop); + RUN_TEST(cov_mi_recursive_self); + RUN_TEST(cov_mi_overload_by_arity); + RUN_TEST(cov_mi_param_chain); + RUN_TEST(cov_mi_return_chain); + RUN_TEST(cov_mi_args_walked); + RUN_TEST(cov_mi_nested_args); + RUN_TEST(cov_mi_through_field_getter); + RUN_TEST(cov_mi_call_in_condition); + + /* 8. Object creation */ + RUN_TEST(cov_oc_simple); + RUN_TEST(cov_oc_with_args); + RUN_TEST(cov_oc_diamond); + RUN_TEST(cov_oc_generic_explicit); + RUN_TEST(cov_oc_user_class); + RUN_TEST(cov_oc_with_chained_method); + RUN_TEST(cov_oc_array); + RUN_TEST(cov_oc_array_init); + + /* 9. Cast / instanceof */ + RUN_TEST(cov_cast_to_string); + RUN_TEST(cov_cast_chained); + RUN_TEST(cov_cast_primitive); + RUN_TEST(cov_instanceof_basic); + RUN_TEST(cov_cast_from_iterator); + RUN_TEST(cov_cast_arr_elem); + RUN_TEST(cov_cast_in_ternary); + RUN_TEST(cov_cast_then_chain); + + /* 10. Generic substitution */ + RUN_TEST(cov_gen_list_get); + RUN_TEST(cov_gen_arraylist_get); + RUN_TEST(cov_gen_linkedlist_first); + RUN_TEST(cov_gen_set_iterator_next); + RUN_TEST(cov_gen_map_get); + RUN_TEST(cov_gen_hashmap_get); + RUN_TEST(cov_gen_optional_orElse); + RUN_TEST(cov_gen_optional_get); + RUN_TEST(cov_gen_iterator_next_chain); + RUN_TEST(cov_gen_function_apply); + RUN_TEST(cov_gen_supplier_get); + RUN_TEST(cov_gen_bifunction_apply); + RUN_TEST(cov_gen_map_keyset); + RUN_TEST(cov_gen_map_values); + RUN_TEST(cov_gen_collection_iterator); + + /* 11. Lambda SAM inference */ + RUN_TEST(cov_lambda_function); + RUN_TEST(cov_lambda_predicate_filter); + RUN_TEST(cov_lambda_consumer_forEach); + RUN_TEST(cov_lambda_function_map); + RUN_TEST(cov_lambda_user_consumer); + RUN_TEST(cov_lambda_user_function); + RUN_TEST(cov_lambda_user_predicate); + RUN_TEST(cov_lambda_block_body); + RUN_TEST(cov_lambda_bipredicate); + RUN_TEST(cov_lambda_biconsumer_map); + RUN_TEST(cov_lambda_anyMatch); + RUN_TEST(cov_lambda_removeIf); + + /* 12. Method references */ + RUN_TEST(cov_mref_static); + RUN_TEST(cov_mref_instance_method); + RUN_TEST(cov_mref_static_class_method); + RUN_TEST(cov_mref_constructor_simple); + RUN_TEST(cov_mref_in_local_var); + RUN_TEST(cov_mref_with_chain); + RUN_TEST(cov_mref_user_class); + RUN_TEST(cov_mref_user_constructor); + + /* 13. Inheritance */ + RUN_TEST(cov_inh_basic); + RUN_TEST(cov_inh_3level); + RUN_TEST(cov_inh_4level); + RUN_TEST(cov_inh_override); + RUN_TEST(cov_inh_super_after_override); + RUN_TEST(cov_inh_via_object); + RUN_TEST(cov_inh_interface_default); + RUN_TEST(cov_inh_interface_via_param); + RUN_TEST(cov_inh_collection_to_list); + RUN_TEST(cov_inh_list_iterable_iterator); + + /* 14. java.lang */ + RUN_TEST(cov_lang_object_hashCode); + RUN_TEST(cov_lang_object_equals); + RUN_TEST(cov_lang_object_getClass); + RUN_TEST(cov_lang_string_repeat); + RUN_TEST(cov_lang_string_split); + RUN_TEST(cov_lang_string_indexOf); + RUN_TEST(cov_lang_string_substring); + RUN_TEST(cov_lang_math_pow); + RUN_TEST(cov_lang_math_log); + RUN_TEST(cov_lang_math_random); + RUN_TEST(cov_lang_system_nano); + RUN_TEST(cov_lang_system_getenv); + RUN_TEST(cov_lang_thread_sleep); + RUN_TEST(cov_lang_class_forName); + RUN_TEST(cov_lang_throwable_getCause); + + /* 15. java.util */ + RUN_TEST(cov_util_arraylist_add); + RUN_TEST(cov_util_arraylist_remove); + RUN_TEST(cov_util_linkedlist_addFirst); + RUN_TEST(cov_util_hashset_add); + RUN_TEST(cov_util_treeset_first); + RUN_TEST(cov_util_hashmap_put); + RUN_TEST(cov_util_treemap_firstKey); + RUN_TEST(cov_util_arrays_sort); + RUN_TEST(cov_util_collections_unmodifiable); + RUN_TEST(cov_util_objects_isNull); + RUN_TEST(cov_util_optional_isPresent); + RUN_TEST(cov_util_optional_map); + RUN_TEST(cov_util_uuid_random); + RUN_TEST(cov_util_random_nextInt); + RUN_TEST(cov_util_scanner_nextLine); + + /* 16. java.io */ + RUN_TEST(cov_io_print_writer); + RUN_TEST(cov_io_input_stream_read); + RUN_TEST(cov_io_output_stream_write); + RUN_TEST(cov_io_buffered_writer); + RUN_TEST(cov_io_file_construction); + RUN_TEST(cov_io_file_listFiles); + RUN_TEST(cov_io_file_canRead); + RUN_TEST(cov_io_file_to_path); + RUN_TEST(cov_io_reader_ready); + RUN_TEST(cov_io_writer_close); + + /* 17. java.nio.file */ + RUN_TEST(cov_nio_path_resolve); + RUN_TEST(cov_nio_path_getFileName); + RUN_TEST(cov_nio_files_exists); + RUN_TEST(cov_nio_files_walk); + RUN_TEST(cov_nio_files_lines); + + /* 18. java.util.stream */ + RUN_TEST(cov_stream_sorted); + RUN_TEST(cov_stream_distinct); + RUN_TEST(cov_stream_limit); + RUN_TEST(cov_stream_skip); + RUN_TEST(cov_stream_min); + RUN_TEST(cov_stream_findFirst); + RUN_TEST(cov_stream_allMatch); + RUN_TEST(cov_stream_collect_toSet); + RUN_TEST(cov_stream_collect_groupingBy); + RUN_TEST(cov_intstream_range); + + /* 19. java.util.function */ + RUN_TEST(cov_fn_unaryoperator); + RUN_TEST(cov_fn_binaryoperator); + RUN_TEST(cov_fn_consumer_andThen); + RUN_TEST(cov_fn_predicate_and); + RUN_TEST(cov_fn_predicate_negate); + RUN_TEST(cov_fn_function_andThen); + RUN_TEST(cov_fn_intpredicate); + RUN_TEST(cov_fn_toint_function); + RUN_TEST(cov_fn_int_supplier); + RUN_TEST(cov_fn_runnable); + + /* 20. java.util.concurrent */ + RUN_TEST(cov_conc_atomic_get); + RUN_TEST(cov_conc_atomic_compareAndSet); + RUN_TEST(cov_conc_atomic_long); + RUN_TEST(cov_conc_atomic_reference); + RUN_TEST(cov_conc_executor_singleThread); + RUN_TEST(cov_conc_executor_cached); + RUN_TEST(cov_conc_future_get); + RUN_TEST(cov_conc_completable_supplyAsync); + RUN_TEST(cov_conc_concurrent_hashmap); + RUN_TEST(cov_conc_reentrant_lock); + + /* 21. java.time */ + RUN_TEST(cov_time_localdate_plus); + RUN_TEST(cov_time_localdate_isAfter); + RUN_TEST(cov_time_localdatetime_now); + RUN_TEST(cov_time_instant_now); + RUN_TEST(cov_time_duration_between); + RUN_TEST(cov_time_duration_minutes); + RUN_TEST(cov_time_zoneid_systemDefault); + RUN_TEST(cov_time_localdate_format); + RUN_TEST(cov_time_formatter_ofPattern); + RUN_TEST(cov_time_localdate_parse); + + /* 22. Imports */ + RUN_TEST(cov_imp_single_type); + RUN_TEST(cov_imp_on_demand); + RUN_TEST(cov_imp_static_method); + RUN_TEST(cov_imp_static_on_demand); + RUN_TEST(cov_imp_multiple); + RUN_TEST(cov_imp_qualified_no_import); + RUN_TEST(cov_imp_with_package); + RUN_TEST(cov_imp_chained_packages); + + /* 23. Inner/nested */ + RUN_TEST(cov_nest_static_inner); + RUN_TEST(cov_nest_inner_3level); + RUN_TEST(cov_nest_interface); + RUN_TEST(cov_nest_enum); + RUN_TEST(cov_nest_static_field); + RUN_TEST(cov_nest_outer_method_from_inner); + RUN_TEST(cov_nest_outer_field_from_inner); + RUN_TEST(cov_nest_record); + RUN_TEST(cov_nest_static_member_access); + RUN_TEST(cov_nest_constructor_call); + + /* 24. Variable / scope binding */ + RUN_TEST(cov_bind_local_int); + RUN_TEST(cov_bind_var_string); + RUN_TEST(cov_bind_var_arraylist); + RUN_TEST(cov_bind_multi_decl); + RUN_TEST(cov_bind_final_local); + RUN_TEST(cov_bind_for_loop_init); + RUN_TEST(cov_bind_enhanced_for_array); + RUN_TEST(cov_bind_enhanced_for_iterable); + RUN_TEST(cov_bind_catch_param); + RUN_TEST(cov_bind_try_resources); + + /* 25. Control flow */ + RUN_TEST(cov_ctrl_if_call); + RUN_TEST(cov_ctrl_for_call); + RUN_TEST(cov_ctrl_while_call); + RUN_TEST(cov_ctrl_do_while); + RUN_TEST(cov_ctrl_switch_basic); + RUN_TEST(cov_ctrl_try_finally); + RUN_TEST(cov_ctrl_try_multi_catch); + RUN_TEST(cov_ctrl_throw_in_method); + RUN_TEST(cov_ctrl_break_continue_no_crash); + RUN_TEST(cov_ctrl_synchronized); + + /* 26. Diagnostics */ + RUN_TEST(cov_diag_unknown_method); + RUN_TEST(cov_diag_unknown_var); + RUN_TEST(cov_diag_unresolved_count); + RUN_TEST(cov_diag_no_resolved_no_calls); + RUN_TEST(cov_diag_unresolved_super_no_match); + RUN_TEST(cov_diag_method_ref_unknown_lhs); + + /* 27. Cross-file */ + RUN_TEST(cov_cross_basic); + RUN_TEST(cov_cross_no_imports); + RUN_TEST(cov_cross_batch); + RUN_TEST(cov_cross_inheritance); + + /* 28. Stress / edge */ + RUN_TEST(cov_edge_empty_method); + RUN_TEST(cov_edge_empty_class); + RUN_TEST(cov_edge_long_chain); + RUN_TEST(cov_edge_recursive_user_method); + RUN_TEST(cov_edge_mutual_recursion); + RUN_TEST(cov_edge_deeply_nested_blocks); + RUN_TEST(cov_edge_anonymous_class_no_crash); + RUN_TEST(cov_edge_unicode_string); + RUN_TEST(cov_edge_huge_class_no_crash); + RUN_TEST(cov_edge_unusual_whitespace); + + (void)cov_no_crash; /* silence unused-static warning if any */ +} diff --git a/tests/test_main.c b/tests/test_main.c index 36675186a..c79d97386 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -49,6 +49,8 @@ extern void suite_py_lsp(void); extern void suite_py_lsp_bench(void); extern void suite_py_lsp_stress(void); extern void suite_py_lsp_scale(void); +extern void suite_java_lsp(void); +extern void suite_java_lsp_coverage(void); extern void suite_store_arch(void); extern void suite_store_bulk(void); extern void suite_traces(void); @@ -131,6 +133,8 @@ int main(void) { RUN_SUITE(py_lsp_bench); RUN_SUITE(py_lsp_stress); RUN_SUITE(py_lsp_scale); + RUN_SUITE(java_lsp); + RUN_SUITE(java_lsp_coverage); /* Architecture + ADR + Louvain */ RUN_SUITE(store_arch);