graph: one identity per conditional definition, SPAWNS edges, no cross-language guesses

The test-impact engine selects tests by walking the graph, so it can only
be as right as the graph. Three gaps made it wrong:

One identity for a definition's mutually exclusive variants
- A definition written once per #if/#else branch, per #[cfg], per
  platform file (foo_linux.c / foo_win.c, platform directories) or per Go
  build tag is ONE node. The graph buffer keeps every variant's span in a
  `variants` node property (definition labels only), so a change in any
  variant maps to the definition.
- Extraction now sees definitions inside conditional blocks where it
  skipped them: C++/C#/ObjC class members under #if (one cursor walk,
  conditional block types per language in lang_specs), Elixir defs under
  if/unless, Fortran .F90/.F95/.F03/.F08, Rust #[cfg] twins (no more
  `name#cfg(...)` QNs that lost every call), C platform files and
  directories (non-static file-scope functions).
- Haskell: the vendored scanner reads only an #if group's first branch.
  The grammar stays upstream; every further branch is parsed as its own
  same-length view (cpp_branch_views.c) and adds what the raw parse
  missed. haskell/bytestring: no new partial parses, +27 functions,
  IMPORTS 322 -> 329, 131 definitions with variant spans.
- Incremental closure repair re-parses every file of such a definition
  when one of them changes (variant partners through DEFINES), so a
  repair equals a full index.

No cross-language name guesses
- A name-only resolution (unique_name, suffix_match, qualified_suffix,
  field_type_hint, and same_module across files that only share a path
  stem) never binds a symbol another language defines.
- Bash imports are the files a script sources: every top-level command
  was read as an import of its name (`set -e` imported "set", bound to a
  TSX method in 137 scripts here).
- This repository: cross-language CALLS 5,916 -> 5,091; the remainder are
  C/C++ pairs, builtins and same-stem module collisions (recorded).

