Skip to main content

perl_tdd_support/tdd/
test_generator.rs

1//! Test generator for TDD workflow support
2//!
3//! This module provides automatic test generation for Perl code,
4//! supporting the red-green-refactor cycle of Test-Driven Development.
5//!
6//! ## LSP Workflow Integration
7//!
8//! Test generation operates within the **LSP workflow**:
9//! **Parse → Index → Navigate → Complete → Analyze**
10//!
11//! - **Parse Stage**: Analyzes Perl functions/methods to determine test requirements
12//! - **Index Stage**: Standardizes test patterns and identifies edge cases
13//! - **Navigate Stage**: Links related test cases and maintains test dependency relationships
14//! - **Complete Stage**: Generates test code using appropriate test frameworks (Test::More, Test2::V0)
15//! - **Analyze Stage**: Updates test coverage metrics and integrates with workspace symbols
16//!
17//! Essential for maintaining high-quality test coverage in enterprise Perl development
18//! systems where reliability and correctness are critical business requirements.
19//!
20//! ## Usage Examples
21//!
22//! ```rust,ignore
23//! use perl_parser::test_generator::{TestGenerator, TestFramework};
24//! use perl_parser::{Parser, ast::Node};
25//!
26//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
27//! let mut parser = Parser::new("sub add { my ($a, $b) = @_; return $a + $b; }");
28//! let ast = parser.parse()?;
29//! let generator = TestGenerator::new(TestFramework::TestMore);
30//! let test_cases = generator.generate_tests(&ast, "sub add { my ($a, $b) = @_; return $a + $b; }");
31//! println!("Generated {} test cases", test_cases.len());
32//! # Ok(())
33//! # }
34//! ```
35
36use crate::ast::{Node, NodeKind};
37use std::collections::HashMap;
38
39/// Test generator for creating unit tests from code
40///
41/// Automatically generates comprehensive test suites with support for multiple
42/// test frameworks and TDD workflow patterns.
43pub struct TestGenerator {
44    /// Test framework to use (Test::More, Test2::V0, etc.)
45    framework: TestFramework,
46    /// Options for test generation
47    options: TestGeneratorOptions,
48}
49
50/// Supported Perl test frameworks
51///
52/// Different test frameworks provide varying levels of functionality and syntax.
53#[derive(Debug, Clone, PartialEq)]
54pub enum TestFramework {
55    /// Test::More - Classic Perl testing framework
56    TestMore,
57    /// Test2::V0 - Modern Test2 ecosystem
58    Test2V0,
59    /// Test::Simple - Basic testing framework
60    TestSimple,
61    /// Test::Class - Class-based testing framework
62    TestClass,
63}
64
65/// Configuration options for test generation
66///
67/// Controls various aspects of test generation including coverage,
68/// performance testing, and mock usage.
69#[derive(Debug, Clone)]
70pub struct TestGeneratorOptions {
71    /// Generate tests for private methods
72    pub test_private: bool,
73    /// Generate edge case tests
74    pub edge_cases: bool,
75    /// Generate mock objects for dependencies
76    pub use_mocks: bool,
77    /// Generate data-driven tests
78    pub data_driven: bool,
79    /// Generate performance tests
80    pub perf_tests: bool,
81    /// Expected return values for generated tests
82    pub expected_values: HashMap<String, String>,
83    /// Performance thresholds in seconds for benchmarks
84    pub perf_thresholds: HashMap<String, f64>,
85}
86
87impl Default for TestGeneratorOptions {
88    fn default() -> Self {
89        Self {
90            test_private: false,
91            edge_cases: true,
92            use_mocks: true,
93            data_driven: true,
94            perf_tests: false,
95            expected_values: HashMap::new(),
96            perf_thresholds: HashMap::new(),
97        }
98    }
99}
100
101/// Generated test case
102#[derive(Debug, Clone)]
103pub struct TestCase {
104    /// Unique identifier for the test (e.g., "test_add")
105    pub name: String,
106    /// Human-readable description of what the test validates
107    pub description: String,
108    /// Generated Perl test code
109    pub code: String,
110    /// Whether the test is marked as TODO (needs user input)
111    pub is_todo: bool,
112}
113
114impl TestGenerator {
115    /// Create a new test generator for Perl parsing workflow test automation
116    ///
117    /// # Arguments
118    ///
119    /// * `framework` - Test framework to use for generating test code
120    ///
121    /// # Returns
122    ///
123    /// A configured test generator ready for Perl script test generation
124    ///
125    /// # Examples
126    ///
127    /// ```rust,ignore
128    /// use perl_parser::{TestGenerator, TestFramework};
129    ///
130    /// let generator = TestGenerator::new(TestFramework::TestMore);
131    /// // Generator ready for Perl parsing workflow test generation
132    /// ```
133    pub fn new(framework: TestFramework) -> Self {
134        Self { framework, options: TestGeneratorOptions::default() }
135    }
136
137    /// Create a new test generator with custom options
138    ///
139    /// # Arguments
140    ///
141    /// * `framework` - Test framework to use for generating test code
142    /// * `options` - Custom configuration options for test generation
143    pub fn with_options(framework: TestFramework, options: TestGeneratorOptions) -> Self {
144        Self { framework, options }
145    }
146
147    /// Generate tests for a given AST
148    pub fn generate_tests(&self, ast: &Node, source: &str) -> Vec<TestCase> {
149        let mut tests = Vec::new();
150
151        // Find all subroutines
152        let subs = self.find_subroutines(ast);
153
154        for sub in subs {
155            // Generate basic test
156            tests.push(self.generate_basic_test(&sub, source));
157
158            // Generate edge case tests if enabled
159            if self.options.edge_cases {
160                tests.extend(self.generate_edge_cases(&sub, source));
161            }
162
163            // Generate data-driven tests if enabled
164            if self.options.data_driven {
165                if let Some(test) = self.generate_data_driven_test(&sub, source) {
166                    tests.push(test);
167                }
168            }
169
170            // Generate performance test if enabled
171            if self.options.perf_tests {
172                tests.push(self.generate_perf_test(&sub, source));
173            }
174        }
175
176        // Generate module-level tests
177        tests.extend(self.generate_module_tests(ast, source));
178
179        tests
180    }
181
182    /// Find all subroutines in the AST
183    fn find_subroutines(&self, node: &Node) -> Vec<SubroutineInfo> {
184        let mut subs = Vec::new();
185        self.find_subroutines_recursive(node, &mut subs);
186        subs
187    }
188
189    fn find_subroutines_recursive(&self, node: &Node, subs: &mut Vec<SubroutineInfo>) {
190        match &node.kind {
191            NodeKind::Subroutine { name, signature, .. } => {
192                if let Some(name) = name {
193                    let is_private = name.starts_with('_');
194                    if !is_private || self.options.test_private {
195                        // Extract parameters from signature
196                        let params = self.extract_parameters(signature.as_deref());
197                        subs.push(SubroutineInfo {
198                            name: name.clone(),
199                            params,
200                            node: node.clone(),
201                            is_private,
202                        });
203                    }
204                }
205            }
206            _ => {
207                for child in node.children() {
208                    self.find_subroutines_recursive(child, subs);
209                }
210            }
211        }
212    }
213
214    /// Generate a basic test for a subroutine
215    fn generate_basic_test(&self, sub: &SubroutineInfo, _source: &str) -> TestCase {
216        let test_name = format!("test_{}", sub.name);
217        let description = format!("Basic test for {}", sub.name);
218
219        let code = match self.framework {
220            TestFramework::TestMore => self.generate_test_more_basic(&sub.name, &sub.params),
221            TestFramework::Test2V0 => self.generate_test2_basic(&sub.name, &sub.params),
222            TestFramework::TestSimple => self.generate_test_simple_basic(&sub.name, &sub.params),
223            TestFramework::TestClass => self.generate_test_class_basic(&sub.name, &sub.params),
224        };
225
226        TestCase { name: test_name, description, code, is_todo: false }
227    }
228
229    fn generate_test_more_basic(&self, name: &str, params: &Option<Vec<String>>) -> String {
230        let mut code = String::new();
231        code.push_str("use Test::More;\n\n");
232
233        code.push_str(&format!("subtest '{}' => sub {{\n", name));
234
235        if let Some(params) = params {
236            let args = self.generate_sample_args(params.len());
237            code.push_str(&format!("    my $result = {}({});\n", name, args));
238        } else {
239            code.push_str(&format!("    my $result = {}();\n", name));
240        }
241
242        if let Some(expected) = self.options.expected_values.get(name) {
243            code.push_str(&format!("    is($result, {}, 'Returns expected value');\n", expected));
244        } else {
245            code.push_str("    ok(defined $result, 'Function returns defined value');\n");
246        }
247
248        if name.starts_with("is_") || name.starts_with("has_") {
249            code.push_str("    ok($result == 0 || $result == 1, 'Returns boolean');\n");
250        }
251
252        code.push_str("};\n");
253        code
254    }
255
256    fn generate_test2_basic(&self, name: &str, params: &Option<Vec<String>>) -> String {
257        let mut code = String::new();
258        code.push_str("use Test2::V0;\n\n");
259
260        code.push_str(&format!("subtest '{}' => sub {{\n", name));
261
262        if let Some(params) = params {
263            let args = self.generate_sample_args(params.len());
264            code.push_str(&format!("    my $result = {}({});\n", name, args));
265            code.push_str("    ok($result, 'Function returns value');\n");
266        } else {
267            code.push_str(&format!("    my $result = {}();\n", name));
268            code.push_str("    ok($result, 'Function returns value');\n");
269        }
270
271        code.push_str("};\n");
272        code
273    }
274
275    fn generate_test_simple_basic(&self, name: &str, params: &Option<Vec<String>>) -> String {
276        let mut code = String::new();
277        code.push_str("use Test::Simple tests => 1;\n\n");
278
279        if let Some(params) = params {
280            let args = self.generate_sample_args(params.len());
281            code.push_str(&format!("ok({}({}), 'Test {}');\n", name, args, name));
282        } else {
283            code.push_str(&format!("ok({}(), 'Test {}');\n", name, name));
284        }
285
286        code
287    }
288
289    fn generate_test_class_basic(&self, name: &str, params: &Option<Vec<String>>) -> String {
290        let mut code = String::new();
291        code.push_str("use Test::Class::Most;\n\n");
292
293        code.push_str(&format!("sub test_{} : Test {{\n", name));
294        code.push_str("    my $self = shift;\n");
295
296        if let Some(params) = params {
297            let args = self.generate_sample_args(params.len());
298            code.push_str(&format!("    my $result = $self->{}({});\n", name, args));
299        } else {
300            code.push_str(&format!("    my $result = $self->{}();\n", name));
301        }
302
303        code.push_str("    ok($result, 'Function works');\n");
304        code.push_str("}\n");
305
306        code
307    }
308
309    /// Generate edge case tests
310    fn generate_edge_cases(&self, sub: &SubroutineInfo, _source: &str) -> Vec<TestCase> {
311        let mut tests = Vec::new();
312
313        if sub.params.is_some() {
314            // Test with undef parameters
315            tests.push(self.generate_undef_test(sub));
316
317            // Test with empty parameters
318            tests.push(self.generate_empty_test(sub));
319
320            // Test with wrong type parameters
321            tests.push(self.generate_type_test(sub));
322        }
323
324        tests
325    }
326
327    fn generate_undef_test(&self, sub: &SubroutineInfo) -> TestCase {
328        let test_name = format!("test_{}_undef", sub.name);
329        let description = format!("Test {} with undef parameters", sub.name);
330        let args = self.generate_edge_case_args(sub, "undef");
331
332        let code = match self.framework {
333            TestFramework::TestMore => {
334                format!(
335                    "use Test::More;\n\n\
336                     subtest '{} with undef' => sub {{\n    \
337                     eval {{ {}({}) }};\n    \
338                     ok(!$@, 'Handles undef gracefully');\n\
339                     }};\n",
340                    sub.name, sub.name, args
341                )
342            }
343            _ => String::new(), // Simplified for other frameworks
344        };
345
346        TestCase { name: test_name, description, code, is_todo: false }
347    }
348
349    fn generate_empty_test(&self, sub: &SubroutineInfo) -> TestCase {
350        let test_name = format!("test_{}_empty", sub.name);
351        let description = format!("Test {} with empty parameters", sub.name);
352        let args = self.generate_edge_case_args(sub, "''");
353
354        let code = match self.framework {
355            TestFramework::TestMore => {
356                format!(
357                    "use Test::More;\n\n\
358                     subtest '{} with empty params' => sub {{\n    \
359                     eval {{ {}({}) }};\n    \
360                     ok(!$@, 'Handles empty values');\n\
361                     }};\n",
362                    sub.name, sub.name, args
363                )
364            }
365            _ => String::new(),
366        };
367
368        TestCase { name: test_name, description, code, is_todo: false }
369    }
370
371    fn generate_type_test(&self, sub: &SubroutineInfo) -> TestCase {
372        let test_name = format!("test_{}_types", sub.name);
373        let description = format!("Test {} with different types", sub.name);
374        let numeric_args = self.generate_edge_case_args(sub, "123");
375        let string_args = self.generate_edge_case_args(sub, "'string'");
376        let array_args = self.generate_edge_case_args(sub, "[1,2,3]");
377        let hash_args = self.generate_edge_case_args(sub, "{a=>1}");
378
379        let code = match self.framework {
380            TestFramework::TestMore => {
381                format!(
382                    "use Test::More;\n\n\
383                     subtest '{} type checking' => sub {{\n    \
384                     # Test with different types\n    \
385                     eval {{ {}({}) }};\n    \
386                     eval {{ {}({}) }};\n    \
387                     eval {{ {}({}) }};\n    \
388                     eval {{ {}({}) }};\n    \
389                     pass('Handles different types');\n\
390                     }};\n",
391                    sub.name,
392                    sub.name,
393                    numeric_args,
394                    sub.name,
395                    string_args,
396                    sub.name,
397                    array_args,
398                    sub.name,
399                    hash_args
400                )
401            }
402            _ => String::new(),
403        };
404
405        TestCase { name: test_name, description, code, is_todo: false }
406    }
407
408    /// Generate data-driven test
409    fn generate_data_driven_test(&self, sub: &SubroutineInfo, _source: &str) -> Option<TestCase> {
410        sub.params.as_ref()?;
411
412        let test_name = format!("test_{}_data_driven", sub.name);
413        let description = format!("Data-driven test for {}", sub.name);
414
415        let code = match self.framework {
416            TestFramework::TestMore => {
417                format!(
418                    "use Test::More;\n\n\
419                     my @test_cases = (\n    \
420                     {{ input => 'test1', expected => 'result1' }},\n    \
421                     {{ input => 'test2', expected => 'result2' }},\n    \
422                     {{ input => 'test3', expected => 'result3' }},\n\
423                     );\n\n\
424                     for my $case (@test_cases) {{\n    \
425                     my $result = {}($case->{{input}});\n    \
426                     is($result, $case->{{expected}}, \n       \
427                     \"{}($case->{{input}}) returns $case->{{expected}}\");\n\
428                     }}\n",
429                    sub.name, sub.name
430                )
431            }
432            _ => String::new(),
433        };
434
435        Some(TestCase {
436            name: test_name,
437            description,
438            code,
439            is_todo: true, // Mark as TODO since user needs to fill in test data
440        })
441    }
442
443    /// Generate performance test
444    fn generate_perf_test(&self, sub: &SubroutineInfo, _source: &str) -> TestCase {
445        let test_name = format!("test_{}_performance", sub.name);
446        let description = format!("Performance test for {}", sub.name);
447
448        let code = match self.framework {
449            TestFramework::TestMore => {
450                let mut snippet = String::new();
451                snippet.push_str("use Test::More;\n");
452                snippet.push_str("use Benchmark qw(timeit);\n\n");
453                snippet.push_str(&format!("subtest '{} performance' => sub {{\n", sub.name));
454                snippet.push_str(&format!(
455                    "    my $result = timeit(1000, sub {{ {}() }});\n",
456                    sub.name
457                ));
458                if let Some(threshold) = self.options.perf_thresholds.get(&sub.name) {
459                    snippet.push_str(&format!(
460                        "    cmp_ok($result->real, '<', {}, 'Executes under threshold');\n",
461                        threshold
462                    ));
463                } else {
464                    snippet.push_str("    ok($result->real >= 0, 'Execution time recorded');\n");
465                }
466                snippet.push_str("};\n");
467                snippet
468            }
469            _ => String::new(),
470        };
471
472        TestCase { name: test_name, description, code, is_todo: true }
473    }
474
475    /// Generate module-level tests
476    fn generate_module_tests(&self, ast: &Node, _source: &str) -> Vec<TestCase> {
477        let mut tests = Vec::new();
478
479        // Find package declaration
480        if let Some(package_name) = self.find_package_name(ast) {
481            // Generate module load test
482            tests.push(self.generate_module_load_test(&package_name));
483
484            // Generate export test if module exports functions
485            if self.has_exports(ast) {
486                tests.push(self.generate_export_test(&package_name));
487            }
488
489            // Generate new() test if it's an OO module
490            if self.has_constructor(ast) {
491                tests.push(self.generate_constructor_test(&package_name));
492            }
493        }
494
495        tests
496    }
497
498    fn find_package_name(&self, node: &Node) -> Option<String> {
499        match &node.kind {
500            NodeKind::Package { name, .. } => Some(name.clone()),
501            _ => {
502                for child in node.children() {
503                    if let Some(name) = self.find_package_name(child) {
504                        return Some(name);
505                    }
506                }
507                None
508            }
509        }
510    }
511
512    fn has_exports(&self, node: &Node) -> bool {
513        // Check if module uses Exporter
514        self.find_use_statement(node, "Exporter").is_some()
515    }
516
517    fn has_constructor(&self, node: &Node) -> bool {
518        // Check if module has a new() method
519        self.find_subroutine(node, "new").is_some()
520    }
521
522    fn find_use_statement(&self, node: &Node, module: &str) -> Option<Node> {
523        match &node.kind {
524            NodeKind::Use { module: m, .. } if m == module => Some(node.clone()),
525            _ => {
526                for child in node.children() {
527                    if let Some(result) = self.find_use_statement(child, module) {
528                        return Some(result);
529                    }
530                }
531                None
532            }
533        }
534    }
535
536    fn find_subroutine(&self, node: &Node, name: &str) -> Option<Node> {
537        match &node.kind {
538            NodeKind::Subroutine { name: Some(n), .. } if n == name => Some(node.clone()),
539            _ => {
540                for child in node.children() {
541                    if let Some(result) = self.find_subroutine(child, name) {
542                        return Some(result);
543                    }
544                }
545                None
546            }
547        }
548    }
549
550    fn generate_module_load_test(&self, package: &str) -> TestCase {
551        let test_name = "test_module_loads".to_string();
552        let description = format!("Test that {} loads correctly", package);
553
554        let code = match self.framework {
555            TestFramework::TestMore => {
556                format!(
557                    "use Test::More;\n\n\
558                     BEGIN {{\n    \
559                     use_ok('{}') || print \"Bail out!\\n\";\n\
560                     }}\n\n\
561                     diag(\"Testing {} ${}::VERSION, Perl $], $^X\");\n",
562                    package, package, package
563                )
564            }
565            _ => String::new(),
566        };
567
568        TestCase { name: test_name, description, code, is_todo: false }
569    }
570
571    fn generate_export_test(&self, package: &str) -> TestCase {
572        let test_name = "test_exports".to_string();
573        let description = format!("Test {} exports", package);
574
575        let code = match self.framework {
576            TestFramework::TestMore => {
577                format!(
578                    "use Test::More;\n\
579                     use {};\n\n\
580                     can_ok('main', @{}::EXPORT);\n",
581                    package, package
582                )
583            }
584            _ => String::new(),
585        };
586
587        TestCase { name: test_name, description, code, is_todo: false }
588    }
589
590    fn generate_constructor_test(&self, package: &str) -> TestCase {
591        let test_name = "test_constructor".to_string();
592        let description = format!("Test {} constructor", package);
593
594        let code = match self.framework {
595            TestFramework::TestMore => {
596                format!(
597                    "use Test::More;\n\
598                     use {};\n\n\
599                     subtest 'constructor' => sub {{\n    \
600                     my $obj = {}->new();\n    \
601                     isa_ok($obj, '{}');\n    \
602                     can_ok($obj, 'new');\n\
603                     }};\n",
604                    package, package, package
605                )
606            }
607            _ => String::new(),
608        };
609
610        TestCase { name: test_name, description, code, is_todo: false }
611    }
612
613    fn generate_sample_args(&self, count: usize) -> String {
614        let args: Vec<String> = (0..count).map(|i| format!("'arg{}'", i + 1)).collect();
615        args.join(", ")
616    }
617
618    fn generate_edge_case_args(&self, sub: &SubroutineInfo, value: &str) -> String {
619        let count = sub.params.as_ref().map_or(0, Vec::len);
620        vec![value; count].join(", ")
621    }
622
623    /// Extract parameters from a subroutine signature
624    fn extract_parameters(&self, signature: Option<&Node>) -> Option<Vec<String>> {
625        if let Some(sig) = signature {
626            if let NodeKind::Signature { parameters } = &sig.kind {
627                let mut param_names = Vec::new();
628                for param in parameters {
629                    match &param.kind {
630                        NodeKind::MandatoryParameter { variable }
631                        | NodeKind::OptionalParameter { variable, .. } => {
632                            if let NodeKind::Variable { name, .. } = &variable.kind {
633                                param_names.push(name.clone());
634                            }
635                        }
636                        _ => {}
637                    }
638                }
639                if param_names.is_empty() { None } else { Some(param_names) }
640            } else {
641                None
642            }
643        } else {
644            None
645        }
646    }
647}
648
649#[derive(Debug, Clone)]
650struct SubroutineInfo {
651    name: String,
652    params: Option<Vec<String>>,
653    #[allow(dead_code)]
654    node: Node,
655    #[allow(dead_code)]
656    is_private: bool,
657}
658
659/// Test runner integration for executing Perl tests
660///
661/// Provides functionality for running test suites, watch mode for continuous
662/// testing, and coverage tracking integration.
663pub struct TestRunner {
664    /// Command to run tests (e.g., "prove -l")
665    test_command: String,
666    /// Whether watch mode is enabled for continuous testing
667    watch_mode: bool,
668    /// Whether coverage tracking is enabled
669    coverage: bool,
670}
671
672impl Default for TestRunner {
673    fn default() -> Self {
674        Self::new()
675    }
676}
677
678impl TestRunner {
679    /// Create a new test runner with default settings
680    ///
681    /// Uses "prove -l" as the default test command.
682    pub fn new() -> Self {
683        Self { test_command: "prove -l".to_string(), watch_mode: false, coverage: false }
684    }
685
686    /// Create a new test runner with a custom command
687    ///
688    /// # Arguments
689    ///
690    /// * `command` - The shell command to execute tests (e.g., "prove -v -l")
691    pub fn with_command(command: String) -> Self {
692        Self { test_command: command, watch_mode: false, coverage: false }
693    }
694
695    /// Run tests and return results
696    pub fn run_tests(&self, test_files: &[String]) -> TestResults {
697        let mut results = TestResults::default();
698
699        // Build command
700        let mut cmd = self.test_command.clone();
701
702        if self.coverage {
703            cmd = format!("cover -test {}", cmd);
704        }
705
706        for file in test_files {
707            cmd.push(' ');
708            cmd.push_str(file);
709        }
710
711        // Execute tests (simplified - would use std::process::Command in real impl)
712        results.total = test_files.len();
713        results.passed = test_files.len(); // Assume all pass for now
714
715        results
716    }
717
718    /// Run tests in watch mode
719    pub fn watch(&self, _test_files: &[String]) -> Result<(), String> {
720        if !self.watch_mode {
721            return Err("Watch mode not enabled".to_string());
722        }
723
724        // Would implement file watching here
725        Ok(())
726    }
727
728    /// Get test coverage
729    pub fn get_coverage(&self) -> Option<CoverageReport> {
730        if !self.coverage {
731            return None;
732        }
733
734        Some(CoverageReport {
735            line_coverage: 85.0,
736            branch_coverage: 75.0,
737            function_coverage: 90.0,
738            uncovered_lines: vec![],
739        })
740    }
741}
742
743/// Results from executing a test suite
744#[derive(Debug, Default, Clone)]
745pub struct TestResults {
746    /// Total number of tests executed
747    pub total: usize,
748    /// Number of tests that passed
749    pub passed: usize,
750    /// Number of tests that failed
751    pub failed: usize,
752    /// Number of tests that were skipped
753    pub skipped: usize,
754    /// Number of tests marked as TODO
755    pub todo: usize,
756    /// Error messages from failed tests
757    pub errors: Vec<String>,
758}
759
760/// Code coverage metrics from test execution
761#[derive(Debug)]
762pub struct CoverageReport {
763    /// Percentage of lines covered (0.0 to 100.0)
764    pub line_coverage: f64,
765    /// Percentage of branches covered (0.0 to 100.0)
766    pub branch_coverage: f64,
767    /// Percentage of functions covered (0.0 to 100.0)
768    pub function_coverage: f64,
769    /// Line numbers that were not covered by tests
770    pub uncovered_lines: Vec<usize>,
771}
772
773/// Refactoring suggestions for the green-to-refactor phase
774///
775/// Analyzes code to identify opportunities for improvement such as
776/// duplicate code, complex methods, and naming issues.
777pub struct RefactoringSuggester {
778    /// Collection of refactoring suggestions found during analysis
779    suggestions: Vec<RefactoringSuggestion>,
780}
781
782/// A specific refactoring suggestion with context and actions
783#[derive(Debug, Clone)]
784pub struct RefactoringSuggestion {
785    /// Short title summarizing the issue
786    pub title: String,
787    /// Detailed description of the problem and recommended fix
788    pub description: String,
789    /// Importance level for prioritizing refactoring efforts
790    pub priority: Priority,
791    /// Type of refactoring suggested
792    pub category: RefactoringCategory,
793    /// LSP code action identifier (e.g., "extract_method", "rename")
794    pub code_action: Option<String>,
795}
796
797/// Priority level for refactoring suggestions
798#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
799pub enum Priority {
800    /// Minor improvement, can be addressed when convenient
801    Low,
802    /// Moderate issue that should be addressed
803    Medium,
804    /// Important issue requiring attention soon
805    High,
806    /// Urgent issue that should be fixed immediately
807    Critical,
808}
809
810/// Categories of refactoring suggestions
811#[derive(Debug, Clone, PartialEq)]
812pub enum RefactoringCategory {
813    /// Code that appears in multiple places and should be extracted
814    DuplicateCode,
815    /// Method with high cyclomatic complexity
816    ComplexMethod,
817    /// Method with too many lines of code
818    LongMethod,
819    /// Function with excessive parameter count
820    TooManyParameters,
821    /// Unreachable or unused code
822    DeadCode,
823    /// Inefficient code that could be optimized
824    Performance,
825    /// Poorly named variables or functions
826    Naming,
827    /// Structural issues with code organization
828    Structure,
829}
830
831impl Default for RefactoringSuggester {
832    fn default() -> Self {
833        Self::new()
834    }
835}
836
837impl RefactoringSuggester {
838    /// Create a new refactoring suggester
839    pub fn new() -> Self {
840        Self { suggestions: Vec::new() }
841    }
842
843    /// Analyze code and generate refactoring suggestions
844    pub fn analyze(&mut self, ast: &Node, source: &str) -> Vec<RefactoringSuggestion> {
845        self.suggestions.clear();
846
847        // Check for duplicate code
848        self.check_duplicate_code(ast, source);
849
850        // Check for complex methods
851        self.check_complex_methods(ast, source);
852
853        // Check for long methods
854        self.check_long_methods(ast, source);
855
856        // Check for too many parameters
857        self.check_parameter_count(ast);
858
859        // Check for naming issues
860        self.check_naming(ast);
861
862        // Sort by priority
863        self.suggestions.sort_by_key(|s| s.priority.clone());
864        self.suggestions.reverse();
865
866        self.suggestions.clone()
867    }
868
869    fn check_duplicate_code(&mut self, _ast: &Node, _source: &str) {
870        // Simplified duplicate detection
871        // In real implementation, would use similarity algorithms
872    }
873
874    fn check_complex_methods(&mut self, ast: &Node, _source: &str) {
875        self.check_complex_methods_recursive(ast);
876    }
877
878    fn check_complex_methods_recursive(&mut self, node: &Node) {
879        match &node.kind {
880            NodeKind::Subroutine { name, .. } => {
881                let complexity = self.calculate_cyclomatic_complexity(node);
882                if complexity > 10 {
883                    self.suggestions.push(RefactoringSuggestion {
884                        title: format!("High complexity in {}", name.as_ref().unwrap_or(&"anonymous".to_string())),
885                        description: format!("Cyclomatic complexity is {}. Consider breaking into smaller functions.", complexity),
886                        priority: if complexity > 20 { Priority::High } else { Priority::Medium },
887                        category: RefactoringCategory::ComplexMethod,
888                        code_action: Some("extract_method".to_string()),
889                    });
890                }
891            }
892            _ => {
893                for child in node.children() {
894                    self.check_complex_methods_recursive(child);
895                }
896            }
897        }
898    }
899
900    fn calculate_cyclomatic_complexity(&self, node: &Node) -> usize {
901        let mut complexity = 1;
902        self.count_decision_points(node, &mut complexity);
903        complexity
904    }
905
906    fn count_decision_points(&self, node: &Node, complexity: &mut usize) {
907        match &node.kind {
908            NodeKind::If { .. }
909            | NodeKind::While { .. }
910            | NodeKind::For { .. }
911            | NodeKind::Ternary { .. } => {
912                *complexity += 1;
913            }
914            NodeKind::Binary { op: operator, .. } => {
915                if operator == "&&" || operator == "||" || operator == "and" || operator == "or" {
916                    *complexity += 1;
917                }
918            }
919            _ => {}
920        }
921
922        for child in node.children() {
923            self.count_decision_points(child, complexity);
924        }
925    }
926
927    fn check_long_methods(&mut self, ast: &Node, source: &str) {
928        self.check_long_methods_recursive(ast, source);
929    }
930
931    fn check_long_methods_recursive(&mut self, node: &Node, source: &str) {
932        match &node.kind {
933            NodeKind::Subroutine { name, .. } => {
934                let lines = self.count_lines(node, source);
935                if lines > 50 {
936                    self.suggestions.push(RefactoringSuggestion {
937                        title: format!(
938                            "Long method: {}",
939                            name.as_ref().unwrap_or(&"anonymous".to_string())
940                        ),
941                        description: format!(
942                            "Method has {} lines. Consider breaking into smaller functions.",
943                            lines
944                        ),
945                        priority: if lines > 100 { Priority::High } else { Priority::Medium },
946                        category: RefactoringCategory::LongMethod,
947                        code_action: Some("extract_method".to_string()),
948                    });
949                }
950            }
951            _ => {
952                for child in node.children() {
953                    self.check_long_methods_recursive(child, source);
954                }
955            }
956        }
957    }
958
959    fn count_lines(&self, node: &Node, source: &str) -> usize {
960        let start = node.location.start;
961        let end = node.location.end;
962
963        let text = &source[start..end.min(source.len())];
964        text.lines().count()
965    }
966
967    fn check_parameter_count(&mut self, ast: &Node) {
968        self.check_parameter_count_recursive(ast);
969    }
970
971    fn check_parameter_count_recursive(&mut self, node: &Node) {
972        match &node.kind {
973            NodeKind::Subroutine { name, signature, .. } => {
974                let params = self.extract_parameters(signature.as_deref());
975                if let Some(params) = &params {
976                    if params.len() > 5 {
977                        self.suggestions.push(RefactoringSuggestion {
978                            title: format!(
979                                "Too many parameters in {}",
980                                name.as_ref().unwrap_or(&"anonymous".to_string())
981                            ),
982                            description: format!(
983                                "Function has {} parameters. Consider using a hash or object.",
984                                params.len()
985                            ),
986                            priority: Priority::Medium,
987                            category: RefactoringCategory::TooManyParameters,
988                            code_action: Some("introduce_parameter_object".to_string()),
989                        });
990                    }
991                }
992            }
993            _ => {
994                for child in node.children() {
995                    self.check_parameter_count_recursive(child);
996                }
997            }
998        }
999    }
1000
1001    fn check_naming(&mut self, ast: &Node) {
1002        self.check_naming_recursive(ast);
1003    }
1004
1005    fn check_naming_recursive(&mut self, node: &Node) {
1006        match &node.kind {
1007            NodeKind::Subroutine { name: Some(name), .. } => {
1008                if !self.is_good_name(name) {
1009                    self.suggestions.push(RefactoringSuggestion {
1010                        title: format!("Poor naming: {}", name),
1011                        description: "Consider using a more descriptive name".to_string(),
1012                        priority: Priority::Low,
1013                        category: RefactoringCategory::Naming,
1014                        code_action: Some("rename".to_string()),
1015                    });
1016                }
1017            }
1018            NodeKind::VariableDeclaration { variable, .. } => {
1019                if let NodeKind::Variable { name, .. } = &variable.kind {
1020                    if !self.is_good_variable_name(name) {
1021                        self.suggestions.push(RefactoringSuggestion {
1022                            title: format!("Poor variable name: {}", name),
1023                            description:
1024                                "Single letter variables should only be used for loop counters"
1025                                    .to_string(),
1026                            priority: Priority::Low,
1027                            category: RefactoringCategory::Naming,
1028                            code_action: Some("rename".to_string()),
1029                        });
1030                    }
1031                }
1032            }
1033            NodeKind::VariableListDeclaration { variables, .. } => {
1034                for var_node in variables {
1035                    if let NodeKind::Variable { name, .. } = &var_node.kind {
1036                        if !self.is_good_variable_name(name) {
1037                            self.suggestions.push(RefactoringSuggestion {
1038                                title: format!("Poor variable name: {}", name),
1039                                description:
1040                                    "Single letter variables should only be used for loop counters"
1041                                        .to_string(),
1042                                priority: Priority::Low,
1043                                category: RefactoringCategory::Naming,
1044                                code_action: Some("rename".to_string()),
1045                            });
1046                        }
1047                    }
1048                }
1049            }
1050            _ => {
1051                for child in node.children() {
1052                    self.check_naming_recursive(child);
1053                }
1054            }
1055        }
1056    }
1057
1058    fn is_good_name(&self, name: &str) -> bool {
1059        // Check for meaningful names
1060        name.len() > 2 && !name.chars().all(|c| c.is_uppercase())
1061    }
1062
1063    fn is_good_variable_name(&self, name: &str) -> bool {
1064        // Allow single letters for common loop variables
1065        if name.len() == 1 {
1066            return matches!(name, "$i" | "$j" | "$k" | "$n" | "$_");
1067        }
1068
1069        // Remove sigil for checking
1070        let clean_name = name.trim_start_matches(['$', '@', '%', '*']);
1071        clean_name.len() > 1
1072    }
1073
1074    /// Extract parameters from a subroutine signature
1075    fn extract_parameters(&self, signature: Option<&Node>) -> Option<Vec<String>> {
1076        if let Some(sig) = signature {
1077            if let NodeKind::Signature { parameters } = &sig.kind {
1078                let mut param_names = Vec::new();
1079                for param in parameters {
1080                    match &param.kind {
1081                        NodeKind::MandatoryParameter { variable }
1082                        | NodeKind::OptionalParameter { variable, .. } => {
1083                            if let NodeKind::Variable { name, .. } = &variable.kind {
1084                                param_names.push(name.clone());
1085                            }
1086                        }
1087                        _ => {}
1088                    }
1089                }
1090                if param_names.is_empty() { None } else { Some(param_names) }
1091            } else {
1092                None
1093            }
1094        } else {
1095            None
1096        }
1097    }
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102    use super::*;
1103
1104    #[test]
1105    fn test_generate_basic_test() {
1106        let generator = TestGenerator::new(TestFramework::TestMore);
1107        let ast = Node::new(
1108            NodeKind::Subroutine {
1109                name: Some("add".to_string()),
1110                name_span: None,
1111                signature: Some(Box::new(Node::new(
1112                    NodeKind::Signature {
1113                        parameters: vec![
1114                            Node::new(
1115                                NodeKind::MandatoryParameter {
1116                                    variable: Box::new(Node::new(
1117                                        NodeKind::Variable {
1118                                            name: "$a".to_string(),
1119                                            sigil: "$".to_string(),
1120                                        },
1121                                        crate::ast::SourceLocation { start: 0, end: 0 },
1122                                    )),
1123                                },
1124                                crate::ast::SourceLocation { start: 0, end: 0 },
1125                            ),
1126                            Node::new(
1127                                NodeKind::MandatoryParameter {
1128                                    variable: Box::new(Node::new(
1129                                        NodeKind::Variable {
1130                                            name: "$b".to_string(),
1131                                            sigil: "$".to_string(),
1132                                        },
1133                                        crate::ast::SourceLocation { start: 0, end: 0 },
1134                                    )),
1135                                },
1136                                crate::ast::SourceLocation { start: 0, end: 0 },
1137                            ),
1138                        ],
1139                    },
1140                    crate::ast::SourceLocation { start: 0, end: 0 },
1141                ))),
1142                body: Box::new(Node::new(
1143                    NodeKind::Block { statements: vec![] },
1144                    crate::ast::SourceLocation { start: 0, end: 0 },
1145                )),
1146                attributes: vec![],
1147                prototype: None,
1148            },
1149            crate::ast::SourceLocation { start: 0, end: 0 },
1150        );
1151
1152        let tests = generator.generate_tests(&ast, "sub add { }");
1153        assert!(!tests.is_empty());
1154        assert!(tests[0].code.contains("Test::More"));
1155        assert!(tests[0].code.contains("add"));
1156    }
1157
1158    #[test]
1159    fn test_edge_case_tests_preserve_signature_arity() -> Result<(), Box<dyn std::error::Error>> {
1160        let generator = TestGenerator::new(TestFramework::TestMore);
1161        let sub = SubroutineInfo {
1162            name: "add".to_string(),
1163            params: Some(vec!["$left".to_string(), "$right".to_string()]),
1164            node: Node::new(
1165                NodeKind::Block { statements: vec![] },
1166                crate::ast::SourceLocation { start: 0, end: 0 },
1167            ),
1168            is_private: false,
1169        };
1170
1171        let undef_test = generator.generate_undef_test(&sub);
1172        assert!(undef_test.code.contains("eval { add(undef, undef) };"));
1173
1174        let empty_test = generator.generate_empty_test(&sub);
1175        assert!(empty_test.code.contains("eval { add('', '') };"));
1176        assert!(!empty_test.code.contains("eval { add('', [], {}) };"));
1177
1178        let type_test = generator.generate_type_test(&sub);
1179        assert!(type_test.code.contains("eval { add(123, 123) };"));
1180        assert!(type_test.code.contains("eval { add('string', 'string') };"));
1181        assert!(type_test.code.contains("eval { add([1,2,3], [1,2,3]) };"));
1182        assert!(type_test.code.contains("eval { add({a=>1}, {a=>1}) };"));
1183
1184        Ok(())
1185    }
1186
1187    #[test]
1188    fn test_refactoring_suggestions() {
1189        let mut suggester = RefactoringSuggester::new();
1190
1191        // Create a complex subroutine
1192        let ast = Node::new(
1193            NodeKind::Subroutine {
1194                name: Some("complex_function".to_string()),
1195                name_span: None,
1196                signature: Some(Box::new(Node::new(
1197                    NodeKind::Signature {
1198                        parameters: (0..7)
1199                            .map(|i| {
1200                                Node::new(
1201                                    NodeKind::MandatoryParameter {
1202                                        variable: Box::new(Node::new(
1203                                            NodeKind::Variable {
1204                                                name: format!("$param{}", i),
1205                                                sigil: "$".to_string(),
1206                                            },
1207                                            crate::ast::SourceLocation { start: 0, end: 0 },
1208                                        )),
1209                                    },
1210                                    crate::ast::SourceLocation { start: 0, end: 0 },
1211                                )
1212                            })
1213                            .collect(),
1214                    },
1215                    crate::ast::SourceLocation { start: 0, end: 0 },
1216                ))),
1217                body: Box::new(Node::new(
1218                    NodeKind::Block {
1219                        statements: vec![
1220                            // Add some if statements to increase complexity
1221                            Node::new(
1222                                NodeKind::If {
1223                                    keyword: None,
1224                                    condition: Box::new(Node::new(
1225                                        NodeKind::Variable {
1226                                            name: "$a".to_string(),
1227                                            sigil: "$".to_string(),
1228                                        },
1229                                        crate::ast::SourceLocation { start: 0, end: 0 },
1230                                    )),
1231                                    then_branch: Box::new(Node::new(
1232                                        NodeKind::Block { statements: vec![] },
1233                                        crate::ast::SourceLocation { start: 0, end: 0 },
1234                                    )),
1235                                    elsif_branches: vec![],
1236                                    else_branch: None,
1237                                },
1238                                crate::ast::SourceLocation { start: 0, end: 0 },
1239                            ),
1240                        ],
1241                    },
1242                    crate::ast::SourceLocation { start: 0, end: 0 },
1243                )),
1244                attributes: vec![],
1245                prototype: None,
1246            },
1247            crate::ast::SourceLocation { start: 0, end: 0 },
1248        );
1249
1250        let suggestions = suggester.analyze(&ast, "sub complex_function { }");
1251
1252        // Should suggest parameter object for too many params
1253        assert!(suggestions.iter().any(|s| s.category == RefactoringCategory::TooManyParameters));
1254    }
1255
1256    #[test]
1257    fn test_cyclomatic_complexity() {
1258        let suggester = RefactoringSuggester::new();
1259
1260        // Create a node with multiple decision points
1261        let node = Node::new(
1262            NodeKind::Block {
1263                statements: vec![Node::new(
1264                    NodeKind::If {
1265                        keyword: None,
1266                        condition: Box::new(Node::new(
1267                            NodeKind::Variable { name: "$x".to_string(), sigil: "$".to_string() },
1268                            crate::ast::SourceLocation { start: 0, end: 0 },
1269                        )),
1270                        then_branch: Box::new(Node::new(
1271                            NodeKind::Block { statements: vec![] },
1272                            crate::ast::SourceLocation { start: 0, end: 0 },
1273                        )),
1274                        elsif_branches: vec![],
1275                        else_branch: None,
1276                    },
1277                    crate::ast::SourceLocation { start: 0, end: 0 },
1278                )],
1279            },
1280            crate::ast::SourceLocation { start: 0, end: 0 },
1281        );
1282
1283        let complexity = suggester.calculate_cyclomatic_complexity(&node);
1284        assert_eq!(complexity, 2); // Base 1 + 1 if statement
1285    }
1286}