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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.182
0.5.183
2 changes: 1 addition & 1 deletion code2llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
and entity resolution with multilingual support.
"""

__version__ = "0.5.182"
__version__ = "0.5.183"
__author__ = "STTS Project"

# Core analysis components (lightweight, always needed)
Expand Down
4 changes: 4 additions & 0 deletions code2llm/cli_exports/code2logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ def _build_code2logic_cmd(args, source_path: Path, output_dir: Path) -> list[str
"-o",
str(output_dir),
]
excludes = getattr(args, "exclude", None)
if excludes:
cmd.append("--exclude")
cmd.extend(str(x) for x in excludes)
if not args.verbose:
cmd.append("-q")
return cmd
Expand Down
4 changes: 4 additions & 0 deletions code2llm/exporters/flow_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
"egg-info",
".tox",
".mypy_cache",
".subactor",
".worktrees",
".planfile",
".benchmarks",
# Backup directories that often contain nested venvs
".algitex",
".backup",
Expand Down
2 changes: 1 addition & 1 deletion code2llm/exporters/map/module_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def render_module_list(result: AnalysisResult, is_excluded_path) -> List[str]:
if is_excluded_path(mi.file):
continue
rel = rel_path(mi.file, result.project_path)
lc = file_line_count(mi.file)
lc = getattr(mi, "line_count", 0) or file_line_count(mi.file)
modules.append((rel, lc))

lines = [f"M[{len(modules)}]:"]
Expand Down
2 changes: 1 addition & 1 deletion code2llm/exporters/map/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def count_total_lines(result: AnalysisResult, is_excluded_path) -> int:
for mi in result.modules.values():
if mi.file and mi.file not in seen and not is_excluded_path(mi.file):
seen.add(mi.file)
total += file_line_count(mi.file)
total += getattr(mi, "line_count", 0) or file_line_count(mi.file)
return total


Expand Down
2 changes: 1 addition & 1 deletion code2llm/exporters/map/yaml_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def _build_module_entry(
) -> Dict[str, Any]:
"""Build the YAML dict for a single module."""
rel = rel_path(mi.file, result.project_path)
lc = file_line_count(mi.file)
lc = getattr(mi, "line_count", 0) or file_line_count(mi.file)
return {
"path": rel,
"lines": lc,
Expand Down
31 changes: 20 additions & 11 deletions code2llm/exporters/toon/_render_section_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from collections import defaultdict
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional

from code2llm.core.models import AnalysisResult
from code2llm.core.config import LANGUAGE_EXTENSIONS
Expand Down Expand Up @@ -147,26 +147,33 @@ def _trace_pipeline(start_func: str, result: AnalysisResult, depth: int) -> List
return chain


def _calculate_purity(chain: List[str], result: AnalysisResult) -> float:
def _calculate_purity(
chain: List[str],
result: AnalysisResult,
name_to_func: Optional[Dict[str, Any]] = None,
) -> float:
"""Calculate purity ratio (functions without side effects)."""
if not chain:
return 0.0
pure_count = 0
for func_name in chain:
full_name = next(
(qname for qname, fi in result.functions.items() if fi.name == func_name),
None,
)
if full_name:
func_info = result.functions.get(full_name)
if func_info and not getattr(func_info, "has_side_effects", False):
pure_count += 1
if name_to_func is not None:
func_info = name_to_func.get(func_name)
else:
full_name = next(
(qname for qname, fi in result.functions.items() if fi.name == func_name),
None,
)
func_info = result.functions.get(full_name) if full_name else None
if func_info and not getattr(func_info, "has_side_effects", False):
pure_count += 1
return pure_count / len(chain)


def render_pipelines_section(ctx: Dict[str, Any]) -> List[str]:
"""Render PIPELINES section - data flow pipelines from entry points."""
result: AnalysisResult = ctx["result"]
name_to_func: Dict[str, Any] = {fi.name: fi for fi in result.functions.values()}
pipelines = []
for func_name, func_info in result.functions.items():
if not func_info.called_by and func_info.calls:
Expand All @@ -176,7 +183,9 @@ def render_pipelines_section(ctx: Dict[str, Any]) -> List[str]:
{
"entry": func_name.split(".")[-1],
"chain": chain,
"purity": _calculate_purity(chain, result),
"purity": _calculate_purity(
chain, result, name_to_func=name_to_func
),
}
)
if not pipelines:
Expand Down
23 changes: 21 additions & 2 deletions code2llm/exporters/toon/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,22 @@ def _hotspot_description(fi: FunctionInfo, fan_out: int) -> str:
return f"calls {fan_out} functions"


_WALK_EXCLUDE = {"venv", ".venv", "node_modules", "__pycache__", ".git", "dist", "build", ".tox", ".mypy_cache", "egg-info"} # noqa: E501
_WALK_EXCLUDE = {
"venv",
".venv",
"node_modules",
"__pycache__",
".git",
"dist",
"build",
".tox",
".mypy_cache",
"egg-info",
".subactor",
".worktrees",
".planfile",
".benchmarks",
}


def _fast_line_counts(pp: Path, result) -> Dict[str, int]:
Expand All @@ -103,7 +118,11 @@ def _fast_line_counts(pp: Path, result) -> Dict[str, int]:
if not mi.file:
continue
try:
lc = len(Path(mi.file).read_text(encoding="utf-8", errors="ignore").splitlines())
lc = getattr(mi, "line_count", 0)
if not lc:
lc = len(
Path(mi.file).read_text(encoding="utf-8", errors="ignore").splitlines()
)
counts[mi.file] = lc
counts[str(Path(mi.file).relative_to(pp))] = lc
except Exception:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "code2llm"
version = "0.5.182"
version = "0.5.183"
description = "High-performance Python code flow analysis with optimized TOON format - CFG, DFG, call graphs, and intelligent code queries"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
74 changes: 74 additions & 0 deletions tests/test_perf_exporters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Tests for performance optimizations in exporters and code2logic dispatch."""

