Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/refactoring/modularization-and-rust-engine-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ graph TD
- Implemented parallel cache validation (`PersistentCache`) with Rayon & SHA-256 in Rust.
- Implemented native call graph resolution with $O(1)$ candidate lookups and module affinity matching.

4. **Phase 4 (In Progress - Ecosystem Modularization)**:
4. **Phase 4 (Complete - Ecosystem Modularization)**:
- **`code2graph` (Complete)**: Extracted graph data models (`AnalysisResult`, `FunctionInfo`, `FlowNode`, `FlowEdge`, `GraphQuery`) into standalone package at `packages/code2graph` and published to `https://github.com/semcod/code2graph`.
- **`code2toon` (Complete)**: Extracted TOON format specification, parser, validator, and diff engine into standalone package at `packages/code2toon`.
- **`code2flow`**: Extract visual diagrams and heavy rendering dependencies into standalone package.
- **`code2toon` (Complete)**: Extracted TOON format specification, parser, validator, and diff engine into standalone package at `packages/code2toon` and published to `https://github.com/semcod/code2toon`.
- **`code2flow` (Complete)**: Extracted Mermaid visual flow diagrams, call graph visualizations, and syntax validators into standalone package at `packages/code2flow`.
13 changes: 13 additions & 0 deletions packages/code2flow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# code2flow

Mermaid flow diagrams, call graph visualizations, and architectural diagrams for static code analysis.

## Features
- **Mermaid Exporters**: Generate compact architectural diagrams, full function-level flowcharts, and filtered call graphs (`flow.mmd`, `compact_flow.mmd`, `calls.mmd`).
- **Syntax Validation & Auto-fix**: Robust syntax validation for Mermaid diagrams with automated sanitization of labels, node names, and subgraph delimiters.
- **Pure Topology Input**: Accepts `AnalysisResult` from `code2graph` without coupling to file system crawlers or AST parsers.

## Installation
```bash
pip install code2flow
```
22 changes: 22 additions & 0 deletions packages/code2flow/code2flow/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""code2flow: Mermaid flow diagrams, call graph visualizations, and validation."""

from .compact import export_compact
from .flow_compact import export_flow_compact
from .flow_detailed import export_flow_detailed
from .flow_full import export_flow_full
from .calls import export_calls
from .validation import validate_mermaid_file
from .fix import fix_mermaid_file

__version__ = "0.1.0"

__all__ = [
"export_compact",
"export_flow_compact",
"export_flow_detailed",
"export_flow_full",
"export_calls",
"validate_mermaid_file",
"fix_mermaid_file",
"__version__",
]
70 changes: 70 additions & 0 deletions packages/code2flow/code2flow/calls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Mermaid calls export — calls.mmd simplified call graph."""

from typing import Dict, List, Set, Tuple

from code2graph import AnalysisResult

from .utils import (
readable_id,
safe_module,
resolve_callee,
write_file,
build_name_index,
)


def _collect_call_edges(
functions: dict, name_index: dict
) -> Tuple[Set[str], List[Tuple[str, str]]]:
"""Collect connected nodes and edges from call graph (max 500 edges)."""
connected: Set[str] = set()
edges: List[Tuple[str, str]] = []
for func_name, fi in functions.items():
for callee in fi.calls[:10]:
resolved = resolve_callee(callee, functions, name_index)
if resolved and resolved != func_name:
connected.add(func_name)
connected.add(resolved)
edges.append((func_name, resolved))
if len(edges) >= 500:
return connected, edges
return connected, edges


def export_calls(result: AnalysisResult, output_path: str) -> None:
"""Export simplified call graph — only connected nodes."""
lines = ["flowchart LR"]
name_index = build_name_index(result.functions)
connected, edges = _collect_call_edges(result.functions, name_index)

# Group connected nodes by module
from .utils import module_of

modules: Dict[str, list] = {}
for fn in connected:
module = module_of(fn)
fi = result.functions.get(fn)
if fi:
modules.setdefault(module, []).append((fn, fi))

