mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-27 00:36:39 +00:00
fix(commonlisp): mint sourceless stubs for import targets so edges don't dangle
The extractor emitted `imports` edges to _cl_id(mod_name) with no node created, so a :use/require edge dangled (verified: sample.lisp left 'cl' and 'alexandria' as edgeless targets) and never resolved to an in-corpus defpackage. Mint a sourceless stub for each import target (the established cross-file pattern): edges now have real targets, the corpus rewire collapses a stub onto a unique in-corpus defpackage of the same name (:use :mylib -> the real package), and an external one (cl) persists as a clean leaf. origin_file is stripped before persist. Adds a no-dangling-edges regression test. Also adds the CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
038ada6f0f
commit
7765d610d8
@@ -4,6 +4,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu
|
||||
|
||||
## 0.9.46 (unreleased)
|
||||
|
||||
- Feature: Common Lisp `.lisp`/`.cl`/`.lsp`/`.asd` extraction via tree-sitter-commonlisp (optional `[commonlisp]` extra) — packages, classes, functions, methods, generics, macros, variable definers, and same-file calls; `open`ed/`:use`d packages resolve cross-file (thanks @fade).
|
||||
|
||||
- Fix: a `graphify query` whose node set fits the budget but whose edges push the total over now prints an honest "complete answer over budget" notice with the real size, instead of silently returning a payload several times the requested budget (and no longer advises raising the budget, which was the exact trigger); edges are still never dropped from a complete answer (#2784, thanks @AromalBiju1).
|
||||
- Fix: a `.gitignore`/`.graphifyignore` saved in a non-UTF-8 encoding no longer silently drops its rules (which let an explicitly-excluded directory get scanned anyway); the file is decoded UTF-8-first, then by a UTF-16 BOM, then the host codepage/latin-1, so the rule survives intact with a warning instead of being truncated (#2798, thanks @abhay-codes07).
|
||||
- Fix: when node dedup merges two nodes, any hyperedge that listed the merged-away node as a member now rewires that member to the survivor instead of silently dropping it, so a grouping no longer loses participants on dedup (#2805, thanks @abhay-codes07).
|
||||
|
||||
@@ -135,6 +135,25 @@ def extract_commonlisp(path: Path) -> dict:
|
||||
file_nid = _cl_id(stem)
|
||||
add_node(file_nid, path.name, 1)
|
||||
|
||||
def add_import_stub(mod_name: str) -> str:
|
||||
"""Mint a SOURCELESS stub for an imported package so its `imports` edge
|
||||
has a real target instead of dangling. The corpus rewire collapses it
|
||||
onto a unique in-corpus `defpackage` of the same name; an external one
|
||||
(cl, alexandria) stays a sourceless leaf. `origin_file` is stripped
|
||||
before persist (no #1899 leak); a sourced bare stub would salt the id."""
|
||||
nid = _cl_id(mod_name)
|
||||
if nid not in seen_ids:
|
||||
seen_ids.add(nid)
|
||||
nodes.append({
|
||||
"id": nid,
|
||||
"label": mod_name,
|
||||
"file_type": "code",
|
||||
"source_file": "",
|
||||
"source_location": "",
|
||||
"origin_file": str_path,
|
||||
})
|
||||
return nid
|
||||
|
||||
def _first_sym(node) -> str | None:
|
||||
"""Get the first sym_lit text from a list_lit's children."""
|
||||
for child in node.children:
|
||||
@@ -178,7 +197,7 @@ def extract_commonlisp(path: Path) -> dict:
|
||||
if uc.type == "kwd_lit" and uc != gc:
|
||||
mod_name = _kwd_text(uc)
|
||||
if mod_name != "use":
|
||||
tgt_nid = _cl_id(mod_name)
|
||||
tgt_nid = add_import_stub(mod_name)
|
||||
add_edge(pkg_nid, tgt_nid, "imports",
|
||||
child.start_point[0] + 1)
|
||||
break
|
||||
@@ -427,7 +446,7 @@ def extract_commonlisp(path: Path) -> dict:
|
||||
if child.type in ("kwd_lit", "str_lit"):
|
||||
mod_name = _kwd_text(child) if child.type == "kwd_lit" else _text(child).strip('"')
|
||||
if mod_name:
|
||||
tgt_nid = _cl_id(mod_name)
|
||||
tgt_nid = add_import_stub(mod_name)
|
||||
add_edge(file_nid, tgt_nid, "imports",
|
||||
top.start_point[0] + 1)
|
||||
break
|
||||
|
||||
@@ -3291,3 +3291,18 @@ def test_cl_defparameter_string_value_not_docstring():
|
||||
path.unlink()
|
||||
|
||||
|
||||
|
||||
@_needs_commonlisp
|
||||
def test_cl_import_edges_are_not_dangling():
|
||||
"""Every `imports` edge must target a real node (a sourceless stub the corpus
|
||||
rewire can collapse), not a nodeless id — otherwise the edge dangles."""
|
||||
r = extract_commonlisp(FIXTURES / "sample.lisp")
|
||||
ids = {n["id"] for n in r["nodes"]}
|
||||
dangling = [e for e in r["edges"] if e["source"] not in ids or e["target"] not in ids]
|
||||
assert not dangling, f"dangling edges: {dangling}"
|
||||
import_targets = [e["target"] for e in r["edges"] if e["relation"] == "imports"]
|
||||
assert import_targets, "sample.lisp uses packages, so it must emit imports edges"
|
||||
assert all(t in ids for t in import_targets)
|
||||
# the import-target stubs are sourceless so the corpus rewire can collapse them
|
||||
stub_labels = {n["label"] for n in r["nodes"] if n.get("source_file") == ""}
|
||||
assert "cl" in stub_labels
|
||||
|
||||
Reference in New Issue
Block a user