Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,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)
Expand Down Expand Up @@ -200,6 +202,7 @@ PIPELINE_SRCS = \
src/pipeline/pass_semantic_edges.c \
src/pipeline/pass_complexity.c \
src/pipeline/pass_cross_repo.c \
src/pipeline/pass_ensemble_routing.c \
src/pipeline/artifact.c \
src/pipeline/pass_pkgmap.c

Expand Down
11 changes: 11 additions & 0 deletions internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,15 @@ static int count_params_from_signature(const char *sig) {
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) {
return cbm_extract_file_ex(source, source_len, language, project, rel_path, timeout_micros,
extra_defines, include_paths, NULL, NULL);
}

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));
Expand Down Expand Up @@ -580,6 +589,8 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
.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),
Expand Down
53 changes: 44 additions & 9 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,15 @@ 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_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_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;

Expand Down Expand Up @@ -485,6 +488,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;
Expand All @@ -495,9 +516,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 ---
Expand All @@ -524,6 +547,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);

Expand Down
208 changes: 207 additions & 1 deletion internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -592,6 +593,60 @@ static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *
if (lang == CBM_LANG_SWIFT) {
return extract_swift_callee(a, node, source, nk);
}
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);
}
Expand Down Expand Up @@ -1120,13 +1175,129 @@ static void extract_jsx_component_ref(CBMExtractCtx *ctx, TSNode node, const cha
}
}

// 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;
}

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), "instance_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;
}
}
}

if (callee && callee[0] && !cbm_is_keyword(callee, ctx->language)) {
CBMCall call = {0};
call.callee_name = callee;
Expand All @@ -1136,12 +1307,47 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk
call.start_line = (int)ts_node_start_point(node).row + TS_LINE_OFFSET;

TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
// ObjectScript stores args under oref_method/method_args, not the
// generic "arguments" field.
if (ts_node_is_null(args) && (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE)) {
TSNode oref = cbm_find_child_by_kind(node, "oref_method");
if (!ts_node_is_null(oref)) {
args = cbm_find_child_by_kind(oref, "method_args");
}
if (ts_node_is_null(args)) {
args = cbm_find_child_by_kind(node, "method_args");
}
}
if (!ts_node_is_null(args)) {
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
if (call.first_string_arg && call.first_string_arg[0] == '/') {
call.second_arg_name = extract_handler_arg(ctx, args);
}
extract_call_args(ctx, args, &call);
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL ||
ctx->language == CBM_LANG_OBJECTSCRIPT_ROUTINE) {
for (uint32_t ai = 0;
ai < ts_node_named_child_count(args) && call.arg_count < CBM_MAX_CALL_ARGS;
ai++) {
TSNode achild = ts_node_named_child(args, ai);
const char *ack = ts_node_type(achild);
if (strcmp(ack, "bracket") == 0) {
continue;
}
if (strcmp(ack, "method_arg") != 0) {
continue;
}
CBMCallArg *ca = &call.args[call.arg_count];
memset(ca, 0, sizeof(*ca));
ca->index = call.arg_count;
ca->expr = cbm_node_text(ctx->arena, achild, ctx->source);
if (ca->expr && ca->expr[0]) {
call.arg_count++;
}
}
} else {
extract_call_args(ctx, args, &call);
}
}

cbm_calls_push(&ctx->result->calls, ctx->arena, call);
Expand Down
Loading
Loading