mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-10 09:13:53 +08:00
test(nix): cover attrpath qualification, quoting, and interpolation
Four extraction cases and one pipeline case, each asserting a distinct claim so a
regression names what it broke:
nix_attrset_scope_disambiguates_leaf_names
two attrsets each holding `dup` produce two definitions with distinct QNs.
Unqualified they shared one QN and the second was discarded.
nix_dotted_attrpath_qualifies_like_nested
`wrap.deep.fn = …` and `wrap = { deep = { fn = …; }; }` produce the SAME
QN. This is the equality that makes attrpath qualification a correctness
fix rather than a preference — the two are the same expression.
nix_quoted_attr_name_strips_quotes
`"kebab-case"` is named kebab-case, and asserts the quoted form is absent
rather than merely that the bare form is present.
nix_interpolated_attr_mints_no_def
`"${dynamic}" = …` mints nothing, and the sibling binding still does — so
the test cannot pass by extracting nothing at all.
pipeline_nix_scoped_binding_calls_resolve (tests/test_pipeline.c)
a call inside a scoped binding reaches its target through the store, for
both routes into a qualified name: an enclosing attrset and a dotted
attrpath.
The pipeline case has to live at that level. Definition QNs and call-scope QNs
come from two separate functions, and if they disagree the edge is dropped at
write with no error — every extraction-level assertion still passes while the
graph quietly loses edges. Only the store shows it.
Adds has_def_qn alongside the existing has_def. find_def_by_name returns the
first match by NAME and so cannot tell two same-named definitions in different
scopes apart, which is exactly what these tests exist to separate.
Extraction and pipeline suites: 491 passed, 0 failed.
Signed-off-by: Jason Bowman <jason@json64.dev>
This commit is contained in:
@@ -53,6 +53,18 @@ static int __attribute__((unused)) has_import(CBMFileResult *r, const char *path
|
||||
}
|
||||
|
||||
/* Count definitions with a given label. */
|
||||
/* Check for a definition with the given qualified name. Distinct from
|
||||
* find_def_by_name, which returns the first match by NAME and so cannot tell two
|
||||
* same-named definitions in different scopes apart — exactly the case that
|
||||
* attrpath qualification exists to separate. */
|
||||
static int has_def_qn(CBMFileResult *r, const char *qn) {
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
if (r->defs.items[i].qualified_name && strcmp(r->defs.items[i].qualified_name, qn) == 0)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int count_defs_with_label(CBMFileResult *r, const char *label) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
@@ -1222,6 +1234,77 @@ TEST(nix_defs_survive_curried_header) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A Nix binding's name is a PATH, and the extractor previously took only its first
|
||||
* segment. Convention here matches C++ namespaces — `ns::serialize` is name
|
||||
* `serialize`, QN `proj.file.ns.serialize` — so a Nix binding is name = leaf
|
||||
* segment, QN = enclosing scope + leaf. */
|
||||
TEST(nix_attrset_scope_disambiguates_leaf_names) {
|
||||
CBMFileResult *r =
|
||||
extract("{\n setA = { dup = x: x + 1; };\n setB = { dup = y: y + 2; };\n}\n",
|
||||
CBM_LANG_NIX, "t", "collide.nix");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
/* Both survive. Unqualified, these shared one QN, so the second definition —
|
||||
* and every CALLS edge sourced from it — was silently discarded at write. */
|
||||
ASSERT(count_defs_with_label(r, "Function") == 2);
|
||||
ASSERT(has_def_qn(r, "t.collide.setA.dup"));
|
||||
ASSERT(has_def_qn(r, "t.collide.setB.dup"));
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* `a.b.fn = …` is sugar for `a = { b = { fn = …; }; }`. Both spellings must yield
|
||||
* the same name and the same QN; the leading segments are scope, not name. */
|
||||
TEST(nix_dotted_attrpath_qualifies_like_nested) {
|
||||
CBMFileResult *r = extract("{\n wrap.deep.fn = z: z + 1;\n}\n", CBM_LANG_NIX, "t", "d.nix");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT(has_def(r, "Function", "fn"));
|
||||
ASSERT(has_def_qn(r, "t.d.wrap.deep.fn"));
|
||||
|
||||
CBMFileResult *n =
|
||||
extract("{\n wrap = { deep = { fn = z: z + 1; }; };\n}\n", CBM_LANG_NIX, "t", "d.nix");
|
||||
ASSERT_NOT_NULL(n);
|
||||
ASSERT_FALSE(n->has_error);
|
||||
/* The equality that makes this a correctness fix rather than a preference. */
|
||||
ASSERT(has_def_qn(n, "t.d.wrap.deep.fn"));
|
||||
cbm_free_result(n);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A quoted segment is an ordinary name that merely needs quoting in source. The
|
||||
* delimiters are not part of it, and leaving them in means every consumer keying
|
||||
* on the name has to know to re-quote. */
|
||||
TEST(nix_quoted_attr_name_strips_quotes) {
|
||||
CBMFileResult *r = extract("{\n \"kebab-case\" = a: a;\n svc.\"my.name\" = b: b;\n}\n",
|
||||
CBM_LANG_NIX, "t", "q.nix");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT(has_def(r, "Function", "kebab-case"));
|
||||
ASSERT_FALSE(has_def_any(r, "\"kebab-case\""));
|
||||
ASSERT(has_def_qn(r, "t.q.kebab-case"));
|
||||
/* A quoted segment may itself contain dots; they are part of the name, not
|
||||
* path separators, but the QN is a dotted string either way. */
|
||||
ASSERT(has_def(r, "Function", "my.name"));
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* `"${x}" = …` has no statically knowable name. Minting it produces a def named
|
||||
* `"${x}"` that nothing can look up or resolve a call against, so mint nothing —
|
||||
* the same call the Makefile dot-prefix guard makes. */
|
||||
TEST(nix_interpolated_attr_mints_no_def) {
|
||||
CBMFileResult *r =
|
||||
extract("{\n \"${dynamic}\" = a: a;\n fixed = b: b;\n}\n", CBM_LANG_NIX, "t", "i.nix");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_FALSE(r->has_error);
|
||||
ASSERT(has_def(r, "Function", "fixed"));
|
||||
ASSERT(count_defs_with_label(r, "Function") == 1);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Descending past the header must not mint a second def from a curried lambda's inner
|
||||
* arm: `iota = a: b: ...` is one named function, not two. The inner `b:` has a
|
||||
* function_expression parent, resolves no name, and must stay out. */
|
||||
@@ -4986,6 +5069,10 @@ SUITE(extraction) {
|
||||
RUN_TEST(nix_defs_survive_function_header_let);
|
||||
RUN_TEST(nix_defs_survive_function_header_attrset);
|
||||
RUN_TEST(nix_defs_survive_curried_header);
|
||||
RUN_TEST(nix_attrset_scope_disambiguates_leaf_names);
|
||||
RUN_TEST(nix_dotted_attrpath_qualifies_like_nested);
|
||||
RUN_TEST(nix_quoted_attr_name_strips_quotes);
|
||||
RUN_TEST(nix_interpolated_attr_mints_no_def);
|
||||
RUN_TEST(nix_curried_lambda_mints_one_def);
|
||||
RUN_TEST(fortran_function);
|
||||
|
||||
|
||||
@@ -851,6 +851,61 @@ static bool cross_file_call_exists(cbm_store_t *s, const char *project, const ch
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Nix attrpath qualification, end to end. A call inside a scoped binding must
|
||||
* source to the QUALIFIED definition.
|
||||
*
|
||||
* This has to be a pipeline test rather than an extraction one. Definition QNs
|
||||
* (extract_defs.c) and call-scope QNs (extract_unified.c) are computed by two
|
||||
* separate functions. If they disagree by even one segment, the CALLS edge names
|
||||
* a source node that was never minted and is dropped at write — with no error,
|
||||
* and with every extraction-level assertion still passing. Only the store sees it.
|
||||
*
|
||||
* Both callers below are scoped, one by an enclosing attrset and one by a dotted
|
||||
* attrpath, so this covers both routes into the qualified name. */
|
||||
TEST(pipeline_nix_scoped_binding_calls_resolve) {
|
||||
if (setup_test_repo() != 0) {
|
||||
FAIL("failed to create temp dir");
|
||||
}
|
||||
|
||||
char nix_path[512];
|
||||
snprintf(nix_path, sizeof(nix_path), "%s/lib.nix", g_tmpdir);
|
||||
FILE *nf = fopen(nix_path, "w");
|
||||
if (!nf) {
|
||||
teardown_test_repo();
|
||||
FAIL("failed to write nix fixture");
|
||||
}
|
||||
fprintf(nf, "{ prelude }:\n"
|
||||
"let\n"
|
||||
" nixTarget = t: t + 1;\n"
|
||||
"in\n"
|
||||
"{\n"
|
||||
" setA = { nixCaller = x: nixTarget x; };\n"
|
||||
" outer.inner.nixDeepCaller = y: nixTarget y;\n"
|
||||
"}\n");
|
||||
fclose(nf);
|
||||
|
||||
char nix_db[512];
|
||||
snprintf(nix_db, sizeof(nix_db), "%s/test_nix_calls.db", g_tmpdir);
|
||||
|
||||
cbm_pipeline_t *np = cbm_pipeline_new(g_tmpdir, nix_db, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(np);
|
||||
ASSERT_EQ(cbm_pipeline_run(np), 0);
|
||||
|
||||
cbm_store_t *ns = cbm_store_open_path(nix_db);
|
||||
ASSERT_NOT_NULL(ns);
|
||||
const char *nix_project = cbm_pipeline_project_name(np);
|
||||
|
||||
/* Scoped by an enclosing attrset: the def QN is proj.lib.setA.nixCaller. */
|
||||
ASSERT(cross_file_call_exists(ns, nix_project, "nixCaller", "nixTarget"));
|
||||
/* Scoped by a dotted attrpath: proj.lib.outer.inner.nixDeepCaller. */
|
||||
ASSERT(cross_file_call_exists(ns, nix_project, "nixDeepCaller", "nixTarget"));
|
||||
|
||||
cbm_store_close(ns);
|
||||
cbm_pipeline_free(np);
|
||||
teardown_test_repo();
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Regression: incremental re-index of an edited file must NOT drop inbound
|
||||
* cross-file CALLS edges whose source lives in an UNCHANGED file.
|
||||
*
|
||||
@@ -7363,6 +7418,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_complexity_transitive_loop_depth);
|
||||
/* Calls pass */
|
||||
RUN_TEST(pipeline_calls_resolution);
|
||||
RUN_TEST(pipeline_nix_scoped_binding_calls_resolve);
|
||||
RUN_TEST(pipeline_incremental_preserves_cross_file_calls);
|
||||
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
|
||||
|
||||
Reference in New Issue
Block a user