From 7f9feb6b63c090925fcf33da5cf093d33c28cf2b Mon Sep 17 00:00:00 2001 From: Jason Bowman Date: Mon, 27 Jul 2026 13:53:22 -0700 Subject: [PATCH] test(nix): cover Variable extraction, scope bound, and the Function/Variable split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nix_module_level_bindings_mint_variables a `let` binding and an attrset binding both mint Variables, and the QN carries the attrpath (services.nginx.enable). nix_nested_bindings_are_not_module_level a binding two attrsets deep mints nothing, and neither `deep` nor `nested` becomes a Variable since both are scopes. nix_lambda_binding_is_function_not_variable a lambda-valued binding is a Function and NOT a Variable; a scalar binding is the reverse. Every absence assertion is paired with a positive one on the same predicate in the same result — `topLevel` in the nesting test, `val` in the split test — so none of them can pass by extracting nothing at all. Several false "clean" results in this area came from predicates that could not have matched. Signed-off-by: Jason Bowman --- tests/test_extraction.c | 72 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 120ee72f8..1a7e479ce 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -1305,6 +1305,75 @@ TEST(nix_interpolated_attr_mints_no_def) { PASS(); } +/* Nix Variables. `nix_var_types` has always declared `binding`, but no Nix name + * resolver existed, so the count was unconditionally zero. + * + * Scope follows the rule every other language uses: extract_variables mints FILE + * scope and never locals — a C++ declaration inside a function body is not a + * Variable. For Nix, file scope is the `let` bindings and the returned attrset; + * anything in a deeper attrset is not. Without that bound a NixOS module's + * settings tree would mint a node per `enable = true`. */ +TEST(nix_module_level_bindings_mint_variables) { + CBMFileResult *r = extract("{ pkgs, lib, ... }:\n" + "let\n" + " privateConst = 42;\n" + "in\n" + "{\n" + " exported = \"value\";\n" + " services.nginx.enable = true;\n" + "}\n", + CBM_LANG_NIX, "t", "mod.nix"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + /* A let binding is file scope in the same sense a C++ file-static is. */ + ASSERT(has_def(r, "Variable", "privateConst")); + ASSERT(has_def(r, "Variable", "exported")); + /* The QN carries the attrpath, exactly as it does for functions. */ + ASSERT(has_def(r, "Variable", "enable")); + ASSERT(has_def_qn(r, "t.mod.services.nginx.enable")); + cbm_free_result(r); + PASS(); +} + +/* The flood guard. Each absence assertion is paired with a positive one on the + * same predicate in the same result, so none can pass by extracting nothing. */ +TEST(nix_nested_bindings_are_not_module_level) { + CBMFileResult *r = extract("{ pkgs }:\n" + "{\n" + " topLevel = 1;\n" + " deep = {\n" + " nested = {\n" + " shouldNotAppear = 2;\n" + " };\n" + " };\n" + "}\n", + CBM_LANG_NIX, "t", "deep.nix"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Variable", "topLevel")); /* positive control */ + ASSERT_FALSE(has_def_any(r, "shouldNotAppear")); + /* `deep` is an attrset — a scope, not a value — so not a Variable either. */ + ASSERT_FALSE(has_def(r, "Variable", "deep")); + ASSERT_FALSE(has_def(r, "Variable", "nested")); + cbm_free_result(r); + PASS(); +} + +/* A lambda-valued binding is already minted as a Function by the def walk. Minting + * it again here would double-count every helper in the ecosystem. */ +TEST(nix_lambda_binding_is_function_not_variable) { + CBMFileResult *r = + extract("{\n fn = x: x + 1;\n val = 7;\n}\n", CBM_LANG_NIX, "t", "mix.nix"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_def(r, "Function", "fn")); + ASSERT_FALSE(has_def(r, "Variable", "fn")); + ASSERT(has_def(r, "Variable", "val")); /* positive control */ + ASSERT_FALSE(has_def(r, "Function", "val")); + 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. */ @@ -5073,6 +5142,9 @@ SUITE(extraction) { 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_module_level_bindings_mint_variables); + RUN_TEST(nix_nested_bindings_are_not_module_level); + RUN_TEST(nix_lambda_binding_is_function_not_variable); RUN_TEST(nix_curried_lambda_mints_one_def); RUN_TEST(fortran_function);