Metadata
| Field |
Value |
| CodeClone version |
2.1.0a1 (reproduced on current main) |
| Affected area |
Quality metrics / health |
What happened?
For repositories using the standard PEP src-layout (src/pkg/... on sys.path), CodeClone builds a dependency graph with zero edges. HTML/CLI show "graph unavailable", fan-in/out 0/0, while per-file metrics (LOC, complexity) still render.
Root cause is a canonicalization mismatch between node names and import targets:
- Node / source names come from
module_name_from_path (codeclone/scanner/__init__.py), which derives dotted names relative to the repository root, so src/pkg/mod.py → src.pkg.mod.
- Edge targets come from
_resolve_import_target (codeclone/analysis/_module_walk.py), which returns Python-resolved module names (e.g. from pkg.b import x → pkg.b) — without the src. prefix.
- Edge filtering in
build_dep_graph → _internal_roots / _is_internal_target (codeclone/metrics/dependencies.py) computes internal roots from node names → {src, tests}. Target pkg.b fails because pkg ∉ {src, tests}. All internal edges are dropped.
Flat layout (pkg/mod.py at repo root) works: nodes are pkg.*, roots are {pkg}, edges survive.
What did you expect instead?
src/pkg/a.py importing pkg.b should produce nodes pkg.a, pkg.b and edge pkg.a → pkg.b.
- Same import in flat layout
pkg/a.py should produce the identical graph (already works today).
- Module Map / dependency metrics should reflect real internal imports for src-layout repos.
Minimal reproduction
src-layout (broken):
repo/
src/
pkg/
__init__.py
a.py # from pkg import b OR from pkg.b import something
b.py
pyproject.toml
Observed:
NODE names : src.pkg.a | src.pkg.b
EDGE target : pkg.b
internal_roots : {'src'}
target internal?: False
dep_graph edges : 0
Flat layout (control — works):
repo/
pkg/
__init__.py
a.py
b.py
Observed:
NODE names : pkg.a | pkg.b
internal_roots : {'pkg'}
target internal?: True
dep_graph edges : 1
Same import statement; only the presence of a top-level src/ directory toggles the graph from working to empty.
Proposed fix (single seam)
Fix module_name_from_path to emit dotted names relative to a detected source root, not the repo root.
Preferred detection order:
- Auto-detect
src/ — strip top-level src/ when it contains packages (zero-config).
[tool.codeclone] source_roots and/or [tool.setuptools] package-dir from pyproject.toml for non-standard layouts.
module_name_from_path call sites: codeclone/core/worker.py, codeclone/metrics/overloaded_modules.py.
Regression test shape
| Layout |
Import |
Expected nodes |
Expected edge |
src/pkg/a.py |
import pkg.b |
pkg.a, pkg.b |
pkg.a → pkg.b |
pkg/a.py (flat) |
import pkg.b |
pkg.a, pkg.b |
pkg.a → pkg.b |
Blast radius / migration notes
| Area |
Impact |
| Clone fingerprints |
Safe — CFG-based SHA-1; qualname is debug-only. No BASELINE_FINGERPRINT_VERSION change. |
| qualname / unit identity |
Shifts for src-layout repos (units.py → dead-code IDs, memory subject paths). Baselines remap under new keys — note in CHANGELOG. |
| Dependency graph / Module Map |
Primary fix target. |
| Overloaded modules table |
Fixed via shared module_name_from_path. |
Additional context
- Diagnosed read-only: traced
scanner/, _module_walk.py, dependencies.py; reproduced with synthetic repos.
- Not an SVG renderer issue — edges extracted then discarded as "external".
- No
source-root / package-dir concept in scanner/ or config/ today.
Metadata
main)What happened?
For repositories using the standard PEP src-layout (
src/pkg/...onsys.path), CodeClone builds a dependency graph with zero edges. HTML/CLI show "graph unavailable", fan-in/out0/0, while per-file metrics (LOC, complexity) still render.Root cause is a canonicalization mismatch between node names and import targets:
module_name_from_path(codeclone/scanner/__init__.py), which derives dotted names relative to the repository root, sosrc/pkg/mod.py→src.pkg.mod._resolve_import_target(codeclone/analysis/_module_walk.py), which returns Python-resolved module names (e.g.from pkg.b import x→pkg.b) — without thesrc.prefix.build_dep_graph→_internal_roots/_is_internal_target(codeclone/metrics/dependencies.py) computes internal roots from node names →{src, tests}. Targetpkg.bfails becausepkg ∉ {src, tests}. All internal edges are dropped.Flat layout (
pkg/mod.pyat repo root) works: nodes arepkg.*, roots are{pkg}, edges survive.What did you expect instead?
src/pkg/a.pyimportingpkg.bshould produce nodespkg.a,pkg.band edgepkg.a → pkg.b.pkg/a.pyshould produce the identical graph (already works today).Minimal reproduction
src-layout (broken):
Observed:
Flat layout (control — works):
Observed:
Same import statement; only the presence of a top-level
src/directory toggles the graph from working to empty.Proposed fix (single seam)
Fix
module_name_from_pathto emit dotted names relative to a detected source root, not the repo root.Preferred detection order:
src/— strip top-levelsrc/when it contains packages (zero-config).[tool.codeclone] source_rootsand/or[tool.setuptools] package-dirfrompyproject.tomlfor non-standard layouts.module_name_from_pathcall sites:codeclone/core/worker.py,codeclone/metrics/overloaded_modules.py.Regression test shape
src/pkg/a.pyimport pkg.bpkg.a,pkg.bpkg.a → pkg.bpkg/a.py(flat)import pkg.bpkg.a,pkg.bpkg.a → pkg.bBlast radius / migration notes
qualnameis debug-only. NoBASELINE_FINGERPRINT_VERSIONchange.units.py→ dead-code IDs, memory subject paths). Baselines remap under new keys — note in CHANGELOG.module_name_from_path.Additional context
scanner/,_module_walk.py,dependencies.py; reproduced with synthetic repos.source-root/package-dirconcept inscanner/orconfig/today.