SPAWNS edges (per language, like the HTTP client patterns)
- A call that starts a program (subprocess.run, exec.Command,
  child_process.spawn, posix_spawn, system, Process.Start, Command::new,
  ... about 30 languages, through the file's own imports) gets a SPAWNS
  edge to a Process node named after the program (literal first word, or
  <dynamic>) instead of a CALLS guess. No target resolution. Both
  resolvers ask at the same point (parity test).

The engine follows the graph
- Seeds map changed lines through every variant span; a test written
  once per #if branch is one test; when a change reaches a non-test
  `main`, the tests that spawn processes are reached too (the process
  runs main, and main reaches everything).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-06 02:44:56 +02:00
parent ec0bc46089
commit 42a9320200
39 changed files with 4130 additions and 214 deletions
+5 -2
View File
@@ -375,12 +375,14 @@ EXTRACTION_SRCS = \
$(CBM_DIR)/extract_dbt.c \
$(CBM_DIR)/sql_values.c \
$(CBM_DIR)/helpers.c \
$(CBM_DIR)/cpp_branch_views.c \
$(CBM_DIR)/result_compact.c \
$(CBM_DIR)/result_spill.c \
$(CBM_DIR)/lang_specs.c \
$(CBM_DIR)/macro_table.c \
$(CBM_DIR)/iris_export_xml.c \
$(CBM_DIR)/service_patterns.c
$(CBM_DIR)/service_patterns.c \
$(CBM_DIR)/spawn_patterns.c
# LSP resolvers (compiled as one unit via lsp_all.c)
LSP_SRCS = $(CBM_DIR)/lsp_all.c
@@ -481,6 +483,7 @@ PIPELINE_SRCS = \
src/pipeline/pass_parallel.c \
src/pipeline/pass_definitions.c \
src/pipeline/pass_calls.c \
src/pipeline/pass_spawns.c \
src/pipeline/pass_lsp_cross.c \
src/pipeline/pass_usages.c \
src/pipeline/pass_semantic.c \
@@ -790,7 +793,7 @@ TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c
TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c tests/test_grammar_probe_d.c tests/test_grammar_probe_e.c tests/test_grammar_probe_f.c tests/test_grammar_probe_g.c
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c tests/test_edge_imports.c tests/test_edge_structural.c tests/test_lsp_resolution_probe.c tests/test_node_creation_probe.c tests/test_edge_types_probe.c tests/test_convergence_probe.c tests/test_matrix_known_classes.c tests/test_matrix_new_constructs.c tests/test_grammar_probe_a.c tests/test_grammar_probe_b.c tests/test_grammar_probe_c.c tests/test_grammar_probe_d.c tests/test_grammar_probe_e.c tests/test_grammar_probe_f.c tests/test_grammar_probe_g.c tests/test_conditional_variants.c tests/test_spawns.c
TEST_TRACES_SRCS = tests/test_traces.c
+2 -1
View File
@@ -229,6 +229,7 @@ The install script placed beside the binary is **reported, not deleted** — uni
- `USAGE` — an identifier is used, but a unique callable target is not proven (including ambiguous or complex expressions)
- `IMPORTS`, `DEFINES`, `IMPLEMENTS`, `INHERITS`
- `HTTP_CALLS`, `ASYNC_CALLS` (cross-service)
- `SPAWNS` — the caller starts another program (`subprocess.run`, `exec.Command`, `posix_spawn`, `Process.Start`, ...); the target is a `Process` node named after the program, or `<dynamic>` when the program is not a literal
- `EMITS`, `LISTENS_ON` (channels)
- `DATA_FLOWS` with arg-to-param mapping + field access chains
- `SIMILAR_TO` (MinHash + LSH near-clone detection, Jaccard scored)
@@ -740,7 +741,7 @@ mode. `index_status` keeps describing the published graph and its freshness.
### Edge Types
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `CALL_REFERENCE`, `HTTP_CALLS`, `ASYNC_CALLS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `REFERENCES_FILE`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
`CONTAINS_PACKAGE`, `CONTAINS_FOLDER`, `CONTAINS_FILE`, `DEFINES`, `DEFINES_METHOD`, `IMPORTS`, `CALLS`, `CALL_REFERENCE`, `HTTP_CALLS`, `ASYNC_CALLS`, `SPAWNS`, `IMPLEMENTS`, `HANDLES`, `USAGE`, `CONFIGURES`, `REFERENCES_FILE`, `WRITES`, `MEMBER_OF`, `TESTS`, `USES_TYPE`, `FILE_CHANGES_WITH`
### Qualified Names
+100
View File
@@ -22,6 +22,7 @@
#include "lsp/rust_lsp.h"
#include "preprocessor.h"
#include "sql_values.h" // #1735: literal INSERT rows kept out of the SQL parse
#include "cpp_branch_views.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h" // cbm_fopen — crash-supervisor per-file marker write
#include "foundation/hash_table.h" // CBMHashTable — crash-supervisor quarantine set
@@ -2146,6 +2147,102 @@ static bool cbm_sql_values_exclusion_on(const char *rel_path) {
return true;
}
/* Drop the entries a branch view re-extracted from code outside the
* conditionals: a definition with a QN the file already has at the same line,
* an import with the same name and path. */
static void cpp_view_drop_seen(CBMFileResult *r, int defs_before, int imports_before) {
int w = defs_before;
for (int i = defs_before; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
bool seen = false;
for (int j = 0; j < w && !seen; j++) {
const CBMDefinition *e = &r->defs.items[j];
seen = e->start_line == d->start_line && e->qualified_name && d->qualified_name &&
strcmp(e->qualified_name, d->qualified_name) == 0;
}
if (!seen) {
r->defs.items[w++] = *d;
}
}
r->defs.count = w;
w = imports_before;
for (int i = imports_before; i < r->imports.count; i++) {
const CBMImport *m = &r->imports.items[i];
bool seen = false;
for (int j = 0; j < w && !seen; j++) {
const CBMImport *e = &r->imports.items[j];
seen = e->local_name && m->local_name && e->module_path && m->module_path &&
strcmp(e->local_name, m->local_name) == 0 &&
strcmp(e->module_path, m->module_path) == 0;
}
if (!seen) {
r->imports.items[w++] = *m;
}
}
r->imports.count = w;
}
/* Haskell: the scanner parses the first branch of an #if group and swallows
* the rest up to #endif as one token, so what is written once per
* configuration (a definition, an import, a call in the other branch) was
* seen in its first variant only. Every further branch is parsed as its own
* view (cpp_branch_views.h: same bytes and lines, the other branches and the
* conditional lines blanked) and extracted into the same result, under the
* raw parse's budget. A definition at another line under a QN the file
* already has is that definition's variant; the graph keeps one identity with
* both spans. Calls and usages outside the conditionals come back as
* duplicates, which the pipeline dedups by caller and callee. The raw parse
* alone decides the file's parse status. */
static void extract_cpp_branch_views(const CBMExtractCtx *raw, const TSLanguage *ts_lang,
int64_t timeout_micros) {
int views = cbm_cpp_branch_view_count(raw->arena, raw->source, raw->source_len);
for (int v = 1; v <= views; v++) {
char *view = cbm_cpp_branch_view(raw->arena, raw->source, raw->source_len, v);
TSParser *parser = view ? get_thread_parser(ts_lang, raw->language) : NULL;
if (!parser) {
return;
}
ts_parser_reset(parser);
CBMStringInput input = {view, (uint32_t)raw->source_len};
TSInput ts_input = {&input, cbm_string_read, TSInputEncodingUTF8, NULL};
TSParseOptions opts = {0};
CBMParseBudget budget = {0}; // cppcheck-suppress unreadVariable
if (timeout_micros > 0) {
uint64_t budget_ns = (uint64_t)timeout_micros * USEC_TO_NSEC;
budget.cpu_deadline_ns = cbm_thread_cpu_time_ns() + budget_ns;
budget.wall_ceiling_ns = now_ns() + budget_ns * CBM_PARSE_WALL_CEILING_FACTOR;
opts.payload = &budget;
opts.progress_callback = cbm_timeout_cb;
}
TSTree *tree = ts_parser_parse_with_options(parser, NULL, ts_input, opts);
if (!tree) {
return;
}
CBMExtractCtx vctx = {
.arena = raw->arena,
.scratch = raw->scratch,
.result = raw->result,
.source = view,
.source_len = raw->source_len,
.language = raw->language,
.project = raw->project,
.rel_path = raw->rel_path,
.module_qn = raw->module_qn,
.root = ts_tree_root_node(tree),
.macro_table = raw->macro_table,
.return_type_table = raw->return_type_table,
.walk_budget_nodes = cbm_walk_max_nodes(),
};
int defs_before = raw->result->defs.count;
int imports_before = raw->result->imports.count;
cbm_extract_definitions(&vctx);
cbm_extract_imports(&vctx);
cbm_extract_unified(&vctx);
cpp_view_drop_seen(raw->result, defs_before, imports_before);
ts_tree_delete(tree);
}
}
static CBMFileResult *extract_file_ex_body(const char *source, int source_len, CBMLanguage language,
const char *project, const char *rel_path,
int64_t timeout_micros, const char **extra_defines,
@@ -2353,6 +2450,9 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
cbm_test_declarations_finish(&ctx);
cbm_extract_imports(&ctx);
cbm_extract_unified(&ctx);
if (cbm_lang_needs_cpp_branch_views(language)) {
extract_cpp_branch_views(&ctx, ts_lang, timeout_micros);
}
result->tree_nodes = ts_node_descendant_count(root);
result->walk_nodes_visited = ctx.walk_nodes_visited;
if (ctx.walk_budget_exhausted) {
+183
View File
@@ -0,0 +1,183 @@
/*
* cpp_branch_views.c — one parse view per #if/#else branch (see the header).
*/
#include "cpp_branch_views.h"
#include <string.h>
enum { CPP_LINE_CODE, CPP_LINE_START, CPP_LINE_ELSE, CPP_LINE_END };
enum { CPP_NEST_MAX = 64 };
bool cbm_lang_needs_cpp_branch_views(CBMLanguage lang) {
return lang == CBM_LANG_HASKELL;
}
static bool cpp_word_char(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}
// Classify the line at src[pos..end): a conditional directive is a '#' in
// column 0, optional blanks, then the directive word — the same lines the
// Haskell scanner treats as conditionals.
static int cpp_line_kind(const char *src, int pos, int end) {
if (pos >= end || src[pos] != '#') {
return CPP_LINE_CODE;
}
int p = pos + 1;
while (p < end && (src[p] == ' ' || src[p] == '\t')) {
p++;
}
int w = p;
while (w < end && cpp_word_char(src[w])) {
w++;
}
static const struct {
const char *word;
int kind;
} words[] = {
{"if", CPP_LINE_START}, {"ifdef", CPP_LINE_START}, {"ifndef", CPP_LINE_START},
{"else", CPP_LINE_ELSE}, {"elif", CPP_LINE_ELSE}, {"elifdef", CPP_LINE_ELSE},
{"elifndef", CPP_LINE_ELSE}, {"endif", CPP_LINE_END},
};
size_t n = (size_t)(w - p);
for (size_t i = 0; i < sizeof(words) / sizeof(words[0]); i++) {
if (strlen(words[i].word) == n && memcmp(src + p, words[i].word, n) == 0) {
return words[i].kind;
}
}
return CPP_LINE_CODE;
}
static int cpp_line_end(const char *src, int pos, int len) {
const char *nl = memchr(src + pos, '\n', (size_t)(len - pos));
return nl ? (int)(nl - src) : len;
}
typedef struct {
int group; // groups are numbered in order of their #if line
int branch; // branch the current line is in
bool outer; // the enclosing context is kept
} cpp_frame_t;
typedef struct {
cpp_frame_t frames[CPP_NEST_MAX];
int depth;
int overflow; // groups nested past CPP_NEST_MAX are not tracked
int next;
} cpp_nest_t;
// Track one line. Returns the innermost tracked group when the line is one of
// its #else/#elif lines, else NULL.
static cpp_frame_t *cpp_nest_step(cpp_nest_t *n, int kind, bool active) {
switch (kind) {
case CPP_LINE_START:
if (n->depth < CPP_NEST_MAX && n->overflow == 0) {
n->frames[n->depth++] = (cpp_frame_t){n->next, 0, active};
} else {
n->overflow++;
}
n->next++;
return NULL;
case CPP_LINE_ELSE:
if (n->overflow > 0 || n->depth == 0) {
return NULL;
}
n->frames[n->depth - 1].branch++;
return &n->frames[n->depth - 1];
case CPP_LINE_END:
if (n->overflow > 0) {
n->overflow--;
} else if (n->depth > 0) {
n->depth--;
}
return NULL;
default:
return NULL;
}
}
// Branch count per group. Returns the group count, or -1 on allocation failure.
static int cpp_group_branches(CBMArena *a, const char *src, int len, int **out) {
int groups = 0;
for (int pos = 0; pos < len;) {
int end = cpp_line_end(src, pos, len);
groups += cpp_line_kind(src, pos, end) == CPP_LINE_START;
pos = end + 1;
}
*out = NULL;
if (groups == 0) {
return 0;
}
int *branches = cbm_arena_alloc(a, sizeof(int) * (size_t)groups);
if (!branches) {
return -1;
}
for (int g = 0; g < groups; g++) {
branches[g] = 1;
}
cpp_nest_t nest = {.depth = 0};
for (int pos = 0; pos < len;) {
int end = cpp_line_end(src, pos, len);
cpp_frame_t *frame = cpp_nest_step(&nest, cpp_line_kind(src, pos, end), true);
if (frame) {
branches[frame->group] = frame->branch + 1;
}
pos = end + 1;
}
*out = branches;
return groups;
}
int cbm_cpp_branch_view_count(CBMArena *a, const char *src, int len) {
int *branches = NULL;
int groups = cpp_group_branches(a, src, len, &branches);
int most = 1;
for (int g = 0; g < groups; g++) {
if (branches[g] > most) {
most = branches[g];
}
}
int views = most - 1;
return views > CBM_CPP_BRANCH_VIEWS_MAX ? CBM_CPP_BRANCH_VIEWS_MAX : views;
}
// The current line belongs to the view: every enclosing group sits in the
// branch the view keeps.
static bool cpp_view_keeps(const cpp_nest_t *n, const int *branches, int view) {
if (n->depth == 0) {
return true;
}
const cpp_frame_t *top = &n->frames[n->depth - 1];
int nb = branches[top->group];
int kept = 0;
if (nb >= 2) {
kept = view < nb - 1 ? view : nb - 1;
}
return top->outer && top->branch == kept;
}
char *cbm_cpp_branch_view(CBMArena *a, const char *src, int len, int view) {
int *branches = NULL;
int groups = cpp_group_branches(a, src, len, &branches);
char *buf = groups >= 0 ? cbm_arena_alloc(a, (size_t)len + 1) : NULL;
if (!buf) {
return NULL;
}
memcpy(buf, src, (size_t)len);
buf[len] = '\0';
cpp_nest_t nest = {.depth = 0};
for (int pos = 0; pos < len;) {
int end = cpp_line_end(src, pos, len);
bool kept = cpp_view_keeps(&nest, branches, view);
int kind = cpp_line_kind(src, pos, end);
cpp_nest_step(&nest, kind, kept);
if (kind != CPP_LINE_CODE || !kept) {
for (int i = pos; i < end; i++) {
buf[i] = buf[i] == '\r' ? '\r' : ' ';
}
}
pos = end + 1;
}
return buf;
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef CBM_CPP_BRANCH_VIEWS_H
#define CBM_CPP_BRANCH_VIEWS_H
#include "cbm.h"
/*
* Branch views of a source whose grammar parses only the first branch of an
* #if/#else group (the Haskell scanner swallows everything from #else to
* #endif as one token, so a definition written once per configuration is seen
* in its first variant only).
*
* A view keeps the source byte for byte and line for line: the branch it
* selects in every group stays, the other branches and all conditional lines
* (#if/#ifdef/#ifndef/#else/#elif.../#endif) become spaces, newlines stay. So
* every span extracted from a view is a raw-source span.
*
* View v (1-based) selects branch min(v, branches - 1) of a group with at
* least two branches, and the only branch of a group without one.
*/
#define CBM_CPP_BRANCH_VIEWS_MAX 3
// True when `lang`'s grammar parses only the first branch of a conditional.
bool cbm_lang_needs_cpp_branch_views(CBMLanguage lang);
// Number of views the source needs (0 when no group has a second branch),
// at most CBM_CPP_BRANCH_VIEWS_MAX.
int cbm_cpp_branch_view_count(CBMArena *a, const char *src, int len);
// View `view` (1..count) as an arena buffer of exactly `len` bytes plus a NUL,
// or NULL on allocation failure.
char *cbm_cpp_branch_view(CBMArena *a, const char *src, int len, int view);
#endif
+132 -59
View File
@@ -3561,12 +3561,6 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou
return result;
}
/* Rust: two same-named functions guarded by mutually-exclusive #[cfg(...)]
* attributes both parse as distinct function_item nodes and otherwise receive
* the SAME qualified_name, so the second graph upsert silently overwrites the
* first and one branch is lost (#495). Fold the cfg predicate into the QN so
* each cfg-gated twin gets a DISTINCT, predicate-encoding QN. Returns the
* (possibly suffixed) QN; the original QN when no cfg attribute is present. */
/* Rust: mark a function as a test when it carries a test attribute (#855).
* cbm's test detection is otherwise file-path-based (cbm_is_test_file:
* *_test.rs / test_*), so inline #[test]/#[tokio::test] functions inside a
@@ -3596,32 +3590,6 @@ static bool rust_def_is_test(const char *const *decorators) {
return false;
}
static const char *rust_cfg_qualified_name(CBMArena *a, const char *base_qn,
const char *const *decorators) {
if (!decorators) {
return base_qn;
}
for (int i = 0; decorators[i]; i++) {
const char *cfg = strstr(decorators[i], "cfg(");
if (!cfg) {
continue;
}
/* Build a compact predicate suffix from the cfg(...) text, dropping
* whitespace and quotes so the QN stays readable and stable. */
char buf[CBM_SZ_256];
size_t bi = 0;
for (const char *p = cfg; *p && bi + 1 < sizeof(buf); p++) {
if (*p == ' ' || *p == '\t' || *p == '"' || *p == '\'') {
continue;
}
buf[bi++] = *p;
}
buf[bi] = '\0';
return cbm_arena_sprintf(a, "%s#%s", base_qn, buf);
}
return base_qn;
}
// Extract base class name text from a single base_class child node.
static char *extract_cpp_base_text(CBMArena *a, TSNode bc, const char *source) {
const char *bk = ts_node_type(bc);
@@ -6261,6 +6229,14 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
* proj.myapp.db.Func, not proj.myapp.db.conn.Func). Other langs unchanged. */
def.qualified_name =
cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path, qn_name, ctx->language);
/* C-family platform variants (foo_win.c / foo_posix.c, src/unix/ vs
* src/win/): an external-linkage file-scope function takes its
* platform-neutral QN, so its variants are one definition. The call-scope
* twin in extract_unified.c applies the same rule. */
if (!ctx->enclosing_class_qn) {
def.qualified_name = cbm_platform_variant_qn(a, ctx->language, ctx->project, ctx->rel_path,
qn_name, def.qualified_name, node);
}
/* A free function declared inside a namespace (C++/C#/PHP) is qualified by
* the namespace scope the def walk carries (enclosing_class_qn was extended
* by is_namespace_scope_kind), so `ns::serialize` is `proj.file.ns.serialize`
@@ -6385,10 +6361,13 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
def.decorators = extract_decorators(a, node, ctx->source, ctx->language, spec);
extract_route_from_decorators(a, node, ctx->source, spec, &def.route_path, &def.route_method);
// Rust: disambiguate cfg-gated twin functions by folding the #[cfg(...)]
// predicate into the QN so both branches survive the graph upsert (#495).
// Rust: cfg-gated twins (#[cfg(windows)] fn f / #[cfg(not(windows))] fn f)
// keep the plain QN: they are variants of ONE definition, and the graph
// keeps every twin's span and the union of their bodies' calls on one node
// (graph_buffer.c, "Definition variants"). #495's lost branch is a variant
// now; the old per-twin `f#cfg(...)` QNs left the bodies' calls without a
// source node (calls are scoped to the plain QN).
if (ctx->language == CBM_LANG_RUST) {
def.qualified_name = rust_cfg_qualified_name(a, def.qualified_name, def.decorators);
def.is_test = rust_def_is_test(def.decorators);
}
@@ -7820,20 +7799,94 @@ static void push_method_def(CBMExtractCtx *ctx, TSNode child, TSNode class_node,
}
// Extract methods from an ObjC implementation_definition node.
static void extract_objc_impl_methods(CBMExtractCtx *ctx, TSNode impl_node, const char *class_qn,
const CBMLangSpec *spec) {
uint32_t nc = ts_node_child_count(impl_node);
for (uint32_t j = 0; j < nc; j++) {
TSNode inner = ts_node_child(impl_node, j);
if (ts_node_is_null(inner)) {
/* Members of a class body, flattened through conditional blocks: a method,
* field or variable inside #if/#ifdef/#else/#elif (cbm_conditional_block_types:
* C, C++, CUDA, Objective-C, C#, ...) is still a member of its class, and its
* #else twin is a variant of the same member (graph_buffer.c, "Definition
* variants"). One tree cursor: linear in the body's width, no recursion. */
enum { MEMBER_CONDITIONAL_DEPTH_MAX = 16 };
typedef struct {
TSTreeCursor cursor;
const char **conditional;
int depth; /* conditional blocks entered below the body */
bool started;
bool named;
} member_iter_t;
static void member_iter_init(member_iter_t *it, TSNode body, CBMLanguage lang, bool named) {
it->cursor = ts_tree_cursor_new(body);
it->conditional = cbm_conditional_block_types(lang);
it->depth = 0;
it->started = false;
it->named = named;
}
static void member_iter_done(member_iter_t *it) {
ts_tree_cursor_delete(&it->cursor);
}
static bool member_iter_advance(member_iter_t *it) {
if (!it->started) {
it->started = true;
return ts_tree_cursor_goto_first_child(&it->cursor);
}
while (!ts_tree_cursor_goto_next_sibling(&it->cursor)) {
if (it->depth == 0) {
return false;
}
ts_tree_cursor_goto_parent(&it->cursor);
it->depth--;
}
return true;
}
static bool member_iter_next(member_iter_t *it, TSNode *out) {
for (bool have = member_iter_advance(it); have; have = member_iter_advance(it)) {
TSNode node = ts_tree_cursor_current_node(&it->cursor);
while (it->conditional && it->depth < MEMBER_CONDITIONAL_DEPTH_MAX &&
cbm_kind_in_set(node, it->conditional) &&
ts_tree_cursor_goto_first_child(&it->cursor)) {
it->depth++;
node = ts_tree_cursor_current_node(&it->cursor);
}
if (it->conditional && cbm_kind_in_set(node, it->conditional)) {
continue; /* an empty (or too deeply nested) block holds no member */
}
if (it->named && !ts_node_is_named(node)) {
continue;
}
if (cbm_kind_in_set(inner, spec->function_node_types)) {
TSNode nm = resolve_method_name(inner, ctx->language);
if (!ts_node_is_null(nm)) {
push_method_def(ctx, inner, impl_node, class_qn, spec, nm);
*out = node;
return true;
}
return false;
}
/* Objective-C wraps every @implementation member in an implementation_definition,
* and a #if/#else block inside @implementation wraps each branch's members in
* another one: walk those nested wrappers too (worklist, no recursion). */
static void extract_objc_impl_methods(CBMExtractCtx *ctx, TSNode impl_node, const char *class_qn,
const CBMLangSpec *spec) {
TSNode pending[MEMBER_CONDITIONAL_DEPTH_MAX];
int count = 0;
pending[count++] = impl_node;
while (count > 0) {
TSNode cur = pending[--count];
member_iter_t it;
member_iter_init(&it, cur, ctx->language, false);
TSNode inner;
while (member_iter_next(&it, &inner)) {
if (cbm_kind_in_set(inner, spec->function_node_types)) {
TSNode nm = resolve_method_name(inner, ctx->language);
if (!ts_node_is_null(nm)) {
push_method_def(ctx, inner, impl_node, class_qn, spec, nm);
}
} else if (count < MEMBER_CONDITIONAL_DEPTH_MAX &&
strcmp(ts_node_type(inner), "implementation_definition") == 0) {
pending[count++] = inner;
}
}
member_iter_done(&it);
}
}
@@ -7845,12 +7898,10 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c
return;
}
uint32_t count = ts_node_child_count(body);
for (uint32_t i = 0; i < count; i++) {
TSNode child = ts_node_child(body, i);
if (ts_node_is_null(child)) {
continue;
}
member_iter_t it;
member_iter_init(&it, body, ctx->language, false);
TSNode child;
while (member_iter_next(&it, &child)) {
if (ctx->language == CBM_LANG_OBJC &&
strcmp(ts_node_type(child), "implementation_definition") == 0) {
@@ -7939,6 +7990,7 @@ static void extract_class_methods(CBMExtractCtx *ctx, TSNode class_node, const c
push_method_def(ctx, method_node, class_node, class_qn, spec, name_node);
}
member_iter_done(&it);
}
// --- Rust impl block extraction ---
@@ -8153,6 +8205,22 @@ static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSp
if (strcmp(macro, "def") == 0 || strcmp(macro, "defp") == 0 ||
strcmp(macro, "defmacro") == 0) {
extract_elixir_func_def(ctx, cur, macro);
} else if (strcmp(macro, "if") == 0 || strcmp(macro, "unless") == 0) {
/* A compile-time if/unless in a module body defines functions
* under a condition: its do and else branches hold variants of the
* same definitions (graph_buffer.c, "Definition variants"). The
* grammar nests the else_block inside the do_block. */
TSNode branch = cbm_find_child_by_kind(cur, "do_block");
for (int pass = 0; pass < 2 && !ts_node_is_null(branch); pass++) {
uint32_t bc = ts_node_child_count(branch);
for (int bi = (int)bc - SKIP_CHAR; bi >= 0; bi--) {
TSNode bchild = ts_node_child(branch, (uint32_t)bi);
if (!ts_node_is_null(bchild) && strcmp(ts_node_type(bchild), "call") == 0) {
ts_nstack_push(&stack, bchild);
}
}
branch = cbm_find_child_by_kind(branch, "else_block");
}
} else if (strcmp(macro, "defmodule") == 0) {
TSNode do_block = emit_elixir_module_class(ctx, cur);
if (!ts_node_is_null(do_block)) {
@@ -9811,10 +9879,10 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
}
CBMArena *a = ctx->arena;
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);
member_iter_t it;
member_iter_init(&it, body, ctx->language, true);
TSNode child;
while (member_iter_next(&it, &child)) {
// 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 &&
@@ -10128,6 +10196,7 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
cbm_defs_push(&ctx->result->defs, a, def);
}
member_iter_done(&it);
}
// Extract class-level variables (field declarations inside class bodies)
@@ -10146,13 +10215,15 @@ static void extract_class_variables(CBMExtractCtx *ctx, TSNode class_node, const
* push_var_def_qn); saved/restored so module-level minting stays bare. */
const char *saved_parent = ctx->var_parent_class;
ctx->var_parent_class = class_qn;
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);
member_iter_t it;
member_iter_init(&it, body, ctx->language, true);
TSNode child;
while (member_iter_next(&it, &child)) {
if (cbm_kind_in_set(child, spec->variable_node_types)) {
extract_var_names(ctx, child, spec);
}
}
member_iter_done(&it);
ctx->var_parent_class = saved_parent;
}
@@ -10312,8 +10383,10 @@ static void push_nested_class_nodes(TSNode body, const CBMLangSpec *spec, wd_sta
wd_push(s, child, enclosing_qn);
} else {
const char *ck = ts_node_type(child);
/* A nested class inside #if/#else is still nested here. */
if (strcmp(ck, "field_declaration") == 0 ||
strcmp(ck, "template_declaration") == 0 || strcmp(ck, "declaration") == 0) {
strcmp(ck, "template_declaration") == 0 || strcmp(ck, "declaration") == 0 ||
cbm_kind_in_set(child, cbm_conditional_block_types(spec->language))) {
ts_nstack_push(&nc_stack, child);
}
}
+9 -7
View File
@@ -47,7 +47,7 @@ static void parse_lisp_imports(CBMExtractCtx *ctx);
static void parse_starlark_imports(CBMExtractCtx *ctx);
static void parse_tcl_imports(CBMExtractCtx *ctx);
static void parse_teal_imports(CBMExtractCtx *ctx);
static void parse_zsh_imports(CBMExtractCtx *ctx);
static void parse_shell_source_imports(CBMExtractCtx *ctx);
static void parse_css_imports(CBMExtractCtx *ctx);
static void parse_html_imports(CBMExtractCtx *ctx);
static void parse_cmake_imports(CBMExtractCtx *ctx);
@@ -2187,10 +2187,13 @@ static void parse_teal_imports(CBMExtractCtx *ctx) {
}
}
// --- Zsh imports ---
// --- Bash / Zsh imports ---
// source file / . file — `command` nodes whose command_name is "source" or ".".
// The argument field carries the sourced path.
static void parse_zsh_imports(CBMExtractCtx *ctx) {
// The argument field carries the sourced path. Every other command is a call,
// not an import: read as an import, `set -e` imported "set", which the import
// resolver then bound to whatever project symbol is named set (a TSX method),
// and every `set` call in the script followed it.
static void parse_shell_source_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, ctx, CBM_SZ_512);
@@ -3049,8 +3052,7 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
parse_generic_imports(ctx, "call");
break;
case CBM_LANG_BASH:
// source/. commands
parse_generic_imports(ctx, "command");
parse_shell_source_imports(ctx);
break;
case CBM_LANG_ZIG:
parse_zig_imports(ctx);
@@ -3120,7 +3122,7 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
parse_teal_imports(ctx);
break;
case CBM_LANG_ZSH:
parse_zsh_imports(ctx);
parse_shell_source_imports(ctx);
break;
case CBM_LANG_CMAKE:
parse_cmake_imports(ctx);
+7 -3
View File
@@ -1110,9 +1110,13 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
return cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, qn_name);
}
/* Java/Go: directory-based module so this enclosing-func QN matches the def
* QN and the LSP caller_qn (the lsp_resolve join keys on exact equality). */
return cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, qn_name,
ctx->language);
* QN and the LSP caller_qn (the lsp_resolve join keys on exact equality).
* C-family platform variants share their platform-neutral QN — the same
* rule extract_func_def applies, so in-body calls source to the def. */
const char *qn = cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, qn_name,
ctx->language);
return cbm_platform_variant_qn(ctx->arena, ctx->language, ctx->project, ctx->rel_path, qn_name,
qn, node);
}
// Compute class QN for scope tracking.
+140
View File
@@ -1678,6 +1678,146 @@ char *cbm_fqn_compute_source_lang(CBMArena *a, const char *project, const char *
return cbm_arena_sprintf(a, "%s.%s", module, name);
}
/* Operating systems and CPU architectures only: a token names a platform, so a
* path segment or stem suffix made of one marks a platform alternative. Vague
* words (generic, default, common, stub) are deliberately absent. */
static const char *const platform_tokens[] = {
"win", "win32", "win64", "windows", "wince", "nt", "posix", "unix",
"linux", "darwin", "mac", "macos", "macosx", "osx", "apple", "ios",
"android", "bsd", "freebsd", "openbsd", "netbsd", "dragonfly", "solaris", "sunos",
"aix", "hpux", "haiku", "fuchsia", "cygwin", "mingw", "msvc", "wasm",
"wasi", "emscripten", "x86", "x86_64", "x64", "amd64", "i386", "i686",
"ia32", "arm", "arm64", "aarch64", "armv7", "riscv", "riscv64", "mips",
"mips64", "ppc", "ppc64", "powerpc", "s390", "s390x", "sparc", "sparc64",
"loongarch", "loongarch64", NULL};
static bool platform_token_eq(const char *s, size_t len) {
for (int i = 0; platform_tokens[i]; i++) {
const char *t = platform_tokens[i];
size_t tl = strlen(t);
if (tl != len) {
continue;
}
size_t k = 0;
while (k < len && tolower((unsigned char)s[k]) == t[k]) {
k++;
}
if (k == len) {
return true;
}
}
return false;
}
/* Length of `stem` without a trailing `_<token>` / `-<token>` / `.<token>`
* platform suffix (the longest one), or `len` when it has none. */
static size_t platform_stem_len(const char *stem, size_t len) {
size_t best = len;
for (size_t sep = 1; sep < len; sep++) {
char c = stem[sep - 1];
if ((c == '_' || c == '-' || c == '.') && platform_token_eq(stem + sep, len - sep)) {
if (sep - 1 < best) {
best = sep - 1;
}
}
}
return best;
}
static bool c_family_variant_lang(CBMLanguage lang) {
return lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
lang == CBM_LANG_OBJC;
}
static bool func_is_static(TSNode func_node) {
uint32_t n = ts_node_child_count(func_node);
for (uint32_t i = 0; i < n; i++) {
TSNode child = ts_node_child(func_node, i);
if (strcmp(ts_node_type(child), "storage_class_specifier") == 0 &&
ts_node_child_count(child) > 0 &&
strcmp(ts_node_type(ts_node_child(child, 0)), "static") == 0) {
return true;
}
}
return false;
}
const char *cbm_platform_variant_qn(CBMArena *a, CBMLanguage lang, const char *project,
const char *rel_path, const char *name, const char *qn,
TSNode func_node) {
if (!c_family_variant_lang(lang) || !rel_path || !name || !name[0] || !qn ||
ts_node_is_null(func_node) || func_is_static(func_node)) {
return qn;
}
char *plain = cbm_fqn_compute(a, project, rel_path, name);
if (!plain || strcmp(plain, qn) != 0) {
return qn; /* namespaced or otherwise scoped: not a file-scope function */
}
/* Rebuild the path without platform tokens: a directory segment that is
* one is dropped; a filename stem loses its platform suffix, and a stem
* that IS a token leaves the function at its directory. */
size_t len = strlen(rel_path);
char *neutral = (char *)cbm_arena_alloc(a, len + SKIP_ONE);
if (!neutral) {
return qn;
}
size_t out = 0;
bool changed = false;
bool dir_level = false;
const char *seg = rel_path;
while (*seg) {
const char *slash = strchr(seg, '/');
size_t seg_len = slash ? (size_t)(slash - seg) : strlen(seg);
if (slash) {
if (platform_token_eq(seg, seg_len)) {
changed = true;
} else {
memcpy(neutral + out, seg, seg_len);
out += seg_len;
neutral[out++] = '/';
}
seg = slash + SKIP_ONE;
continue;
}
const char *dot = NULL;
for (const char *p = seg + seg_len; p > seg; p--) {
if (p[-1] == '.') {
dot = p - 1;
break;
}
}
size_t stem_len = dot && dot > seg ? (size_t)(dot - seg) : seg_len;
size_t keep = platform_token_eq(seg, stem_len) ? 0 : platform_stem_len(seg, stem_len);
if (keep != stem_len) {
changed = true;
}
if (keep == 0) {
dir_level = true; /* the function lives at the directory */
if (out > 0 && neutral[out - SKIP_ONE] == '/') {
out--;
}
} else {
memcpy(neutral + out, seg, keep);
out += keep;
memcpy(neutral + out, seg + stem_len, seg_len - stem_len);
out += seg_len - stem_len;
}
break;
}
neutral[out] = '\0';
if (!changed) {
return qn;
}
if (dir_level) {
/* No file stem left: the folder form keeps every directory segment
* (cbm_fqn_compute would strip a dotted segment as an extension). */
char *folder = cbm_fqn_folder(a, project ? project : "", neutral);
return folder ? cbm_arena_sprintf(a, "%s.%s", folder, name) : qn;
}
char *variant = cbm_fqn_compute(a, project, neutral, name);
return variant ? variant : qn;
}
char *cbm_fqn_folder(CBMArena *a, const char *project, const char *rel_dir) {
// project.dir1.dir2
size_t proj_len = strlen(project);
+11
View File
@@ -250,6 +250,17 @@ char *cbm_fqn_module_source_lang(CBMArena *a, const char *project, const char *r
char *cbm_fqn_compute_source_lang(CBMArena *a, const char *project, const char *rel_path,
const char *name, CBMLanguage lang);
// C-family platform variants: the same external-linkage function defined in
// files that differ only by a platform token (foo_win.c / foo_posix.c,
// src/unix/fs.c / src/win/fs.c) is ONE function with platform variants — two
// external definitions of one name cannot link into one program, so they are
// alternatives by construction. Returns the platform-neutral QN (the path with
// its platform tokens removed) for a non-static file-scope function of
// C/C++/CUDA/ObjC whose `qn` is the plain file-stem form; otherwise `qn`.
const char *cbm_platform_variant_qn(CBMArena *a, CBMLanguage lang, const char *project,
const char *rel_path, const char *name, const char *qn,
TSNode func_node);
// Folder QN: project.dir_parts
char *cbm_fqn_folder(CBMArena *a, const char *project, const char *rel_dir);
+22
View File
@@ -2737,6 +2737,28 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
_Static_assert(sizeof(lang_specs) / sizeof(lang_specs[0]) == CBM_LANG_COUNT,
"lang_specs array size must match CBM_LANG_COUNT");
/* Conditional-compilation blocks per grammar (the `*_in_field_declaration_list`
* / `*_in_top_level` forms are aliased to these names by the grammars). */
static const char *c_family_conditional_types[] = {
"preproc_if", "preproc_ifdef", "preproc_else", "preproc_elif", "preproc_elifdef", NULL};
const char **cbm_conditional_block_types(CBMLanguage lang) {
switch (lang) {
case CBM_LANG_C:
case CBM_LANG_CPP:
case CBM_LANG_CUDA:
case CBM_LANG_OBJC:
case CBM_LANG_CSHARP:
case CBM_LANG_GLSL:
case CBM_LANG_HLSL:
case CBM_LANG_ISPC:
case CBM_LANG_SLANG:
return c_family_conditional_types;
default:
return NULL;
}
}
const CBMLangSpec *cbm_lang_spec(CBMLanguage lang) {
if (lang < 0 || lang >= CBM_LANG_COUNT) {
return NULL;
+7
View File
@@ -48,6 +48,13 @@ typedef struct {
// language rows; the table lives in extract_calls.c next to the dispatch code.
const char **cbm_string_dispatch_suffixes(CBMLanguage lang);
// Returns a NULL-terminated list of node types that wrap conditionally
// compiled alternatives (#if/#ifdef/#else/#elif blocks) for a language, or
// NULL when the grammar has none. A definition inside one is still a member
// of its enclosing scope; its #else twin is a variant of the same definition.
// Side table for the same -Wmissing-field-initializers reason as above.
const char **cbm_conditional_block_types(CBMLanguage lang);
// Get the language spec for a given language. Returns NULL for unsupported.
const CBMLangSpec *cbm_lang_spec(CBMLanguage lang);
+744
View File
@@ -0,0 +1,744 @@
/*
* spawn_patterns.c — process-spawning call sites, per language (see the header).
*
* Spellings come from what the extractor records as the callee (checked per
* language against real calls): Rust `Command::new`, Go `exec.Command`, Java
* `Runtime.getRuntime().exec`, a constructor as its class name.
*/
#include "spawn_patterns.h"
#include "foundation/constants.h"
#include <stdio.h>
#include <string.h>
#define Q CBM_SPAWN_QUALIFIED
#define U CBM_SPAWN_BARE
#define I CBM_SPAWN_BARE_IMPORTED
#define S CBM_SPAWN_STRING_RECEIVER
/* libc / POSIX / Win32. posix_spawn and _spawn* take the path second;
* CreateProcess names the program first or only in its command line. */
static const cbm_spawn_api_t c_family[] = {
{"system", NULL, U, 0, -1}, {"std::system", NULL, U, 0, -1},
{"_wsystem", NULL, U, 0, -1}, {"popen", NULL, U, 0, -1},
{"_popen", NULL, U, 0, -1}, {"_wpopen", NULL, U, 0, -1},
{"execl", NULL, U, 0, -1}, {"execle", NULL, U, 0, -1},
{"execlp", NULL, U, 0, -1}, {"execv", NULL, U, 0, -1},
{"execve", NULL, U, 0, -1}, {"execvp", NULL, U, 0, -1},
{"execvpe", NULL, U, 0, -1}, {"fexecve", NULL, U, 0, -1},
{"posix_spawn", NULL, U, 1, -1}, {"posix_spawnp", NULL, U, 1, -1},
{"_execl", NULL, U, 0, -1}, {"_execv", NULL, U, 0, -1},
{"_execlp", NULL, U, 0, -1}, {"_execvp", NULL, U, 0, -1},
{"_spawnl", NULL, U, 1, -1}, {"_spawnle", NULL, U, 1, -1},
{"_spawnlp", NULL, U, 1, -1}, {"_spawnv", NULL, U, 1, -1},
{"_spawnve", NULL, U, 1, -1}, {"_spawnvp", NULL, U, 1, -1},
{"_wspawnv", NULL, U, 1, -1}, {"_wspawnvp", NULL, U, 1, -1},
{"CreateProcess", NULL, U, 0, 1}, {"CreateProcessA", NULL, U, 0, 1},
{"CreateProcessW", NULL, U, 0, 1}, {"ShellExecute", NULL, U, 2, -1},
{"ShellExecuteA", NULL, U, 2, -1}, {"ShellExecuteW", NULL, U, 2, -1},
{"ShellExecuteExA", NULL, U, 0, -1}, {"ShellExecuteExW", NULL, U, 0, -1},
{"WinExec", NULL, U, 0, -1}, {NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t python[] = {
{"subprocess.run", NULL, Q, 0, -1},
{"subprocess.call", NULL, Q, 0, -1},
{"subprocess.check_call", NULL, Q, 0, -1},
{"subprocess.check_output", NULL, Q, 0, -1},
{"subprocess.Popen", NULL, Q, 0, -1},
{"subprocess.getoutput", NULL, Q, 0, -1},
{"subprocess.getstatusoutput", NULL, Q, 0, -1},
{"os.system", NULL, Q, 0, -1},
{"os.popen", NULL, Q, 0, -1},
{"os.startfile", NULL, Q, 0, -1},
{"os.execl", NULL, Q, 0, -1},
{"os.execle", NULL, Q, 0, -1},
{"os.execlp", NULL, Q, 0, -1},
{"os.execlpe", NULL, Q, 0, -1},
{"os.execv", NULL, Q, 0, -1},
{"os.execve", NULL, Q, 0, -1},
{"os.execvp", NULL, Q, 0, -1},
{"os.execvpe", NULL, Q, 0, -1},
{"os.posix_spawn", NULL, Q, 0, -1},
{"os.posix_spawnp", NULL, Q, 0, -1},
{"os.spawnl", NULL, Q, 1, -1},
{"os.spawnle", NULL, Q, 1, -1},
{"os.spawnlp", NULL, Q, 1, -1},
{"os.spawnlpe", NULL, Q, 1, -1},
{"os.spawnv", NULL, Q, 1, -1},
{"os.spawnve", NULL, Q, 1, -1},
{"os.spawnvp", NULL, Q, 1, -1},
{"os.spawnvpe", NULL, Q, 1, -1},
{"asyncio.create_subprocess_exec", NULL, Q, 0, -1},
{"asyncio.create_subprocess_shell", NULL, Q, 0, -1},
{"pexpect.spawn", NULL, Q, 0, -1},
{"pexpect.run", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
/* Node's child_process (also behind `node:`), the destructured-require form,
* execa / cross-spawn, and the Bun and Deno globals. */
static const cbm_spawn_api_t js_family[] = {
{"child_process.spawn", NULL, Q, 0, -1},
{"child_process.spawnSync", NULL, Q, 0, -1},
{"child_process.exec", NULL, Q, 0, -1},
{"child_process.execSync", NULL, Q, 0, -1},
{"child_process.execFile", NULL, Q, 0, -1},
{"child_process.execFileSync", NULL, Q, 0, -1},
{"child_process.fork", NULL, Q, 0, -1},
{"spawn", "child_process", I, 0, -1},
{"spawnSync", "child_process", I, 0, -1},
{"exec", "child_process", I, 0, -1},
{"execSync", "child_process", I, 0, -1},
{"execFile", "child_process", I, 0, -1},
{"execFileSync", "child_process", I, 0, -1},
{"fork", "child_process", I, 0, -1},
{"execa.execa", NULL, Q, 0, -1},
{"execa.execaSync", NULL, Q, 0, -1},
{"execa.execaCommand", NULL, Q, 0, -1},
{"execa.execaCommandSync", NULL, Q, 0, -1},
{"cross-spawn.spawn", NULL, Q, 0, -1},
{"cross-spawn.sync", NULL, Q, 0, -1},
{"Bun.spawn", NULL, Q, 0, -1},
{"Bun.spawnSync", NULL, Q, 0, -1},
{"Deno.Command", NULL, Q, 0, -1},
{"Deno.run", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t go[] = {
{"os/exec.Command", NULL, Q, 0, -1},
{"os/exec.CommandContext", NULL, Q, 1, -1},
{"golang.org/x/sys/execabs.Command", NULL, Q, 0, -1},
{"golang.org/x/sys/execabs.CommandContext", NULL, Q, 1, -1},
{"os.StartProcess", NULL, Q, 0, -1},
{"syscall.Exec", NULL, Q, 0, -1},
{"syscall.ForkExec", NULL, Q, 0, -1},
{"syscall.StartProcess", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t rust[] = {
{"std::process::Command::new", NULL, Q, 0, -1},
{"tokio::process::Command::new", NULL, Q, 0, -1},
{"async_process::Command::new", NULL, Q, 0, -1},
{"async_std::process::Command::new", NULL, Q, 0, -1},
{"smol::process::Command::new", NULL, Q, 0, -1},
{"duct::cmd", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
/* java.lang is implicit, so the class name alone is the spelling. */
static const cbm_spawn_api_t jvm[] = {
{"ProcessBuilder", NULL, U, 0, -1},
{"java.lang.ProcessBuilder", NULL, Q, 0, -1},
{"Runtime.getRuntime().exec", NULL, U, 0, -1},
{".execute", NULL, S, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t dotnet[] = {
{"Process.Start", NULL, U, 0, -1}, {"System.Diagnostics.Process.Start", NULL, Q, 0, -1},
{"ProcessStartInfo", NULL, U, 0, -1}, {"System.Diagnostics.ProcessStartInfo", NULL, Q, 0, -1},
{"Cli.Wrap", NULL, U, 0, -1}, {NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t ruby[] = {
{"system", NULL, U, 0, -1}, {"exec", NULL, U, 0, -1},
{"spawn", NULL, U, 0, -1}, {"Kernel.system", NULL, U, 0, -1},
{"Kernel.exec", NULL, U, 0, -1}, {"Kernel.spawn", NULL, U, 0, -1},
{"Process.spawn", NULL, U, 0, -1}, {"Process.exec", NULL, U, 0, -1},
{"IO.popen", NULL, U, 0, -1}, {"PTY.spawn", NULL, U, 0, -1},
{"Open3.popen2", NULL, U, 0, -1}, {"Open3.popen2e", NULL, U, 0, -1},
{"Open3.popen3", NULL, U, 0, -1}, {"Open3.capture2", NULL, U, 0, -1},
{"Open3.capture2e", NULL, U, 0, -1}, {"Open3.capture3", NULL, U, 0, -1},
{"Open3.pipeline", NULL, U, 0, -1}, {NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t php[] = {
{"exec", NULL, U, 0, -1},
{"shell_exec", NULL, U, 0, -1},
{"system", NULL, U, 0, -1},
{"passthru", NULL, U, 0, -1},
{"proc_open", NULL, U, 0, -1},
{"popen", NULL, U, 0, -1},
{"pcntl_exec", NULL, U, 0, -1},
{"Symfony\\Component\\Process\\Process", NULL, Q, 0, -1},
{"Symfony\\Component\\Process\\Process::fromShellCommandline", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t perl[] = {
{"system", NULL, U, 0, -1},
{"exec", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t lua[] = {
{"os.execute", NULL, U, 0, -1},
{"io.popen", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t r_lang[] = {
{"system", NULL, U, 0, -1},
{"system2", NULL, U, 0, -1},
{"shell", NULL, U, 0, -1},
{"processx::run", NULL, Q, 0, -1},
{"sys::exec_wait", NULL, Q, 0, -1},
{"sys::exec_internal", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t elixir[] = {
{"System.cmd", NULL, U, 0, -1},
{"System.shell", NULL, U, 0, -1},
{"Port.open", NULL, U, 0, CBM_SPAWN_ANY_LITERAL},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t erlang[] = {
{"os:cmd", NULL, Q, 0, -1},
{"open_port", NULL, U, 0, -1},
{"erlang:open_port", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t haskell[] = {
{"callProcess", "System.Process", I, 0, -1},
{"callCommand", "System.Process", I, 0, -1},
{"readProcess", "System.Process", I, 0, -1},
{"readProcessWithExitCode", "System.Process", I, 0, -1},
{"readCreateProcess", "System.Process", I, 0, -1},
{"readCreateProcessWithExitCode", "System.Process", I, 0, -1},
{"createProcess", "System.Process", I, 0, -1},
{"spawnProcess", "System.Process", I, 0, -1},
{"spawnCommand", "System.Process", I, 0, -1},
{"system", "System.Process", I, 0, -1},
{"rawSystem", "System.Process", I, 0, -1},
{"runProcess", "System.Process", I, 0, -1},
{"runCommand", "System.Process", I, 0, -1},
{"runInteractiveProcess", "System.Process", I, 0, -1},
{"runInteractiveCommand", "System.Process", I, 0, -1},
{"runProcess", "System.Process.Typed", I, 0, -1},
{"readProcess", "System.Process.Typed", I, 0, -1},
{"startProcess", "System.Process.Typed", I, 0, -1},
{"system", "System.Cmd", I, 0, -1},
{"rawSystem", "System.Cmd", I, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t dlang[] = {
{"execute", "std.process", I, 0, -1},
{"executeShell", "std.process", I, 0, -1},
{"spawnProcess", "std.process", I, 0, -1},
{"spawnShell", "std.process", I, 0, -1},
{"pipeProcess", "std.process", I, 0, -1},
{"pipeShell", "std.process", I, 0, -1},
{NULL, NULL, 0, 0, -1},
};
/* Foundation's Process: the program is a property set after construction,
* so a bare construction is a spawn site with a dynamic program. */
static const cbm_spawn_api_t swift[] = {
{"Process.launchedProcess", NULL, U, 0, -1},
{"Process.run", NULL, U, 0, -1},
{"Process", "Foundation", I, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t crystal[] = {
{"Process.run", NULL, U, 0, -1}, {"Process.new", NULL, U, 0, -1},
{"Process.exec", NULL, U, 0, -1}, {"system", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t zig[] = {
{"std.process.Child.init", NULL, Q, 0, -1},
{"std.process.Child.run", NULL, Q, 0, CBM_SPAWN_ANY_LITERAL},
{"std.ChildProcess.init", NULL, Q, 0, -1},
{"std.ChildProcess.run", NULL, Q, 0, CBM_SPAWN_ANY_LITERAL},
{"std.ChildProcess.exec", NULL, Q, 0, CBM_SPAWN_ANY_LITERAL},
{"std.process.execv", NULL, Q, 1, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t ocaml[] = {
{"Sys.command", NULL, U, 0, -1},
{"Unix.system", NULL, U, 0, -1},
{"Unix.create_process", NULL, U, 0, -1},
{"Unix.create_process_env", NULL, U, 0, -1},
{"Unix.open_process", NULL, U, 0, -1},
{"Unix.open_process_in", NULL, U, 0, -1},
{"Unix.open_process_out", NULL, U, 0, -1},
{"Unix.open_process_full", NULL, U, 0, -1},
{"Unix.open_process_args", NULL, U, 0, -1},
{"Unix.execv", NULL, U, 0, -1},
{"Unix.execvp", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t clojure[] = {
{"clojure.java.shell/sh", NULL, Q, 0, -1},
{"sh", "clojure.java.shell", I, 0, -1},
{"babashka.process/process", NULL, Q, 0, -1},
{"babashka.process/shell", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t tcl[] = {
{"exec", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t fortran[] = {
{"execute_command_line", NULL, U, 0, -1},
{"system", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t julia[] = {
{"run", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t powershell[] = {
{"Start-Process", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
#undef Q
#undef U
#undef I
#undef S
static const cbm_spawn_api_t *spawn_table(CBMLanguage lang) {
switch (lang) {
case CBM_LANG_C:
case CBM_LANG_CPP:
case CBM_LANG_CUDA:
case CBM_LANG_OBJC:
return c_family;
case CBM_LANG_PYTHON:
return python;
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX:
case CBM_LANG_ARKTS:
return js_family;
case CBM_LANG_GO:
return go;
case CBM_LANG_RUST:
return rust;
case CBM_LANG_JAVA:
case CBM_LANG_KOTLIN:
case CBM_LANG_SCALA:
case CBM_LANG_GROOVY:
return jvm;
case CBM_LANG_CSHARP:
case CBM_LANG_FSHARP:
return dotnet;
case CBM_LANG_RUBY:
return ruby;
case CBM_LANG_PHP:
return php;
case CBM_LANG_PERL:
return perl;
case CBM_LANG_LUA:
case CBM_LANG_LUAU:
case CBM_LANG_TEAL:
return lua;
case CBM_LANG_R:
return r_lang;
case CBM_LANG_ELIXIR:
return elixir;
case CBM_LANG_ERLANG:
return erlang;
case CBM_LANG_HASKELL:
return haskell;
case CBM_LANG_DLANG:
return dlang;
case CBM_LANG_SWIFT:
return swift;
case CBM_LANG_CRYSTAL:
return crystal;
case CBM_LANG_ZIG:
return zig;
case CBM_LANG_OCAML:
return ocaml;
case CBM_LANG_CLOJURE:
return clojure;
case CBM_LANG_TCL:
return tcl;
case CBM_LANG_FORTRAN:
return fortran;
case CBM_LANG_JULIA:
return julia;
case CBM_LANG_POWERSHELL:
return powershell;
default:
return NULL;
}
}
/* An import value without Node's `node:` scheme. */
static const char spawn_node_scheme[] = "node:";
static const char *spawn_module(const char *val) {
size_t n = sizeof(spawn_node_scheme) - 1;
return strncmp(val, spawn_node_scheme, n) == 0 ? val + n : val;
}
static const char *const spawn_separators[] = {"::", ".", ":", "/", "\\", NULL};
/* Length of the callee's head (before its first separator) and the
* separator's offset, or 0 when the callee has none. */
static size_t spawn_head_len(const char *callee) {
size_t best = 0;
for (int i = 0; spawn_separators[i]; i++) {
const char *at = strstr(callee, spawn_separators[i]);
if (at && at > callee && (best == 0 || (size_t)(at - callee) < best)) {
best = (size_t)(at - callee);
}
}
return best;
}
/* True when `val` names `name` as its last segment ("subprocess.run" for
* run, "java.lang.ProcessBuilder" for ProcessBuilder). */
static bool spawn_val_names(const char *val, const char *name) {
size_t vl = strlen(val);
size_t nl = strlen(name);
if (vl <= nl || strcmp(val + vl - nl, name) != 0) {
return false;
}
char sep = val[vl - nl - 1];
return sep == '.' || sep == ':' || sep == '/' || sep == '\\';
}
/* The callee as its import makes it: `sp.run` -> subprocess.run (an alias),
* `run` -> subprocess.run (a member import), `spawn` -> child_process.spawn
* (a named import mapped to its module). Empty when no import applies. */
static void spawn_expand(const char *callee, const CBMImport *imports, int count, char *out,
size_t n) {
out[0] = '\0';
size_t head = spawn_head_len(callee);
size_t key_len = head ? head : strlen(callee);
for (int i = 0; i < count; i++) {
const char *key = imports[i].local_name;
if (!key || !imports[i].module_path || strlen(key) != key_len ||
strncmp(key, callee, key_len) != 0) {
continue;
}
const char *val = spawn_module(imports[i].module_path);
if (head) {
snprintf(out, n, "%s%s", val, callee + head);
} else if (spawn_val_names(val, callee)) {
snprintf(out, n, "%s", val);
} else {
snprintf(out, n, "%s.%s", val, callee);
}
return;
}
}
static bool spawn_imports_module(const CBMImport *imports, int count, const char *module) {
size_t ml = strlen(module);
for (int i = 0; i < count; i++) {
if (!imports[i].module_path) {
continue;
}
const char *val = spawn_module(imports[i].module_path);
if (strncmp(val, module, ml) == 0 &&
(val[ml] == '\0' || val[ml] == '.' || val[ml] == ':' || val[ml] == '/')) {
return true;
}
}
return false;
}
/* Groovy `"git status".execute`: a quoted receiver, then the api suffix. */
static bool spawn_string_receiver(const char *callee, const char *suffix) {
size_t cl = strlen(callee);
size_t sl = strlen(suffix);
return (callee[0] == '"' || callee[0] == '\'') && cl > sl + 1 &&
strcmp(callee + cl - sl, suffix) == 0 && callee[cl - sl - 1] == callee[0];
}
static bool spawn_entry_matches(const cbm_spawn_api_t *e, const char *callee, const char *expanded,
const CBMImport *imports, int count) {
switch (e->rule) {
case CBM_SPAWN_QUALIFIED:
return strcmp(callee, e->api) == 0 || (expanded[0] && strcmp(expanded, e->api) == 0);
case CBM_SPAWN_BARE:
return strcmp(callee, e->api) == 0;
case CBM_SPAWN_BARE_IMPORTED:
return strcmp(callee, e->api) == 0 && spawn_imports_module(imports, count, e->module);
case CBM_SPAWN_STRING_RECEIVER:
return spawn_string_receiver(callee, e->api);
default:
return false;
}
}
const cbm_spawn_api_t *cbm_spawn_match(CBMLanguage lang, const char *callee,
const CBMImport *imports, int count) {
const cbm_spawn_api_t *table = spawn_table(lang);
if (!table || !callee || !callee[0]) {
return NULL;
}
if (!imports) {
count = 0;
}
char expanded[CBM_SZ_512];
spawn_expand(callee, imports, count, expanded, sizeof(expanded));
for (const cbm_spawn_api_t *e = table; e->api; e++) {
if (spawn_entry_matches(e, callee, expanded, imports, count)) {
return e;
}
}
return NULL;
}
/* The first quoted literal in `text` ("..", '..' or `..`), copied without
* its quotes. False when there is none. */
static bool spawn_first_literal(const char *text, char *out, size_t n) {
for (const char *p = text; *p; p++) {
if (*p != '"' && *p != '\'' && *p != '`') {
continue;
}
const char *end = p + 1;
while (*end && *end != *p) {
end += (*end == '\\' && end[1]) ? 2 : 1;
}
if (!*end) {
return false;
}
size_t len = (size_t)(end - p - 1);
if (len >= n) {
len = n - 1;
}
memcpy(out, p + 1, len);
out[len] = '\0';
return true;
}
return false;
}
static const char *spawn_arg_expr(const CBMCall *call, int index) {
for (int i = 0; i < call->arg_count; i++) {
if (call->args[i].index == index && call->args[i].expr) {
return call->args[i].expr;
}
}
return NULL;
}
static bool spawn_word_char(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}
static const char *spawn_skip_space(const char *p) {
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') {
p++;
}
return p;
}
/* Past a keyword label in front of an argument (`launchPath: `, `args=`). */
static const char *spawn_skip_label(const char *p) {
const char *q = p;
while (spawn_word_char(*q)) {
q++;
}
if (q == p) {
return p;
}
q = spawn_skip_space(q);
if ((*q == ':' && q[1] != ':') || (*q == '=' && q[1] != '=')) {
return spawn_skip_space(q + 1);
}
return p;
}
/* A string literal at the start of `p`, after a prefix such as r, b, f, L
* or @ (r"..", L"..", @".."). */
static bool spawn_leading_literal(const char *p, char *out, size_t n) {
for (int i = 0; i < 2 && *p && strchr("rRbBuUfFL@$", *p); i++) {
p++;
}
return (*p == '"' || *p == '\'' || *p == '`') && spawn_first_literal(p, out, n);
}
/* What may stand before a collection's bracket: nothing, Zig's `&.` / `.`,
* Java's `new String[]`, or a collection constructor. A name before `[` is a
* subscript (`record["argv"]`) and any other call's arguments are not the
* command (`os.path.join("bin", tool)`). */
static bool spawn_collection_prefix(const char *p, size_t len) {
static const char *const ctors[] = {
"listOf", "arrayOf", "mutableListOf", "List.of", "Arrays.asList", "vec!", "c", "Cmd", NULL};
if (len == 0 || (len <= 2 && strspn(p, "&.") == len) ||
(len > 2 && strncmp(p + len - 2, "[]", 2) == 0 && strncmp(p, "new ", 4) == 0)) {
return true;
}
for (int i = 0; ctors[i]; i++) {
if (strlen(ctors[i]) == len && strncmp(p, ctors[i], len) == 0) {
return true;
}
}
return false;
}
/* The first element of a collection written at `p` ([..], (..), {..},
* listOf(..), &.{..}), copied into `out`. False when `p` is no collection. */
static bool spawn_first_element(const char *p, char *out, size_t n) {
const char *open = p;
while (*open && (spawn_word_char(*open) || strchr("!.&:<> ", *open) ||
(open[0] == '[' && open[1] == ']'))) {
open += (open[0] == '[') ? 2 : 1;
}
if ((*open != '[' && *open != '(' && *open != '{') ||
!spawn_collection_prefix(p, (size_t)(open - p))) {
return false;
}
p = open;
const char *start = spawn_skip_space(p + 1);
int depth = 0;
const char *end = start;
for (; *end; end++) {
if (*end == '"' || *end == '\'' || *end == '`') {
const char *close = strchr(end + 1, *end);
if (!close) {
break;
}
end = close;
} else if (strchr("[({", *end)) {
depth++;
} else if (strchr("])}", *end)) {
if (depth-- == 0) {
break;
}
} else if (*end == ',' && depth == 0) {
break;
}
}
size_t len = (size_t)(end - start);
if (len == 0 || len >= n) {
return false;
}
memcpy(out, start, len);
out[len] = '\0';
return true;
}
/* The program literal an argument spells: a leading literal, or the first
* element of a collection when that element is one. A program held in a
* variable (`[binary, "--version"]`) is no literal: the arguments after it
* are not the program. */
static bool spawn_expr_literal(const char *expr, char *out, size_t n) {
char element[CBM_SZ_256];
for (int depth = 0; depth < 3; depth++) {
expr = spawn_skip_label(spawn_skip_space(expr));
if (spawn_leading_literal(expr, out, n)) {
return true;
}
if (!spawn_first_element(expr, element, sizeof(element))) {
return false;
}
expr = element;
}
return false;
}
/* A literal from the argument at `index`. Tcl words are literals unquoted;
* elsewhere an unquoted argument counts only when the extractor itself read
* it as the call's string (Perl's `system("make")`). */
static bool spawn_arg_literal(const CBMCall *call, CBMLanguage lang, int index, bool anywhere,
char *out, size_t n) {
const char *expr = spawn_arg_expr(call, index);
if (!expr) {
if (call->arg_count == 0 && index == 0 && call->first_string_arg) {
snprintf(out, n, "%s", call->first_string_arg);
return true;
}
return false;
}
if (anywhere ? spawn_first_literal(expr, out, n) : spawn_expr_literal(expr, out, n)) {
return true;
}
bool bareword = lang == CBM_LANG_TCL && strpbrk(expr, "$[") == NULL;
if (bareword || (call->first_string_arg && strcmp(expr, call->first_string_arg) == 0)) {
snprintf(out, n, "%s", expr);
return true;
}
return false;
}
/* A shell word that is not the program: an environment assignment
* (`LC_ALL=C make`) or the `env` that runs the program after them
* (`/usr/bin/env make`). */
static bool spawn_prefix_word(const char *word, size_t len) {
static const char env[] = "env";
size_t env_len = sizeof(env) - 1;
if (len >= env_len && strncmp(word + len - env_len, env, env_len) == 0 &&
(len == env_len || word[len - env_len - 1] == '/')) {
return true;
}
size_t i = 0;
while (i < len && spawn_word_char(word[i])) {
i++;
}
return i > 0 && i < len && word[i] == '=';
}
/* The program word of a command line: its first word that is not an
* environment prefix, without directories. Anything built at run time is not
* a program name. */
static void spawn_program_word(const char *literal, char *buf, size_t n) {
size_t len = 0;
for (;;) {
while (*literal == ' ' || *literal == '\t') {
literal++;
}
len = strcspn(literal, " \t\r\n");
if (!spawn_prefix_word(literal, len) || !literal[len]) {
break;
}
literal += len;
}
const char *start = literal;
for (size_t i = 0; i < len; i++) {
if (literal[i] == '/' || literal[i] == '\\') {
start = literal + i + 1;
}
}
len -= (size_t)(start - literal);
bool dynamic = len == 0 || len >= CBM_SPAWN_PROGRAM_MAX || len >= n;
for (size_t i = 0; i < len && !dynamic; i++) {
dynamic = strchr("$%{}`()<>|;&*?[]\"'", start[i]) != NULL;
}
if (dynamic) {
snprintf(buf, n, "%s", CBM_SPAWN_DYNAMIC);
return;
}
memcpy(buf, start, len);
buf[len] = '\0';
}
void cbm_spawn_program(const cbm_spawn_api_t *api, CBMLanguage lang, const CBMCall *call, char *buf,
size_t n) {
char literal[CBM_SZ_256];
bool found = false;
if (api->rule == CBM_SPAWN_STRING_RECEIVER) {
found = spawn_first_literal(call->callee_name, literal, sizeof(literal));
} else {
bool anywhere = api->alt == CBM_SPAWN_ANY_LITERAL;
found = spawn_arg_literal(call, lang, api->arg, anywhere, literal, sizeof(literal)) ||
(api->alt >= 0 &&
spawn_arg_literal(call, lang, api->alt, false, literal, sizeof(literal)));
}
if (!found) {
snprintf(buf, n, "%s", CBM_SPAWN_DYNAMIC);
return;
}
spawn_program_word(literal, buf, n);
}
+71
View File
@@ -0,0 +1,71 @@
/*
* spawn_patterns.h — process-spawning call sites, per language.
*
* Like the HTTP client tables in service_patterns.h: a call that starts
* another program (subprocess.run, exec.Command, posix_spawn, Process.Start,
* Command::new, ...) becomes a SPAWNS edge to a Process node named after the
* program it starts. Nothing resolves the program to code: the edge records
* that the caller leaves the graph through a process boundary, and which
* program it starts when the call spells it as a literal.
*/
#ifndef CBM_SPAWN_PATTERNS_H
#define CBM_SPAWN_PATTERNS_H
#include "cbm.h"
#include <stdbool.h>
#include <stddef.h>
/* How a table spelling matches a call. */
typedef enum {
/* The full spelling, written out or reached through the file's imports
* (`sp.run` with `import subprocess as sp`, `run` from
* `from subprocess import run`, Go `exec.Command` from "os/exec"). */
CBM_SPAWN_QUALIFIED = 0,
/* A bare name (`system`, `popen`, `exec`): the spelling alone. A
* definition of that name in the calling module (or one the call reaches
* through an import) still makes it an ordinary call. */
CBM_SPAWN_BARE,
/* A bare name that counts when the file imports `module`
* (Haskell `callProcess` with System.Process, a destructured
* `const { spawn } = require("child_process")`). */
CBM_SPAWN_BARE_IMPORTED,
/* Groovy `"git status".execute()`: the receiver is the command line. */
CBM_SPAWN_STRING_RECEIVER,
} cbm_spawn_rule_t;
typedef struct {
const char *api; /* canonical spelling: "subprocess.run", "os/exec.Command", "system" */
const char *module; /* the import a CBM_SPAWN_BARE_IMPORTED name needs, else NULL */
unsigned char rule; /* cbm_spawn_rule_t */
signed char arg; /* argument holding the program or command line */
signed char alt; /* second place to look, -1 for none (CreateProcess: name or line),
* or CBM_SPAWN_ANY_LITERAL */
} cbm_spawn_api_t;
/* `alt` value: the program is the first literal anywhere in the argument,
* which is an options tuple or struct (Elixir {:spawn, "cat"}, Zig
* .{ .argv = &.{"ls"} }). Elsewhere only a leading literal or a collection's
* first element is the program. */
#define CBM_SPAWN_ANY_LITERAL (-2)
/* The spawn API a call names, or NULL. `imports` are the calling file's own
* import statements (local name -> module path) as extracted, external
* modules included: subprocess or os/exec never resolve to project nodes, so
* the resolver's import map does not hold them. Whether the call is a spawn
* after all is the resolver's to decide: an evidence-backed resolution onto
* project code (an import, the same module, the LSP; never a name guess)
* makes it an ordinary call (src/pipeline/pass_spawns.c). */
const cbm_spawn_api_t *cbm_spawn_match(CBMLanguage lang, const char *callee,
const CBMImport *imports, int count);
/* The program a matched spawn starts: the first literal in its program
* argument, that literal's first word, without directories
* ("/usr/bin/git status" -> "git"). CBM_SPAWN_DYNAMIC when the program is
* not a literal. Writes into buf (at least CBM_SPAWN_PROGRAM_MAX bytes). */
#define CBM_SPAWN_DYNAMIC "<dynamic>"
#define CBM_SPAWN_PROGRAM_MAX 64
void cbm_spawn_program(const cbm_spawn_api_t *api, CBMLanguage lang, const CBMCall *call, char *buf,
size_t n);
#endif
+1 -1
View File
@@ -1564,7 +1564,7 @@ static const char skill_content[] =
"`check_index_coverage`, `manage_adr`, `ingest_traces`\n"
"\n"
"## Edge Types\n"
"CALLS, HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
"CALLS, HTTP_CALLS, ASYNC_CALLS, SPAWNS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CALL_REFERENCE, CONFIGURES, REFERENCES_FILE,\n"
"FILE_CHANGES_WITH, SIMILAR_TO, SEMANTICALLY_RELATED, CONTAINS_FILE, CONTAINS_FOLDER,\n"
"CONTAINS_PACKAGE\n"
+6 -1
View File
@@ -122,11 +122,16 @@ static const ext_entry_t EXT_TABLE[] = {
{".frm", CBM_LANG_FORM},
{".prc", CBM_LANG_FORM},
/* Fortran */
/* Fortran. Upper-case suffixes are the C-preprocessed spellings of the
* same free-form sources (#ifdef blocks); lookup is case-sensitive. */
{".f03", CBM_LANG_FORTRAN},
{".f08", CBM_LANG_FORTRAN},
{".f90", CBM_LANG_FORTRAN},
{".f95", CBM_LANG_FORTRAN},
{".F03", CBM_LANG_FORTRAN},
{".F08", CBM_LANG_FORTRAN},
{".F90", CBM_LANG_FORTRAN},
{".F95", CBM_LANG_FORTRAN},
/* GLSL */
{".frag", CBM_LANG_GLSL},
+188 -3
View File
@@ -835,6 +835,162 @@ void cbm_gbuf_set_next_id(cbm_gbuf_t *gb, int64_t next_id) {
gb->next_id = next_id;
}
/* ── Definition variants ─────────────────────────────────────────────
* One name defined more than once under mutually exclusive conditions —
* C-family #if/#else, Rust #[cfg], build-tagged files, a definition under an
* `if` — is ONE identity: one QN, one node, whose edges are the union of
* every variant's body. The collision rule below keeps one canonical winner
* for name, file, lines and properties; every variant's span survives in the
* node's `variants` property ([{"file_path","start_line","end_line"}],
* sorted, unique), so a change in any variant maps to the node. Recorded only
* between definitions of the same label with real spans: a different kind
* sharing the QN (C: struct vs function) is no variant, a span on line 0 is
* no location (delta-route proxies), and a full tie is the same entity. */
static void *gb_yy_malloc(void *ctx, size_t size) {
(void)ctx;
return cbm_alloc(CBM_MEM_CLASS_GBUF_STRING, size);
}
static void *gb_yy_realloc(void *ctx, void *block, size_t old_size, size_t size) {
(void)ctx;
(void)old_size;
return cbm_realloc(CBM_MEM_CLASS_GBUF_STRING, block, size);
}
static void gb_yy_free(void *ctx, void *block) {
(void)ctx;
cbm_free(CBM_MEM_CLASS_GBUF_STRING, block);
}
static const yyjson_alc gb_yy_alc = {gb_yy_malloc, gb_yy_realloc, gb_yy_free, NULL};
enum { GB_VARIANTS_MAX = 64 };
typedef struct {
const char *file;
int start;
int end;
} gb_span_t;
static bool gb_variant_label(const char *label) {
static const char *const labels[] = {"Function", "Method", "Class", "Interface", "Enum",
"Struct", "Trait", "Type", "Macro"};
for (size_t i = 0; label && i < sizeof(labels) / sizeof(labels[0]); i++) {
if (strcmp(label, labels[i]) == 0) {
return true;
}
}
return false;
}
static int gb_span_cmp(const void *a, const void *b) {
const gb_span_t *x = a;
const gb_span_t *y = b;
int c = strcmp(x->file, y->file);
if (c == 0) {
c = (x->start > y->start) - (x->start < y->start);
}
if (c == 0) {
c = (x->end > y->end) - (x->end < y->end);
}
return c;
}
/* A definition's spans: its `variants` property, else its own span. */
static int gb_spans_of(yyjson_doc *doc, const char *file, int start, int end, gb_span_t *out,
int n) {
yyjson_val *list = doc ? yyjson_obj_get(yyjson_doc_get_root(doc), "variants") : NULL;
if (yyjson_is_arr(list) && yyjson_arr_size(list) > 0) {
size_t i;
size_t max;
yyjson_val *v;
yyjson_arr_foreach(list, i, max, v) {
const char *f = yyjson_get_str(yyjson_obj_get(v, "file_path"));
int s = (int)yyjson_get_int(yyjson_obj_get(v, "start_line"));
int e = (int)yyjson_get_int(yyjson_obj_get(v, "end_line"));
if (f && s > 0 && n < GB_VARIANTS_MAX) {
out[n++] = (gb_span_t){f, s, e};
}
}
return n;
}
if (start > 0 && n < GB_VARIANTS_MAX) {
out[n++] = (gb_span_t){file ? file : "", start, end};
}
return n;
}
/* The survivor's properties with the union of both definitions' variant
* spans. Returns a GBUF_STRING-owned JSON string, or NULL when there is
* nothing to record (fewer than two distinct spans) or on any failure — the
* survivor then keeps its own properties: a lost variants list costs
* precision, never data. */
static char *gb_variants_props(const char *survivor_props, const char *a_props, const char *a_file,
int a_start, int a_end, const char *b_props, const char *b_file,
int b_start, int b_end) {
if (a_start <= 0 || b_start <= 0) {
return NULL;
}
yyjson_doc *a_doc =
a_props ? yyjson_read_opts((char *)a_props, strlen(a_props), 0, &gb_yy_alc, NULL) : NULL;
yyjson_doc *b_doc =
b_props ? yyjson_read_opts((char *)b_props, strlen(b_props), 0, &gb_yy_alc, NULL) : NULL;
gb_span_t spans[GB_VARIANTS_MAX * 2];
int n = gb_spans_of(a_doc, a_file, a_start, a_end, spans, 0);
n = gb_spans_of(b_doc, b_file, b_start, b_end, spans, n);
qsort(spans, (size_t)n, sizeof(spans[0]), gb_span_cmp);
int unique = 0;
for (int i = 0; i < n; i++) {
if (unique == 0 || gb_span_cmp(&spans[unique - 1], &spans[i]) != 0) {
spans[unique++] = spans[i];
}
}
char *out = NULL;
if (unique >= 2) {
yyjson_mut_doc *mut = yyjson_mut_doc_new(&gb_yy_alc);
yyjson_doc *s_doc = survivor_props
? yyjson_read_opts((char *)survivor_props, strlen(survivor_props),
0, &gb_yy_alc, NULL)
: NULL;
yyjson_mut_val *root = s_doc && yyjson_is_obj(yyjson_doc_get_root(s_doc))
? yyjson_val_mut_copy(mut, yyjson_doc_get_root(s_doc))
: yyjson_mut_obj(mut);
yyjson_mut_val *list = yyjson_mut_arr(mut);
for (int i = 0; root && list && i < unique; i++) {
yyjson_mut_val *v = yyjson_mut_arr_add_obj(mut, list);
yyjson_mut_obj_add_strcpy(mut, v, "file_path", spans[i].file);
yyjson_mut_obj_add_int(mut, v, "start_line", spans[i].start);
yyjson_mut_obj_add_int(mut, v, "end_line", spans[i].end);
}
if (mut && root && list) {
yyjson_mut_obj_remove_key(root, "variants");
yyjson_mut_obj_add_val(mut, root, "variants", list);
yyjson_mut_doc_set_root(mut, root);
out = yyjson_mut_write_opts(mut, 0, &gb_yy_alc, NULL, NULL);
}
yyjson_doc_free(s_doc);
yyjson_mut_doc_free(mut);
}
yyjson_doc_free(a_doc);
yyjson_doc_free(b_doc);
return out;
}
/* Record the variant spans of a same-QN collision on the surviving node. */
static void gb_record_variants(cbm_gbuf_node_t *survivor, const char *a_label, const char *a_props,
const char *a_file, int a_start, int a_end, const char *b_label,
const char *b_props, const char *b_file, int b_start, int b_end) {
if (!gb_variant_label(a_label) || !b_label || strcmp(a_label, b_label) != 0) {
return;
}
char *merged = gb_variants_props(survivor->properties_json, a_props, a_file, a_start, a_end,
b_props, b_file, b_start, b_end);
if (merged) {
cbm_free(CBM_MEM_CLASS_GBUF_STRING, survivor->properties_json);
survivor->properties_json = merged;
}
}
/* ── Node operations ─────────────────────────────────────────────── */
int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name,
@@ -892,8 +1048,19 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name
c = strcmp(existing->label ? existing->label : "", label ? label : "");
}
if (c > 0) {
gb_record_variants(existing, existing->label, existing->properties_json,
existing->file_path, existing->start_line, existing->end_line, label,
properties_json, file_path, start_line, end_line);
return existing->id; /* existing entity is the canonical winner */
}
/* The arrival wins: its properties, plus every variant span both carry. */
char *variant_props = NULL;
if (gb_variant_label(label) && existing->label && strcmp(existing->label, label) == 0) {
variant_props =
gb_variants_props(properties_json, existing->properties_json, existing->file_path,
existing->start_line, existing->end_line, properties_json,
file_path, start_line, end_line);
}
/* Update in-place. name/properties are strdup'd BEFORE freeing old ones
* (callers may pass existing->name as an argument). label/file_path are
* interned: gb_intern returns a stable pool pointer (idempotent even when
@@ -903,7 +1070,9 @@ int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name
* label/name — cbm_gbuf_find_by_label/name then missed or mis-listed it,
* which is how the flickering Function set reached the semantic pass). */
char *new_name = heap_strdup(name);
char *new_props = properties_json ? heap_strdup(properties_json) : NULL;
char *new_props = variant_props ? variant_props
: properties_json ? heap_strdup(properties_json)
: NULL;
const char *new_label_interned = gb_intern(gb, label);
bool label_changed = !existing->label || !new_label_interned ||
strcmp(existing->label, new_label_interned) != 0;
@@ -1493,7 +1662,22 @@ static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing,
c = strcmp(existing->label ? existing->label : "", sn->label ? sn->label : "");
}
bool sn_wins = c <= 0;
if (!sn_wins) {
gb_record_variants(existing, existing->label, existing->properties_json,
existing->file_path, existing->start_line, existing->end_line,
sn->label, sn->properties_json, sn->file_path, sn->start_line,
sn->end_line);
}
if (sn_wins) {
/* src's properties, plus every variant span both carry. */
char *variant_props = NULL;
if (gb_variant_label(sn->label) && existing->label &&
strcmp(existing->label, sn->label) == 0) {
variant_props = gb_variants_props(sn->properties_json, existing->properties_json,
existing->file_path, existing->start_line,
existing->end_line, sn->properties_json,
sn->file_path, sn->start_line, sn->end_line);
}
/* Keep the secondary indexes consistent when the surviving
* label/name changes (the old code left the node listed under its
* original label/name, so find_by_label/name mis-listed it). */
@@ -1518,9 +1702,10 @@ static void merge_update_existing(cbm_gbuf_t *dst, cbm_gbuf_node_t *existing,
existing->file_path = (char *)gb_intern(dst, sn->file_path);
existing->start_line = sn->start_line;
existing->end_line = sn->end_line;
if (sn->properties_json) {
if (variant_props || sn->properties_json) {
cbm_free(CBM_MEM_CLASS_GBUF_STRING, existing->properties_json);
existing->properties_json = heap_strdup(sn->properties_json);
existing->properties_json =
variant_props ? variant_props : heap_strdup(sn->properties_json);
}
if (label_changed) {
node_ptr_array_t *by_label = get_or_create_node_array(
+82 -11
View File
@@ -22,8 +22,10 @@
* reached, never expanded (their caller is the runner's main). */
#define TE_RUNNER_ROLE_PREFIX "tf_maybe_run_"
static const char *const te_walk_types[] = {"CALLS", "ASYNC_CALLS", "USAGE", "CALL_REFERENCE",
"READS"};
/* SPAWNS only carries reach from a Process node, which enters the walk
* through te_spawn_lift alone. */
static const char *const te_walk_types[] = {"CALLS", "ASYNC_CALLS", "USAGE",
"CALL_REFERENCE", "READS", "SPAWNS"};
typedef struct {
int64_t *items;
@@ -47,6 +49,10 @@ static bool te_is_function(const cbm_ti_node_t *n) {
* definition starts on the line of its TEST(...) / SUITE(...) — graphs name
* them differently (`x` without configured declarations, `TEST_x` with) —
* else every function named `name`. *count of them, ids appended. */
static bool te_named_match(const cbm_ti_node_t *n, int pass, int line, const char *name) {
return te_is_function(n) && (pass == 0 ? n->start_line == line : strcmp(n->name, name) == 0);
}
static bool te_named(te_ctx_t *c, const char *file, const char *name, int line, te_ids_t *out,
int *count) {
bool ok = true;
@@ -59,10 +65,17 @@ static bool te_named(te_ctx_t *c, const char *file, const char *name, int line,
*count = 0;
for (int pass = 0; pass < 2 && *count == 0; pass++) {
for (int i = 0; i < n; i++) {
bool match =
te_is_function(&nodes[i]) &&
(pass == 0 ? nodes[i].start_line == line : strcmp(nodes[i].name, name) == 0);
if (!match) {
/* Any span of a definition names it by its start line, the
* primary one or a variant (graph_buffer.c "Definition
* variants"); each definition counts once. */
if (!te_named_match(&nodes[i], pass, line, name)) {
continue;
}
bool counted = false;
for (int k = 0; k < i && !counted; k++) {
counted = nodes[k].id == nodes[i].id && te_named_match(&nodes[k], pass, line, name);
}
if (counted) {
continue;
}
(*count)++;
@@ -239,7 +252,7 @@ static bool te_runner_sinks(te_ctx_t *c, te_sinks_t *s) {
false;
}
for (int k = 0; k < count; k++) {
if (!te_is_function(&nodes[k])) {
if (!te_is_function(&nodes[k]) || nodes[k].variant_span) {
continue;
}
bool main_fn = strcmp(nodes[k].name, "main") == 0;
@@ -440,7 +453,7 @@ static bool te_gap_seeds(te_ctx_t *c, const te_gap_t *g, te_ids_t *out) {
int n = 0;
const cbm_ti_node_t *nodes = ok ? cbm_ti_graph_file(c->graph, g->file, &n, &ok) : NULL;
for (int i = 0; ok && i < n; i++) {
ok = te_ids_push(c, out, nodes[i].id);
ok = nodes[i].variant_span || te_ids_push(c, out, nodes[i].id);
}
return ok || te_fallback(c, CBM_TEST_RESULT_FALLBACK_GRAPH_UNAVAILABLE, "graph read failed");
}
@@ -519,7 +532,9 @@ static bool te_reach_rows(te_ctx_t *c, cbm_impact_walk_t *walk, const bool *fixt
int changed_n = 0;
const int *changed = cbm_ti_seeds_changed_cases(c->seeds, &changed_n);
c->reach = cbm_arena_calloc(&c->arena, (size_t)(n ? n : 1) * sizeof(*c->reach));
if (!c->reach) {
/* The graph node each row's name mapped to (0 = none or several). */
int64_t *row_id = cbm_arena_calloc(&c->arena, (size_t)(n ? n : 1) * sizeof(*row_id));
if (!c->reach || !row_id) {
return te_oom(c);
}
for (int i = 0; i < n; i++) {
@@ -540,6 +555,7 @@ static bool te_reach_rows(te_ctx_t *c, cbm_impact_walk_t *walk, const bool *fixt
for (int k = 0; k < ids.count; k++) {
reached = reached || cbm_impact_walk_reached(walk, ids.items[k]);
}
int64_t id = count == 1 && ids.count == 1 ? ids.items[0] : 0;
cbm_free(CBM_MEM_CLASS_EXTRACT, ids.items);
bool was_changed = fixture_cases[i];
for (int k = 0; k < changed_n; k++) {
@@ -553,12 +569,18 @@ static bool te_reach_rows(te_ctx_t *c, cbm_impact_walk_t *walk, const bool *fixt
continue;
}
if (at >= 0) {
/* the same name twice in one file: ambiguous, never narrowed */
c->reach[at].mapped = false;
/* The same name twice in one file is ambiguous and never narrowed,
* unless both are the same graph node: one test written once per
* #if/#elif branch is one definition with variants (graph_buffer.c
* "Definition variants"), as the graph already counts it. */
if (id == 0 || row_id[at] != id) {
c->reach[at].mapped = false;
}
c->reach[at].reached = c->reach[at].reached || reached;
c->reach[at].changed = c->reach[at].changed || was_changed;
continue;
}
row_id[c->reach_count] = id;
c->reach[c->reach_count++] = (cbm_test_reach_t){.file = cases[i].file,
.test = cases[i].name,
.mapped = count == 1,
@@ -666,6 +688,52 @@ static bool te_header_triggers(te_ctx_t *c) {
return true;
}
/* ── Spawned processes (src/pipeline/pass_spawns.c) ─────────────────
* A test that starts a program runs code no CALLS edge leads to: the program
* runs its main, and main reaches everything. The graph does not resolve
* which program a Process node is, so when the walk reaches a `main` outside
* the tests (the product's entry), every Process node joins the walk as a
* further seed, and SPAWNS, walked from the process to whoever starts it,
* carries the reach to the tests that spawn. More, never fewer. */
static bool te_spawn_lift(te_ctx_t *c, cbm_impact_walk_t *walk) {
cbm_node_t *mains = NULL;
int main_count = 0;
if (cbm_store_find_nodes_by_name(c->store, "test-impact", "main", &mains, &main_count) !=
CBM_STORE_OK) {
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_GRAPH_UNAVAILABLE, "graph read failed") &&
false;
}
bool product_main = false;
for (int i = 0; i < main_count && !product_main; i++) {
product_main = mains[i].label && strcmp(mains[i].label, "Function") == 0 &&
mains[i].file_path && !te_test_file(c, mains[i].file_path) &&
cbm_impact_walk_reached(walk, mains[i].id);
}
cbm_store_free_nodes(mains, main_count);
if (!product_main) {
return true;
}
cbm_node_t *processes = NULL;
int process_count = 0;
if (cbm_store_find_nodes_by_label(c->store, "test-impact", "Process", &processes,
&process_count) != CBM_STORE_OK) {
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_GRAPH_UNAVAILABLE, "graph read failed") &&
false;
}
te_ids_t seeds = {0};
bool ok = true;
for (int i = 0; ok && i < process_count; i++) {
ok = te_ids_push(c, &seeds, processes[i].id);
}
cbm_store_free_nodes(processes, process_count);
if (ok && seeds.count && cbm_impact_walk_run(walk, seeds.items, seeds.count) != CBM_STORE_OK) {
ok = te_fallback(c, CBM_TEST_RESULT_FALLBACK_ENGINE_SATURATED, "the impact walk failed") &&
false;
}
cbm_free(CBM_MEM_CLASS_EXTRACT, seeds.items);
return ok;
}
bool te_reach(te_ctx_t *c) {
int64_t *extra = NULL;
int extra_count = 0;
@@ -683,6 +751,9 @@ bool te_reach(te_ctx_t *c) {
}
}
}
/* After the runner check: the runner re-executing itself is a spawn too,
* and reaching its main through that is no change to the runner. */
ok = ok && te_spawn_lift(c, walk);
ok = ok && te_reach_rows(c, walk, fixture_cases) && te_suite_triggers(c, walk) &&
te_rule_triggers(c) && te_header_triggers(c);
c->static_complete = ok;
+51 -14
View File
@@ -89,8 +89,8 @@ struct cbm_ti_graph {
};
static bool ts_seedless(const char *label) {
static const char *const seedless[] = {"File", "Folder", "Project", "Module",
"Package", "Section", "Route", "Resource"};
static const char *const seedless[] = {"File", "Folder", "Project", "Module", "Package",
"Section", "Route", "Process", "Resource"};
for (size_t i = 0; i < sizeof(seedless) / sizeof(seedless[0]); i++) {
if (label && strcmp(label, seedless[i]) == 0) {
return true;
@@ -146,32 +146,69 @@ static int ts_node_compare(const void *left, const void *right) {
return a->id < b->id ? -1 : (a->id > b->id ? 1 : 0);
}
static bool ts_node_from_row(cbm_ti_graph_t *g, cbm_ti_node_t *n, const cbm_node_t *row) {
n->id = row->id;
n->label = cbm_arena_strdup(&g->arena, row->label ? row->label : "");
n->name = cbm_arena_strdup(&g->arena, row->name ? row->name : "");
n->qualified_name = cbm_arena_strdup(&g->arena, row->qualified_name ? row->qualified_name : "");
n->start_line = row->start_line;
n->end_line = row->end_line;
n->variant_span = false;
return n->label && n->name && n->qualified_name;
}
static bool ts_has_span(const cbm_ti_node_t *nodes, int count, const cbm_node_t *row) {
for (int i = 0; i < count; i++) {
if (nodes[i].id == row->id && nodes[i].start_line == row->start_line &&
nodes[i].end_line == row->end_line) {
return true;
}
}
return false;
}
/* The file's definitions, each with every span it has in the file: a
* definition written once per #if branch or platform file is ONE node
* (graph_buffer.c "Definition variants") whose own span is one variant, so
* its other variants here (other branches, or a node whose file_path names
* another platform file) join as further spans of the same node. A change
* in any variant then seeds that definition. */
static ts_file_t *ts_load_file(cbm_ti_graph_t *g, const char *file) {
cbm_node_t *rows = NULL;
int count = 0;
if (cbm_store_find_nodes_by_file(g->store, g->project, file, &rows, &count) != CBM_STORE_OK) {
return NULL;
}
cbm_node_t *spans = NULL;
int span_count = 0;
if (cbm_store_find_variant_spans_by_file(g->store, g->project, file, &spans, &span_count) !=
CBM_STORE_OK) {
cbm_store_free_nodes(rows, count);
return NULL;
}
int total = count + span_count;
ts_file_t *f = cbm_arena_calloc(&g->arena, sizeof(*f));
cbm_ti_node_t *nodes = cbm_arena_alloc(&g->arena, (size_t)(count ? count : 1) * sizeof(*nodes));
cbm_ti_node_t *nodes = cbm_arena_alloc(&g->arena, (size_t)(total ? total : 1) * sizeof(*nodes));
const char *key = cbm_arena_strdup(&g->arena, file);
bool ok = f && nodes && key;
int kept = 0;
for (int i = 0; ok && i < count; i++) {
if (ts_seedless(rows[i].label)) {
continue;
if (!ts_seedless(rows[i].label)) {
ok = ts_node_from_row(g, &nodes[kept++], &rows[i]);
}
}
for (int i = 0; ok && i < span_count; i++) {
if (!ts_seedless(spans[i].label) && !ts_has_span(nodes, kept, &spans[i])) {
bool listed = false;
for (int j = 0; j < kept && !listed; j++) {
listed = nodes[j].id == spans[i].id;
}
ok = ts_node_from_row(g, &nodes[kept], &spans[i]);
nodes[kept++].variant_span = listed;
}
cbm_ti_node_t *n = &nodes[kept++];
n->id = rows[i].id;
n->label = cbm_arena_strdup(&g->arena, rows[i].label ? rows[i].label : "");
n->name = cbm_arena_strdup(&g->arena, rows[i].name ? rows[i].name : "");
n->qualified_name =
cbm_arena_strdup(&g->arena, rows[i].qualified_name ? rows[i].qualified_name : "");
n->start_line = rows[i].start_line;
n->end_line = rows[i].end_line;
ok = n->label && n->name && n->qualified_name;
}
cbm_store_free_nodes(rows, count);
cbm_store_free_nodes(spans, span_count);
if (!ok) {
return NULL;
}
+4
View File
@@ -41,6 +41,10 @@ typedef struct {
const char *qualified_name;
int start_line;
int end_line;
/* A further span of a definition already listed (another #if branch or
* platform-file variant, graph_buffer.c "Definition variants"): it maps
* lines to the node, but enumerating definitions counts the node once. */
bool variant_span;
} cbm_ti_node_t;
/* Borrows the store until free. Nodes of a file are read once, on first use. */
+25
View File
@@ -686,6 +686,15 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
res.confidence = lsp->confidence;
res.strategy = lsp->strategy;
res.candidate_count = 1;
/* An LSP answer on a synthetic builtin (`<python-builtins>`) is
* not project code: a spawn spelling still spawns. The parallel
* pass keeps the LSP answer in `res` and asks the same question.
* MUST match pass_parallel.c. */
cbm_pipeline_spawn_t spawn;
if (cbm_pipeline_spawn_site(ctx->gbuf, lang, call, imports, &res, &spawn)) {
cbm_pipeline_emit_spawn(ctx->gbuf, source_node, call, &spawn);
return SKIP_ONE;
}
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys,
imp_vals, imp_count, false, route_mount);
return SKIP_ONE;
@@ -755,6 +764,14 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
if (!rust_external) {
res = cbm_registry_resolve(ctx->registry, call->callee_name, module_qn, imp_keys, imp_vals,
imp_count);
/* Cross-language veto: a name-only guess never binds another
* language's symbol (a JS app.get must not become Python's
* builtins.dict.get). The vetoed answer becomes EMPTY here, before
* the fallbacks below, so the route/HTTP classification still runs.
* MUST match pass_parallel.c. */
if (cbm_registry_name_guess_vetoed(ctx->registry, lang, &res)) {
res = (cbm_resolution_t){0};
}
}
/* `p->open(fd)`: the registry does not split a callee on the arrow, and a
* member is not a name it could resolve anyway. The object's type decides,
@@ -768,6 +785,14 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
call->enclosing_func_qn, call->callee_name,
cbm_c_member_rule_file(lang, rel), &res) != NULL;
}
/* A call that starts another program (subprocess.run, exec.Command,
* posix_spawn): a SPAWNS edge to its Process node instead of a CALLS
* edge. MUST match pass_parallel.c. */
cbm_pipeline_spawn_t spawn;
if (cbm_pipeline_spawn_site(ctx->gbuf, lang, call, imports, &res, &spawn)) {
cbm_pipeline_emit_spawn(ctx->gbuf, source_node, call, &spawn);
return SKIP_ONE;
}
if (!res.qualified_name || res.qualified_name[0] == '\0') {
/* Resolution is empty when the callee belongs to an EXTERNAL client
* library whose source is not in the indexed tree (e.g. `requests.get`,
+3 -3
View File
@@ -381,7 +381,7 @@ static void pd_add_file_doc(const cbm_gbuf_node_t *file_node, const char *doc) {
* `file_doc`, the file's own doc (passed with its first def only), goes on the
* File node. */
static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const char *rel,
const char *file_doc) {
const char *file_doc, CBMLanguage lang) {
if (!def->qualified_name || !def->name) {
return;
}
@@ -401,7 +401,7 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
* (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed
* through the same predicate, so the three registries cannot diverge. */
if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
cbm_registry_add_lang(ctx->registry, def->name, def->qualified_name, def->label, lang);
}
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
@@ -956,7 +956,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
/* Create nodes for each definition */
const char *file_doc = result->module_doc; /* goes with the first def */
for (int d = 0; d < result->defs.count; d++) {
process_def(ctx, &result->defs.items[d], rel, file_doc);
process_def(ctx, &result->defs.items[d], rel, file_doc, lang);
file_doc = NULL;
total_defs++;
}
+27 -10
View File
@@ -1730,7 +1730,7 @@ int cbm_parallel_extract(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
* once per file by the caller: computing the file QN and finding its node for
* every definition was 700 k allocations and lookups on the Go corpus. */
static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def,
int64_t file_node_id, int *reg_entries) {
int64_t file_node_id, CBMLanguage lang, int *reg_entries) {
int edges = 0;
if (!def->name || !def->qualified_name || !def->label) {
return 0;
@@ -1738,7 +1738,7 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
* (helpers.c) — see pass_definitions.c for the per-label rationale. */
if (cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
cbm_registry_add_lang(ctx->registry, def->name, def->qualified_name, def->label, lang);
(*reg_entries)++;
}
const cbm_gbuf_node_t *def_node = cbm_gbuf_find_by_qn(ctx->gbuf, def->qualified_name);
@@ -1933,8 +1933,8 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
free(file_qn);
pp_add_file_doc(file_node, result->module_doc);
for (int d = 0; d < result->defs.count; d++) {
defines_edges +=
register_and_link_def(ctx, &result->defs.items[d], file_node_id, &reg_entries);
defines_edges += register_and_link_def(ctx, &result->defs.items[d], file_node_id,
files[i].language, &reg_entries);
}
}
@@ -3186,6 +3186,13 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
_rc_t0 = extract_now_ns();
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
/* Cross-language veto — the same predicate as pass_calls.c, applied
* AFTER the field-type hint so the hint may still re-pick a compatible
* candidate first, and BEFORE the empty-resolution fallbacks below so a
* vetoed call still reaches route/HTTP classification. */
if (cbm_registry_name_guess_vetoed(rc->registry, lang, &res)) {
res = (cbm_resolution_t){0};
}
atomic_fetch_add_explicit(&rc->time_ns_rc_hint, extract_now_ns() - _rc_t0,
memory_order_relaxed);
@@ -3281,19 +3288,29 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
}
}
/* A call that starts another program (subprocess.run, exec.Command,
* posix_spawn): a SPAWNS edge to its Process node instead of a CALLS
* edge. `res` holds the LSP answer when there is one, as the
* sequential pass asks inside its LSP branch. MUST match pass_calls.c. */
cbm_pipeline_spawn_t spawn;
if (cbm_pipeline_spawn_site(rc->main_gbuf, lang, call, &result->imports, &res, &spawn)) {
cbm_pipeline_emit_spawn(ws->local_edge_buf, source_node, call, &spawn);
continue;
}
if (!res.qualified_name || res.qualified_name[0] == '\0') {
if (cbm_service_pattern_route_method(call->callee_name) != NULL) {
cbm_resolution_t fake_res = {.qualified_name = call->callee_name,
.confidence = PP_HALF_CONF,
.strategy = "callee_suffix"};
/* #2053: an LSP-external Rust call (`map.get(k)` on a std
* HashMap) reaches this branch only because its registry
* fallback was skipped. Without a route path the plain-CALLS
* fall-through would bind source -> source, a fabricated
* self-call, so it keeps only the route/service edges. */
/* The target here is the SOURCE itself (nothing resolved), so
* the plain-CALLS fall-through for a route-verb call without a
* route path (`cache.get(key)`) would be a self-loop; that holds
* for #2053's LSP-external Rust calls too. Suppress it always;
* route/HTTP/service classification is unchanged. */
emit_service_edge(ws->local_edge_buf, source_node, source_node, call, &fake_res,
module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals,
imp_count, rust_external, route_mount);
imp_count, true, route_mount);
} else if (cbm_service_pattern_is_global_fetch(call->callee_name)) {
/* Native `fetch()` (#856): only the global API once resolution
* has failed to find a local/imported `fetch`. Call the low-level
+115
View File
@@ -0,0 +1,115 @@
/*
* pass_spawns.c — SPAWNS edges: a call that starts another program, to the
* Process node of that program (internal/cbm/spawn_patterns.h).
*
* Shared by both call resolvers (pass_calls.c, pass_parallel.c), which ask
* at the same point — after registry resolution and the cross-language veto,
* before the empty-resolution fallbacks — so they emit the same edges.
*/
#include "pipeline/pipeline_internal.h"
#include "graph_buffer/graph_buffer.h"
#include "foundation/constants.h"
#include "foundation/str_util.h"
#include "spawn_patterns.h"
#include <stdio.h>
#include <string.h>
/* Edge confidence: a site read from the source, or only from the
* macro-expanded buffer of a C-family file. */
static const double SPAWN_CONF_SOURCE = 1.0;
static const double SPAWN_CONF_EXPANDED = 0.5;
/* Strategies that only guess by name: they never make a spawn spelling an
* ordinary call (`subprocess.run` must not become the project's `run`, nor
* a C `system(...)` the static `system` of another file). */
static bool spawn_name_guess(const char *strategy) {
static const char *const guesses[] = {"unique_name", "suffix_match", "qualified_suffix",
"field_type_hint", "fuzzy", NULL};
for (int i = 0; strategy && guesses[i]; i++) {
if (strcmp(strategy, guesses[i]) == 0) {
return true;
}
}
return false;
}
/* Labels of code a call can run: a function, or a type it constructs. */
static bool spawn_code_label(const char *label) {
static const char *const labels[] = {"Function", "Method", "Class", "Struct", "Interface",
"Trait", "Enum", "Type", "Macro", NULL};
for (int i = 0; label && labels[i]; i++) {
if (strcmp(label, labels[i]) == 0) {
return true;
}
}
return false;
}
/* The call reached code in the project's own sources. A synthetic builtin
* (a `<python-builtins>` node), a QN without a node, and a Variable are not:
* `const { spawn } = require("child_process")` mints a Variable `spawn` that
* the call resolves to by same module, and it is the import binding. For a
* qualified spelling, same_module is a guess too: it matched the suffix
* alone, so `subprocess.run` inside a module's own `def run` found that
* `run`. */
static bool spawn_resolved_to_project(const cbm_gbuf_t *gbuf, const cbm_resolution_t *res,
bool qualified) {
if (!res || !res->qualified_name || !res->qualified_name[0]) {
return false;
}
const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(gbuf, res->qualified_name);
if (!node || !node->file_path || !node->file_path[0] || node->file_path[0] == '<' ||
!spawn_code_label(node->label)) {
return false;
}
bool guess = spawn_name_guess(res->strategy) ||
(qualified && res->strategy && strcmp(res->strategy, "same_module") == 0);
return !guess;
}
bool cbm_pipeline_spawn_site(const cbm_gbuf_t *gbuf, CBMLanguage lang, const CBMCall *call,
const CBMImportArray *imports, const cbm_resolution_t *res,
cbm_pipeline_spawn_t *out) {
const cbm_spawn_api_t *api = cbm_spawn_match(
lang, call->callee_name, imports ? imports->items : NULL, imports ? imports->count : 0);
bool qualified = strpbrk(call->callee_name, ".:/\\") != NULL;
if (!api || spawn_resolved_to_project(gbuf, res, qualified)) {
return false;
}
out->api = api->api;
cbm_spawn_program(api, lang, call, out->program, sizeof(out->program));
return true;
}
void cbm_pipeline_emit_spawn(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
const cbm_pipeline_spawn_t *spawn) {
char program[CBM_SZ_128];
char api[CBM_SZ_256];
cbm_json_escape(program, (int)sizeof(program), spawn->program);
cbm_json_escape(api, (int)sizeof(api), spawn->api);
char qn[CBM_SZ_128];
snprintf(qn, sizeof(qn), "__process__%s", spawn->program);
char node_props[CBM_SZ_256];
snprintf(node_props, sizeof(node_props), "{\"program\":\"%s\"}", program);
int64_t process_id =
cbm_gbuf_upsert_node(gbuf, "Process", spawn->program, qn, "", 0, 0, node_props);
if (process_id <= 0) {
return;
}
/* C-family files are extracted twice; a call from the macro-expanded
* buffer carries an expanded-buffer line, not a source line. It records
* no line and less confidence, so the raw site's edge (which wins on
* confidence when both exist) keeps the source line. */
char edge_props[CBM_SZ_512];
if (call->source_origin == CBM_SOURCE_ORIGIN_RAW) {
snprintf(edge_props, sizeof(edge_props),
"{\"api\":\"%s\",\"program\":\"%s\",\"line\":%d,\"confidence\":%.2f}", api,
program, call->start_line, SPAWN_CONF_SOURCE);
} else {
snprintf(edge_props, sizeof(edge_props),
"{\"api\":\"%s\",\"program\":\"%s\",\"confidence\":%.2f}", api, program,
SPAWN_CONF_EXPANDED);
}
cbm_gbuf_insert_edge(gbuf, source->id, process_id, "SPAWNS", edge_props);
}
+34 -1
View File
@@ -235,10 +235,28 @@ typedef struct {
cbm_registry_t *cbm_registry_new(void);
void cbm_registry_free(cbm_registry_t *r);
/* Register a function/method/class. All strings are copied. */
/* Register a function/method/class. All strings are copied. The entry's
* language is unknown, so name-based resolution never filters it out. */
void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label);
/* As cbm_registry_add, recording `lang`: the DETECTED language of the file
* that defines the symbol (CBM_LANG_COUNT = unknown), which the cross-language
* veto (cbm_registry_name_guess_vetoed) checks against the caller. */
void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label, CBMLanguage lang);
/* True when code in `caller` can call a symbol defined in `target` by bare
* name: same language, same interop family (JS/TS + script hosts, C/C++/CUDA/
* ObjC, JVM, .NET, ...; the table lives in registry.c), or Swift with the C
* family. An unknown language (CBM_LANG_COUNT) on either side is compatible. */
bool cbm_lang_resolution_compatible(CBMLanguage caller, CBMLanguage target);
/* Language of a synthetic definition source minted by a language's own LSP
* layer ("<python-builtins>" -> Python, "<kotlin-builtins>" -> Kotlin);
* CBM_LANG_COUNT for anything else. */
CBMLanguage cbm_registry_synthetic_path_language(const char *file_path);
/* Resolve a callee name using prioritized strategies.
* import_map: NULL-terminated array of {local_name, resolved_qn} pairs, or NULL.
* Returns result with qualified_name="" if unresolved.
@@ -250,6 +268,21 @@ cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *calle
const char *module_qn, const char **import_map_keys,
const char **import_map_vals, int import_map_count);
/* Cross-language veto for a CALLS resolution made by a caller written in
* `caller_lang` (its file's detected language). True when `res` is a
* name-only answer (qualified_suffix, unique_name, suffix_match, the
* parallel field_type_hint re-pick, or same_module: two files of different
* languages with the same path stem share a module QN) whose target's recorded language the
* caller cannot call (cbm_lang_resolution_compatible): a JS call must never
* bind a Python symbol by spelling alone. Callers treat a vetoed answer as
* EMPTY before their empty-resolution fallbacks (route registration, HTTP
* clients), so those still classify the call. A veto, not a re-pick: the
* strategy chain is unchanged. Import-map and lsp_* answers are never
* vetoed; CBM_LANG_COUNT never vetoes. pass_calls.c and pass_parallel.c
* MUST apply it identically. Pure; unit-tested in test_registry.c. */
bool cbm_registry_name_guess_vetoed(const cbm_registry_t *r, CBMLanguage caller_lang,
const cbm_resolution_t *res);
/* Relation-permitting resolve for SQL FROM/JOIN lineage usages ONLY — the one
* consumer allowed to bind Table/View targets. Uncached (the per-file resolve
* cache stores the default variant's relation-vetoed answers). */
+134 -6
View File
@@ -1254,12 +1254,52 @@ static bool incr_label_is_registry_symbol(const char *label) {
* project's definition symbols so the resolver can match cross-file symbols
* during incremental. Mirrors the full-index registry contents exactly so an
* incremental re-resolve picks the same nodes a full reindex would. */
/* Each symbol is seeded with its file's DETECTED language, exactly what the
* full index recorded (pass_definitions.c / pass_parallel.c), so the cross-
* language veto judges an incremental re-resolve the same way. Stored nodes carry no
* language, so it comes from the current discovery (rel_path -> language);
* synthetic LSP sources (<python-builtins>) map by their fixed path, which is
* also why a builtin stub that outlives the last .py file stays Python. */
typedef struct {
cbm_registry_t *registry;
CBMHashTable *lang_by_path; /* rel_path -> (CBMLanguage + 1); keys borrowed */
} registry_seed_t;
static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) {
cbm_registry_t *r = (cbm_registry_t *)userdata;
registry_seed_t *seed = (registry_seed_t *)userdata;
if (!incr_label_is_registry_symbol(node->label)) {
return;
}
cbm_registry_add(r, node->name, node->qualified_name, node->label);
CBMLanguage lang = cbm_registry_synthetic_path_language(node->file_path);
if (lang == CBM_LANG_COUNT && seed->lang_by_path && node->file_path) {
void *v = cbm_ht_get(seed->lang_by_path, node->file_path);
if (v) {
lang = (CBMLanguage)((uintptr_t)v - SKIP_ONE);
}
}
cbm_registry_add_lang(seed->registry, node->name, node->qualified_name, node->label, lang);
}
/* Seed the registry from every registry symbol in the graph buffer, with
* languages taken from the discovered files (see registry_visitor). */
static void registry_seed_from_gbuf(cbm_registry_t *registry, const cbm_gbuf_t *gbuf,
const cbm_file_info_t *files, int file_count) {
registry_seed_t seed = {.registry = registry, .lang_by_path = NULL};
if (files && file_count > 0) {
seed.lang_by_path = cbm_ht_create((size_t)file_count * PAIR_LEN);
}
if (seed.lang_by_path) {
for (int i = 0; i < file_count; i++) {
if (files[i].rel_path) {
cbm_ht_set(seed.lang_by_path, files[i].rel_path,
(void *)((uintptr_t)files[i].language + SKIP_ONE));
}
}
}
cbm_gbuf_foreach_node(gbuf, registry_visitor, &seed);
if (seed.lang_by_path) {
cbm_ht_free(seed.lang_by_path);
}
}
static void free_incremental_result_cache(CBMFileResult **cache, int count) {
@@ -1294,6 +1334,9 @@ enum {
* always worth attempting; the percentage governs at scale, where it is
* the honest signal that a rebuild costs less than a repair. */
CLOSURE_BUDGET_FLOOR_FILES = 8,
/* Variant partners of partners: real chains are one step (a header and
* its source, a set of platform files); a longer one declines. */
CLOSURE_VARIANT_ROUNDS_MAX = 8,
};
typedef struct {
@@ -1975,6 +2018,84 @@ static void closure_seed_governed(CBMHashTable *closure_set, const cbm_file_info
}
}
static bool closure_path_listed(char **paths, int count, const char *path) {
for (int i = 0; i < count; i++) {
if (strcmp(paths[i], path) == 0) {
return true;
}
}
return false;
}
/* Variant partners (graph_buffer.c "Definition variants"): a definition
* written in several files (platform files, a struct's header declaration
* and its source definition, Go build-tagged files) is ONE node whose
* file_path names one of them, with a DEFINES edge from every file that
* writes a variant. Re-parsing one of those files alone rebuilds the node
* without the others' spans and outbound edges, or leaves a changed
* partner's stale span behind; so every file of such a node joins the
* closure, to a fixpoint. `paths` is the delta (changed, then deleted from
* index n_changed on); `dependents` already joined. A deleted partner is in
* the delta already; any other partner outside the discovery declines, as a
* dependent would. Returns the decline reason, or NULL. */
static const char *closure_add_variant_partners(cbm_store_t *store, const char *project,
CBMHashTable *closure_set,
CBMHashTable *files_by_path, char **paths,
int path_count, int n_changed, char **dependents,
int dependent_count) {
int cap = path_count + dependent_count + CBM_SZ_64;
const char **pending = cbm_alloc(CBM_MEM_CLASS_OTHER, (size_t)cap * sizeof(char *));
if (!pending) {
return "alloc";
}
int n = 0;
for (int i = 0; i < path_count; i++) {
pending[n++] = paths[i];
}
for (int i = 0; i < dependent_count; i++) {
pending[n++] = dependents[i];
}
const char *decline = NULL;
for (int round = 0; n > 0 && !decline; round++) {
char **partners = NULL;
int pc = 0;
if (round == CLOSURE_VARIANT_ROUNDS_MAX) {
decline = "variant_partner_rounds";
} else if (cbm_store_get_variant_partner_files(store, project, pending, n, &partners,
&pc) != CBM_STORE_OK) {
decline = "variant_partner_query_failed";
}
n = 0;
for (int i = 0; i < pc && !decline; i++) {
const cbm_file_info_t *fi = cbm_ht_get(files_by_path, partners[i]);
if (!fi) {
if (!closure_path_listed(paths + n_changed, path_count - n_changed, partners[i])) {
decline = "variant_partner_not_discovered";
}
continue;
}
if (cbm_ht_get(closure_set, fi->rel_path)) {
continue;
}
cbm_ht_set(closure_set, fi->rel_path, (void *)fi->rel_path);
if (n == cap) {
cap *= PAIR_LEN;
const char **grown =
cbm_realloc(CBM_MEM_CLASS_OTHER, pending, (size_t)cap * sizeof(char *));
if (!grown) {
decline = "alloc";
break;
}
pending = grown;
}
pending[n++] = fi->rel_path;
}
cbm_store_free_dependent_files(partners, pc);
}
cbm_free(CBM_MEM_CLASS_OTHER, pending);
return decline;
}
/* Decide whether this manifest delta is closure-repairable, and if so build
* the plan. Returns 1 with *plan filled, or 0 (decline → FULL) with the
* reason logged. Every early exit declines; nothing here mutates state. */
@@ -2194,6 +2315,12 @@ static int closure_try_plan(cbm_pipeline_t *p, cbm_store_t *store, const char *p
cbm_ht_set(closure_set, dependents[i], dependents[i]);
}
}
decline =
closure_add_variant_partners(store, project, closure_set, files_by_path, changed_paths,
n_changed + n_deleted, n_changed, dependents, dependent_count);
if (decline) {
goto done;
}
/* closure_count == 0 is legitimate: a deleted-only delta with no
* dependents has nothing to re-parse, but the purge itself still needs
* the executor. */
@@ -2265,7 +2392,8 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
closure_plan_t *plan, cbm_file_info_t *changed_files, int ci,
char **deleted, int deleted_count, cbm_file_hash_t *mode_skipped,
int mode_skipped_count, cbm_coverage_row_t *old_cov, int old_cov_count,
cbm_store_t *route_store, struct timespec t0) {
cbm_store_t *route_store, const cbm_file_info_t *files, int file_count,
struct timespec t0) {
struct timespec t;
cbm_store_close(route_store);
@@ -2335,7 +2463,7 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
if (!registry) {
goto out;
}
cbm_gbuf_foreach_node(gbuf, registry_visitor, registry);
registry_seed_from_gbuf(registry, gbuf, files, file_count);
cbm_log_info("delta.preseed_done", "registry", itoa_buf(cbm_registry_size(registry)),
"elapsed_ms", itoa_buf((int)elapsed_ms(t)));
@@ -2928,7 +3056,7 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
return run_closure_delta(p, db_path, project, baseline_manifest, baseline_count,
&closure_plan, changed_files, ci, deleted, deleted_count,
mode_skipped, mode_skipped_count, old_cov, old_cov_count, store,
t0);
files, file_count, t0);
}
struct timespec t;
@@ -3035,7 +3163,7 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
/* Step 3-5: Registry + extract + resolve */
cbm_registry_t *registry = cbm_registry_new();
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
cbm_gbuf_foreach_node(existing, registry_visitor, registry);
registry_seed_from_gbuf(registry, existing, files, file_count);
cbm_log_info("incremental.registry_seed", "symbols", itoa_buf(cbm_registry_size(registry)),
"elapsed_ms", itoa_buf((int)elapsed_ms(t)));
+24
View File
@@ -17,6 +17,7 @@
#include "discover/test_conventions.h"
#include "git/git_context.h"
#include "foundation/hash_table.h"
#include "spawn_patterns.h"
#include "cbm.h"
#include "lsp/go_lsp.h" /* CBMLSPDef for cbm_parallel_resolve cross-LSP inputs */
#include <stdatomic.h>
@@ -572,6 +573,29 @@ bool cbm_pipeline_http_client_call_url(const cbm_gbuf_t *gbuf, const char *proje
const char **imp_vals, int imp_count, const CBMCall *call,
char *out, size_t out_sz);
/* A spawn site (pass_spawns.c): the API the call names and the program it
* starts (CBM_SPAWN_DYNAMIC when not a literal). */
typedef struct {
const char *api;
char program[CBM_SPAWN_PROGRAM_MAX];
} cbm_pipeline_spawn_t;
/* True when `call` starts another program (spawn_patterns.h) and `res`, the
* registry answer after the cross-language veto, does not make it a call into
* the project's own code. `imports` are the file's extracted imports, not the
* resolver's import map: that one holds only imports that resolved to project
* nodes, and subprocess or os/exec never do. Both call resolvers ask at the
* same point and, on true, emit cbm_pipeline_emit_spawn INSTEAD of a CALLS
* edge. */
bool cbm_pipeline_spawn_site(const cbm_gbuf_t *gbuf, CBMLanguage lang, const CBMCall *call,
const CBMImportArray *imports, const cbm_resolution_t *res,
cbm_pipeline_spawn_t *out);
/* The Process node of the spawned program (QN __process__<program>, one per
* program) and the SPAWNS edge from `source` to it. */
void cbm_pipeline_emit_spawn(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source, const CBMCall *call,
const cbm_pipeline_spawn_t *spawn);
/* Resolve an import to its in-graph target node, or NULL if unresolvable.
*
* Resolution order (first hit wins):
+223 -10
View File
@@ -84,8 +84,14 @@ typedef struct {
int cap;
uint8_t *is_test; /* is_test_qn(items[i]), one byte per entry */
int is_test_cap;
/* Language of the file that defined items[i] (a CBMLanguage; CBM_LANG_COUNT
* = unknown, compatible with every caller), one byte per entry. Grown in
* lockstep with is_test, so is_test_cap bounds it too. */
uint8_t *lang;
} qn_array_t;
_Static_assert(CBM_LANG_COUNT <= UINT8_MAX, "CBMLanguage must fit the per-entry lang byte");
struct cbm_registry {
/* Interned label strings (<=~30 distinct labels; owned here, freed in
* _free). The exact map's VALUES point into this pool instead of one
@@ -724,6 +730,152 @@ static bool c_cpp_family(CBMLanguage lang) {
return lang == CBM_LANG_C || lang == CBM_LANG_CPP;
}
/* ── Cross-language resolution compatibility ─────────────────────────
* A name-based registry guess (qualified_suffix / unique_name / suffix_match)
* carries no import or type evidence, so its answer only stands when the
* caller's language can actually call the target by that name
* (cbm_registry_name_guess_vetoed). same_module joins them: module QNs drop
* the extension, so tests/x.c and tests/x.py share one module, and a call in
* the C file found the Python function by name inside it. Families are the interop
* groups the resolvers already honour, plus the well-known interop pairs:
* - JS family: JS/TS/TSX/ArkTS (the #725 and #1928 guards) and the script
* hosts whose calls ARE JS/TS (HTML/Vue/Svelte/Astro, the weak-member
* guard's embedded-script set; QML embeds JS and imports .js files).
* - C family: C/C++ (.h maps to C++, the #1928 guard), plus CUDA and ObjC,
* which call C/C++ declarations from shared headers directly.
* - JVM: Java/Kotlin/Scala/Groovy call each other's classes directly (the
* JVM cross-file LSP pairs Java with Kotlin).
* - .NET: C#/F#. PHP: PHP/Blade. Go: Go/templ (templ compiles to Go).
* Lua: Lua/Luau/Teal. BEAM: Erlang/Elixir/Gleam. Shell: Bash/Zsh.
* SQL: SQL/PL-SQL. Salesforce: Apex/SOQL/SOSL. HDL: Verilog/SystemVerilog.
* CFML: tag and script dialects. ObjectScript: UDL/routine/export.
* - Swift additionally calls the C family (C and ObjC headers import into
* Swift as native declarations), and Python calls ObjectScript (IRIS
* Embedded Python, iris.cls(...), #1260) and back.
* Every other language is its own family. An unknown language on either side
* (CBM_LANG_COUNT) is compatible with everything: without evidence, no filter.
* The languages are the DETECTED languages of the calling file and of the
* defining file (not a filename guess: .m is MATLAB by extension but ObjC by
* content), so full, parallel and incremental registries agree. */
enum {
XL_FAM_NONE = 0,
XL_FAM_JS,
XL_FAM_C,
XL_FAM_JVM,
XL_FAM_DOTNET,
XL_FAM_PHP,
XL_FAM_GO,
XL_FAM_LUA,
XL_FAM_BEAM,
XL_FAM_SHELL,
XL_FAM_SQL,
XL_FAM_SALESFORCE,
XL_FAM_HDL,
XL_FAM_CFML,
XL_FAM_OBJECTSCRIPT,
};
static int xl_family(CBMLanguage lang) {
switch (lang) {
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX:
case CBM_LANG_ARKTS:
case CBM_LANG_HTML:
case CBM_LANG_VUE:
case CBM_LANG_SVELTE:
case CBM_LANG_ASTRO:
case CBM_LANG_QML:
return XL_FAM_JS;
case CBM_LANG_C:
case CBM_LANG_CPP:
case CBM_LANG_CUDA:
case CBM_LANG_OBJC:
return XL_FAM_C;
case CBM_LANG_JAVA:
case CBM_LANG_KOTLIN:
case CBM_LANG_SCALA:
case CBM_LANG_GROOVY:
return XL_FAM_JVM;
case CBM_LANG_CSHARP:
case CBM_LANG_FSHARP:
return XL_FAM_DOTNET;
case CBM_LANG_PHP:
case CBM_LANG_BLADE:
return XL_FAM_PHP;
case CBM_LANG_GO:
case CBM_LANG_TEMPL:
return XL_FAM_GO;
case CBM_LANG_LUA:
case CBM_LANG_LUAU:
case CBM_LANG_TEAL:
return XL_FAM_LUA;
case CBM_LANG_ERLANG:
case CBM_LANG_ELIXIR:
case CBM_LANG_GLEAM:
return XL_FAM_BEAM;
case CBM_LANG_BASH:
case CBM_LANG_ZSH:
return XL_FAM_SHELL;
case CBM_LANG_SQL:
case CBM_LANG_PLSQL:
return XL_FAM_SQL;
case CBM_LANG_APEX:
case CBM_LANG_SOQL:
case CBM_LANG_SOSL:
return XL_FAM_SALESFORCE;
case CBM_LANG_VERILOG:
case CBM_LANG_SYSTEMVERILOG:
return XL_FAM_HDL;
case CBM_LANG_CFML:
case CBM_LANG_CFSCRIPT:
return XL_FAM_CFML;
case CBM_LANG_OBJECTSCRIPT_UDL:
case CBM_LANG_OBJECTSCRIPT_ROUTINE:
case CBM_LANG_OBJECTSCRIPT_EXPORT:
return XL_FAM_OBJECTSCRIPT;
default:
return XL_FAM_NONE;
}
}
bool cbm_lang_resolution_compatible(CBMLanguage caller, CBMLanguage target) {
if (caller < 0 || caller >= CBM_LANG_COUNT || target < 0 || target >= CBM_LANG_COUNT) {
return true; /* unknown on either side: no evidence to filter on */
}
if (caller == target) {
return true;
}
int cf = xl_family(caller);
int tf = xl_family(target);
if (cf != XL_FAM_NONE && cf == tf) {
return true;
}
/* Swift imports C and ObjC headers as native declarations. IRIS Embedded
* Python and ObjectScript call each other's classes by name
* (iris.cls("Pkg.A").Run(), #1260). */
return (caller == CBM_LANG_SWIFT && tf == XL_FAM_C) ||
(target == CBM_LANG_SWIFT && cf == XL_FAM_C) ||
(caller == CBM_LANG_PYTHON && tf == XL_FAM_OBJECTSCRIPT) ||
(target == CBM_LANG_PYTHON && cf == XL_FAM_OBJECTSCRIPT);
}
/* Synthetic definition sources minted by a language's own LSP layer
* (py_builtins.c, kotlin_builtins.c). They have no real file to detect, but
* their language is a fact of the injecting LSP. */
CBMLanguage cbm_registry_synthetic_path_language(const char *file_path) {
if (!file_path) {
return CBM_LANG_COUNT;
}
if (strcmp(file_path, "<python-builtins>") == 0) {
return CBM_LANG_PYTHON;
}
if (strcmp(file_path, "<kotlin-builtins>") == 0) {
return CBM_LANG_KOTLIN;
}
return CBM_LANG_COUNT;
}
static const char *path_basename(const char *path) {
if (!path || !path[0]) {
return path;
@@ -918,6 +1070,7 @@ static void free_qn_array(const char *key, void *value, void *ud) {
if (arr) {
/* items borrow the exact map's keys — freed there, not here */
cbm_free(CBM_MEM_CLASS_DYN_ARRAY, arr->is_test);
cbm_free(CBM_MEM_CLASS_DYN_ARRAY, arr->lang);
cbm_da_free(arr);
free(arr);
}
@@ -946,7 +1099,8 @@ void cbm_registry_free(cbm_registry_t *r) {
*
* No array dedup needed: cbm_registry_add's exact-map check guarantees the QN
* is new, and it calls this at most once per distinct key. */
static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn) {
static void index_under_name(cbm_registry_t *r, const char *key, const char *owned_qn,
CBMLanguage lang) {
qn_array_t *arr = cbm_ht_get(r->by_name, key);
if (!arr) {
arr = calloc(CBM_ALLOC_ONE, sizeof(qn_array_t));
@@ -963,16 +1117,31 @@ static void index_under_name(cbm_registry_t *r, const char *key, const char *own
cbm_realloc(CBM_MEM_CLASS_DYN_ARRAY, arr->is_test, (size_t)want * sizeof(uint8_t));
if (grown) {
arr->is_test = grown;
arr->is_test_cap = want;
uint8_t *grown_lang =
cbm_realloc(CBM_MEM_CLASS_DYN_ARRAY, arr->lang, (size_t)want * sizeof(uint8_t));
/* Both side arrays share is_test_cap: on failure it stays below
* count, so this name serves neither cached test verdicts nor
* language filtering, exactly the unfiltered answer. */
if (grown_lang) {
arr->lang = grown_lang;
arr->is_test_cap = want;
}
}
}
if (arr->count <= arr->is_test_cap) {
arr->is_test[arr->count - SKIP_ONE] = is_test_qn(owned_qn) ? 1 : 0;
arr->lang[arr->count - SKIP_ONE] =
(uint8_t)(lang >= 0 && lang < CBM_LANG_COUNT ? lang : CBM_LANG_COUNT);
}
}
void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label) {
cbm_registry_add_lang(r, name, qualified_name, label, CBM_LANG_COUNT);
}
void cbm_registry_add_lang(cbm_registry_t *r, const char *name, const char *qualified_name,
const char *label, CBMLanguage lang) {
if (!r || !qualified_name || !label) {
return;
}
@@ -1011,10 +1180,11 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified
* The derived key is the one every language has always used and it stays
* unconditional, so a grammar whose QNs carry no '#' is byte-identical to
* before this commit. The second key exists for one shape: a fenced tail is
* a literal string no bare callee is ever written as. rust_cfg_qualified_name
* mints a `#[cfg(test)]` twin as "proj.lib.add#cfg(test)", simple_name() has
* no '#' handling, so the derived key filed that function under the whole
* string "add#cfg(test)" where no bare `add` callee could reach it.
* a literal string no bare callee is ever written as. A QN such as
* "proj.lib.add#cfg(test)" (Rust cfg twins were minted that way until they
* became variants of one definition) would otherwise be filed under the
* whole string "add#cfg(test)" — simple_name() has no '#' handling — where
* no bare `add` callee could reach it.
*
* Gating on the fence rather than on the file's language is what makes
* "unchanged for every other grammar" checkable instead of asserted. Every
@@ -1034,9 +1204,9 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified
const char *derived = simple_name(qualified_name);
const char *primary =
name && cbm_qn_callable_base_len_named(owned_qn, name) < strlen(owned_qn) ? name : derived;
index_under_name(r, primary, owned_qn);
/* '#' is a QN fence, and extract_defs.c's rust_cfg_qualified_name is the
* only thing in the tree that mints one today. A grammar that starts
index_under_name(r, primary, owned_qn, lang);
/* '#' is a QN fence; no extractor mints one for a definition today (Rust
* cfg twins are variants of one plain QN). A grammar that starts
* minting a '#' opts into this second key by doing so, whatever it means by
* the fence: its symbols become reachable under the passed name as well,
* and they share that name's bucket with everything else filed under it.
@@ -1044,7 +1214,7 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified
* signature-qualified callable is filed under `name` already, and its
* capped suffix may carry a '#' of its own. */
if (name && name[0] && strchr(derived, '#') && strcmp(name, primary) != 0) {
index_under_name(r, name, owned_qn);
index_under_name(r, name, owned_qn, lang);
}
}
@@ -1505,6 +1675,49 @@ static cbm_resolution_t registry_resolve_chain(const cbm_registry_t *r, const ch
return res;
}
/* The recorded language of the registry entry `qn` (the owned key pointer a
* resolution returns), or CBM_LANG_COUNT when unknown. The by-name bucket is
* at most REG_MAX_CANDIDATES long whenever a name strategy produced `qn`. */
static CBMLanguage registry_entry_language(const cbm_registry_t *r, const char *qn) {
const qn_array_t *arr = cbm_ht_get(r->by_name, simple_name(qn));
if (!arr || !arr->lang || arr->is_test_cap < arr->count) {
return CBM_LANG_COUNT;
}
for (int i = 0; i < arr->count; i++) {
if (arr->items[i] == qn || strcmp(arr->items[i], qn) == 0) {
return (CBMLanguage)arr->lang[i];
}
}
return CBM_LANG_COUNT;
}
/* The cross-language veto (see cbm_lang_resolution_compatible). A name-only
* answer carries no import or module evidence, so it only stands when the
* caller's language can call the target: a JS `app.get` must not bind
* Python's builtins.dict.get because it is the one "get" in the project.
* Callers turn a vetoed answer into an EMPTY resolution BEFORE their empty-
* resolution fallbacks, so route registration / HTTP classification still
* run. A veto, not a re-pick among the remaining candidates: re-picking
* promotes the next same-language short-name guess (measured on n8n: +3.1k
* CALLS, 88% of them jest `describe()` bound to a project method).
* field_type_hint is included: it re-picks by type-name substring across ALL
* candidates, language-blind. */
bool cbm_registry_name_guess_vetoed(const cbm_registry_t *r, CBMLanguage caller_lang,
const cbm_resolution_t *res) {
if (!r || !res || caller_lang < 0 || caller_lang >= CBM_LANG_COUNT || !res->qualified_name ||
!res->qualified_name[0] || !res->strategy) {
return false;
}
if (strcmp(res->strategy, "unique_name") != 0 && strcmp(res->strategy, "suffix_match") != 0 &&
strcmp(res->strategy, "qualified_suffix") != 0 &&
strcmp(res->strategy, "field_type_hint") != 0 &&
strcmp(res->strategy, "same_module") != 0) {
return false;
}
return !cbm_lang_resolution_compatible(caller_lang,
registry_entry_language(r, res->qualified_name));
}
cbm_resolution_t cbm_registry_resolve(const cbm_registry_t *r, const char *callee_name,
const char *module_qn, const char **import_map_keys,
const char **import_map_vals, int import_map_count) {
+117 -34
View File
@@ -4721,6 +4721,42 @@ static char *store_like_dot_suffix(const char *text, const char *tail) {
return pat;
}
int cbm_store_find_variant_spans_by_file(cbm_store_t *s, const char *project, const char *file_path,
cbm_node_t **out, int *count) {
*out = NULL;
*count = 0;
if (!s || !s->db || !project || !file_path) {
return CBM_STORE_ERR;
}
/* The file's own nodes with variants (idx_nodes_file), and the nodes of
* other files it writes a variant of (its File/Module DEFINES edges);
* then each one's variant spans that lie in this file. */
const char *sql =
"SELECT n.id, n.project, n.label, n.name, n.qualified_name, n.file_path,"
" CAST(json_extract(v.value, '$.start_line') AS INTEGER),"
" CAST(json_extract(v.value, '$.end_line') AS INTEGER), n.properties"
" FROM nodes n, json_each(n.properties, '$.variants') v"
" WHERE n.id IN ("
" SELECT id FROM nodes WHERE project = ?1 AND file_path = ?2"
" AND json_array_length(properties, '$.variants') > 1"
" UNION SELECT tgt.id FROM nodes src"
" CROSS JOIN edges e ON e.source_id = src.id"
" CROSS JOIN nodes tgt ON tgt.id = e.target_id"
" WHERE src.project = ?1 AND src.file_path = ?2 AND src.label IN ('File','Module')"
" AND e.type = 'DEFINES' AND tgt.file_path <> ?2"
" AND json_array_length(tgt.properties, '$.variants') > 1)"
" AND json_extract(v.value, '$.file_path') = ?2"
" ORDER BY n.id, 7, 8";
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, "variant spans prepare");
return CBM_STORE_ERR;
}
bind_text(stmt, SKIP_ONE, project);
bind_text(stmt, ST_COL_2, file_path);
return store_collect_nodes(s, stmt, NULL, NULL, out, count);
}
int cbm_store_find_nodes_by_qn_suffix(cbm_store_t *s, const char *project, const char *suffix,
cbm_node_t **out, int *count) {
*out = NULL;
@@ -5349,43 +5385,40 @@ void cbm_store_free_lsp_surfaces(cbm_lsp_surface_row_t *rows, int count) {
free(rows);
}
/* One chunk of the dependent-file query. Binding an IN list needs one
* placeholder per element, so the SQL is assembled per chunk; the chunk cap
/* One chunk of a file-path query over a list of files. Binding an IN list
* needs one placeholder per element, so the SQL is assembled per chunk from
* `parts`, with the placeholder list ?2..?n+1 between consecutive parts (a
* query may name the list more than once); ?1 is the project. The chunk cap
* keeps it bounded. Dedup across chunks happens in the caller's hash set. */
enum { DEPFILE_CHUNK = 200 };
static int dependent_files_chunk(cbm_store_t *s, const char *project,
const char *const *target_files, int chunk_count,
CBMHashTable *seen, char ***out, int *out_count, int *out_cap) {
char sql[CBM_SZ_4K];
int pos = snprintf(sql, sizeof(sql),
/* CROSS JOIN pins nodes-first (see the delta snapshot query:
* the free planner walks every project edge instead). */
"SELECT DISTINCT src.file_path FROM nodes tgt"
" CROSS JOIN edges e ON e.target_id = tgt.id"
" CROSS JOIN nodes src ON e.source_id = src.id"
" WHERE tgt.project = ?1 AND tgt.file_path IN (");
for (int i = 0; i < chunk_count; i++) {
pos += snprintf(sql + pos, sizeof(sql) - (size_t)pos, "%s?%d", i ? "," : "", i + ST_COL_2);
static int file_query_chunk(cbm_store_t *s, const char *const *parts, int nparts, const char *what,
const char *project, const char *const *target_files, int chunk_count,
CBMHashTable *seen, char ***out, int *out_count, int *out_cap) {
char sql[CBM_SZ_8K];
sql[0] = '\0';
if (nparts < 1) {
return CBM_STORE_ERR;
}
int pos = 0;
for (int part = 0; part < nparts; part++) {
if (part > 0) {
for (int i = 0; i < chunk_count; i++) {
pos += snprintf(sql + pos, sizeof(sql) - (size_t)pos, "%s?%d", i ? "," : "",
i + ST_COL_2);
if ((size_t)pos >= sizeof(sql)) {
return CBM_STORE_ERR;
}
}
}
pos += snprintf(sql + pos, sizeof(sql) - (size_t)pos, "%s", parts[part]);
if ((size_t)pos >= sizeof(sql)) {
return CBM_STORE_ERR;
}
}
/* Structural containment is not resolution: a Folder/Project container
* (whose file_path is a properties placeholder, not a real file) or a
* CONTAINS_* edge says nothing about who consumed the target's
* definitions, and the closure planner would otherwise decline on a
* "dependent" no discovery can ever produce. */
pos += snprintf(sql + pos, sizeof(sql) - (size_t)pos,
") AND src.file_path <> '' AND src.file_path IS NOT NULL"
" AND src.label NOT IN ('Folder','Project')"
" AND e.type NOT IN ('CONTAINS_FILE','CONTAINS_FOLDER')");
if ((size_t)pos >= sizeof(sql)) {
return CBM_STORE_ERR;
}
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, "dependent_files prepare");
store_set_error_sqlite(s, what);
return CBM_STORE_ERR;
}
bind_text(stmt, ST_COL_1, project);
@@ -5420,16 +5453,18 @@ static int dependent_files_chunk(cbm_store_t *s, const char *project,
return step_rc == SQLITE_DONE ? CBM_STORE_OK : CBM_STORE_ERR;
}
int cbm_store_get_dependent_files(cbm_store_t *s, const char *project,
const char *const *target_files, int target_count, char ***out,
int *out_count) {
/* Run `parts` over every chunk of target_files; the targets themselves are
* excluded from the result. */
static int file_query(cbm_store_t *s, const char *const *parts, int nparts, const char *what,
const char *project, const char *const *target_files, int target_count,
char ***out, int *out_count) {
*out = NULL;
*out_count = 0;
if (!s || !project || target_count <= 0) {
return target_count == 0 ? CBM_STORE_OK : CBM_STORE_ERR;
}
/* Exclude the targets themselves up front: an edge between two files of
* the closure adds nothing, and the caller wants "who ELSE consumed". */
* the closure adds nothing, and the caller wants "who ELSE". */
CBMHashTable *seen = cbm_ht_create((size_t)target_count * PAIR_LEN);
if (!seen) {
return CBM_STORE_ERR;
@@ -5446,8 +5481,8 @@ int cbm_store_get_dependent_files(cbm_store_t *s, const char *project,
if (chunk > DEPFILE_CHUNK) {
chunk = DEPFILE_CHUNK;
}
rc = dependent_files_chunk(s, project, target_files + off, chunk, seen, &files, &count,
&cap);
rc = file_query_chunk(s, parts, nparts, what, project, target_files + off, chunk, seen,
&files, &count, &cap);
}
cbm_ht_free(seen);
if (rc != CBM_STORE_OK) {
@@ -5459,6 +5494,54 @@ int cbm_store_get_dependent_files(cbm_store_t *s, const char *project,
return CBM_STORE_OK;
}
int cbm_store_get_dependent_files(cbm_store_t *s, const char *project,
const char *const *target_files, int target_count, char ***out,
int *out_count) {
static const char *const parts[] = {
/* CROSS JOIN pins nodes-first (see the delta snapshot query: the
* free planner walks every project edge instead). */
"SELECT DISTINCT src.file_path FROM nodes tgt"
" CROSS JOIN edges e ON e.target_id = tgt.id"
" CROSS JOIN nodes src ON e.source_id = src.id"
" WHERE tgt.project = ?1 AND tgt.file_path IN (",
/* Structural containment is not resolution: a Folder/Project
* container (whose file_path is a properties placeholder, not a real
* file) or a CONTAINS_* edge says nothing about who consumed the
* target's definitions, and the closure planner would otherwise
* decline on a "dependent" no discovery can ever produce. */
") AND src.file_path <> '' AND src.file_path IS NOT NULL"
" AND src.label NOT IN ('Folder','Project')"
" AND e.type NOT IN ('CONTAINS_FILE','CONTAINS_FOLDER')"};
return file_query(s, parts, 2, "dependent_files prepare", project, target_files, target_count,
out, out_count);
}
int cbm_store_get_variant_partner_files(cbm_store_t *s, const char *project,
const char *const *files, int count, char ***out,
int *out_count) {
static const char *const parts[] = {
/* A file that writes a variant of a node whose file_path is
* another file. */
"SELECT tgt.file_path FROM nodes src"
" CROSS JOIN edges e ON e.source_id = src.id"
" CROSS JOIN nodes tgt ON e.target_id = tgt.id"
" WHERE src.project = ?1 AND src.label IN ('File','Module') AND e.type = 'DEFINES'"
" AND tgt.file_path <> src.file_path AND tgt.file_path <> ''"
" AND json_array_length(tgt.properties, '$.variants') > 1"
" AND src.file_path IN (",
/* The other files that write a variant of a node in the file. */
") UNION SELECT src.file_path FROM nodes tgt"
" CROSS JOIN edges e ON e.target_id = tgt.id"
" CROSS JOIN nodes src ON e.source_id = src.id"
" WHERE tgt.project = ?1 AND e.type = 'DEFINES' AND src.label IN ('File','Module')"
" AND src.file_path <> tgt.file_path AND src.file_path <> ''"
" AND json_array_length(tgt.properties, '$.variants') > 1"
" AND tgt.file_path IN (",
")"};
return file_query(s, parts, 3, "variant_partner_files prepare", project, files, count, out,
out_count);
}
void cbm_store_free_dependent_files(char **files, int count) {
if (!files) {
return;
+18
View File
@@ -99,6 +99,14 @@ typedef struct {
const char *config_ctx; /* governing-config context hash ("" = none) */
} cbm_lsp_surface_row_t;
/* The definition variants that lie in a file (graph_buffer.c "Definition
* variants"): one row per variant span in `file_path`, as the node it
* belongs to with start_line/end_line set to that span. Covers the file's
* own nodes (their other #if branches) and nodes whose file_path names
* another file that the file writes a variant of. */
int cbm_store_find_variant_spans_by_file(cbm_store_t *s, const char *project, const char *file_path,
cbm_node_t **out, int *count);
/* Find nodes overlapping a line range in a file (excludes Module/Package). */
int cbm_store_find_nodes_by_file_overlap(cbm_store_t *s, const char *project, const char *file_path,
int start_line, int end_line, cbm_node_t **out,
@@ -170,6 +178,16 @@ int cbm_store_get_dependent_files(cbm_store_t *s, const char *project,
int *out_count);
void cbm_store_free_dependent_files(char **files, int count);
/* Variant partners for closure-repair routing: the files that write another
* variant of a definition written in any of `files` (one node for a
* definition's platform files, header declaration and source definition, Go
* build-tagged files; a DEFINES edge from each file's File node). Excludes
* `files` themselves; served by idx_nodes_file + the edge source/target
* indexes. Free with cbm_store_free_dependent_files. */
int cbm_store_get_variant_partner_files(cbm_store_t *s, const char *project,
const char *const *files, int count, char ***out,
int *out_count);
/* Find edges whose properties contain a url_path matching the keyword. */
int cbm_store_find_edges_by_url_path(cbm_store_t *s, const char *project, const char *keyword,
cbm_edge_t **out, int *count);
+635
View File
@@ -0,0 +1,635 @@
/*
* test_conditional_variants.c — one definition with mutually exclusive
* variants is ONE identity in the graph, in every language.
*
* A variant is a second definition of the same name that only one build or
* run ever sees: C-family #if/#else, C#/F# #if, Rust #[cfg], Swift #if,
* Go build-tagged files, D version blocks, Erlang -ifdef, Haskell CPP,
* Fortran .F90 preprocessing, Verilog `ifdef, Elixir/Python/Ruby/PHP
* definitions under an `if`, and C platform files (foo_win.c / foo_posix.c).
*
* Every fixture defines `pick` twice — variant A calls `alpha`, variant B
* calls `beta` — plus a `caller` that calls `pick`. The graph must hold:
* - exactly one node for `pick` (no per-variant twins, no condition text
* in its qualified name);
* - CALLS from that node to alpha AND beta (every variant's body counts);
* - a CALLS edge from caller to it;
* - its `variants` property listing every variant's span, so a change in
* any variant maps to the definition.
* Fixtures avoid each language's unrelated extraction gaps (Ruby and Pascal
* calls carry parentheses; Haskell calls are applications).
*/
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include <foundation/compat_fs.h>
#include <mcp/mcp.h>
#include <pipeline/pipeline.h>
#include <pipeline/pipeline_internal.h>
#include <store/store.h>
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
typedef struct {
const char *name;
const char *content;
} cv_file_t;
typedef struct {
char tmpdir[256];
char dbpath[1024];
char *project;
cbm_mcp_server_t *srv;
cbm_store_t *store;
} cv_proj_t;
static bool cv_write(const char *root, const cv_file_t *file) {
char path[700];
snprintf(path, sizeof(path), "%s/%s", root, file->name);
char *slash = strrchr(path, '/');
if (slash && slash > path + strlen(root)) {
*slash = '\0';
cbm_mkdir_p(path, 0755);
*slash = '/';
}
FILE *f = cbm_fopen(path, "wb");
if (!f) {
return false;
}
bool ok = fputs(file->content, f) >= 0;
return fclose(f) == 0 && ok;
}
static bool cv_index(cv_proj_t *p, const cv_file_t *files, int count) {
memset(p, 0, sizeof(*p));
snprintf(p->tmpdir, sizeof(p->tmpdir), "%s", th_mktempdir("cbm-cv"));
if (!p->tmpdir[0]) {
return false;
}
for (int i = 0; i < count; i++) {
if (!cv_write(p->tmpdir, &files[i])) {
return false;
}
}
p->project = cbm_project_name_from_path(p->tmpdir);
if (!p->project) {
return false;
}
/* Where the MCP server writes the project DB: CBM_CACHE_DIR, else the
* per-user cache (same resolution as test_lang_contract.c). */
char cache[700];
const char *configured = getenv("CBM_CACHE_DIR");
const char *home = getenv("HOME");
if (configured && configured[0]) {
snprintf(cache, sizeof(cache), "%s", configured);
} else {
snprintf(cache, sizeof(cache), "%s/.cache/codebase-memory-mcp", home ? home : "/tmp");
}
cbm_mkdir_p(cache, 0755);
snprintf(p->dbpath, sizeof(p->dbpath), "%s/%s.db", cache, p->project);
unlink(p->dbpath);
p->srv = cbm_mcp_server_new(NULL);
if (!p->srv) {
return false;
}
char args[800];
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", p->tmpdir);
free(cbm_mcp_handle_tool(p->srv, "index_repository", args));
p->store = cbm_store_open_path(p->dbpath);
return p->store != NULL;
}
static void cv_cleanup(cv_proj_t *p) {
if (p->store) {
cbm_store_close(p->store);
}
if (p->srv) {
cbm_mcp_server_free(p->srv);
}
if (p->dbpath[0]) {
char side[1100];
unlink(p->dbpath);
snprintf(side, sizeof(side), "%s-wal", p->dbpath);
unlink(side);
snprintf(side, sizeof(side), "%s-shm", p->dbpath);
unlink(side);
}
free(p->project);
if (p->tmpdir[0]) {
th_rmtree(p->tmpdir);
}
memset(p, 0, sizeof(*p));
}
/* One integer from SQL with text parameters; -1 on any query failure. */
static int cv_count(cv_proj_t *p, const char *sql, const char *a, const char *b) {
sqlite3 *db = cbm_store_get_db(p->store);
sqlite3_stmt *stmt = NULL;
if (!db || sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
sqlite3_finalize(stmt);
return -1;
}
int n = sqlite3_bind_parameter_count(stmt);
const char *values[] = {p->project, a, b};
for (int i = 0; i < n && i < 3; i++) {
sqlite3_bind_text(stmt, i + 1, values[i], -1, SQLITE_TRANSIENT);
}
int count = sqlite3_step(stmt) == SQLITE_ROW ? sqlite3_column_int(stmt, 0) : -1;
sqlite3_finalize(stmt);
return count;
}
#define CV_DEF_LABELS "('Function','Method','Module','Class')"
typedef struct {
int nodes; /* distinct `pick` definitions */
int clean_names; /* of those, qualified names without condition text */
int to_alpha; /* CALLS pick -> alpha */
int to_beta; /* CALLS pick -> beta */
int from_caller; /* CALLS caller -> pick */
int variant_spans; /* entries of pick's `variants` property */
} cv_result_t;
static cv_result_t cv_measure(cv_proj_t *p, const char *pick, const char *alpha, const char *beta,
const char *caller) {
cv_result_t r;
r.nodes = cv_count(p,
"SELECT COUNT(DISTINCT qualified_name) FROM nodes WHERE project=?1 "
"AND name=?2 AND label IN " CV_DEF_LABELS,
pick, NULL);
r.clean_names = cv_count(
p,
"SELECT COUNT(*) FROM nodes WHERE project=?1 AND name=?2 AND label IN " CV_DEF_LABELS
" AND qualified_name NOT GLOB '*[#[]()]*'",
pick, NULL);
const char *calls = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id "
"JOIN nodes t ON t.id=e.target_id WHERE e.project=?1 AND e.type='CALLS' "
"AND s.name=?2 AND t.name=?3";
r.to_alpha = cv_count(p, calls, pick, alpha);
r.to_beta = cv_count(p, calls, pick, beta);
r.from_caller = cv_count(p, calls, caller, pick);
r.variant_spans =
cv_count(p,
"SELECT COALESCE(MAX(json_array_length(properties,'$.variants')),0) "
"FROM nodes WHERE project=?1 AND name=?2 AND label IN " CV_DEF_LABELS,
pick, NULL);
return r;
}
static int cv_expect(const char *lang, cv_result_t r) {
bool ok = r.nodes == 1 && r.clean_names == 1 && r.to_alpha >= 1 && r.to_beta >= 1 &&
r.from_caller >= 1 && r.variant_spans >= 2;
if (!ok) {
fprintf(
stderr, " [%s] nodes=%d clean=%d ->alpha=%d ->beta=%d caller->=%d variant_spans=%d\n",
lang, r.nodes, r.clean_names, r.to_alpha, r.to_beta, r.from_caller, r.variant_spans);
}
return ok;
}
static int cv_run(const char *lang, const cv_file_t *files, int count, const char *pick,
const char *alpha, const char *beta, const char *caller) {
cv_proj_t p;
bool indexed = cv_index(&p, files, count);
int ok = indexed && cv_expect(lang, cv_measure(&p, pick, alpha, beta, caller));
if (!indexed) {
fprintf(stderr, " [%s] fixture could not be indexed\n", lang);
}
cv_cleanup(&p);
return ok;
}
#define CV_ONE(lang, path, src) \
do { \
static const cv_file_t files_[] = {{path, src}}; \
ASSERT_TRUE(cv_run(lang, files_, 1, "pick", "alpha", "beta", "caller")); \
} while (0)
TEST(cv_c_preprocessor) {
CV_ONE("c", "pick.c",
"void alpha(void) {}\nvoid beta(void) {}\n#ifdef _WIN32\nvoid pick(void) { alpha(); }\n"
"#else\nvoid pick(void) { beta(); }\n#endif\nvoid caller(void) { pick(); }\n");
PASS();
}
TEST(cv_cpp_elif) {
CV_ONE(
"cpp", "pick.cpp",
"void alpha() {}\nvoid beta() {}\n#if defined(_WIN32)\nvoid pick() { alpha(); }\n"
"#elif defined(__linux__)\nvoid pick() { beta(); }\n#endif\nvoid caller() { pick(); }\n");
PASS();
}
TEST(cv_cpp_class_members) {
CV_ONE("cpp_member", "pick.cpp",
"struct P {\n void alpha() {}\n void beta() {}\n#ifdef _WIN32\n void pick() { "
"alpha(); }\n"
"#else\n void pick() { beta(); }\n#endif\n void caller() { pick(); }\n};\n");
PASS();
}
TEST(cv_objc_methods) {
CV_ONE("objc", "P.m",
"@implementation P\n- (void)alpha {}\n- (void)beta {}\n#if TARGET_OS_IPHONE\n"
"- (void)pick { alpha(); }\n#else\n- (void)pick { beta(); }\n#endif\n"
"- (void)caller { pick(); }\n@end\nvoid alpha(void) {}\nvoid beta(void) {}\n"
"void pick(void);\n");
PASS();
}
TEST(cv_cuda) {
CV_ONE(
"cuda", "pick.cu",
"__device__ void alpha() {}\n__device__ void beta() {}\n#ifdef USE_FAST\n"
"__device__ void pick() { alpha(); }\n#else\n__device__ void pick() { beta(); }\n#endif\n"
"__device__ void caller() { pick(); }\n");
PASS();
}
TEST(cv_csharp_class_members) {
static const cv_file_t files[] = {
{"Picker.cs", "class Picker {\n static void Alpha() {}\n static void Beta() {}\n"
"#if WINDOWS\n static void Pick() { Alpha(); }\n#else\n"
" static void Pick() { Beta(); }\n#endif\n"
" static void Caller() { Pick(); }\n}\n"}};
ASSERT_TRUE(cv_run("csharp", files, 1, "Pick", "Alpha", "Beta", "Caller"));
PASS();
}
TEST(cv_fsharp) {
CV_ONE("fsharp", "Pick.fs",
"module Pick\nlet alpha () = ()\nlet beta () = ()\n#if WINDOWS\nlet pick () = alpha ()\n"
"#else\nlet pick () = beta ()\n#endif\nlet caller () = pick ()\n");
PASS();
}
TEST(cv_rust_cfg_free_functions) {
static const cv_file_t files[] = {
{"Cargo.toml", "[package]\nname = \"cv\"\nversion = \"0.1.0\"\n"},
{"src/lib.rs", "fn alpha() {}\nfn beta() {}\n#[cfg(windows)]\nfn pick() { alpha(); }\n"
"#[cfg(not(windows))]\nfn pick() { beta(); }\nfn caller() { pick(); }\n"}};
ASSERT_TRUE(cv_run("rust", files, 2, "pick", "alpha", "beta", "caller"));
PASS();
}
TEST(cv_rust_cfg_methods) {
static const cv_file_t files[] = {
{"Cargo.toml", "[package]\nname = \"cv\"\nversion = \"0.1.0\"\n"},
{"src/lib.rs",
"struct P;\nfn alpha() {}\nfn beta() {}\nimpl P {\n #[cfg(target_os = \"linux\")]\n"
" fn pick(&self) { alpha(); }\n #[cfg(not(target_os = \"linux\"))]\n"
" fn pick(&self) { beta(); }\n fn caller(&self) { self.pick(); }\n}\n"}};
ASSERT_TRUE(cv_run("rust_impl", files, 2, "pick", "alpha", "beta", "caller"));
PASS();
}
TEST(cv_go_build_tag_files) {
static const cv_file_t files[] = {
{"go.mod", "module example.com/cv\n\ngo 1.22\n"},
{"pick/pick_windows.go", "//go:build windows\npackage pick\n\nfunc Pick() { alpha() }\n"},
{"pick/pick_posix.go", "//go:build !windows\npackage pick\n\nfunc Pick() { beta() }\n"},
{"pick/common.go",
"package pick\n\nfunc alpha() {}\nfunc beta() {}\nfunc Caller() { Pick() }\n"}};
ASSERT_TRUE(cv_run("go", files, 4, "Pick", "alpha", "beta", "Caller"));
PASS();
}
TEST(cv_swift) {
CV_ONE("swift", "pick.swift",
"func alpha() {}\nfunc beta() {}\n#if os(Windows)\nfunc pick() { alpha() }\n#else\n"
"func pick() { beta() }\n#endif\nfunc caller() { pick() }\n");
PASS();
}
TEST(cv_python_if) {
CV_ONE("python", "pick.py",
"import sys\ndef alpha(): pass\ndef beta(): pass\nif sys.platform == \"win32\":\n"
" def pick():\n alpha()\nelse:\n def pick():\n beta()\n"
"def caller():\n pick()\n");
PASS();
}
TEST(cv_ruby_if) {
CV_ONE(
"ruby", "pick.rb",
"def alpha(); end\ndef beta(); end\nif RUBY_PLATFORM =~ /mswin/\n def pick\n alpha()\n"
" end\nelse\n def pick\n beta()\n end\nend\ndef caller\n pick()\nend\n");
PASS();
}
TEST(cv_php_if) {
CV_ONE("php", "pick.php",
"<?php\nfunction alpha() {}\nfunction beta() {}\nif (PHP_OS_FAMILY === 'Windows') {\n"
" function pick() { alpha(); }\n} else {\n function pick() { beta(); }\n}\n"
"function caller() { pick(); }\n");
PASS();
}
TEST(cv_d_version) {
CV_ONE("d", "pick.d",
"void alpha() {}\nvoid beta() {}\nversion (Windows) {\n void pick() { alpha(); }\n"
"} else {\n void pick() { beta(); }\n}\nvoid caller() { pick(); }\n");
PASS();
}
TEST(cv_erlang_ifdef) {
CV_ONE("erlang", "pick.erl",
"-module(pick).\n-export([caller/0]).\nalpha() -> ok.\nbeta() -> ok.\n-ifdef(WINDOWS).\n"
"pick() -> alpha().\n-else.\npick() -> beta().\n-endif.\ncaller() -> pick().\n");
PASS();
}
TEST(cv_haskell_cpp) {
CV_ONE("haskell", "Pick.hs",
"{-# LANGUAGE CPP #-}\nmodule Pick where\nalpha :: Int -> Int\nalpha x = x\n"
"beta :: Int -> Int\nbeta x = x\n#if defined(mingw32_HOST_OS)\npick :: Int -> Int\n"
"pick x = alpha x\n#else\npick :: Int -> Int\npick x = beta x\n#endif\n"
"caller :: Int -> Int\ncaller x = pick x\n");
PASS();
}
/* The first branch calls neither target: alpha is reached only through the
* #elif branch, beta only through the nested group's # else inside #else. */
TEST(cv_haskell_cpp_elif_nested) {
CV_ONE("haskell", "Pick.hs",
"{-# LANGUAGE CPP #-}\nmodule Pick where\nalpha :: Int -> Int\nalpha x = x\n"
"beta :: Int -> Int\nbeta x = x\n#if defined(mingw32_HOST_OS)\npick :: Int -> Int\n"
"pick x = x\n#elif defined(darwin_HOST_OS)\npick :: Int -> Int\npick x = alpha x\n"
"#else\n# if WORD_SIZE_IN_BITS == 64\npick :: Int -> Int\npick x = x\n# else\n"
"pick :: Int -> Int\npick x = beta x\n# endif\n#endif\n"
"caller :: Int -> Int\ncaller x = pick x\n");
PASS();
}
TEST(cv_fortran_preprocessed) {
CV_ONE("fortran", "pick.F90",
"module pickmod\ncontains\n subroutine alpha()\n end subroutine alpha\n"
" subroutine beta()\n end subroutine beta\n#ifdef WINDOWS\n subroutine pick()\n"
" call alpha()\n end subroutine pick\n#else\n subroutine pick()\n call beta()\n"
" end subroutine pick\n#endif\n subroutine caller()\n call pick()\n"
" end subroutine caller\nend module pickmod\n");
PASS();
}
TEST(cv_pascal_ifdef) {
CV_ONE("pascal", "pick.pas",
"unit pick;\ninterface\nprocedure caller;\nimplementation\nprocedure alpha; begin end;\n"
"procedure beta; begin end;\n{$IFDEF WINDOWS}\nprocedure pick; begin alpha(); end;\n"
"{$ELSE}\nprocedure pick; begin beta(); end;\n{$ENDIF}\n"
"procedure caller; begin pick(); end;\nend.\n");
PASS();
}
TEST(cv_julia_static_if) {
CV_ONE("julia", "pick.jl",
"alpha() = nothing\nbeta() = nothing\n@static if Sys.iswindows()\n pick() = alpha()\n"
"else\n pick() = beta()\nend\ncaller() = pick()\n");
PASS();
}
TEST(cv_elixir_compile_time_if) {
CV_ONE("elixir", "pick.ex",
"defmodule Pick do\n def alpha, do: :ok\n def beta, do: :ok\n"
" if Code.ensure_loaded?(:win32) do\n def pick, do: alpha()\n else\n"
" def pick, do: beta()\n end\n def caller, do: pick()\nend\n");
PASS();
}
/* Verilog: the identity only. Module instantiation produces no CALLS edge in
* this graph even without any `ifdef (a general Verilog gap, not a variant
* one), so the body and caller edges are not this suite's to assert. */
TEST(cv_verilog_ifdef) {
static const cv_file_t files[] = {
{"pick.sv", "`ifdef FAST\nmodule pick(input a); alpha u(); endmodule\n`else\n"
"module pick(input a); beta u(); endmodule\n`endif\nmodule alpha(); endmodule\n"
"module beta(); endmodule\nmodule caller(); pick p(.a(1'b0)); endmodule\n"}};
cv_proj_t p;
ASSERT_TRUE(cv_index(&p, files, 1));
cv_result_t r = cv_measure(&p, "pick", "alpha", "beta", "caller");
cv_cleanup(&p);
ASSERT_EQ(r.nodes, 1);
ASSERT_EQ(r.clean_names, 1);
ASSERT_GTE(r.variant_spans, 2);
PASS();
}
/* C platform files: the same function in foo_win.c and foo_posix.c is one
* definition with two variants, and a caller in a shared file binds to it. */
TEST(cv_c_platform_files) {
static const cv_file_t files[] = {
{"src/plat_win.c", "void alpha(void);\nvoid pick(void) { alpha(); }\n"},
{"src/plat_posix.c", "void beta(void);\nvoid pick(void) { beta(); }\n"},
{"src/plat.c", "void alpha(void) {}\nvoid beta(void) {}\nvoid pick(void);\n"
"void caller(void) { pick(); }\n"}};
ASSERT_TRUE(cv_run("c_platform_files", files, 3, "pick", "alpha", "beta", "caller"));
PASS();
}
/* Platform directories (libuv's src/unix/ and src/win/) are platform variants
* the same way platform suffixes are. */
TEST(cv_c_platform_directories) {
static const cv_file_t files[] = {
{"src/unix/fs.c", "void alpha(void);\nvoid pick(void) { alpha(); }\n"},
{"src/win/fs.c", "void beta(void);\nvoid pick(void) { beta(); }\n"},
{"src/core.c", "void alpha(void) {}\nvoid beta(void) {}\nvoid pick(void);\n"
"void caller(void) { pick(); }\n"}};
ASSERT_TRUE(cv_run("c_platform_dirs", files, 3, "pick", "alpha", "beta", "caller"));
PASS();
}
/* Not variants: static functions are file-local even in platform files, and
* same-named external functions in files without a platform token may belong
* to different programs. Each keeps its own node. */
TEST(cv_c_non_variants_keep_their_nodes) {
static const cv_file_t files[] = {{"src/plat_win.c", "static void pick(void) {}\n"},
{"src/plat_posix.c", "static void pick(void) {}\n"},
{"tools/one.c", "void helper(void) {}\n"},
{"tools/two.c", "void helper(void) {}\n"}};
cv_proj_t p;
ASSERT_TRUE(cv_index(&p, files, 4));
int picks = cv_count(&p,
"SELECT COUNT(DISTINCT qualified_name) FROM nodes WHERE project=?1 "
"AND name='pick' AND label='Function'",
NULL, NULL);
int helpers = cv_count(&p,
"SELECT COUNT(DISTINCT qualified_name) FROM nodes WHERE project=?1 "
"AND name='helper' AND label='Function'",
NULL, NULL);
cv_cleanup(&p);
ASSERT_EQ(picks, 2);
ASSERT_EQ(helpers, 2);
PASS();
}
/* A call that names a function no definition of its own language provides
* must not bind to a same-named definition in another language. */
TEST(cv_no_cross_language_name_binding) {
static const cv_file_t files[] = {
{"go.mod", "module example.com/cv\n\ngo 1.22\n"},
{"pick/pick.go", "package pick\n\nfunc Pick() {}\n"},
{"cs/Caller.cs", "class C {\n static void Caller() { Pick(); }\n}\n"}};
cv_proj_t p;
ASSERT_TRUE(cv_index(&p, files, 3));
/* Positive controls: both definitions are in the graph, so a missing
* edge below is the rule at work, not an empty index. */
int go_pick = cv_count(&p,
"SELECT COUNT(*) FROM nodes WHERE project=?1 AND name='Pick' "
"AND file_path LIKE '%.go'",
NULL, NULL);
int cs_caller = cv_count(&p,
"SELECT COUNT(*) FROM nodes WHERE project=?1 AND name='Caller' "
"AND file_path LIKE '%.cs'",
NULL, NULL);
int crossing =
cv_count(&p,
"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id "
"JOIN nodes t ON t.id=e.target_id WHERE e.project=?1 AND e.type='CALLS' "
"AND s.name='Caller' AND t.file_path LIKE '%.go'",
NULL, NULL);
cv_cleanup(&p);
ASSERT_EQ(go_pick, 1);
ASSERT_EQ(cs_caller, 1);
ASSERT_EQ(crossing, 0);
PASS();
}
/* ── Incremental: a definition written in several files ───────────────
* Closure repair re-parses only the changed files and their recorded
* dependents. `pick` written in two platform files is ONE node; editing
* either file must re-parse the other too, or the repair drops the other
* variant's span and calls (when the edited file holds the node) or keeps
* the edited file's stale ones. After each edit the repaired graph must be
* the graph a full index of the same tree builds. */
static bool cv_pipeline(const char *repo, const char *db, cbm_incremental_route_t *route) {
cbm_pipeline_t *p = cbm_pipeline_new(repo, db, CBM_MODE_FULL);
if (!p) {
return false;
}
bool ok = cbm_pipeline_set_project_name(p, "cvinc");
cbm_pipeline_set_persistence(p, false);
ok = ok && cbm_pipeline_run(p) == 0;
if (route) {
*route = cbm_pipeline_incremental_test_last_route();
}
cbm_pipeline_free(p);
return ok;
}
/* CALLS edges by name plus pick's variant spans, one line each. */
static bool cv_signature(const char *db_path, char *out, size_t n) {
sqlite3 *db = NULL;
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
sqlite3_close(db);
return false;
}
const char *sql =
"SELECT line FROM (SELECT s.name || '>' || t.name AS line FROM edges e "
"JOIN nodes s ON s.id=e.source_id JOIN nodes t ON t.id=e.target_id WHERE e.type='CALLS' "
"UNION ALL SELECT 'variants ' || json_extract(properties,'$.variants') FROM nodes "
"WHERE name='pick' AND label='Function') ORDER BY line";
sqlite3_stmt *stmt = NULL;
bool ok = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK;
out[0] = '\0';
while (ok && sqlite3_step(stmt) == SQLITE_ROW) {
const char *line = (const char *)sqlite3_column_text(stmt, 0);
size_t used = strlen(out);
snprintf(out + used, n - used, "%s\n", line ? line : "?");
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return ok;
}
/* Rewrite `name` in `repo`, repair the incremental DB, index the same tree
* from scratch, and compare. */
static bool cv_edit_matches_full(const char *repo, const char *dbdir, const char *name,
const char *content, char *sig, size_t n) {
char inc_db[600];
char full_db[600];
snprintf(inc_db, sizeof(inc_db), "%s/inc.db", dbdir);
snprintf(full_db, sizeof(full_db), "%s/full.db", dbdir);
static char full_sig[8192];
const cv_file_t file = {name, content};
cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE;
bool ok = cv_write(repo, &file) && cv_pipeline(repo, inc_db, &route);
if (route != CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR) {
fprintf(stderr, " [incremental %s] route %d, not closure repair\n", name, (int)route);
ok = false;
}
unlink(full_db);
ok = ok && cv_pipeline(repo, full_db, NULL) && cv_signature(inc_db, sig, n) &&
cv_signature(full_db, full_sig, sizeof(full_sig));
if (ok && strcmp(sig, full_sig) != 0) {
fprintf(stderr, " [incremental %s] repaired:\n%s full:\n%s", name, sig, full_sig);
ok = false;
}
return ok;
}
TEST(cv_incremental_variant_partners) {
char repo[300];
char dbdir[300];
snprintf(repo, sizeof(repo), "%s", th_mktempdir("cbm-cvinc"));
snprintf(dbdir, sizeof(dbdir), "%s", th_mktempdir("cbm-cvdb"));
ASSERT_TRUE(repo[0] && dbdir[0]);
static const cv_file_t files[] = {
{"util.c", "void alpha(void) {}\nvoid beta(void) {}\nvoid gamma_fn(void) {}\n"},
{"pick_linux.c", "void pick(void) { alpha(); }\n"},
{"pick_win.c", "void pick(void) { beta(); }\n"},
{"caller.c", "void caller(void) { pick(); }\n"},
};
for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); i++) {
ASSERT_TRUE(cv_write(repo, &files[i]));
}
char inc_db[600];
snprintf(inc_db, sizeof(inc_db), "%s/inc.db", dbdir);
ASSERT_TRUE(cv_pipeline(repo, inc_db, NULL));
static char sig[8192];
/* Edit each variant file in turn (one of them holds the node). */
ASSERT_TRUE(cv_edit_matches_full(repo, dbdir, "pick_linux.c",
"void pick(void) { alpha(); gamma_fn(); }\n", sig,
sizeof(sig)));
ASSERT_TRUE(strstr(sig, "pick>beta\n") && strstr(sig, "pick>gamma_fn\n"));
ASSERT_TRUE(cv_edit_matches_full(repo, dbdir, "pick_win.c", "void pick(void) {\n beta();\n}\n",
sig, sizeof(sig)));
ASSERT_TRUE(strstr(sig, "pick>alpha\n") && strstr(sig, "pick>beta\n"));
th_rmtree(repo);
th_rmtree(dbdir);
PASS();
}
SUITE(conditional_variants) {
RUN_TEST(cv_c_preprocessor);
RUN_TEST(cv_cpp_elif);
RUN_TEST(cv_cpp_class_members);
RUN_TEST(cv_objc_methods);
RUN_TEST(cv_cuda);
RUN_TEST(cv_csharp_class_members);
RUN_TEST(cv_fsharp);
RUN_TEST(cv_rust_cfg_free_functions);
RUN_TEST(cv_rust_cfg_methods);
RUN_TEST(cv_go_build_tag_files);
RUN_TEST(cv_swift);
RUN_TEST(cv_python_if);
RUN_TEST(cv_ruby_if);
RUN_TEST(cv_php_if);
RUN_TEST(cv_d_version);
RUN_TEST(cv_erlang_ifdef);
RUN_TEST(cv_haskell_cpp);
RUN_TEST(cv_haskell_cpp_elif_nested);
RUN_TEST(cv_fortran_preprocessed);
RUN_TEST(cv_pascal_ifdef);
RUN_TEST(cv_julia_static_if);
RUN_TEST(cv_elixir_compile_time_if);
RUN_TEST(cv_verilog_ifdef);
RUN_TEST(cv_c_platform_files);
RUN_TEST(cv_c_platform_directories);
RUN_TEST(cv_c_non_variants_keep_their_nodes);
RUN_TEST(cv_no_cross_language_name_binding);
RUN_TEST(cv_incremental_variant_partners);
}
+32 -4
View File
@@ -3758,6 +3758,32 @@ TEST(go_cgo_pseudo_import_dropped) {
PASS();
}
/* A Bash script imports the files it sources and nothing else. Every top-level
* command used to be read as an import of its command name (`set -e` imported
* "set", `source x.sh` imported "source"), which the import resolver bound to
* any project symbol of that name; the sourced paths themselves were lost. */
TEST(bash_imports_are_sourced_files_only) {
CBMFileResult *r = extract("#!/usr/bin/env bash\nset -euo pipefail\nsource ./lib/util.sh\n"
". \"$DIR/other.sh\"\ncd build\nmake all\n"
"if true; then\n source nested.sh\nfi\n",
CBM_LANG_BASH, "t", "run.sh");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_EQ(r->imports.count, 3);
ASSERT(has_import(r, "lib/util.sh"));
ASSERT(has_import(r, "other.sh"));
ASSERT(has_import(r, "nested.sh"));
static const char *const commands[] = {"set", "source", ".", "cd", "make"};
for (int i = 0; i < r->imports.count; i++) {
ASSERT_NOT_NULL(r->imports.items[i].module_path);
for (size_t c = 0; c < sizeof(commands) / sizeof(commands[0]); c++) {
ASSERT_TRUE(strcmp(r->imports.items[i].module_path, commands[c]) != 0);
}
}
cbm_free_result(r);
PASS();
}
/* #1935: Go struct fields were never extracted — find_class_body() returns the
* struct_type node, whose only named child is a field_declaration_list, so the
* member loop matched nothing and every field was silently skipped (0 Field
@@ -5248,10 +5274,11 @@ TEST(swift_non_url_constructor_untouched_issue1892) {
* the per-file constant map and resolved at the call site, for both return
* statements and arrow expression bodies. */
TEST(extract_ts_await_generic_call_issue2210) {
CBMFileResult *r = extract("function parseJsonBody<T>() { return {} as T; }\n"
"async function plain() { return await parseJsonBody(); }\n"
"async function generic() { return await parseJsonBody<string>(); }\n",
CBM_LANG_TYPESCRIPT, "t", "await.ts");
CBMFileResult *r =
extract("function parseJsonBody<T>() { return {} as T; }\n"
"async function plain() { return await parseJsonBody(); }\n"
"async function generic() { return await parseJsonBody<string>(); }\n",
CBM_LANG_TYPESCRIPT, "t", "await.ts");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_EQ(count_calls_named(r, "parseJsonBody"), 2);
@@ -9748,6 +9775,7 @@ SUITE(extraction) {
RUN_TEST(js_imports);
RUN_TEST(go_imports);
RUN_TEST(go_cgo_pseudo_import_dropped);
RUN_TEST(bash_imports_are_sourced_files_only);
RUN_TEST(extract_go_struct_fields_have_nodes);
RUN_TEST(java_imports);
RUN_TEST(rust_imports);
+12 -13
View File
@@ -463,11 +463,10 @@ TEST(contract_python_relative_import) {
/* Python: relative aliased from-import must CALLS the real def (execute). */
TEST(contract_python_aliased_from_import_calls) {
static const LangFile f[] = {
{"gate.py", "def execute(x):\n return x\n"},
{"router.py",
"from .gate import execute as bridge_execute\n\n\n"
"def submit_task(y):\n return bridge_execute(y)\n"}};
static const LangFile f[] = {{"gate.py", "def execute(x):\n return x\n"},
{"router.py",
"from .gate import execute as bridge_execute\n\n\n"
"def submit_task(y):\n return bridge_execute(y)\n"}};
LangProj lp;
cbm_store_t *store = lang_index_files(&lp, f, 2);
ASSERT_TRUE(store != NULL);
@@ -560,10 +559,8 @@ TEST(contract_python_absolute_aliased_from_import_calls) {
/* Python: unresolvable module + alias must emit no CALLS (no invented edge). */
TEST(contract_python_ghost_module_aliased_from_import_no_calls) {
static const LangFile f[] = {
{"router.py",
"from ghost_module import nope as alias\n\n\n"
"def submit_task(y):\n return alias(y)\n"}};
static const LangFile f[] = {{"router.py", "from ghost_module import nope as alias\n\n\n"
"def submit_task(y):\n return alias(y)\n"}};
LangProj lp;
cbm_store_t *store = lang_index_files(&lp, f, 1);
ASSERT_TRUE(store != NULL);
@@ -1073,6 +1070,7 @@ static const char *ALL_EDGE_TYPES[] = {"CALLS",
"REFERENCES_FILE",
"SEMANTICALLY_RELATED",
"SIMILAR_TO",
"SPAWNS",
"TESTS_FILE",
"TESTS",
"TRPC_CALLS",
@@ -1760,10 +1758,11 @@ TEST(contract_edge_parallel_service_edges) {
if (store) {
sqlite3_stmt *stmt = NULL;
sqlite3 *db = cbm_store_get_db(store);
if (db && sqlite3_prepare_v2(db,
"SELECT count(*) FROM edges WHERE properties IS NOT NULL "
"AND properties != '' AND json_valid(properties)=0;",
-1, &stmt, NULL) == SQLITE_OK &&
if (db &&
sqlite3_prepare_v2(db,
"SELECT count(*) FROM edges WHERE properties IS NOT NULL "
"AND properties != '' AND json_valid(properties)=0;",
-1, &stmt, NULL) == SQLITE_OK &&
sqlite3_step(stmt) == SQLITE_ROW) {
invalid_props = sqlite3_column_int(stmt, 0);
}
+24 -20
View File
@@ -12,22 +12,22 @@ int tf_deselected_count = 0;
#include "test_framework.h"
#include "test_helpers.h"
#include "test_daemon_runtime_contract.h"
#include "foundation/compat.h" /* cbm_setenv — #845 supervisor kill switch */
#include "foundation/compat_fs.h" /* cbm_fopen — worker response file */
#include "foundation/constants.h" /* CBM_SZ_4K — forced stderr buffer */
#include "foundation/log.h" /* crash-durable worker log probe */
#include "foundation/mem.h" /* cbm_mem_init — worker budget */
#include "foundation/log.h" /* worker liveness heartbeat probe */
#include "foundation/platform.h" /* cbm_file_exists — blocking-git marker */
#include "foundation/compat.h" /* cbm_setenv — #845 supervisor kill switch */
#include "foundation/compat_fs.h" /* cbm_fopen — worker response file */
#include "foundation/constants.h" /* CBM_SZ_4K — forced stderr buffer */
#include "foundation/log.h" /* crash-durable worker log probe */
#include "foundation/mem.h" /* cbm_mem_init — worker budget */
#include "foundation/log.h" /* worker liveness heartbeat probe */
#include "foundation/platform.h" /* cbm_file_exists — blocking-git marker */
#include "foundation/test_selection.h" /* shared executable selection token bound */
#include "daemon/bootstrap.h" /* runner rendezvous isolation */
#include "daemon/runtime.h" /* bounded worker response probe */
#include "daemon/ipc.h" /* Windows private-lock re-exec probe */
#include "daemon/version_cohort.h" /* Windows crash-turnover re-exec probe */
#include "mcp/index_supervisor.h" /* cbm_index_set_worker_role */
#include "mcp/mcp.h" /* cbm_mcp_handle_tool — act as a real worker */
#include "ui/http_server.h" /* deleted-self executable probe */
#include "result_spill.h" /* pinned free disk: spill verdicts ignore the host disk */
#include "daemon/bootstrap.h" /* runner rendezvous isolation */
#include "daemon/runtime.h" /* bounded worker response probe */
#include "daemon/ipc.h" /* Windows private-lock re-exec probe */
#include "daemon/version_cohort.h" /* Windows crash-turnover re-exec probe */
#include "mcp/index_supervisor.h" /* cbm_index_set_worker_role */
#include "mcp/mcp.h" /* cbm_mcp_handle_tool — act as a real worker */
#include "ui/http_server.h" /* deleted-self executable probe */
#include "result_spill.h" /* pinned free disk: spill verdicts ignore the host disk */
#include <sqlite3.h>
#include <ctype.h>
#include <errno.h>
@@ -49,11 +49,11 @@ int tf_deselected_count = 0;
#include <sys/mman.h>
#endif
#ifdef CBM_TEST_COVERAGE
#include <dlfcn.h> /* the libc functions behind the profile-keeping ones */
#include <fcntl.h> /* the marker of a forked child */
#include <poll.h> /* non-consuming child-failure observation */
#include <pthread.h> /* pthread_atfork: a forked child names its own profile */
#include <stdarg.h> /* execl */
#include <dlfcn.h> /* the libc functions behind the profile-keeping ones */
#include <fcntl.h> /* the marker of a forked child */
#include <poll.h> /* non-consuming child-failure observation */
#include <pthread.h> /* pthread_atfork: a forked child names its own profile */
#include <stdarg.h> /* execl */
#include <sys/stat.h> /* reject detectable failure-channel descriptor replacement */
#endif
#endif
@@ -1853,6 +1853,8 @@ extern void suite_security(void);
extern void suite_yaml(void);
extern void suite_integration(void);
extern void suite_lang_contract(void);
extern void suite_conditional_variants(void);
extern void suite_spawns(void);
extern void suite_edge_imports(void);
extern void suite_edge_structural(void);
extern void suite_lsp_resolution_probe(void);
@@ -2312,6 +2314,8 @@ int main(int argc, char **argv) {
/* Per-language graph contracts (node/edge types, attribution, no-crash) */
RUN_SELECTED_SUITE(lang_contract);
RUN_SELECTED_SUITE(conditional_variants);
RUN_SELECTED_SUITE(spawns);
RUN_SELECTED_SUITE(edge_imports);
RUN_SELECTED_SUITE(edge_structural);
RUN_SELECTED_SUITE(lsp_resolution_probe);
+228
View File
@@ -1999,6 +1999,231 @@ TEST(pipeline_objectscript_export_incremental_matches_full_relationships) {
PASS();
}
/* Cross-language unique_name leak. Every Python file injects a few builtin
* stub definitions (builtins.dict.get, builtins.len, ...; see
* internal/cbm/lsp/py_builtins.c) into the project registry. A JS Express
* registration `app.get('/users', listUsers)` has no LSP target, falls
* through to the registry, and its short name "get" had exactly one
* candidate project-wide: Python's builtins.dict.get (strategy unique_name).
* The non-empty resolution skipped the route-registration fallback (it runs
* only on an EMPTY resolution), and the weak-member guard then dropped the
* plain CALLS edge, so the Route node and its HANDLES edge vanished -- on the
* sequential and the parallel path alike, and on an incremental reindex whose
* registry is re-seeded from stored nodes (the builtin stubs survive there
* even after the last .py file is gone).
*
* The same leak fabricates CALLS edges on both paths: a bare JS `print(msg)`
* bound to Python's builtins.print by unique_name. `cache.get(key)` guards the
* other side of the veto: an unresolved route verb without a route path must
* not become a CALLS self-loop on the parallel path.
*
* The Python control proves the builtins stay reachable from Python itself:
* lookup() -> builtins.dict.get (lsp_generic_method) and -> builtins.len. */
typedef struct {
int run_rc;
bool store_opened;
int routes; /* Route node "/users" */
int handles; /* listUsers -[HANDLES]-> "/users" */
int py_dict_get; /* lookup -[CALLS]-> get */
int py_len; /* lookup -[CALLS]-> len */
int js_print; /* report -[CALLS]-> print (JS global, never Python's) */
int self_loop; /* readCache -[CALLS]-> readCache (unresolved route verb) */
} XlangRouteObservation;
static XlangRouteObservation observe_xlang_route(const char *repo_path, const char *db_name) {
XlangRouteObservation o = {.run_rc = -1,
.routes = -1,
.handles = -1,
.py_dict_get = -1,
.py_len = -1,
.js_print = -1,
.self_loop = -1};
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/%s", repo_path, db_name);
cbm_pipeline_t *pipeline = cbm_pipeline_new(repo_path, db_path, CBM_MODE_FULL);
if (!pipeline) {
return o;
}
o.run_rc = cbm_pipeline_run(pipeline);
const char *project = cbm_pipeline_project_name(pipeline);
cbm_store_t *store = cbm_store_open_path(db_path);
o.store_opened = store != NULL;
if (store && project) {
o.routes = named_node_count(store, project, "/users");
o.handles = named_edge_count(store, project, "HANDLES", "listUsers", "/users");
o.py_dict_get = named_edge_count(store, project, "CALLS", "lookup", "get");
o.py_len = named_edge_count(store, project, "CALLS", "lookup", "len");
o.js_print = named_edge_count(store, project, "CALLS", "report", "print");
o.self_loop = named_edge_count(store, project, "CALLS", "readCache", "readCache");
}
if (store) {
cbm_store_close(store);
}
cbm_pipeline_free(pipeline);
return o;
}
static const char XLANG_APP_JS[] = "const express = require('express');\n"
"const app = express();\n"
"\n"
"function listUsers(req, res) {\n"
" res.json([]);\n"
"}\n"
"\n"
"app.get('/users', listUsers);\n"
"\n"
"function report(msg) {\n"
" print(msg);\n"
"}\n"
"\n"
"function readCache(cache, key) {\n"
" return cache.get(key);\n"
"}\n"
"module.exports = { app, report, readCache };\n";
static const char XLANG_TOOL_PY[] = "def lookup(xs):\n"
" d = {\"k\": 1}\n"
" n = len(xs)\n"
" return d.get(\"k\"), n\n";
/* Writes app.js + tool.py + `pad` JS filler files (the fillers only select the
* parallel path: more than MIN_FILES_FOR_PARALLEL files). 0 on success. */
static int write_xlang_route_fixture(const char *dir, int pad) {
char path[512];
snprintf(path, sizeof(path), "%s/app.js", dir);
if (th_write_file(path, XLANG_APP_JS) != 0) {
return -1;
}
snprintf(path, sizeof(path), "%s/tool.py", dir);
if (th_write_file(path, XLANG_TOOL_PY) != 0) {
return -1;
}
for (int i = 0; i < pad; i++) {
char source[128];
snprintf(path, sizeof(path), "%s/xlang_pad_%02d.js", dir, i);
snprintf(source, sizeof(source), "function xlangPad%02d() { return %d; }\n", i, i);
if (th_write_file(path, source) != 0) {
return -1;
}
}
return 0;
}
/* Saves and overrides the two worker-selection variables. */
typedef struct {
char *workers;
char *single;
} XlangEnvSave;
static XlangEnvSave xlang_env_select(bool parallel) {
XlangEnvSave save = {0};
const char *w = getenv("CBM_WORKERS");
const char *s = getenv("CBM_INDEX_SINGLE_THREAD");
save.workers = w ? strdup(w) : NULL;
save.single = s ? strdup(s) : NULL;
if (parallel) {
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
cbm_setenv("CBM_WORKERS", "4", 1);
} else {
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
}
return save;
}
static void xlang_env_restore(XlangEnvSave *save) {
if (save->workers) {
cbm_setenv("CBM_WORKERS", save->workers, 1);
free(save->workers);
} else {
cbm_unsetenv("CBM_WORKERS");
}
if (save->single) {
cbm_setenv("CBM_INDEX_SINGLE_THREAD", save->single, 1);
free(save->single);
} else {
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
}
save->workers = NULL;
save->single = NULL;
}
static XlangRouteObservation run_xlang_route_case(bool parallel, int pad) {
XlangRouteObservation o = {.run_rc = -1};
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_xlang_route_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
return o;
}
if (write_xlang_route_fixture(tmp, pad) == 0) {
XlangEnvSave save = xlang_env_select(parallel);
o = observe_xlang_route(tmp, "xlang.db");
xlang_env_restore(&save);
}
th_rmtree(tmp);
return o;
}
TEST(pipeline_js_route_not_bound_to_python_builtin_sequential) {
XlangRouteObservation o = run_xlang_route_case(false, 0);
ASSERT_EQ(o.run_rc, 0);
ASSERT_TRUE(o.store_opened);
ASSERT_EQ(o.routes, 1);
ASSERT_EQ(o.handles, 1);
ASSERT_EQ(o.py_dict_get, 1);
ASSERT_EQ(o.py_len, 1);
ASSERT_EQ(o.js_print, 0);
ASSERT_EQ(o.self_loop, 0);
PASS();
}
TEST(pipeline_js_route_not_bound_to_python_builtin_parallel) {
XlangRouteObservation o = run_xlang_route_case(true, 55);
ASSERT_EQ(o.run_rc, 0);
ASSERT_TRUE(o.store_opened);
ASSERT_EQ(o.routes, 1);
ASSERT_EQ(o.handles, 1);
ASSERT_EQ(o.py_dict_get, 1);
ASSERT_EQ(o.py_len, 1);
ASSERT_EQ(o.js_print, 0);
ASSERT_EQ(o.self_loop, 0);
PASS();
}
/* Incremental: the registry is re-seeded from stored nodes, where the Python
* builtin stubs outlive the last .py file (their file_path is synthetic). */
TEST(pipeline_js_route_not_bound_to_python_builtin_incremental) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_xlang_incr_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
if (write_xlang_route_fixture(tmp, 0) != 0) {
th_rmtree(tmp);
FAIL("fixture");
}
XlangEnvSave save = xlang_env_select(false);
XlangRouteObservation initial = observe_xlang_route(tmp, "xlang-incr.db");
char path[512];
snprintf(path, sizeof(path), "%s/tool.py", tmp);
int rm_rc = remove(path);
snprintf(path, sizeof(path), "%s/app.js", tmp);
int append_rc = th_append_file(path, "// edited: re-resolve this file\n");
XlangRouteObservation incremental = observe_xlang_route(tmp, "xlang-incr.db");
xlang_env_restore(&save);
th_rmtree(tmp);
ASSERT_EQ(rm_rc, 0);
ASSERT_EQ(append_rc, 0);
ASSERT_EQ(initial.run_rc, 0);
ASSERT_EQ(incremental.run_rc, 0);
ASSERT_TRUE(incremental.store_opened);
ASSERT_EQ(incremental.routes, 1);
ASSERT_EQ(incremental.handles, 1);
ASSERT_EQ(incremental.js_print, 0);
ASSERT_EQ(incremental.self_loop, 0);
PASS();
}
static bool test_buffer_appendf(char *buffer, size_t capacity, size_t *used, const char *format,
...) {
if (!buffer || !used || *used >= capacity) {
@@ -18151,6 +18376,9 @@ SUITE(pipeline) {
RUN_TEST(pipeline_incremental_preserves_cross_file_calls);
RUN_TEST(pipeline_objectscript_export_preserves_calls_sequential_parallel);
RUN_TEST(pipeline_objectscript_export_incremental_matches_full_relationships);
RUN_TEST(pipeline_js_route_not_bound_to_python_builtin_sequential);
RUN_TEST(pipeline_js_route_not_bound_to_python_builtin_parallel);
RUN_TEST(pipeline_js_route_not_bound_to_python_builtin_incremental);
RUN_TEST(pipeline_objectscript_export_aggregate_exceeds_arena_block_table);
#if defined(CBM_COVERAGE_MARKER_TEST_API) && CBM_COVERAGE_MARKER_TEST_API
RUN_TEST(pipeline_objectscript_export_range_join_keeps_one_trailing_marker);
+110
View File
@@ -961,6 +961,113 @@ TEST(registry_tie_break_is_independent_of_registration_order) {
PASS();
}
TEST(lang_resolution_compatible_families) {
/* Same language and the interop families the resolvers already honour. */
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_PYTHON, CBM_LANG_PYTHON));
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_JAVASCRIPT, CBM_LANG_TYPESCRIPT));
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_VUE, CBM_LANG_TSX));
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_C, CBM_LANG_CPP));
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_OBJC, CBM_LANG_C));
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_KOTLIN, CBM_LANG_JAVA));
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_SWIFT, CBM_LANG_OBJC));
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_OBJC, CBM_LANG_SWIFT));
/* Unknown on either side: no evidence, no filter. */
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_COUNT, CBM_LANG_PYTHON));
ASSERT_TRUE(cbm_lang_resolution_compatible(CBM_LANG_JAVASCRIPT, CBM_LANG_COUNT));
/* Across families: never by name alone. */
ASSERT_FALSE(cbm_lang_resolution_compatible(CBM_LANG_JAVASCRIPT, CBM_LANG_PYTHON));
ASSERT_FALSE(cbm_lang_resolution_compatible(CBM_LANG_PYTHON, CBM_LANG_TSX));
ASSERT_FALSE(cbm_lang_resolution_compatible(CBM_LANG_GO, CBM_LANG_C));
ASSERT_FALSE(cbm_lang_resolution_compatible(CBM_LANG_SWIFT, CBM_LANG_KOTLIN));
ASSERT_FALSE(cbm_lang_resolution_compatible(CBM_LANG_MAKEFILE, CBM_LANG_C));
/* Synthetic LSP sources carry their language. */
ASSERT_EQ(cbm_registry_synthetic_path_language("<python-builtins>"), CBM_LANG_PYTHON);
ASSERT_EQ(cbm_registry_synthetic_path_language("<kotlin-builtins>"), CBM_LANG_KOTLIN);
ASSERT_EQ(cbm_registry_synthetic_path_language("app.py"), CBM_LANG_COUNT);
ASSERT_EQ(cbm_registry_synthetic_path_language(NULL), CBM_LANG_COUNT);
PASS();
}
/* Resolve with the unfiltered chain, then apply the cross-language veto the
* way pass_calls.c / pass_parallel.c do. */
static cbm_resolution_t xl_resolve(const cbm_registry_t *r, CBMLanguage lang, const char *callee,
const char *module_qn) {
cbm_resolution_t res = cbm_registry_resolve(r, callee, module_qn, NULL, NULL, 0);
if (cbm_registry_name_guess_vetoed(r, lang, &res)) {
res = (cbm_resolution_t){0};
}
return res;
}
TEST(name_guess_vetoed_across_languages) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add_lang(r, "get", "builtins.dict.get", "Method", CBM_LANG_PYTHON);
cbm_registry_add_lang(r, "patch", "proj.web.Panel.patch", "Function", CBM_LANG_TSX);
cbm_registry_add_lang(r, "patch", "proj.api.client.patch", "Function", CBM_LANG_PYTHON);
cbm_registry_add(r, "legacy", "proj.x.legacy", "Function"); /* unknown language */
/* JS `app.get`: the only "get" is Python's -> EMPTY, not unique_name. */
cbm_resolution_t res = xl_resolve(r, CBM_LANG_JAVASCRIPT, "app.get", "proj.app");
ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0');
/* Python keeps its own builtin. */
res = xl_resolve(r, CBM_LANG_PYTHON, "d.get", "proj.tool");
ASSERT_STR_EQ(res.qualified_name, "builtins.dict.get");
ASSERT_STR_EQ(res.strategy, "unique_name");
/* Two same-named candidates: the chain still picks by import distance;
* the answer stands only for a caller that can call the winner. */
res = xl_resolve(r, CBM_LANG_PYTHON, "patch", "proj.api.x");
ASSERT_STR_EQ(res.qualified_name, "proj.api.client.patch");
ASSERT_STR_EQ(res.strategy, "suffix_match");
res = xl_resolve(r, CBM_LANG_TYPESCRIPT, "patch", "proj.api.x");
ASSERT_TRUE(res.qualified_name == NULL || res.qualified_name[0] == '\0');
res = xl_resolve(r, CBM_LANG_TYPESCRIPT, "patch", "proj.web.ui");
ASSERT_STR_EQ(res.qualified_name, "proj.web.Panel.patch");
/* Unknown-language entries and an unknown caller are never vetoed. */
res = xl_resolve(r, CBM_LANG_GO, "legacy", "proj.go");
ASSERT_STR_EQ(res.qualified_name, "proj.x.legacy");
res = xl_resolve(r, CBM_LANG_COUNT, "app.get", "proj.app");
ASSERT_STR_EQ(res.qualified_name, "builtins.dict.get");
/* Evidence-backed strategies are never vetoed, whatever the language. */
cbm_resolution_t imp = {.qualified_name = "builtins.dict.get",
.strategy = "import_map",
.confidence = 0.95,
.candidate_count = 1};
ASSERT_FALSE(cbm_registry_name_guess_vetoed(r, CBM_LANG_JAVASCRIPT, &imp));
imp.strategy = "lsp_generic_method";
ASSERT_FALSE(cbm_registry_name_guess_vetoed(r, CBM_LANG_JAVASCRIPT, &imp));
/* The parallel field-type hint re-picks language-blind: vetoed too. */
imp.strategy = "field_type_hint";
ASSERT_TRUE(cbm_registry_name_guess_vetoed(r, CBM_LANG_JAVASCRIPT, &imp));
ASSERT_FALSE(cbm_registry_name_guess_vetoed(r, CBM_LANG_PYTHON, &imp));
cbm_registry_free(r);
PASS();
}
/* tests/cov.c and tests/cov.py share the module QN proj.tests.cov (module QNs
* drop the extension): a same_module answer across languages is that
* collision, not evidence. Within the caller's language it stands. */
TEST(same_module_vetoed_across_languages) {
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add_lang(r, "reducer_worker", "proj.tests.cov.reducer_worker", "Function",
CBM_LANG_PYTHON);
cbm_registry_add_lang(r, "reducer_worker", "proj.other.reducer_worker", "Function",
CBM_LANG_PYTHON);
cbm_registry_add_lang(r, "stat_child", "proj.tests.cov.stat_child", "Function", CBM_LANG_C);
cbm_resolution_t res =
cbm_registry_resolve(r, "reducer_worker", "proj.tests.cov", NULL, NULL, 0);
ASSERT_STR_EQ(res.strategy, "same_module");
ASSERT_TRUE(cbm_registry_name_guess_vetoed(r, CBM_LANG_C, &res));
ASSERT_FALSE(cbm_registry_name_guess_vetoed(r, CBM_LANG_PYTHON, &res));
res = cbm_registry_resolve(r, "stat_child", "proj.tests.cov", NULL, NULL, 0);
ASSERT_STR_EQ(res.qualified_name, "proj.tests.cov.stat_child");
ASSERT_FALSE(cbm_registry_name_guess_vetoed(r, CBM_LANG_C, &res));
cbm_registry_free(r);
PASS();
}
TEST(cross_language_suffix_match_drops_py_vs_js) {
/* #725: two same-named symbols in different languages. suffix_match is the
* strategy that collapses them; unique_name is #1572 and must stay. */
@@ -1473,6 +1580,9 @@ SUITE(registry) {
RUN_TEST(perl_suppress_drops_weak_builtin_and_method_matches);
RUN_TEST(perl_suppress_keeps_high_confidence_and_genuine_calls);
RUN_TEST(cross_language_suffix_match_drops_py_vs_js);
RUN_TEST(lang_resolution_compatible_families);
RUN_TEST(name_guess_vetoed_across_languages);
RUN_TEST(same_module_vetoed_across_languages);
RUN_TEST(cross_language_config_caller_drops_unique_name_too);
RUN_TEST(registry_tie_break_is_independent_of_registration_order);
RUN_TEST(cross_language_ref_drops_go_vs_c);
+376
View File
@@ -0,0 +1,376 @@
/*
* test_spawns.c — SPAWNS edges: a call that starts another program, to the
* Process node of that program (internal/cbm/spawn_patterns.h,
* src/pipeline/pass_spawns.c).
*
* Unit tests pin how a call is recognised (spellings, the file's imports,
* bare names only when nothing in the project answers) and which program it
* starts (a literal, a collection's first element, never an argument after a
* variable program). Pipeline tests pin the edges in an indexed repo and that
* the sequential and parallel resolvers emit the same ones.
*/
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include <foundation/compat_fs.h>
#include <mcp/mcp.h>
#include <pipeline/pipeline.h>
#include <store/store.h>
#include <sqlite3.h>
#include "spawn_patterns.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* ── Recognition ─────────────────────────────────────────────────── */
static const char *sp_api(CBMLanguage lang, const char *callee, const CBMImport *imports,
int count) {
const cbm_spawn_api_t *api = cbm_spawn_match(lang, callee, imports, count);
return api ? api->api : NULL;
}
TEST(spawn_match_through_python_imports) {
const CBMImport imports[] = {{.local_name = "sp", .module_path = "subprocess"},
{.local_name = "run", .module_path = "subprocess.run"},
{.local_name = "os", .module_path = "os"}};
ASSERT_STR_EQ(sp_api(CBM_LANG_PYTHON, "sp.check_output", imports, 3),
"subprocess.check_output");
ASSERT_STR_EQ(sp_api(CBM_LANG_PYTHON, "run", imports, 3), "subprocess.run");
ASSERT_STR_EQ(sp_api(CBM_LANG_PYTHON, "subprocess.Popen", NULL, 0), "subprocess.Popen");
ASSERT_STR_EQ(sp_api(CBM_LANG_PYTHON, "os.spawnv", imports, 3), "os.spawnv");
/* A bare `run` is not subprocess.run without the import that says so. */
ASSERT_NULL(sp_api(CBM_LANG_PYTHON, "run", NULL, 0));
ASSERT_NULL(sp_api(CBM_LANG_PYTHON, "system", NULL, 0));
PASS();
}
TEST(spawn_match_through_go_rust_js_imports) {
const CBMImport go[] = {{.local_name = "exec", .module_path = "os/exec"}};
ASSERT_STR_EQ(sp_api(CBM_LANG_GO, "exec.Command", go, 1), "os/exec.Command");
ASSERT_STR_EQ(sp_api(CBM_LANG_GO, "exec.CommandContext", go, 1), "os/exec.CommandContext");
ASSERT_NULL(sp_api(CBM_LANG_GO, "exec.Command", NULL, 0));
const CBMImport rust[] = {{.local_name = "Command", .module_path = "std::process::Command"}};
ASSERT_STR_EQ(sp_api(CBM_LANG_RUST, "Command::new", rust, 1), "std::process::Command::new");
ASSERT_NULL(sp_api(CBM_LANG_RUST, "Command::new", NULL, 0));
/* `import { spawn } from "node:child_process"`, `const cp = require(..)`,
* and a destructured require that records only the module. */
const CBMImport named[] = {{.local_name = "spawn", .module_path = "node:child_process"},
{.local_name = "cp", .module_path = "child_process"}};
ASSERT_STR_EQ(sp_api(CBM_LANG_TYPESCRIPT, "spawn", named, 2), "child_process.spawn");
ASSERT_STR_EQ(sp_api(CBM_LANG_JAVASCRIPT, "cp.execSync", named, 2), "child_process.execSync");
const CBMImport destructured[] = {
{.local_name = "child_process", .module_path = "child_process"}};
ASSERT_STR_EQ(sp_api(CBM_LANG_JAVASCRIPT, "execFile", destructured, 1), "execFile");
ASSERT_NULL(sp_api(CBM_LANG_JAVASCRIPT, "execFile", NULL, 0));
PASS();
}
/* Bare names: libc-style ones by spelling (a definition of the name in the
* calling module still wins, see spawns_in_the_graph), import-gated ones only
* with their import. */
TEST(spawn_match_bare_names) {
ASSERT_STR_EQ(sp_api(CBM_LANG_C, "system", NULL, 0), "system");
ASSERT_STR_EQ(sp_api(CBM_LANG_CPP, "std::system", NULL, 0), "std::system");
ASSERT_STR_EQ(sp_api(CBM_LANG_RUBY, "spawn", NULL, 0), "spawn");
const CBMImport hs[] = {{.local_name = "Process", .module_path = "System.Process"}};
ASSERT_STR_EQ(sp_api(CBM_LANG_HASKELL, "callProcess", hs, 1), "callProcess");
ASSERT_NULL(sp_api(CBM_LANG_HASKELL, "callProcess", NULL, 0));
ASSERT_NULL(sp_api(CBM_LANG_GO, "system", NULL, 0));
ASSERT_NULL(sp_api(CBM_LANG_C, "systemd_notify", NULL, 0));
PASS();
}
/* ── The program ─────────────────────────────────────────────────── */
/* Program of `callee(arg0, arg1)` as the extractor records it. */
static const char *sp_program(CBMLanguage lang, const char *callee, const char *arg0,
const char *arg1, const CBMImport *imports, int count, char *buf) {
CBMCallArg args[2] = {{.expr = arg0, .index = 0}, {.expr = arg1, .index = 1}};
CBMCall call = {.callee_name = callee, .args = args, .arg_count = arg1 ? 2 : (arg0 ? 1 : 0)};
const cbm_spawn_api_t *api = cbm_spawn_match(lang, callee, imports, count);
if (!api) {
return "(no match)";
}
cbm_spawn_program(api, lang, &call, buf, CBM_SPAWN_PROGRAM_MAX);
return buf;
}
TEST(spawn_program_rules) {
char b[CBM_SPAWN_PROGRAM_MAX];
const CBMLanguage py = CBM_LANG_PYTHON;
ASSERT_STR_EQ(sp_program(py, "subprocess.run", "[\"git\", \"status\"]", NULL, NULL, 0, b),
"git");
/* The program is the variable; the arguments after it are not. */
ASSERT_STR_EQ(sp_program(py, "subprocess.run", "[binary, \"--version\"]", NULL, NULL, 0, b),
CBM_SPAWN_DYNAMIC);
ASSERT_STR_EQ(sp_program(py, "subprocess.Popen", "record[\"argv\"]", NULL, NULL, 0, b),
CBM_SPAWN_DYNAMIC);
ASSERT_STR_EQ(sp_program(py, "subprocess.run", "os.path.join(\"bin\", tool)", NULL, NULL, 0, b),
CBM_SPAWN_DYNAMIC);
ASSERT_STR_EQ(sp_program(py, "subprocess.run", "f\"{tool} --x\"", NULL, NULL, 0, b),
CBM_SPAWN_DYNAMIC);
ASSERT_STR_EQ(sp_program(py, "subprocess.run", "args=[\"make\", \"all\"]", NULL, NULL, 0, b),
"make");
ASSERT_STR_EQ(sp_program(py, "os.system", "\"rm -rf build\"", NULL, NULL, 0, b), "rm");
/* Environment prefixes and directories are not the program. */
ASSERT_STR_EQ(sp_program(CBM_LANG_C, "popen", "\"LC_ALL=C ps -eo pid\"", "\"r\"", NULL, 0, b),
"ps");
ASSERT_STR_EQ(
sp_program(CBM_LANG_C, "system", "\"/usr/bin/env FOO=1 make all\"", NULL, NULL, 0, b),
"make");
/* posix_spawnp names the program second; CreateProcess in its command
* line when the application name is NULL. */
ASSERT_STR_EQ(sp_program(CBM_LANG_C, "posix_spawnp", "&pid", "\"git\"", NULL, 0, b), "git");
ASSERT_STR_EQ(sp_program(CBM_LANG_C, "CreateProcessW", "NULL", "L\"cmd /c dir\"", NULL, 0, b),
"cmd");
ASSERT_STR_EQ(sp_program(CBM_LANG_C, "execvp", "argv[0]", "argv", NULL, 0, b),
CBM_SPAWN_DYNAMIC);
ASSERT_STR_EQ(
sp_program(CBM_LANG_KOTLIN, "ProcessBuilder", "listOf(\"git\", \"log\")", NULL, NULL, 0, b),
"git");
ASSERT_STR_EQ(sp_program(CBM_LANG_SWIFT, "Process.launchedProcess", "launchPath: \"/bin/ls\"",
"arguments: []", NULL, 0, b),
"ls");
/* Options tuples / structs: the program is the literal inside. */
ASSERT_STR_EQ(sp_program(CBM_LANG_ELIXIR, "Port.open", "{:spawn, \"cat\"}", "[]", NULL, 0, b),
"cat");
/* Groovy: the receiver is the command line. */
ASSERT_STR_EQ(sp_program(CBM_LANG_GROOVY, "\"git status\".execute", NULL, NULL, NULL, 0, b),
"git");
PASS();
}
/* ── In the graph ────────────────────────────────────────────────── */
typedef struct {
const char *name;
const char *content;
} sp_file_t;
typedef struct {
char tmpdir[256];
char dbpath[1024];
char *project;
cbm_mcp_server_t *srv;
cbm_store_t *store;
} sp_proj_t;
static bool sp_write(const char *root, const char *name, const char *content) {
char path[700];
snprintf(path, sizeof(path), "%s/%s", root, name);
char *slash = strrchr(path, '/');
if (slash && slash > path + strlen(root)) {
*slash = '\0';
cbm_mkdir_p(path, 0755);
*slash = '/';
}
FILE *f = cbm_fopen(path, "wb");
if (!f) {
return false;
}
bool ok = fputs(content, f) >= 0;
return fclose(f) == 0 && ok;
}
/* Index `files`, plus `pad` trivial Python files (enough of them moves the
* index onto the parallel resolver, MIN_FILES_FOR_PARALLEL). */
static bool sp_index(sp_proj_t *p, const sp_file_t *files, int count, int pad) {
memset(p, 0, sizeof(*p));
snprintf(p->tmpdir, sizeof(p->tmpdir), "%s", th_mktempdir("cbm-sp"));
if (!p->tmpdir[0]) {
return false;
}
for (int i = 0; i < count; i++) {
if (!sp_write(p->tmpdir, files[i].name, files[i].content)) {
return false;
}
}
for (int i = 0; i < pad; i++) {
char name[64];
char body[96];
snprintf(name, sizeof(name), "pad/p%d.py", i);
snprintf(body, sizeof(body), "def pad_%d():\n return %d\n", i, i);
if (!sp_write(p->tmpdir, name, body)) {
return false;
}
}
p->project = cbm_project_name_from_path(p->tmpdir);
if (!p->project) {
return false;
}
char cache[700];
const char *configured = getenv("CBM_CACHE_DIR");
const char *home = getenv("HOME");
if (configured && configured[0]) {
snprintf(cache, sizeof(cache), "%s", configured);
} else {
snprintf(cache, sizeof(cache), "%s/.cache/codebase-memory-mcp", home ? home : "/tmp");
}
cbm_mkdir_p(cache, 0755);
snprintf(p->dbpath, sizeof(p->dbpath), "%s/%s.db", cache, p->project);
unlink(p->dbpath);
p->srv = cbm_mcp_server_new(NULL);
if (!p->srv) {
return false;
}
char args[800];
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", p->tmpdir);
free(cbm_mcp_handle_tool(p->srv, "index_repository", args));
p->store = cbm_store_open_path(p->dbpath);
return p->store != NULL;
}
static void sp_cleanup(sp_proj_t *p) {
if (p->store) {
cbm_store_close(p->store);
}
if (p->srv) {
cbm_mcp_server_free(p->srv);
}
if (p->dbpath[0]) {
char side[1100];
unlink(p->dbpath);
snprintf(side, sizeof(side), "%s-wal", p->dbpath);
unlink(side);
snprintf(side, sizeof(side), "%s-shm", p->dbpath);
unlink(side);
}
free(p->project);
if (p->tmpdir[0]) {
th_rmtree(p->tmpdir);
}
memset(p, 0, sizeof(*p));
}
static int sp_count(sp_proj_t *p, const char *sql, const char *a, const char *b) {
sqlite3 *db = cbm_store_get_db(p->store);
sqlite3_stmt *stmt = NULL;
if (!db || sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
sqlite3_finalize(stmt);
return -1;
}
int n = sqlite3_bind_parameter_count(stmt);
const char *values[] = {p->project, a, b};
for (int i = 0; i < n && i < 3; i++) {
sqlite3_bind_text(stmt, i + 1, values[i], -1, SQLITE_TRANSIENT);
}
int count = sqlite3_step(stmt) == SQLITE_ROW ? sqlite3_column_int(stmt, 0) : -1;
sqlite3_finalize(stmt);
return count;
}
/* SPAWNS edges from the definition named `caller` to the Process `program`. */
static int sp_spawns(sp_proj_t *p, const char *caller, const char *program) {
return sp_count(p,
"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id "
"JOIN nodes t ON t.id=e.target_id WHERE e.project=?1 AND e.type='SPAWNS' "
"AND s.name=?2 AND t.name=?3 AND t.label='Process'",
caller, program);
}
static int sp_calls(sp_proj_t *p, const char *caller, const char *callee) {
return sp_count(p,
"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id "
"JOIN nodes t ON t.id=e.target_id WHERE e.project=?1 AND e.type='CALLS' "
"AND s.name=?2 AND t.name=?3",
caller, callee);
}
/* Every SPAWNS edge as "caller>program|api" lines, sorted, into `out`. */
static bool sp_edge_set(sp_proj_t *p, char *out, size_t n) {
sqlite3 *db = cbm_store_get_db(p->store);
sqlite3_stmt *stmt = NULL;
const char *sql = "SELECT s.name || '>' || t.name || '|' || json_extract(e.properties,'$.api') "
"FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON "
"t.id=e.target_id WHERE e.project=?1 AND e.type='SPAWNS' ORDER BY 1";
if (!db || sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
sqlite3_finalize(stmt);
return false;
}
sqlite3_bind_text(stmt, 1, p->project, -1, SQLITE_TRANSIENT);
out[0] = '\0';
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *row = (const char *)sqlite3_column_text(stmt, 0);
size_t used = strlen(out);
snprintf(out + used, n - used, "%s\n", row ? row : "?");
}
sqlite3_finalize(stmt);
return true;
}
static const sp_file_t SP_FILES[] = {
/* subprocess.run inside the module's own `def run`: the resolver's
* suffix guess (same_module) must not make it a call to `run`. */
{"tools.py", "import subprocess\n\n\ndef run(cmd):\n return subprocess.run(cmd)\n\n\n"
"def build():\n subprocess.check_output([\"make\", \"all\"])\n"},
{"native.c", "#include <stdlib.h>\nint clean(void) { return system(\"rm -rf build\"); }\n"},
/* A project-defined `system` answers the bare name: a call, no spawn. */
{"own.c", "static int system(const char *c) { return c != 0; }\n"
"int use_own(void) { return system(\"x\"); }\n"},
{"deploy.go", "package main\n\nimport \"os/exec\"\n\n"
"func deploy() { exec.Command(\"kubectl\", \"apply\").Run() }\n"},
{"launch.js", "const { spawn } = require(\"child_process\");\n"
"function start() { spawn(\"node\", [\"server.js\"]); }\n"},
};
#define SP_FILE_COUNT ((int)(sizeof(SP_FILES) / sizeof(SP_FILES[0])))
TEST(spawns_in_the_graph) {
sp_proj_t p;
ASSERT_TRUE(sp_index(&p, SP_FILES, SP_FILE_COUNT, 0));
ASSERT_EQ(sp_spawns(&p, "build", "make"), 1);
ASSERT_EQ(sp_spawns(&p, "run", CBM_SPAWN_DYNAMIC), 1);
ASSERT_EQ(sp_spawns(&p, "clean", "rm"), 1);
ASSERT_EQ(sp_spawns(&p, "deploy", "kubectl"), 1);
ASSERT_EQ(sp_spawns(&p, "start", "node"), 1);
/* The spawn replaces the CALLS edge a guess would have made. */
ASSERT_EQ(sp_calls(&p, "build", "run"), 0);
/* A project-defined system() is code, not a process. */
ASSERT_EQ(sp_calls(&p, "use_own", "system"), 1);
ASSERT_EQ(sp_count(&p,
"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id WHERE "
"e.project=?1 AND e.type='SPAWNS' AND s.name=?2",
"use_own", NULL),
0);
/* One Process node per program, named and qualified by it. */
ASSERT_EQ(sp_count(&p,
"SELECT COUNT(*) FROM nodes WHERE label='Process' AND name=?2 AND "
"qualified_name='__process__make'",
"make", NULL),
1);
sp_cleanup(&p);
PASS();
}
/* The sequential (few files) and parallel (past MIN_FILES_FOR_PARALLEL)
* resolvers ask at the same point and emit the same SPAWNS edges. */
TEST(spawns_parallel_matches_sequential) {
static char seq[4096];
static char par[4096];
sp_proj_t p;
ASSERT_TRUE(sp_index(&p, SP_FILES, SP_FILE_COUNT, 0));
ASSERT_TRUE(sp_edge_set(&p, seq, sizeof(seq)));
sp_cleanup(&p);
ASSERT_TRUE(sp_index(&p, SP_FILES, SP_FILE_COUNT, 60));
ASSERT_TRUE(sp_edge_set(&p, par, sizeof(par)));
sp_cleanup(&p);
ASSERT_TRUE(seq[0] != '\0');
if (strcmp(seq, par) != 0) {
fprintf(stderr, " sequential:\n%s parallel:\n%s", seq, par);
}
ASSERT_STR_EQ(seq, par);
PASS();
}
SUITE(spawns) {
RUN_TEST(spawn_match_through_python_imports);
RUN_TEST(spawn_match_through_go_rust_js_imports);
RUN_TEST(spawn_match_bare_names);
RUN_TEST(spawn_program_rules);
RUN_TEST(spawns_in_the_graph);
RUN_TEST(spawns_parallel_matches_sequential);
}
+109 -11
View File
@@ -122,14 +122,20 @@ static const char tie_lib[] = "int lib_value(void) {\n"
" return 2;\n"
"}\n";
static bool tie_open_with(tie_fixture_t *fx, const char *extra_path, const char *extra);
static bool tie_open_files(tie_fixture_t *fx, const char *const *extra, int extra_count);
static bool tie_open(tie_fixture_t *fx) {
return tie_open_with(fx, NULL, NULL);
return tie_open_files(fx, NULL, 0);
}
/* The fixture, plus one more file in the base commit. */
static bool tie_open_with(tie_fixture_t *fx, const char *extra_path, const char *extra) {
const char *const files[] = {extra_path, extra};
return tie_open_files(fx, files, 1);
}
/* The fixture, plus (path, content) pairs written over it in the base commit. */
static bool tie_open_files(tie_fixture_t *fx, const char *const *extra, int extra_count) {
memset(fx, 0, sizeof(*fx));
const char *git = cbm_find_cli("git", cbm_get_home_dir());
const char *home = th_mktempdir("cbm-ti-engine");
@@ -145,15 +151,18 @@ static bool tie_open_with(tie_fixture_t *fx, const char *extra_path, const char
const char *init[] = {"-c", "init.templateDir=", "init",
"--quiet", "--initial-branch=main", NULL};
const char *topic[] = {"checkout", "--quiet", "-b", "topic", NULL};
return th_mkdir_p(fx->repo) == 0 && th_mkdir_p(fx->work) == 0 && chmod(fx->work, 0700) == 0 &&
tie_git(fx, init) && tie_write(fx, ".codebase-memory.json", tie_config) &&
tie_write(fx, "tests/test_main.c", tie_main) &&
tie_write(fx, "tests/test_alpha.c", tie_alpha) &&
tie_write(fx, "tests/test_beta.c", tie_beta) &&
tie_write(fx, "tests/test_matrix.c", tie_matrix) &&
tie_write(fx, "src/lib.c", tie_lib) && tie_write(fx, "Makefile", "all:\n") &&
(!extra_path || tie_write(fx, extra_path, extra)) && tie_commit(fx, "base") &&
tie_git(fx, topic);
bool ok = th_mkdir_p(fx->repo) == 0 && th_mkdir_p(fx->work) == 0 &&
chmod(fx->work, 0700) == 0 && tie_git(fx, init) &&
tie_write(fx, ".codebase-memory.json", tie_config) &&
tie_write(fx, "tests/test_main.c", tie_main) &&
tie_write(fx, "tests/test_alpha.c", tie_alpha) &&
tie_write(fx, "tests/test_beta.c", tie_beta) &&
tie_write(fx, "tests/test_matrix.c", tie_matrix) &&
tie_write(fx, "src/lib.c", tie_lib) && tie_write(fx, "Makefile", "all:\n");
for (int i = 0; ok && i < extra_count; i++) {
ok = tie_write(fx, extra[2 * i], extra[2 * i + 1]);
}
return ok && tie_commit(fx, "base") && tie_git(fx, topic);
}
static void tie_close(tie_fixture_t *fx) {
@@ -463,7 +472,96 @@ TEST(test_impact_engine_coverage_reaches_unmapped_code_through_mapped_callers) {
PASS();
}
/* One test written once per #if branch is one graph node (graph_buffer.c
* "Definition variants"): the engine maps it as the graph counts it, never as
* an ambiguous name. */
static const char tie_alpha_variants[] = "int lib_value(void);\n"
"#ifdef ALPHA_WIDE\n"
"TEST(alpha_uses_lib) {\n"
" return lib_value() == 1 ? 0 : 1;\n"
"}\n"
"#else\n"
"TEST(alpha_uses_lib) {\n"
" return lib_value() > 0 ? 0 : 1;\n"
"}\n"
"#endif\n"
"TEST(alpha_plain) {\n"
" return 0;\n"
"}\n"
"SUITE(alpha) {\n"
" RUN_TEST(alpha_uses_lib);\n"
" RUN_TEST(alpha_plain);\n"
"}\n";
TEST(test_impact_engine_variant_tests_are_one_test) {
tie_fixture_t fx;
ASSERT_TRUE(tie_open_with(&fx, "tests/test_alpha.c", tie_alpha_variants));
ASSERT_TRUE(tie_write(&fx, "src/lib.c",
"int lib_value(void) {\n return 1 + 0;\n}\n"
"int lib_other(void) {\n return 2;\n}\n"));
ASSERT_TRUE(tie_commit(&fx, "change lib_value"));
char *json = tie_answer(&fx);
ASSERT_NOT_NULL(json);
if (!tie_suite_has(json, "alpha", "STATIC") || tie_suite_has(json, "alpha", "UNMAPPED")) {
printf(" %.900s\n", json);
}
ASSERT_TRUE(tie_suite_has(json, "alpha", "STATIC"));
ASSERT_FALSE(tie_suite_has(json, "alpha", "UNMAPPED"));
free(json);
tie_close(&fx);
PASS();
}
/* A test that starts a program runs code no CALLS edge leads to; the program
* runs main, and main reaches everything. A change that reaches the product's
* main reaches the tests that spawn; one that does not, does not. */
static const char tie_spawn_main[] = "int lib_value(void);\n"
"int main(void) {\n"
" return lib_value();\n"
"}\n";
static const char tie_spawn_beta[] = "#include <stdlib.h>\n"
"TEST(beta_alone) {\n"
" return system(\"./app --version\");\n"
"}\n"
"SUITE(beta) {\n"
" RUN_TEST(beta_alone);\n"
"}\n";
TEST(test_impact_engine_spawning_tests_follow_main) {
const char *const extra[] = {"src/main.c", tie_spawn_main, "tests/test_beta.c", tie_spawn_beta};
tie_fixture_t fx;
ASSERT_TRUE(tie_open_files(&fx, extra, 2));
ASSERT_TRUE(tie_write(&fx, "src/lib.c",
"int lib_value(void) {\n return 1 + 0;\n}\n"
"int lib_other(void) {\n return 2;\n}\n"));
ASSERT_TRUE(tie_commit(&fx, "change lib_value"));
char *json = tie_answer(&fx);
ASSERT_NOT_NULL(json);
if (!tie_suite_has(json, "beta", "STATIC")) {
printf(" %.900s\n", json);
}
ASSERT_TRUE(tie_suite_has(json, "alpha", "STATIC"));
ASSERT_TRUE(tie_suite_has(json, "beta", "STATIC"));
ASSERT_FALSE(tie_suite_has(json, "matrix", "STATIC"));
free(json);
/* lib_other: main never calls it, so the spawning test is not reached. */
ASSERT_TRUE(tie_write(&fx, "src/lib.c",
"int lib_value(void) {\n return 1;\n}\n"
"int lib_other(void) {\n return 2 + 0;\n}\n"));
ASSERT_TRUE(tie_commit(&fx, "change lib_other instead"));
json = tie_answer(&fx);
ASSERT_NOT_NULL(json);
ASSERT_TRUE(tie_suite_has(json, "matrix", "STATIC"));
ASSERT_FALSE(tie_suite_has(json, "beta", "STATIC"));
free(json);
tie_close(&fx);
PASS();
}
SUITE(test_impact_engine) {
RUN_TEST(test_impact_engine_variant_tests_are_one_test);
RUN_TEST(test_impact_engine_spawning_tests_follow_main);
RUN_TEST(test_impact_engine_reached_suites_carry_static);
RUN_TEST(test_impact_engine_macro_registered_tests_select_their_suite);
RUN_TEST(test_impact_engine_rules_unmapped_and_empty);
+85
View File
@@ -286,8 +286,93 @@ TEST(test_impact_seed_unclassified_header_lines_escalate_the_file) {
PASS();
}
/* One definition with variants (graph_buffer.c "Definition variants") is one
* node holding one variant's span; the others are in its `variants`
* property, in the same file (#if branches) or in another file that DEFINES
* it too (platform files). A hunk in ANY variant seeds that node. */
TEST(test_impact_seed_variant_spans_seed_their_definition) {
static const char pick_src[] = "#ifdef _WIN32\nvoid pick(void) {\n beta();\n}\n#else\n"
"void pick(void) {\n alpha();\n}\n#endif\n";
const char *files[] = {"src/pick.c", pick_src,
"src/plat_linux.c", "void plat(void) {\n alpha();\n}\n",
"src/plat_win.c", "void plat(void) {\n beta();\n}\n"};
tsd_fixture_t f;
ASSERT_TRUE(tsd_open(&f, files, 6));
cbm_node_t pick = {.project = "p",
.label = "Function",
.name = "pick",
.qualified_name = "p.src.pick.pick",
.file_path = "src/pick.c",
.start_line = 2,
.end_line = 4,
.properties_json = "{\"variants\":["
"{\"file_path\":\"src/pick.c\",\"start_line\":2,"
"\"end_line\":4},"
"{\"file_path\":\"src/pick.c\",\"start_line\":6,"
"\"end_line\":8}]}"};
int64_t pick_id = cbm_store_upsert_node(f.store, &pick);
cbm_node_t plat = {.project = "p",
.label = "Function",
.name = "plat",
.qualified_name = "p.src.plat.plat",
.file_path = "src/plat_linux.c",
.start_line = 1,
.end_line = 3,
.properties_json = "{\"variants\":["
"{\"file_path\":\"src/plat_linux.c\",\"start_line\":1,"
"\"end_line\":3},"
"{\"file_path\":\"src/plat_win.c\",\"start_line\":1,"
"\"end_line\":3}]}"};
int64_t plat_id = cbm_store_upsert_node(f.store, &plat);
cbm_node_t win_file = {.project = "p",
.label = "File",
.name = "plat_win.c",
.qualified_name = "p.src.plat_win.c.__file__",
.file_path = "src/plat_win.c"};
int64_t win_file_id = cbm_store_upsert_node(f.store, &win_file);
cbm_edge_t defines = {
.project = "p", .source_id = win_file_id, .target_id = plat_id, .type = "DEFINES"};
ASSERT_TRUE(pick_id > 0 && plat_id > 0 && win_file_id > 0);
ASSERT_TRUE(cbm_store_insert_edge(f.store, &defines) > 0);
/* The #else branch (lines 6-8). */
static const char *const added[] = {" alpha2();"};
static const char *const removed[] = {" alpha();"};
cbm_diff_hunk_t branch = {.start = 7,
.count = 1,
.added = added,
.added_count = 1,
.removed = removed,
.removed_count = 1};
cbm_ti_change_t change = {.path = "src/pick.c", .hunks = &branch, .hunk_count = 1};
cbm_ti_seeds_t *s = tsd_seed(&f, &change, 1);
ASSERT_NOT_NULL(s);
ASSERT_EQ(tsd_seed_count(s), 1);
ASSERT_TRUE(tsd_seeded(s, pick_id));
cbm_ti_seeds_free(s);
/* The Windows platform file, whose variant lives on plat_linux.c's node. */
static const char *const win_added[] = {" beta2();"};
static const char *const win_removed[] = {" beta();"};
cbm_diff_hunk_t win = {.start = 2,
.count = 1,
.added = win_added,
.added_count = 1,
.removed = win_removed,
.removed_count = 1};
cbm_ti_change_t win_change = {.path = "src/plat_win.c", .hunks = &win, .hunk_count = 1};
s = tsd_seed(&f, &win_change, 1);
ASSERT_NOT_NULL(s);
ASSERT_EQ(tsd_seed_count(s), 1);
ASSERT_TRUE(tsd_seeded(s, plat_id));
cbm_ti_seeds_free(s);
tsd_close(&f);
PASS();
}
SUITE(test_impact_seed) {
RUN_TEST(test_impact_seed_hunks_follow_the_reference);
RUN_TEST(test_impact_seed_variant_spans_seed_their_definition);
RUN_TEST(test_impact_seed_changed_cases_and_suites_are_reported);
RUN_TEST(test_impact_seed_deleted_names_are_found_by_text);
RUN_TEST(test_impact_seed_header_names_propagate_through_includes_and_macros);