for module, funcs in sorted(modules.items()):
safe_mod = safe_module(module)
lines.append(f" subgraph {safe_mod}")
for func_name, fi in funcs:
sid = readable_id(func_name)
short = fi.name[:30]
lines.append(f' {sid}["{short}"]')
lines.append(" end")

# Edges
seen: Set[Tuple[str, str]] = set()
for src, dst in edges:
pair = (readable_id(src), readable_id(dst))
if pair not in seen:
seen.add(pair)
lines.append(f" {pair[0]} --> {pair[1]}")

write_file(output_path, lines)


__all__ = ["export_calls"]
52 changes: 52 additions & 0 deletions packages/code2flow/code2flow/compact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Mermaid compact export — compact_flow.mmd module-level aggregation."""

from collections import defaultdict
from typing import Dict, Set, Tuple

from code2graph import AnalysisResult

from .utils import safe_module, resolve_callee, write_file, build_name_index


def _compute_module_stats(functions: dict, module_of) -> Tuple[Dict, Dict]:
"""Return (mod_funcs, mod_lines) counters keyed by module name."""
mod_funcs: Dict[str, int] = defaultdict(int)
mod_lines: Dict[str, int] = defaultdict(int)
for func_name, fi in functions.items():
mod = module_of(func_name)
mod_funcs[mod] += 1
mod_lines[mod] += (fi.end_line - fi.line if getattr(fi, "end_line", None) else 0)
return mod_funcs, mod_lines


def _compute_cross_edges(functions: dict, name_index: dict, module_of) -> Dict[Tuple[str, str], int]:
"""Return weighted cross-module edge counter."""
cross_edges: Dict[Tuple[str, str], int] = defaultdict(int)
for func_name, fi in functions.items():
src_mod = module_of(func_name)
for callee in fi.calls:
resolved = resolve_callee(callee, functions, name_index)
if resolved:
dst_mod = module_of(resolved)
if dst_mod != src_mod:
cross_edges[(src_mod, dst_mod)] += 1
return cross_edges


def export_compact(result: AnalysisResult, output_path: str) -> None:
"""Export module-level graph: one node per module, weighted edges."""
from .utils import module_of
lines = ["flowchart TD"]
name_index = build_name_index(result.functions)
mod_funcs, _ = _compute_module_stats(result.functions, module_of)
cross_edges = _compute_cross_edges(result.functions, name_index, module_of)
active_mods: Set[str] = {m for pair in cross_edges for m in pair} or set(mod_funcs)
for mod in sorted(active_mods):
lines.append(f' {safe_module(mod)}["{mod}<br/>{mod_funcs.get(mod, 0)} funcs"]')
for (src, dst), weight in sorted(cross_edges.items(), key=lambda x: -x[1]):
arrow = "==>" if weight > 3 else "-->"
lines.append(f" {safe_module(src)} {arrow}|{weight}| {safe_module(dst)}")
write_file(output_path, lines)


__all__ = ["export_compact"]
152 changes: 152 additions & 0 deletions packages/code2flow/code2flow/fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Mermaid file fixer — auto-repair common syntax errors."""

from pathlib import Path
from typing import List, Optional, Union


def _sanitize_label_text(txt: str) -> str:
"""Replace Mermaid syntax chars in labels with HTML entities."""
return (
txt.replace("&", "&amp;")
.replace('"', "&quot;")
.replace("[", "&#91;")
.replace("]", "&#93;")
.replace("(", "&#40;")
.replace(")", "&#41;")
.replace("{", "&#123;")
.replace("}", "&#125;")
.replace("|", "&#124;")
)


def _sanitize_node_id(node_id: str) -> str:
"""Make a Mermaid-safe node identifier."""
import re

node_id = (node_id or "").strip()
node_id = re.split(r"[\[\]\(\)\{\}\"\|\s]", node_id, maxsplit=1)[0]
node_id = re.sub(r"[^A-Za-z0-9_]", "_", node_id)
return node_id or "N"


