"""Repo-wide module callsite graph. For each local-source module dir, list callsite dirs and sibling modules (other modules instantiated alongside it at any callsite). """ from __future__ import annotations from pathlib import Path, PurePosixPath import hcl2 from scripts.manifest import ModuleGraphEntry def _strip_hcl_quotes(s): if isinstance(s, str) and len(s) >= 2 and s[0] == '"' and s[-1] == '"': return s[1:-1] return s def _iter_module_blocks(tf_file: Path): try: with tf_file.open() as fh: parsed = hcl2.load(fh) except Exception: return for block in parsed.get("module", []): if not isinstance(block, dict): continue for name, body in block.items(): if isinstance(body, dict): src = body.get("source") if isinstance(src, list): src = src[0] if src else None src = _strip_hcl_quotes(src) yield _strip_hcl_quotes(name), src def _is_local_source(src: str | None) -> bool: if not isinstance(src, str): return False return src.startswith(("./", "../")) def _resolve_local(callsite_dir: PurePosixPath, source: str) -> str: combined = (callsite_dir / source).as_posix() parts: list[str] = [] for part in combined.split("/"): if part in ("", "."): continue if part == "..": if parts: parts.pop() continue parts.append(part) return "/".join(parts) def build_module_graph(repo_root: Path | str) -> dict[str, ModuleGraphEntry]: root = Path(repo_root) raw: dict[str, list[str]] = {} callsite_to_modules: dict[str, dict[str, str]] = {} for tf in root.rglob("*.tf"): rel_dir = tf.parent.relative_to(root).as_posix() callsite_dir = PurePosixPath(rel_dir) for name, src in _iter_module_blocks(tf): if not _is_local_source(src): continue target = _resolve_local(callsite_dir, src) # type: ignore[arg-type] raw.setdefault(target, []).append(rel_dir) callsite_to_modules.setdefault(rel_dir, {})[name] = target out: dict[str, ModuleGraphEntry] = {} for mod_dir, callsites in raw.items(): unique_callsites = sorted(set(callsites)) siblings: set[str] = set() for cs in unique_callsites: for other in callsite_to_modules.get(cs, {}).values(): if other != mod_dir: siblings.add(other) out[mod_dir] = ModuleGraphEntry( callsites=unique_callsites, sibling_modules_at_callsites=sorted(siblings), callsite_local_names={ cs: dict(callsite_to_modules.get(cs, {})) for cs in unique_callsites }, ) return out