mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-25 06:33:13 +08:00
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:
+5
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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, ®_entries);
|
||||
defines_edges += register_and_link_def(ctx, &result->defs.items[d], file_node_id,
|
||||
files[i].language, ®_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
|
||||
|
||||
@@ -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
@@ -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). */
|
||||
|
||||
@@ -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)));
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user