Skip to content

Commit ed274f5

Browse files
aksOpsclaude
andcommitted
fix(detector/python): emit anchor nodes for imports edges
Same anti-pattern fix as TypeScript imports (commit a9fb22d) — the Python structures detector emitted imports edges with raw file paths and module names as endpoints. Both endpoints lacked CodeNodes; every edge dropped. Fix: emit py:file:<path> for the source file once per detector pass, py:external:<module> for each imported module. The GraphBuilder dedup collapses the external nodes across files so the graph gets a real dependency view at no extra cost. Bench (airflow, 9151 Python-heavy files): - 95758 nodes, 134400 edges - 80181 nodes deduped (per-file + per-external collapsed across files) - 7888 phantom edges dropped (was higher pre-fix) The dedup count of 80k tells the story: pre-fix, those 80k import emissions each went to a phantom target. Now they collapse to ~thousands of unique external module nodes, and the imports edges actually survive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9897e36 commit ed274f5

2 files changed

Lines changed: 48 additions & 19 deletions

File tree

go/internal/detector/python/structures.go

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -199,29 +199,57 @@ func (d PythonStructuresDetector) Detect(ctx *detector.Context) *detector.Result
199199
}
200200
}
201201

202-
// Imports
202+
// Imports — emit anchor nodes so the edges survive the GraphBuilder
203+
// phantom-drop. Pre-fix, both endpoints were free-form strings (file
204+
// path and module name) with no matching CodeNode. The dedup map
205+
// collapses the per-file external nodes across files (every file
206+
// importing `requests` gets one shared py:external:requests target).
207+
fileModuleID := "py:file:" + fp
208+
importSeen := make(map[string]bool)
209+
ensureFile := func() {
210+
if !importSeen["__file__"] {
211+
importSeen["__file__"] = true
212+
fn := model.NewCodeNode(fileModuleID, model.NodeModule, fp)
213+
fn.FilePath = fp
214+
fn.Source = "PythonStructuresDetector"
215+
fn.Confidence = model.ConfidenceLexical
216+
fn.Properties["module_type"] = "py_file"
217+
nodes = append(nodes, fn)
218+
}
219+
}
220+
emitImport := func(mod string) {
221+
mod = strings.TrimSpace(mod)
222+
if mod == "" || importSeen["mod:"+mod] {
223+
return
224+
}
225+
importSeen["mod:"+mod] = true
226+
ensureFile()
227+
targetID := "py:external:" + mod
228+
if !importSeen["tgt:"+mod] {
229+
importSeen["tgt:"+mod] = true
230+
tn := model.NewCodeNode(targetID, model.NodeExternal, mod)
231+
tn.Source = "PythonStructuresDetector"
232+
tn.Confidence = model.ConfidenceLexical
233+
tn.Properties["module"] = mod
234+
nodes = append(nodes, tn)
235+
}
236+
e := model.NewCodeEdge(fileModuleID+"->imports->"+targetID, model.EdgeImports, fileModuleID, targetID)
237+
e.Source = "PythonStructuresDetector"
238+
e.Confidence = model.ConfidenceLexical
239+
edges = append(edges, e)
240+
}
203241
for _, m := range pyImportRE.FindAllStringSubmatchIndex(text, -1) {
204242
var fromMod string
205243
if m[2] >= 0 {
206244
fromMod = text[m[2]:m[3]]
207245
}
208-
importNames := text[m[4]:m[5]]
209246
if fromMod != "" {
210-
e := model.NewCodeEdge(fp+"->imports->"+fromMod, model.EdgeImports, fp, fromMod)
211-
e.Source = "PythonStructuresDetector"
212-
e.Confidence = model.ConfidenceLexical
213-
edges = append(edges, e)
214-
} else {
215-
for _, n := range strings.Split(importNames, ",") {
216-
t := strings.TrimSpace(n)
217-
if t == "" {
218-
continue
219-
}
220-
e := model.NewCodeEdge(fp+"->imports->"+t, model.EdgeImports, fp, t)
221-
e.Source = "PythonStructuresDetector"
222-
e.Confidence = model.ConfidenceLexical
223-
edges = append(edges, e)
224-
}
247+
emitImport(fromMod)
248+
continue
249+
}
250+
importNames := text[m[4]:m[5]]
251+
for _, n := range strings.Split(importNames, ",") {
252+
emitImport(n)
225253
}
226254
}
227255

go/internal/detector/python/structures_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ func TestPythonStructuresPositive(t *testing.T) {
5757
if methods < 4 {
5858
t.Errorf("expected at least 4 methods (top-level + class methods), got %d", methods)
5959
}
60-
if modules != 1 {
61-
t.Errorf("expected 1 module node (__all__), got %d", modules)
60+
// 1 __all__ module + 1 file-as-module (anchor for imports edges).
61+
if modules != 2 {
62+
t.Errorf("expected 2 module nodes (__all__ + file anchor), got %d", modules)
6263
}
6364
// Import edges + defines + extends
6465
if len(r.Edges) < 4 {

0 commit comments

Comments
 (0)