def fix_mermaid_file(mmd_path: Union[str, Path]) -> bool:
"""Attempt to fix common Mermaid syntax errors."""
mmd_path = Path(mmd_path)
try:
content = mmd_path.read_text(encoding="utf-8")
lines = content.split("\n")
fixed_lines = []

for line in lines:
line = _fix_edge_line(line)
line = _fix_subgraph_line(line)
result = _fix_class_line(line)
if result is not None:
fixed_lines.extend(result)
else:
fixed_lines.append(line)

fixed_content = "\n".join(fixed_lines)
if fixed_content != content:
mmd_path.write_text(fixed_content, encoding="utf-8")
return True

except Exception as e:
print(f"Error fixing {mmd_path}: {e}")

return False


def _fix_edge_line(line: str) -> str:
"""Fix edge labels and endpoint issues."""
import re

if "-->" not in line:
return line

# Fix edge labels with pipe issues
line = _fix_edge_label_pipes(line)

# Fix stray trailing '|' after node IDs
line = re.sub(r"(\b[A-Za-z]\w*)\|\s*$", r"\1", line)

# Sanitize edge label content inside |...|
def _sanitize_edge_label(m):
"""Replace unsafe characters inside a Mermaid |label| with safe equivalents."""
return f"|{_sanitize_label_text(m.group(1))}|"

if "|" in line:
line = re.sub(r"\|([^|]{1,200})\|", _sanitize_edge_label, line)

# Sanitize edge endpoints (lines without labels)
if "|" not in line:
m = re.match(r"^(\s*)([^\s-]+)\s*-->\s*([^\s]+)\s*$", line)
if m:
indent, src, dst = m.groups()
line = f"{indent}{_sanitize_node_id(src)} --> {_sanitize_node_id(dst)}"

return line


def _fix_edge_label_pipes(line: str) -> str:
"""Fix edge labels with pipe/parenthesis issues."""
if "|" not in line or "-->|" not in line:
return line
parts = line.split("-->|", 1)
if len(parts) != 2:
return line
label_and_target = parts[1]
if "|" not in label_and_target:
return line
parts2 = label_and_target.split("|", 1)
if len(parts2) != 2:
return line
label_content, target = parts2
label_content = label_content.strip("|")
if label_content.endswith("("):
label_content = label_content[:-1]
elif label_content.count("(") > label_content.count(")"):
missing = label_content.count("(") - label_content.count(")")
label_content += ")" * missing
return f"{parts[0]}-->|{label_content}|{target}"


def _fix_subgraph_line(line: str) -> str:
"""Fix malformed subgraph IDs."""
if line.strip().startswith("subgraph "):
subgraph_part = line.strip()[9:].split("(", 1)
if len(subgraph_part) == 2:
subgraph_id, rest = subgraph_part
subgraph_id = (
subgraph_id.replace(".", "_").replace("-", "_").replace(":", "_")
)
line = f" subgraph {subgraph_id}({rest}"
return line


def _fix_class_line(line: str) -> Optional[List[str]]:
"""Fix class definitions with too many nodes. Returns list of lines or None."""
if line.strip().startswith("class ") and "," in line:
class_parts = line.split(" ", 1)
if len(class_parts) == 2:
nodes_and_class = class_parts[1]
nodes, class_name = nodes_and_class.rsplit(" ", 1)
node_list = nodes.split(",")
if len(node_list) > 10:
result = []
for i in range(0, len(node_list), 10):
chunk = ",".join(node_list[i : i + 10])
result.append(f" class {chunk} {class_name}")
return result
return None


__all__ = [
"fix_mermaid_file",
"_sanitize_label_text",
"_sanitize_node_id",
"_fix_edge_line",
"_fix_edge_label_pipes",
"_fix_subgraph_line",
"_fix_class_line",
]
Loading
Loading