Merge branch 'main' into freebsd-hw-physmem

This commit is contained in:
Pedro Ramos
2026-07-15 23:28:09 +01:00
38 changed files with 3949 additions and 111 deletions
+2
View File
@@ -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)
+9
View File
@@ -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 |
+134 -24
View File
@@ -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;
}
+45 -10
View File
@@ -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 (<Export generator="Cache">)
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);
+207 -1
View File
@@ -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);
+412 -31
View File
@@ -10,7 +10,7 @@
#include "semantic/ast_profile.h"
#include "tree_sitter/api.h" // TSNode, ts_node_*
#include <stdint.h> // uint32_t
#include <stdio.h> // snprintf
#include <stdio.h> // snprintf (ObjectScript storage/trigger sidecars)
#include <stdlib.h> // getenv, atoi
#include <string.h>
#include <ctype.h>
@@ -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), "</%s>", 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 = "<Global>";
const char *sql_end = "</Global>";
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]) {
+283
View File
@@ -10,6 +10,7 @@ enum { MAX_INFRA_BINDINGS = 8 };
#include <stdint.h> // uint32_t, uint8_t
#include <string.h>
#include <strings.h> // 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: <cffunction name="foo"> 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);
+16
View File
@@ -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
@@ -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"
+4
View File
@@ -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"
+444
View File
@@ -0,0 +1,444 @@
#include "iris_export_xml.h"
#include "arena.h"
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#define EXPORT_MARKER "<Export generator="
#define MAX_NAME 256
#define MAX_PARAMS 32
#define MAX_CLASSES 64
#define BUF_CAP (1024 * 64)
typedef struct {
char param_name[MAX_NAME];
char param_value[MAX_NAME];
} PropParam;
typedef struct {
char *buf;
int pos;
int cap;
} UdlBuf;
static const char *skip_ws(const char *p, const char *end) {
while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n'))
p++;
return p;
}
static bool sw(const char *p, const char *end, const char *tag) {
size_t n = strlen(tag);
return (size_t)(end - p) >= n && memcmp(p, tag, n) == 0;
}
static const char *find_s(const char *p, const char *end, const char *needle) {
size_t n = strlen(needle);
while (p + n <= end) {
if (memcmp(p, needle, n) == 0)
return p;
p++;
}
return NULL;
}
static const char *skip_tag(const char *p, const char *end) {
while (p < end && *p != '>')
p++;
return p < end ? p + 1 : end;
}
static bool is_self_closing(const char *start, const char *gt) {
const char *p = gt - 1;
while (p > start && (*p == ' ' || *p == '\t'))
p--;
return *p == '/';
}
static void extract_attr(const char *ts, const char *te, const char *attr, char *out, size_t sz) {
out[0] = '\0';
size_t an = strlen(attr);
for (const char *p = ts; p < te - an; p++) {
if (memcmp(p, attr, an) == 0 && p[an] == '=') {
p += an + 1;
char q = *p;
if (q != '"' && q != '\'')
return;
p++;
const char *v = p;
while (p < te && *p != q)
p++;
size_t vl = (size_t)(p - v);
if (vl >= 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, "<![CDATA[")) {
cs += 9;
const char *ce = find_s(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), "</%s>", 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, "<Parameter ");
if (!po)
break;
const char *pg = find_s(po, 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, "<Method ") || sw(p, ce, "<Method>")) {
const char *gt = find_s(p, ce, ">");
if (!gt)
break;
const char *me = find_s(gt + 1, ce, "</Method>");
if (!me) {
p = gt + 1;
continue;
}
emit_method(&b, p, me + strlen("</Method>"));
p = me + strlen("</Method>");
continue;
}
if (sw(p, ce, "<Property ")) {
const char *gt = find_s(p, ce, ">");
if (!gt)
break;
const char *pe = find_s(gt + 1, ce, "</Property>");
if (!pe) {
p = gt + 1;
continue;
}
emit_property(&b, p, pe + strlen("</Property>"));
p = pe + strlen("</Property>");
continue;
}
if (sw(p, ce, "<Parameter ")) {
const char *gt = find_s(p, ce, ">");
if (!gt)
break;
if (is_self_closing(p, gt)) {
p = gt + 1;
continue;
}
const char *pe = find_s(gt + 1, ce, "</Parameter>");
if (!pe) {
p = gt + 1;
continue;
}
emit_parameter(&b, p, pe + strlen("</Parameter>"));
p = pe + strlen("</Parameter>");
continue;
}
if (sw(p, ce, "<Index ") || sw(p, ce, "<Index>")) {
const char *gt = find_s(p, ce, ">");
if (!gt)
break;
const char *ie = find_s(gt + 1, ce, "</Index>");
if (!ie) {
p = gt + 1;
continue;
}
emit_index(&b, p, ie + strlen("</Index>"));
p = ie + strlen("</Index>");
continue;
}
if (sw(p, ce, "<XData ") || sw(p, ce, "<XData>")) {
const char *gt = find_s(p, ce, ">");
if (!gt)
break;
const char *xe = find_s(gt + 1, ce, "</XData>");
if (!xe) {
p = gt + 1;
continue;
}
emit_xdata(&b, p, xe + strlen("</XData>"));
p = xe + strlen("</XData>");
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, "<Class ");
if (!co)
break;
const char *gt = find_s(co, end, ">");
if (!gt)
break;
const char *cc = find_s(gt + 1, end, "</Class>");
if (!cc)
break;
char *udl = transcode_class(arena, co, cc);
if (udl && udl[0])
results[count++] = udl;
p = cc + strlen("</Class>");
}
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;
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "arena.h"
/*
* IRIS Studio Export XML transcoder.
*
* Converts <Export generator="Cache"> 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 <Class> 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);
+43
View File
@@ -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,
+251
View File
@@ -0,0 +1,251 @@
#include "macro_table.h"
#include "arena.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
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;
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <stddef.h>
#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);
+166 -26
View File
@@ -10,45 +10,145 @@
#include <vector>
#include <cstring>
#include <cstdlib>
#include <cstdint>
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"
+18
View File
@@ -2,11 +2,19 @@
#define CBM_PREPROCESSOR_H
#include <stddef.h>
#include <stdint.h>
#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
+22
View File
@@ -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 (<Export generator="...">) 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, "<Export generator=")) {
return CBM_LANG_OBJECTSCRIPT_EXPORT;
}
}
}
/* Check ignored JSON files */
if (lang == CBM_LANG_JSON && str_in_list(entry_name, IGNORED_JSON_FILES)) {
return CBM_LANG_COUNT;
+11
View File
@@ -39,6 +39,17 @@ const char *cbm_language_name(CBMLanguage lang);
* On read failure, defaults to CBM_LANG_MATLAB. */
CBMLanguage cbm_disambiguate_m(const char *path);
/* Disambiguate .cls files by reading first 4KB of content.
* Returns CBM_LANG_OBJECTSCRIPT_UDL if a line starts with "Class <Uppercase>",
* 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 <Uppercase>" 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;
+93 -1
View File
@@ -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 <UppercasePackage>...". 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 <Name>" 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 <Uppercase>". */
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;
}
+17 -4
View File
@@ -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 ("<name>::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) {
+90 -2
View File
@@ -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;
+37 -4
View File
@@ -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 <Class> 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) {
+49 -2
View File
@@ -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 <Class> 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;
+159 -2
View File
@@ -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 <name>/__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,
+77 -1
View File
@@ -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;
}
+24 -2
View File
@@ -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
+8
View File
@@ -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,
+5
View File
@@ -0,0 +1,5 @@
#include "NodeController.h"
int node_controller_value() {
return 1;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
struct NodeController {
int value;
};
+5
View File
@@ -0,0 +1,5 @@
#include "SystemController.h"
int system_controller_value() {
return 2;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
struct SystemController {
int value;
};
+6
View File
@@ -0,0 +1,6 @@
#include "NodeController.h"
#include <SystemController.h>
int main() {
return 0;
}
+150
View File
@@ -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);
+27 -1
View File
@@ -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);
+948
View File
@@ -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 <time.h>
#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 { <Routes/> }\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"
"<Production Name=\"Sample.Production\">\n"
" <Item Name=\"MyService\" ClassName=\"Sample.Service\" Enabled=\"true\">\n"
" </Item>\n"
" <Item Name=\"MyOperation\" ClassName=\"Sample.Operation\" Enabled=\"false\">\n"
" </Item>\n"
"</Production>\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"
"<Production Name=\"HS.Flash.Production\">\n"
" <Item Name=\"FHIRService\" ClassName=\"HS.Flash.FHIRService\" Enabled=\"true\">\n"
" <Setting Target=\"Host\" Name=\"TargetConfigName\">FHIROps</Setting>\n"
" <Setting Target=\"Host\" Name=\"PatientHost\">PatientOps</Setting>\n"
" <Setting Target=\"Host\" Name=\"ConformanceOperation\">ConformOps</Setting>\n"
" </Item>\n"
"</Production>\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 \
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
"<Export generator=\"Cache\" version=\"25\">\n" \
"<Class name=\"Test.Simple\">\n" \
"<Super>%RegisteredObject</Super>\n" \
"<Method name=\"Hello\">\n" \
"<ReturnType>%String</ReturnType>\n" \
"<Implementation><![CDATA[\n" \
"\tQuit \"hello\"\n" \
"]]></Implementation>\n" \
"</Method>\n" \
"</Class>\n" \
"</Export>\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 \
"<?xml version=\"1.0\"?>\n" \
"<Export generator=\"Cache\" version=\"25\">\n" \
"<Class name=\"Test.CM\">\n" \
"<Method name=\"Run\">\n" \
"<ClassMethod>1</ClassMethod>\n" \
"<FormalSpec>pArg:%String,pFlag:%Boolean=0</FormalSpec>\n" \
"<ReturnType>%Status</ReturnType>\n" \
"<Implementation><![CDATA[\n" \
"\tQuit $$$OK\n" \
"]]></Implementation>\n" \
"</Method>\n" \
"</Class>\n" \
"</Export>\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 \
"<?xml version=\"1.0\"?>\n" \
"<Export generator=\"Cache\" version=\"25\">\n" \
"<Class name=\"Test.Members\">\n" \
"<Property name=\"Name\">\n" \
"<Type>%String</Type>\n" \
"<Parameter name=\"MAXLEN\" value=\"200\"/>\n" \
"</Property>\n" \
"<Parameter name=\"VERSION\">\n" \
"<Default>1</Default>\n" \
"</Parameter>\n" \
"<Index name=\"NameIdx\">\n" \
"<Properties>Name</Properties>\n" \
"<Unique>1</Unique>\n" \
"</Index>\n" \
"</Class>\n" \
"</Export>\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 \
"<?xml version=\"1.0\"?>\n" \
"<Export generator=\"Cache\" version=\"25\">\n" \
"<Class name=\"Test.Caller\">\n" \
"<Super>%RegisteredObject</Super>\n" \
"<Method name=\"Run\">\n" \
"<ClassMethod>1</ClassMethod>\n" \
"<ReturnType>%Status</ReturnType>\n" \
"<Implementation><![CDATA[\n" \
"\tSet obj = ##class(Target.Worker).%New()\n" \
"\tDo obj.Execute()\n" \
"\tQuit $$$OK\n" \
"]]></Implementation>\n" \
"</Method>\n" \
"</Class>\n" \
"</Export>\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 \
"<?xml version=\"1.0\"?>\n" \
"<Export generator=\"Cache\" version=\"25\">\n" \
"<Class name=\"Test.First\">\n" \
"<Method name=\"One\"><Implementation><![CDATA[\tQuit 1\n]]></Implementation></Method>\n" \
"</Class>\n" \
"<Class name=\"Test.Second\">\n" \
"<Method name=\"Two\"><Implementation><![CDATA[\tQuit 2\n]]></Implementation></Method>\n" \
"</Class>\n" \
"</Export>\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);
+31
View File
@@ -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);
+76
View File
@@ -4780,6 +4780,81 @@ TEST(tool_resolve_store_by_internal_name_issue704) {
PASS();
}
/* ── #1044: a "<name>::missed" shadow row must not hide the project ──
*
* The miss-graph pass inserts a second `projects` row ("<name>::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);