mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 16:09:26 +00:00
fix(extract): collapse byte-identical duplicate edges at emission (#3251)
This commit is contained in:
3 files changed
+120
-3
No files matched your search
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
from graphify.extractors.base import _LANGUAGE_BUILTIN_GLOBALS, _file_stem, _make_id, _read_text
|
||||
from graphify.ids import normalize_id
|
||||
from graphify.extractors.models import LanguageConfig
|
||||
@@ -5979,9 +5980,23 @@ def _extract_generic(
|
||||
# ── Clean edges ───────────────────────────────────────────────────────────
|
||||
valid_ids = seen_ids
|
||||
clean_edges = []
|
||||
# Byte-identical duplicates collapse to one edge (#3251): a signature that
|
||||
# annotates the same type twice (``def f(a: Path, b: Path)``) is ONE
|
||||
# reference relationship at one location, but the per-occurrence emission
|
||||
# loops above append it once per annotation — in every language block, since
|
||||
# neither add_edge nor the raw appends de-duplicate. The copies carry zero
|
||||
# information (build's dedup drops them anyway) and their only observable
|
||||
# effect is tripping diagnose_extraction's exact_duplicate_edges health
|
||||
# warning. Only edges whose ENTIRE payload is identical collapse; any
|
||||
# differing field (source_location, context, metadata, …) keeps both.
|
||||
_seen_edge_payloads: set[str] = set()
|
||||
for edge in edges:
|
||||
src, tgt = edge["source"], edge["target"]
|
||||
if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from", "re_exports")):
|
||||
payload = json.dumps(edge, sort_keys=True, default=str)
|
||||
if payload in _seen_edge_payloads:
|
||||
continue
|
||||
_seen_edge_payloads.add(payload)
|
||||
clean_edges.append(edge)
|
||||
|
||||
# Ruby mixins were collected during the node walk (before raw_calls existed);
|
||||
|
||||
@@ -213,7 +213,16 @@ def test_call_site_generic_args_appear_in_issue_repro(tmp_path):
|
||||
),
|
||||
})
|
||||
izeta_refs = [tgt for src, tgt, _ctx in all_di_refs if src == ".Di()" and tgt == "IZeta"]
|
||||
assert len(izeta_refs) >= 2, (
|
||||
"s.AddScoped<IZeta, Box<IZeta>>() must link BOTH the outer IZeta "
|
||||
f"and the inner IZeta (inside the Box<...> argument); got {izeta_refs!r}"
|
||||
# The outer IZeta and the inner IZeta (inside Box<...>) are the same
|
||||
# reference relationship at the same location, so since #3251 the two
|
||||
# walk occurrences collapse into ONE edge at extraction (they were always
|
||||
# collapsed by build, and the raw duplicate tripped
|
||||
# diagnose_extraction's exact_duplicate_edges warning). The call-site bug
|
||||
# this test guards emitted NO edge at all, so existence keeps the teeth.
|
||||
assert len(izeta_refs) == 1, (
|
||||
"s.AddScoped<IZeta, Box<IZeta>>() must link IZeta exactly once "
|
||||
f"(one relationship, no duplicate edge); got {izeta_refs!r}"
|
||||
)
|
||||
assert (".Di()", "Box") in {(s, t) for s, t, _c in all_di_refs}, (
|
||||
"the Box<...> argument itself must still link from the Di method"
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Exact-duplicate edge emission from repeated annotations (#3251).
|
||||
|
||||
A signature that annotates the same type twice (``def f(a: Path, b: Path)``)
|
||||
used to emit one identical ``references`` edge per occurrence — same source,
|
||||
target, relation, source_location and context. build() drops the copy, but
|
||||
diagnose_extraction first counts it under ``exact_duplicate_edges`` and the
|
||||
run ends with an unexplained GRAPH HEALTH WARNING. Duplicates now collapse at
|
||||
extraction; anything differing in any field still survives.
|
||||
"""
|
||||
|
||||
import collections
|
||||
|
||||
from graphify.diagnostics import diagnose_extraction
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _edge_counts(result):
|
||||
return collections.Counter(
|
||||
(e["source"], e["target"], e["relation"],
|
||||
e.get("source_location"), e.get("context"))
|
||||
for e in result["edges"]
|
||||
)
|
||||
|
||||
|
||||
def test_python_repeated_parameter_annotation_emits_one_edge(tmp_path, monkeypatch):
|
||||
"""The issue's exact repro: two same-typed params, one references edge."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "sample.py").write_text(
|
||||
"from pathlib import Path\n\n\n"
|
||||
"def two_params(a: Path, b: Path) -> None:\n"
|
||||
" print(a, b)\n\n\n"
|
||||
"def one_param(a: Path) -> None:\n"
|
||||
" print(a)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
r = extract([tmp_path / "sample.py"], cache_root=tmp_path)
|
||||
counts = _edge_counts(r)
|
||||
dupes = {k: v for k, v in counts.items() if v > 1}
|
||||
assert dupes == {}, f"exact duplicate edges emitted: {dupes}"
|
||||
assert counts[("sample_two_params", "path", "references",
|
||||
"L4", "parameter_type")] == 1
|
||||
assert counts[("sample_one_param", "path", "references",
|
||||
"L8", "parameter_type")] == 1
|
||||
|
||||
|
||||
def test_csharp_repeated_parameter_annotation_emits_one_edge(tmp_path, monkeypatch):
|
||||
"""The same disease exists in every language block — C# as the witness."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "Svc.cs").write_text(
|
||||
"public class Svc {\n"
|
||||
" public void Copy(Widget a, Widget b) { }\n"
|
||||
"}\n"
|
||||
"public class Widget { }\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
r = extract([tmp_path / "Svc.cs"], cache_root=tmp_path)
|
||||
dupes = {k: v for k, v in _edge_counts(r).items() if v > 1}
|
||||
assert dupes == {}, f"exact duplicate edges emitted: {dupes}"
|
||||
|
||||
|
||||
def test_diagnose_reports_zero_exact_duplicates(tmp_path, monkeypatch):
|
||||
"""The health warning the issue hit is gone at the source."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "sample.py").write_text(
|
||||
"from pathlib import Path\n\n\n"
|
||||
"def two_params(a: Path, b: Path) -> None:\n"
|
||||
" print(a, b)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
r = extract([tmp_path / "sample.py"], cache_root=tmp_path)
|
||||
summary = diagnose_extraction(r)
|
||||
assert summary["exact_duplicate_edges"] == 0
|
||||
|
||||
|
||||
def test_differing_locations_and_contexts_survive(tmp_path, monkeypatch):
|
||||
"""Only byte-identical edges collapse: the same type referenced at two
|
||||
locations, or under two contexts at one location, keeps every edge."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "sample.py").write_text(
|
||||
"from pathlib import Path\n\n\n"
|
||||
"def alpha(a: Path) -> None:\n"
|
||||
" print(a)\n\n\n"
|
||||
"def beta(b: Path) -> Path:\n"
|
||||
" return b\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
r = extract([tmp_path / "sample.py"], cache_root=tmp_path)
|
||||
counts = _edge_counts(r)
|
||||
# Two locations → two edges.
|
||||
assert counts[("sample_alpha", "path", "references", "L4", "parameter_type")] == 1
|
||||
assert counts[("sample_beta", "path", "references", "L8", "parameter_type")] == 1
|
||||
# Same location, different context (parameter vs return) → both edges.
|
||||
assert counts[("sample_beta", "path", "references", "L8", "return_type")] == 1
|
||||
Reference in new issue
Block a user