mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge origin/main into fix/permanent-daemon-watch
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
9c836894f6
18 files changed
+1124
-97
No files matched your search
@@ -245,6 +245,15 @@ root because they are too broad or too sensitive to index as a unit:
|
||||
- a credential directory at any depth — `.ssh`, `.aws`, `.gnupg`, `.kube`,
|
||||
`.docker`, `.netrc`, `.git-credentials`, `.password-store`, macOS `Keychains`.
|
||||
|
||||
A single top-level directory can still be indexed as one root on purpose, for
|
||||
example a container or build machine that keeps its project in `/work`, `/app` or
|
||||
`/src`, by approving that exact path in a terminal:
|
||||
`codebase-memory-mcp allow-root --approve-broad /work`. The approval is recorded
|
||||
like any other allowed root, listed as `(approved broad)` by `allow-root --list`,
|
||||
and covers no other top-level directory; as with any recorded root, indexing is
|
||||
then confined to the recorded roots. Filesystem, drive and share roots cannot be
|
||||
approved.
|
||||
|
||||
Two limits are worth stating plainly. This constrains *scope*, not
|
||||
*sensitivity*: inside a root that is allowed, every file the process can read may
|
||||
be indexed and later returned. And the credential list is a denylist, so it
|
||||
|
||||
@@ -1789,9 +1789,25 @@ static uint32_t active_lexical_scope_id(const WalkState *state) {
|
||||
static uint32_t lexical_ancestor_of_kind(const WalkState *state, uint32_t start_id,
|
||||
bool want_function, bool want_block);
|
||||
|
||||
static bool python_default_value_reference(TSNode node) {
|
||||
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
|
||||
parent = ts_node_parent(parent)) {
|
||||
/* One step up: on the occurrence cursor when there is one, else with
|
||||
* ts_node_parent, which descends from the ROOT, so a climb costs O(depth^2). */
|
||||
static TSNode usage_parent_step(TSTreeCursor *cursor, TSNode current) {
|
||||
if (cursor) {
|
||||
return ts_tree_cursor_goto_parent(cursor) ? ts_tree_cursor_current_node(cursor)
|
||||
: (TSNode){0};
|
||||
}
|
||||
usage_slow_parent_fallback_test_note();
|
||||
return ts_node_parent(current);
|
||||
}
|
||||
|
||||
/* Asked for every Python usage, and a deep expression puts O(depth) parents
|
||||
* above each one. A tree without errors climbs them on the walk cursor, like
|
||||
* python_scope_directive_kind; a tree with errors keeps ts_node_parent. */
|
||||
static bool python_default_value_reference(CBMExtractCtx *ctx, WalkState *state, TSNode node) {
|
||||
TSTreeCursor *cursor =
|
||||
ts_node_has_error(ctx->root) ? NULL : reset_occurrence_cursor(state, node);
|
||||
for (TSNode parent = usage_parent_step(cursor, node); !ts_node_is_null(parent);
|
||||
parent = usage_parent_step(cursor, parent)) {
|
||||
const char *kind = ts_node_type(parent);
|
||||
/* A nested executable scope inside the default owns its own lookups;
|
||||
* only a direct default expression is evaluated in the declaring
|
||||
@@ -1812,10 +1828,10 @@ static bool python_default_value_reference(TSNode node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint32_t usage_lexical_scope_id_for_node(CBMExtractCtx *ctx, const WalkState *state,
|
||||
TSNode node) {
|
||||
static uint32_t usage_lexical_scope_id_for_node(CBMExtractCtx *ctx, WalkState *state, TSNode node) {
|
||||
uint32_t scope_id = active_lexical_scope_id(state);
|
||||
if (!ctx || ctx->language != CBM_LANG_PYTHON || !python_default_value_reference(node)) {
|
||||
if (!ctx || ctx->language != CBM_LANG_PYTHON ||
|
||||
!python_default_value_reference(ctx, state, node)) {
|
||||
return scope_id;
|
||||
}
|
||||
uint32_t function_id = lexical_ancestor_of_kind(state, scope_id, true, false);
|
||||
@@ -1891,8 +1907,8 @@ static bool ensure_lexical_binding_capacity(WalkState *state) {
|
||||
}
|
||||
|
||||
static bool lexical_ancestor_kind(TSNode node, const char *kind) {
|
||||
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
|
||||
parent = ts_node_parent(parent)) {
|
||||
for (TSNode parent = usage_parent_step(NULL, node); !ts_node_is_null(parent);
|
||||
parent = usage_parent_step(NULL, parent)) {
|
||||
if (strcmp(ts_node_type(parent), kind) == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -1900,6 +1916,34 @@ static bool lexical_ancestor_kind(TSNode node, const char *kind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Is `node` a name listed by a Python global/nonlocal statement? The grammar
|
||||
* makes those names direct children of the statement, so in a tree without
|
||||
* errors one parent step on the walk cursor answers it. The full climb above
|
||||
* steps with ts_node_parent, which descends from the ROOT each time: O(depth^2)
|
||||
* per identifier, asked for every Python identifier, so one deep arithmetic
|
||||
* expression went cubic. Error recovery can put a name under an ERROR child of
|
||||
* the statement, so a tree with errors keeps the full climb. */
|
||||
static CBMPythonDirectiveKind python_scope_directive_kind(CBMExtractCtx *ctx, WalkState *state,
|
||||
TSNode node) {
|
||||
if (ts_node_has_error(ctx->root)) {
|
||||
if (lexical_ancestor_kind(node, "global_statement")) {
|
||||
return CBM_PYTHON_DIRECTIVE_GLOBAL;
|
||||
}
|
||||
return lexical_ancestor_kind(node, "nonlocal_statement") ? CBM_PYTHON_DIRECTIVE_NONLOCAL
|
||||
: 0;
|
||||
}
|
||||
TSNode parent;
|
||||
const char *field;
|
||||
if (!occurrence_parent(reset_occurrence_cursor(state, node), node, &parent, &field)) {
|
||||
return 0;
|
||||
}
|
||||
const char *kind = ts_node_type(parent);
|
||||
if (strcmp(kind, "global_statement") == 0) {
|
||||
return CBM_PYTHON_DIRECTIVE_GLOBAL;
|
||||
}
|
||||
return strcmp(kind, "nonlocal_statement") == 0 ? CBM_PYTHON_DIRECTIVE_NONLOCAL : 0;
|
||||
}
|
||||
|
||||
static TSNode declared_function_name_owner(TSNode node, const CBMLangSpec *spec) {
|
||||
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
|
||||
parent = ts_node_parent(parent)) {
|
||||
@@ -2246,13 +2290,11 @@ static void record_lexical_binding(CBMExtractCtx *ctx, WalkState *state, TSNode
|
||||
TSNode function_declaration = declared_function_name_owner(node, spec);
|
||||
TSNode class_declaration = declared_class_name_owner(node, spec);
|
||||
|
||||
if (ctx->language == CBM_LANG_PYTHON && (lexical_ancestor_kind(node, "global_statement") ||
|
||||
lexical_ancestor_kind(node, "nonlocal_statement"))) {
|
||||
CBMPythonDirectiveKind scope_directive =
|
||||
ctx->language == CBM_LANG_PYTHON ? python_scope_directive_kind(ctx, state, node) : 0;
|
||||
if (scope_directive) {
|
||||
uint32_t function_id = lexical_ancestor_of_kind(state, current_id, true, false);
|
||||
CBMPythonDirectiveKind directive = lexical_ancestor_kind(node, "global_statement")
|
||||
? CBM_PYTHON_DIRECTIVE_GLOBAL
|
||||
: CBM_PYTHON_DIRECTIVE_NONLOCAL;
|
||||
record_python_directive(state, function_id, name, directive);
|
||||
record_python_directive(state, function_id, name, scope_directive);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2719,8 +2761,7 @@ void handle_usages(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Wal
|
||||
}
|
||||
|
||||
bool python_scope_directive =
|
||||
ctx->language == CBM_LANG_PYTHON && (lexical_ancestor_kind(node, "global_statement") ||
|
||||
lexical_ancestor_kind(node, "nonlocal_statement"));
|
||||
ctx->language == CBM_LANG_PYTHON && python_scope_directive_kind(ctx, state, node) != 0;
|
||||
if (python_scope_directive || is_binding_occurrence(ctx, node, spec, state)) {
|
||||
char *binding_name = reference_name(ctx, node);
|
||||
if (binding_name && binding_name[0] && !cbm_is_keyword(binding_name, ctx->language)) {
|
||||
|
||||
@@ -58,6 +58,12 @@ CBMLanguage cbm_disambiguate_frm(const char *path);
|
||||
* CBM_LANG_RESCRIPT. On read failure, defaults to CBM_LANG_RESCRIPT. */
|
||||
CBMLanguage cbm_disambiguate_res(const char *path);
|
||||
|
||||
/* Disambiguate .d files by reading first 4KB of content.
|
||||
* Returns CBM_LANG_COUNT (not source) for a make-style dependency file written
|
||||
* by rustc/cargo, gcc/clang -MD or CMake ("target ...: prereq ..."), otherwise
|
||||
* CBM_LANG_DLANG. On read failure or any doubt, defaults to CBM_LANG_DLANG. */
|
||||
CBMLanguage cbm_disambiguate_d(const char *path);
|
||||
|
||||
/* Disambiguate .inc files by reading first 4KB of content.
|
||||
* Returns CBM_LANG_OBJECTSCRIPT_ROUTINE if it looks like an ObjectScript
|
||||
* include (a "ROUTINE <Uppercase>" header), otherwise CBM_LANG_BITBAKE.
|
||||
|
||||
@@ -1436,6 +1436,107 @@ CBMLanguage cbm_disambiguate_res(const char *path) {
|
||||
return lang_res_bytes(head, n);
|
||||
}
|
||||
|
||||
/* A make rule's targets end at a ':' followed by whitespace or the end of the
|
||||
* file, which skips drive-letter colons ("C:/", "C:\"). */
|
||||
static bool is_rule_colon(const char *p, bool whole_file) {
|
||||
char next = p[SKIP_ONE];
|
||||
return *p == ':' && (next == ' ' || next == '\t' || next == '\r' || next == '\n' ||
|
||||
(next == '\0' && whole_file));
|
||||
}
|
||||
|
||||
/* Bytes of D whitespace at p, or 0: space, tab, vertical tab, form feed, CR
|
||||
* and the UTF-8 line and paragraph separators U+2028/U+2029, all of which split
|
||||
* D tokens. '\n' ends the line and is the caller's. */
|
||||
static size_t d_space_len(const char *p) {
|
||||
if (*p == ' ' || *p == '\t' || *p == '\v' || *p == '\f' || *p == '\r') {
|
||||
return SKIP_ONE;
|
||||
}
|
||||
if (strncmp(p, "\xE2\x80\xA8", SLEN("\xE2\x80\xA8")) == 0 ||
|
||||
strncmp(p, "\xE2\x80\xA9", SLEN("\xE2\x80\xA9")) == 0) {
|
||||
return SLEN("\xE2\x80\xA8");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* True if the line at p is a make rule "target...: prereq..." whose targets all
|
||||
* look like paths (contain '/', '\' or '.'); a backslash-escaped space stays
|
||||
* inside its target. D source fails this: "public:", "@safe:", "extern(C):"
|
||||
* and "import a.b : c" all have a non-path word or D punctuation before the
|
||||
* colon. An attribute ('@', as in "@1.0:") or a comment between tokens
|
||||
* ("public/+ +/:") is D even with a '.' or '/' before the colon, so a target
|
||||
* holding either is never dep-info. */
|
||||
static bool is_dep_rule_line(const char *p, bool whole_file) {
|
||||
bool in_target = false;
|
||||
bool path_like = false;
|
||||
bool any_target = false;
|
||||
for (; *p && *p != '\n'; p++) {
|
||||
if (is_rule_colon(p, whole_file)) {
|
||||
return in_target ? path_like : any_target;
|
||||
}
|
||||
size_t space = d_space_len(p);
|
||||
if (space) {
|
||||
if (in_target && !path_like) {
|
||||
return false;
|
||||
}
|
||||
in_target = false;
|
||||
path_like = false;
|
||||
p += space - SKIP_ONE;
|
||||
continue;
|
||||
}
|
||||
if (strchr("(){};=\"',@", *p)) {
|
||||
return false;
|
||||
}
|
||||
if (*p == '/' && (p[SKIP_ONE] == '*' || p[SKIP_ONE] == '+' || p[SKIP_ONE] == '/')) {
|
||||
return false;
|
||||
}
|
||||
in_target = true;
|
||||
any_target = true;
|
||||
path_like = path_like || *p == '/' || *p == '\\' || *p == '.';
|
||||
if (*p == '\\' && p[SKIP_ONE] == ' ') {
|
||||
p++; /* escaped space: part of this target */
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Disambiguate .d files: shared by D source and make-style dependency files
|
||||
* written by rustc/cargo (target/<profile>/deps/<crate>-<hash>.d), gcc/clang -MD
|
||||
* and CMake (<object>.o.d). Those are "target: prereq ..." rules, often tens of
|
||||
* KB on one line, which the D grammar parses slowly into nothing but a module
|
||||
* node, so they are reported as unsupported (CBM_LANG_COUNT). Defaults to D on
|
||||
* any doubt (preserves existing behaviour). `n` is the count of head bytes buf
|
||||
* was made from. */
|
||||
static CBMLanguage lang_d_text(const char *buf, size_t n) {
|
||||
/* A colon at the end of buf only ends the rule if nothing was cut off. */
|
||||
bool whole_file = n < LANG_PROBE_HEAD && strlen(buf) == n;
|
||||
|
||||
const char *p = buf;
|
||||
if (strncmp(p, "\xEF\xBB\xBF", SLEN("\xEF\xBB\xBF")) == 0) {
|
||||
p += SLEN("\xEF\xBB\xBF");
|
||||
}
|
||||
while (*p && isspace((unsigned char)*p)) {
|
||||
p++;
|
||||
}
|
||||
/* A shebang or a leading D comment (including the DUB single-file
|
||||
* "/+ dub.sdl:" recipe) is D; dep-info never starts that way. */
|
||||
if (*p == '#' ||
|
||||
(p[0] == '/' && (p[SKIP_ONE] == '/' || p[SKIP_ONE] == '*' || p[SKIP_ONE] == '+'))) {
|
||||
return CBM_LANG_DLANG;
|
||||
}
|
||||
return is_dep_rule_line(p, whole_file) ? CBM_LANG_COUNT : CBM_LANG_DLANG;
|
||||
}
|
||||
|
||||
CBMLanguage cbm_disambiguate_d(const char *path) {
|
||||
unsigned char head[LANG_PROBE_HEAD];
|
||||
size_t n = 0;
|
||||
if (!lang_read_head(path, head, sizeof(head), &n)) {
|
||||
return CBM_LANG_DLANG;
|
||||
}
|
||||
char buf[LANG_PROBE_HEAD + SKIP_ONE];
|
||||
lang_head_text(buf, LANG_PROBE_HEAD, head, n);
|
||||
return lang_d_text(buf, n);
|
||||
}
|
||||
|
||||
/* Disambiguate .cls files: shared by InterSystems ObjectScript UDL, Salesforce
|
||||
* Apex and Visual Basic 6 class modules (#721). ObjectScript class files begin
|
||||
* with a line of the form "Class <UppercasePackage>..."; VB6 class modules
|
||||
@@ -1611,6 +1712,13 @@ static const char *lang_probe_extension(const char *filename) {
|
||||
return (dot && lang_name_in(dot, PROBED)) ? dot : NULL;
|
||||
}
|
||||
|
||||
/* .d is shared by D and make-style dep-info (cargo, gcc -MD). Probe only while
|
||||
* .d maps to D, so a user override to another language wins. */
|
||||
static bool lang_probe_d(CBMLanguage lang, const char *filename) {
|
||||
const char *dot = strrchr(filename, '.');
|
||||
return lang == CBM_LANG_DLANG && dot && strcmp(dot, ".d") == 0;
|
||||
}
|
||||
|
||||
/* "<Export generator=" in the first line-probe bytes: an ObjectScript Studio
|
||||
* export, which is XML by name. */
|
||||
static bool lang_objectscript_export(const unsigned char *head, size_t n) {
|
||||
@@ -1633,6 +1741,9 @@ size_t cbm_language_probe_bytes_with(const cbm_userconfig_t *config, const char
|
||||
if (ext) {
|
||||
return strcmp(ext, ".cfc") == 0 ? LANG_PROBE_CFC : LANG_PROBE_HEAD;
|
||||
}
|
||||
if (lang_probe_d(lang, filename)) {
|
||||
return LANG_PROBE_HEAD;
|
||||
}
|
||||
return lang == CBM_LANG_XML ? LANG_PROBE_LINE : 0;
|
||||
}
|
||||
|
||||
@@ -1676,6 +1787,11 @@ CBMLanguage cbm_language_classify_with(const cbm_userconfig_t *config, const cha
|
||||
/* .res: ReScript, or a binary Godot / Windows resource (#2176) */
|
||||
lang = readable ? lang_res_bytes(head, head_len) : CBM_LANG_RESCRIPT;
|
||||
}
|
||||
} else if (lang_probe_d(lang, filename)) {
|
||||
/* D, or a make-style dep-info file */
|
||||
char buf[LANG_PROBE_HEAD + SKIP_ONE];
|
||||
lang_head_text(buf, LANG_PROBE_HEAD, head, head_len);
|
||||
lang = readable ? lang_d_text(buf, head_len) : CBM_LANG_DLANG;
|
||||
}
|
||||
if (lang == CBM_LANG_XML && readable && lang_objectscript_export(head, head_len)) {
|
||||
return CBM_LANG_OBJECTSCRIPT_EXPORT;
|
||||
|
||||
+109
-40
@@ -392,8 +392,18 @@ enum { WS_LINE_MAX = 4096 };
|
||||
/* A line may carry a leading '!' meaning "a person explicitly approved this
|
||||
* sensitive root". The marker is stored rather than inferred so re-classifying
|
||||
* later (a new credential name is added to the list, say) cannot silently
|
||||
* upgrade an ordinary grant into a sensitive one. */
|
||||
* upgrade an ordinary grant into a sensitive one. A leading '^' is the same
|
||||
* kind of record for a root refused as too broad (allow-root --approve-broad).
|
||||
* Neither character can begin a canonical absolute path, so a marked line can
|
||||
* never be misread as an ordinary grant. */
|
||||
static const char WS_SENSITIVE_MARK = '!';
|
||||
static const char WS_BROAD_MARK = '^';
|
||||
|
||||
typedef enum {
|
||||
WS_GRANT_ORDINARY = 0,
|
||||
WS_GRANT_SENSITIVE, /* explicit approval of a CBM_WS_DENY_SENSITIVE root */
|
||||
WS_GRANT_BROAD, /* explicit approval of a CBM_WS_DENY_TOO_SHALLOW root */
|
||||
} ws_grant_kind_t;
|
||||
|
||||
bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz) {
|
||||
if (!cache_dir || !cache_dir[0] || !out || out_sz == 0) {
|
||||
@@ -406,7 +416,8 @@ bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz) {
|
||||
/* Walk the grant file, invoking visit for each entry. visit returns true to stop.
|
||||
* Returns the number of entries seen. */
|
||||
static int ws_grant_walk(const char *cache_dir,
|
||||
bool (*visit)(const char *root, bool sensitive, void *ctx), void *ctx) {
|
||||
bool (*visit)(const char *root, ws_grant_kind_t kind, void *ctx),
|
||||
void *ctx) {
|
||||
char store[WS_LINE_MAX];
|
||||
if (!cbm_workspace_grant_path(cache_dir, store, sizeof(store))) {
|
||||
return 0;
|
||||
@@ -425,13 +436,18 @@ static int ws_grant_walk(const char *cache_dir,
|
||||
if (len == 0 || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
bool sensitive = line[0] == WS_SENSITIVE_MARK;
|
||||
const char *root = sensitive ? line + 1 : line;
|
||||
ws_grant_kind_t kind = WS_GRANT_ORDINARY;
|
||||
if (line[0] == WS_SENSITIVE_MARK) {
|
||||
kind = WS_GRANT_SENSITIVE;
|
||||
} else if (line[0] == WS_BROAD_MARK) {
|
||||
kind = WS_GRANT_BROAD;
|
||||
}
|
||||
const char *root = kind == WS_GRANT_ORDINARY ? line : line + 1;
|
||||
if (!root[0]) {
|
||||
continue;
|
||||
}
|
||||
seen++;
|
||||
if (visit && visit(root, sensitive, ctx)) {
|
||||
if (visit && visit(root, kind, ctx)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -443,14 +459,21 @@ typedef struct {
|
||||
const char *candidate;
|
||||
bool contained; /* candidate is at or below some granted root */
|
||||
bool exact_sensitive; /* candidate is itself a grant marked sensitive */
|
||||
bool exact_broad; /* candidate is itself a grant marked broad */
|
||||
} ws_match_t;
|
||||
|
||||
static bool ws_match_visit(const char *root, bool sensitive, void *ctx) {
|
||||
static bool ws_match_visit(const char *root, ws_grant_kind_t kind, void *ctx) {
|
||||
ws_match_t *m = ctx;
|
||||
if (cbm_path_within_root(root, m->candidate)) {
|
||||
m->contained = true;
|
||||
if (sensitive && ws_paths_equal(root, m->candidate)) {
|
||||
m->exact_sensitive = true;
|
||||
/* Exact equality, never a prefix: an approval names one root, and must
|
||||
* not quietly cover every root below it. */
|
||||
if (kind != WS_GRANT_ORDINARY && ws_paths_equal(root, m->candidate)) {
|
||||
if (kind == WS_GRANT_SENSITIVE) {
|
||||
m->exact_sensitive = true;
|
||||
} else {
|
||||
m->exact_broad = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; /* visit every entry: a later one may be the exact match */
|
||||
@@ -462,10 +485,15 @@ typedef struct {
|
||||
size_t used;
|
||||
} ws_list_t;
|
||||
|
||||
static bool ws_list_visit(const char *root, bool sensitive, void *ctx) {
|
||||
static bool ws_list_visit(const char *root, ws_grant_kind_t kind, void *ctx) {
|
||||
ws_list_t *l = ctx;
|
||||
int n = snprintf(l->out + l->used, l->out_sz - l->used, "%s%s\n",
|
||||
sensitive ? "(approved) " : "", root);
|
||||
const char *label = "";
|
||||
if (kind == WS_GRANT_SENSITIVE) {
|
||||
label = "(approved) ";
|
||||
} else if (kind == WS_GRANT_BROAD) {
|
||||
label = "(approved broad) ";
|
||||
}
|
||||
int n = snprintf(l->out + l->used, l->out_sz - l->used, "%s%s\n", label, root);
|
||||
if (n > 0 && (size_t)n < l->out_sz - l->used) {
|
||||
l->used += (size_t)n;
|
||||
}
|
||||
@@ -481,9 +509,49 @@ bool cbm_workspace_grant_list(const char *cache_dir, char *out, size_t out_sz) {
|
||||
return ws_grant_walk(cache_dir, ws_list_visit, &l) > 0;
|
||||
}
|
||||
|
||||
/* The allow-root flag that records an explicit approval for a refused verdict,
|
||||
* or NULL when nothing can lift it (absolute denials). */
|
||||
static const char *ws_verdict_approval_flag(cbm_ws_verdict_t verdict) {
|
||||
if (verdict == CBM_WS_DENY_TOO_SHALLOW) {
|
||||
return "--approve-broad";
|
||||
}
|
||||
if (cbm_workspace_verdict_is_overridable(verdict)) {
|
||||
return "--approve-sensitive";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* True when a refused verdict carries the explicit approval that lifts it. Each
|
||||
* flag lifts only its own verdict: --approve-sensitive never widens breadth and
|
||||
* --approve-broad never widens sensitivity; absolute denials lift for neither.
|
||||
* Refuses by default and names the flag, rather than asking a question whose
|
||||
* answer we cannot authenticate. */
|
||||
static bool ws_grant_verdict_approved(cbm_ws_verdict_t verdict, bool approve_sensitive,
|
||||
bool approve_broad, char *err, size_t err_sz) {
|
||||
bool approved = false;
|
||||
if (verdict == CBM_WS_DENY_TOO_SHALLOW) {
|
||||
approved = approve_broad;
|
||||
} else if (cbm_workspace_verdict_is_overridable(verdict)) {
|
||||
approved = approve_sensitive;
|
||||
}
|
||||
if (approved) {
|
||||
return true;
|
||||
}
|
||||
if (err) {
|
||||
const char *flag = ws_verdict_approval_flag(verdict);
|
||||
if (flag) {
|
||||
snprintf(err, err_sz, "%s; re-run with %s if that is intended",
|
||||
cbm_workspace_verdict_reason(verdict), flag);
|
||||
} else {
|
||||
snprintf(err, err_sz, "%s", cbm_workspace_verdict_reason(verdict));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
|
||||
const char *canonical_path, bool approve_sensitive, char *err,
|
||||
size_t err_sz) {
|
||||
const char *canonical_path, bool approve_sensitive, bool approve_broad,
|
||||
char *err, size_t err_sz) {
|
||||
if (err && err_sz) {
|
||||
err[0] = '\0';
|
||||
}
|
||||
@@ -495,33 +563,22 @@ bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
|
||||
}
|
||||
|
||||
cbm_ws_verdict_t verdict = cbm_workspace_classify_root(canonical_path, home_dir, cache_dir);
|
||||
if (verdict != CBM_WS_ALLOW) {
|
||||
if (!cbm_workspace_verdict_is_overridable(verdict)) {
|
||||
if (err) {
|
||||
snprintf(err, err_sz, "%s", cbm_workspace_verdict_reason(verdict));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!approve_sensitive) {
|
||||
/* Refuse by default and name the flag, rather than asking a question
|
||||
* whose answer we cannot authenticate. */
|
||||
if (err) {
|
||||
snprintf(err, err_sz, "%s; re-run with --approve-sensitive if that is intended",
|
||||
cbm_workspace_verdict_reason(verdict));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (verdict != CBM_WS_ALLOW &&
|
||||
!ws_grant_verdict_approved(verdict, approve_sensitive, approve_broad, err, err_sz)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sensitive = verdict == CBM_WS_DENY_SENSITIVE;
|
||||
bool broad = verdict == CBM_WS_DENY_TOO_SHALLOW;
|
||||
|
||||
/* Already present is success, not a duplicate error. A sensitive approval
|
||||
/* Already present is success, not a duplicate error. An explicit approval
|
||||
* is narrower: an old ordinary grant (or an ordinary ancestor) establishes
|
||||
* containment, but does not record the exact human-approved exception that
|
||||
* cbm_workspace_root_allowed requires. Append that exact marked entry once. */
|
||||
ws_match_t existing = {canonical_path, false, false};
|
||||
ws_match_t existing = {canonical_path, false, false, false};
|
||||
(void)ws_grant_walk(cache_dir, ws_match_visit, &existing);
|
||||
if (existing.contained && (!sensitive || existing.exact_sensitive)) {
|
||||
if (existing.contained && (!sensitive || existing.exact_sensitive) &&
|
||||
(!broad || existing.exact_broad)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -539,7 +596,13 @@ bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
|
||||
}
|
||||
return false;
|
||||
}
|
||||
(void)fprintf(f, "%s%s\n", sensitive ? "!" : "", canonical_path);
|
||||
char mark[2] = {'\0', '\0'};
|
||||
if (sensitive) {
|
||||
mark[0] = WS_SENSITIVE_MARK;
|
||||
} else if (broad) {
|
||||
mark[0] = WS_BROAD_MARK;
|
||||
}
|
||||
(void)fprintf(f, "%s%s\n", mark, canonical_path);
|
||||
bool ok = fclose(f) == 0;
|
||||
if (!ok && err) {
|
||||
snprintf(err, err_sz, "cannot write %s", store);
|
||||
@@ -560,7 +623,7 @@ bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir
|
||||
return false;
|
||||
}
|
||||
|
||||
ws_match_t match = {canonical_path, false, false};
|
||||
ws_match_t match = {canonical_path, false, false, false};
|
||||
int grants = ws_grant_walk(cache_dir, ws_match_visit, &match);
|
||||
|
||||
/* A configured root behaves as an additional grant so existing
|
||||
@@ -605,17 +668,23 @@ bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir
|
||||
return true;
|
||||
}
|
||||
/* An explicit human approval recorded for exactly this path is the only thing
|
||||
* that lifts a sensitive refusal. Absolute and shallow refusals cannot be
|
||||
* lifted at all. */
|
||||
* that lifts a sensitive or a too-broad refusal, each by its own marker: a
|
||||
* broad approval says "index this whole tree on purpose", which is not a
|
||||
* statement about credentials, and the reverse. Exact match, never a prefix,
|
||||
* so one broad approval cannot cover every root below it — the "/Users"-style
|
||||
* breadth the depth rule exists to catch. Absolute refusals cannot be lifted. */
|
||||
if (verdict == CBM_WS_DENY_SENSITIVE && match.exact_sensitive) {
|
||||
return true;
|
||||
}
|
||||
if (verdict == CBM_WS_DENY_TOO_SHALLOW && match.exact_broad) {
|
||||
return true;
|
||||
}
|
||||
if (err) {
|
||||
if (cbm_workspace_verdict_is_overridable(verdict)) {
|
||||
const char *flag = ws_verdict_approval_flag(verdict);
|
||||
if (flag) {
|
||||
snprintf(err, err_sz,
|
||||
"%s: %s. To index it anyway, run: codebase-memory-mcp allow-root "
|
||||
"--approve-sensitive %s",
|
||||
canonical_path, cbm_workspace_verdict_reason(verdict), canonical_path);
|
||||
"%s: %s. To index it anyway, run: codebase-memory-mcp allow-root %s %s",
|
||||
canonical_path, cbm_workspace_verdict_reason(verdict), flag, canonical_path);
|
||||
} else {
|
||||
snprintf(err, err_sz, "%s: %s", canonical_path, cbm_workspace_verdict_reason(verdict));
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
|
||||
typedef enum {
|
||||
CBM_WS_ALLOW = 0,
|
||||
/* Fewer path components than the platform minimum — e.g. "/etc", "/home". */
|
||||
/* Fewer path components than the platform minimum — e.g. "/etc", "/home".
|
||||
* Lifted only for one exact path by a recorded broad approval
|
||||
* (allow-root --approve-broad), never by a manifest or a tool call. */
|
||||
CBM_WS_DENY_TOO_SHALLOW,
|
||||
/* A filesystem root, a drive root, a UNC share root, or a path that is or
|
||||
* contains the cbm cache directory. Never overridable. */
|
||||
@@ -42,8 +44,10 @@ cbm_ws_verdict_t cbm_workspace_classify_root(const char *canonical_path, const c
|
||||
/* Stable, human-facing reason for a verdict. Never NULL. */
|
||||
const char *cbm_workspace_verdict_reason(cbm_ws_verdict_t verdict);
|
||||
|
||||
/* True when an explicit human action may override this verdict. Absolute denials
|
||||
* and shallow paths are never overridable; sensitive ones are. */
|
||||
/* True when a sensitive approval (allow-root --approve-sensitive) may override
|
||||
* this verdict. Absolute denials never are. A shallow path is not overridable
|
||||
* in this sense either: only an exact broad approval lifts it, see
|
||||
* cbm_workspace_grant_add. */
|
||||
bool cbm_workspace_verdict_is_overridable(cbm_ws_verdict_t verdict);
|
||||
|
||||
/* Number of path components below the volume root, ignoring repeated and
|
||||
@@ -81,11 +85,13 @@ bool cbm_path_within_root(const char *root_path, const char *abs_path);
|
||||
bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz);
|
||||
|
||||
/* Record canonical_path as an allowed root. approve_sensitive records an explicit
|
||||
* human override for a CBM_WS_DENY_SENSITIVE path; absolute denials and shallow
|
||||
* paths are never grantable. Returns false and fills err on refusal. */
|
||||
* human override for a CBM_WS_DENY_SENSITIVE path; approve_broad records one for a
|
||||
* CBM_WS_DENY_TOO_SHALLOW path. Each flag lifts only its own verdict, and only
|
||||
* for exactly this path. Absolute denials are never grantable. Returns false and
|
||||
* fills err on refusal. */
|
||||
bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
|
||||
const char *canonical_path, bool approve_sensitive, char *err,
|
||||
size_t err_sz);
|
||||
const char *canonical_path, bool approve_sensitive, bool approve_broad,
|
||||
char *err, size_t err_sz);
|
||||
|
||||
/* Newline-separated granted roots into out. True when at least one exists. */
|
||||
bool cbm_workspace_grant_list(const char *cache_dir, char *out, size_t out_sz);
|
||||
|
||||
+11
-4
@@ -1179,21 +1179,27 @@ static void print_help(void) {
|
||||
|
||||
/* Try to handle a subcommand (cli/install/uninstall/update/config/--version/--help).
|
||||
* Returns -1 if no subcommand matched, otherwise the exit code. */
|
||||
/* `allow-root [--approve-sensitive] <path>` — record an indexing root.
|
||||
/* `allow-root [--approve-sensitive | --approve-broad] <path>` — record an
|
||||
* indexing root.
|
||||
*
|
||||
* Enrollment lives here, in a command a person types, and deliberately nowhere
|
||||
* else: the whole point of the grant store is that neither an indexed repository
|
||||
* nor a tool caller can widen its own boundary. A confirmation delivered through
|
||||
* the MCP surface would be answered by the same agent that may have been
|
||||
* influenced, so it would not be a human decision at all. */
|
||||
* influenced, so it would not be a human decision at all. --approve-broad is the
|
||||
* same kind of record for one exact root refused as too broad ("/work", "/app"),
|
||||
* so it is listed, revocable and seen identically by stdio and daemon sessions. */
|
||||
static int main_run_allow_root(int argc, char **argv) {
|
||||
const char *path = NULL;
|
||||
bool approve_sensitive = false;
|
||||
bool approve_broad = false;
|
||||
bool list_only = false;
|
||||
bool approve_manifest = false;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--approve-sensitive") == 0) {
|
||||
approve_sensitive = true;
|
||||
} else if (strcmp(argv[i], "--approve-broad") == 0) {
|
||||
approve_broad = true;
|
||||
} else if (strcmp(argv[i], "--list") == 0) {
|
||||
list_only = true;
|
||||
} else if (strcmp(argv[i], "--approve-manifest") == 0) {
|
||||
@@ -1225,7 +1231,8 @@ static int main_run_allow_root(int argc, char **argv) {
|
||||
}
|
||||
if (!path && !list_only) {
|
||||
(void)fprintf(stderr,
|
||||
"usage: codebase-memory-mcp allow-root [--approve-sensitive] <path>\n"
|
||||
"usage: codebase-memory-mcp allow-root [--approve-sensitive | "
|
||||
"--approve-broad] <path>\n"
|
||||
" codebase-memory-mcp allow-root --approve-manifest <project>\n"
|
||||
" codebase-memory-mcp allow-root --list\n");
|
||||
return EXIT_FAILURE;
|
||||
@@ -1268,7 +1275,7 @@ static int main_run_allow_root(int argc, char **argv) {
|
||||
|
||||
char err[CBM_SZ_1K];
|
||||
if (!cbm_workspace_grant_add(cache_dir, cbm_workspace_home_dir(), canonical, approve_sensitive,
|
||||
err, sizeof(err))) {
|
||||
approve_broad, err, sizeof(err))) {
|
||||
(void)fprintf(stderr, "refused: %s\n", err[0] ? err : "not an allowable root");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
+136
-15
@@ -10,18 +10,23 @@
|
||||
* document says (the field tests' rule for bare file names).
|
||||
*
|
||||
* Into the file: a line range picks the innermost code definition that holds
|
||||
* the WHOLE range; a member (`path::Name`, `path::Owner::name`) picks the one
|
||||
* definition of that name in the file. Otherwise the edge goes to the file,
|
||||
* and the range stays on it ("target_lines"). Every edge is exact: the
|
||||
* document wrote the path.
|
||||
* the WHOLE range; a member (`path::Name`, `path::Owner::name`; a call form
|
||||
* `path::name()` reads as `path::name`) picks the one definition of that name
|
||||
* in the file. Otherwise the edge goes to the file, and the range stays on it
|
||||
* ("target_lines"); so does a member the code file writes without a
|
||||
* definition the graph binds (a macro, a prototype, an enum member, a class
|
||||
* attribute). Every edge is exact: the document wrote the path.
|
||||
*
|
||||
* Unresolved:
|
||||
* missing the path is not in the graph, but its directory is, or a
|
||||
* directory above it that the reference wrote is: the document
|
||||
* names something that is not there (doc rot; a moved or
|
||||
* renamed directory)
|
||||
* renamed directory); or a member whose name the code file
|
||||
* does not write at all
|
||||
* ambiguous a file name alone that is not next to the document but exists
|
||||
* elsewhere in the repository: the document does not say which
|
||||
* elsewhere in the repository: the document does not say which;
|
||||
* or a member name the file defines more than once, with no
|
||||
* owner to pick one
|
||||
* Not a reference to this repository at all (LOCAL: no edge, no row): a link
|
||||
* back into the same document; a path that is neither indexed nor of a type
|
||||
* the indexer reads (an image, an archive, an extensionless page); a path
|
||||
@@ -35,11 +40,14 @@
|
||||
#include "discover/discover.h" /* cbm_language_for_filename */
|
||||
#include "doclink.h"
|
||||
#include "foundation/arena.h"
|
||||
#include "foundation/compat_fs.h" /* cbm_fopen */
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/hash_table.h"
|
||||
#include "foundation/limits.h" /* cbm_max_file_bytes */
|
||||
#include "foundation/mem_core.h"
|
||||
#include "graph_buffer/graph_buffer.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -90,6 +98,7 @@ typedef struct {
|
||||
typedef struct {
|
||||
const char *project;
|
||||
size_t project_len;
|
||||
const char *repo; /* the repository root (NULL: unknown; no file is read) */
|
||||
const char **run_paths; /* run_file -> rel_path (NULL: not a Markdown file of this run) */
|
||||
int run_count;
|
||||
/* base name -> number of File nodes with it (a file name alone that is
|
||||
@@ -287,6 +296,7 @@ static void *mdr_build(const cbm_doclink_build_in_t *in) {
|
||||
cbm_arena_init(&x->arena);
|
||||
x->project = in->ctx ? in->ctx->project_name : NULL;
|
||||
x->project_len = x->project ? strlen(x->project) : 0;
|
||||
x->repo = in->ctx ? in->ctx->repo_path : NULL;
|
||||
x->run_count = in->run_file_count;
|
||||
x->run_paths = in->run_file_count > 0
|
||||
? (const char **)cbm_calloc(CBM_MEM_CLASS_OTHER,
|
||||
@@ -438,14 +448,31 @@ static bool mdr_code_label(const char *label) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The length of the member text as a name: a call form (`name()`,
|
||||
* `Owner.name(a, b)`) ends before its first '('. */
|
||||
static size_t mdr_member_len(const char *member) {
|
||||
size_t n = strlen(member);
|
||||
if (n > 0 && member[n - SKIP_ONE] == ')') {
|
||||
const char *open = memchr(member, '(', n);
|
||||
if (open) {
|
||||
return (size_t)(open - member);
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* The member text's last segment (after "::" or "."), and the one before it. */
|
||||
static void mdr_member_names(const char *member, const char **name, size_t *name_len,
|
||||
const char **owner, size_t *owner_len) {
|
||||
const char *segs[PAIR_LEN] = {NULL, NULL};
|
||||
size_t lens[PAIR_LEN] = {0, 0};
|
||||
const char *s = member;
|
||||
while (*s) {
|
||||
const char *end = member + mdr_member_len(member);
|
||||
while (s < end) {
|
||||
size_t n = strcspn(s, ":.");
|
||||
if (n > (size_t)(end - s)) {
|
||||
n = (size_t)(end - s);
|
||||
}
|
||||
if (n > 0) {
|
||||
segs[0] = segs[SKIP_ONE];
|
||||
lens[0] = lens[SKIP_ONE];
|
||||
@@ -453,7 +480,7 @@ static void mdr_member_names(const char *member, const char **name, size_t *name
|
||||
lens[SKIP_ONE] = n;
|
||||
}
|
||||
s += n;
|
||||
while (*s == ':' || *s == '.') {
|
||||
while (s < end && (*s == ':' || *s == '.')) {
|
||||
s++;
|
||||
}
|
||||
}
|
||||
@@ -499,12 +526,24 @@ static void mdr_file_defs(const mdr_index_t *x, const char *path, int *lo, int *
|
||||
*hi = e;
|
||||
}
|
||||
|
||||
/* How a member lookup ended: found, or why not. */
|
||||
typedef enum {
|
||||
MDR_MEMBER_FOUND = 0,
|
||||
MDR_MEMBER_MISSING, /* no definition of that name in the file */
|
||||
MDR_MEMBER_AMBIGUOUS, /* several, and the owner does not pick one */
|
||||
} mdr_member_t;
|
||||
|
||||
/* The code definition of file `path` that the range or the member names, or
|
||||
* NULL. Read from the index's definitions by file, never from DEFINES edges:
|
||||
* an incremental run's graph holds the edges of the files it re-extracts
|
||||
* only, while every node is there. */
|
||||
* only, while every node is there. *member_result (when not NULL) says how a
|
||||
* member lookup ended. */
|
||||
static const cbm_gbuf_node_t *mdr_segment(const mdr_index_t *x, const char *path, uint32_t first,
|
||||
uint32_t last, const char *member) {
|
||||
uint32_t last, const char *member,
|
||||
mdr_member_t *member_result) {
|
||||
if (member_result) {
|
||||
*member_result = MDR_MEMBER_FOUND;
|
||||
}
|
||||
int lo = 0;
|
||||
int hi = 0;
|
||||
mdr_file_defs(x, path, &lo, &hi);
|
||||
@@ -537,6 +576,9 @@ static const cbm_gbuf_node_t *mdr_segment(const mdr_index_t *x, const char *path
|
||||
size_t owner_len;
|
||||
mdr_member_names(member, &name, &name_len, &owner, &owner_len);
|
||||
if (!name) {
|
||||
if (member_result) {
|
||||
*member_result = MDR_MEMBER_MISSING;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
const cbm_gbuf_node_t *hit = NULL;
|
||||
@@ -558,7 +600,70 @@ static const cbm_gbuf_node_t *mdr_segment(const mdr_index_t *x, const char *path
|
||||
if (owned_hits == SKIP_ONE) {
|
||||
return owned;
|
||||
}
|
||||
return owned_hits == 0 && hits == SKIP_ONE ? hit : NULL;
|
||||
if (owned_hits == 0 && hits == SKIP_ONE) {
|
||||
return hit;
|
||||
}
|
||||
if (member_result) {
|
||||
*member_result = hits == 0 ? MDR_MEMBER_MISSING : MDR_MEMBER_AMBIGUOUS;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* A byte of an identifier: a letter, a digit, '_', and every byte of a
|
||||
* multi-byte character (pipeline_incremental.c doc_row_ident_byte). */
|
||||
static bool mdr_ident_byte(unsigned char c) {
|
||||
return isalnum(c) || c == '_' || c >= CBM_SZ_128;
|
||||
}
|
||||
|
||||
/* Does text[0, n) hold name[0, len) as a whole identifier (case-sensitive)? */
|
||||
static bool mdr_writes_ident(const char *text, size_t n, const char *name, size_t len) {
|
||||
for (size_t i = 0; len > 0 && i + len <= n;) {
|
||||
const char *hit = memchr(text + i, name[0], n - len - i + SKIP_ONE);
|
||||
if (!hit) {
|
||||
return false;
|
||||
}
|
||||
size_t at = (size_t)(hit - text);
|
||||
if (memcmp(hit, name, len) == 0 && (at == 0 || !mdr_ident_byte((unsigned char)hit[-1])) &&
|
||||
(at + len == n || !mdr_ident_byte((unsigned char)hit[len]))) {
|
||||
return true;
|
||||
}
|
||||
i = at + SKIP_ONE;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Does the code file `path` write the member's name at all? A name the graph
|
||||
* holds no definition for (a macro, a prototype, an enum member, a class
|
||||
* attribute) is still there when the file writes it. Read from the file,
|
||||
* never cached (resolvers run on worker threads). True -- no claim -- when
|
||||
* the file cannot be read whole: a member is missing only when the file was
|
||||
* read and does not write its name. */
|
||||
static bool mdr_file_writes(const mdr_index_t *x, const char *path, const char *member) {
|
||||
const char *name;
|
||||
const char *owner;
|
||||
size_t name_len;
|
||||
size_t owner_len;
|
||||
mdr_member_names(member, &name, &name_len, &owner, &owner_len);
|
||||
char abs_path[CBM_PATH_MAX];
|
||||
int w = name && x->repo ? snprintf(abs_path, sizeof(abs_path), "%s/%s", x->repo, path) : 0;
|
||||
if (w <= 0 || (size_t)w >= sizeof(abs_path)) {
|
||||
return true;
|
||||
}
|
||||
FILE *f = cbm_fopen(abs_path, "rb");
|
||||
if (!f) {
|
||||
return true;
|
||||
}
|
||||
long size = fseek(f, 0, SEEK_END) == 0 ? ftell(f) : CBM_NOT_FOUND;
|
||||
char *text = size >= 0 && size <= cbm_max_file_bytes() && fseek(f, 0, SEEK_SET) == 0
|
||||
? (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, (size_t)size + SKIP_ONE)
|
||||
: NULL;
|
||||
bool writes = true;
|
||||
if (text && fread(text, SKIP_ONE, (size_t)size, f) == (size_t)size) {
|
||||
writes = mdr_writes_ident(text, (size_t)size, name, name_len);
|
||||
}
|
||||
(void)fclose(f);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, text);
|
||||
return writes;
|
||||
}
|
||||
|
||||
/* Is `path` of a file type the indexer reads? */
|
||||
@@ -1183,9 +1288,24 @@ static void mdr_resolve_path(const mdr_index_t *x, const cbm_gbuf_t *graph, cons
|
||||
out->kind = CBM_DOCLINK_LOCAL; /* a link into the same document */
|
||||
return;
|
||||
}
|
||||
const cbm_gbuf_node_t *seg = (r->first > 0 || r->member)
|
||||
? mdr_segment(x, t, r->first, r->last, r->member)
|
||||
: NULL;
|
||||
mdr_member_t member_result = MDR_MEMBER_FOUND;
|
||||
const cbm_gbuf_node_t *seg =
|
||||
(r->first > 0 || r->member)
|
||||
? mdr_segment(x, t, r->first, r->last, r->member, &member_result)
|
||||
: NULL;
|
||||
/* A named member that a code file does not define is no coarser
|
||||
* link to the file: it is a stale or unclear reference. One the
|
||||
* file writes without a definition the graph binds (a macro, a
|
||||
* prototype, an enum member) is there, and keeps the file edge.
|
||||
* A document's own headings are not definitions, so members of
|
||||
* other files keep the file edge too. */
|
||||
if (!seg && r->member && member_result != MDR_MEMBER_FOUND && mdr_code_file(t) &&
|
||||
(member_result == MDR_MEMBER_AMBIGUOUS || !mdr_file_writes(x, t, r->member))) {
|
||||
out->kind = CBM_DOCLINK_UNRESOLVED;
|
||||
out->reason = member_result == MDR_MEMBER_AMBIGUOUS ? CBM_DOCLINK_REASON_AMBIGUOUS
|
||||
: CBM_DOCLINK_REASON_MISSING;
|
||||
return;
|
||||
}
|
||||
mdr_edge(out, seg ? seg : file, r->first, r->last);
|
||||
return;
|
||||
}
|
||||
@@ -1425,7 +1545,8 @@ static void mdr_resolve(const void *index, void *state, int run_file, const CBMD
|
||||
|
||||
const cbm_gbuf_node_t *cbm_doclink_md_segment(const void *md_index, const char *path,
|
||||
uint32_t first, uint32_t last, const char *member) {
|
||||
return md_index ? mdr_segment((const mdr_index_t *)md_index, path, first, last, member) : NULL;
|
||||
return md_index ? mdr_segment((const mdr_index_t *)md_index, path, first, last, member, NULL)
|
||||
: NULL;
|
||||
}
|
||||
|
||||
/* ── A region of a file's text (the field test's H8 bind_range) ─────── */
|
||||
|
||||
@@ -1582,7 +1582,12 @@ static int surface_added_names(const char *stored_json, const char *fresh_json,
|
||||
* change to it is GLOBAL (it sets the global usings of a whole project);
|
||||
* - a REMOVED name can make another file's unresolved reference resolve (an
|
||||
* overload group shrinks) or change its reason: the files whose rows
|
||||
* mention it re-resolve with the closure. */
|
||||
* mention it re-resolve with the closure;
|
||||
* - an unresolved `path::member` row (missing, ambiguous) depends on the
|
||||
* member's file as a whole, its text included (a member the file writes
|
||||
* without a definition is no missing member, doc_links_md.c) and the size
|
||||
* of a same-named group: the files whose such rows name a changed or
|
||||
* deleted file, by its base name, re-resolve with the closure. */
|
||||
|
||||
/* Add s[0..n) to a heap-keyed name set. false when it cannot be recorded (too
|
||||
* long for a key, or out of memory): callers fail closed. */
|
||||
@@ -1730,6 +1735,58 @@ static void doc_row_name_dependents(const cbm_doc_link_row_t *rows, int n,
|
||||
}
|
||||
}
|
||||
|
||||
/* Is an unresolved row's reason one a member lookup gives (doc_links_md.c)? */
|
||||
static bool doc_row_member_reason(const char *reason) {
|
||||
return reason && (strcmp(reason, "missing") == 0 || strcmp(reason, "ambiguous") == 0);
|
||||
}
|
||||
|
||||
/* Files whose unresolved `path::member` rows name a member of one of `paths`
|
||||
* (the changed and deleted files), and no edge leads from such a row to the
|
||||
* file: a body edit can write or drop a missing member's name without
|
||||
* changing a definition, and a same-named group can shrink to one without
|
||||
* removing its name. Matched by the base name before "::", since a
|
||||
* reference may be written relative to the document or to the root. When
|
||||
* memory runs out every such row's file is added: re-resolving a file that did
|
||||
* not need it costs time, keeping a stale row costs correctness. Keys are
|
||||
* borrowed from `rows`. */
|
||||
static void doc_row_member_dependents(const cbm_doc_link_row_t *rows, int n, char *const *paths,
|
||||
int npaths, CBMHashTable *out_paths) {
|
||||
if (npaths <= 0) {
|
||||
return;
|
||||
}
|
||||
CBMHashTable *bases = cbm_ht_create(CBM_SZ_64); /* NULL: fail closed, every member row */
|
||||
for (int i = 0; bases && i < npaths; i++) {
|
||||
const char *slash = strrchr(paths[i], '/');
|
||||
const char *base = slash ? slash + SKIP_ONE : paths[i];
|
||||
cbm_ht_set(bases, base, (void *)base); /* borrowed from `paths` */
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char *raw = rows[i].raw;
|
||||
const char *rel = rows[i].rel_path;
|
||||
const char *sep = raw ? strstr(raw, "::") : NULL;
|
||||
if (!sep || !rel || !rel[0] || !doc_row_member_reason(rows[i].reason) ||
|
||||
cbm_ht_get(out_paths, rel)) {
|
||||
continue;
|
||||
}
|
||||
const char *base = sep;
|
||||
while (base > raw && base[-1] != '/') {
|
||||
base--;
|
||||
}
|
||||
char key[CBM_SZ_512];
|
||||
size_t bl = (size_t)(sep - base);
|
||||
bool hit = !bases || bl >= sizeof(key);
|
||||
if (!hit && bl > 0) {
|
||||
memcpy(key, base, bl);
|
||||
key[bl] = '\0';
|
||||
hit = cbm_ht_get(bases, key) != NULL;
|
||||
}
|
||||
if (hit) {
|
||||
cbm_ht_set(out_paths, rel, (void *)rel);
|
||||
}
|
||||
}
|
||||
cbm_ht_free(bases);
|
||||
}
|
||||
|
||||
/* Doc-link carry-forward state of the legacy partial route: the previous
|
||||
* rows, the stored scopes of the files it does not re-extract, and the set
|
||||
* of re-extracted or deleted paths whose old rows it replaces. */
|
||||
@@ -3178,8 +3235,11 @@ static int closure_try_plan(cbm_pipeline_t *p, cbm_store_t *store, const char *p
|
||||
}
|
||||
}
|
||||
/* Files whose unresolved doc-link references name something a changed or
|
||||
* deleted file no longer declares re-resolve too. */
|
||||
* deleted file no longer declares re-resolve too, and so do the files
|
||||
* whose unresolved `path::member` rows name a changed or deleted file. */
|
||||
doc_row_name_dependents(plan->doc_rows, plan->doc_row_count, removed_names, row_deps);
|
||||
doc_row_member_dependents(plan->doc_rows, plan->doc_row_count, changed_paths,
|
||||
n_changed + n_deleted, row_deps);
|
||||
{
|
||||
row_dep_walk_t walk = {.closure = closure_set, .files = files_by_path, .added = 0};
|
||||
cbm_ht_foreach(row_deps, row_dep_visitor, &walk);
|
||||
|
||||
@@ -963,6 +963,59 @@ TEST(repro_python_global_assignment_is_module_binding) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A global/nonlocal name is a directive, not a read (each name of `global a,
|
||||
* b`), and the rebinding lands where it says: the module from that point on,
|
||||
* or the enclosing function. Source 1 is an error tree, whose directive and
|
||||
* default-value checks take the ts_node_parent fallback. */
|
||||
TEST(repro_python_scope_directives_bind_the_named_scope) {
|
||||
static const char *const sources[] = {
|
||||
"def handler():\n pass\n"
|
||||
"def outer():\n handler = None\n"
|
||||
" def inner():\n nonlocal handler\n handler = outer\n"
|
||||
"def early():\n accept_early(handler)\n"
|
||||
"def caller():\n def nested():\n global handler, other\n"
|
||||
" handler = outer\n accept_outer(handler)\n"
|
||||
"def sibling():\n accept_sibling(handler)\n",
|
||||
"def handler():\n pass\n"
|
||||
"def make(handler=handler):\n return handler\n"
|
||||
"def caller():\n global handler other\n handler = make\n make(handler)\n",
|
||||
};
|
||||
static const struct {
|
||||
int source;
|
||||
const char *marker, *name;
|
||||
int count, blocked, shadow;
|
||||
} sites[] = {
|
||||
{0, "global handler", "handler", 0, 0, 0},
|
||||
{0, ", other", "other", 0, 0, 0},
|
||||
{0, "nonlocal handler", "handler", 0, 0, 0},
|
||||
{0, "accept_early(handler)", "handler", 1, 0, 0},
|
||||
{0, "accept_outer(handler)", "handler", 1, 1, 0},
|
||||
{0, "accept_sibling(handler)", "handler", 1, 1, 0},
|
||||
{1, "global handler", "handler", 0, 0, 0},
|
||||
{1, "=handler", "handler", 1, 0, 0},
|
||||
{1, "make(handler)", "handler", 1, 1, 0},
|
||||
};
|
||||
int failures = 0;
|
||||
for (int i = 0; i < (int)(sizeof(sites) / sizeof(sites[0])); i++) {
|
||||
const char *source = sources[sites[i].source];
|
||||
uint32_t start = lb_identifier_offset(source, sites[i].marker, sites[i].name);
|
||||
ASSERT_NEQ(start, UINT32_MAX);
|
||||
CBMFileResult *result = lb_extract(source, CBM_LANG_PYTHON, "directives.py");
|
||||
ASSERT_NOT_NULL(result);
|
||||
ASSERT_EQ(result->parse_incomplete, sites[i].source);
|
||||
LBBindingSite got = lb_binding_site(result, NULL, sites[i].name, start);
|
||||
cbm_free_result(result);
|
||||
if (got.count != sites[i].count || got.blocked_count != sites[i].blocked ||
|
||||
got.local_shadow_count != sites[i].shadow) {
|
||||
printf(" %s: count %d blocked %d shadow %d\n", sites[i].marker, got.count,
|
||||
got.blocked_count, got.local_shadow_count);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Module assignments participate in name lookup too. A later function must
|
||||
* not raw-name-fallback through a module variable to an older same-named
|
||||
* callable declaration. */
|
||||
@@ -1055,6 +1108,7 @@ SUITE(repro_lexical_binding_precision) {
|
||||
RUN_TEST(repro_objectscript_udl_set_target_binds_method_local);
|
||||
RUN_TEST(repro_python_default_expression_uses_enclosing_namespace);
|
||||
RUN_TEST(repro_python_global_assignment_is_module_binding);
|
||||
RUN_TEST(repro_python_scope_directives_bind_the_named_scope);
|
||||
RUN_TEST(repro_python_module_assignment_blocks_callable_fallback);
|
||||
RUN_TEST(repro_go_function_literal_parameter_does_not_leak);
|
||||
}
|
||||
@@ -2643,8 +2643,9 @@ TEST(daemon_application_sensitive_root_blocks_auto_index_but_preserves_controls)
|
||||
app_wait_for_active_jobs(application, 0);
|
||||
|
||||
char grant_error[APP_TEST_PATH_CAP];
|
||||
bool approved = ordinary_reaped && cbm_workspace_grant_add(cache, sensitive, sensitive, true,
|
||||
grant_error, sizeof(grant_error));
|
||||
bool approved =
|
||||
ordinary_reaped && cbm_workspace_grant_add(cache, sensitive, sensitive, true, false,
|
||||
grant_error, sizeof(grant_error));
|
||||
bool approved_initialized =
|
||||
approved && app_test_initialize_profile(&callbacks, approved_session, sensitive,
|
||||
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL);
|
||||
@@ -2769,8 +2770,9 @@ TEST(daemon_application_sensitive_root_blocks_watch_but_preserves_controls) {
|
||||
bool ordinary_released = ordinary_watched && cbm_watcher_watch_count(watcher) == 0;
|
||||
|
||||
char grant_error[APP_TEST_PATH_CAP];
|
||||
bool approved = ordinary_released && cbm_workspace_grant_add(cache, sensitive, sensitive, true,
|
||||
grant_error, sizeof(grant_error));
|
||||
bool approved =
|
||||
ordinary_released && cbm_workspace_grant_add(cache, sensitive, sensitive, true, false,
|
||||
grant_error, sizeof(grant_error));
|
||||
bool approved_initialized =
|
||||
approved && app_test_initialize_profile(&callbacks, approved_session, sensitive,
|
||||
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "test_framework.h"
|
||||
#include "test_helpers.h"
|
||||
#include "discover/discover.h"
|
||||
#include "discover/userconfig.h"
|
||||
#include "discover/discover_policy_internal.h"
|
||||
#include "foundation/platform.h"
|
||||
|
||||
@@ -2444,6 +2445,49 @@ TEST(shebang_oversized_first_line_unindexed) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── .d: D source vs make/cargo dep-info ───────────────────────── */
|
||||
|
||||
/* rustc/cargo dep-info (and gcc -MD, CMake .o.d) files share .d with D source
|
||||
* but are Makefile rules the D grammar parses slowly into nothing useful, so
|
||||
* they must not be indexed as D. */
|
||||
static const char CARGO_DEP_INFO[] = "/home/u/proj/t/debug/deps/foo-abc123.d: src/lib.rs src/a.rs\n"
|
||||
"\n"
|
||||
"src/lib.rs:\n"
|
||||
"src/a.rs:\n";
|
||||
|
||||
TEST(discover_d_dep_info_not_indexed_as_dlang) {
|
||||
CBMLanguage dep;
|
||||
CBMLanguage src;
|
||||
ASSERT(shebang_probe("cbm_disc_dotd", "t/debug/deps/foo-abc123.d", CARGO_DEP_INFO, &dep));
|
||||
ASSERT(shebang_probe("cbm_disc_dotd", "app.d", "module app;\nimport std.stdio : writeln;\n",
|
||||
&src));
|
||||
ASSERT_EQ(dep, CBM_LANG_COUNT);
|
||||
ASSERT_EQ(src, CBM_LANG_DLANG);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The probe runs only while .d maps to D, so a user override to another
|
||||
* language wins. */
|
||||
TEST(discover_d_user_override_skips_dep_info_probe) {
|
||||
char *cfg_dir = th_mktempdir("cbm_disc_dotd_uc");
|
||||
ASSERT(cfg_dir != NULL);
|
||||
ASSERT_EQ(th_write_file(TH_PATH(cfg_dir, ".codebase-memory.json"),
|
||||
"{\"extra_extensions\":{\".d\":\"makefile\"}}"),
|
||||
0);
|
||||
cbm_userconfig_t *cfg = cbm_userconfig_load(cfg_dir);
|
||||
th_cleanup(cfg_dir);
|
||||
ASSERT_NOT_NULL(cfg);
|
||||
|
||||
cbm_set_user_lang_config(cfg);
|
||||
CBMLanguage lang;
|
||||
bool ok = shebang_probe("cbm_disc_dotd_uc", "foo-abc123.d", CARGO_DEP_INFO, &lang);
|
||||
cbm_set_user_lang_config(NULL);
|
||||
cbm_userconfig_free(cfg);
|
||||
ASSERT(ok);
|
||||
ASSERT_EQ(lang, CBM_LANG_MAKEFILE);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite ─────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(discover) {
|
||||
@@ -2539,6 +2583,10 @@ SUITE(discover) {
|
||||
RUN_TEST(shebang_embedded_nul_unindexed);
|
||||
RUN_TEST(shebang_oversized_first_line_unindexed);
|
||||
|
||||
/* .d: D source vs make/cargo dep-info */
|
||||
RUN_TEST(discover_d_dep_info_not_indexed_as_dlang);
|
||||
RUN_TEST(discover_d_user_override_skips_dep_info_probe);
|
||||
|
||||
/* Integration tests (cross-platform) */
|
||||
RUN_TEST(discover_simple);
|
||||
RUN_TEST(discover_wide_sibling_fanout_exceeds_initial_walk_stack);
|
||||
|
||||
@@ -388,6 +388,166 @@ TEST(doc_links_md_resolve) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2587: a `path::member` reference whose member the code file does not
|
||||
* define was an exact edge to the file, so a renamed or deleted definition
|
||||
* looked like a valid, coarser link. It is a missing reference; a member the
|
||||
* file defines more than once, with no owner to pick one, is ambiguous. */
|
||||
TEST(doc_links_md_member_not_defined_issue2587) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlmd_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
|
||||
th_write_file(TH_PATH(tmp, "docs/members.md"),
|
||||
"# Members\n"
|
||||
"\n"
|
||||
"See `src/util/rounding.py::clamp` and "
|
||||
"`src/util/rounding.py::no_such_function`.\n"
|
||||
"Areas: `src/shapes.py::Square::area`, `src/shapes.py::area`.\n"
|
||||
"Notes: `docs/other.md::Intro`.\n");
|
||||
th_write_file(TH_PATH(tmp, "docs/other.md"), "# Intro\n\nText.\n");
|
||||
th_write_file(TH_PATH(tmp, "src/util/rounding.py"), "def clamp(x, lo, hi):\n"
|
||||
" return max(lo, min(hi, x))\n");
|
||||
th_write_file(TH_PATH(tmp, "src/shapes.py"), "class Circle:\n"
|
||||
" def area(self):\n"
|
||||
" return 3\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"class Square:\n"
|
||||
" def area(self):\n"
|
||||
" return 4\n");
|
||||
char db[512];
|
||||
snprintf(db, sizeof(db), "%s/md.db", tmp);
|
||||
ASSERT_EQ(dm_index(tmp, db, NULL), 0);
|
||||
char props[512];
|
||||
char reason[64];
|
||||
|
||||
/* a member the file defines, alone or through its owner */
|
||||
ASSERT_EQ(md_edge(db, "Members", "rounding.clamp", props, sizeof(props)), 1);
|
||||
ASSERT_EQ(md_edge(db, "Members", "Square.area", props, sizeof(props)), 1);
|
||||
ASSERT_EQ(md_edge(db, "Members", "Circle.area", props, sizeof(props)), 0);
|
||||
|
||||
/* a member the file does not define: no edge to the file, a missing row */
|
||||
ASSERT_EQ(dm_count(db, "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.type = 'MENTIONS' "
|
||||
"AND s.qualified_name LIKE '%.Members' AND t.label = 'File' "
|
||||
"AND t.file_path = 'src/util/rounding.py'"),
|
||||
0);
|
||||
dm_row(db, "docs/members.md", "src/util/rounding.py::no_such_function", reason, sizeof(reason),
|
||||
NULL, 0);
|
||||
ASSERT_STR_EQ(reason, "missing");
|
||||
|
||||
/* defined twice and no owner written: ambiguous, not the file */
|
||||
dm_row(db, "docs/members.md", "src/shapes.py::area", reason, sizeof(reason), NULL, 0);
|
||||
ASSERT_STR_EQ(reason, "ambiguous");
|
||||
ASSERT_EQ(dm_count(db, "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.type = 'MENTIONS' "
|
||||
"AND s.qualified_name LIKE '%.Members' AND t.label = 'File' "
|
||||
"AND t.file_path = 'src/shapes.py'"),
|
||||
0);
|
||||
|
||||
/* a document's headings are no definitions: the file edge stays */
|
||||
ASSERT_EQ(dm_count(db, "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.type = 'MENTIONS' "
|
||||
"AND s.qualified_name LIKE '%.Members' AND t.label = 'File' "
|
||||
"AND t.file_path = 'docs/other.md'"),
|
||||
1);
|
||||
|
||||
th_cleanup(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* MENTIONS edges from the section whose qualified name ends with `section` to
|
||||
* the File node of `file`. */
|
||||
static int md_file_edges(const char *db, const char *section, const char *file) {
|
||||
char sql[512];
|
||||
snprintf(sql, sizeof(sql),
|
||||
"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.type = 'MENTIONS' "
|
||||
"AND s.qualified_name LIKE '%%.%s' AND t.label = 'File' AND t.file_path = '%s'",
|
||||
section, file);
|
||||
return dm_count(db, sql);
|
||||
}
|
||||
|
||||
/* #2587, the members a code file has although the graph defines no node for
|
||||
* them: a call form, a macro, a prototype, a namespace, an enum member, a
|
||||
* class attribute. Each is there -- the file writes it -- so it keeps its
|
||||
* edge; only a name the file does not write at all is missing. */
|
||||
static const struct {
|
||||
const char *section;
|
||||
const char *raw;
|
||||
const char *file;
|
||||
bool missing;
|
||||
} MD_WRITTEN[] = {
|
||||
{"CallForm", "src/util.py::clamp()", NULL, false},
|
||||
{"Macro", "include/limits.h::MAX_ITEMS", "include/limits.h", false},
|
||||
{"FunctionMacro", "include/limits.h::SQUARE", "include/limits.h", false},
|
||||
{"Prototype", "include/limits.h::limits_init", "include/limits.h", false},
|
||||
{"Namespace", "src/ns.ts::Shapes", "src/ns.ts", false},
|
||||
{"EnumMember", "src/color.py::Color::RED", "src/color.py", false},
|
||||
{"Attribute", "src/model.py::Model::name", "src/model.py", false},
|
||||
/* not written: `blue` and LIGHT_BLUE are no BLUE */
|
||||
{"NotWritten", "src/color.py::Color::BLUE", "src/color.py", true},
|
||||
{"NotDefined", "src/util.py::no_such_function", "src/util.py", true},
|
||||
};
|
||||
|
||||
TEST(doc_links_md_member_written_issue2587) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlmd_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
|
||||
char doc[2048] = "";
|
||||
size_t used = 0;
|
||||
for (size_t i = 0; i < sizeof(MD_WRITTEN) / sizeof(MD_WRITTEN[0]); i++) {
|
||||
used += (size_t)snprintf(doc + used, sizeof(doc) - used, "# %s\n\nSee `%s`.\n\n",
|
||||
MD_WRITTEN[i].section, MD_WRITTEN[i].raw);
|
||||
ASSERT(used < sizeof(doc));
|
||||
}
|
||||
th_write_file(TH_PATH(tmp, "docs/written.md"), doc);
|
||||
th_write_file(TH_PATH(tmp, "src/util.py"), "def clamp(x, lo, hi):\n"
|
||||
" return max(lo, min(hi, x))\n");
|
||||
th_write_file(TH_PATH(tmp, "include/limits.h"), "#ifndef LIMITS_H\n"
|
||||
"#define LIMITS_H\n"
|
||||
"#define MAX_ITEMS 64\n"
|
||||
"#define SQUARE(x) ((x) * (x))\n"
|
||||
"int limits_init(void);\n"
|
||||
"#endif\n");
|
||||
th_write_file(TH_PATH(tmp, "src/ns.ts"), "export namespace Shapes {\n"
|
||||
" export const unit = 1;\n"
|
||||
"}\n");
|
||||
th_write_file(TH_PATH(tmp, "src/color.py"), "from enum import Enum\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"class Color(Enum):\n"
|
||||
" RED = 1\n"
|
||||
" LIGHT_BLUE = 2 # blue\n");
|
||||
th_write_file(TH_PATH(tmp, "src/model.py"), "class Model:\n"
|
||||
" name: str\n");
|
||||
char db[512];
|
||||
snprintf(db, sizeof(db), "%s/md.db", tmp);
|
||||
ASSERT_EQ(dm_index(tmp, db, NULL), 0);
|
||||
char props[512];
|
||||
char reason[64];
|
||||
|
||||
/* every reference, under its own heading: an edge, or a missing row */
|
||||
int wrong = 0;
|
||||
for (size_t i = 0; i < sizeof(MD_WRITTEN) / sizeof(MD_WRITTEN[0]); i++) {
|
||||
dm_row(db, "docs/written.md", MD_WRITTEN[i].raw, reason, sizeof(reason), NULL, 0);
|
||||
int edges = MD_WRITTEN[i].file
|
||||
? md_file_edges(db, MD_WRITTEN[i].section, MD_WRITTEN[i].file)
|
||||
: dm_mentions_from(db, MD_WRITTEN[i].section);
|
||||
bool ok = MD_WRITTEN[i].missing ? strcmp(reason, "missing") == 0 && edges == 0
|
||||
: reason[0] == '\0' && edges >= 1;
|
||||
if (!ok) {
|
||||
printf(" %s: row '%s', %d edge(s)\n", MD_WRITTEN[i].raw, reason, edges);
|
||||
wrong++;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(wrong, 0);
|
||||
/* the call form binds the function itself */
|
||||
ASSERT_EQ(md_edge(db, "CallForm", "util.clamp", props, sizeof(props)), 1);
|
||||
|
||||
th_cleanup(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── incremental == full ─────────────────────────────────────────── */
|
||||
|
||||
TEST(doc_links_md_incremental) {
|
||||
@@ -444,6 +604,76 @@ TEST(doc_links_md_incremental) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2587 incremental == full: whether a `path::member` row is missing depends
|
||||
* on the member file's text, and the row leads to that file by no edge. A
|
||||
* body edit that writes the name (no definition added) turns the row into a
|
||||
* file edge, one that drops it turns the edge back into the row; a same-named
|
||||
* group that shrinks to one turns an ambiguous row into an edge. The group is
|
||||
* Ruby: a Python file whose surface changes takes the full route (the Python
|
||||
* namespace admission), and a language with a doc-link scope (Rust, Go, ...)
|
||||
* reports the lost method as a removed name, so neither reaches the rule for
|
||||
* ambiguous member rows. */
|
||||
TEST(doc_links_md_member_missing_incremental) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlmd_inc_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
|
||||
char repo[512];
|
||||
snprintf(repo, sizeof(repo), "%s/repo", tmp);
|
||||
th_write_file(TH_PATH(repo, "docs/helpers.md"), "# Helpers\n\nSee `src/tools.py::helper2`.\n");
|
||||
th_write_file(TH_PATH(repo, "docs/areas.md"), "# Areas\n\nSee `src/shapes.rb::area`.\n");
|
||||
th_write_file(TH_PATH(repo, "src/tools.py"), "def helper():\n"
|
||||
" return 1\n");
|
||||
th_write_file(TH_PATH(repo, "src/shapes.rb"), "class Circle\n"
|
||||
" def area\n"
|
||||
" 3\n"
|
||||
" end\n"
|
||||
"end\n"
|
||||
"\n"
|
||||
"class Square\n"
|
||||
" def area\n"
|
||||
" 4\n"
|
||||
" end\n"
|
||||
"end\n");
|
||||
char db[512];
|
||||
char full_db[512];
|
||||
snprintf(db, sizeof(db), "%s/inc.db", tmp);
|
||||
snprintf(full_db, sizeof(full_db), "%s/full.db", tmp);
|
||||
ASSERT_EQ(dm_index(repo, db, NULL), 0);
|
||||
char reason[64];
|
||||
dm_row(db, "docs/helpers.md", "src/tools.py::helper2", reason, sizeof(reason), NULL, 0);
|
||||
ASSERT_STR_EQ(reason, "missing");
|
||||
dm_row(db, "docs/areas.md", "src/shapes.rb::area", reason, sizeof(reason), NULL, 0);
|
||||
ASSERT_STR_EQ(reason, "ambiguous");
|
||||
|
||||
/* the body writes the name, no definition added: the row becomes an edge */
|
||||
th_write_file(TH_PATH(repo, "src/tools.py"), "def helper():\n"
|
||||
" return 1 # helper2 follows\n");
|
||||
ASSERT_EQ(dm_step(repo, db, full_db, "member written", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR),
|
||||
0);
|
||||
ASSERT_EQ(md_file_edges(db, "Helpers", "src/tools.py"), 1);
|
||||
/* and drops it again: missing again */
|
||||
th_write_file(TH_PATH(repo, "src/tools.py"), "def helper():\n"
|
||||
" return 1\n");
|
||||
ASSERT_EQ(dm_step(repo, db, full_db, "member dropped", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR),
|
||||
0);
|
||||
dm_row(db, "docs/helpers.md", "src/tools.py::helper2", reason, sizeof(reason), NULL, 0);
|
||||
ASSERT_STR_EQ(reason, "missing");
|
||||
/* one of two `area` methods goes, the name stays: the one left binds */
|
||||
th_write_file(TH_PATH(repo, "src/shapes.rb"), "class Circle\n"
|
||||
"end\n"
|
||||
"\n"
|
||||
"class Square\n"
|
||||
" def area\n"
|
||||
" 4\n"
|
||||
" end\n"
|
||||
"end\n");
|
||||
ASSERT_EQ(dm_step(repo, db, full_db, "group shrinks", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
|
||||
char props[512];
|
||||
ASSERT_EQ(md_edge(db, "Areas", "Square.area", props, sizeof(props)), 1);
|
||||
th_cleanup(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2588: a reference into a directory that was moved or renamed named a
|
||||
* path whose directory is gone, so it counted as no reference to this
|
||||
* repository: no edge and no row. A directory above it that the reference
|
||||
@@ -914,7 +1144,10 @@ SUITE(doc_links_md) {
|
||||
RUN_TEST(doc_links_md_repeated_headings);
|
||||
RUN_TEST(doc_links_md_classify_span);
|
||||
RUN_TEST(doc_links_md_resolve);
|
||||
RUN_TEST(doc_links_md_member_not_defined_issue2587);
|
||||
RUN_TEST(doc_links_md_member_written_issue2587);
|
||||
RUN_TEST(doc_links_md_incremental);
|
||||
RUN_TEST(doc_links_md_member_missing_incremental);
|
||||
RUN_TEST(doc_links_md_moved_directory_issue2588);
|
||||
RUN_TEST(doc_links_md_adr);
|
||||
RUN_TEST(doc_links_md_adr_label_choice);
|
||||
|
||||
@@ -10933,6 +10933,49 @@ TEST(extract_rescript_let_bindings_use_the_walk_cursor) {
|
||||
ASSERT_EQ(big_fallbacks, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Every Python usage asks whether it is a global/nonlocal name and whether it
|
||||
* sits in a parameter default. Both climbed with ts_node_parent, which
|
||||
* descends from the ROOT: O(d^2) for a usage d deep, so one deep expression
|
||||
* went cubic (2.2 s / 17 s / >90 s at 500/1000/2000 terms). Count every such
|
||||
* root-descending step at N and 2N terms. */
|
||||
TEST(extract_python_deep_default_parent_climbs_are_linear) {
|
||||
enum { SMALL = 200 };
|
||||
uint64_t slow_parents[2];
|
||||
for (int round = 0; round < 2; round++) {
|
||||
/* def f(x=x + x + ... + x): return x -- a binary_operator chain. */
|
||||
int terms = SMALL << round;
|
||||
size_t capacity = 32U + (size_t)terms * 4U;
|
||||
char *source = malloc(capacity);
|
||||
ASSERT_NOT_NULL(source);
|
||||
int length = snprintf(source, capacity, "def f(x=x");
|
||||
for (int i = 1; i < terms; i++) {
|
||||
length += snprintf(source + length, capacity - (size_t)length, " + x");
|
||||
}
|
||||
length += snprintf(source + length, capacity - (size_t)length, "):\n return x\n");
|
||||
cbm_usage_field_lookup_test_reset();
|
||||
CBMFileResult *result =
|
||||
cbm_extract_file(source, length, CBM_LANG_PYTHON, "proj", "deep.py", 0, NULL, NULL);
|
||||
free(source);
|
||||
ASSERT_NOT_NULL(result);
|
||||
slow_parents[round] = cbm_usage_slow_parent_fallback_test_count();
|
||||
/* Anti-vacuous: every default x climbed its chain to the parameter and
|
||||
* reads the enclosing namespace; only `return x` is shadowed by it. */
|
||||
int defaults = 0;
|
||||
for (int i = 0; i < result->usages.count; i++) {
|
||||
const CBMUsage *usage = &result->usages.items[i];
|
||||
defaults += usage->ref_name && strcmp(usage->ref_name, "x") == 0 &&
|
||||
!usage->semantic_reference_local_shadow;
|
||||
}
|
||||
cbm_free_result(result);
|
||||
ASSERT_EQ(defaults, terms);
|
||||
}
|
||||
fprintf(stderr, " [python-deep-default] slow_parents(%d)=%llu slow_parents(%d)=%llu\n", SMALL,
|
||||
(unsigned long long)slow_parents[0], 2 * SMALL, (unsigned long long)slow_parents[1]);
|
||||
/* Twice the terms may cost about twice the steps; the cubic climb took 4x. */
|
||||
ASSERT_LTE(slow_parents[1], slow_parents[0] * 3U + 64U);
|
||||
PASS();
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ===================================================================
|
||||
@@ -13455,6 +13498,7 @@ SUITE(extraction) {
|
||||
RUN_TEST(extract_wide_flat_reference_fields_are_linear);
|
||||
RUN_TEST(extract_csharp_argument_values_use_the_walk_cursor);
|
||||
RUN_TEST(extract_rescript_let_bindings_use_the_walk_cursor);
|
||||
RUN_TEST(extract_python_deep_default_parent_climbs_are_linear);
|
||||
#endif
|
||||
|
||||
/* Perl call-graph noise (#459 follow-up) */
|
||||
|
||||
@@ -885,6 +885,73 @@ TEST(lang_cfc_default_on_read_fail) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── .d: D source vs make/cargo dep-info ───────────────────────── */
|
||||
|
||||
/* Write content to a temp .d file and return cbm_disambiguate_d() for it. A
|
||||
* setup failure returns -1 so neither a D nor a COUNT expectation can pass
|
||||
* vacuously. */
|
||||
static int disambiguate_d_content(const char *content) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "%s/test_lang_dotd.d", cbm_tmpdir());
|
||||
if (!write_probe_file(path, content)) {
|
||||
return -1;
|
||||
}
|
||||
CBMLanguage lang = cbm_disambiguate_d(path);
|
||||
remove(path);
|
||||
return (int)lang;
|
||||
}
|
||||
|
||||
TEST(lang_d_dep_info_unsupported) {
|
||||
/* cargo (absolute target), gcc -MD continuation, CMake object rule, Windows
|
||||
* drive letter (not the rule colon), escaped space (one target, not "app"),
|
||||
* and the GNU make manual's "foo.o foo.d : ..." (two targets, spaced colon). */
|
||||
ASSERT_EQ(disambiguate_d_content("/home/u/t/debug/deps/foo-abc123.d: src/lib.rs src/a.rs\n"
|
||||
"\nsrc/lib.rs:\n"),
|
||||
CBM_LANG_COUNT);
|
||||
ASSERT_EQ(disambiguate_d_content("foo.o: foo.c /usr/include/stdio.h \\\n"
|
||||
" /usr/include/features.h\n"),
|
||||
CBM_LANG_COUNT);
|
||||
ASSERT_EQ(disambiguate_d_content("CMakeFiles/x.dir/a.cpp.o: \\\n /src/a.cpp\n"),
|
||||
CBM_LANG_COUNT);
|
||||
ASSERT_EQ(disambiguate_d_content("C:/b/foo.o: C:/s/foo.c \\\r\n C:/s/foo.h\r\n"),
|
||||
CBM_LANG_COUNT);
|
||||
ASSERT_EQ(disambiguate_d_content("out/my\\ app: main.c\n"), CBM_LANG_COUNT);
|
||||
ASSERT_EQ(disambiguate_d_content("foo.o foo.d : foo.c defs.h\n"), CBM_LANG_COUNT);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(lang_d_source_stays_dlang) {
|
||||
ASSERT_EQ(disambiguate_d_content("module a;\n"), CBM_LANG_DLANG);
|
||||
ASSERT_EQ(disambiguate_d_content("import std.stdio : writeln;\n"), CBM_LANG_DLANG);
|
||||
ASSERT_EQ(disambiguate_d_content("@safe:\nvoid f() {}\n"), CBM_LANG_DLANG);
|
||||
/* A leading comment is D, even DUB's "/+ dub.sdl:" single-file recipe. */
|
||||
ASSERT_EQ(disambiguate_d_content("/+ dub.sdl:\n name \"hello\"\n+/\nvoid main() {}\n"),
|
||||
CBM_LANG_DLANG);
|
||||
ASSERT_EQ(disambiguate_d_content("enum E : ubyte {\n a,\n}\n"), CBM_LANG_DLANG);
|
||||
/* A float UDA ("@ FloatLiteral") has a '.' before its colon, alone or glued
|
||||
* to a keyword. */
|
||||
ASSERT_EQ(disambiguate_d_content("@1.0:\nvoid f() {}\n"), CBM_LANG_DLANG);
|
||||
ASSERT_EQ(disambiguate_d_content("nothrow@1.0:\nvoid f() {}\n"), CBM_LANG_DLANG);
|
||||
/* A comment between tokens puts a '/' before the colon. */
|
||||
ASSERT_EQ(disambiguate_d_content("public/**/:\nvoid f() {}\n"), CBM_LANG_DLANG);
|
||||
ASSERT_EQ(disambiguate_d_content("public/+ +/:\nvoid f() {}\n"), CBM_LANG_DLANG);
|
||||
ASSERT_EQ(disambiguate_d_content("public//note: x\nvoid f() {}\n"), CBM_LANG_DLANG);
|
||||
/* Every D whitespace splits tokens, not only space, tab and CR: form feed,
|
||||
* vertical tab, U+2028 and U+2029. */
|
||||
ASSERT_EQ(disambiguate_d_content("import\fstd.stdio : writeln;\n"), CBM_LANG_DLANG);
|
||||
ASSERT_EQ(disambiguate_d_content("import\vstd.stdio : writeln;\n"), CBM_LANG_DLANG);
|
||||
ASSERT_EQ(disambiguate_d_content("import\xE2\x80\xA8"
|
||||
"std.stdio : writeln;\n"),
|
||||
CBM_LANG_DLANG);
|
||||
ASSERT_EQ(disambiguate_d_content("import\xE2\x80\xA9"
|
||||
"std.stdio : writeln;\n"),
|
||||
CBM_LANG_DLANG);
|
||||
/* Default on doubt: empty or unreadable. */
|
||||
ASSERT_EQ(disambiguate_d_content(""), CBM_LANG_DLANG);
|
||||
ASSERT_EQ(cbm_disambiguate_d("/tmp/nonexistent_file_12345.d"), CBM_LANG_DLANG);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* --- New languages (auto-generated) --- */
|
||||
TEST(lang_ext_solidity) {
|
||||
ASSERT_EQ(cbm_language_for_extension(".sol"), CBM_LANG_SOLIDITY);
|
||||
@@ -1398,6 +1465,7 @@ TEST(lang_probe_bytes_follow_the_name) {
|
||||
ASSERT_EQ(cbm_language_probe_bytes("foo.inc"), 4096);
|
||||
ASSERT_EQ(cbm_language_probe_bytes("Form1.frm"), 4096);
|
||||
ASSERT_EQ(cbm_language_probe_bytes("App.res"), 4096);
|
||||
ASSERT_EQ(cbm_language_probe_bytes("app.d"), 4096);
|
||||
ASSERT_EQ(cbm_language_probe_bytes("Widget.cfc"), 16384);
|
||||
ASSERT_EQ(cbm_language_probe_bytes("pom.xml"), 255);
|
||||
/* An unknown name falls back to a shebang probe of the first line. */
|
||||
@@ -1439,6 +1507,9 @@ TEST(lang_classify_matches_every_content_rule) {
|
||||
{"p.frm", "#procedure foo\nLocal F = a;\n", CBM_LANG_FORM, cbm_disambiguate_frm},
|
||||
{"q.res", "RSRC\0\0\0\1binary", CBM_LANG_COUNT, cbm_disambiguate_res},
|
||||
{"r.res", "let x = 1\n", CBM_LANG_RESCRIPT, cbm_disambiguate_res},
|
||||
{"dep.d", "target/debug/deps/app-0a1b2c.d: src/main.rs src/lib.rs\n", CBM_LANG_COUNT,
|
||||
cbm_disambiguate_d},
|
||||
{"app.d", "module app;\nimport std.stdio : writeln;\n", CBM_LANG_DLANG, cbm_disambiguate_d},
|
||||
{"s.xml", "<?xml version=\"1.0\"?>\n<Export generator=\"IRIS\" version=\"26\">\n",
|
||||
CBM_LANG_OBJECTSCRIPT_EXPORT, NULL},
|
||||
{"t.xml", "<?xml version=\"1.0\"?>\n<project/>\n", CBM_LANG_XML, NULL},
|
||||
@@ -1486,6 +1557,7 @@ TEST(lang_classify_unreadable_content_keeps_the_name_default) {
|
||||
ASSERT_EQ(cbm_language_classify("a.cfc", NULL, 0, false, false), CBM_LANG_CFSCRIPT);
|
||||
ASSERT_EQ(cbm_language_classify("a.frm", NULL, 0, false, false), CBM_LANG_FORM);
|
||||
ASSERT_EQ(cbm_language_classify("a.res", NULL, 0, false, false), CBM_LANG_RESCRIPT);
|
||||
ASSERT_EQ(cbm_language_classify("a.d", NULL, 0, false, false), CBM_LANG_DLANG);
|
||||
ASSERT_EQ(cbm_language_classify("a.xml", NULL, 0, false, false), CBM_LANG_XML);
|
||||
ASSERT_EQ(cbm_language_classify("run-tests", NULL, 0, false, false), CBM_LANG_COUNT);
|
||||
ASSERT_EQ(cbm_language_classify("main.go", NULL, 0, false, false), CBM_LANG_GO);
|
||||
@@ -1716,6 +1788,8 @@ SUITE(language) {
|
||||
RUN_TEST(lang_frm_form_stays_form);
|
||||
RUN_TEST(lang_res_binary_resource_unsupported);
|
||||
RUN_TEST(lang_res_rescript_stays_rescript);
|
||||
RUN_TEST(lang_d_dep_info_unsupported);
|
||||
RUN_TEST(lang_d_source_stays_dlang);
|
||||
|
||||
/* Go test ports */
|
||||
/* New languages */
|
||||
|
||||
+1
-1
@@ -2046,7 +2046,7 @@ static int issue403_initialize_count_calls(const char *session_root, bool approv
|
||||
char err[1024];
|
||||
bool approved =
|
||||
!approve_sensitive || cbm_workspace_grant_add(cache, cbm_workspace_home_dir(), session_root,
|
||||
true, err, sizeof(err));
|
||||
true, false, err, sizeof(err));
|
||||
cbm_config_t *cfg = approved ? cbm_config_open(cache) : NULL;
|
||||
cbm_mcp_server_t *srv = cfg ? cbm_mcp_server_new(NULL) : NULL;
|
||||
int calls = -2;
|
||||
|
||||
@@ -636,6 +636,71 @@ TEST(pipeline_structure_nodes) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Number of `label` nodes whose file_path starts with `prefix`, or -1. */
|
||||
static int count_nodes_with_path_prefix(cbm_store_t *s, const char *project, const char *label,
|
||||
const char *prefix) {
|
||||
cbm_node_t *nodes = NULL;
|
||||
int count = 0;
|
||||
if (cbm_store_find_nodes_by_label(s, project, label, &nodes, &count) != CBM_STORE_OK) {
|
||||
return -1;
|
||||
}
|
||||
int hits = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (nodes[i].file_path && strncmp(nodes[i].file_path, prefix, strlen(prefix)) == 0) {
|
||||
hits++;
|
||||
}
|
||||
}
|
||||
cbm_store_free_nodes(nodes, count);
|
||||
return hits;
|
||||
}
|
||||
|
||||
/* Make-style dep-info .d files are dropped at discovery, so the graph holds no
|
||||
* File or Module node for them and no Folder for a directory that held only
|
||||
* dep-info. Real D source beside a dep-info file keeps its nodes and folder. */
|
||||
TEST(pipeline_d_dep_info_leaves_no_nodes) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dotd_graph_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
|
||||
char repo[512];
|
||||
snprintf(repo, sizeof(repo), "%s/repo", tmp);
|
||||
write_temp_file(repo, "cargo-target/debug/deps/app-0123abcd.d",
|
||||
"/r/cargo-target/debug/deps/app-0123abcd.d: src/main.rs\n\nsrc/main.rs:\n");
|
||||
write_temp_file(repo, "src/app.d", "module app;\n\nvoid main() {}\n");
|
||||
write_temp_file(repo, "src/app.o.d", "src/app.o: src/app.d\n");
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/test.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *project = cbm_pipeline_project_name(p);
|
||||
|
||||
int dep_folders = count_nodes_with_path_prefix(s, project, "Folder", "cargo-target");
|
||||
int dep_files = count_nodes_with_path_prefix(s, project, "File", "cargo-target");
|
||||
int dep_modules = count_nodes_with_path_prefix(s, project, "Module", "cargo-target");
|
||||
int obj_files = count_nodes_with_path_prefix(s, project, "File", "src/app.o.d");
|
||||
int obj_modules = count_nodes_with_path_prefix(s, project, "Module", "src/app.o.d");
|
||||
int src_folders = count_nodes_with_path_prefix(s, project, "Folder", "src");
|
||||
int src_files = count_nodes_with_path_prefix(s, project, "File", "src/app.d");
|
||||
int src_modules = count_nodes_with_path_prefix(s, project, "Module", "src/app.d");
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
|
||||
ASSERT_EQ(dep_folders, 0);
|
||||
ASSERT_EQ(dep_files, 0);
|
||||
ASSERT_EQ(dep_modules, 0);
|
||||
ASSERT_EQ(obj_files, 0);
|
||||
ASSERT_EQ(obj_modules, 0);
|
||||
ASSERT_EQ(src_folders, 1);
|
||||
ASSERT_EQ(src_files, 1);
|
||||
ASSERT_EQ(src_modules, 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Issue #516: an ADR stored via manage_adr (project_summaries) must survive a
|
||||
* full re-index. A full re-index deletes the DB and rebuilds it from the graph
|
||||
* buffer, which writes an empty project_summaries table; the fix captures the
|
||||
@@ -28890,6 +28955,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_structure_nodes);
|
||||
RUN_TEST(pipeline_stem_folder_collision_keeps_both_nodes);
|
||||
RUN_TEST(pipeline_kustomize_entry_beside_same_stem_file);
|
||||
RUN_TEST(pipeline_d_dep_info_leaves_no_nodes);
|
||||
RUN_TEST(pipeline_committed_counts_match_persisted);
|
||||
RUN_TEST(pipeline_adr_survives_full_reindex);
|
||||
RUN_TEST(pipeline_export_error_snapshot_on_artifact_failure);
|
||||
|
||||
+79
-8
@@ -75,6 +75,74 @@ TEST(ws_posix_top_level_trees_are_too_shallow) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A recorded broad approval (allow-root --approve-broad) is the only thing that
|
||||
* lifts CBM_WS_DENY_TOO_SHALLOW, and only for that exact path. Classification
|
||||
* and cbm_workspace_verdict_is_overridable stay exactly as strict as the test
|
||||
* above checks; the approval is a grant, so it is listed like any other human
|
||||
* decision, and it lifts no other verdict. */
|
||||
TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only) {
|
||||
char cache[256];
|
||||
char *created = th_mktempdir("cbm_ws_allow_broad");
|
||||
ASSERT_NOT_NULL(created);
|
||||
snprintf(cache, sizeof(cache), "%s", created);
|
||||
|
||||
char err[1024];
|
||||
char listed[4096];
|
||||
/* Refused out of the box, and the refusal names the command a person runs. */
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed("/usr", HOME, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "allow-root --approve-broad /usr"));
|
||||
|
||||
/* Neither an ordinary grant nor a sensitive approval lifts breadth; a broad
|
||||
* approval lifts neither an absolute nor a sensitive verdict. A refused grant
|
||||
* records nothing. */
|
||||
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/usr", false, false, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "--approve-broad"));
|
||||
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/usr", true, false, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/", false, true, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, HOME, false, true, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
|
||||
ASSERT_FALSE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
|
||||
|
||||
/* The explicit approval is recorded and marked. */
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, "/usr", false, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
|
||||
ASSERT_STR_EQ(listed, "(approved broad) /usr\n");
|
||||
|
||||
#ifndef _WIN32
|
||||
/* Containment resolves real paths, so the rest needs a top-level directory
|
||||
* that exists on every POSIX host: "/usr". A repeated approval is recognized
|
||||
* rather than appended again. */
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, "/usr", false, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
|
||||
ASSERT_STR_EQ(listed, "(approved broad) /usr\n");
|
||||
|
||||
/* It lifts exactly that path. A configured root of "/" puts "/dev" inside the
|
||||
* declared boundary too, so the only thing still refusing it is that the
|
||||
* approval names another path. */
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed("/usr", HOME, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed("/usr", HOME, cache, "/", err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed("/dev", HOME, cache, "/", err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "allow-root --approve-broad /dev"));
|
||||
#endif
|
||||
|
||||
/* Each marker lifts only its own verdict: a broad mark on a credential
|
||||
* directory, which grant_add would never write, still does not lift it. */
|
||||
char secret[1024];
|
||||
snprintf(secret, sizeof(secret), "%s/.ssh", cache);
|
||||
ASSERT_EQ(cbm_mkdir(secret), 0);
|
||||
char store[1024];
|
||||
ASSERT_TRUE(cbm_workspace_grant_path(cache, store, sizeof(store)));
|
||||
FILE *f = cbm_fopen(store, "a");
|
||||
ASSERT_NOT_NULL(f);
|
||||
(void)fprintf(f, "^%s\n", secret);
|
||||
ASSERT_EQ(fclose(f), 0);
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(secret, HOME, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
|
||||
|
||||
th_cleanup(cache);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Legitimately shallow project roots must survive: these are the false positives
|
||||
* a blanket depth rule would cause, which is why Windows counts drive-relative. */
|
||||
TEST(ws_legitimate_shallow_roots_are_allowed) {
|
||||
@@ -177,7 +245,7 @@ TEST(ws_sensitive_root_explicit_approval_is_preserved) {
|
||||
char err[1024];
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
|
||||
|
||||
th_cleanup(root);
|
||||
@@ -197,14 +265,14 @@ TEST(ws_sensitive_approval_upgrades_existing_ordinary_exact_grant) {
|
||||
snprintf(cache, sizeof(cache), "%s", created);
|
||||
|
||||
char err[1024];
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, root, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, root, false, false, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
|
||||
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
|
||||
/* A repeated explicit approval must recognize the marked exact grant rather
|
||||
* than append another exception. */
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
|
||||
|
||||
char listed[4096];
|
||||
char expected[4096];
|
||||
@@ -232,13 +300,15 @@ TEST(ws_sensitive_approval_adds_exact_exception_under_ordinary_ancestor) {
|
||||
snprintf(sensitive, sizeof(sensitive), "%s/private-project", ancestor);
|
||||
ASSERT_EQ(cbm_mkdir(sensitive), 0);
|
||||
char err[1024];
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, ancestor, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, ancestor, false, false, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, err, sizeof(err)));
|
||||
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, sensitive, sensitive, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(
|
||||
cbm_workspace_grant_add(cache, sensitive, sensitive, true, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(
|
||||
cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, sensitive, sensitive, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(
|
||||
cbm_workspace_grant_add(cache, sensitive, sensitive, true, false, err, sizeof(err)));
|
||||
|
||||
char listed[4096];
|
||||
char expected[4096];
|
||||
@@ -774,7 +844,7 @@ TEST(ws_manifest_ordinary_entries_unchanged) {
|
||||
|
||||
char err[1024];
|
||||
/* The grant store names a root the sdk is not under. */
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, granted, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, granted, false, false, err, sizeof(err)));
|
||||
|
||||
ws_manifest_write_line(base, sdk);
|
||||
ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
|
||||
@@ -918,6 +988,7 @@ SUITE(workspace) {
|
||||
RUN_TEST(ws_volume_roots_are_absolutely_denied);
|
||||
RUN_TEST(ws_non_absolute_paths_are_denied);
|
||||
RUN_TEST(ws_posix_top_level_trees_are_too_shallow);
|
||||
RUN_TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only);
|
||||
RUN_TEST(ws_legitimate_shallow_roots_are_allowed);
|
||||
RUN_TEST(ws_home_itself_is_sensitive_but_subdirs_are_fine);
|
||||
RUN_TEST(ws_credential_directories_are_sensitive_at_any_depth);
|
||||
|
||||
Reference in new issue
Block a user