diff --git a/VERSION b/VERSION index bcfc69a..1db40e7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.182 +0.5.183 diff --git a/code2llm/__init__.py b/code2llm/__init__.py index 630d773..d621f8a 100644 --- a/code2llm/__init__.py +++ b/code2llm/__init__.py @@ -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) diff --git a/code2llm/cli_exports/code2logic.py b/code2llm/cli_exports/code2logic.py index 3bfcb3f..4c7c7f2 100644 --- a/code2llm/cli_exports/code2logic.py +++ b/code2llm/cli_exports/code2logic.py @@ -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 diff --git a/code2llm/exporters/flow_constants.py b/code2llm/exporters/flow_constants.py index f72960f..9d63f7f 100644 --- a/code2llm/exporters/flow_constants.py +++ b/code2llm/exporters/flow_constants.py @@ -27,6 +27,10 @@ "egg-info", ".tox", ".mypy_cache", + ".subactor", + ".worktrees", + ".planfile", + ".benchmarks", # Backup directories that often contain nested venvs ".algitex", ".backup", diff --git a/code2llm/exporters/map/module_list.py b/code2llm/exporters/map/module_list.py index 22b0db5..80c2f64 100644 --- a/code2llm/exporters/map/module_list.py +++ b/code2llm/exporters/map/module_list.py @@ -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)}]:"] diff --git a/code2llm/exporters/map/utils.py b/code2llm/exporters/map/utils.py index d0f765f..443d84e 100644 --- a/code2llm/exporters/map/utils.py +++ b/code2llm/exporters/map/utils.py @@ -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 diff --git a/code2llm/exporters/map/yaml_export.py b/code2llm/exporters/map/yaml_export.py index ab6d358..1be315a 100644 --- a/code2llm/exporters/map/yaml_export.py +++ b/code2llm/exporters/map/yaml_export.py @@ -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, diff --git a/code2llm/exporters/toon/_render_section_helpers.py b/code2llm/exporters/toon/_render_section_helpers.py index 22a869a..10ece12 100644 --- a/code2llm/exporters/toon/_render_section_helpers.py +++ b/code2llm/exporters/toon/_render_section_helpers.py @@ -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 @@ -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: @@ -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: diff --git a/code2llm/exporters/toon/helpers.py b/code2llm/exporters/toon/helpers.py index 47ed06f..c916a7c 100644 --- a/code2llm/exporters/toon/helpers.py +++ b/code2llm/exporters/toon/helpers.py @@ -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]: @@ -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: diff --git a/pyproject.toml b/pyproject.toml index d762a0e..1ea5ebc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/test_perf_exporters.py b/tests/test_perf_exporters.py new file mode 100644 index 0000000..59035b4 --- /dev/null +++ b/tests/test_perf_exporters.py @@ -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") diff --git a/uv.lock b/uv.lock index 13caf46..abb1c83 100644 --- a/uv.lock +++ b/uv.lock @@ -649,7 +649,7 @@ source = { editable = "packages/code2graph" } [[package]] name = "code2llm" -version = "0.5.182" +version = "0.5.183" source = { editable = "." } dependencies = [ { name = "astroid" },