From 632fd08cf1c299468bd1d55a936bbb9a41f8229d Mon Sep 17 00:00:00 2001 From: Sinjan-Debnath Date: Thu, 9 Jul 2026 17:21:58 +0000 Subject: [PATCH 1/7] fix: resolve C/C++ includes to header file targets by matching stems Closes #964 Signed-off-by: Sinjan-Debnath --- internal/cbm/extract_defs.c | 4 +- src/pipeline/pass_pkgmap.c | 161 +++++++++++++++++- tests/fixtures/cpp_include/NodeController.cpp | 5 + tests/fixtures/cpp_include/NodeController.h | 5 + .../fixtures/cpp_include/SystemController.cpp | 5 + tests/fixtures/cpp_include/SystemController.h | 5 + tests/fixtures/cpp_include/main.cpp | 6 + tests/test_edge_imports.c | 150 ++++++++++++++++ 8 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/cpp_include/NodeController.cpp create mode 100644 tests/fixtures/cpp_include/NodeController.h create mode 100644 tests/fixtures/cpp_include/SystemController.cpp create mode 100644 tests/fixtures/cpp_include/SystemController.h create mode 100644 tests/fixtures/cpp_include/main.cpp diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 2de63ffbb..9fa24af94 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -5540,8 +5540,8 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch * For the nested case, the child has no "type" field directly. Detect by * walking named children for a variable_declaration. */ TSNode type_node = ts_node_child_by_field_name(child, TS_FIELD("type")); - TSNode name_node = - ts_node_is_null(type_node) ? (TSNode){0} : resolve_field_name_node(child); + TSNode empty_node = {0}; + TSNode name_node = ts_node_is_null(type_node) ? empty_node : resolve_field_name_node(child); if (ts_node_is_null(type_node)) { uint32_t cnc = ts_node_named_child_count(child); diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 61803254b..511a84564 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -426,8 +426,12 @@ static void parse_pyproject_toml(const char *source, int source_len, const char pkg_entries_push(entries, name, entry); /* Also register /__init__ as alternative (no src/ prefix) */ - snprintf(suffix, sizeof(suffix), "%s/__init__", name_copy); - char *alt_entry = build_entry_path(rel_path, suffix); + char *alt_entry = NULL; + if (name_copy) { + snprintf(suffix, sizeof(suffix), "%s/__init__", name_copy); + alt_entry = build_entry_path(rel_path, suffix); + } + if (name_copy && alt_entry) { pkg_entries_push(entries, name_copy, alt_entry); } else { @@ -1265,6 +1269,151 @@ static bool import_targetable_label(const char *label) { return false; } +static const char *path_leaf(const char *path) { + const char *leaf = path; + for (const char *p = path; p && *p; p++) { + if (*p == '/' || *p == '\\') { + leaf = p + 1; + } + } + return leaf; +} + +static bool is_header_include(const char *path) { + if (!path || !path[0]) { + return false; + } + static const char *header_exts[] = {".h", ".hh", ".hpp", ".hxx", ".inc", ".inl", ".ipp", NULL}; + for (const char **ext = header_exts; *ext; ext++) { + size_t path_len = strlen(path); + size_t ext_len = strlen(*ext); + if (path_len > ext_len && strcmp(path + path_len - ext_len, *ext) == 0) { + return true; + } + } + return false; +} + +static bool is_c_family_source(const char *source_rel) { + if (!source_rel || !source_rel[0]) { + return false; + } + static const char *exts[] = {".c", ".cc", ".cpp", ".cxx", ".c++", + ".h", ".hh", ".hpp", ".hxx", NULL}; + for (const char **ext = exts; *ext; ext++) { + size_t path_len = strlen(source_rel); + size_t ext_len = strlen(*ext); + if (path_len > ext_len && strcmp(source_rel + path_len - ext_len, *ext) == 0) { + return true; + } + } + return false; +} + +static const cbm_gbuf_node_t *resolve_exact_file_node(const cbm_pipeline_ctx_t *ctx, + const char *file_path, + const char *source_file_qn) { + if (!ctx || !file_path || !file_path[0]) { + return NULL; + } + + const char *leaf = path_leaf(file_path); + if (!leaf || !leaf[0]) { + return NULL; + } + + char stem[PKGMAP_PATH_BUF]; + snprintf(stem, sizeof(stem), "%s", leaf); + char *last_dot = strrchr(stem, '.'); + if (last_dot && last_dot != stem) { + *last_dot = '\0'; + } + + const char *names[2] = {stem[0] ? stem : NULL, leaf}; + const cbm_gbuf_node_t *best = NULL; + for (int ni = 0; ni < 2; ni++) { + const char *name = names[ni]; + if (!name || !name[0]) { + continue; + } + + const cbm_gbuf_node_t **hits = NULL; + int hit_count = 0; + if (cbm_gbuf_find_by_name(ctx->gbuf, name, &hits, &hit_count) != 0 || !hits) { + continue; + } + + for (int i = 0; i < hit_count; i++) { + const cbm_gbuf_node_t *cand = hits[i]; + if (!cand || !cand->file_path) { + continue; + } + + /* Tie-breaker: Ensure the candidate's actual file_path ends with the + * specific include path requested (e.g., 'a/util.h' instead of just 'util.h'). */ + if (!ends_with(cand->file_path, file_path)) { + continue; + } + + if (!cand->label || !import_targetable_label(cand->label)) { + continue; + } + if (source_file_qn && cand->qualified_name && + strcmp(cand->qualified_name, source_file_qn) == 0) { + continue; + } + if (strcmp(cand->label, "File") == 0) { + return cand; + } + if (!best) { + best = cand; + } + } + if (best) { + return best; + } + } + return NULL; +} + +static const cbm_gbuf_node_t *resolve_header_include(const cbm_pipeline_ctx_t *ctx, + const char *source_rel, + const char *source_file_qn, + const char *module_path) { + if (!is_c_family_source(source_rel) || !is_header_include(module_path)) { + return NULL; + } + + const char *base = module_path; + if (base[0] == '.' && base[1] == '/') { + base += 2; + } + + const cbm_gbuf_node_t *exact = resolve_exact_file_node(ctx, base, source_file_qn); + if (exact) { + return exact; + } + + if (source_rel && source_rel[0]) { + char *dir = path_dirname(source_rel); + if (dir) { + char candidate[PKGMAP_PATH_BUF]; + if (dir[0]) { + snprintf(candidate, sizeof(candidate), "%s/%s", dir, base); + } else { + snprintf(candidate, sizeof(candidate), "%s", base); + } + free(dir); + exact = resolve_exact_file_node(ctx, candidate, source_file_qn); + if (exact) { + return exact; + } + } + } + + return NULL; +} + /* Resolve a sibling-file import: a bare path/name (no leading "./") that names * a file relative to the importer's directory. This covers build/markup * grammars whose import string is a sibling filename or directory rather than a @@ -1352,6 +1501,14 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t return NULL; } + /* Prefer exact header-file nodes for C/C++ includes so same-stem source or + * module nodes do not steal the edge target. */ + const cbm_gbuf_node_t *header_target = + resolve_header_include(ctx, source_rel, source_file_qn, imp->module_path); + if (header_target) { + return header_target; + } + /* Strategy 1: module-path resolution → existing node (Python/TS/Go). * No label filter here: directory-module languages (Go/Java packages) * legitimately resolve straight to a Folder node -- that's the intended, diff --git a/tests/fixtures/cpp_include/NodeController.cpp b/tests/fixtures/cpp_include/NodeController.cpp new file mode 100644 index 000000000..8f27fb91e --- /dev/null +++ b/tests/fixtures/cpp_include/NodeController.cpp @@ -0,0 +1,5 @@ +#include "NodeController.h" + +int node_controller_value() { + return 1; +} \ No newline at end of file diff --git a/tests/fixtures/cpp_include/NodeController.h b/tests/fixtures/cpp_include/NodeController.h new file mode 100644 index 000000000..72a8bafa8 --- /dev/null +++ b/tests/fixtures/cpp_include/NodeController.h @@ -0,0 +1,5 @@ +#pragma once + +struct NodeController { + int value; +}; \ No newline at end of file diff --git a/tests/fixtures/cpp_include/SystemController.cpp b/tests/fixtures/cpp_include/SystemController.cpp new file mode 100644 index 000000000..8937a1b67 --- /dev/null +++ b/tests/fixtures/cpp_include/SystemController.cpp @@ -0,0 +1,5 @@ +#include "SystemController.h" + +int system_controller_value() { + return 2; +} \ No newline at end of file diff --git a/tests/fixtures/cpp_include/SystemController.h b/tests/fixtures/cpp_include/SystemController.h new file mode 100644 index 000000000..521c84f5b --- /dev/null +++ b/tests/fixtures/cpp_include/SystemController.h @@ -0,0 +1,5 @@ +#pragma once + +struct SystemController { + int value; +}; \ No newline at end of file diff --git a/tests/fixtures/cpp_include/main.cpp b/tests/fixtures/cpp_include/main.cpp new file mode 100644 index 000000000..a6e46c126 --- /dev/null +++ b/tests/fixtures/cpp_include/main.cpp @@ -0,0 +1,6 @@ +#include "NodeController.h" +#include + +int main() { + return 0; +} \ No newline at end of file diff --git a/tests/test_edge_imports.c b/tests/test_edge_imports.c index 8f4d20762..caf723267 100644 --- a/tests/test_edge_imports.c +++ b/tests/test_edge_imports.c @@ -72,6 +72,10 @@ typedef struct { const char *content; } EILangFile; +typedef struct { + const char *name; /* fixture filename relative to a checked-in fixture root */ +} EILangFixtureFile; + static void ei_to_fwd_slashes(char *p) { for (; *p; p++) { if (*p == '\\') *p = '/'; @@ -123,6 +127,89 @@ static cbm_store_t *ei_index_files(EILangProj *lp, const EILangFile *files, int return cbm_store_open_path(lp->dbpath); } +static char *ei_slurp_file(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) { + return NULL; + } + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return NULL; + } + long size = ftell(f); + if (size < 0) { + fclose(f); + return NULL; + } + if (fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + char *buf = (char *)calloc((size_t)size + 1, 1); + if (!buf) { + fclose(f); + return NULL; + } + size_t nread = fread(buf, 1, (size_t)size, f); + fclose(f); + buf[nread] = '\0'; + return buf; +} + +static cbm_store_t *ei_index_fixture_files(EILangProj *lp, const char *fixture_root, + const EILangFixtureFile *files, int nfiles) { + EILangFile *loaded = (EILangFile *)calloc((size_t)nfiles, sizeof(EILangFile)); + char **contents = (char **)calloc((size_t)nfiles, sizeof(char *)); + if (!loaded || !contents) { + free(loaded); + free(contents); + return NULL; + } + + cbm_store_t *store = NULL; + for (int i = 0; i < nfiles; i++) { + char path[512]; + snprintf(path, sizeof(path), "%s/%s", fixture_root, files[i].name); + contents[i] = ei_slurp_file(path); + if (!contents[i]) { + goto done; + } + loaded[i].name = files[i].name; + loaded[i].content = contents[i]; + } + + store = ei_index_files(lp, loaded, nfiles); + +done: + for (int i = 0; i < nfiles; i++) { + free(contents[i]); + } + free(contents); + free(loaded); + return store; +} + +static int64_t ei_node_id_for_file_label(cbm_store_t *store, const char *project, + const char *file_path, const char *label) { + cbm_node_t *nodes = NULL; + int count = 0; + if (cbm_store_find_nodes_by_file(store, project, file_path, &nodes, &count) != CBM_STORE_OK) { + return 0; + } + int64_t id = 0; + for (int i = 0; i < count; i++) { + if (nodes[i].label && strcmp(nodes[i].label, label) == 0) { + id = nodes[i].id; + break; + } + } + if (id == 0 && count > 0) { + id = nodes[0].id; + } + cbm_store_free_nodes(nodes, count); + return id; +} + static void ei_cleanup(EILangProj *lp, cbm_store_t *store) { if (store) cbm_store_close(store); if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; } @@ -409,6 +496,68 @@ TEST(ei_go_two_consumers_same_package) { PASS(); } +/* C++: header include should resolve to the header file node, not the same-stem + * source node. Also exercises angle-bracket include resolution. */ +TEST(ei_cpp_header_include_targets_header_file) { + static const EILangFixtureFile fixture_files[] = { + {"main.cpp"}, + {"NodeController.h"}, + {"NodeController.cpp"}, + {"SystemController.h"}, + {"SystemController.cpp"}, + }; + + EILangProj lp; + cbm_store_t *store = ei_index_fixture_files(&lp, "tests/fixtures/cpp_include", + fixture_files, + (int)(sizeof(fixture_files) / sizeof(fixture_files[0]))); + ASSERT_NOT_NULL(store); + + int64_t main_id = ei_node_id_for_file_label(store, lp.project, "main.cpp", "File"); + int64_t node_source_id = ei_node_id_for_file_label(store, lp.project, "NodeController.cpp", "File"); + int64_t system_source_id = ei_node_id_for_file_label(store, lp.project, "SystemController.cpp", "File"); + + ASSERT_GT(main_id, 0); + ASSERT_GT(node_source_id, 0); + ASSERT_GT(system_source_id, 0); + + cbm_edge_t *edges = NULL; + int edge_count = 0; + int rc = cbm_store_find_edges_by_source_type(store, main_id, "IMPORTS", &edges, &edge_count); + ASSERT_EQ(rc, CBM_STORE_OK); + ASSERT_TRUE(edge_count >= 2); + + bool saw_node_header = false; + bool saw_system_header = false; + for (int i = 0; i < edge_count; i++) { + cbm_node_t *target = (cbm_node_t *)calloc(1, sizeof(cbm_node_t)); + + /* Pass target directly (no &) because it is already a pointer */ + ASSERT_EQ(cbm_store_find_node_by_id(store, edges[i].target_id, target), CBM_STORE_OK); + ASSERT_EQ(edges[i].source_id, main_id); + ASSERT_NEQ(edges[i].target_id, node_source_id); + ASSERT_NEQ(edges[i].target_id, system_source_id); + + /* Use -> instead of . to access fields on a pointer */ + if (target->file_path && strcmp(target->file_path, "NodeController.h") == 0) { + saw_node_header = true; + } + if (target->file_path && strcmp(target->file_path, "SystemController.h") == 0) { + saw_system_header = true; + } + + /* Free the node inside the loop */ + cbm_store_free_nodes(target, 1); + } + cbm_store_free_edges(edges, edge_count); + + ASSERT_TRUE(saw_node_header); + ASSERT_TRUE(saw_system_header); + + ei_cleanup(&lp, store); + PASS(); +} + /* ═══════════════════════════════════════════════════════════════════════════ * RED REPRODUCTION — Rust * @@ -868,6 +1017,7 @@ SUITE(edge_imports) { RUN_TEST(ei_go_subpackage_import); RUN_TEST(ei_go_blank_import); RUN_TEST(ei_go_two_consumers_same_package); + RUN_TEST(ei_cpp_header_include_targets_header_file); /* ── RED REPRODUCTIONS — Rust (expected to FAIL until pipeline fixed) ── */ RUN_TEST(ei_rust_mod_plus_use); From 4d6f2dd34aa819d84838d9ef3602d016a6e77d76 Mon Sep 17 00:00:00 2001 From: Thomas Dyar Date: Sun, 14 Jun 2026 11:57:02 -0400 Subject: [PATCH 2/7] feat(objectscript): add InterSystems IRIS ObjectScript language support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ObjectScript (InterSystems IRIS / Caché) as a supported language, covering the UDL class format (.cls), MAC/INT routines (.mac/.int/.rtn), include/macro files (.inc), and IRIS Studio Export XML. Definition extraction (extract_defs.c): Class, Method, ClassMethod, Property, Parameter, Index, Trigger (with body text), XData, Storage, and Query members as graph nodes; base classes from the Extends clause. Call dispatch resolution (extract_calls.c) — four ObjectScript patterns that are structurally invisible to text search: 1. ##class(Pkg.Class).Method() explicit cross-class call 2. ..Method() relative-dot self-call (the dominant intra-class form; large impact on CALLS completeness) 3. $$$Macro macro expansion via a per-project table built from .inc files 4. type inference from %New/%OpenId + declared return types Ensemble production topology (pass_ensemble_routing.c): EnsembleItem nodes per production component and ROUTES_TO edges resolved from ProductionDefinition XData, plus WorkMgr .Queue("##class(X).method") dispatch — all parsed statically at index time, no live IRIS required. Language detection (language.c): .mac/.int/.rtn map to ObjectScript routine directly; .cls (shared with Apex) and .inc (shared with BitBake) are disambiguated by content, defaulting to the existing language on any doubt so neither Apex nor BitBake detection regresses. The two new per-project tables (macros, return types) are threaded through a new internal cbm_extract_file_ex() so the public cbm_extract_file() signature is unchanged. The tree-sitter grammars for ObjectScript UDL and routine are vendored in internal/cbm/vendored/grammars/objectscript_{udl,routine}/ from https://github.com/intersystems/tree-sitter-objectscript (MIT, ABI 15). Refs #462 Signed-off-by: Thomas Dyar --- Makefile.cbm | 2 + THIRD_PARTY.md | 9 + internal/cbm/cbm.c | 28 +- internal/cbm/cbm.h | 55 +- internal/cbm/extract_calls.c | 208 ++++- internal/cbm/extract_defs.c | 326 +++++++- internal/cbm/extract_unified.c | 283 +++++++ internal/cbm/extract_unified.h | 16 + internal/cbm/grammar_objectscript_routine.c | 4 + internal/cbm/grammar_objectscript_udl.c | 4 + internal/cbm/iris_export_xml.c | 444 ++++++++++ internal/cbm/iris_export_xml.h | 19 + internal/cbm/lang_specs.c | 43 + internal/cbm/macro_table.c | 251 ++++++ internal/cbm/macro_table.h | 47 ++ .../grammars/objectscript_routine/scanner.c | 179 ++-- .../grammars/objectscript_udl/scanner.c | 181 ++-- src/discover/discover.c | 22 + src/discover/discover.h | 11 + src/discover/language.c | 94 ++- src/pipeline/pass_calls.c | 92 ++- src/pipeline/pass_definitions.c | 41 +- src/pipeline/pass_parallel.c | 51 +- src/pipeline/pipeline.c | 78 +- src/pipeline/pipeline_incremental.c | 13 + src/pipeline/pipeline_internal.h | 8 + tests/test_extraction.c | 779 ++++++++++++++++++ 27 files changed, 3070 insertions(+), 218 deletions(-) create mode 100644 internal/cbm/grammar_objectscript_routine.c create mode 100644 internal/cbm/grammar_objectscript_udl.c create mode 100644 internal/cbm/iris_export_xml.c create mode 100644 internal/cbm/iris_export_xml.h create mode 100644 internal/cbm/macro_table.c create mode 100644 internal/cbm/macro_table.h diff --git a/Makefile.cbm b/Makefile.cbm index 44ccb7473..373ad3b99 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -132,6 +132,8 @@ EXTRACTION_SRCS = \ $(CBM_DIR)/extract_k8s.c \ $(CBM_DIR)/helpers.c \ $(CBM_DIR)/lang_specs.c \ + $(CBM_DIR)/macro_table.c \ + $(CBM_DIR)/iris_export_xml.c \ $(CBM_DIR)/service_patterns.c # LSP resolvers (compiled as one unit via lsp_all.c) diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md index 40f26f732..a8923aa46 100644 --- a/THIRD_PARTY.md +++ b/THIRD_PARTY.md @@ -48,6 +48,15 @@ License summary: original upstream authors' licenses — see the manifest for per-grammar provenance. +### tree-sitter-objectscript (UDL + routine) + +- **Project:** [intersystems/tree-sitter-objectscript](https://github.com/intersystems/tree-sitter-objectscript) +- **License:** MIT +- **Copyright:** (c) 2025 InterSystems Corporation +- **Vendored at:** `internal/cbm/vendored/grammars/objectscript_udl/`, `internal/cbm/vendored/grammars/objectscript_routine/` +- **Pinned commit:** `a7ffcdf` +- **Notes:** InterSystems-maintained grammar for the ObjectScript language (InterSystems IRIS / Caché). Vendor-maintained; not in nvim-treesitter or Helix registries. Each `scanner.c`'s upstream `#include "../../common/scanner.h"` is repointed to a per-directory `objectscript_common.h` (verbatim copy of upstream `common/scanner.h`). + ## Vendored C/C++ Libraries | Library | Path | License | Project | diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 8de94b941..74c5aada1 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -721,11 +721,6 @@ static bool cbm_source_nesting_exceeds(const char *source, int source_len, int c return false; } -static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, - CBMLanguage language, const char *project, - const char *rel_path, int64_t timeout_micros, - const char **extra_defines, const char **include_paths); - /* Best-effort parse-coverage collection (#963). Walks only the has_error paths * of the tree and records the 1-based line ranges of the TOP-MOST ERROR/MISSING * nodes (does not descend into an error subtree — one range per failed region). @@ -854,17 +849,17 @@ static const char *cbm_error_ranges_str(CBMArena *a, const cbm_error_regions_t * CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage language, const char *project, const char *rel_path, int64_t timeout_micros, const char **extra_defines, const char **include_paths) { - CBMFileResult *r = cbm_extract_file_impl(source, source_len, language, project, rel_path, - timeout_micros, extra_defines, include_paths); - cbm_index_mark_done(rel_path); + CBMFileResult *r = + cbm_extract_file_ex(source, source_len, language, project, rel_path, timeout_micros, + extra_defines, include_paths, NULL, NULL); return r; } -static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, - CBMLanguage language, const char *project, - const char *rel_path, int64_t timeout_micros, - const char **extra_defines, - const char **include_paths) { +CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLanguage language, + const char *project, const char *rel_path, + int64_t timeout_micros, const char **extra_defines, + const char **include_paths, const CBMMacroTable *macro_table, + const CBMReturnTypeTable *return_type_table) { // Allocate result on heap (arena inside for all string data) enum { SINGLE = 1 }; CBMFileResult *result = (CBMFileResult *)calloc(SINGLE, sizeof(CBMFileResult)); @@ -894,6 +889,7 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, if (!spec) { result->has_error = true; result->error_msg = cbm_arena_strdup(a, "unsupported language"); + cbm_index_mark_done(rel_path); return result; } @@ -902,6 +898,7 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, if (!ts_lang) { result->has_error = true; result->error_msg = cbm_arena_strdup(a, "no tree-sitter grammar"); + cbm_index_mark_done(rel_path); return result; } @@ -921,6 +918,7 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, if (!parser) { result->has_error = true; result->error_msg = cbm_arena_strdup(a, "parser alloc failed"); + cbm_index_mark_done(rel_path); return result; } @@ -953,6 +951,7 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, result->has_error = true; result->error_msg = cbm_arena_strdup(a, timeout_micros > 0 ? "parse timeout" : "parse failed"); + cbm_index_mark_done(rel_path); return result; } @@ -977,6 +976,8 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, .rel_path = rel_path, .module_qn = result->module_qn, .root = root, + .macro_table = macro_table, + .return_type_table = return_type_table, }; // Run extractors: defs + imports use separate walks (unique recursion patterns), @@ -1240,6 +1241,7 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, // Retain tree for cross-file LSP reuse (caller frees via cbm_free_tree) result->cached_tree = tree; result->cached_lang = language; + cbm_index_mark_done(rel_path); return result; } diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index c8645f555..28de37fb7 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -164,13 +164,16 @@ typedef enum { CBM_LANG_APEX, CBM_LANG_SOQL, CBM_LANG_SOSL, - CBM_LANG_KUSTOMIZE, // kustomization.yaml — Kubernetes overlay tool - CBM_LANG_K8S, // Generic Kubernetes manifest (apiVersion: detected) - CBM_LANG_PINE, // Pine Script (TradingView indicator / strategy language) - CBM_LANG_QML, // Qt QML (Qt Modeling Language — declarative UI + embedded JS) - CBM_LANG_CFSCRIPT, // CFML script dialect (.cfc components — Lucee/ColdFusion) - CBM_LANG_CFML, // CFML tag dialect (.cfm templates — Lucee/ColdFusion) - CBM_LANG_MOJO, // Mojo + CBM_LANG_KUSTOMIZE, // kustomization.yaml — Kubernetes overlay tool + CBM_LANG_K8S, // Generic Kubernetes manifest (apiVersion: detected) + CBM_LANG_PINE, // Pine Script (TradingView indicator / strategy language) + CBM_LANG_QML, // Qt QML (Qt Modeling Language — declarative UI + embedded JS) + CBM_LANG_CFSCRIPT, // CFML script dialect (.cfc components — Lucee/ColdFusion) + CBM_LANG_CFML, // CFML tag dialect (.cfm templates — Lucee/ColdFusion) + CBM_LANG_MOJO, // Mojo + CBM_LANG_OBJECTSCRIPT_UDL, // InterSystems ObjectScript UDL (.cls class files) + CBM_LANG_OBJECTSCRIPT_ROUTINE, // InterSystems ObjectScript routine (.mac/.int/.rtn/.inc) + CBM_LANG_OBJECTSCRIPT_EXPORT, // InterSystems Studio Export XML () CBM_LANG_COUNT } CBMLanguage; @@ -499,6 +502,24 @@ typedef struct { int count; } CBMStringConstantMap; +// Forward declaration: ObjectScript macro table (defined in macro_table.h). +typedef struct CBMMacroTable CBMMacroTable; + +// Method-return-type table for ObjectScript variable type inference. Populated +// from definition nodes (method QN -> declared return type) so a later +// `Set x = obj.Method()` can resolve x's class. +#define CBM_RETURN_TYPE_TABLE_CAP 2048 + +typedef struct { + const char *method_qn; + const char *return_type; +} CBMReturnTypeEntry; + +typedef struct { + CBMReturnTypeEntry entries[CBM_RETURN_TYPE_TABLE_CAP]; + int count; +} CBMReturnTypeTable; + typedef struct { CBMArena *arena; CBMFileResult *result; @@ -509,9 +530,11 @@ typedef struct { const char *rel_path; const char *module_qn; TSNode root; - EFCache ef_cache; // enclosing function cache - const char *enclosing_class_qn; // for nested class QN computation - CBMStringConstantMap string_constants; // module-level NAME = "value" pairs + EFCache ef_cache; // enclosing function cache + const char *enclosing_class_qn; // for nested class QN computation + CBMStringConstantMap string_constants; // module-level NAME = "value" pairs + const CBMMacroTable *macro_table; // ObjectScript $$$macro table (NULL if none) + const CBMReturnTypeTable *return_type_table; // ObjectScript method return types (NULL if none) } CBMExtractCtx; // --- Public API --- @@ -561,6 +584,18 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage const char **include_paths // NULL-terminated, or NULL ); +// Pipeline-internal variant of cbm_extract_file() carrying ObjectScript +// per-project tables (macro table + method-return-type table). The public +// cbm_extract_file() is a thin wrapper that passes NULL, NULL for both. +CBMFileResult *cbm_extract_file_ex( + const char *source, int source_len, CBMLanguage language, const char *project, + const char *rel_path, int64_t timeout_micros, + const char **extra_defines, // NULL-terminated, or NULL + const char **include_paths, // NULL-terminated, or NULL + const CBMMacroTable *macro_table, // ObjectScript macros, or NULL + const CBMReturnTypeTable *return_type_table // OS return types, or NULL +); + // Free all memory associated with a result. void cbm_free_result(CBMFileResult *result); diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index afe2938be..0c0b677b8 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -2,6 +2,7 @@ #include "arena.h" // CBMArena, cbm_arena_sprintf #include "helpers.h" #include "lang_specs.h" +#include "macro_table.h" #include "extract_unified.h" #include "foundation/constants.h" #include "extract_node_stack.h" @@ -1160,6 +1161,60 @@ static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char * return c; } } + if (lang == CBM_LANG_OBJECTSCRIPT_UDL || lang == CBM_LANG_OBJECTSCRIPT_ROUTINE) { + // ##class(Pkg.Class).Method() -> "Pkg.Class.Method" + if (strcmp(nk, "class_method_call") == 0) { + TSNode class_ref = cbm_find_child_by_kind(node, "class_ref"); + TSNode method_name = cbm_find_child_by_kind(node, "method_name"); + if (!ts_node_is_null(class_ref) && !ts_node_is_null(method_name)) { + TSNode cname = cbm_find_child_by_kind(class_ref, "class_name"); + if (ts_node_is_null(cname)) { + return NULL; + } + char *cls = cbm_node_text(a, cname, source); + if (!cls || !cls[0]) { + return NULL; + } + TSNode mname_ident = ts_node_named_child_count(method_name) > 0 + ? ts_node_named_child(method_name, 0) + : (TSNode){0}; + if (ts_node_is_null(mname_ident)) { + return cls; + } + char *meth = cbm_node_text(a, mname_ident, source); + if (!meth || !meth[0]) { + return cls; + } + return cbm_arena_sprintf(a, "%s.%s", cls, meth); + } + return NULL; + } + // $$label^routine extrinsic / routine tag call -> the line_ref text + if (strcmp(nk, "routine_tag_call") == 0) { + TSNode line_ref = cbm_find_child_by_kind(node, "line_ref"); + if (!ts_node_is_null(line_ref)) { + return cbm_node_text(a, line_ref, source); + } + return NULL; + } + // $$$Macro(...) -> raw "$$$Name" callee (expanded later in handle_calls) + if (strcmp(nk, "macro") == 0) { + char *raw = cbm_node_text(a, node, source); + if (!raw || raw[0] != '$' || raw[1] != '$' || raw[2] != '$') { + return NULL; + } + char *name_start = raw + 3; + char *paren = strchr(name_start, '('); + if (paren) { + *paren = '\0'; + } + if (!name_start[0]) { + return NULL; + } + return cbm_arena_sprintf(a, "$$$%s", name_start); + } + return NULL; + } return extract_scripting_callee(a, node, source, lang, nk); } @@ -1866,6 +1921,72 @@ static void extract_java_method_reference(CBMExtractCtx *ctx, TSNode node, const cbm_calls_push(&ctx->result->calls, ctx->arena, call); } +// ObjectScript: resolve `var.Method(...)` / `..Property.Method(...)` instance +// calls against the per-method variable type map. Returns arena "Class.Method" +// or NULL if the receiver's type is unknown. +static char *resolve_objectscript_instance_call(CBMArena *a, TSNode node, const char *source, + os_type_map_t *type_map) { + TSNode receiver = {0}; + TSNode oref = {0}; + const char *nk_first = NULL; + for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) { + TSNode child = ts_node_named_child(node, i); + const char *ck = ts_node_type(child); + if (strcmp(ck, "lvn") == 0 || strcmp(ck, "variable") == 0) { + receiver = child; + } else if (strcmp(ck, "relative_dot_property") == 0) { + receiver = child; + nk_first = "relative_dot_property"; + } else if (strcmp(ck, "oref_method") == 0) { + oref = child; + } + } + if (ts_node_is_null(oref)) { + return NULL; + } + TSNode method_name_node = cbm_find_child_by_kind(oref, "method_name"); + if (ts_node_is_null(method_name_node)) { + return NULL; + } + TSNode mn_ident = ts_node_named_child_count(method_name_node) > 0 + ? ts_node_named_child(method_name_node, 0) + : (TSNode){0}; + if (ts_node_is_null(mn_ident)) { + return NULL; + } + char *method = cbm_node_text(a, mn_ident, source); + if (!method || !method[0]) { + return NULL; + } + if (ts_node_is_null(receiver)) { + return NULL; + } + char *var_text = NULL; + if (nk_first && strcmp(nk_first, "relative_dot_property") == 0) { + TSNode prop_name = cbm_find_child_by_kind(receiver, "member_name"); + if (!ts_node_is_null(prop_name)) { + char *pname = cbm_node_text(a, prop_name, source); + if (pname && pname[0]) { + var_text = cbm_arena_sprintf(a, "..%s", pname); + } + } + if (!var_text) { + var_text = cbm_node_text(a, receiver, source); + } + } else { + var_text = cbm_node_text(a, receiver, source); + } + if (!var_text || !var_text[0]) { + return NULL; + } + for (int i = 0; i < type_map->count; i++) { + if (strcasecmp(type_map->entries[i].var_name, var_text) == 0) { + return cbm_arena_sprintf(a, "%s.%s", type_map->entries[i].class_name, method); + } + } + return NULL; +} + void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { if (!spec->call_node_types || !spec->call_node_types[0]) { return; @@ -1873,6 +1994,56 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk if (cbm_kind_in_set(node, spec->call_node_types)) { char *callee = extract_callee_name(ctx->arena, node, ctx->source, ctx->language); + + // ObjectScript: var.Method() / ..Property.Method() instance dispatch. + if (!callee && + (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL || + ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) && + strcmp(ts_node_type(node), "method_call") == 0) { + callee = resolve_objectscript_instance_call(ctx->arena, node, ctx->source, + &state->os_type_map); + } + + // ObjectScript: ..Method() oref self-call resolves against the enclosing class. + if (!callee && + (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL || + ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) && + strcmp(ts_node_type(node), "relative_dot_method") == 0 && state->enclosing_class_qn && + state->enclosing_class_qn[0]) { + TSNode oref = cbm_find_child_by_kind(node, "oref_method"); + if (!ts_node_is_null(oref)) { + TSNode mname_node = cbm_find_child_by_kind(oref, "method_name"); + if (!ts_node_is_null(mname_node)) { + TSNode ident = ts_node_named_child_count(mname_node) > 0 + ? ts_node_named_child(mname_node, 0) + : (TSNode){0}; + if (!ts_node_is_null(ident)) { + char *mname = cbm_node_text(ctx->arena, ident, ctx->source); + if (mname && mname[0]) { + callee = cbm_arena_sprintf(ctx->arena, "%s.%s", + state->enclosing_class_qn, mname); + } + } + } + } + } + + // ObjectScript: expand a $$$Macro callee via the macro table. + if (callee && callee[0] == '$' && callee[1] == '$' && callee[2] == '$' && + ctx->macro_table) { + const char *macro_name = callee + 3; + const CBMMacroEntry *entry = cbm_macro_table_find(ctx->macro_table, macro_name); + if (entry) { + if (entry->resolved_callee) { + callee = cbm_arena_strdup(ctx->arena, entry->resolved_callee); + } else if (entry->expansion) { + callee = cbm_macro_extract_callee(ctx->arena, entry->expansion); + } else { + callee = NULL; + } + } + } + // Keyword-filter callees, but keep builtins we mint a node for (len, str, // ...) so the LSP-resolved builtin call still forms a CALLS edge. if (callee && callee[0] && @@ -1917,12 +2088,47 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk } TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); + // ObjectScript stores args under oref_method/method_args, not the + // generic "arguments" field. + if (ts_node_is_null(args) && (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL || + ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE)) { + TSNode oref = cbm_find_child_by_kind(node, "oref_method"); + if (!ts_node_is_null(oref)) { + args = cbm_find_child_by_kind(oref, "method_args"); + } + if (ts_node_is_null(args)) { + args = cbm_find_child_by_kind(node, "method_args"); + } + } if (!ts_node_is_null(args)) { call.first_string_arg = extract_url_or_topic_arg(ctx, args); if (call.first_string_arg && call.first_string_arg[0] == '/') { call.second_arg_name = extract_handler_arg(ctx, args); } - extract_call_args(ctx, args, &call); + if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL || + ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) { + for (uint32_t ai = 0; + ai < ts_node_named_child_count(args) && call.arg_count < CBM_MAX_CALL_ARGS; + ai++) { + TSNode achild = ts_node_named_child(args, ai); + const char *ack = ts_node_type(achild); + if (strcmp(ack, "bracket") == 0) { + continue; + } + if (strcmp(ack, "method_arg") != 0) { + continue; + } + CBMCallArg *ca = &call.args[call.arg_count]; + memset(ca, 0, sizeof(*ca)); + ca->index = call.arg_count; + ca->expr = cbm_node_text(ctx->arena, achild, ctx->source); + if (ca->expr && ca->expr[0]) { + call.arg_count++; + } + } + } else { + extract_call_args(ctx, args, &call); + } } cbm_calls_push(&ctx->result->calls, ctx->arena, call); diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 2de63ffbb..7cd51a617 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -10,7 +10,7 @@ #include "semantic/ast_profile.h" #include "tree_sitter/api.h" // TSNode, ts_node_* #include // uint32_t -#include // snprintf +#include // snprintf (ObjectScript storage/trigger sidecars) #include // getenv, atoi #include #include @@ -98,7 +98,9 @@ static char *extract_body_ident_tokens(CBMExtractCtx *ctx, TSNode body) { if (nc == 0) { const char *k = ts_node_type(nd); if (strcmp(k, "identifier") == 0 || strcmp(k, "field_identifier") == 0 || - strcmp(k, "property_identifier") == 0) { + strcmp(k, "property_identifier") == 0 || + strcmp(k, "objectscript_identifier") == 0 || + strcmp(k, "identifier_segment_immediate") == 0) { uint32_t s = ts_node_start_byte(nd); int len = (int)(ts_node_end_byte(nd) - s); if (len > 0 && len < CBM_SZ_64 && s < (uint32_t)ctx->source_len) { @@ -677,6 +679,25 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) { return null_node; } + // ObjectScript routine tag is its own name node. + if (lang == CBM_LANG_OBJECTSCRIPT_ROUTINE && strcmp(kind, "tag") == 0) { + return node; + } + // ObjectScript method/classmethod: name lives under method_definition -> + // method_name -> first named child. + if (lang == CBM_LANG_OBJECTSCRIPT_UDL && + (strcmp(kind, "method") == 0 || strcmp(kind, "classmethod") == 0)) { + TSNode mdef = cbm_find_child_by_kind(node, "method_definition"); + if (!ts_node_is_null(mdef)) { + TSNode mname = cbm_find_child_by_kind(mdef, "method_name"); + if (!ts_node_is_null(mname) && ts_node_named_child_count(mname) > 0) { + return ts_node_named_child(mname, 0); + } + } + TSNode null_node = {0}; + return null_node; + } + TSNode name = func_name_node(node); if (lang == CBM_LANG_R && strcmp(kind, "function_definition") == 0) { @@ -2372,6 +2393,37 @@ static const char **extract_julia_base_classes(CBMArena *a, TSNode node, const c static const char **extract_base_classes(CBMArena *a, TSNode node, const char *source, CBMLanguage lang) { + // ObjectScript: `Class X Extends (A, B)` — bases are class_name children of + // the class_extends node. + if (lang == CBM_LANG_OBJECTSCRIPT_UDL) { + TSNode ext = cbm_find_child_by_kind(node, "class_extends"); + if (!ts_node_is_null(ext)) { + const char *bases[MAX_BASES]; + int base_count = 0; + uint32_t nc = ts_node_named_child_count(ext); + for (uint32_t i = 0; i < nc && base_count < MAX_BASES_MINUS_1; i++) { + TSNode ch = ts_node_named_child(ext, i); + if (strcmp(ts_node_type(ch), "class_name") == 0) { + char *base = cbm_node_text(a, ch, source); + if (base && base[0]) { + bases[base_count++] = base; + } + } + } + if (base_count > 0) { + const char **result = + (const char **)cbm_arena_alloc(a, (base_count + 1) * sizeof(const char *)); + if (result) { + for (int i = 0; i < base_count; i++) { + result[i] = bases[i]; + } + result[base_count] = NULL; + return result; + } + } + } + return NULL; + } // Languages whose heritage is not exposed via a tree-sitter field need // dedicated walkers; the generic field/keyword path mis-captures them. if (lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) { @@ -3506,6 +3558,10 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJC) { name_node = cbm_find_child_by_kind(node, "identifier"); } + // ObjectScript UDL: class name is a `class_name` child (no "name" field). + if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJECTSCRIPT_UDL) { + name_node = cbm_find_child_by_kind(node, "class_name"); + } // Swift and newer tree-sitter-kotlin: class/object name is a type_identifier // child (no "name" field). if (ts_node_is_null(name_node) && @@ -4076,6 +4132,22 @@ static TSNode resolve_method_name(TSNode child, CBMLanguage lang) { return cbm_find_child_by_kind(child, "identifier"); } + // ObjectScript method/classmethod: name under method_definition->method_name. + if (lang == CBM_LANG_OBJECTSCRIPT_UDL && + (strcmp(ck, "method") == 0 || strcmp(ck, "classmethod") == 0)) { + TSNode mdef = cbm_find_child_by_kind(child, "method_definition"); + if (!ts_node_is_null(mdef)) { + TSNode mname = cbm_find_child_by_kind(mdef, "method_name"); + if (!ts_node_is_null(mname) && ts_node_named_child_count(mname) > 0) { + return ts_node_named_child(mname, 0); + } + } + } + // ObjectScript query member. + if (lang == CBM_LANG_OBJECTSCRIPT_UDL && strcmp(ck, "query") == 0) { + return cbm_find_child_by_kind(child, "query_name"); + } + if (strcmp(ck, "arrow_function") == 0) { return resolve_arrow_func_name(child); } @@ -4109,6 +4181,11 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node, def.is_exported = cbm_is_exported(name, ctx->language); TSNode params = ts_node_child_by_field_name(child, TS_FIELD("parameters")); + // ObjectScript exposes the parameter list under a `parameter_list` field. + if (ts_node_is_null(params) && (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL || + ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE)) { + params = ts_node_child_by_field_name(child, TS_FIELD("parameter_list")); + } if (!ts_node_is_null(params)) { def.signature = cbm_node_text(a, params, ctx->source); def.param_types = extract_param_types(a, params, ctx->source, ctx->language); @@ -4126,6 +4203,22 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node, } } + // ObjectScript: return type is method_definition -> return_type -> typename. + if (!def.return_type && (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL || + ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE)) { + TSNode mdef = cbm_find_child_by_kind(child, "method_definition"); + if (ts_node_is_null(mdef)) { + mdef = child; + } + TSNode rt_node = cbm_find_child_by_kind(mdef, "return_type"); + if (!ts_node_is_null(rt_node)) { + TSNode tname = cbm_find_child_by_kind(rt_node, "typename"); + if (!ts_node_is_null(tname)) { + def.return_type = cbm_node_text(a, tname, ctx->source); + } + } + } + // C++: trailing return type (auto method() -> Type) if (def.return_type && strcmp(def.return_type, "auto") == 0 && (ctx->language == CBM_LANG_CPP || ctx->language == CBM_LANG_CUDA)) { @@ -4229,6 +4322,19 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c continue; } + // ObjectScript UDL wraps each method/classmethod in a class_statement. + if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL && + strcmp(ts_node_type(child), "class_statement") == 0) { + if (ts_node_named_child_count(child) == 0) { + continue; + } + TSNode inner = ts_node_named_child(child, 0); + if (!cbm_kind_in_set(inner, spec->function_node_types)) { + continue; + } + method_node = inner; + } + if (!cbm_kind_in_set(method_node, spec->function_node_types)) { continue; } @@ -5516,6 +5622,14 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch uint32_t count = ts_node_named_child_count(body); for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_named_child(body, i); + + // ObjectScript UDL wraps each member in a class_statement node. + if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL && + strcmp(ts_node_type(child), "class_statement") == 0 && + ts_node_named_child_count(child) > 0) { + child = ts_node_named_child(child, 0); + } + if (!cbm_kind_in_set(child, spec->field_node_types)) { continue; } @@ -5531,6 +5645,211 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch continue; } + // ObjectScript UDL member extraction. property/parameter -> Variable; + // index/trigger/xdata/storage/foreignkey -> labelled members with + // storage-XML and trigger-body sidecars. + if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL) { + if (strcmp(ts_node_type(child), "property") == 0 || + strcmp(ts_node_type(child), "parameter") == 0) { + TSNode pname = cbm_find_child_by_kind(child, "property_name"); + if (ts_node_is_null(pname)) { + pname = cbm_find_child_by_kind(child, "parameter_name"); + } + if (!ts_node_is_null(pname) && ts_node_named_child_count(pname) > 0) { + TSNode ident = ts_node_named_child(pname, 0); + char *pn = cbm_node_text(a, ident, ctx->source); + if (pn && pn[0]) { + CBMDefinition pdef; + memset(&pdef, 0, sizeof(pdef)); + pdef.name = pn; + pdef.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, pn); + pdef.label = "Variable"; + pdef.file_path = ctx->rel_path; + pdef.parent_class = class_qn; + pdef.start_line = ts_node_start_point(child).row + TS_LINE_OFFSET; + pdef.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET; + cbm_defs_push(&ctx->result->defs, a, pdef); + } + } + continue; + } + + const char *ntype = ts_node_type(child); + const char *name_child_kind = NULL; + const char *member_label = NULL; + if (strcmp(ntype, "index") == 0) { + name_child_kind = "index_name"; + member_label = "Index"; + } else if (strcmp(ntype, "trigger") == 0) { + name_child_kind = "trigger_name"; + member_label = "Trigger"; + } else if (strcmp(ntype, "xdata") == 0) { + name_child_kind = "xdata_name"; + member_label = "XData"; + } else if (strcmp(ntype, "storage") == 0) { + name_child_kind = "storage_name"; + member_label = "Storage"; + } else if (strcmp(ntype, "foreignkey") == 0) { + name_child_kind = "foreignkey_name"; + member_label = "Variable"; + } + + if (name_child_kind) { + TSNode nname = cbm_find_child_by_kind(child, name_child_kind); + if (!ts_node_is_null(nname)) { + char *mn = cbm_node_text(a, nname, ctx->source); + if (mn && mn[0]) { + CBMDefinition mdef; + memset(&mdef, 0, sizeof(mdef)); + mdef.name = mn; + mdef.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, mn); + mdef.label = member_label; + mdef.file_path = ctx->rel_path; + mdef.parent_class = class_qn; + mdef.start_line = ts_node_start_point(child).row + TS_LINE_OFFSET; + mdef.end_line = ts_node_end_point(child).row + TS_LINE_OFFSET; + + if (strcmp(member_label, "Storage") == 0) { + TSNode sbody = cbm_find_child_by_kind(child, "storage_body"); + if (!ts_node_is_null(sbody)) { + char *xml = cbm_node_text(a, sbody, ctx->source); + if (xml) { + char props[CBM_SZ_2K]; + int pos = snprintf(props, sizeof(props), "{"); + static const struct { + const char *tag; + const char *key; + } kv[] = {{"ExtentSize", "extent_size"}, + {"DataLocation", "data_global"}, + {"IdLocation", "id_global"}, + {"IndexLocation", "index_global"}, + {"StreamLocation", "stream_global"}, + {"Type", "storage_type"}, + {NULL, NULL}}; + bool first = true; + for (int ki = 0; kv[ki].tag; ki++) { + char open[64], close[64], buf[256]; + snprintf(open, sizeof(open), "<%s>", kv[ki].tag); + snprintf(close, sizeof(close), "", kv[ki].tag); + const char *s = strstr(xml, open); + if (!s) { + continue; + } + s += strlen(open); + const char *e = strstr(s, close); + if (!e) { + continue; + } + size_t vlen = (size_t)(e - s); + if (vlen >= sizeof(buf)) { + vlen = sizeof(buf) - 1; + } + memcpy(buf, s, vlen); + buf[vlen] = '\0'; + char esc[300]; + int ei = 0; + for (size_t ci = 0; ci < vlen && ei < (int)sizeof(esc) - 2; + ci++) { + if (buf[ci] == '"' || buf[ci] == '\\') { + esc[ei++] = '\\'; + } + esc[ei++] = buf[ci]; + } + esc[ei] = '\0'; + if (pos < 0 || pos >= (int)sizeof(props) - 1) { + break; // buffer full — stop appending + } + pos += snprintf(props + pos, sizeof(props) - (size_t)pos, + "%s\"%s\":\"%s\"", first ? "" : ",", + kv[ki].key, esc); + if (pos >= (int)sizeof(props)) { + pos = (int)sizeof(props) - 1; // truncated + } + first = false; + } + const char *sql_tag = ""; + const char *sql_end = ""; + char sql_map_buf[512]; + int smi = 0; + const char *sp = xml; + bool sql_first = true; + while ((sp = strstr(sp, sql_tag)) != NULL) { + sp += strlen(sql_tag); + const char *ep = strstr(sp, sql_end); + if (!ep) { + break; + } + size_t glen = (size_t)(ep - sp); + if (smi + (int)glen + 2 < (int)sizeof(sql_map_buf) - 1) { + if (!sql_first) { + sql_map_buf[smi++] = ' '; + } + memcpy(sql_map_buf + smi, sp, glen); + smi += (int)glen; + sql_first = false; + } + sp = ep + strlen(sql_end); + } + sql_map_buf[smi] = '\0'; + if (smi > 0 && pos >= 0 && pos < (int)sizeof(props) - 1) { + pos += snprintf(props + pos, sizeof(props) - (size_t)pos, + "%s\"sql_map_globals\":\"%s\"", + first ? "" : ",", sql_map_buf); + if (pos >= (int)sizeof(props)) { + pos = (int)sizeof(props) - 1; // truncated + } + first = false; + } + if (pos < (int)sizeof(props) - 1) { + props[pos++] = '}'; + props[pos] = '\0'; + } + if (!first) { + mdef.docstring = cbm_arena_strdup(a, props); + } + } + } + } + + if (strcmp(member_label, "Trigger") == 0) { + TSNode tbody = cbm_find_child_by_kind(child, "core_trigger"); + if (ts_node_is_null(tbody)) { + tbody = cbm_find_child_by_kind(child, "external_trigger"); + } + if (!ts_node_is_null(tbody)) { + mdef.body_tokens = extract_body_ident_tokens(ctx, tbody); + char *raw = cbm_node_text(a, tbody, ctx->source); + if (raw && raw[0]) { + char esc[CBM_SZ_512]; + int ei = 0; + for (int ci = 0; raw[ci] && ei < (int)sizeof(esc) - 3; ci++) { + if (raw[ci] == '"' || raw[ci] == '\\') { + esc[ei++] = '\\'; + } else if (raw[ci] == '\n') { + esc[ei++] = '\\'; + esc[ei++] = 'n'; + continue; + } else if (raw[ci] == '\r') { + continue; + } + esc[ei++] = raw[ci]; + } + esc[ei] = '\0'; + char props[CBM_SZ_512]; + snprintf(props, sizeof(props), "{\"trigger_body\":\"%s\"}", + esc); + mdef.docstring = cbm_arena_strdup(a, props); + } + } + } + + cbm_defs_push(&ctx->result->defs, a, mdef); + } + } + continue; + } + } + /* Locate the field's "type" + name node. Two shapes: * - direct (Java/Go/Rust/C/C++): * field_declaration .type=identifier .declarator=variable_declarator(.name) @@ -5812,6 +6131,9 @@ static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const char if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_SWIFT) { name_node = cbm_find_child_by_kind(node, "type_identifier"); } + if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_OBJECTSCRIPT_UDL) { + name_node = cbm_find_child_by_kind(node, "class_name"); + } if (!ts_node_is_null(name_node)) { char *cname = cbm_node_text(ctx->arena, name_node, ctx->source); if (cname && cname[0]) { diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index 4b747789a..d6f2380e2 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -10,6 +10,7 @@ enum { MAX_INFRA_BINDINGS = 8 }; #include // uint32_t, uint8_t #include +#include // strcasecmp (ObjectScript type inference) // --- Scope stack management --- @@ -264,6 +265,225 @@ static const char *compute_gotemplate_func_qn(CBMExtractCtx *ctx, TSNode node) { return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, raw); } +// --- ObjectScript variable type inference (instance_method_call resolution) --- + +// Insert or update var_name -> class_name. Silent on overflow. +static void os_type_map_add(os_type_map_t *map, const char *var_name, const char *class_name) { + if (map->count >= OS_TYPE_MAP_CAP || !var_name || !class_name) { + return; + } + for (int i = 0; i < map->count; i++) { + if (strcmp(map->entries[i].var_name, var_name) == 0) { + map->entries[i].class_name = class_name; + return; + } + } + map->entries[map->count].var_name = var_name; + map->entries[map->count].class_name = class_name; + map->count++; +} + +// Locate the class_method_call inside an RHS expression (peeking through a +// couple of common ObjectScript expression container node types). +static TSNode find_class_method_call(TSNode root, const char *end) { + (void)end; + if (strcmp(ts_node_type(root), "class_method_call") == 0) { + return root; + } + static const char *containers[] = {"expression", "expr_atom", NULL}; + for (const char **c = containers; *c; c++) { + TSNode inner = cbm_find_child_by_kind(root, *c); + if (!ts_node_is_null(inner)) { + TSNode hit = cbm_find_child_by_kind(inner, "class_method_call"); + if (!ts_node_is_null(hit)) { + return hit; + } + TSNode inner2 = cbm_find_child_by_kind(inner, "expr_atom"); + if (!ts_node_is_null(inner2)) { + hit = cbm_find_child_by_kind(inner2, "class_method_call"); + if (!ts_node_is_null(hit)) { + return hit; + } + } + } + } + return cbm_find_child_by_kind(root, "class_method_call"); +} + +// On a `Set var = ##class(X).%New()` (or %OpenId/%Open, or a method whose +// return type is known) map var -> X. On a class `Property`/`Relationship`, +// map `..PropName -> typename` (surviving method-scope resets). +static void handle_objectscript_type_map(CBMExtractCtx *ctx, TSNode node, WalkState *state) { + if (ctx->language != CBM_LANG_OBJECTSCRIPT_UDL && + ctx->language != CBM_LANG_OBJECTSCRIPT_ROUTINE) { + return; + } + + const char *nk = ts_node_type(node); + + if (strcmp(nk, "command_set") == 0) { + for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) { + TSNode set_arg = ts_node_named_child(node, i); + const char *sak = ts_node_type(set_arg); + if (strcmp(sak, "set_argument") != 0 && strcmp(sak, "assignment") != 0) { + continue; + } + TSNode lhs = {0}; + TSNode rhs = {0}; + for (uint32_t j = 0; j < ts_node_named_child_count(set_arg); j++) { + TSNode achild = ts_node_named_child(set_arg, j); + const char *ak = ts_node_type(achild); + if (strcmp(ak, "set_target") == 0 || strcmp(ak, "lvn") == 0 || + strcmp(ak, "variable") == 0 || strcmp(ak, "glvn") == 0) { + lhs = achild; + } else if (strcmp(ak, "expression") == 0 || strcmp(ak, "expr_atom") == 0 || + strcmp(ak, "class_method_call") == 0) { + rhs = achild; + } + } + if (ts_node_is_null(lhs) || ts_node_is_null(rhs)) { + continue; + } + + TSNode cm_call = find_class_method_call(rhs, NULL); + if (ts_node_is_null(cm_call)) { + continue; + } + + TSNode method_name_node = cbm_find_child_by_kind(cm_call, "method_name"); + if (ts_node_is_null(method_name_node)) { + continue; + } + TSNode mn_ident = ts_node_named_child_count(method_name_node) > 0 + ? ts_node_named_child(method_name_node, 0) + : (TSNode){0}; + if (ts_node_is_null(mn_ident)) { + continue; + } + char *method_text = cbm_node_text(ctx->arena, mn_ident, ctx->source); + if (!method_text) { + continue; + } + + TSNode class_ref = cbm_find_child_by_kind(cm_call, "class_ref"); + if (ts_node_is_null(class_ref)) { + continue; + } + TSNode cname = cbm_find_child_by_kind(class_ref, "class_name"); + if (ts_node_is_null(cname)) { + continue; + } + char *cls = cbm_node_text(ctx->arena, cname, ctx->source); + if (!cls || !cls[0]) { + continue; + } + + bool is_constructor = + (strcasecmp(method_text, "%New") == 0 || strcasecmp(method_text, "%OpenId") == 0 || + strcasecmp(method_text, "%Open") == 0); + if (!is_constructor) { + if (!ctx->return_type_table) { + continue; + } + char *method_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", cls, method_text); + for (int rti = 0; rti < ctx->return_type_table->count; rti++) { + if (strcasecmp(ctx->return_type_table->entries[rti].method_qn, method_qn) == + 0) { + cls = cbm_arena_strdup(ctx->arena, + ctx->return_type_table->entries[rti].return_type); + is_constructor = true; + break; + } + } + if (!is_constructor) { + continue; + } + } + + TSNode var_node = lhs; + TSNode inner = cbm_find_child_by_kind(lhs, "glvn"); + if (!ts_node_is_null(inner)) { + var_node = inner; + } + inner = cbm_find_child_by_kind(var_node, "lvn"); + if (!ts_node_is_null(inner)) { + var_node = inner; + } + char *var = cbm_node_text(ctx->arena, var_node, ctx->source); + if (!var || !var[0]) { + continue; + } + + os_type_map_add(&state->os_type_map, var, cls); + } + } + + if (strcmp(nk, "property") == 0 || strcmp(nk, "relationship") == 0) { + TSNode prop_name_node = cbm_find_child_by_kind(node, "property_name"); + if (ts_node_is_null(prop_name_node)) { + prop_name_node = cbm_find_child_by_kind(node, "relationship_name"); + } + TSNode ret_type = cbm_find_child_by_kind(node, "return_type"); + if (!ts_node_is_null(prop_name_node) && !ts_node_is_null(ret_type)) { + TSNode tname = cbm_find_child_by_kind(ret_type, "typename"); + if (!ts_node_is_null(tname)) { + char *pname = cbm_node_text(ctx->arena, prop_name_node, ctx->source); + char *ptype = cbm_node_text(ctx->arena, tname, ctx->source); + if (pname && pname[0] && ptype && ptype[0]) { + char *dot_name = cbm_arena_sprintf(ctx->arena, "..%s", pname); + os_type_map_add(&state->os_type_map, dot_name, ptype); + state->os_type_map.class_base_count = state->os_type_map.count; + } + } + } + } +} + +// Resolve the FQN of an ObjectScript class_definition node (via its class_name). +static const char *objectscript_get_class_name(CBMExtractCtx *ctx, TSNode node) { + for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) { + TSNode child = ts_node_named_child(node, i); + if (strcmp(ts_node_type(child), "class_name") == 0) { + char *name = cbm_node_text(ctx->arena, child, ctx->source); + if (name && name[0]) { + return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name); + } + } + } + return NULL; +} + +// Resolve the QN of an ObjectScript method/classmethod node for scope tracking. +static const char *objectscript_get_method_qn(CBMExtractCtx *ctx, TSNode node, + const char *enclosing_class_qn) { + const char *nk = ts_node_type(node); + if (strcmp(nk, "method") != 0 && strcmp(nk, "classmethod") != 0) { + return NULL; + } + for (uint32_t i = 0; i < ts_node_named_child_count(node); i++) { + TSNode child = ts_node_named_child(node, i); + if (strcmp(ts_node_type(child), "method_definition") == 0) { + for (uint32_t j = 0; j < ts_node_named_child_count(child); j++) { + TSNode mchild = ts_node_named_child(child, j); + if (strcmp(ts_node_type(mchild), "method_name") == 0) { + if (ts_node_named_child_count(mchild) > 0) { + TSNode ident = ts_node_named_child(mchild, 0); + char *name = cbm_node_text(ctx->arena, ident, ctx->source); + if (name && name[0]) { + if (enclosing_class_qn) { + return cbm_arena_sprintf(ctx->arena, "%s.%s", enclosing_class_qn, + name); + } + return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name); + } + } + } + } + } + } + return NULL; +} + // Compute function QN for scope tracking (mirrors cbm_enclosing_func_qn logic). static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { @@ -271,6 +491,18 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan if (ctx->language == CBM_LANG_WOLFRAM) { return compute_wolfram_func_qn(ctx, node); } + if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL) { + return objectscript_get_method_qn(ctx, node, state->enclosing_class_qn); + } + if (ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) { + if (strcmp(ts_node_type(node), "tag") == 0) { + char *name = cbm_node_text(ctx->arena, node, ctx->source); + if (name && name[0]) { + return cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, name); + } + } + return NULL; + } /* CFML tag dialect: is a cf_function_tag whose name * lives in a cf_attribute, not a `name` field — gate here where ctx->source @@ -423,6 +655,9 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan // Compute class QN for scope tracking. static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const WalkState *state) { + if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL) { + return objectscript_get_class_name(ctx, node); + } TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name")); /* Newer tree-sitter-kotlin: class/object name is a type_identifier child. */ if (ts_node_is_null(name_node) && ctx->language == CBM_LANG_KOTLIN) { @@ -1129,12 +1364,59 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS const char *fqn = compute_func_qn(ctx, node, spec, state); if (fqn) { push_scope(state, SCOPE_FUNC, depth, fqn); + // ObjectScript: entering a method resets local var types (keeping + // class-level property types) and seeds the declared parameter types. + if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL || + ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) { + state->os_type_map.count = state->os_type_map.class_base_count; + TSNode mdef = cbm_find_child_by_kind(node, "method_definition"); + if (ts_node_is_null(mdef)) { + mdef = node; + } + TSNode args_node = cbm_find_child_by_kind(mdef, "arguments"); + if (!ts_node_is_null(args_node)) { + for (uint32_t ai = 0; ai < ts_node_named_child_count(args_node); ai++) { + TSNode arg = ts_node_named_child(args_node, ai); + if (strcmp(ts_node_type(arg), "argument") != 0) { + continue; + } + TSNode param_name_node = {0}; + TSNode type_node = {0}; + for (uint32_t pi = 0; pi < ts_node_named_child_count(arg); pi++) { + TSNode pchild = ts_node_named_child(arg, pi); + const char *pk = ts_node_type(pchild); + if (strcmp(pk, "method_arg") == 0) { + param_name_node = pchild; + } else if (strcmp(pk, "return_type") == 0) { + type_node = cbm_find_child_by_kind(pchild, "typename"); + } + } + if (!ts_node_is_null(param_name_node) && !ts_node_is_null(type_node)) { + TSNode lvn = cbm_find_child_by_kind(param_name_node, "expr_atom"); + if (ts_node_is_null(lvn)) { + lvn = param_name_node; + } + char *pname = cbm_node_text(ctx->arena, lvn, ctx->source); + char *ptype = cbm_node_text(ctx->arena, type_node, ctx->source); + if (pname && pname[0] && ptype && ptype[0]) { + os_type_map_add(&state->os_type_map, pname, ptype); + } + } + } + } + } } } } else if (spec->class_node_types && cbm_kind_in_set(node, spec->class_node_types)) { const char *cqn = compute_class_qn(ctx, node, state); if (cqn) { push_scope(state, SCOPE_CLASS, depth, cqn); + // ObjectScript: a new class clears the type map entirely. + if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL || + ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) { + state->os_type_map.count = 0; + state->os_type_map.class_base_count = 0; + } } } else if (ctx->language == CBM_LANG_RUST && strcmp(ts_node_type(node), "impl_item") == 0) { TSNode type_node = ts_node_child_by_field_name(node, TS_FIELD("type")); @@ -1202,6 +1484,7 @@ void cbm_extract_unified(CBMExtractCtx *ctx) { recompute_state(&state, ctx->module_qn); handle_string_constants(ctx, node, &state); + handle_objectscript_type_map(ctx, node, &state); handle_calls(ctx, node, spec, &state); handle_usages(ctx, node, spec, &state); handle_throws(ctx, node, spec, &state); diff --git a/internal/cbm/extract_unified.h b/internal/cbm/extract_unified.h index 6e2eb60f5..ac9d6b64d 100644 --- a/internal/cbm/extract_unified.h +++ b/internal/cbm/extract_unified.h @@ -14,6 +14,20 @@ #define MAX_SCOPES 64 +// ObjectScript type map: variable name → class name (for instance_method_call +// resolution). Stack-allocated, per-method scope. Overflow is silent (no crash). +#define OS_TYPE_MAP_CAP 64 +typedef struct { + const char *var_name; + const char *class_name; +} os_type_entry_t; + +typedef struct { + os_type_entry_t entries[OS_TYPE_MAP_CAP]; + int count; + int class_base_count; // entries [0,class_base_count) survive method-scope resets +} os_type_map_t; + // WalkState tracks scope context during the unified cursor walk. // Replaces parent-chain walks for enclosing_func_qn, inside_call, etc. typedef struct { @@ -30,6 +44,8 @@ typedef struct { uint8_t kind; } scopes[MAX_SCOPES]; int scope_top; + + os_type_map_t os_type_map; // ObjectScript variable → type mapping } WalkState; // Per-node handler prototypes. Each is called once per node during the diff --git a/internal/cbm/grammar_objectscript_routine.c b/internal/cbm/grammar_objectscript_routine.c new file mode 100644 index 000000000..9006eda3a --- /dev/null +++ b/internal/cbm/grammar_objectscript_routine.c @@ -0,0 +1,4 @@ +// Vendored tree-sitter grammar: objectscript_routine +// Each grammar compiled as separate unit (conflicting static symbols). +#include "vendored/grammars/objectscript_routine/parser.c" +#include "vendored/grammars/objectscript_routine/scanner.c" diff --git a/internal/cbm/grammar_objectscript_udl.c b/internal/cbm/grammar_objectscript_udl.c new file mode 100644 index 000000000..5aea58c44 --- /dev/null +++ b/internal/cbm/grammar_objectscript_udl.c @@ -0,0 +1,4 @@ +// Vendored tree-sitter grammar: objectscript_udl +// Each grammar compiled as separate unit (conflicting static symbols). +#include "vendored/grammars/objectscript_udl/parser.c" +#include "vendored/grammars/objectscript_udl/scanner.c" diff --git a/internal/cbm/iris_export_xml.c b/internal/cbm/iris_export_xml.c new file mode 100644 index 000000000..148d4a3ac --- /dev/null +++ b/internal/cbm/iris_export_xml.c @@ -0,0 +1,444 @@ +#include "iris_export_xml.h" +#include "arena.h" +#include +#include +#include +#include + +#define EXPORT_MARKER "= sz) + vl = sz - 1; + memcpy(out, v, vl); + out[vl] = '\0'; + return; + } + } +} +static const char *elem_content(const char *p, const char *end, const char *tag, char *buf, + size_t bufsz) { + buf[0] = '\0'; + char open[MAX_NAME + 2]; + snprintf(open, sizeof(open), "<%s", tag); + const char *start = find_s(p, end, open); + if (!start) + return NULL; + const char *gt = find_s(start, end, ">"); + if (!gt) + return NULL; + if (is_self_closing(start, gt)) + return gt + 1; + const char *cs = gt + 1; + if (sw(cs, end, ""); + if (!ce) + return NULL; + size_t l = (size_t)(ce - cs); + if (l >= bufsz) + l = bufsz - 1; + memcpy(buf, cs, l); + buf[l] = '\0'; + return ce + 3; + } + char close[MAX_NAME + 4]; + snprintf(close, sizeof(close), "", tag); + const char *cl = find_s(cs, end, close); + if (!cl) + return NULL; + size_t l = (size_t)(cl - cs); + if (l >= bufsz) + l = bufsz - 1; + memcpy(buf, cs, l); + buf[l] = '\0'; + return cl + strlen(close); +} +static bool tag_is_one(const char *p, const char *end, const char *tag) { + char buf[8]; + return elem_content(p, end, tag, buf, sizeof(buf)) && strcmp(buf, "1") == 0; +} + +static void ub_init(UdlBuf *b, CBMArena *arena) { + b->buf = (char *)cbm_arena_alloc(arena, BUF_CAP); + b->pos = 0; + b->cap = BUF_CAP; + if (b->buf) + b->buf[0] = '\0'; +} +static void ub_app(UdlBuf *b, const char *s) { + if (!b->buf || !s) + return; + size_t n = strlen(s); + if (b->pos + (int)n + 1 >= b->cap) + return; + memcpy(b->buf + b->pos, s, n); + b->pos += (int)n; + b->buf[b->pos] = '\0'; +} + +static void emit_header(UdlBuf *b, const char *cs, const char *ce) { + char name[MAX_NAME]; + extract_attr(cs, ce, "name", name, sizeof(name)); + if (!name[0]) + return; + ub_app(b, "Class "); + ub_app(b, name); + char sup[MAX_NAME * 4] = ""; + elem_content(cs, ce, "Super", sup, sizeof(sup)); + if (sup[0]) { + if (strchr(sup, ',')) { + ub_app(b, " Extends ("); + ub_app(b, sup); + ub_app(b, ")"); + } else { + ub_app(b, " Extends "); + ub_app(b, sup); + } + } + char pragma[64] = ""; + if (tag_is_one(cs, ce, "Abstract")) + strncat(pragma, "Abstract,", sizeof(pragma) - strlen(pragma) - 1); + if (tag_is_one(cs, ce, "Final")) + strncat(pragma, "Final,", sizeof(pragma) - strlen(pragma) - 1); + if (pragma[0]) { + pragma[strlen(pragma) - 1] = '\0'; + ub_app(b, " [ "); + ub_app(b, pragma); + ub_app(b, " ]"); + } + ub_app(b, "\n{\n\n"); +} + +static void emit_method(UdlBuf *b, const char *ms, const char *me) { + char mn[MAX_NAME]; + extract_attr(ms, me, "name", mn, sizeof(mn)); + if (!mn[0]) + return; + bool cm = tag_is_one(ms, me, "ClassMethod"); + char formal[1024] = ""; + elem_content(ms, me, "FormalSpec", formal, sizeof(formal)); + char ret[MAX_NAME] = ""; + elem_content(ms, me, "ReturnType", ret, sizeof(ret)); + char desc[4096] = ""; + elem_content(ms, me, "Description", desc, sizeof(desc)); + if (desc[0]) { + ub_app(b, "/// "); + for (char *c = desc; *c; c++) { + if (*c == '\n') + ub_app(b, "\n/// "); + else { + char t[2] = {*c, 0}; + ub_app(b, t); + } + } + ub_app(b, "\n"); + } + ub_app(b, cm ? "ClassMethod " : "Method "); + ub_app(b, mn); + ub_app(b, "("); + ub_app(b, formal); + ub_app(b, ")"); + if (ret[0]) { + ub_app(b, " As "); + ub_app(b, ret); + } + ub_app(b, "\n{\n"); + char impl[1024 * 32] = ""; + elem_content(ms, me, "Implementation", impl, sizeof(impl)); + ub_app(b, impl); + ub_app(b, "}\n\n"); +} + +static void emit_property(UdlBuf *b, const char *ps, const char *pe) { + char pn[MAX_NAME]; + extract_attr(ps, pe, "name", pn, sizeof(pn)); + if (!pn[0]) + return; + char pt[MAX_NAME] = ""; + elem_content(ps, pe, "Type", pt, sizeof(pt)); + PropParam params[MAX_PARAMS]; + int np = 0; + const char *pp = ps; + while (pp < pe && np < MAX_PARAMS) { + const char *po = find_s(pp, pe, ""); + if (!pg) + break; + extract_attr(po, pg, "name", params[np].param_name, MAX_NAME); + extract_attr(po, pg, "value", params[np].param_value, MAX_NAME); + if (!params[np].param_value[0]) { + char db[MAX_NAME]; + const char *a = elem_content(po, pe, "Parameter", db, MAX_NAME); + if (a && db[0]) + strncpy(params[np].param_value, db, MAX_NAME - 1); + } + if (params[np].param_name[0]) + np++; + pp = pg + 1; + } + ub_app(b, "Property "); + ub_app(b, pn); + if (pt[0]) { + ub_app(b, " As "); + ub_app(b, pt); + } + if (np > 0) { + ub_app(b, "("); + for (int i = 0; i < np; i++) { + if (i > 0) + ub_app(b, ", "); + ub_app(b, params[i].param_name); + if (params[i].param_value[0]) { + ub_app(b, " = "); + ub_app(b, params[i].param_value); + } + } + ub_app(b, ")"); + } + ub_app(b, ";\n\n"); +} + +static void emit_parameter(UdlBuf *b, const char *ps, const char *pe) { + char pn[MAX_NAME]; + extract_attr(ps, pe, "name", pn, sizeof(pn)); + if (!pn[0]) + return; + char dv[MAX_NAME] = ""; + elem_content(ps, pe, "Default", dv, sizeof(dv)); + ub_app(b, "Parameter "); + ub_app(b, pn); + if (dv[0]) { + ub_app(b, " = \""); + ub_app(b, dv); + ub_app(b, "\""); + } + ub_app(b, ";\n\n"); +} + +static void emit_index(UdlBuf *b, const char *is_, const char *ie) { + char in_[MAX_NAME]; + extract_attr(is_, ie, "name", in_, sizeof(in_)); + if (!in_[0]) + return; + char props[MAX_NAME * 4] = ""; + elem_content(is_, ie, "Properties", props, sizeof(props)); + bool uniq = tag_is_one(is_, ie, "Unique"); + bool pkey = tag_is_one(is_, ie, "PrimaryKey"); + ub_app(b, "Index "); + ub_app(b, in_); + if (props[0]) { + ub_app(b, " On "); + ub_app(b, props); + } + if (uniq || pkey) { + ub_app(b, " [ "); + if (pkey) + ub_app(b, "PrimaryKey, "); + if (uniq) + ub_app(b, "Unique"); + ub_app(b, " ]"); + } + ub_app(b, ";\n\n"); +} + +static void emit_xdata(UdlBuf *b, const char *xs, const char *xe) { + char xn[MAX_NAME]; + extract_attr(xs, xe, "name", xn, sizeof(xn)); + if (!xn[0]) + return; + char data[1024 * 32] = ""; + elem_content(xs, xe, "Data", data, sizeof(data)); + ub_app(b, "XData "); + ub_app(b, xn); + ub_app(b, "\n{\n"); + ub_app(b, data); + ub_app(b, "\n}\n\n"); +} + +static char *transcode_class(CBMArena *arena, const char *cs, const char *ce) { + UdlBuf b; + ub_init(&b, arena); + if (!b.buf) + return NULL; + emit_header(&b, cs, ce); + const char *p = cs; + while (p < ce) { + p = skip_ws(p, ce); + if (p >= ce || *p != '<') { + if (p < ce) + p++; + continue; + } + if (sw(p, ce, "")) { + const char *gt = find_s(p, ce, ">"); + if (!gt) + break; + const char *me = find_s(gt + 1, ce, ""); + if (!me) { + p = gt + 1; + continue; + } + emit_method(&b, p, me + strlen("")); + p = me + strlen(""); + continue; + } + if (sw(p, ce, ""); + if (!gt) + break; + const char *pe = find_s(gt + 1, ce, ""); + if (!pe) { + p = gt + 1; + continue; + } + emit_property(&b, p, pe + strlen("")); + p = pe + strlen(""); + continue; + } + if (sw(p, ce, ""); + if (!gt) + break; + if (is_self_closing(p, gt)) { + p = gt + 1; + continue; + } + const char *pe = find_s(gt + 1, ce, ""); + if (!pe) { + p = gt + 1; + continue; + } + emit_parameter(&b, p, pe + strlen("")); + p = pe + strlen(""); + continue; + } + if (sw(p, ce, "")) { + const char *gt = find_s(p, ce, ">"); + if (!gt) + break; + const char *ie = find_s(gt + 1, ce, ""); + if (!ie) { + p = gt + 1; + continue; + } + emit_index(&b, p, ie + strlen("")); + p = ie + strlen(""); + continue; + } + if (sw(p, ce, "")) { + const char *gt = find_s(p, ce, ">"); + if (!gt) + break; + const char *xe = find_s(gt + 1, ce, ""); + if (!xe) { + p = gt + 1; + continue; + } + emit_xdata(&b, p, xe + strlen("")); + p = xe + strlen(""); + continue; + } + p = skip_tag(p, ce); + } + ub_app(&b, "}\n"); + return b.buf; +} + +char **cbm_iris_export_to_udl(CBMArena *arena, const char *xml, int xml_len, int *class_count) { + if (class_count) + *class_count = 0; + if (!arena || !xml || xml_len <= 0) + return NULL; + const char *end = xml + xml_len; + if (!find_s(xml, end, EXPORT_MARKER)) + return NULL; + char *results[MAX_CLASSES]; + int count = 0; + const char *p = xml; + while (p < end && count < MAX_CLASSES) { + const char *co = find_s(p, end, ""); + if (!gt) + break; + const char *cc = find_s(gt + 1, end, ""); + if (!cc) + break; + char *udl = transcode_class(arena, co, cc); + if (udl && udl[0]) + results[count++] = udl; + p = cc + strlen(""); + } + if (!count) + return NULL; + char **arr = (char **)cbm_arena_alloc(arena, (size_t)(count + 1) * sizeof(char *)); + if (!arr) + return NULL; + for (int i = 0; i < count; i++) + arr[i] = results[i]; + arr[count] = NULL; + if (class_count) + *class_count = count; + return arr; +} diff --git a/internal/cbm/iris_export_xml.h b/internal/cbm/iris_export_xml.h new file mode 100644 index 000000000..30c289ff9 --- /dev/null +++ b/internal/cbm/iris_export_xml.h @@ -0,0 +1,19 @@ +#pragma once +#include "arena.h" + +/* + * IRIS Studio Export XML transcoder. + * + * Converts XML files to equivalent UDL text so + * they can be fed to the existing ObjectScript UDL extraction pipeline. + * The XML-to-UDL mapping is 1:1; no new extraction logic is needed. + * + * One Export file may contain multiple blocks. Each produces a + * separate UDL string. The caller iterates the returned array and calls + * cbm_extract_file(..., CBM_LANG_OBJECTSCRIPT_UDL, ...) for each entry. + * + * Returns arena-allocated array of NUL-terminated UDL strings, or NULL + * if the file is not an Export file or parsing fails gracefully. + * *class_count is set to the number of classes found (0 on failure). + */ +char **cbm_iris_export_to_udl(CBMArena *arena, const char *xml, int xml_len, int *class_count); diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index e328e8d88..bcc4b5548 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -165,6 +165,8 @@ extern const TSLanguage *tree_sitter_soql(void); extern const TSLanguage *tree_sitter_sosl(void); extern const TSLanguage *tree_sitter_pine(void); extern const TSLanguage *tree_sitter_mojo(void); +extern const TSLanguage *tree_sitter_objectscript_udl(void); +extern const TSLanguage *tree_sitter_objectscript_routine(void); // -- Empty sentinel -- static const char *empty_types[] = {NULL}; @@ -1612,6 +1614,24 @@ static const char *mojo_branch_types[] = {"if_statement", NULL}; static const char *mojo_var_types[] = {"assignment", NULL}; static const char *mojo_assign_types[] = {"assignment", "augmented_assignment", NULL}; + +// InterSystems ObjectScript. Node names verified against +// intersystems/tree-sitter-objectscript grammar. +static const char *objectscript_udl_func_types[] = {"method", "classmethod", "query", NULL}; +static const char *objectscript_udl_class_types[] = {"class_definition", NULL}; +static const char *objectscript_udl_field_types[] = { + "property", "parameter", "index", "trigger", "xdata", "storage", "foreignkey", NULL}; +static const char *objectscript_udl_call_types[] = {"class_method_call", "method_call", + "relative_dot_method", "macro", NULL}; +static const char *objectscript_udl_module_types[] = {"source_file", NULL}; +/* Branching nodes for cyclomatic complexity (verified against grammar node-types) */ +static const char *objectscript_udl_branch_types[] = { + "command_if", "command_for", "command_while", "elseif_block", "catch_block", NULL}; + +static const char *objectscript_routine_func_types[] = {"tag", NULL}; +static const char *objectscript_routine_call_types[] = {"extrinsic_function", "routine_tag_call", + NULL}; +static const char *objectscript_routine_module_types[] = {"source_file", NULL}; // ==================== SPEC TABLE ==================== static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = { @@ -2593,6 +2613,29 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = { mojo_branch_types, mojo_var_types, mojo_assign_types, empty_types, NULL, empty_types, NULL, NULL, tree_sitter_mojo, NULL}, + // CBM_LANG_OBJECTSCRIPT_UDL — InterSystems ObjectScript class (.cls) UDL. + // intersystems/tree-sitter-objectscript. + [CBM_LANG_OBJECTSCRIPT_UDL] = {CBM_LANG_OBJECTSCRIPT_UDL, objectscript_udl_func_types, + objectscript_udl_class_types, objectscript_udl_field_types, + objectscript_udl_module_types, objectscript_udl_call_types, + empty_types, empty_types, objectscript_udl_branch_types, + empty_types, empty_types, empty_types, NULL, empty_types, NULL, + NULL, tree_sitter_objectscript_udl, NULL}, + + // CBM_LANG_OBJECTSCRIPT_ROUTINE — InterSystems ObjectScript routine (.mac/.int/.rtn/.inc). + [CBM_LANG_OBJECTSCRIPT_ROUTINE] = {CBM_LANG_OBJECTSCRIPT_ROUTINE, + objectscript_routine_func_types, empty_types, empty_types, + objectscript_routine_module_types, + objectscript_routine_call_types, empty_types, empty_types, + empty_types, empty_types, empty_types, empty_types, NULL, + empty_types, NULL, NULL, tree_sitter_objectscript_routine, + NULL}, + + // CBM_LANG_OBJECTSCRIPT_EXPORT — Studio Export XML. No grammar row: the + // pipeline transcodes Export XML to UDL (iris_export_xml.c) and re-extracts + // each class as CBM_LANG_OBJECTSCRIPT_UDL, so this language never reaches + // cbm_lang_spec()/cbm_ts_language() directly. Left as a zero spec. + }; _Static_assert(sizeof(lang_specs) / sizeof(lang_specs[0]) == CBM_LANG_COUNT, diff --git a/internal/cbm/macro_table.c b/internal/cbm/macro_table.c new file mode 100644 index 000000000..1707f37cd --- /dev/null +++ b/internal/cbm/macro_table.c @@ -0,0 +1,251 @@ +#include "macro_table.h" +#include "arena.h" +#include +#include +#include +#include + +static const struct { + const char *name; + int param_count; + const char *callee; +} SYSTEM_MACROS[] = {{"OK", 0, NULL}, + {"ISERR", 1, "%SYSTEM.Status.IsError"}, + {"ISOK", 1, "%SYSTEM.Status.IsOK"}, + {"GETERRORTEXT", 1, "%SYSTEM.Status.GetErrorText"}, + {"ADDSC", 2, "%SYSTEM.Status.AppendStatus"}, + {"ThrowStatus", 1, "%SYSTEM.Status.ThrowStatus"}, + {"ThrowOnError", 1, "%SYSTEM.Status.ThrowStatus"}, + {"ERROR", 2, "%SYSTEM.Status.Error"}, + {"NULLOREF", 0, NULL}, + {"LISTBUILD", -1, NULL}, + {"LISTGET", 2, NULL}, + {"LISTNEXT", 3, NULL}, + {"LISTLENGTH", 1, NULL}, + {"SORTBEGIN", 1, NULL}, + {"SORTEND", 0, NULL}, + {"AUDITSTART", 3, "%SYSTEM.Audit.Event"}, + {"logoutput", 1, NULL}, + {"objExists", 1, NULL}, + {"traceStatus", 1, NULL}, + {NULL, 0, NULL}}; + +void cbm_macro_table_free(CBMMacroTable *t) { + if (!t) + return; + cbm_arena_destroy(&t->arena); + free(t); +} + +void cbm_macro_table_init_system(CBMMacroTable *t) { + t->count = 0; + for (int i = 0; SYSTEM_MACROS[i].name; i++) { + if (t->count >= CBM_MACRO_TABLE_CAP) + break; + CBMMacroEntry *e = &t->entries[t->count++]; + e->name = SYSTEM_MACROS[i].name; + e->param_count = SYSTEM_MACROS[i].param_count; + e->expansion = NULL; + e->resolved_callee = SYSTEM_MACROS[i].callee; + for (int p = 0; p < CBM_MACRO_MAX_PARAMS; p++) + e->param_names[p] = NULL; + } +} + +void cbm_macro_table_add(CBMMacroTable *t, CBMArena *arena, const char *name, int param_count, + const char **param_names, const char *expansion, + const char *resolved_callee) { + if (t->count >= CBM_MACRO_TABLE_CAP || !name) + return; + for (int i = 0; i < t->count; i++) { + if (strcasecmp(t->entries[i].name, name) == 0) + return; + } + CBMMacroEntry *e = &t->entries[t->count++]; + e->name = cbm_arena_strdup(arena, name); + e->param_count = param_count; + e->expansion = expansion ? cbm_arena_strdup(arena, expansion) : NULL; + e->resolved_callee = resolved_callee ? cbm_arena_strdup(arena, resolved_callee) : NULL; + for (int p = 0; p < CBM_MACRO_MAX_PARAMS; p++) { + e->param_names[p] = + (param_names && p < param_count) ? cbm_arena_strdup(arena, param_names[p]) : NULL; + } +} + +const CBMMacroEntry *cbm_macro_table_find(const CBMMacroTable *t, const char *name) { + if (!t || !name) + return NULL; + for (int i = 0; i < t->count; i++) { + if (strcasecmp(t->entries[i].name, name) == 0) + return &t->entries[i]; + } + return NULL; +} + +void cbm_parse_inc_file(CBMMacroTable *t, CBMArena *arena, const char *content) { + if (!content) + return; + const char *line = content; + while (*line) { + const char *end = strchr(line, '\n'); + if (!end) + end = line + strlen(line); + + const char *p = line; + while (*p == ' ' || *p == '\t') + p++; + + if (strncmp(p, "#define", 7) == 0 && (p[7] == ' ' || p[7] == '\t')) { + p += 8; + while (*p == ' ' || *p == '\t') + p++; + + const char *name_start = p; + while (*p && *p != '(' && *p != ' ' && *p != '\t' && p < end) + p++; + if (p == name_start) + goto next_line; + + char name[256]; + int nlen = (int)(p - name_start); + if (nlen >= (int)sizeof(name)) + goto next_line; + memcpy(name, name_start, nlen); + name[nlen] = '\0'; + + int param_count = -1; + char param_names_buf[CBM_MACRO_MAX_PARAMS][64]; + const char *param_name_ptrs[CBM_MACRO_MAX_PARAMS] = {NULL}; + + if (*p == '(') { + param_count = 0; + p++; + while (*p && *p != ')' && p < end) { + while (*p == ' ' || *p == '\t') + p++; + if (*p == ')') + break; + const char *pn_start = p; + while (*p && *p != ',' && *p != ')' && p < end) + p++; + int plen = (int)(p - pn_start); + while (plen > 0 && (pn_start[plen - 1] == ' ' || pn_start[plen - 1] == '\t')) + plen--; + if (plen > 0 && param_count < CBM_MACRO_MAX_PARAMS) { + memcpy(param_names_buf[param_count], pn_start, plen < 63 ? plen : 63); + param_names_buf[param_count][plen < 63 ? plen : 63] = '\0'; + param_name_ptrs[param_count] = param_names_buf[param_count]; + param_count++; + } + if (*p == ',') + p++; + } + if (*p == ')') + p++; + } + + while (*p == ' ' || *p == '\t') + p++; + int explen = (int)(end - p); + while (explen > 0 && + (p[explen - 1] == '\r' || p[explen - 1] == ' ' || p[explen - 1] == '\t')) + explen--; + char *expansion = NULL; + if (explen > 0) { + expansion = cbm_arena_strndup(arena, p, explen); + } + + cbm_macro_table_add(t, arena, name, param_count, + param_count > 0 ? param_name_ptrs : NULL, expansion, NULL); + } + + next_line: + if (!*end) + break; + line = end + 1; + } +} + +char *cbm_macro_expand(CBMArena *arena, const CBMMacroEntry *entry, const char **args, + int arg_count) { + if (!entry || !entry->expansion) + return NULL; + const char *tmpl = entry->expansion; + char buf[1024]; + int out = 0; + const char *p = tmpl; + while (*p && out < (int)sizeof(buf) - 1) { + if (*p == '%') { + bool matched = false; + for (int i = 0; i < entry->param_count && i < CBM_MACRO_MAX_PARAMS; i++) { + if (!entry->param_names[i]) + continue; + int pnlen = (int)strlen(entry->param_names[i]); + if (strncasecmp(p, entry->param_names[i], pnlen) == 0) { + const char *arg = (args && i < arg_count) ? args[i] : ""; + int alen = (int)strlen(arg); + if (out + alen < (int)sizeof(buf) - 1) { + memcpy(buf + out, arg, alen); + out += alen; + } + p += pnlen; + matched = true; + break; + } + } + if (!matched) + buf[out++] = *p++; + } else { + buf[out++] = *p++; + } + } + buf[out] = '\0'; + return cbm_arena_strdup(arena, buf); +} + +char *cbm_macro_extract_callee(CBMArena *arena, const char *expansion) { + if (!expansion) + return NULL; + + const char *p = strstr(expansion, "##class("); + if (p) { + p += 8; + const char *cls_end = strchr(p, ')'); + if (!cls_end) + return NULL; + int clen = (int)(cls_end - p); + const char *dot = cls_end + 1; + if (*dot != '.') + return NULL; + dot++; + const char *method_start = dot; + const char *method_end = method_start; + while (*method_end && *method_end != '(' && *method_end != ' ') + method_end++; + int mlen = (int)(method_end - method_start); + if (clen <= 0 || mlen <= 0) + return NULL; + return cbm_arena_sprintf(arena, "%.*s.%.*s", clen, p, mlen, method_start); + } + + p = strstr(expansion, "$$"); + if (p && p[2] != '$') { + p += 2; + const char *tag_end = p; + while (*tag_end && *tag_end != '^' && *tag_end != '(' && *tag_end != ' ') + tag_end++; + if (*tag_end == '^') { + const char *rtn = tag_end + 1; + const char *rtn_end = rtn; + while (*rtn_end && *rtn_end != '(' && *rtn_end != ' ') + rtn_end++; + int tlen = (int)(tag_end - p); + int rlen = (int)(rtn_end - rtn); + if (tlen > 0 && rlen > 0) { + return cbm_arena_sprintf(arena, "%.*s^%.*s", tlen, p, rlen, rtn); + } + } + } + + return NULL; +} diff --git a/internal/cbm/macro_table.h b/internal/cbm/macro_table.h new file mode 100644 index 000000000..b6f82ab1c --- /dev/null +++ b/internal/cbm/macro_table.h @@ -0,0 +1,47 @@ +#pragma once +#include +#include "arena.h" + +#define CBM_MACRO_MAX_PARAMS 4 +#define CBM_MACRO_TABLE_CAP 4096 + +typedef struct { + const char *name; + int param_count; + const char *param_names[CBM_MACRO_MAX_PARAMS]; + const char *expansion; + const char *resolved_callee; +} CBMMacroEntry; + +typedef struct CBMMacroTable { + CBMMacroEntry entries[CBM_MACRO_TABLE_CAP]; + int count; + CBMArena arena; +} CBMMacroTable; + +// Add an entry. Silently drops on overflow. +void cbm_macro_table_add(CBMMacroTable *t, CBMArena *arena, const char *name, int param_count, + const char **param_names, const char *expansion, + const char *resolved_callee); + +// Look up by name. Returns NULL if not found. +const CBMMacroEntry *cbm_macro_table_find(const CBMMacroTable *t, const char *name); + +// Parse a single .inc file content into the table (arena-allocated strings). +void cbm_parse_inc_file(CBMMacroTable *t, CBMArena *arena, const char *content); + +// Expand a macro call: substitute args into expansion text. +// Returns arena-allocated expanded text, or NULL if no expansion. +char *cbm_macro_expand(CBMArena *arena, const CBMMacroEntry *entry, const char **args, + int arg_count); + +// Extract a callee name from expanded text (looks for ##class(X).Method or $$Label^Routine). +// Returns arena-allocated "X.Method" or "Label^Routine", or NULL. +char *cbm_macro_extract_callee(CBMArena *arena, const char *expansion); + +// Allocate and populate a new table with the hardcoded system macros. +// Caller owns the table (stack or heap). +void cbm_macro_table_init_system(CBMMacroTable *t); + +// Destroy the arena inside t and free t itself. NULL-safe. +void cbm_macro_table_free(CBMMacroTable *t); diff --git a/internal/cbm/vendored/grammars/objectscript_routine/scanner.c b/internal/cbm/vendored/grammars/objectscript_routine/scanner.c index 771db9eef..c64ac9e42 100644 --- a/internal/cbm/vendored/grammars/objectscript_routine/scanner.c +++ b/internal/cbm/vendored/grammars/objectscript_routine/scanner.c @@ -4,122 +4,125 @@ // Keep this in sync with grammar externals. enum TokenType { - COMPILED_HEADER = OBJECTSCRIPT_CORE_TOKEN_TYPE_MAX, - RTN_DOT, + COMPILED_HEADER = OBJECTSCRIPT_CORE_TOKEN_TYPE_MAX, + RTN_DOT, }; struct ObjectScript_Routine_Scanner { - struct ObjectScript_Core_Scanner core_scanner; + struct ObjectScript_Core_Scanner core_scanner; }; static bool lex_rtn_dot(TSLexer *lexer) { - lexer->mark_end(lexer); - if (lexer->get_column(lexer) != 0) return false; - if (lexer->lookahead != '.') return false; - - advance(lexer); - - // Only match a line that is exactly "." with no trailing content. - if (lexer->eof(lexer) || lexer->lookahead == '\n' || lexer->lookahead == '\r') { lexer->mark_end(lexer); - lexer->result_symbol = RTN_DOT; - return true; - } + if (lexer->get_column(lexer) != 0) + return false; + if (lexer->lookahead != '.') + return false; - return false; + advance(lexer); + + // Only match a line that is exactly "." with no trailing content. + if (lexer->eof(lexer) || lexer->lookahead == '\n' || lexer->lookahead == '\r') { + lexer->mark_end(lexer); + lexer->result_symbol = RTN_DOT; + return true; + } + + return false; } static bool lex_compiled_header(TSLexer *lexer) { - lexer->mark_end(lexer); - while(!lexer->eof(lexer) && lexer->lookahead != '\n') { - advance(lexer); - } - if (lexer->lookahead != '\n') return false; - - advance(lexer); - if (lexer->lookahead != '%') return false; - advance(lexer); - if (lexer->lookahead != 'R') return false; - advance(lexer); - if (lexer->lookahead != 'O') return false; - advance(lexer); - - lexer->result_symbol = COMPILED_HEADER; - int newline_count = 0; - while(newline_count < 2 && !lexer->eof(lexer)) { - if (lexer->lookahead == '\n') { - newline_count++; + lexer->mark_end(lexer); + while (!lexer->eof(lexer) && lexer->lookahead != '\n') { + advance(lexer); } + if (lexer->lookahead != '\n') + return false; + advance(lexer); - } - lexer->mark_end(lexer); - return true; + if (lexer->lookahead != '%') + return false; + advance(lexer); + if (lexer->lookahead != 'R') + return false; + advance(lexer); + if (lexer->lookahead != 'O') + return false; + advance(lexer); + + lexer->result_symbol = COMPILED_HEADER; + int newline_count = 0; + while (newline_count < 2 && !lexer->eof(lexer)) { + if (lexer->lookahead == '\n') { + newline_count++; + } + advance(lexer); + } + lexer->mark_end(lexer); + return true; } static bool scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { - struct ObjectScript_Routine_Scanner *scanner = - (struct ObjectScript_Routine_Scanner *)payload; + struct ObjectScript_Routine_Scanner *scanner = (struct ObjectScript_Routine_Scanner *)payload; + + // Tree-sitter marks all terminals as valid during error recovery. + if (valid_symbols[SENTINEL]) { + return false; + } + + if (ObjectScript_Core_Scanner_scan(&scanner->core_scanner, lexer, valid_symbols)) { + return true; + } + + if (scanner->core_scanner.is_rtn_dot) { + scanner->core_scanner.is_rtn_dot = false; + lexer->mark_end(lexer); + lexer->result_symbol = RTN_DOT; + return true; + } + + if (valid_symbols[COMPILED_HEADER] && lex_compiled_header(lexer)) { + return true; + } + + if (valid_symbols[RTN_DOT] && lex_rtn_dot(lexer)) { + return true; + } - // Tree-sitter marks all terminals as valid during error recovery. - if (valid_symbols[SENTINEL]) { return false; - } - - if (ObjectScript_Core_Scanner_scan(&scanner->core_scanner, lexer, - valid_symbols)) { - return true; - } - - if (scanner->core_scanner.is_rtn_dot) { - scanner->core_scanner.is_rtn_dot = false; - lexer->mark_end(lexer); - lexer->result_symbol = RTN_DOT; - return true; - } - - if (valid_symbols[COMPILED_HEADER] && lex_compiled_header(lexer)) { - return true; - } - - if (valid_symbols[RTN_DOT] && lex_rtn_dot(lexer)) { - return true; - } - - return false; } void *tree_sitter_objectscript_routine_external_scanner_create() { - struct ObjectScript_Routine_Scanner *scanner = - (struct ObjectScript_Routine_Scanner *)calloc( - 1, sizeof(struct ObjectScript_Routine_Scanner)); - // scanner->saw_compiled_header = false; - // scanner->saw_routine_header = false; - ObjectScript_Core_Scanner_init(&scanner->core_scanner); - scanner->core_scanner.column1_statement_mode = false; - scanner->core_scanner.routine_token_mode = true; - return scanner; + struct ObjectScript_Routine_Scanner *scanner = (struct ObjectScript_Routine_Scanner *)calloc( + 1, sizeof(struct ObjectScript_Routine_Scanner)); + // scanner->saw_compiled_header = false; + // scanner->saw_routine_header = false; + ObjectScript_Core_Scanner_init(&scanner->core_scanner); + scanner->core_scanner.column1_statement_mode = false; + scanner->core_scanner.routine_token_mode = true; + return scanner; } -bool tree_sitter_objectscript_routine_external_scanner_scan( - void *payload, TSLexer *lexer, const bool *valid_symbols) { - return scan(payload, lexer, valid_symbols); +bool tree_sitter_objectscript_routine_external_scanner_scan(void *payload, TSLexer *lexer, + const bool *valid_symbols) { + return scan(payload, lexer, valid_symbols); } -unsigned tree_sitter_objectscript_routine_external_scanner_serialize(void *payload, - char *buffer) { - struct ObjectScript_Routine_Scanner *scanner = - (struct ObjectScript_Routine_Scanner *)payload; - memcpy(buffer, scanner, sizeof(struct ObjectScript_Routine_Scanner)); - return sizeof(struct ObjectScript_Routine_Scanner); +unsigned tree_sitter_objectscript_routine_external_scanner_serialize(void *payload, char *buffer) { + struct ObjectScript_Routine_Scanner *scanner = (struct ObjectScript_Routine_Scanner *)payload; + memcpy(buffer, scanner, sizeof(struct ObjectScript_Routine_Scanner)); + return sizeof(struct ObjectScript_Routine_Scanner); } -void tree_sitter_objectscript_routine_external_scanner_deserialize( - void *payload, const char *buffer, unsigned length) { - memcpy(payload, buffer, length); +void tree_sitter_objectscript_routine_external_scanner_deserialize(void *payload, + const char *buffer, + unsigned length) { + if (length == 0) + return; + memcpy(payload, buffer, length); } void tree_sitter_objectscript_routine_external_scanner_destroy(void *payload) { - struct ObjectScript_Routine_Scanner *scanner = - (struct ObjectScript_Routine_Scanner *)payload; - free(scanner); + struct ObjectScript_Routine_Scanner *scanner = (struct ObjectScript_Routine_Scanner *)payload; + free(scanner); } diff --git a/internal/cbm/vendored/grammars/objectscript_udl/scanner.c b/internal/cbm/vendored/grammars/objectscript_udl/scanner.c index fb1d01322..276f68263 100644 --- a/internal/cbm/vendored/grammars/objectscript_udl/scanner.c +++ b/internal/cbm/vendored/grammars/objectscript_udl/scanner.c @@ -5,125 +5,118 @@ // There is no way to extend enums, so keep this in sync with base.h // All new entries should be appended at the bottom of the list enum TokenType { - EXTERNAL_METHOD_BODY_CONTENT = OBJECTSCRIPT_CORE_TOKEN_TYPE_MAX, - IRIS_USERNAME, + EXTERNAL_METHOD_BODY_CONTENT = OBJECTSCRIPT_CORE_TOKEN_TYPE_MAX, + IRIS_USERNAME, }; struct ObjectScript_Udl_Scanner { - char in_body; - struct ObjectScript_Core_Scanner core_scanner; + char in_body; + struct ObjectScript_Core_Scanner core_scanner; }; -static bool lex_fenced_text(TSLexer *lexer, - enum TokenType desired_symbol, char l_delim, +static bool lex_fenced_text(TSLexer *lexer, enum TokenType desired_symbol, char l_delim, char r_delim) { - int leftRightDiff = 1; - while (!lexer->eof(lexer)) { - if (lexer->lookahead == r_delim) { - leftRightDiff -= 1; - } else if (lexer->lookahead == l_delim) { - leftRightDiff += 1; + int leftRightDiff = 1; + while (!lexer->eof(lexer)) { + if (lexer->lookahead == r_delim) { + leftRightDiff -= 1; + } else if (lexer->lookahead == l_delim) { + leftRightDiff += 1; + } + if (leftRightDiff == 0) { + lexer->result_symbol = desired_symbol; + return true; + } + advance(lexer); } - if (leftRightDiff == 0) { - lexer->result_symbol = desired_symbol; - return true; - } - advance(lexer); - } - return false; + return false; } /// This is the interesting function. The rest is infrastructure static bool scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { - // struct Scanner *scanner = (struct Scanner *)payload; - // - // Tree sitter will mark all terminals as valid on error - // The sentinel should never be valid in a good parse, so this ensures - // we are not in error recovery mode - // printf("Lookahead: '%c'. Col %d. Tag is valid? %d", lexer->lookahead, - // lexer->get_column(lexer), valid_symbols[TAG]); - // printf("lexer->lookahead: '%c'\n", lexer->lookahead); - // printf("====\n"); - // debug_enum(valid_symbols); - // printf("====\n"); - if (valid_symbols[SENTINEL]) { - // printf("Thats not good!\n"); - // // TODO: ERROR RECOVERY MODE ?? Why would we be better than TS's - // default? if (iswspace(lexer->lookahead)) { - // skip(lexer); - // eat_whitespace(lexer); - // lexer->result_symbol = _WHITESPACE; - // return true; - // } - return false; - } - if (valid_symbols[EXTERNAL_METHOD_BODY_CONTENT]) { - // A valid method_body is one that is whose text fences - // are evenly balanced (so far only { }) - // e.g. VALID: {{{ [^{}]* }}} INVALID: {{{ [^{}]* } - return lex_fenced_text(lexer, EXTERNAL_METHOD_BODY_CONTENT, '{', - '}'); - } - - if (valid_symbols[IRIS_USERNAME] && !(lexer->lookahead=='}' || lexer->lookahead=='@' || lexer->lookahead=='*')) { - int count=0; - lexer->mark_end(lexer); - while(!lexer->eof(lexer) && !(lexer->lookahead=='}' || lexer->lookahead=='@' || lexer->lookahead=='*') && count<160 ) { - count++; - advance(lexer); + // struct Scanner *scanner = (struct Scanner *)payload; + // + // Tree sitter will mark all terminals as valid on error + // The sentinel should never be valid in a good parse, so this ensures + // we are not in error recovery mode + // printf("Lookahead: '%c'. Col %d. Tag is valid? %d", lexer->lookahead, + // lexer->get_column(lexer), valid_symbols[TAG]); + // printf("lexer->lookahead: '%c'\n", lexer->lookahead); + // printf("====\n"); + // debug_enum(valid_symbols); + // printf("====\n"); + if (valid_symbols[SENTINEL]) { + // printf("Thats not good!\n"); + // // TODO: ERROR RECOVERY MODE ?? Why would we be better than TS's + // default? if (iswspace(lexer->lookahead)) { + // skip(lexer); + // eat_whitespace(lexer); + // lexer->result_symbol = _WHITESPACE; + // return true; + // } + return false; } - - - if(lexer->lookahead=='}') { - lexer->mark_end(lexer); - lexer->result_symbol = IRIS_USERNAME; - return true; + if (valid_symbols[EXTERNAL_METHOD_BODY_CONTENT]) { + // A valid method_body is one that is whose text fences + // are evenly balanced (so far only { }) + // e.g. VALID: {{{ [^{}]* }}} INVALID: {{{ [^{}]* } + return lex_fenced_text(lexer, EXTERNAL_METHOD_BODY_CONTENT, '{', '}'); } - else { - return false; + + if (valid_symbols[IRIS_USERNAME] && + !(lexer->lookahead == '}' || lexer->lookahead == '@' || lexer->lookahead == '*')) { + int count = 0; + lexer->mark_end(lexer); + while (!lexer->eof(lexer) && + !(lexer->lookahead == '}' || lexer->lookahead == '@' || lexer->lookahead == '*') && + count < 160) { + count++; + advance(lexer); + } + + if (lexer->lookahead == '}') { + lexer->mark_end(lexer); + lexer->result_symbol = IRIS_USERNAME; + return true; + } else { + return false; + } } - - } - - struct ObjectScript_Udl_Scanner *scanner = - (struct ObjectScript_Udl_Scanner *)payload; - return ObjectScript_Core_Scanner_scan(&scanner->core_scanner, lexer, - valid_symbols); + struct ObjectScript_Udl_Scanner *scanner = (struct ObjectScript_Udl_Scanner *)payload; + return ObjectScript_Core_Scanner_scan(&scanner->core_scanner, lexer, valid_symbols); } void *tree_sitter_objectscript_udl_external_scanner_create() { - struct ObjectScript_Udl_Scanner *scanner = - (struct ObjectScript_Udl_Scanner *)calloc( - 1, sizeof(struct ObjectScript_Udl_Scanner)); - scanner->in_body = 0; - ObjectScript_Core_Scanner_init(&scanner->core_scanner); - scanner->core_scanner.column1_statement_mode = false; - return scanner; + struct ObjectScript_Udl_Scanner *scanner = + (struct ObjectScript_Udl_Scanner *)calloc(1, sizeof(struct ObjectScript_Udl_Scanner)); + scanner->in_body = 0; + ObjectScript_Core_Scanner_init(&scanner->core_scanner); + scanner->core_scanner.column1_statement_mode = false; + return scanner; } -bool tree_sitter_objectscript_udl_external_scanner_scan( - void *payload, TSLexer *lexer, const bool *valid_symbols) { - return scan(payload, lexer, valid_symbols); +bool tree_sitter_objectscript_udl_external_scanner_scan(void *payload, TSLexer *lexer, + const bool *valid_symbols) { + return scan(payload, lexer, valid_symbols); } -unsigned tree_sitter_objectscript_udl_external_scanner_serialize(void *payload, - char *buffer) { - struct ObjectScript_Udl_Scanner *scanner = - (struct ObjectScript_Udl_Scanner *)payload; - memcpy(buffer, scanner, sizeof(struct ObjectScript_Udl_Scanner)); - return sizeof(struct ObjectScript_Udl_Scanner); +unsigned tree_sitter_objectscript_udl_external_scanner_serialize(void *payload, char *buffer) { + struct ObjectScript_Udl_Scanner *scanner = (struct ObjectScript_Udl_Scanner *)payload; + memcpy(buffer, scanner, sizeof(struct ObjectScript_Udl_Scanner)); + return sizeof(struct ObjectScript_Udl_Scanner); } -void tree_sitter_objectscript_udl_external_scanner_deserialize( - void *payload, const char *buffer, unsigned length) { - // This one is a bit funky. - // length includes the sizeof(struct Scanner) and the structs it points to - memcpy(payload, buffer, length); +void tree_sitter_objectscript_udl_external_scanner_deserialize(void *payload, const char *buffer, + unsigned length) { + if (length == 0) + return; + // This one is a bit funky. + // length includes the sizeof(struct Scanner) and the structs it points to + memcpy(payload, buffer, length); } void tree_sitter_objectscript_udl_external_scanner_destroy(void *payload) { - struct ObjectScript_Udl_Scanner *scanner = - (struct ObjectScript_Udl_Scanner *)payload; - free(scanner); + struct ObjectScript_Udl_Scanner *scanner = (struct ObjectScript_Udl_Scanner *)payload; + free(scanner); } diff --git a/src/discover/discover.c b/src/discover/discover.c index 785640713..56c1594cc 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -621,6 +621,28 @@ static CBMLanguage detect_file_language(const char *entry_name, const char *abs_ if (dot && strcmp(dot, ".m") == 0) { lang = cbm_disambiguate_m(abs_path); } + /* Special: .cls is shared by ObjectScript UDL and Apex */ + if (dot && strcmp(dot, ".cls") == 0) { + lang = cbm_disambiguate_cls(abs_path); + } + /* Special: .inc is shared by BitBake and ObjectScript include files */ + if (dot && strcmp(dot, ".inc") == 0) { + lang = cbm_disambiguate_inc(abs_path); + } + /* Special: ObjectScript Studio Export XML () is + * detected by content; otherwise .xml stays XML. */ + if (lang == CBM_LANG_XML) { + FILE *xf = fopen(abs_path, "r"); + if (xf) { + char xbuf[CBM_SZ_256]; + size_t xn = fread(xbuf, SKIP_ONE, sizeof(xbuf) - SKIP_ONE, xf); + (void)fclose(xf); + xbuf[xn] = '\0'; + if (strstr(xbuf, "", + * otherwise CBM_LANG_APEX. On read failure, defaults to CBM_LANG_APEX. */ +CBMLanguage cbm_disambiguate_cls(const char *path); + +/* Disambiguate .inc files by reading first 4KB of content. + * Returns CBM_LANG_OBJECTSCRIPT_ROUTINE if it looks like an ObjectScript + * include (a "ROUTINE " header), otherwise CBM_LANG_BITBAKE. + * On read failure, defaults to CBM_LANG_BITBAKE. */ +CBMLanguage cbm_disambiguate_inc(const char *path); + /* ── Gitignore pattern matching ──────────────────────────────────── */ typedef struct cbm_gitignore cbm_gitignore_t; diff --git a/src/discover/language.c b/src/discover/language.c index 65b3cf0ce..4451b1b26 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -268,9 +268,17 @@ static const ext_entry_t EXT_TABLE[] = { {"WORKSPACE", CBM_LANG_STARLARK}, {"WORKSPACE.bazel", CBM_LANG_STARLARK}, - /* BitBake include fragments — `require/include foo.inc` target files. */ + /* BitBake include fragments — `require/include foo.inc` target files. + * NOTE: .inc is also used by ObjectScript include (macro) files; the + * ambiguity is resolved by content in cbm_disambiguate_inc(). */ {".inc", CBM_LANG_BITBAKE}, + /* InterSystems ObjectScript routines (.mac/.int/.rtn unambiguous; .cls is + * shared with Apex and resolved by content in cbm_disambiguate_cls()). */ + {".mac", CBM_LANG_OBJECTSCRIPT_ROUTINE}, + {".int", CBM_LANG_OBJECTSCRIPT_ROUTINE}, + {".rtn", CBM_LANG_OBJECTSCRIPT_ROUTINE}, + /* Vue */ {".vue", CBM_LANG_VUE}, @@ -839,6 +847,9 @@ static const char *LANG_NAMES[CBM_LANG_COUNT] = { [CBM_LANG_SOQL] = "SOQL", [CBM_LANG_SOSL] = "SOSL", [CBM_LANG_MOJO] = "Mojo", + [CBM_LANG_OBJECTSCRIPT_UDL] = "ObjectScript UDL", + [CBM_LANG_OBJECTSCRIPT_ROUTINE] = "ObjectScript Routine", + [CBM_LANG_OBJECTSCRIPT_EXPORT] = "ObjectScript Export XML", }; @@ -1030,3 +1041,84 @@ CBMLanguage cbm_disambiguate_m(const char *path) { return CBM_LANG_MATLAB; } + +/* Disambiguate .cls files: shared by InterSystems ObjectScript UDL and + * Salesforce Apex. ObjectScript class files begin with a line of the form + * "Class ...". Defaults to Apex on any doubt. */ +CBMLanguage cbm_disambiguate_cls(const char *path) { + if (!path) { + return CBM_LANG_APEX; + } + + FILE *f = cbm_fopen(path, "r"); + if (!f) { + return CBM_LANG_APEX; + } + + char buf[CBM_SZ_4K + SKIP_ONE]; + size_t n = fread(buf, SKIP_ONE, CBM_SZ_4K, f); + buf[n] = '\0'; + (void)fclose(f); + + const char *line = buf; + while (*line) { + if (strncmp(line, "Class ", SLEN("Class ")) == 0 && + isupper((unsigned char)line[SLEN("Class ")])) { + return CBM_LANG_OBJECTSCRIPT_UDL; + } + const char *nl = strchr(line, '\n'); + if (!nl) { + break; + } + line = nl + SKIP_ONE; + } + return CBM_LANG_APEX; +} + +/* Disambiguate .inc files: shared by BitBake include fragments and + * InterSystems ObjectScript include (macro) files. ObjectScript .inc files are + * predominantly macro definitions ("#define NAME ..." / "#def1arg NAME ..."); + * some also carry a "ROUTINE " header. The macro-preprocessor directives + * are the strongest signal because that is the primary content of an .inc file, + * whereas BitBake uses '#' only for "# comment" lines (always '#' + space). + * We therefore match ObjectScript preprocessor directives ('#' immediately + * followed by 'def'/';'), which BitBake never produces. Defaults to BitBake on + * any doubt (preserves existing behaviour). */ +CBMLanguage cbm_disambiguate_inc(const char *path) { + if (!path) { + return CBM_LANG_BITBAKE; + } + + FILE *f = cbm_fopen(path, "r"); + if (!f) { + return CBM_LANG_BITBAKE; + } + + char buf[CBM_SZ_4K + SKIP_ONE]; + size_t n = fread(buf, SKIP_ONE, CBM_SZ_4K, f); + buf[n] = '\0'; + (void)fclose(f); + + const char *line = buf; + while (*line) { + /* ObjectScript include header: a line beginning "ROUTINE ". */ + if (strncmp(line, "ROUTINE ", SLEN("ROUTINE ")) == 0 && + isupper((unsigned char)line[SLEN("ROUTINE ")])) { + return CBM_LANG_OBJECTSCRIPT_ROUTINE; + } + /* ObjectScript macro directives — the primary content of .inc files. + * "#define"/"#def1arg" (macro defs) and "#;" (line comment). BitBake's + * only '#' use is "# comment" (hash + space), so these never collide. */ + if (strncmp(line, "#define", SLEN("#define")) == 0 || + strncmp(line, "#def1arg", SLEN("#def1arg")) == 0 || + strncmp(line, "#;", SLEN("#;")) == 0) { + return CBM_LANG_OBJECTSCRIPT_ROUTINE; + } + const char *nl = strchr(line, '\n'); + if (!nl) { + break; + } + line = nl + SKIP_ONE; + } + return CBM_LANG_BITBAKE; +} diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 2cff8fb56..76d5e955b 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -603,6 +603,83 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, return SKIP_ONE; } +/* ObjectScript: build a method-QN -> return-type table from the Method nodes + * already in the graph buffer (definitions pass ran first). Scalar return types + * (%String, %Integer, ...) are skipped since they cannot host method dispatch. + * Returns NULL when no usable entries exist. Caller owns the heap table. */ +static CBMReturnTypeTable *build_return_type_table(const cbm_gbuf_t *gbuf) { + if (!gbuf) { + return NULL; + } + const cbm_gbuf_node_t **method_nodes = NULL; + int method_count = 0; + if (cbm_gbuf_find_by_label(gbuf, "Method", &method_nodes, &method_count) != 0 || + method_count <= 0 || !method_nodes) { + return NULL; + } + + CBMReturnTypeTable *rtt = (CBMReturnTypeTable *)calloc(1, sizeof(CBMReturnTypeTable)); + if (!rtt) { + return NULL; + } + + static const char *scalar_types[] = {"%String", "%Integer", "%Float", "%Boolean", + "%Status", "%Numeric", "%Date", "%Time", + "%TimeStamp", "%Binary", NULL}; + + for (int i = 0; i < method_count && rtt->count < CBM_RETURN_TYPE_TABLE_CAP; i++) { + const cbm_gbuf_node_t *n = method_nodes[i]; + if (!n->qualified_name || !n->properties_json) { + continue; + } + + const char *p = strstr(n->properties_json, "\"return_type\":"); + if (!p) { + continue; + } + p += 14; /* strlen("\"return_type\":") */ + while (*p == ' ') { + p++; + } + if (*p != '"') { + continue; + } + p++; + const char *end = strchr(p, '"'); + if (!end) { + continue; + } + int rtlen = (int)(end - p); + if (rtlen <= 0 || rtlen > 255) { + continue; + } + + char rt_buf[256]; + memcpy(rt_buf, p, (size_t)rtlen); + rt_buf[rtlen] = '\0'; + + bool is_scalar = false; + for (int si = 0; scalar_types[si]; si++) { + if (strcmp(rt_buf, scalar_types[si]) == 0) { + is_scalar = true; + break; + } + } + if (is_scalar) { + continue; + } + + rtt->entries[rtt->count].method_qn = n->qualified_name; + rtt->entries[rtt->count].return_type = strdup(rt_buf); + rtt->count++; + } + if (rtt->count == 0) { + free(rtt); + return NULL; + } + return rtt; +} + static CBMFileResult *calls_get_or_extract(cbm_pipeline_ctx_t *ctx, int idx, const cbm_file_info_t *fi, bool *owned) { *owned = false; @@ -614,8 +691,9 @@ static CBMFileResult *calls_get_or_extract(cbm_pipeline_ctx_t *ctx, int idx, if (!src) { return NULL; } - CBMFileResult *r = cbm_extract_file(src, slen, fi->language, ctx->project_name, fi->rel_path, - CBM_EXTRACT_BUDGET, NULL, NULL); + CBMFileResult *r = cbm_extract_file_ex(src, slen, fi->language, ctx->project_name, fi->rel_path, + CBM_EXTRACT_BUDGET, NULL, NULL, ctx->macro_table, + ctx->return_type_table); free(src); if (r) { *owned = true; @@ -626,6 +704,16 @@ static CBMFileResult *calls_get_or_extract(cbm_pipeline_ctx_t *ctx, int idx, int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count) { cbm_log_info("pass.start", "pass", "calls", "files", itoa_log(file_count)); + /* ObjectScript: build the method-return-type table from the definitions + * already in the graph buffer so `Set x = obj.Method()` can resolve x's + * class for subsequent x.Method() dispatch. NULL if no Method nodes. */ + if (!ctx->return_type_table) { + CBMReturnTypeTable *rtt = build_return_type_table(ctx->gbuf); + if (rtt) { + ctx->return_type_table = rtt; + } + } + int total_calls = 0; int resolved = 0; int unresolved = 0; diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index d1f506b5c..ebbb95414 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -25,6 +25,8 @@ enum { PD_JSON_FIELD_OVERHEAD = 6 }; #include "foundation/compat_fs.h" #include "foundation/limits.h" #include "cbm.h" +#include "arena.h" +#include "iris_export_xml.h" #include "simhash/minhash.h" #include "semantic/ast_profile.h" @@ -566,11 +568,42 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t continue; } + /* ObjectScript Studio Export XML: transcode each to UDL and + * extract it as CBM_LANG_OBJECTSCRIPT_UDL. The XML→UDL mapping is 1:1, + * so the same UDL extractor handles the result. These files are not + * cached (their defs/edges are emitted directly here). */ + if (lang == CBM_LANG_OBJECTSCRIPT_EXPORT) { + CBMArena export_arena; + cbm_arena_init(&export_arena); + int class_count = 0; + char **udl_strings = + cbm_iris_export_to_udl(&export_arena, source, source_len, &class_count); + free(source); + for (int ci = 0; ci < class_count; ci++) { + CBMFileResult *xr = cbm_extract_file_ex( + udl_strings[ci], (int)strlen(udl_strings[ci]), CBM_LANG_OBJECTSCRIPT_UDL, + ctx->project_name, rel, CBM_EXTRACT_BUDGET, NULL, NULL, ctx->macro_table, NULL); + if (!xr) { + continue; + } + for (int d = 0; d < xr->defs.count; d++) { + process_def(ctx, &xr->defs.items[d], rel); + total_defs++; + } + total_calls += xr->calls.count; + total_imports += create_import_edges_for_file(ctx, xr, rel, NULL); + create_channel_edges_for_file(ctx, xr, rel); + create_env_configures_for_file(ctx, xr, rel); + cbm_free_result(xr); + } + cbm_arena_destroy(&export_arena); + continue; + } + /* Extract */ - CBMFileResult *result = - cbm_extract_file(source, source_len, lang, ctx->project_name, rel, CBM_EXTRACT_BUDGET, - NULL, NULL /* no extra defines or include paths */ - ); + CBMFileResult *result = cbm_extract_file_ex( + source, source_len, lang, ctx->project_name, rel, CBM_EXTRACT_BUDGET, NULL, + NULL /* no extra defines or include paths */, ctx->macro_table, NULL); free(source); if (!result) { diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 18db122be..7f025cec8 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -87,6 +87,9 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 }; #include "foundation/compat_regex.h" #include "foundation/limits.h" #include "cbm.h" +#include "arena.h" +#include "macro_table.h" +#include "iris_export_xml.h" #include "simhash/minhash.h" #include "semantic/ast_profile.h" @@ -708,6 +711,9 @@ typedef struct { * While set, pulls skip the nap (the designed soft overshoot); the cheap * over-budget probe re-arms the gate once RSS drains under budget. */ _Atomic int bp_futile; + + const CBMMacroTable *macro_table; /* ObjectScript $$$macros (NULL if none) */ + const CBMReturnTypeTable *return_type_table; /* ObjectScript return types (NULL if none) */ } extract_ctx_t; /* Cap on the number of index.file_oversized WARN lines (the full list still goes @@ -880,8 +886,38 @@ static void extract_worker(int worker_id, void *ctx_ptr) { uint64_t file_t0 = extract_now_ns(); - CBMFileResult *result = cbm_extract_file(source, source_len, fi->language, ec->project_name, - fi->rel_path, CBM_EXTRACT_BUDGET, NULL, NULL); + /* ObjectScript Studio Export XML: transcode each to UDL and + * extract directly into the local gbuf (the per-file cache holds a single + * result, so multi-class Export files are processed inline here). */ + if (fi->language == CBM_LANG_OBJECTSCRIPT_EXPORT) { + CBMArena ea; + cbm_arena_init(&ea); + int cc = 0; + char **udls = cbm_iris_export_to_udl(&ea, source, source_len, &cc); + for (int ci = 0; ci < cc; ci++) { + CBMFileResult *xr = + cbm_extract_file_ex(udls[ci], (int)strlen(udls[ci]), CBM_LANG_OBJECTSCRIPT_UDL, + ec->project_name, fi->rel_path, CBM_EXTRACT_BUDGET, NULL, + NULL, ec->macro_table, ec->return_type_table); + if (!xr) { + continue; + } + for (int d = 0; d < xr->defs.count; d++) { + CBMDefinition *def = &xr->defs.items[d]; + if (def->qualified_name && def->name) { + insert_def_into_gbuf(ws, fi, def); + } + } + cbm_free_result(xr); + } + cbm_arena_destroy(&ea); + free_source(source); + continue; + } + + CBMFileResult *result = cbm_extract_file_ex( + source, source_len, fi->language, ec->project_name, fi->rel_path, CBM_EXTRACT_BUDGET, + NULL, NULL, ec->macro_table, ec->return_type_table); uint64_t file_elapsed_ms = (extract_now_ns() - file_t0) / PP_USEC_PER_MS; @@ -1035,6 +1071,10 @@ static void log_extract_mem_stats(int worker_count) { } } +/* Forward declaration: macro table builder lives in pipeline.c (shared path). */ +CBMMacroTable *cbm_build_macro_table_from_files(const cbm_file_info_t *files, int count, + const char *repo_path); + int cbm_parallel_extract_ex(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count, CBMFileResult **result_cache, _Atomic int64_t *shared_ids, int worker_count, const cbm_parallel_extract_opts_t *opts) { @@ -1111,6 +1151,10 @@ int cbm_parallel_extract_ex(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file * sequential merge loop below). */ pp_err_list_t *err_lists = calloc((size_t)worker_count, sizeof(pp_err_list_t)); + /* ObjectScript macro table (NULL when no .inc include files present). */ + CBMMacroTable *pp_macro_table = + cbm_build_macro_table_from_files(files, file_count, ctx->repo_path); + extract_ctx_t ec = { .files = files, .sorted = sorted, @@ -1127,6 +1171,8 @@ int cbm_parallel_extract_ex(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file .retain_sources = resolved_opts.retain_sources, .retain_total_budget_bytes = resolved_opts.retain_total_budget_bytes, .retain_per_file_max_bytes = resolved_opts.retain_per_file_max_bytes, + .macro_table = pp_macro_table, + .return_type_table = ctx->return_type_table, }; atomic_init(&ec.next_worker_id, 0); atomic_init(&ec.next_file_idx, 0); @@ -1177,6 +1223,7 @@ int cbm_parallel_extract_ex(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file cbm_aligned_free(workers); free(sorted); + cbm_macro_table_free(pp_macro_table); /* ObjectScript macro table (NULL-safe) */ if (atomic_load(ctx->cancelled)) { return CBM_NOT_FOUND; diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index fd80382dd..488f42351 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -22,6 +22,8 @@ enum { CBM_DIR_PERMS = 0755, PL_RING = 4, PL_RING_MASK = 3, PL_SEQ_PASSES = 6, P #include "graph_buffer/graph_buffer.h" #include "git/git_context.h" #include "store/store.h" +#include "macro_table.h" +#include "arena.h" #include "discover/discover.h" #include "discover/userconfig.h" #include "foundation/platform.h" @@ -722,7 +724,6 @@ static void predump_cfg(cbm_pipeline_ctx_t *ctx) { static void predump_complexity(cbm_pipeline_ctx_t *ctx) { cbm_pipeline_pass_complexity(ctx); } - static void run_predump_passes(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx) { static const struct { predump_pass_fn fn; @@ -766,6 +767,62 @@ static int seq_pass_lsp_cross_dispatch(cbm_pipeline_ctx_t *ctx, const cbm_file_i } /* Run the sequential pipeline path: definitions, k8s, lsp_cross, calls, usages, semantic. */ +/* Build the ObjectScript $$$macro table from .inc include files in the repo. + * Returns NULL (and does no work) when no ObjectScript include files exist. + * Caller owns the returned heap table (free via cbm_macro_table_free). */ +CBMMacroTable *cbm_build_macro_table_from_files(const cbm_file_info_t *files, int count, + const char *repo_path) { + (void)repo_path; + bool has_inc = false; + for (int i = 0; i < count; i++) { + if (files[i].language == CBM_LANG_OBJECTSCRIPT_ROUTINE && files[i].path && + (strrchr(files[i].path, '.') != NULL && + strcmp(strrchr(files[i].path, '.'), ".inc") == 0)) { + has_inc = true; + break; + } + } + if (!has_inc) { + return NULL; + } + + CBMMacroTable *mt = (CBMMacroTable *)calloc(1, sizeof(CBMMacroTable)); + if (!mt) { + return NULL; + } + + cbm_arena_init(&mt->arena); + cbm_macro_table_init_system(mt); + + for (int i = 0; i < count; i++) { + if (files[i].language != CBM_LANG_OBJECTSCRIPT_ROUTINE) { + continue; + } + if (!files[i].path || !(strrchr(files[i].path, '.') != NULL && + strcmp(strrchr(files[i].path, '.'), ".inc") == 0)) { + continue; + } + FILE *f = cbm_fopen(files[i].path, "rb"); + if (!f) { + continue; + } + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + rewind(f); + if (fsize > 0) { + char *src = (char *)malloc((size_t)fsize + 1); + if (src) { + size_t nread = fread(src, 1, (size_t)fsize, f); + src[nread] = '\0'; + cbm_parse_inc_file(mt, &mt->arena, src); + free(src); + } + } + (void)fclose(f); + } + return mt; +} + static int run_sequential_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count, struct timespec *t) { @@ -783,6 +840,13 @@ static int run_sequential_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx, if (seq_cache) { ctx->result_cache = seq_cache; } + + /* ObjectScript: build the $$$macro table from .inc include files so that + * pass_calls can resolve macro-mediated dispatch. NULL when not present. */ + CBMMacroTable *mt = cbm_build_macro_table_from_files(files, file_count, ctx->repo_path); + if (mt) { + ctx->macro_table = mt; + } typedef int (*seq_pass_fn)(cbm_pipeline_ctx_t *, const cbm_file_info_t *, int); static const struct { seq_pass_fn fn; @@ -840,6 +904,18 @@ static int run_sequential_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx, * mimalloc-epoch memory through slab_free -> plain free() and libmalloc * aborts — the #773 second-index SIGABRT. */ cbm_destroy_thread_parser(); + /* ObjectScript: free the macro / return-type tables built for this run. */ + if (ctx->macro_table) { + cbm_macro_table_free((CBMMacroTable *)ctx->macro_table); + ctx->macro_table = NULL; + } + if (ctx->return_type_table) { + for (int i = 0; i < ctx->return_type_table->count; i++) { + free((void *)ctx->return_type_table->entries[i].return_type); + } + free((void *)ctx->return_type_table); + ctx->return_type_table = NULL; + } return rc; } diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index ee69fc9c2..88ff76169 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -858,6 +858,19 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil cbm_pipeline_pass_k8s(&ctx, changed_files, ci); run_postpasses(&ctx, changed_files, ci, project); + /* Free ObjectScript tables built by pass_calls during run_extract_resolve. */ + if (ctx.return_type_table) { + for (int i = 0; i < ctx.return_type_table->count; i++) { + free((void *)ctx.return_type_table->entries[i].return_type); + } + free((void *)ctx.return_type_table); + ctx.return_type_table = NULL; + } + if (ctx.macro_table) { + free((void *)ctx.macro_table); + ctx.macro_table = NULL; + } + /* Coverage rows (#963): merge = previous FAILURE rows for files NOT * re-extracted this run + this run's fresh entries (changed files replace * their old rows — a file that parses cleanly now simply contributes diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index a3d806558..ed8c7d6e0 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -110,6 +110,14 @@ typedef struct { * resolve. */ CBMArena seq_cross_arena; bool seq_cross_arena_live; + + /* ObjectScript $$$macro table built from .inc files in the repo (NULL if + * no ObjectScript include files were found). Owned by pipeline.c. */ + const CBMMacroTable *macro_table; + + /* ObjectScript method-return-type table built from extracted definitions + * (NULL until pass_calls builds it). Owned by pipeline.c. */ + const CBMReturnTypeTable *return_type_table; } cbm_pipeline_ctx_t; static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *const *excluded_dirs, diff --git a/tests/test_extraction.c b/tests/test_extraction.c index f8228e46a..6ba4b37c6 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -9,6 +9,8 @@ #include "cbm.h" #include "../src/foundation/compat.h" /* cbm_clock_gettime (wide-flat scaling guard) */ #include +#include "macro_table.h" +#include "iris_export_xml.h" /* ── Helpers ───────────────────────────────────────────────────── */ @@ -66,6 +68,14 @@ static CBMFileResult *extract(const char *src, CBMLanguage lang, const char *pro return r; } +/* As extract(), but threads an ObjectScript macro table through. */ +static CBMFileResult *extract_with_macros(const char *src, CBMLanguage lang, const char *proj, + const char *path, const CBMMacroTable *mt) { + CBMFileResult *r = + cbm_extract_file_ex(src, (int)strlen(src), lang, proj, path, 0, NULL, NULL, mt, NULL); + return r; +} + /* ═══════════════════════════════════════════════════════════════════ * Group A: OOP Languages * ═══════════════════════════════════════════════════════════════════ */ @@ -3488,6 +3498,741 @@ TEST(extract_wide_flat_file_is_linear) { PASS(); } +/* =================================================================== + * Group H3: ObjectScript return type extraction + * =================================================================== */ + +TEST(objectscript_udl_method_return_type) { + CBMFileResult *r = extract("Class MyApp.Factory Extends %RegisteredObject\n" + "{\n" + "Method GetAdapter() As EnsLib.SQL.OutboundAdapter\n" + "{\n" + " Quit ##class(EnsLib.SQL.OutboundAdapter).%New()\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Factory.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + bool found_rt = false; + for (int i = 0; i < r->defs.count; i++) { + if (strcmp(r->defs.items[i].name, "GetAdapter") == 0) { + ASSERT_NOT_NULL(r->defs.items[i].return_type); + ASSERT(strstr(r->defs.items[i].return_type, "EnsLib.SQL.OutboundAdapter") != NULL); + found_rt = true; + } + } + ASSERT(found_rt); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_scalar_return_type_not_resolved) { + CBMFileResult *r = extract("Class MyApp.Counter Extends %RegisteredObject\n" + "{\n" + "Method GetName() As %String\n" + "{\n" + " Quit \"hello\"\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Counter.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + for (int i = 0; i < r->defs.count; i++) { + if (strcmp(r->defs.items[i].name, "GetName") == 0) { + ASSERT_NOT_NULL(r->defs.items[i].return_type); + ASSERT(strstr(r->defs.items[i].return_type, "%String") != NULL); + } + } + cbm_free_result(r); + PASS(); +} + +/* =================================================================== + * Group H2: ObjectScript macro expansion + * =================================================================== */ + +TEST(objectscript_udl_class) { + CBMFileResult *r = extract("Class MyApp.Patient Extends %Persistent\n{\n}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Patient.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Class", "MyApp.Patient")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_methods_after_goto_label) { + CBMFileResult *r = extract("Class Graph.KG.Test Extends %RegisteredObject\n" + "{\n" + "ClassMethod First() As %String\n" + "{\n" + " If 1 { Goto Done }\n" + "Done\n" + " Quit \"x\"\n" + "}\n" + "ClassMethod Second() As %String\n" + "{\n" + " Quit \"y\"\n" + "}\n" + "ClassMethod Third() As %String\n" + "{\n" + " Quit \"z\"\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Test.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Class", "Graph.KG.Test")); + ASSERT(has_def(r, "Method", "First")); + ASSERT(has_def(r, "Method", "Second")); + ASSERT(has_def(r, "Method", "Third")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_methods) { + CBMFileResult *r = extract("Class MyApp.Utils Extends %RegisteredObject\n" + "{\n" + "ClassMethod Format(pVal As %String) As %String\n" + "{\n" + " Quit pVal\n" + "}\n" + "Method Save() As %Status\n" + "{\n" + " Quit ..%Save()\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Utils.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Class", "MyApp.Utils")); + ASSERT(has_def(r, "Method", "Format")); + ASSERT(has_def(r, "Method", "Save")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_base_classes) { + CBMFileResult *r = extract("Class MyApp.Patient Extends %Persistent\n" + "{\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Patient.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Class", "MyApp.Patient")); + int found = 0; + for (int i = 0; i < r->defs.count; i++) { + if (strcmp(r->defs.items[i].name, "MyApp.Patient") == 0) { + found = 1; + ASSERT_NOT_NULL(r->defs.items[i].base_classes); + ASSERT_NOT_NULL(r->defs.items[i].base_classes[0]); + ASSERT_STR_EQ(r->defs.items[i].base_classes[0], "%Persistent"); + } + } + ASSERT_TRUE(found); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_multiple_bases) { + CBMFileResult *r = extract("Class MyApp.Dual Extends (MyApp.Base, %RegisteredObject)\n" + "{\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Dual.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + int found = 0; + for (int i = 0; i < r->defs.count; i++) { + if (strcmp(r->defs.items[i].name, "MyApp.Dual") == 0) { + found = 1; + ASSERT_NOT_NULL(r->defs.items[i].base_classes); + ASSERT_NOT_NULL(r->defs.items[i].base_classes[0]); + ASSERT_NOT_NULL(r->defs.items[i].base_classes[1]); + } + } + ASSERT_TRUE(found); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_properties) { + CBMFileResult *r = extract("Class MyApp.Patient Extends %Persistent\n" + "{\n" + "Property Name As %String;\n" + "Property DOB As %Date;\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Patient.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Class", "MyApp.Patient")); + ASSERT(has_def(r, "Variable", "Name")); + ASSERT(has_def(r, "Variable", "DOB")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_routine_tags) { + CBMFileResult *r = extract("UTILS\n" + " Quit\n" + "\n" + "Format(value,fmt)\n" + " Set result = $ZDate(value, fmt)\n" + " Quit result\n" + "\n" + "Log(msg)\n" + " Write msg,!\n" + " Quit\n", + CBM_LANG_OBJECTSCRIPT_ROUTINE, "t", "Utils.mac"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Function", "Format")); + ASSERT(has_def(r, "Function", "Log")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_query_member) { + CBMFileResult *r = + extract("Class MyApp.Repo Extends %Persistent\n" + "{\n" + "Query FindAll(name As %String) As %SQLQuery { SELECT * FROM MyApp_Repo }\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Repo.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Class", "MyApp.Repo")); + ASSERT(has_def(r, "Method", "FindAll")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_index_member) { + CBMFileResult *r = extract("Class MyApp.Repo Extends %Persistent\n" + "{\n" + "Property Name As %String;\n" + "Index NameIdx On Name;\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Repo.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Index", "NameIdx")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_xdata_member) { + CBMFileResult *r = extract("Class MyApp.Service Extends %CSP.REST\n" + "{\n" + "XData UrlMap { }\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Service.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "XData", "UrlMap")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_trigger_member) { + CBMFileResult *r = extract("Class MyApp.Log Extends %Persistent\n" + "{\n" + "Trigger AfterInsert [ Event = INSERT ] { }\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Log.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Trigger", "AfterInsert")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_trigger_body_quit) { + CBMFileResult *r = extract("Class MyApp.Patient Extends %Persistent\n" + "{\n" + "Trigger OnDeleteSQL [ Event = DELETE, Time = AFTER ] {\n" + " Quit\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Patient.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Trigger", "OnDeleteSQL")); + for (int i = 0; i < r->defs.count; i++) { + if (strcmp(r->defs.items[i].label, "Trigger") == 0 && + strcmp(r->defs.items[i].name, "OnDeleteSQL") == 0) { + ASSERT_NOT_NULL(r->defs.items[i].docstring); + ASSERT(strstr(r->defs.items[i].docstring, "trigger_body") != NULL); + ASSERT(strstr(r->defs.items[i].docstring, "Quit") != NULL); + break; + } + } + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_trigger_body_tokens) { + CBMFileResult *r = extract("Class MyApp.Order Extends %Persistent\n" + "{\n" + "Trigger AfterInsert [ Event = INSERT, Time = AFTER ] {\n" + " Set id = ..%Id()\n" + " Do ##class(MyApp.Audit).Log(id)\n" + " Quit\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Order.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Trigger", "AfterInsert")); + for (int i = 0; i < r->defs.count; i++) { + if (strcmp(r->defs.items[i].label, "Trigger") == 0 && + strcmp(r->defs.items[i].name, "AfterInsert") == 0) { + ASSERT_NOT_NULL(r->defs.items[i].docstring); + ASSERT(strstr(r->defs.items[i].docstring, "trigger_body") != NULL); + ASSERT_NOT_NULL(r->defs.items[i].body_tokens); + ASSERT(strstr(r->defs.items[i].body_tokens, "Log") != NULL || + strstr(r->defs.items[i].body_tokens, "Audit") != NULL || + strstr(r->defs.items[i].body_tokens, "id") != NULL); + break; + } + } + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_self_call_relative_dot_method) { + CBMFileResult *r = + extract("Class HS.Flash.UpdateManager Extends Ens.BusinessProcess\n" + "{\n" + "Method MakeMRNUpToDate(pRequest As HS.Message.FlashQueueUpdate) As %Status\n" + "{\n" + " Set tSC = ..processStreamlet(pSession, pTS, tMPIID, tSourceMRN, ii)\n" + " Quit tSC\n" + "}\n" + "Method processStreamlet(pSession As %Integer) As %Status\n" + "{\n" + " Quit $$$OK\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "UpdateManager.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Method", "MakeMRNUpToDate")); + ASSERT(has_call(r, "HS.Flash.UpdateManager.processStreamlet")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_calls_typed_new) { + CBMFileResult *r = extract("Class MyApp.Caller Extends %RegisteredObject\n" + "{\n" + "Method Run() As %Status\n" + "{\n" + " Set adapter = ##class(EnsLib.SQL.OutboundAdapter).%New()\n" + " Do adapter.ExecuteQuery(\"SELECT 1\")\n" + " Quit $$$OK\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_call(r, "EnsLib.SQL.OutboundAdapter.ExecuteQuery")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_ensemble_production_def_parses_items) { + CBMFileResult *r = + extract("Class Sample.Production Extends Ens.Production\n" + "{\n" + "XData ProductionDefinition\n" + "{\n" + "\n" + " \n" + " \n" + " \n" + " \n" + "\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Production.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "XData", "ProductionDefinition")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_ensemble_production_def_hs_settings) { + CBMFileResult *r = extract( + "Class HS.Flash.Production Extends Ens.Production\n" + "{\n" + "XData ProductionDefinition\n" + "{\n" + "\n" + " \n" + " FHIROps\n" + " PatientOps\n" + " ConformOps\n" + " \n" + "\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "HSProduction.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "XData", "ProductionDefinition")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_ensemble_production_def_absent_no_error) { + CBMFileResult *r = extract("Class Sample.NonProduction Extends %Persistent\n" + "{\n" + "Method DoSomething() As %Status\n" + "{\n" + " Quit $$$OK\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "NonProduction.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(!has_def(r, "XData", "ProductionDefinition")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_calls_typed_param) { + CBMFileResult *r = extract("Class MyApp.Handler Extends %RegisteredObject\n" + "{\n" + "Method Process(req As Ens.Request) As %Status\n" + "{\n" + " Do req.Send()\n" + " Quit $$$OK\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Handler.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_call(r, "Ens.Request.Send")); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_udl_calls_typed_property) { + CBMFileResult *r = extract("Class MyApp.Service Extends Ens.BusinessService\n" + "{\n" + "Property Adapter As EnsLib.SQL.InboundAdapter;\n" + "Method OnProcessInput() As %Status\n" + "{\n" + " Do ..Adapter.ExecuteQuery(\"SELECT 1\")\n" + " Quit $$$OK\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Service.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_call(r, "EnsLib.SQL.InboundAdapter.ExecuteQuery")); + cbm_free_result(r); + PASS(); +} + +/* =================================================================== + * Group H2: ObjectScript macro expansion + * =================================================================== */ + +TEST(objectscript_macro_expand_system) { + CBMMacroTable mt; + cbm_macro_table_init_system(&mt); + CBMFileResult *r = extract_with_macros("Class MyApp.Caller Extends %RegisteredObject\n" + "{\n" + "Method Run(sc As %Status) As %Status\n" + "{\n" + " If $$$ISERR(sc) { Quit sc }\n" + " Quit $$$OK\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls", &mt); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_call(r, "%SYSTEM.Status.IsError")); + cbm_free_result(r); + PASS(); +} + +/* ═══════════════════════════════════════════════════════════════════ + * Group H3: ObjectScript DATA_FLOWS argument extraction + * ═══════════════════════════════════════════════════════════════════ */ + +static int find_call_args(const CBMFileResult *r, const char *callee, const char **out_arg0, + const char **out_arg1) { + if (out_arg0) + *out_arg0 = NULL; + if (out_arg1) + *out_arg1 = NULL; + for (int i = 0; i < r->calls.count; i++) { + if (strstr(r->calls.items[i].callee_name, callee)) { + if (out_arg0 && r->calls.items[i].arg_count > 0) + *out_arg0 = r->calls.items[i].args[0].expr; + if (out_arg1 && r->calls.items[i].arg_count > 1) + *out_arg1 = r->calls.items[i].args[1].expr; + return r->calls.items[i].arg_count; + } + } + return -1; +} + +TEST(objectscript_data_flows_class_method_args) { + CBMFileResult *r = extract("Class MyApp.Caller Extends %RegisteredObject\n" + "{\n" + "Method Run() As %Status\n" + "{\n" + " Set sql = \"SELECT 1\"\n" + " Do ##class(MyApp.Utils).Transform(sql, \"JSON\")\n" + " Quit $$$OK\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_call(r, "MyApp.Utils.Transform")); + const char *arg0 = NULL; + const char *arg1 = NULL; + int argc = find_call_args(r, "MyApp.Utils.Transform", &arg0, &arg1); + ASSERT(argc == 2); + ASSERT_NOT_NULL(arg0); + ASSERT(strstr(arg0, "sql") != NULL); + cbm_free_result(r); + PASS(); +} + +TEST(objectscript_macro_expand_local) { + CBMMacroTable mt; + cbm_macro_table_init_system(&mt); + CBMArena arena; + cbm_arena_init(&arena); + const char *inc_content = "ROUTINE MyApp.Include [Type=INC]\n" + "#define MyCheck(%sc) ##class(MyApp.Utils).Validate(%sc)\n"; + cbm_parse_inc_file(&mt, &arena, inc_content); + CBMFileResult *r = extract_with_macros("Class MyApp.Caller Extends %RegisteredObject\n" + "{\n" + "Method Run(sc As %Status) As %Status\n" + "{\n" + " If $$$MyCheck(sc) { Quit $$$OK }\n" + " Quit $$$OK\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls", &mt); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_call(r, "MyApp.Utils.Validate")); + cbm_free_result(r); + cbm_arena_destroy(&arena); + PASS(); +} + +TEST(objectscript_macro_constant_no_extra_call) { + CBMMacroTable mt; + cbm_macro_table_init_system(&mt); + CBMArena arena; + cbm_arena_init(&arena); + const char *inc_content = "ROUTINE MyApp.Include [Type=INC]\n" + "#define MyConst 42\n"; + cbm_parse_inc_file(&mt, &arena, inc_content); + CBMFileResult *r = extract_with_macros("Class MyApp.Caller Extends %RegisteredObject\n" + "{\n" + "Method Run() As %Integer\n" + "{\n" + " Set x = $$$MyConst\n" + " Quit x\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Caller.cls", &mt); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(!has_call(r, "$$$MyConst")); + cbm_free_result(r); + cbm_arena_destroy(&arena); + PASS(); +} + +TEST(objectscript_data_flows_instance_method_args) { + CBMFileResult *r = extract("Class MyApp.Service Extends %RegisteredObject\n" + "{\n" + "Method Run() As %Status\n" + "{\n" + " Set adapter = ##class(EnsLib.SQL.OutboundAdapter).%New()\n" + " Do adapter.ExecuteQuery(\"SELECT 1\")\n" + " Quit $$$OK\n" + "}\n" + "}\n", + CBM_LANG_OBJECTSCRIPT_UDL, "t", "Service.cls"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_call(r, "EnsLib.SQL.OutboundAdapter.ExecuteQuery")); + const char *arg0 = NULL; + int argc = find_call_args(r, "EnsLib.SQL.OutboundAdapter.ExecuteQuery", &arg0, NULL); + ASSERT(argc == 1); + ASSERT_NOT_NULL(arg0); + cbm_free_result(r); + PASS(); +} + +/* =================================================================== + * Group H4: IRIS Export XML → UDL transcoder + * =================================================================== */ + +#define SIMPLE_EXPORT \ + "\n" \ + "\n" \ + "\n" \ + "%RegisteredObject\n" \ + "\n" \ + "%String\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" + +TEST(iris_export_xml_simple_class) { + CBMArena arena; + cbm_arena_init(&arena); + int count = 0; + char **udl = cbm_iris_export_to_udl(&arena, SIMPLE_EXPORT, (int)strlen(SIMPLE_EXPORT), &count); + ASSERT_NOT_NULL(udl); + ASSERT(count == 1); + ASSERT_NOT_NULL(udl[0]); + ASSERT(strstr(udl[0], "Test.Simple") != NULL); + ASSERT(strstr(udl[0], "%RegisteredObject") != NULL); + ASSERT(strstr(udl[0], "Hello") != NULL); + ASSERT(strstr(udl[0], "Quit \"hello\"") != NULL); + cbm_arena_destroy(&arena); + PASS(); +} + +#define CLASSMETHOD_EXPORT \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "1\n" \ + "pArg:%String,pFlag:%Boolean=0\n" \ + "%Status\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" + +TEST(iris_export_xml_classmethod) { + CBMArena arena; + cbm_arena_init(&arena); + int count = 0; + char **udl = + cbm_iris_export_to_udl(&arena, CLASSMETHOD_EXPORT, (int)strlen(CLASSMETHOD_EXPORT), &count); + ASSERT_NOT_NULL(udl); + ASSERT(count == 1); + ASSERT(strstr(udl[0], "ClassMethod") != NULL); + ASSERT(strstr(udl[0], "pArg") != NULL); + ASSERT(strstr(udl[0], "pFlag") != NULL); + ASSERT(strstr(udl[0], "%Status") != NULL); + cbm_arena_destroy(&arena); + PASS(); +} + +#define MEMBER_EXPORT \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "%String\n" \ + "\n" \ + "\n" \ + "\n" \ + "1\n" \ + "\n" \ + "\n" \ + "Name\n" \ + "1\n" \ + "\n" \ + "\n" \ + "\n" + +TEST(iris_export_xml_property_parameter_index) { + CBMArena arena; + cbm_arena_init(&arena); + int count = 0; + char **udl = cbm_iris_export_to_udl(&arena, MEMBER_EXPORT, (int)strlen(MEMBER_EXPORT), &count); + ASSERT_NOT_NULL(udl); + ASSERT(count == 1); + ASSERT(strstr(udl[0], "Property Name") != NULL); + ASSERT(strstr(udl[0], "%String") != NULL); + ASSERT(strstr(udl[0], "Parameter VERSION") != NULL); + ASSERT(strstr(udl[0], "Index NameIdx") != NULL); + cbm_arena_destroy(&arena); + PASS(); +} + +#define CALLS_EXPORT \ + "\n" \ + "\n" \ + "\n" \ + "%RegisteredObject\n" \ + "\n" \ + "1\n" \ + "%Status\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" + +TEST(iris_export_xml_calls_extracted) { + CBMArena arena; + cbm_arena_init(&arena); + int count = 0; + char **udl = cbm_iris_export_to_udl(&arena, CALLS_EXPORT, (int)strlen(CALLS_EXPORT), &count); + ASSERT_NOT_NULL(udl); + ASSERT(count == 1); + CBMFileResult *r = cbm_extract_file(udl[0], (int)strlen(udl[0]), CBM_LANG_OBJECTSCRIPT_UDL, "t", + "Caller.cls", 0, NULL, NULL); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_call(r, "Target.Worker.Execute")); + cbm_free_result(r); + cbm_arena_destroy(&arena); + PASS(); +} + +#define MULTI_EXPORT \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" + +TEST(iris_export_xml_multi_class) { + CBMArena arena; + cbm_arena_init(&arena); + int count = 0; + char **udl = cbm_iris_export_to_udl(&arena, MULTI_EXPORT, (int)strlen(MULTI_EXPORT), &count); + ASSERT_NOT_NULL(udl); + ASSERT(count == 2); + ASSERT(strstr(udl[0], "Test.First") != NULL || strstr(udl[1], "Test.First") != NULL); + ASSERT(strstr(udl[0], "Test.Second") != NULL || strstr(udl[1], "Test.Second") != NULL); + cbm_arena_destroy(&arena); + PASS(); +} + SUITE(extraction) { /* Initialize extraction library */ cbm_init(); @@ -3504,6 +4249,40 @@ SUITE(extraction) { RUN_TEST(extract_ts_this_super_receiver_not_flagged); RUN_TEST(extract_js_member_call_flags_is_method); + /* InterSystems ObjectScript (UDL / routine / Export XML). */ + RUN_TEST(objectscript_udl_class); + RUN_TEST(objectscript_udl_methods_after_goto_label); + RUN_TEST(objectscript_udl_methods); + RUN_TEST(objectscript_udl_base_classes); + RUN_TEST(objectscript_udl_multiple_bases); + RUN_TEST(objectscript_udl_properties); + RUN_TEST(objectscript_routine_tags); + RUN_TEST(objectscript_udl_query_member); + RUN_TEST(objectscript_udl_index_member); + RUN_TEST(objectscript_udl_xdata_member); + RUN_TEST(objectscript_udl_trigger_member); + RUN_TEST(objectscript_udl_trigger_body_quit); + RUN_TEST(objectscript_udl_trigger_body_tokens); + RUN_TEST(objectscript_udl_ensemble_production_def_parses_items); + RUN_TEST(objectscript_udl_ensemble_production_def_hs_settings); + RUN_TEST(objectscript_udl_ensemble_production_def_absent_no_error); + RUN_TEST(objectscript_udl_self_call_relative_dot_method); + RUN_TEST(objectscript_udl_calls_typed_new); + RUN_TEST(objectscript_udl_calls_typed_param); + RUN_TEST(objectscript_udl_calls_typed_property); + RUN_TEST(objectscript_macro_expand_system); + RUN_TEST(objectscript_macro_expand_local); + RUN_TEST(objectscript_macro_constant_no_extra_call); + RUN_TEST(objectscript_udl_method_return_type); + RUN_TEST(objectscript_udl_scalar_return_type_not_resolved); + RUN_TEST(objectscript_data_flows_class_method_args); + RUN_TEST(objectscript_data_flows_instance_method_args); + RUN_TEST(iris_export_xml_simple_class); + RUN_TEST(iris_export_xml_classmethod); + RUN_TEST(iris_export_xml_property_parameter_index); + RUN_TEST(iris_export_xml_calls_extracted); + RUN_TEST(iris_export_xml_multi_class); + /* R box-module imports + member calls */ RUN_TEST(extract_r_box_use_imports_issue218); RUN_TEST(extract_r_dollar_call_issue219); From 3b1ea5826e5904ef43bdb3b257f518103ce70d15 Mon Sep 17 00:00:00 2001 From: metehanulusoy Date: Sun, 12 Jul 2026 14:55:22 +0300 Subject: [PATCH 3/7] fix(mcp): ignore ::missed shadow rows when resolving a db's project name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The miss-graph pass inserts a second `projects` row ("::missed") so its nodes satisfy the FK on nodes.project. Since #704, db_internal_project_name required the projects table to hold exactly one row, so any project with a miss graph vanished from list_projects and the graph UI, and the fallback-scan resolve path failed — the index looked empty while fully present. Filter out internal shadow rows (names containing "::") and require exactly one primary row instead. Adds a reproduce-first regression test modeled on the #704 fixture (red on the old code: 150 passed, 1 failed; green with the fix: 151 passed). Fixes #1044 Signed-off-by: metehanulusoy Co-Authored-By: Claude Fable 5 --- src/mcp/mcp.c | 21 ++++++++++--- tests/test_mcp.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 0d7aa14d6..59c1f94a0 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -1335,10 +1335,23 @@ static bool db_internal_project_name(const char *full_path, char *name_out, size cbm_project_t *projs = NULL; int n = 0; bool ok = false; - if (cbm_store_list_projects(st, &projs, &n) == CBM_STORE_OK && n == 1 && projs[0].name && - projs[0].name[0]) { - snprintf(name_out, name_sz, "%s", projs[0].name); - ok = true; + if (cbm_store_list_projects(st, &projs, &n) == CBM_STORE_OK) { + /* Ignore internal shadow projects ("::missed" miss-graph rows): + * they share the db with the primary project and must not make it + * unresolvable — requiring n == 1 over ALL rows made every project + * with a miss graph vanish from list_projects and the UI (#1044). */ + int primary = -1; + int primary_count = 0; + for (int i = 0; i < n; i++) { + if (projs[i].name && projs[i].name[0] && !strstr(projs[i].name, "::")) { + primary = i; + primary_count++; + } + } + if (primary_count == 1) { + snprintf(name_out, name_sz, "%s", projs[primary].name); + ok = true; + } } cbm_store_free_projects(projs, n); if (ok && out_store) { diff --git a/tests/test_mcp.c b/tests/test_mcp.c index e24ec0d73..a5ae60ceb 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -4134,6 +4134,81 @@ TEST(tool_resolve_store_by_internal_name_issue704) { PASS(); } +/* ── #1044: a "::missed" shadow row must not hide the project ── + * + * The miss-graph pass inserts a second `projects` row ("::missed") so + * its nodes satisfy the FK on nodes.project. db_internal_project_name + * required the projects table to hold EXACTLY ONE row, so any project with + * a miss graph vanished from list_projects and the graph UI, and the + * fallback-scan resolve path failed. + * + * RED on buggy code / GREEN on the fix: + * A. list_projects still advertises "delta1044" while the shadow row exists. + * B. the shadow name itself is never advertised. + * C. search_graph(project="delta1044") still resolves and returns the node. + */ +TEST(tool_list_projects_ignores_missed_shadow_issue1044) { + char cache[256]; + snprintf(cache, sizeof(cache), "/tmp/cbm-issue1044-XXXXXX"); + if (!cbm_mkdtemp(cache)) { + PASS(); /* skip if mkdtemp fails — not a #1044 signal */ + } + + const char *saved = getenv("CBM_CACHE_DIR"); + char *saved_copy = saved ? strdup(saved) : NULL; + cbm_setenv("CBM_CACHE_DIR", cache, 1); + + ASSERT_TRUE(issue704_make_db(cache, "delta1044.db", "delta1044", "deltaFunc1044")); + + /* Add the shadow row exactly the way the miss-graph pass does. */ + char db_path[700]; + snprintf(db_path, sizeof(db_path), "%s/delta1044.db", cache); + cbm_store_t *st = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(st); + ASSERT_EQ(cbm_store_upsert_project(st, "delta1044::missed", ""), CBM_STORE_OK); + cbm_store_close(st); + + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + + /* ── A + B: primary advertised, shadow hidden ─────────────────── */ + char *list = + cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"list_projects\",\"arguments\":{}}}"); + ASSERT_NOT_NULL(list); + ASSERT_NOT_NULL(strstr(list, "delta1044")); /* RED before: db skipped as ghost */ + ASSERT_NULL(strstr(list, "::missed")); /* shadow never advertised */ + free(list); + + /* ── C: the project still resolves and returns its node ───────── */ + char *q = cbm_mcp_server_handle( + srv, "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\"," + "\"params\":{\"name\":\"search_graph\",\"arguments\":{" + "\"project\":\"delta1044\",\"name_pattern\":\"deltaFunc1044\",\"limit\":5}}}"); + ASSERT_NOT_NULL(q); + ASSERT_NOT_NULL(strstr(q, "deltaFunc1044")); + ASSERT_NULL(strstr(q, "not found")); + free(q); + + cbm_mcp_server_free(srv); + + /* ── cleanup ───────────────────────────────────────────────────── */ + if (saved_copy) { + cbm_setenv("CBM_CACHE_DIR", saved_copy, 1); + free(saved_copy); + } else { + cbm_unsetenv("CBM_CACHE_DIR"); + } + cbm_unlink(db_path); + char side1044[740]; + snprintf(side1044, sizeof(side1044), "%s-wal", db_path); + cbm_unlink(side1044); + snprintf(side1044, sizeof(side1044), "%s-shm", db_path); + cbm_unlink(side1044); + cbm_rmdir(cache); + PASS(); +} + /* ══════════════════════════════════════════════════════════════════ * QUERY STORE READ-ONLY (data-integrity reproductions) * @@ -5763,6 +5838,7 @@ SUITE(mcp) { RUN_TEST(tool_bad_project_name_no_overflow_issue235); RUN_TEST(tool_bad_project_error_valid_json_issue235); RUN_TEST(tool_resolve_store_by_internal_name_issue704); + RUN_TEST(tool_list_projects_ignores_missed_shadow_issue1044); /* auto_watch gate (distilled from PR #625) */ RUN_TEST(mcp_auto_watch_default_registers_watcher_on_connect); From 2833ef55f7ca80a78771dfe8c3c3119d65e829d3 Mon Sep 17 00:00:00 2001 From: Tommy Corbett Date: Fri, 10 Jul 2026 11:12:18 +0200 Subject: [PATCH 4/7] fix(incremental): name File nodes by basename, matching the full pipeline The incremental path passed changed_files[i].rel_path as the File node NAME, while the full pipeline names File nodes by basename (pipeline.c "Create File node"). Node upserts match by qualified name, so an incremental re-index of a touched file RENAMED its File node in place ("applications.py" -> "fastapi/applications.py"). File nodes are USAGE-edge sources, so: - the same tree yields different graphs depending on whether it was indexed fresh or incrementally, and the divergence grows as files are edited (add AND modify both rename); - name-based traversal dead-ends at any touched file's node (e.g. walking inbound USAGE transitively misses everything that reaches a target through a recently added/modified class). Mirror the full pipeline's node shape at the incremental call site: basename as the name, and the same {"extension": ...} props (previously "{}"). Adds incr_file_node_name_stays_basename to the incremental suite: modify a file, incremental re-index, then assert no File node name contains '/'. Red without the fix, green with it; full incremental suite 162/162. Signed-off-by: Tommy Corbett --- src/pipeline/pipeline_incremental.c | 13 ++++++++++-- tests/test_incremental.c | 31 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index ee69fc9c2..3b223e2bc 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -848,8 +848,17 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil for (int i = 0; i < ci; i++) { char *file_qn = cbm_pipeline_fqn_compute(project, changed_files[i].rel_path, "__file__"); if (file_qn) { - cbm_gbuf_upsert_node(existing, "File", changed_files[i].rel_path, file_qn, - changed_files[i].rel_path, 0, 0, "{}"); + /* #994: the name must be the BASENAME with extension props, + * mirroring the full build's File node (pipeline.c) — upserts + * match by QN, so any other name here renames the node in place + * and the incremental graph diverges from a full build. */ + const char *rel = changed_files[i].rel_path; + const char *slash = strrchr(rel, '/'); + const char *basename = slash ? slash + SKIP_ONE : rel; + char props[CBM_SZ_256]; + const char *ext = strrchr(basename, '.'); + snprintf(props, sizeof(props), "{\"extension\":\"%s\"}", ext ? ext : ""); + cbm_gbuf_upsert_node(existing, "File", basename, file_qn, rel, 0, 0, props); free(file_qn); } } diff --git a/tests/test_incremental.c b/tests/test_incremental.c index ae425b936..00c2ea53a 100644 --- a/tests/test_incremental.c +++ b/tests/test_incremental.c @@ -964,6 +964,36 @@ static int resp_lacks_key(const char *resp, const char *key) { /* ── list_projects ─────────────────────────────────────────────── */ +TEST(incr_file_node_name_stays_basename) { + /* #994: File nodes must keep their basename NAME through an incremental + * re-index (the bug renamed touched files' nodes to rel_path, diverging + * from a full build). Modify a file, re-index, assert no File node name + * contains a path separator. */ + char path[512]; + snprintf(path, sizeof(path), "%s/fastapi/applications.py", g_repodir); + FILE *f = fopen(path, "a"); + ASSERT(f != NULL); + fprintf(f, "\n# incr_file_node_name_stays_basename touch\n"); + fclose(f); + + char *resp = index_repo(); + ASSERT(resp != NULL); + free(resp); + + double ms; + char *r = call_tool_timed("query_graph", &ms, + "{\"project\":\"%s\",\"format\":\"json\"," + "\"query\":\"MATCH (n:File) WHERE n.name CONTAINS '/'" + " RETURN n.name LIMIT 5\"}", + g_project); + TOOL_OK(r, ms); + /* No File node may carry a path separator in its name; the query engine + * returns an empty rows array when nothing matches. */ + ASSERT(strstr(r, "\"rows\":[]") != NULL || strstr(r, "\\\"rows\\\":[]") != NULL); + free(r); + PASS(); +} + TEST(tool_list_projects_basic) { double ms; char *r = call_tool_timed("list_projects", &ms, "{}"); @@ -2970,6 +3000,7 @@ SUITE(incremental) { /* Phase 3: Incremental deltas */ RUN_TEST(incr_modify_file); + RUN_TEST(incr_file_node_name_stays_basename); RUN_TEST(incr_formatter_run); RUN_TEST(incr_add_file); RUN_TEST(incr_delete_file); From 99d39192cd2928ea8bf44233964fe120b201607a Mon Sep 17 00:00:00 2001 From: Blank_Answer <97771966+blankanswer@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:21:54 +0000 Subject: [PATCH 5/7] fix(cpp): remap preprocessed recovery lines Signed-off-by: Blank_Answer <97771966+blankanswer@users.noreply.github.com> --- internal/cbm/cbm.c | 130 +++++++++++++++++++++-- internal/cbm/preprocessor.cpp | 192 +++++++++++++++++++++++++++++----- internal/cbm/preprocessor.h | 18 ++++ tests/test_extraction.c | 136 ++++++++++++++++++++++++ 4 files changed, 439 insertions(+), 37 deletions(-) diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index af454426f..8409aa9d8 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -849,6 +849,106 @@ static bool cbm_line_contains(const char *src, int src_len, uint32_t line, const return false; } +static bool cbm_identifier_char(char c) { + return isalnum((unsigned char)c) || c == '_'; +} + +/* Verify that the mapped original span contains callable-definition syntax, + * not merely the name at a macro invocation or call site. */ +static bool cbm_span_contains_callable_def(const char *src, int src_len, uint32_t start_line, + uint32_t end_line, const char *name) { + if (!src || src_len <= 0 || !name || !name[0] || start_line == 0 || end_line < start_line) { + return false; + } + int span_start = 0; + uint32_t line = 1; + while (span_start < src_len && line < start_line) { + if (src[span_start++] == '\n') { + line++; + } + } + if (line != start_line) { + return false; + } + int span_end = span_start; + while (span_end < src_len && line <= end_line) { + if (src[span_end++] == '\n') { + line++; + } + } + size_t name_len = strlen(name); + for (int pos = span_start; pos + (int)name_len <= span_end; pos++) { + if (strncmp(src + pos, name, name_len) != 0 || + (pos > 0 && cbm_identifier_char(src[pos - 1])) || + (pos + (int)name_len < src_len && cbm_identifier_char(src[pos + name_len]))) { + continue; + } + int before = pos; + while (before > span_start && isspace((unsigned char)src[before - 1])) { + before--; + } + if (before > span_start && strchr("(,=!?[.", src[before - 1])) { + continue; + } + int open = pos + (int)name_len; + while (open < span_end && isspace((unsigned char)src[open])) { + open++; + } + if (open >= span_end || src[open] != '(') { + continue; + } + int depth = 0; + int close = -1; + for (int i = open; i < span_end; i++) { + if (src[i] == '(') { + depth++; + } else if (src[i] == ')' && --depth == 0) { + close = i; + break; + } + } + if (close < 0) { + continue; + } + for (int i = close + 1; i < span_end; i++) { + if (src[i] == '{') { + return true; + } + if (src[i] == ';') { + break; + } + } + } + return false; +} + +/* Remap an expanded-source definition back to the original input file. Every + * line in the definition must be attributable to the main file; generated + * macro bodies, included headers, and ambiguous spans fail closed. */ +static bool cbm_remap_preprocessed_def(CBMDefinition *def, const CBMPreprocessedSource *pp) { + if (!def || !pp || !pp->original_line_by_expanded_line || !pp->belongs_to_main_file || + def->start_line == 0 || def->end_line < def->start_line || + def->end_line > (uint32_t)pp->expanded_line_count) { + return false; + } + + uint32_t original_start = pp->original_line_by_expanded_line[def->start_line]; + uint32_t original_end = pp->original_line_by_expanded_line[def->end_line]; + if (!original_start || !original_end || original_end < original_start) { + return false; + } + for (uint32_t line = def->start_line; line <= def->end_line; line++) { + if (!pp->belongs_to_main_file[line] || !pp->original_line_by_expanded_line[line]) { + return false; + } + } + + def->start_line = original_start; + def->end_line = original_end; + def->lines = (int)(original_end - original_start + 1); + return true; +} + static void cbm_subtract_recovered_regions(cbm_error_regions_t *regs, const CBMDefArray *defs) { int kept = 0; for (int i = 0; i < regs->count; i++) { @@ -1088,9 +1188,10 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, // Defs keep original-source line numbers; only CALLS are extracted from expanded source. if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) { uint64_t pp_start = now_ns(); - char *expanded = cbm_preprocess(source, source_len, rel_path, extra_defines, include_paths, - language != CBM_LANG_C); - if (expanded) { + CBMPreprocessedSource *preprocessed = cbm_preprocess_with_map( + source, source_len, rel_path, extra_defines, include_paths, language != CBM_LANG_C); + if (preprocessed && preprocessed->source) { + char *expanded = preprocessed->source; int expanded_len = (int)strlen(expanded); // Record calls count before second pass int calls_before = result->calls.count; @@ -1155,15 +1256,20 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, for (int i = defs_before; i < result->defs.count; i++) { CBMDefinition *d = &result->defs.items[i]; bool adopt = false; - for (int rj = 0; rj < raw_regs.count && !adopt; rj++) { - if (d->start_line <= raw_regs.ends[rj] && - d->end_line >= raw_regs.starts[rj]) { - adopt = true; + if (cbm_remap_preprocessed_def(d, preprocessed)) { + for (int rj = 0; rj < raw_regs.count && !adopt; rj++) { + if (d->start_line <= raw_regs.ends[rj] && + d->end_line >= raw_regs.starts[rj]) { + adopt = true; + } } } - if (adopt && - (!d->name || !cbm_line_contains(source, source_len, - d->start_line, d->name))) { + if (adopt && (!d->name || + !cbm_line_contains(source, source_len, d->start_line, + d->name) || + !cbm_span_contains_callable_def( + source, source_len, d->start_line, d->end_line, + d->name))) { adopt = false; } for (int j = 0; j < defs_before && adopt; j++) { @@ -1184,9 +1290,11 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len, ts_tree_delete(pp_tree); } } - cbm_preprocess_free(expanded); + cbm_preprocessed_source_free(preprocessed); atomic_fetch_add(&total_files_preprocessed, 1); (void)calls_before; // used for future logging + } else { + cbm_preprocessed_source_free(preprocessed); } atomic_fetch_add(&total_preprocess_ns, now_ns() - pp_start); } diff --git a/internal/cbm/preprocessor.cpp b/internal/cbm/preprocessor.cpp index 69f91c35a..54cb073fe 100644 --- a/internal/cbm/preprocessor.cpp +++ b/internal/cbm/preprocessor.cpp @@ -10,45 +10,145 @@ #include #include #include +#include extern "C" { -char* cbm_preprocess( - const char* source, int source_len, - const char* filename, - const char** extra_defines, - const char** include_paths, - int cpp_mode -) { - if (!source || source_len <= 0) return NULL; - - // Fast-path: skip if no preprocessor directives worth expanding. - bool has_macros = false; +static bool has_preprocessor_work(const char *source, int source_len) { + if (!source || source_len <= 0) { + return false; + } for (int i = 0; i < source_len - 1; i++) { if (source[i] == '#') { // Skip whitespace after # int j = i + 1; - while (j < source_len && (source[j] == ' ' || source[j] == '\t')) j++; + while (j < source_len && (source[j] == ' ' || source[j] == '\t')) + j++; int remaining = source_len - j; if (remaining >= 6 && strncmp(source + j, "define", 6) == 0) { - has_macros = true; - break; + return true; } if (remaining >= 5 && strncmp(source + j, "ifdef", 5) == 0) { - has_macros = true; - break; + return true; } if (remaining >= 6 && strncmp(source + j, "ifndef", 6) == 0) { - has_macros = true; - break; + return true; } if (remaining >= 3 && strncmp(source + j, "if ", 3) == 0) { - has_macros = true; - break; + return true; } } } - if (!has_macros) return NULL; // NULL = no expansion needed, use original + return false; +} + +static int count_expanded_lines(const std::string &text) { + int count = 1; + for (char c : text) { + if (c == '\n') { + count++; + } + } + return count; +} + +static bool parse_line_directive(const char *line, size_t len, uint32_t *out_line, + std::string *out_file) { + size_t i = 0; + while (i < len && (line[i] == ' ' || line[i] == '\t')) { + i++; + } + if (i >= len || line[i++] != '#') { + return false; + } + while (i < len && (line[i] == ' ' || line[i] == '\t')) { + i++; + } + static const char prefix[] = "line"; + if (i + sizeof(prefix) - 1 > len || strncmp(line + i, prefix, sizeof(prefix) - 1) != 0) { + return false; + } + i += sizeof(prefix) - 1; + if (i >= len || (line[i] != ' ' && line[i] != '\t')) { + return false; + } + while (i < len && (line[i] == ' ' || line[i] == '\t')) { + i++; + } + if (i >= len || line[i] < '0' || line[i] > '9') { + return false; + } + uint64_t parsed_line = 0; + while (i < len && line[i] >= '0' && line[i] <= '9') { + parsed_line = parsed_line * 10u + (uint64_t)(line[i] - '0'); + if (parsed_line > UINT32_MAX) { + return false; + } + i++; + } + while (i < len && (line[i] == ' ' || line[i] == '\t')) { + i++; + } + if (i >= len || line[i++] != '"') { + return false; + } + size_t file_start = i; + while (i < len && line[i] != '"') { + i++; + } + if (i >= len) { + return false; + } + *out_line = (uint32_t)parsed_line; + *out_file = std::string(line + file_start, i - file_start); + return true; +} + +static bool build_line_map(const std::string &expanded, const std::string &main_file, + uint32_t *original_line_by_expanded_line, + uint8_t *belongs_to_main_file) { + std::string current_file = main_file; + uint32_t current_line = 1; + int expanded_line = 1; + size_t line_start = 0; + + while (line_start <= expanded.size()) { + size_t line_end = expanded.find('\n', line_start); + if (line_end == std::string::npos) { + line_end = expanded.size(); + } + + uint32_t directive_line = 0; + std::string directive_file; + if (parse_line_directive(expanded.c_str() + line_start, line_end - line_start, + &directive_line, &directive_file)) { + current_file = directive_file; + current_line = directive_line; + original_line_by_expanded_line[expanded_line] = 0; + belongs_to_main_file[expanded_line] = 0; + } else { + original_line_by_expanded_line[expanded_line] = current_line; + belongs_to_main_file[expanded_line] = current_file == main_file ? 1 : 0; + if (current_line < UINT32_MAX) { + current_line++; + } + } + + if (line_end == expanded.size()) { + break; + } + line_start = line_end + 1; + expanded_line++; + } + return true; +} + +CBMPreprocessedSource *cbm_preprocess_with_map(const char *source, int source_len, + const char *filename, const char **extra_defines, + const char **include_paths, int cpp_mode) { + if (!has_preprocessor_work(source, source_len)) { + return NULL; // NULL = no expansion needed, use original + } try { simplecpp::DUI dui; @@ -78,18 +178,58 @@ char* cbm_preprocess( // Clean up loaded file data simplecpp::cleanup(filedata); - char* out = (char*)malloc(result.size() + 1); - if (!out) return NULL; - memcpy(out, result.c_str(), result.size() + 1); - return out; + CBMPreprocessedSource *pp = (CBMPreprocessedSource *)calloc(1, sizeof(*pp)); + if (!pp) { + return NULL; + } + int line_count = count_expanded_lines(result); + pp->source = (char *)malloc(result.size() + 1); + pp->original_line_by_expanded_line = + (uint32_t *)calloc((size_t)line_count + 1u, sizeof(uint32_t)); + pp->belongs_to_main_file = (uint8_t *)calloc((size_t)line_count + 1u, sizeof(uint8_t)); + pp->expanded_line_count = line_count; + if (!pp->source || !pp->original_line_by_expanded_line || !pp->belongs_to_main_file) { + cbm_preprocessed_source_free(pp); + return NULL; + } + memcpy(pp->source, result.c_str(), result.size() + 1); + if (!build_line_map(result, files[0], pp->original_line_by_expanded_line, + pp->belongs_to_main_file)) { + cbm_preprocessed_source_free(pp); + return NULL; + } + return pp; } catch (...) { // Graceful fallback: return NULL = use original source return NULL; } } -void cbm_preprocess_free(char* expanded) { +char *cbm_preprocess(const char *source, int source_len, const char *filename, + const char **extra_defines, const char **include_paths, int cpp_mode) { + CBMPreprocessedSource *pp = cbm_preprocess_with_map(source, source_len, filename, extra_defines, + include_paths, cpp_mode); + if (!pp) { + return NULL; + } + char *out = pp->source; + pp->source = NULL; + cbm_preprocessed_source_free(pp); + return out; +} + +void cbm_preprocess_free(char *expanded) { free(expanded); } +void cbm_preprocessed_source_free(CBMPreprocessedSource *pp) { + if (!pp) { + return; + } + free(pp->source); + free(pp->original_line_by_expanded_line); + free(pp->belongs_to_main_file); + free(pp); +} + } // extern "C" diff --git a/internal/cbm/preprocessor.h b/internal/cbm/preprocessor.h index 06edbc087..72828c012 100644 --- a/internal/cbm/preprocessor.h +++ b/internal/cbm/preprocessor.h @@ -2,11 +2,19 @@ #define CBM_PREPROCESSOR_H #include +#include #ifdef __cplusplus extern "C" { #endif +typedef struct { + char *source; + uint32_t *original_line_by_expanded_line; // 1-based; 0 means directive/unmapped. + uint8_t *belongs_to_main_file; // 1-based; true only for the original input file. + int expanded_line_count; +} CBMPreprocessedSource; + // Preprocess C/C++ source: expand macros, evaluate #ifdef, resolve #include. // Returns malloc-allocated expanded source, or NULL if no expansion needed/on failure. // extra_defines: NULL-terminated array of "NAME=VALUE" strings (can be NULL). @@ -15,9 +23,19 @@ extern "C" { char *cbm_preprocess(const char *source, int source_len, const char *filename, const char **extra_defines, const char **include_paths, int cpp_mode); +// Preprocess and return source plus expanded-line -> original-line ownership map. +// Returns NULL if no expansion is needed or preprocessing fails. +// Free with cbm_preprocessed_source_free(). +CBMPreprocessedSource *cbm_preprocess_with_map(const char *source, int source_len, + const char *filename, const char **extra_defines, + const char **include_paths, int cpp_mode); + // Free preprocessed source returned by cbm_preprocess. void cbm_preprocess_free(char *expanded); +// Free preprocessed source and line maps returned by cbm_preprocess_with_map. +void cbm_preprocessed_source_free(CBMPreprocessedSource *pp); + #ifdef __cplusplus } #endif diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 686691974..eed871ff7 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -8,6 +8,7 @@ #include "test_framework.h" #include "cbm.h" #include "../src/foundation/compat.h" /* cbm_clock_gettime (wide-flat scaling guard) */ +#include "../src/foundation/compat_fs.h" #include /* ── Helpers ───────────────────────────────────────────────────── */ @@ -3426,6 +3427,138 @@ TEST(extract_c_ifdef_split_brace_fn_recovered_issue961) { PASS(); } +static const char *CPP_PREPROC_SIGNATURE_GAP_SRC = + "struct Rect {};\n" + "struct IBinder {};\n" + "struct IRegionSamplingListener {};\n" + "typedef int status_t;\n" + "\n" + "class SurfaceFlinger {\n" + "public:\n" + " status_t addRegionSamplingListener(const Rect&, const IBinder&,\n" + " const IRegionSamplingListener&, bool);\n" + " status_t addRegionSamplingListener(const Rect&, const IBinder&,\n" + " const IRegionSamplingListener&);\n" + " void commit();\n" + " void composite();\n" + "};\n" + "\n" + "#ifdef FLYME_GRAPHICS_EXTEND_LUMARGB\n" + "status_t SurfaceFlinger::addRegionSamplingListener(const Rect& samplingArea,\n" + " const IBinder& stopLayerHandle,\n" + " const IRegionSamplingListener& listener,\n" + " const bool rgbSample) {\n" + "#else\n" + "status_t SurfaceFlinger::addRegionSamplingListener(const Rect& samplingArea,\n" + " const IBinder& stopLayerHandle,\n" + " const IRegionSamplingListener& listener) " + "{\n" + "#endif\n" + " return 0;\n" + "}\n" + "\n" + "void SurfaceFlinger::commit() {}\n" + "\n" + "void SurfaceFlinger::composite() {}\n"; + +/* #946 fixture from the original report: both preprocessor choices must keep + * raw definitions primary while recovering later methods at original lines. */ +TEST(extract_cpp_preproc_signature_gap_issue946) { + const char *defines[] = {"FLYME_GRAPHICS_EXTEND_LUMARGB", NULL}; + for (int enabled = 0; enabled < 2; enabled++) { + CBMFileResult *r = cbm_extract_file( + CPP_PREPROC_SIGNATURE_GAP_SRC, (int)strlen(CPP_PREPROC_SIGNATURE_GAP_SRC), CBM_LANG_CPP, + "t", "SurfaceFlinger.cpp", 0, enabled ? defines : NULL, NULL); + ASSERT_NOT_NULL(r); + const CBMDefinition *add = find_def(r, "addRegionSamplingListener"); + const CBMDefinition *commit = find_def(r, "commit"); + const CBMDefinition *composite = find_def(r, "composite"); + ASSERT_NOT_NULL(add); + ASSERT_NOT_NULL(commit); + ASSERT_NOT_NULL(composite); + ASSERT_EQ(add->start_line, 22u); + ASSERT_EQ(add->end_line, 27u); + ASSERT_EQ(commit->start_line, 29u); + ASSERT_EQ(commit->end_line, 29u); + ASSERT_EQ(composite->start_line, 31u); + ASSERT_EQ(composite->end_line, 31u); + cbm_free_result(r); + } + PASS(); +} + +/* Macro expansion can produce callable-looking AST nodes, but no callable + * definition exists in the original span; recovery must fail closed. */ +TEST(extract_cpp_preproc_macro_generated_callable_skipped_issue949) { + const char *src = "#define MAKE_FN(name) int name() { return 1; }\n" + "#ifdef ENABLE_GENERATED\n" + "MAKE_FN(generated)\n" + "#endif\n" + "int visible() { return 0; }\n"; + const char *defines[] = {"ENABLE_GENERATED", NULL}; + CBMFileResult *r = + cbm_extract_file(src, (int)strlen(src), CBM_LANG_CPP, "t", "macro.cpp", 0, defines, NULL); + ASSERT_NOT_NULL(r); + ASSERT_NULL(find_def(r, "generated")); + ASSERT_NOT_NULL(find_def(r, "visible")); + ASSERT_TRUE(r->parse_incomplete); + ASSERT_GTE(r->error_region_count, 1); + ASSERT_NOT_NULL(r->error_ranges); + cbm_free_result(r); + PASS(); +} + +/* #949 follow-up: an included header shifts physical lines in simplecpp's + * expanded output. The #1050 name-on-same-line guard skipped this recoverable + * definition; explicit source ownership mapping must restore its original + * coordinates while keeping header definitions out of the main file. */ +TEST(extract_c_ifdef_split_brace_after_include_remapped_issue949) { + char tmpdir[512]; + snprintf(tmpdir, sizeof(tmpdir), "%s/cbm_line_map_XXXXXX", cbm_tmpdir()); + ASSERT_NOT_NULL(cbm_mkdtemp(tmpdir)); + + char header_path[512]; + snprintf(header_path, sizeof(header_path), "%s/padding.h", tmpdir); + FILE *header = cbm_fopen(header_path, "wb"); + ASSERT_NOT_NULL(header); + for (int i = 0; i < 40; i++) { + ASSERT_GTE(fprintf(header, "static int header_pad_%d(void) { return %d; }\n", i, i), 0); + } + ASSERT_EQ(fclose(header), 0); + + const char *includes[] = {tmpdir, NULL}; + const char *src = "#include \"padding.h\"\n" + "static int before(void) { return 1; }\n" + "int shifted_split(int x) {\n" + "#ifdef _WIN32\n" + " if (before()) {\n" + "#else\n" + " if (x > 0) {\n" + "#endif\n" + " x += 1;\n" + " }\n" + " return x;\n" + "}\n" + "int after(void) { return 2; }\n"; + CBMFileResult *r = + cbm_extract_file(src, (int)strlen(src), CBM_LANG_C, "t", "shifted.c", 0, NULL, includes); + cbm_unlink(header_path); + cbm_rmdir(tmpdir); + + ASSERT_NOT_NULL(r); + const CBMDefinition *d = find_def(r, "shifted_split"); + ASSERT_NOT_NULL(d); + ASSERT_EQ(d->start_line, 3u); + ASSERT_EQ(d->end_line, 12u); + ASSERT_NULL(find_def(r, "header_pad_0")); + ASSERT_NOT_NULL(find_def(r, "after")); + ASSERT_FALSE(r->parse_incomplete); + ASSERT_EQ(r->error_region_count, 0); + ASSERT_NULL(r->error_ranges); + cbm_free_result(r); + PASS(); +} + /* #961 inverse guard: a clean C file must not gain duplicate or phantom * defs from the recovery path (it only engages on raw-parse ERROR regions). */ TEST(extract_c_clean_file_no_recovery_duplicates_issue961) { @@ -3865,6 +3998,9 @@ SUITE(extraction) { RUN_TEST(complexity_delegation_receivers_not_recursive_issue876); RUN_TEST(complexity_access_depth_and_params); RUN_TEST(extract_c_ifdef_split_brace_fn_recovered_issue961); + RUN_TEST(extract_cpp_preproc_signature_gap_issue946); + RUN_TEST(extract_cpp_preproc_macro_generated_callable_skipped_issue949); + RUN_TEST(extract_c_ifdef_split_brace_after_include_remapped_issue949); RUN_TEST(extract_c_clean_file_no_recovery_duplicates_issue961); RUN_TEST(walk_defs_no_truncation_over_4096_issue668); RUN_TEST(extract_rust_test_attr_marks_is_test_issue855); From 041eb99c11692b93d43ea72c884cc50c51285db5 Mon Sep 17 00:00:00 2001 From: Charles Queiroz Date: Fri, 10 Jul 2026 13:03:43 -0300 Subject: [PATCH 6/7] fix: compose JAX-RS method-level @Path with class-level prefix (#1005) JAX-RS splits a route across two annotations: the verb comes from a bare @GET/@POST/... and the path from a sibling @Path. The annotation scan returned on the first mapping annotation, so the @GET matched, defaulted the path to "/" and the sibling @Path was never read. Class-level @Path was never recognized as a prefix either, because a lone @Path carries no verb and the scan reported no route. scan_route_annotations() now collects the whole annotation set (mapping verb + optional inline path, and a separate JAX-RS @Path), then: - method-level: verb required; path = inline mapping path, else @Path, else "/" - class-level prefix: inline mapping path, else @Path Also recognizes bare @HEAD/@OPTIONS verbs. Spring behavior is unchanged (mapping annotations keep their inline path); covered by the existing test suite plus a new regression test. Signed-off-by: Charles Queiroz --- internal/cbm/extract_defs.c | 113 ++++++++++++++++++++++++++-------- tests/test_edge_types_probe.c | 28 ++++++++- tests/test_extraction.c | 33 ++++++++++ 3 files changed, 146 insertions(+), 28 deletions(-) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 2de63ffbb..b4c79c890 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -1296,7 +1296,8 @@ static const char *annotation_route_method(const char *name) { } /* JAX-RS bare-verb annotations (@GET/@POST/...) — path comes from @Path. */ if (strcmp(name, "GET") == 0 || strcmp(name, "POST") == 0 || strcmp(name, "PUT") == 0 || - strcmp(name, "DELETE") == 0 || strcmp(name, "PATCH") == 0) { + strcmp(name, "DELETE") == 0 || strcmp(name, "PATCH") == 0 || strcmp(name, "HEAD") == 0 || + strcmp(name, "OPTIONS") == 0) { return name; } return NULL; @@ -1605,35 +1606,85 @@ static bool try_route_from_annotation(CBMArena *a, TSNode annotation, const char return true; } -/* Scan the annotation nodes nested in a JVM/C# `modifiers`/`attribute_list` - * wrapper (and direct children) for a route-mapping annotation. Java/Kotlin - * Spring annotations (@GetMapping, @RequestMapping, ...) live here rather than - * as prev-siblings, so the prev-sibling decorator walk never sees them. */ -static bool extract_route_from_annotations(CBMArena *a, TSNode func_node, const char *source, - const CBMLangSpec *spec, const char **out_path, - const char **out_method) { - TSNode modifiers = find_jvm_modifiers(func_node, spec->language); +/* Scan ALL annotation nodes nested in a JVM/C# `modifiers`/`attribute_list` + * wrapper (and direct children), collecting route information from the whole + * set instead of stopping at the first mapping annotation. Java/Kotlin Spring + * annotations (@GetMapping, @RequestMapping, ...) live here rather than as + * prev-siblings, so the prev-sibling decorator walk never sees them. + * + * JAX-RS splits the route across two annotations: the verb comes from a bare + * @GET/@POST/... and the path from a sibling @Path("..."). Returning on the + * first mapping annotation therefore dropped every method-level @Path + * (the @GET matched first, defaulted the path to "/", and @Path was never + * read), and class-level @Path prefixes were never recognized at all. */ +static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source, + const CBMLangSpec *spec, const char **out_map_path, + const char **out_method, const char **out_jax_path) { + *out_map_path = NULL; + *out_method = NULL; + *out_jax_path = NULL; + + TSNode wrappers[2]; + int wn = 0; + TSNode modifiers = find_jvm_modifiers(owner, spec->language); if (!ts_node_is_null(modifiers)) { - uint32_t mc = ts_node_child_count(modifiers); - for (uint32_t mi = 0; mi < mc; mi++) { - TSNode mchild = ts_node_child(modifiers, mi); - if (cbm_kind_in_set(mchild, spec->decorator_node_types) && - try_route_from_annotation(a, mchild, source, out_path, out_method)) { - return true; - } - } + wrappers[wn++] = modifiers; } /* Direct-child annotations (some grammars attach the annotation as a child * of the method node rather than under `modifiers`). */ - uint32_t cc = ts_node_child_count(func_node); - for (uint32_t ci = 0; ci < cc; ci++) { - TSNode child = ts_node_child(func_node, ci); - if (cbm_kind_in_set(child, spec->decorator_node_types) && - try_route_from_annotation(a, child, source, out_path, out_method)) { - return true; + wrappers[wn++] = owner; + + for (int w = 0; w < wn; w++) { + uint32_t cc = ts_node_child_count(wrappers[w]); + for (uint32_t ci = 0; ci < cc; ci++) { + TSNode child = ts_node_child(wrappers[w], ci); + if (!cbm_kind_in_set(child, spec->decorator_node_types)) { + continue; + } + TSNode name_node = annotation_name_node(child); + if (ts_node_is_null(name_node)) { + continue; + } + char *name = cbm_node_text(a, name_node, source); + if (!name) { + continue; + } + if (!*out_jax_path && strcmp(name, "Path") == 0) { + TSNode args = annotation_args_node(child); + if (!ts_node_is_null(args)) { + *out_jax_path = extract_route_path_from_args(a, args, source); + } + continue; + } + if (!*out_method) { + const char *method = annotation_route_method(name); + if (method) { + *out_method = method; + TSNode args = annotation_args_node(child); + if (!ts_node_is_null(args)) { + *out_map_path = extract_route_path_from_args(a, args, source); + } + } + } } } - return false; +} + +static bool extract_route_from_annotations(CBMArena *a, TSNode func_node, const char *source, + const CBMLangSpec *spec, const char **out_path, + const char **out_method) { + const char *map_path = NULL; + const char *method = NULL; + const char *jax_path = NULL; + scan_route_annotations(a, func_node, source, spec, &map_path, &method, &jax_path); + /* Method-level routes still require a verb/mapping annotation; a lone + * @Path (JAX-RS sub-resource locator) is not an endpoint by itself. */ + if (!method) { + return false; + } + *out_method = method; + *out_path = map_path ? map_path : (jax_path ? jax_path : "/"); + return true; } static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const char *source, @@ -1702,10 +1753,18 @@ static const char *join_route_paths(CBMArena *a, const char *prefix, const char static const char *spring_class_route_prefix(CBMArena *a, TSNode class_node, const char *source, const CBMLangSpec *spec) { - const char *prefix = NULL; + const char *map_path = NULL; const char *method = NULL; - if (extract_route_from_annotations(a, class_node, source, spec, &prefix, &method)) { - return prefix; + const char *jax_path = NULL; + scan_route_annotations(a, class_node, source, spec, &map_path, &method, &jax_path); + if (map_path) { + return map_path; /* @RequestMapping("/api") and friends */ + } + if (jax_path) { + return jax_path; /* JAX-RS class-level @Path("/api") carries no verb */ + } + if (method) { + return "/"; /* mapping annotation without a path argument */ } return NULL; } diff --git a/tests/test_edge_types_probe.c b/tests/test_edge_types_probe.c index ca4f24419..f38b4e9cd 100644 --- a/tests/test_edge_types_probe.c +++ b/tests/test_edge_types_probe.c @@ -381,6 +381,31 @@ TEST(handles_spring_kotlin) { PASS(); } +/* JAX-RS (Java): the verb (@GET) and the path (@Path) are sibling + * annotations, and the class-level @Path must prefix method-level paths. + * Reproduce-first (#1005): the first-mapping-annotation scan dropped every + * method-level @Path, collapsing all methods onto a single "/" Route node; + * only the exact Route-name set catches that through the emission dedup. */ +TEST(handles_jaxrs_java) { + static const char *routes[] = {"/api/v1/widgets", "/api/v1/widgets/count", NULL}; + static const EtFile f[] = { + {"WidgetResource.java", + "package com.example;\n\n" + "import jakarta.ws.rs.GET;\n" + "import jakarta.ws.rs.Path;\n\n" + "@Path(\"/api/v1/widgets\")\npublic class WidgetResource {\n" + " @GET\n" + " public String list() {\n" + " return \"widgets\";\n }\n\n" + " @GET\n" + " @Path(\"/count\")\n" + " public String count() {\n" + " return \"42\";\n }\n}\n"}}; + ASSERT_TRUE(et_edge_present(f, 1, "HANDLES", 2)); + ASSERT_TRUE(et_routes_exact(f, 1, routes)); + PASS(); +} + /* ASP.NET Minimal API (C#) — route registration via static MapGet/MapPost calls * with identifier handlers, under a Microsoft/AspNetCore path so the resolved * callee QN carries the "MapGet"/"Microsoft.AspNetCore" route-reg substrings. @@ -1442,7 +1467,7 @@ TEST(override_go_interface) { * ══════════════════════════════════════════════════════════════════ */ SUITE(edge_types_probe) { - /* HANDLES — route→handler across web frameworks (8 frameworks) */ + /* HANDLES — route→handler across web frameworks */ RUN_TEST(handles_flask_python); RUN_TEST(handles_fastapi_python); RUN_TEST(handles_drf_action_python); @@ -1451,6 +1476,7 @@ SUITE(edge_types_probe) { RUN_TEST(handles_gin_go); RUN_TEST(handles_spring_java); RUN_TEST(handles_spring_kotlin); + RUN_TEST(handles_jaxrs_java); RUN_TEST(handles_aspnet_csharp); RUN_TEST(handles_laravel_php); RUN_TEST(handles_rails_ruby); diff --git a/tests/test_extraction.c b/tests/test_extraction.c index f8228e46a..5904bbb31 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -2728,6 +2728,38 @@ TEST(extract_java_method_annotations_issue382) { PASS(); } +/* Issue #1005: JAX-RS splits a route across two annotations (@GET carries the + * verb, a sibling @Path carries the path). Returning on the first mapping + * annotation dropped every method-level @Path, and the class-level @Path + * prefix was never recognized at all. */ +TEST(extract_java_jaxrs_path_composition_issue1005) { + CBMFileResult *r = extract("import jakarta.ws.rs.GET;\n" + "import jakarta.ws.rs.Path;\n" + "@Path(\"/api/v1/widgets\")\n" + "public class WidgetResource {\n" + " @GET\n" + " public String list() { return \"\"; }\n" + " @GET\n" + " @Path(\"/count\")\n" + " public String count() { return \"\"; }\n" + "}\n", + CBM_LANG_JAVA, "t", "WidgetResource.java"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + const CBMDefinition *list = find_def_by_name(r, "list"); + ASSERT_NOT_NULL(list); + ASSERT_NOT_NULL(list->route_path); + ASSERT_STR_EQ(list->route_path, "/api/v1/widgets"); + ASSERT_STR_EQ(list->route_method, "GET"); + const CBMDefinition *count = find_def_by_name(r, "count"); + ASSERT_NOT_NULL(count); + ASSERT_NOT_NULL(count->route_path); + ASSERT_STR_EQ(count->route_path, "/api/v1/widgets/count"); + ASSERT_STR_EQ(count->route_method, "GET"); + cbm_free_result(r); + PASS(); +} + /* Find an in-body call by its raw callee text; returns the call or NULL. */ static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee) { for (int i = 0; i < r->calls.count; i++) { @@ -3720,6 +3752,7 @@ SUITE(extraction) { RUN_TEST(js_index_module_qn_not_collide_with_folder); RUN_TEST(python_regular_module_qn_unchanged); RUN_TEST(extract_java_method_annotations_issue382); + RUN_TEST(extract_java_jaxrs_path_composition_issue1005); RUN_TEST(extract_java_no_double_class_qn); RUN_TEST(extract_go_no_filename_in_module_qn); RUN_TEST(extract_large_ts_has_functions_issue213); From 6207d16fdf46c1266cf87b7c7949fabcc3e2c4fa Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 15 Jul 2026 00:04:05 +0200 Subject: [PATCH 7/7] fix(review): keep vendored scanners byte-identical; wide-safe XML sniff Two review items applied maintainer-side to save a roundtrip: 1. Restore both vendored ObjectScript scanner.c files to the pristine upstream bytes (a formatter pass had rewritten indentation/braces). Vendored grammar files stay byte-for-byte upstream except for the include repoint documented in MANIFEST.md - style churn breaks upstream diffability on re-vendor. 2. The Studio Export XML sniff in detect_file_language used a raw fopen on a repository path; cbm_fopen keeps it working on non-ASCII Windows paths (project rule, same class as #996). Signed-off-by: Martin Vogel --- .../grammars/objectscript_routine/scanner.c | 177 +++++++++-------- .../grammars/objectscript_udl/scanner.c | 183 +++++++++--------- src/discover/discover.c | 2 +- 3 files changed, 183 insertions(+), 179 deletions(-) diff --git a/internal/cbm/vendored/grammars/objectscript_routine/scanner.c b/internal/cbm/vendored/grammars/objectscript_routine/scanner.c index c64ac9e42..771db9eef 100644 --- a/internal/cbm/vendored/grammars/objectscript_routine/scanner.c +++ b/internal/cbm/vendored/grammars/objectscript_routine/scanner.c @@ -4,125 +4,122 @@ // Keep this in sync with grammar externals. enum TokenType { - COMPILED_HEADER = OBJECTSCRIPT_CORE_TOKEN_TYPE_MAX, - RTN_DOT, + COMPILED_HEADER = OBJECTSCRIPT_CORE_TOKEN_TYPE_MAX, + RTN_DOT, }; struct ObjectScript_Routine_Scanner { - struct ObjectScript_Core_Scanner core_scanner; + struct ObjectScript_Core_Scanner core_scanner; }; static bool lex_rtn_dot(TSLexer *lexer) { + lexer->mark_end(lexer); + if (lexer->get_column(lexer) != 0) return false; + if (lexer->lookahead != '.') return false; + + advance(lexer); + + // Only match a line that is exactly "." with no trailing content. + if (lexer->eof(lexer) || lexer->lookahead == '\n' || lexer->lookahead == '\r') { lexer->mark_end(lexer); - if (lexer->get_column(lexer) != 0) - return false; - if (lexer->lookahead != '.') - return false; + lexer->result_symbol = RTN_DOT; + return true; + } - advance(lexer); - - // Only match a line that is exactly "." with no trailing content. - if (lexer->eof(lexer) || lexer->lookahead == '\n' || lexer->lookahead == '\r') { - lexer->mark_end(lexer); - lexer->result_symbol = RTN_DOT; - return true; - } - - return false; + return false; } static bool lex_compiled_header(TSLexer *lexer) { - lexer->mark_end(lexer); - while (!lexer->eof(lexer) && lexer->lookahead != '\n') { - advance(lexer); - } - if (lexer->lookahead != '\n') - return false; + lexer->mark_end(lexer); + while(!lexer->eof(lexer) && lexer->lookahead != '\n') { + advance(lexer); + } + if (lexer->lookahead != '\n') return false; - advance(lexer); - if (lexer->lookahead != '%') - return false; - advance(lexer); - if (lexer->lookahead != 'R') - return false; - advance(lexer); - if (lexer->lookahead != 'O') - return false; - advance(lexer); + advance(lexer); + if (lexer->lookahead != '%') return false; + advance(lexer); + if (lexer->lookahead != 'R') return false; + advance(lexer); + if (lexer->lookahead != 'O') return false; + advance(lexer); - lexer->result_symbol = COMPILED_HEADER; - int newline_count = 0; - while (newline_count < 2 && !lexer->eof(lexer)) { - if (lexer->lookahead == '\n') { - newline_count++; - } - advance(lexer); + lexer->result_symbol = COMPILED_HEADER; + int newline_count = 0; + while(newline_count < 2 && !lexer->eof(lexer)) { + if (lexer->lookahead == '\n') { + newline_count++; } - lexer->mark_end(lexer); - return true; + advance(lexer); + } + lexer->mark_end(lexer); + return true; } static bool scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { - struct ObjectScript_Routine_Scanner *scanner = (struct ObjectScript_Routine_Scanner *)payload; - - // Tree-sitter marks all terminals as valid during error recovery. - if (valid_symbols[SENTINEL]) { - return false; - } - - if (ObjectScript_Core_Scanner_scan(&scanner->core_scanner, lexer, valid_symbols)) { - return true; - } - - if (scanner->core_scanner.is_rtn_dot) { - scanner->core_scanner.is_rtn_dot = false; - lexer->mark_end(lexer); - lexer->result_symbol = RTN_DOT; - return true; - } - - if (valid_symbols[COMPILED_HEADER] && lex_compiled_header(lexer)) { - return true; - } - - if (valid_symbols[RTN_DOT] && lex_rtn_dot(lexer)) { - return true; - } + struct ObjectScript_Routine_Scanner *scanner = + (struct ObjectScript_Routine_Scanner *)payload; + // Tree-sitter marks all terminals as valid during error recovery. + if (valid_symbols[SENTINEL]) { return false; + } + + if (ObjectScript_Core_Scanner_scan(&scanner->core_scanner, lexer, + valid_symbols)) { + return true; + } + + if (scanner->core_scanner.is_rtn_dot) { + scanner->core_scanner.is_rtn_dot = false; + lexer->mark_end(lexer); + lexer->result_symbol = RTN_DOT; + return true; + } + + if (valid_symbols[COMPILED_HEADER] && lex_compiled_header(lexer)) { + return true; + } + + if (valid_symbols[RTN_DOT] && lex_rtn_dot(lexer)) { + return true; + } + + return false; } void *tree_sitter_objectscript_routine_external_scanner_create() { - struct ObjectScript_Routine_Scanner *scanner = (struct ObjectScript_Routine_Scanner *)calloc( - 1, sizeof(struct ObjectScript_Routine_Scanner)); - // scanner->saw_compiled_header = false; - // scanner->saw_routine_header = false; - ObjectScript_Core_Scanner_init(&scanner->core_scanner); - scanner->core_scanner.column1_statement_mode = false; - scanner->core_scanner.routine_token_mode = true; - return scanner; + struct ObjectScript_Routine_Scanner *scanner = + (struct ObjectScript_Routine_Scanner *)calloc( + 1, sizeof(struct ObjectScript_Routine_Scanner)); + // scanner->saw_compiled_header = false; + // scanner->saw_routine_header = false; + ObjectScript_Core_Scanner_init(&scanner->core_scanner); + scanner->core_scanner.column1_statement_mode = false; + scanner->core_scanner.routine_token_mode = true; + return scanner; } -bool tree_sitter_objectscript_routine_external_scanner_scan(void *payload, TSLexer *lexer, - const bool *valid_symbols) { - return scan(payload, lexer, valid_symbols); +bool tree_sitter_objectscript_routine_external_scanner_scan( + void *payload, TSLexer *lexer, const bool *valid_symbols) { + return scan(payload, lexer, valid_symbols); } -unsigned tree_sitter_objectscript_routine_external_scanner_serialize(void *payload, char *buffer) { - struct ObjectScript_Routine_Scanner *scanner = (struct ObjectScript_Routine_Scanner *)payload; - memcpy(buffer, scanner, sizeof(struct ObjectScript_Routine_Scanner)); - return sizeof(struct ObjectScript_Routine_Scanner); +unsigned tree_sitter_objectscript_routine_external_scanner_serialize(void *payload, + char *buffer) { + struct ObjectScript_Routine_Scanner *scanner = + (struct ObjectScript_Routine_Scanner *)payload; + memcpy(buffer, scanner, sizeof(struct ObjectScript_Routine_Scanner)); + return sizeof(struct ObjectScript_Routine_Scanner); } -void tree_sitter_objectscript_routine_external_scanner_deserialize(void *payload, - const char *buffer, - unsigned length) { - if (length == 0) - return; - memcpy(payload, buffer, length); +void tree_sitter_objectscript_routine_external_scanner_deserialize( + void *payload, const char *buffer, unsigned length) { + memcpy(payload, buffer, length); } void tree_sitter_objectscript_routine_external_scanner_destroy(void *payload) { - struct ObjectScript_Routine_Scanner *scanner = (struct ObjectScript_Routine_Scanner *)payload; - free(scanner); + struct ObjectScript_Routine_Scanner *scanner = + (struct ObjectScript_Routine_Scanner *)payload; + free(scanner); } diff --git a/internal/cbm/vendored/grammars/objectscript_udl/scanner.c b/internal/cbm/vendored/grammars/objectscript_udl/scanner.c index 276f68263..fb1d01322 100644 --- a/internal/cbm/vendored/grammars/objectscript_udl/scanner.c +++ b/internal/cbm/vendored/grammars/objectscript_udl/scanner.c @@ -5,118 +5,125 @@ // There is no way to extend enums, so keep this in sync with base.h // All new entries should be appended at the bottom of the list enum TokenType { - EXTERNAL_METHOD_BODY_CONTENT = OBJECTSCRIPT_CORE_TOKEN_TYPE_MAX, - IRIS_USERNAME, + EXTERNAL_METHOD_BODY_CONTENT = OBJECTSCRIPT_CORE_TOKEN_TYPE_MAX, + IRIS_USERNAME, }; struct ObjectScript_Udl_Scanner { - char in_body; - struct ObjectScript_Core_Scanner core_scanner; + char in_body; + struct ObjectScript_Core_Scanner core_scanner; }; -static bool lex_fenced_text(TSLexer *lexer, enum TokenType desired_symbol, char l_delim, +static bool lex_fenced_text(TSLexer *lexer, + enum TokenType desired_symbol, char l_delim, char r_delim) { - int leftRightDiff = 1; - while (!lexer->eof(lexer)) { - if (lexer->lookahead == r_delim) { - leftRightDiff -= 1; - } else if (lexer->lookahead == l_delim) { - leftRightDiff += 1; - } - if (leftRightDiff == 0) { - lexer->result_symbol = desired_symbol; - return true; - } - advance(lexer); + int leftRightDiff = 1; + while (!lexer->eof(lexer)) { + if (lexer->lookahead == r_delim) { + leftRightDiff -= 1; + } else if (lexer->lookahead == l_delim) { + leftRightDiff += 1; } - return false; + if (leftRightDiff == 0) { + lexer->result_symbol = desired_symbol; + return true; + } + advance(lexer); + } + return false; } /// This is the interesting function. The rest is infrastructure static bool scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { - // struct Scanner *scanner = (struct Scanner *)payload; - // - // Tree sitter will mark all terminals as valid on error - // The sentinel should never be valid in a good parse, so this ensures - // we are not in error recovery mode - // printf("Lookahead: '%c'. Col %d. Tag is valid? %d", lexer->lookahead, - // lexer->get_column(lexer), valid_symbols[TAG]); - // printf("lexer->lookahead: '%c'\n", lexer->lookahead); - // printf("====\n"); - // debug_enum(valid_symbols); - // printf("====\n"); - if (valid_symbols[SENTINEL]) { - // printf("Thats not good!\n"); - // // TODO: ERROR RECOVERY MODE ?? Why would we be better than TS's - // default? if (iswspace(lexer->lookahead)) { - // skip(lexer); - // eat_whitespace(lexer); - // lexer->result_symbol = _WHITESPACE; - // return true; - // } - return false; - } - if (valid_symbols[EXTERNAL_METHOD_BODY_CONTENT]) { - // A valid method_body is one that is whose text fences - // are evenly balanced (so far only { }) - // e.g. VALID: {{{ [^{}]* }}} INVALID: {{{ [^{}]* } - return lex_fenced_text(lexer, EXTERNAL_METHOD_BODY_CONTENT, '{', '}'); - } + // struct Scanner *scanner = (struct Scanner *)payload; + // + // Tree sitter will mark all terminals as valid on error + // The sentinel should never be valid in a good parse, so this ensures + // we are not in error recovery mode + // printf("Lookahead: '%c'. Col %d. Tag is valid? %d", lexer->lookahead, + // lexer->get_column(lexer), valid_symbols[TAG]); + // printf("lexer->lookahead: '%c'\n", lexer->lookahead); + // printf("====\n"); + // debug_enum(valid_symbols); + // printf("====\n"); + if (valid_symbols[SENTINEL]) { + // printf("Thats not good!\n"); + // // TODO: ERROR RECOVERY MODE ?? Why would we be better than TS's + // default? if (iswspace(lexer->lookahead)) { + // skip(lexer); + // eat_whitespace(lexer); + // lexer->result_symbol = _WHITESPACE; + // return true; + // } + return false; + } + if (valid_symbols[EXTERNAL_METHOD_BODY_CONTENT]) { + // A valid method_body is one that is whose text fences + // are evenly balanced (so far only { }) + // e.g. VALID: {{{ [^{}]* }}} INVALID: {{{ [^{}]* } + return lex_fenced_text(lexer, EXTERNAL_METHOD_BODY_CONTENT, '{', + '}'); + } - if (valid_symbols[IRIS_USERNAME] && - !(lexer->lookahead == '}' || lexer->lookahead == '@' || lexer->lookahead == '*')) { - int count = 0; - lexer->mark_end(lexer); - while (!lexer->eof(lexer) && - !(lexer->lookahead == '}' || lexer->lookahead == '@' || lexer->lookahead == '*') && - count < 160) { - count++; - advance(lexer); - } - - if (lexer->lookahead == '}') { - lexer->mark_end(lexer); - lexer->result_symbol = IRIS_USERNAME; - return true; - } else { - return false; - } + if (valid_symbols[IRIS_USERNAME] && !(lexer->lookahead=='}' || lexer->lookahead=='@' || lexer->lookahead=='*')) { + int count=0; + lexer->mark_end(lexer); + while(!lexer->eof(lexer) && !(lexer->lookahead=='}' || lexer->lookahead=='@' || lexer->lookahead=='*') && count<160 ) { + count++; + advance(lexer); } + - struct ObjectScript_Udl_Scanner *scanner = (struct ObjectScript_Udl_Scanner *)payload; - return ObjectScript_Core_Scanner_scan(&scanner->core_scanner, lexer, valid_symbols); + if(lexer->lookahead=='}') { + lexer->mark_end(lexer); + lexer->result_symbol = IRIS_USERNAME; + return true; + } + else { + return false; + } + + } + + + struct ObjectScript_Udl_Scanner *scanner = + (struct ObjectScript_Udl_Scanner *)payload; + return ObjectScript_Core_Scanner_scan(&scanner->core_scanner, lexer, + valid_symbols); } void *tree_sitter_objectscript_udl_external_scanner_create() { - struct ObjectScript_Udl_Scanner *scanner = - (struct ObjectScript_Udl_Scanner *)calloc(1, sizeof(struct ObjectScript_Udl_Scanner)); - scanner->in_body = 0; - ObjectScript_Core_Scanner_init(&scanner->core_scanner); - scanner->core_scanner.column1_statement_mode = false; - return scanner; + struct ObjectScript_Udl_Scanner *scanner = + (struct ObjectScript_Udl_Scanner *)calloc( + 1, sizeof(struct ObjectScript_Udl_Scanner)); + scanner->in_body = 0; + ObjectScript_Core_Scanner_init(&scanner->core_scanner); + scanner->core_scanner.column1_statement_mode = false; + return scanner; } -bool tree_sitter_objectscript_udl_external_scanner_scan(void *payload, TSLexer *lexer, - const bool *valid_symbols) { - return scan(payload, lexer, valid_symbols); +bool tree_sitter_objectscript_udl_external_scanner_scan( + void *payload, TSLexer *lexer, const bool *valid_symbols) { + return scan(payload, lexer, valid_symbols); } -unsigned tree_sitter_objectscript_udl_external_scanner_serialize(void *payload, char *buffer) { - struct ObjectScript_Udl_Scanner *scanner = (struct ObjectScript_Udl_Scanner *)payload; - memcpy(buffer, scanner, sizeof(struct ObjectScript_Udl_Scanner)); - return sizeof(struct ObjectScript_Udl_Scanner); +unsigned tree_sitter_objectscript_udl_external_scanner_serialize(void *payload, + char *buffer) { + struct ObjectScript_Udl_Scanner *scanner = + (struct ObjectScript_Udl_Scanner *)payload; + memcpy(buffer, scanner, sizeof(struct ObjectScript_Udl_Scanner)); + return sizeof(struct ObjectScript_Udl_Scanner); } -void tree_sitter_objectscript_udl_external_scanner_deserialize(void *payload, const char *buffer, - unsigned length) { - if (length == 0) - return; - // This one is a bit funky. - // length includes the sizeof(struct Scanner) and the structs it points to - memcpy(payload, buffer, length); +void tree_sitter_objectscript_udl_external_scanner_deserialize( + void *payload, const char *buffer, unsigned length) { + // This one is a bit funky. + // length includes the sizeof(struct Scanner) and the structs it points to + memcpy(payload, buffer, length); } void tree_sitter_objectscript_udl_external_scanner_destroy(void *payload) { - struct ObjectScript_Udl_Scanner *scanner = (struct ObjectScript_Udl_Scanner *)payload; - free(scanner); + struct ObjectScript_Udl_Scanner *scanner = + (struct ObjectScript_Udl_Scanner *)payload; + free(scanner); } diff --git a/src/discover/discover.c b/src/discover/discover.c index 56c1594cc..19d4a3190 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -632,7 +632,7 @@ static CBMLanguage detect_file_language(const char *entry_name, const char *abs_ /* Special: ObjectScript Studio Export XML () is * detected by content; otherwise .xml stays XML. */ if (lang == CBM_LANG_XML) { - FILE *xf = fopen(abs_path, "r"); + FILE *xf = cbm_fopen(abs_path, "r"); if (xf) { char xbuf[CBM_SZ_256]; size_t xn = fread(xbuf, SKIP_ONE, sizeof(xbuf) - SKIP_ONE, xf);