from pathlib import Path
from types import SimpleNamespace
from code2llm.core.models import AnalysisResult, ModuleInfo, FunctionInfo
from code2llm.exporters.toon.helpers import _fast_line_counts, _WALK_EXCLUDE
from code2llm.exporters.flow_constants import EXCLUDE_PATTERNS, is_excluded_path
from code2llm.exporters.map.utils import count_total_lines
from code2llm.exporters.toon._render_section_helpers import _calculate_purity
from code2llm.cli_exports.code2logic import _build_code2logic_cmd


def test_fast_line_counts_reuses_module_line_count():
"""Verify _fast_line_counts uses mi.line_count without reading non-existent file."""
result = AnalysisResult()
result.modules = {
"fake.mod": ModuleInfo(
name="fake.mod",
file="/non/existent/path/fake.py",
line_count=42,
)
}
counts = _fast_line_counts(Path("/non/existent"), result)
assert counts.get("/non/existent/path/fake.py") == 42


def test_map_count_total_lines_reuses_line_count():
"""Verify count_total_lines uses mi.line_count."""
result = AnalysisResult()
result.modules = {
"mod1": ModuleInfo(name="mod1", file="/path/a.py", line_count=100),
"mod2": ModuleInfo(name="mod2", file="/path/b.py", line_count=50),
}
total = count_total_lines(result, lambda p: False)
assert total == 150


def test_calculate_purity_indexed():
"""Verify _calculate_purity works with indexed name_to_func."""
result = AnalysisResult()
f1 = FunctionInfo(name="func1", qualified_name="m.func1", file="a.py", line=1)
f2 = FunctionInfo(name="func2", qualified_name="m.func2", file="a.py", line=10)
setattr(f2, "has_side_effects", True)
result.functions = {"m.func1": f1, "m.func2": f2}

name_to_func = {fi.name: fi for fi in result.functions.values()}
purity = _calculate_purity(["func1", "func2"], result, name_to_func=name_to_func)
assert purity == 0.5


def test_build_code2logic_cmd_forwards_exclude():
"""Verify _build_code2logic_cmd forwards args.exclude."""
args = SimpleNamespace(
verbose=False,
exclude=["vendor/**", "node_modules"],
)
cmd = _build_code2logic_cmd(args, Path("/src"), Path("/out"))
assert "--exclude" in cmd
idx = cmd.index("--exclude")
assert cmd[idx + 1 : idx + 3] == ["vendor/**", "node_modules"]


def test_exclude_patterns_contain_subactor_worktrees():
"""Verify standard exclusions contain .subactor, .worktrees, etc."""
assert ".subactor" in EXCLUDE_PATTERNS
assert ".worktrees" in EXCLUDE_PATTERNS
assert ".planfile" in EXCLUDE_PATTERNS
assert ".benchmarks" in EXCLUDE_PATTERNS

assert ".subactor" in _WALK_EXCLUDE
assert ".worktrees" in _WALK_EXCLUDE

assert is_excluded_path("/project/.subactor/cache/file.py")
assert is_excluded_path("/project/.worktrees/ticket-123/main.py")
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading