diff --git a/Makefile.cbm b/Makefile.cbm index 73269ecc4..b78018c11 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -135,6 +135,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 af454426f..fb99e4417 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). @@ -849,6 +844,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++) { @@ -886,17 +981,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)); @@ -926,6 +1021,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; } @@ -934,6 +1030,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; } @@ -953,6 +1050,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; } @@ -985,6 +1083,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; } @@ -1009,6 +1108,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), @@ -1088,9 +1189,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 +1257,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 +1291,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); } @@ -1318,6 +1427,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 6f6505062..ea0fb8b94 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" @@ -1162,6 +1163,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); } @@ -2013,6 +2068,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; @@ -2020,6 +2141,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] && @@ -2064,6 +2235,18 @@ 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); /* #952: routes registered inside Laravel `prefix()->group()` @@ -2087,7 +2270,30 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk 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 9b7acc42a..deee8a8e7 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) { @@ -1296,7 +1317,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 +1627,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 +1774,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; } @@ -2372,6 +2452,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 +3617,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 +4191,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 +4240,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 +4262,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 +4381,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; } @@ -5558,6 +5723,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; } @@ -5573,6 +5746,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) @@ -5582,8 +5960,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); @@ -5854,6 +6232,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/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/src/discover/discover.c b/src/discover/discover.c index 785640713..19d4a3190 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 = cbm_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/mcp/mcp.c b/src/mcp/mcp.c index af95e139e..b1e653a01 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -1793,10 +1793,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/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 7f5d0e36e..6db2a73f7 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -615,6 +615,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; @@ -626,8 +703,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; @@ -638,6 +716,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/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/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index d4f2ade5c..e5e62634c 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 c84df7d73..8b5766978 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -885,8 +885,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); } } @@ -895,6 +904,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 2b127473c..d2180a47a 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -115,6 +115,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/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); diff --git a/tests/test_edge_types_probe.c b/tests/test_edge_types_probe.c index 5cae85015..11946613e 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. @@ -1488,7 +1513,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); @@ -1497,6 +1522,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_laravel_facade_routes_issue952); diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 686691974..0598cadb7 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -8,7 +8,10 @@ #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 +#include "macro_table.h" +#include "iris_export_xml.h" /* ── Helpers ───────────────────────────────────────────────────── */ @@ -66,6 +69,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 * ═══════════════════════════════════════════════════════════════════ */ @@ -2728,6 +2739,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++) { @@ -3426,6 +3469,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) { @@ -3614,6 +3789,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(); @@ -3630,6 +4540,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); @@ -3846,6 +4790,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); @@ -3865,6 +4810,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); 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); diff --git a/tests/test_mcp.c b/tests/test_mcp.c index 7ca8ccc64..98ad0bafb 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -4780,6 +4780,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) * @@ -6439,6 +6514,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);