"""Run experiments to compare code replacement approaches for JavaScript/TypeScript. This script tests all three approaches against the test cases and generates a comparison report. """ from __future__ import annotations import time from dataclasses import dataclass, field from pathlib import Path from typing import Optional from test_cases import get_test_cases @dataclass class ApproachResult: """Result from testing an approach on one test case.""" test_name: str passed: bool time_ms: float error: Optional[str] = None output: Optional[str] = None @dataclass class ApproachSummary: """Summary of results for one approach.""" name: str description: str passed: int = 0 failed: int = 0 errors: int = 0 total_time_ms: float = 0.0 available: bool = True results: list[ApproachResult] = field(default_factory=list) @property def total(self) -> int: return self.passed + self.failed + self.errors @property def pass_rate(self) -> float: if self.total == 0: return 0.0 return self.passed / self.total * 100 def test_approach_b() -> ApproachSummary: """Test Approach B: Text-based replacement.""" from approach_b_text_based import TextBasedReplacer summary = ApproachSummary( name="Approach B: Text-Based", description="Pure Python text manipulation using line numbers" ) replacer = TextBasedReplacer() for tc in get_test_cases(): start_time = time.perf_counter() try: result = replacer.replace_function(tc.original_source, tc.start_line, tc.end_line, tc.new_function) end_time = time.perf_counter() time_ms = (end_time - start_time) * 1000 # Normalize for comparison result_normalized = result.replace("\r\n", "\n") expected_normalized = tc.expected_result.replace("\r\n", "\n") passed = result_normalized == expected_normalized summary.results.append( ApproachResult(test_name=tc.name, passed=passed, time_ms=time_ms, output=result if not passed else None) ) if passed: summary.passed += 1 else: summary.failed += 1 summary.total_time_ms += time_ms except Exception as e: end_time = time.perf_counter() time_ms = (end_time - start_time) * 1000 summary.results.append(ApproachResult(test_name=tc.name, passed=False, time_ms=time_ms, error=str(e))) summary.errors += 1 summary.total_time_ms += time_ms return summary def test_approach_c() -> ApproachSummary: """Test Approach C: Hybrid (tree-sitter + text).""" try: from approach_c_hybrid import TREE_SITTER_AVAILABLE, HybridReplacer except ImportError: return ApproachSummary( name="Approach C: Hybrid", description="Tree-sitter analysis + text replacement", available=False ) if not TREE_SITTER_AVAILABLE: return ApproachSummary( name="Approach C: Hybrid", description="Tree-sitter analysis + text replacement", available=False ) summary = ApproachSummary(name="Approach C: Hybrid", description="Tree-sitter analysis + text replacement") js_replacer = HybridReplacer("javascript") ts_replacer = HybridReplacer("typescript") for tc in get_test_cases(): # Use TypeScript parser for TypeScript test cases is_typescript = "typescript" in tc.name or "interface" in tc.description.lower() replacer = ts_replacer if is_typescript else js_replacer start_time = time.perf_counter() try: result = replacer.replace_function_by_lines(tc.original_source, tc.start_line, tc.end_line, tc.new_function) end_time = time.perf_counter() time_ms = (end_time - start_time) * 1000 # Normalize for comparison result_normalized = result.replace("\r\n", "\n") expected_normalized = tc.expected_result.replace("\r\n", "\n") passed = result_normalized == expected_normalized summary.results.append( ApproachResult(test_name=tc.name, passed=passed, time_ms=time_ms, output=result if not passed else None) ) if passed: summary.passed += 1 else: summary.failed += 1 summary.total_time_ms += time_ms except Exception as e: end_time = time.perf_counter() time_ms = (end_time - start_time) * 1000 summary.results.append(ApproachResult(test_name=tc.name, passed=False, time_ms=time_ms, error=str(e))) summary.errors += 1 summary.total_time_ms += time_ms return summary def test_approach_a() -> ApproachSummary: """Test Approach A: jscodeshift/recast.""" summary = ApproachSummary( name="Approach A: jscodeshift", description="AST-based replacement via Node.js subprocess" ) try: from approach_a_jscodeshift import JsCodeshiftReplacer replacer = JsCodeshiftReplacer() if not replacer._check_node_available(): # noqa: SLF001 summary.available = False return summary except Exception: summary.available = False return summary # Note: Full jscodeshift testing requires npm packages # For now, we'll mark it as available but note limited testing summary.available = True # We won't run full tests since jscodeshift requires npm setup # Instead, note that this approach requires external dependencies return summary def generate_report(summaries: list[ApproachSummary]) -> str: """Generate a markdown report of the experiment results.""" report = [] report.append("# Code Replacement Experiment Results\n") report.append(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") # Overview table report.append("## Summary\n") report.append("| Approach | Available | Passed | Failed | Errors | Pass Rate | Total Time |") report.append("|----------|-----------|--------|--------|--------|-----------|------------|") for s in summaries: if s.available: report.append( f"| {s.name} | Yes | {s.passed} | {s.failed} | {s.errors} | " f"{s.pass_rate:.1f}% | {s.total_time_ms:.2f}ms |" ) else: report.append(f"| {s.name} | No | - | - | - | - | - |") report.append("") # Detailed results per approach for s in summaries: if not s.available: report.append(f"## {s.name}\n") report.append("**Status**: Not available (missing dependencies)\n") report.append(f"**Description**: {s.description}\n") continue report.append(f"## {s.name}\n") report.append(f"**Description**: {s.description}\n") report.append(f"**Pass Rate**: {s.pass_rate:.1f}% ({s.passed}/{s.total})\n") report.append(f"**Total Time**: {s.total_time_ms:.2f}ms\n") # List failures failures = [r for r in s.results if not r.passed] if failures: report.append("\n### Failed Tests\n") for f in failures: report.append(f"- **{f.test_name}**") if f.error: report.append(f" - Error: {f.error}") report.append("") # Recommendations report.append("## Recommendations\n") available_summaries = [s for s in summaries if s.available] if available_summaries: best = max(available_summaries, key=lambda s: (s.pass_rate, -s.total_time_ms)) report.append(f"**Recommended Approach**: {best.name}\n") report.append(f"- Pass Rate: {best.pass_rate:.1f}%") report.append(f"- Average Time: {best.total_time_ms / max(best.total, 1):.2f}ms per test") return "\n".join(report) def main() -> None: """Run all experiments and generate report.""" print("=" * 70) print("Code Replacement Strategy Experiments") print("=" * 70) print() summaries = [] # Test Approach B (always available) print("Testing Approach B: Text-Based...") summary_b = test_approach_b() summaries.append(summary_b) print(f" Results: {summary_b.passed}/{summary_b.total} passed ({summary_b.pass_rate:.1f}%)") print() # Test Approach C (requires tree-sitter) print("Testing Approach C: Hybrid (tree-sitter + text)...") summary_c = test_approach_c() summaries.append(summary_c) if summary_c.available: print(f" Results: {summary_c.passed}/{summary_c.total} passed ({summary_c.pass_rate:.1f}%)") else: print(" Not available (install tree-sitter packages)") print() # Test Approach A (requires Node.js) print("Testing Approach A: jscodeshift...") summary_a = test_approach_a() summaries.append(summary_a) if summary_a.available: print(" Available but requires full npm setup for testing") else: print(" Not available (Node.js not found)") print() # Generate report report = generate_report(summaries) # Save report report_path = Path(__file__).parent / "EXPERIMENT_RESULTS.md" report_path.write_text(report) print(f"Report saved to: {report_path}") print() # Print summary print("=" * 70) print("Summary") print("=" * 70) print(report) if __name__ == "__main__": main()