From e6b0efb66b31d67dd705e1cbd23ba6011dfc8f3a Mon Sep 17 00:00:00 2001 From: safishamsi Date: Sat, 15 Aug 2026 17:24:35 +0100 Subject: [PATCH] fix(cpp): guard empty-normalize collapse for punctuation-only test names (#2594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #2751 recovery minted node ids via _make_id(stem, name); a punctuation-only name (TEST_CASE("***")) normalizes to empty, collapsing onto the bare file-stem id — colliding with the file namespace and swallowing every later such test under one id via seen_ids (#1899). Detect the collapse and fall back to a stable line-positional id so each stays distinct. Adds a regression test (also covers SCENARIO) and the CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + graphify/extract.py | 7 +++++++ tests/test_languages.py | 25 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d9f782..6e6954c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.44 (unreleased) +- Fix: doctest/Catch2 string-named test cases (`TEST_CASE("...")`, `SCENARIO`, `TEST_CASE_TEMPLATE`), which tree-sitter-cpp drops as ERROR nodes, are recovered as callable nodes contained by the file (#2594, thanks @ousamabenyounes); a punctuation-only test name gets a distinct line-positional id instead of collapsing onto the file-stem id. - Fix: `graphify affected` resolves an absolute-path seed against the repo root derived from the graph's own location instead of the current working directory, so a blast-radius query with an absolute seed run from anywhere (an editor, a script) no longer silently returns nothing; a seed outside the root still misses cleanly (#2706, thanks @ousamabenyounes). - Fix: a lazy CommonJS `require(...)` inside a function body (the idiom for breaking circular dependencies) now emits the same `imports_from`/`imports` dependency edges as a top-level require, attributed to the enclosing function, instead of being silently dropped; a dynamic `require(variable)` is still skipped (#2700, thanks @rajanpanth). - Fix: a JS/TS identifier bound by an import whose target resolves outside the scanned corpus (e.g. a `lucide-react` icon) is now shadowed, so using it as a value no longer fabricates an INFERRED `indirect_call` onto an unrelated same-named callable elsewhere in the corpus; a relative/in-corpus import still resolves to its real target (#2757, thanks @phudayyy). diff --git a/graphify/extract.py b/graphify/extract.py index 459fa87..a829409 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -1910,6 +1910,11 @@ def _augment_cpp_string_tests(path: Path, result: dict) -> dict: str_path = str(path) stem = _file_stem(path) file_nid = _make_id(str_path) + # A test name that is all punctuation (TEST_CASE("***")) normalizes to empty, + # so _make_id(stem, name) collapses onto this bare-stem id — colliding with + # the file's namespace and silently swallowing every later such test under + # one id (#1899). Detect that collapse and fall back to a line-positional id. + stem_collapse_id = _make_id(stem) nodes = result.setdefault("nodes", []) edges = result.setdefault("edges", []) seen_ids = {n.get("id") for n in nodes} @@ -1918,6 +1923,8 @@ def _augment_cpp_string_tests(path: Path, result: dict) -> dict: test_name = m.group(1) line = source.count("\n", 0, m.start()) + 1 test_nid = _make_id(stem, test_name) + if test_nid == stem_collapse_id: + test_nid = _make_id(stem, "test", f"L{line}") if test_nid in seen_ids: continue seen_ids.add(test_nid) diff --git a/tests/test_languages.py b/tests/test_languages.py index a65b650..37466ec 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -215,6 +215,31 @@ def test_cpp_recovers_doctest_string_named_test_cases(): } assert test_ids and test_ids <= contained + +def test_cpp_string_tests_punctuation_only_names_stay_distinct(tmp_path): + """A punctuation-only test name normalizes to empty, so a naive + ``_make_id(stem, name)`` collapses onto the bare file-stem id — colliding + with the file namespace and swallowing every later such test under one id + (#1899). The guard gives each a distinct line-positional id.""" + f = tmp_path / "punct_tests.cpp" + f.write_text( + 'TEST_CASE("***") { CHECK(1); }\n' + 'TEST_CASE("...") { CHECK(2); }\n' + 'SCENARIO("boots up") { REQUIRE(1); }\n', + encoding="utf-8", + ) + r = extract_cpp(f) + from graphify.extract import _make_id, _file_stem + stem_collapse = _make_id(_file_stem(f)) + tests = [n for n in r["nodes"] if n["label"].startswith('"')] + ids = [n["id"] for n in tests] + # both punctuation-only cases recovered, plus the SCENARIO macro + assert '"***"' in {n["label"] for n in tests} + assert '"..."' in {n["label"] for n in tests} + assert '"boots up"' in {n["label"] for n in tests} + assert len(set(ids)) == len(ids) # all distinct + assert all(i != stem_collapse for i in ids) # none squats on the file stem + def test_cpp_finds_includes(): r = extract_cpp(FIXTURES / "sample.cpp") assert "imports" in _relations(r)