Skip to content

Commit 35985ed

Browse files
committed
fix(python): resolve inheritance targets canonically
This is the resolution of the bug that inheritance in Python was not resolved to an "implements" edge. The fix adds a post-processing step to the Python extractor: + Collect all parsed symbols and index them by short class name. + Resolve unambiguous inheritance targets to fully qualified symbols. + Preserve ambiguous targets unchanged to avoid incorrect edges.
1 parent 053939c commit 35985ed

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

internal/extractors/pythonextractor/python.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ func (e *PythonExtractor) Extract(ctx context.Context, repoPath string, files []
176176
fileModules[strings.TrimSuffix(f, ".py")] = true
177177
}
178178
resolveCallTargets(allFacts, fileModules, pkgDirs)
179+
resolveImplementsTargets(allFacts, fileModules, pkgDirs)
179180

180181
// Fold FastAPI include_router mount prefixes onto the bare decorator paths, so
181182
// a route reads as the path it actually serves ("/api/v1/cognify") rather than

internal/extractors/pythonextractor/resolve.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,52 @@ func resolveCallTargets(allFacts []facts.Fact, fileModules map[string]bool, pkgD
113113
}
114114
}
115115

116+
func resolveImplementsTargets(allFacts []facts.Fact, fileModules map[string]bool, pkgDirs map[string]bool) {
117+
fileIdx := buildSuffixIndex(fileModules, pkgDirs)
118+
topPkgs := importableRoots(fileModules, pkgDirs)
119+
reexports := buildReexportIndex(allFacts, pkgDirs)
120+
symbols := make(map[string]bool)
121+
byShortName := make(map[string][]string)
122+
for i := range allFacts {
123+
if allFacts[i].Kind != facts.KindSymbol {
124+
continue
125+
}
126+
name := allFacts[i].Name
127+
symbols[name] = true
128+
short := name
129+
if dot := strings.LastIndexByte(name, '.'); dot >= 0 {
130+
short = name[dot+1:]
131+
}
132+
byShortName[short] = append(byShortName[short], name)
133+
}
134+
135+
for i := range allFacts {
136+
f := &allFacts[i]
137+
if f.Kind != facts.KindSymbol {
138+
continue
139+
}
140+
for j := range f.Relations {
141+
rel := &f.Relations[j]
142+
if rel.Kind != facts.RelImplements {
143+
continue
144+
}
145+
if symbols[rel.Target] {
146+
continue
147+
}
148+
if strings.ContainsRune(rel.Target, '.') {
149+
if resolved, keep := resolveDottedTarget(rel.Target, fileIdx, topPkgs, fileDir(f.File), reexports, symbols); keep && symbols[resolved] {
150+
rel.Target = resolved
151+
}
152+
continue
153+
}
154+
candidates := byShortName[rel.Target]
155+
if len(candidates) == 1 {
156+
rel.Target = candidates[0]
157+
}
158+
}
159+
}
160+
}
161+
116162
// isDottedCallTarget reports whether a call target is an unresolved dotted path
117163
// (e.g. "a.b.c") rather than an already-resolved slash symbol name
118164
// ("dir/mod.sym") or a bare short name ("Foo").

0 commit comments

Comments
 (0)