diff --git a/scripts/refactoring_opportunity_finder.py b/scripts/refactoring_opportunity_finder.py new file mode 100755 index 0000000..ecd33b0 --- /dev/null +++ b/scripts/refactoring_opportunity_finder.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +Finds refactoring opportunities in codebases + +Engine ID: 10.4 + +Usage: + python3 scripts/refactoring_opportunity_finder.py --output proposals/refactoring_opportunity_finder.json + python3 scripts/refactoring_opportunity_finder.py --output proposals/refactoring_opportunity_finder.json --dry-run +""" + +import argparse +import json +import sys +from datetime import datetime, timezone + + +def generate_proposals(): + """Generate sample proposals for this engine.""" + # TODO: Implement actual proposal generation logic + return [ + { + "title": f"Sample improvement from 10.4", + "description": "This is a sample improvement proposal", + "impact": 5, + "effort": 3, + "category": "improvement", + "source_engine": "10.4", + "timestamp": datetime.now(timezone.utc).isoformat() + } + ] + + +def main(): + parser = argparse.ArgumentParser(description="Finds refactoring opportunities in codebases") + parser.add_argument("--output", required=True, help="Output file for proposals") + parser.add_argument("--dry-run", action="store_true", help="Don't write output file") + + args = parser.parse_args() + + proposals = generate_proposals() + + if not args.dry_run: + with open(args.output, "w") as f: + json.dump({"proposals": proposals}, f, indent=2) + print(f"Generated {len(proposals)} proposals -> {args.output}") + else: + print(f"Would generate {len(proposals)} proposals") + for p in proposals: + print(f" - {p['title']}") + + +if __name__ == "__main__": + main() diff --git a/scripts/test_refactoring_opportunity_finder.py b/scripts/test_refactoring_opportunity_finder.py new file mode 100644 index 0000000..de2e6fd --- /dev/null +++ b/scripts/test_refactoring_opportunity_finder.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Tests for scripts/refactoring_opportunity_finder.py — 10 tests.""" + +import json +import os +import sys +import tempfile + +sys.path.insert(0, os.path.dirname(__file__) or ".") +import importlib.util +spec = importlib.util.spec_from_file_location( + "rof", os.path.join(os.path.dirname(__file__) or ".", "refactoring_opportunity_finder.py")) +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) + +compute_file_complexity = mod.compute_file_complexity +calculate_refactoring_score = mod.calculate_refactoring_score +FileMetrics = mod.FileMetrics + + +def test_complexity_simple_function(): + """Simple function should have low complexity.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write(""" +def simple(): + return 42 +""") + f.flush() + avg, max_c, funcs, classes, lines = compute_file_complexity(f.name) + assert avg == 1.0, f"Expected 1.0, got {avg}" + assert max_c == 1, f"Expected 1, got {max_c}" + assert funcs == 1, f"Expected 1, got {funcs}" + assert classes == 0, f"Expected 0, got {classes}" + os.unlink(f.name) + print("PASS: test_complexity_simple_function") + + +def test_complexity_with_conditionals(): + """Function with if/else should have higher complexity.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write(""" +def complex_func(x): + if x > 0: + if x > 10: + return "big" + else: + return "small" + elif x < 0: + return "negative" + else: + return "zero" +""") + f.flush() + avg, max_c, funcs, classes, lines = compute_file_complexity(f.name) + # Base 1 + 3 if/elif + 1 nested if = 5 + assert max_c >= 4, f"Expected max_c >= 4, got {max_c}" + assert funcs == 1, f"Expected 1, got {funcs}" + os.unlink(f.name) + print("PASS: test_complexity_with_conditionals") + + +def test_complexity_with_loops(): + """Function with loops should increase complexity.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write(""" +def loop_func(items): + result = [] + for item in items: + if item > 0: + result.append(item) + while len(result) > 10: + result.pop() + return result +""") + f.flush() + avg, max_c, funcs, classes, lines = compute_file_complexity(f.name) + # Base 1 + 1 for + 1 if + 1 while = 4 + assert max_c >= 3, f"Expected max_c >= 3, got {max_c}" + os.unlink(f.name) + print("PASS: test_complexity_with_loops") + + +def test_complexity_with_class(): + """Class with methods should count both.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write(""" +class MyClass: + def method1(self): + if True: + pass + + def method2(self): + for i in range(10): + pass +""") + f.flush() + avg, max_c, funcs, classes, lines = compute_file_complexity(f.name) + assert classes == 1, f"Expected 1 class, got {classes}" + assert funcs == 2, f"Expected 2 functions, got {funcs}" + os.unlink(f.name) + print("PASS: test_complexity_with_class") + + +def test_complexity_syntax_error(): + """File with syntax error should return zeros.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: + f.write("def broken(:\n pass") + f.flush() + avg, max_c, funcs, classes, lines = compute_file_complexity(f.name) + assert avg == 0.0, f"Expected 0.0, got {avg}" + assert funcs == 0, f"Expected 0, got {funcs}" + os.unlink(f.name) + print("PASS: test_complexity_syntax_error") + + +def test_refactoring_score_high_complexity(): + """High complexity should give high score.""" + metrics = FileMetrics( + path="test.py", + lines=200, + complexity=15.0, + max_complexity=25, + functions=10, + classes=2, + churn_30d=5, + churn_90d=15, + test_coverage=0.3, + refactoring_score=0.0 + ) + score = calculate_refactoring_score(metrics) + assert score > 50, f"Expected score > 50, got {score}" + print("PASS: test_refactoring_score_high_complexity") + + +def test_refactoring_score_low_complexity(): + """Low complexity should give lower score.""" + metrics = FileMetrics( + path="test.py", + lines=50, + complexity=2.0, + max_complexity=3, + functions=3, + classes=0, + churn_30d=0, + churn_90d=1, + test_coverage=0.9, + refactoring_score=0.0 + ) + score = calculate_refactoring_score(metrics) + assert score < 30, f"Expected score < 30, got {score}" + print("PASS: test_refactoring_score_low_complexity") + + +def test_refactoring_score_high_churn(): + """High churn should increase score.""" + metrics = FileMetrics( + path="test.py", + lines=100, + complexity=5.0, + max_complexity=8, + functions=5, + classes=0, + churn_30d=10, + churn_90d=20, + test_coverage=0.5, + refactoring_score=0.0 + ) + score = calculate_refactoring_score(metrics) + # Churn should contribute significantly + assert score > 40, f"Expected score > 40 for high churn, got {score}" + print("PASS: test_refactoring_score_high_churn") + + +def test_refactoring_score_no_coverage(): + """No coverage data should assume medium risk.""" + metrics = FileMetrics( + path="test.py", + lines=100, + complexity=5.0, + max_complexity=8, + functions=5, + classes=0, + churn_30d=1, + churn_90d=2, + test_coverage=None, + refactoring_score=0.0 + ) + score = calculate_refactoring_score(metrics) + # Should have some score from the 5-point coverage component + assert score > 0, f"Expected positive score, got {score}" + print("PASS: test_refactoring_score_no_coverage") + + +def test_refactoring_score_large_file(): + """Large files should score higher.""" + metrics_small = FileMetrics( + path="small.py", + lines=50, + complexity=5.0, + max_complexity=8, + functions=3, + classes=0, + churn_30d=1, + churn_90d=2, + test_coverage=0.8, + refactoring_score=0.0 + ) + metrics_large = FileMetrics( + path="large.py", + lines=1000, + complexity=5.0, + max_complexity=8, + functions=3, + classes=0, + churn_30d=1, + churn_90d=2, + test_coverage=0.8, + refactoring_score=0.0 + ) + score_small = calculate_refactoring_score(metrics_small) + score_large = calculate_refactoring_score(metrics_large) + assert score_large > score_small, \ + f"Large file ({score_large}) should score higher than small ({score_small})" + print("PASS: test_refactoring_score_large_file") + + +def run_all(): + test_complexity_simple_function() + test_complexity_with_conditionals() + test_complexity_with_loops() + test_complexity_with_class() + test_complexity_syntax_error() + test_refactoring_score_high_complexity() + test_refactoring_score_low_complexity() + test_refactoring_score_high_churn() + test_refactoring_score_no_coverage() + test_refactoring_score_large_file() + print("\nAll 10 tests passed!") + + +if __name__ == "__main__": + run_all() \ No newline at end of file