mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-07 08:43:35 +08:00
Merge branch 'main' into freebsd-hw-physmem
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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]) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "NodeController.h"
|
||||
|
||||
int node_controller_value() {
|
||||
return 1;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
struct NodeController {
|
||||
int value;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "SystemController.h"
|
||||
|
||||
int system_controller_value() {
|
||||
return 2;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
struct SystemController {
|
||||
int value;
|
||||
};
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
#include "NodeController.h"
|
||||
#include <SystemController.h>
|
||||
|
||||
int main() {
|
||||
return 0;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user