Skip to content

[Bug]: relative imports in __init__.py resolve to wrong or empty targets (missing package context) #54

Description

@orenlab

Metadata

Field Value
CodeClone version 2.1.0a1 (reproduced on release/2.1.0a2, commit 932d4b73)
Affected area Quality metrics / health

What happened?

Relative imports written inside a package __init__.py are resolved incorrectly or silently dropped, corrupting the dependency graph, coupling/cohesion inputs, and relationship facts. This is independent of the src-layout defect (#53) and reproduces in flat layouts too.

Root cause: _resolve_import_target (codeclone/analysis/_module_walk.py:78-90) has no package context. It strips level segments from the raw module_name string, assuming the last dotted segment is always "the module itself". For pkg/__init__.py, module_name_from_path already collapses the name to pkg (the package itself), so the arithmetic over-strips by one level. There is no is_package concept anywhere in the codebase (rg "is_package" → 0 hits).

Proof by executing the unmodified production function:

in pkg/__init__.py (module_name = "pkg"):
  from . import x          -> ""            (falsy target; edge silently dropped
                                             at _module_walk.py:385,410)
  from .sibling import y   -> "sibling"     WRONG — missing "pkg." prefix; edge
                                             points at nonexistent top-level module
in pkg/mod.py (module_name = "pkg.mod"):
  from . import x          -> "pkg"         correct
  from .sibling import y   -> "pkg.sibling" correct

Downstream: _collect_import_from_node (_module_walk.py:377-419) only records a ModuleDep/import name when the target is truthy, so from . import x in any __init__.py produces no dependency edge, no import_names entry, no relationship binding — invisible to build_dep_graph, fan-in/out, cycles, coupling, and MCP implementation context. from .sibling import y records an edge to a module that does not exist.

What did you expect instead?

Relative imports must resolve from package context (package, is_package), not from arithmetic over the raw module string:

  • from . import x in pkg/__init__.py → target pkg (plus proven submodule pkg.x when it exists in the module inventory);
  • from .sibling import y in pkg/__init__.pypkg.sibling;
  • over-stripped levels (from ... beyond the top) → explicit unresolved_relative observation, never a silently dropped edge.

Minimal reproduction

# repo/pkg/__init__.py
from . import a            # edge pkg -> pkg.a: MISSING today
from .a import helper      # edge should be pkg -> pkg.a; today points at top-level "a"

# repo/pkg/a.py
def helper(): ...

Observed: dependency graph has no pkg -> pkg.a edge from __init__.py; the from .a import helper edge targets a, which is classified external and dropped by _is_internal_target.

Additional context

  • Fix contract: Phase 39 spec specs/phase-39-baseline-container-lanes.md §10.2.1 (normative decision table; mandatory package + is_package + node_kind on every module identity).
  • Related: [Bug]: dependency graph empty for src-layout repositories (module name canonicalization mismatch) #53 (src-layout canonicalization) — same missing-identity-owner root cause, different symptom; both are fixed by the Module Identity v2 registry.
  • Blast: dependency edges, cycles, depth, overloaded modules, coupling classes, relationship/call facts, MCP blast radius and implementation context. Clone fingerprints unaffected (qualname is metadata only, excluded from hash inputs).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions