Skip to main content

perl_tdd_support/tdd/
tdd_basic.rs

1//! Basic TDD workflow support for LSP
2//!
3//! Simplified TDD implementation focused on core red-green-refactor cycle
4
5use crate::ast::{Node, NodeKind};
6
7// Internal diagnostic type for TDD workflow (not dependent on lsp_types)
8#[derive(Debug, Clone)]
9pub struct Diagnostic {
10    /// Byte offset range (start line, end line) - 0-indexed
11    pub range: (usize, usize),
12    /// Severity level
13    pub severity: DiagnosticSeverity,
14    /// Optional diagnostic code
15    pub code: Option<String>,
16    /// Human-readable message
17    pub message: String,
18    /// Related diagnostic information
19    pub related_information: Vec<String>,
20    /// Diagnostic tags
21    pub tags: Vec<String>,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum DiagnosticSeverity {
26    Error,
27    Warning,
28    Information,
29    Hint,
30}
31
32/// Basic test generator
33pub struct TestGenerator {
34    framework: String,
35}
36
37impl TestGenerator {
38    pub fn new(framework: &str) -> Self {
39        Self { framework: framework.to_string() }
40    }
41
42    /// Generate basic test for a subroutine
43    pub fn generate_test(&self, name: &str, params: usize) -> String {
44        let args = (0..params).map(|i| format!("'arg{}'", i + 1)).collect::<Vec<_>>().join(", ");
45
46        match self.framework.as_str() {
47            "Test2::V0" => {
48                format!(
49                    "use Test2::V0;\n\n\
50                     subtest '{}' => sub {{\n    \
51                     my $result = {}({});\n    \
52                     ok($result, 'Function returns value');\n\
53                     }};\n\n\
54                     done_testing();\n",
55                    name, name, args
56                )
57            }
58            _ => {
59                // Default to Test::More
60                format!(
61                    "use Test::More;\n\n\
62                     subtest '{}' => sub {{\n    \
63                     my $result = {}({});\n    \
64                     ok(defined $result, 'Function returns defined value');\n\
65                     }};\n\n\
66                     done_testing();\n",
67                    name, name, args
68                )
69            }
70        }
71    }
72
73    /// Find all subroutines in AST
74    pub fn find_subroutines(&self, node: &Node) -> Vec<SubroutineInfo> {
75        let mut subs = Vec::new();
76        self.find_subroutines_recursive(node, &mut subs);
77        subs
78    }
79
80    fn find_subroutines_recursive(&self, node: &Node, subs: &mut Vec<SubroutineInfo>) {
81        match &node.kind {
82            NodeKind::Subroutine { name, signature, .. } => {
83                subs.push(SubroutineInfo {
84                    name: name.clone().unwrap_or_else(|| "anonymous".to_string()),
85                    param_count: signature
86                        .as_ref()
87                        .map(|s| {
88                            if let NodeKind::Signature { parameters } = &s.kind {
89                                parameters.len()
90                            } else {
91                                0
92                            }
93                        })
94                        .unwrap_or(0),
95                });
96            }
97            NodeKind::Program { statements } => {
98                for stmt in statements {
99                    self.find_subroutines_recursive(stmt, subs);
100                }
101            }
102            NodeKind::Block { statements } => {
103                for stmt in statements {
104                    self.find_subroutines_recursive(stmt, subs);
105                }
106            }
107            NodeKind::If { then_branch, elsif_branches, else_branch, .. } => {
108                self.find_subroutines_recursive(then_branch, subs);
109                for (_, branch) in elsif_branches {
110                    self.find_subroutines_recursive(branch, subs);
111                }
112                if let Some(branch) = else_branch {
113                    self.find_subroutines_recursive(branch, subs);
114                }
115            }
116            NodeKind::While { body, continue_block, .. }
117            | NodeKind::For { body, continue_block, .. } => {
118                self.find_subroutines_recursive(body, subs);
119                if let Some(cont) = continue_block {
120                    self.find_subroutines_recursive(cont, subs);
121                }
122            }
123            NodeKind::Foreach { body, .. }
124            | NodeKind::Given { body, .. }
125            | NodeKind::When { body, .. }
126            | NodeKind::Default { body } => {
127                self.find_subroutines_recursive(body, subs);
128            }
129            NodeKind::Package { block, .. } => {
130                if let Some(blk) = block {
131                    self.find_subroutines_recursive(blk, subs);
132                }
133            }
134            NodeKind::Class { body, .. } => {
135                self.find_subroutines_recursive(body, subs);
136            }
137            _ => {
138                // Other node types don't contain subroutines
139            }
140        }
141    }
142}
143
144#[derive(Debug, Clone)]
145pub struct SubroutineInfo {
146    pub name: String,
147    pub param_count: usize,
148}
149
150/// Basic refactoring analyzer
151pub struct RefactoringAnalyzer {
152    max_complexity: usize,
153    max_lines: usize,
154    max_params: usize,
155}
156
157impl Default for RefactoringAnalyzer {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163impl RefactoringAnalyzer {
164    pub fn new() -> Self {
165        Self { max_complexity: 10, max_lines: 50, max_params: 5 }
166    }
167
168    /// Analyze code and suggest refactorings
169    pub fn analyze(&self, node: &Node, source: &str) -> Vec<RefactoringSuggestion> {
170        let mut suggestions = Vec::new();
171        self.analyze_recursive(node, source, &mut suggestions);
172        suggestions
173    }
174
175    fn analyze_recursive(
176        &self,
177        node: &Node,
178        source: &str,
179        suggestions: &mut Vec<RefactoringSuggestion>,
180    ) {
181        match &node.kind {
182            NodeKind::Subroutine { name, signature, body, .. } => {
183                let sub_name = name.clone().unwrap_or_else(|| "anonymous".to_string());
184
185                // Check parameter count
186                let param_count = signature
187                    .as_ref()
188                    .map(|s| {
189                        if let NodeKind::Signature { parameters } = &s.kind {
190                            parameters.len()
191                        } else {
192                            0
193                        }
194                    })
195                    .unwrap_or(0);
196                if param_count > self.max_params {
197                    suggestions.push(RefactoringSuggestion {
198                        title: format!("Too many parameters in {}", sub_name),
199                        description: format!(
200                            "Function has {} parameters, consider using a hash",
201                            param_count
202                        ),
203                        category: RefactoringCategory::TooManyParameters,
204                    });
205                }
206
207                // Check complexity
208                let complexity = self.calculate_complexity(body);
209                if complexity > self.max_complexity {
210                    suggestions.push(RefactoringSuggestion {
211                        title: format!("High complexity in {}", sub_name),
212                        description: format!(
213                            "Cyclomatic complexity is {}, consider breaking into smaller functions",
214                            complexity
215                        ),
216                        category: RefactoringCategory::HighComplexity,
217                    });
218                }
219
220                // Check length
221                let lines = self.count_lines(body, source);
222                if lines > self.max_lines {
223                    suggestions.push(RefactoringSuggestion {
224                        title: format!("Long method: {}", sub_name),
225                        description: format!(
226                            "Method has {} lines, consider breaking into smaller functions",
227                            lines
228                        ),
229                        category: RefactoringCategory::LongMethod,
230                    });
231                }
232
233                // Recurse into body
234                self.analyze_recursive(body, source, suggestions);
235            }
236            NodeKind::Program { statements } | NodeKind::Block { statements } => {
237                for stmt in statements {
238                    self.analyze_recursive(stmt, source, suggestions);
239                }
240            }
241            NodeKind::If { then_branch, elsif_branches, else_branch, .. } => {
242                self.analyze_recursive(then_branch, source, suggestions);
243                for (_, branch) in elsif_branches {
244                    self.analyze_recursive(branch, source, suggestions);
245                }
246                if let Some(branch) = else_branch {
247                    self.analyze_recursive(branch, source, suggestions);
248                }
249            }
250            NodeKind::While { body, .. }
251            | NodeKind::For { body, .. }
252            | NodeKind::Foreach { body, .. }
253            | NodeKind::Given { body, .. }
254            | NodeKind::When { body, .. }
255            | NodeKind::Default { body }
256            | NodeKind::Class { body, .. } => {
257                self.analyze_recursive(body, source, suggestions);
258            }
259            NodeKind::Package { block, .. } => {
260                if let Some(blk) = block {
261                    self.analyze_recursive(blk, source, suggestions);
262                }
263            }
264            _ => {
265                // Other node types don't need analysis
266            }
267        }
268    }
269
270    fn calculate_complexity(&self, node: &Node) -> usize {
271        let mut complexity = 1;
272        self.count_decision_points(node, &mut complexity);
273        complexity
274    }
275
276    fn count_decision_points(&self, node: &Node, complexity: &mut usize) {
277        match &node.kind {
278            NodeKind::If { elsif_branches, .. } => {
279                *complexity += 1 + elsif_branches.len();
280            }
281            NodeKind::While { .. } | NodeKind::For { .. } | NodeKind::Foreach { .. } => {
282                *complexity += 1;
283            }
284            NodeKind::Binary { op, left, right } => {
285                if op == "&&" || op == "||" || op == "and" || op == "or" {
286                    *complexity += 1;
287                }
288                self.count_decision_points(left, complexity);
289                self.count_decision_points(right, complexity);
290                return;
291            }
292            _ => {}
293        }
294
295        // Recurse into children
296        match &node.kind {
297            NodeKind::Program { statements } | NodeKind::Block { statements } => {
298                for stmt in statements {
299                    self.count_decision_points(stmt, complexity);
300                }
301            }
302            NodeKind::If { then_branch, elsif_branches, else_branch, .. } => {
303                self.count_decision_points(then_branch, complexity);
304                for (_, branch) in elsif_branches {
305                    self.count_decision_points(branch, complexity);
306                }
307                if let Some(branch) = else_branch {
308                    self.count_decision_points(branch, complexity);
309                }
310            }
311            NodeKind::While { body, .. }
312            | NodeKind::For { body, .. }
313            | NodeKind::Foreach { body, .. }
314            | NodeKind::Given { body, .. }
315            | NodeKind::When { body, .. }
316            | NodeKind::Default { body }
317            | NodeKind::Class { body, .. } => {
318                self.count_decision_points(body, complexity);
319            }
320            _ => {}
321        }
322    }
323
324    fn count_lines(&self, node: &Node, source: &str) -> usize {
325        let start = node.location.start;
326        let end = node.location.end.min(source.len());
327
328        if start >= end {
329            return 0;
330        }
331
332        source[start..end].lines().count()
333    }
334}
335
336#[derive(Debug, Clone)]
337pub struct RefactoringSuggestion {
338    pub title: String,
339    pub description: String,
340    pub category: RefactoringCategory,
341}
342
343#[derive(Debug, Clone, PartialEq)]
344pub enum RefactoringCategory {
345    TooManyParameters,
346    HighComplexity,
347    LongMethod,
348}
349
350/// Simple TDD workflow state
351#[derive(Debug, Clone, PartialEq)]
352pub enum TddState {
353    Red,
354    Green,
355    Refactor,
356    Idle,
357}
358
359/// TDD workflow manager
360pub struct TddWorkflow {
361    state: TddState,
362    generator: TestGenerator,
363    analyzer: RefactoringAnalyzer,
364}
365
366impl TddWorkflow {
367    pub fn new(framework: &str) -> Self {
368        Self {
369            state: TddState::Idle,
370            generator: TestGenerator::new(framework),
371            analyzer: RefactoringAnalyzer::new(),
372        }
373    }
374
375    /// Start TDD cycle
376    pub fn start_cycle(&mut self, test_name: &str) -> TddResult {
377        self.state = TddState::Red;
378        TddResult {
379            state: self.state.clone(),
380            message: format!("Starting TDD cycle for '{}'", test_name),
381        }
382    }
383
384    /// Run tests and update state
385    pub fn run_tests(&mut self, success: bool) -> TddResult {
386        self.state = if success { TddState::Green } else { TddState::Red };
387
388        TddResult {
389            state: self.state.clone(),
390            message: if success {
391                "Tests passing, ready to refactor".to_string()
392            } else {
393                "Tests failing, fix implementation".to_string()
394            },
395        }
396    }
397
398    /// Move to refactor phase
399    pub fn start_refactor(&mut self) -> TddResult {
400        self.state = TddState::Refactor;
401        TddResult {
402            state: self.state.clone(),
403            message: "Refactoring phase - improve code while keeping tests green".to_string(),
404        }
405    }
406
407    /// Complete cycle
408    pub fn complete_cycle(&mut self) -> TddResult {
409        self.state = TddState::Idle;
410        TddResult { state: self.state.clone(), message: "TDD cycle complete".to_string() }
411    }
412
413    /// Generate test for function
414    pub fn generate_test(&self, name: &str, params: usize) -> String {
415        self.generator.generate_test(name, params)
416    }
417
418    /// Analyze code for refactoring
419    pub fn analyze_for_refactoring(&self, ast: &Node, source: &str) -> Vec<RefactoringSuggestion> {
420        self.analyzer.analyze(ast, source)
421    }
422
423    /// Get coverage diagnostics
424    pub fn get_coverage_diagnostics(&self, uncovered_lines: &[usize]) -> Vec<Diagnostic> {
425        uncovered_lines
426            .iter()
427            .map(|&line| Diagnostic {
428                range: (line, line),
429                severity: DiagnosticSeverity::Warning,
430                code: Some("tdd.uncovered".to_string()),
431                message: "Line not covered by tests".to_string(),
432                related_information: vec![],
433                tags: vec![],
434            })
435            .collect()
436    }
437}
438
439#[derive(Debug, Clone)]
440pub struct TddResult {
441    pub state: TddState,
442    pub message: String,
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    use crate::SourceLocation;
449
450    #[test]
451    fn test_test_generation() {
452        let generator = TestGenerator::new("Test::More");
453        let test = generator.generate_test("add", 2);
454        assert!(test.contains("Test::More"));
455        assert!(test.contains("add"));
456        assert!(test.contains("arg1"));
457        assert!(test.contains("arg2"));
458    }
459
460    #[test]
461    fn test_find_subroutines() {
462        let ast = Node::new(
463            NodeKind::Program {
464                statements: vec![Node::new(
465                    NodeKind::Subroutine {
466                        name: Some("test_func".to_string()),
467                        name_span: None,
468                        prototype: None,
469                        signature: None,
470                        attributes: vec![],
471                        body: Box::new(Node::new(
472                            NodeKind::Block { statements: vec![] },
473                            SourceLocation { start: 0, end: 0 },
474                        )),
475                    },
476                    SourceLocation { start: 0, end: 0 },
477                )],
478            },
479            SourceLocation { start: 0, end: 0 },
480        );
481
482        let generator = TestGenerator::new("Test::More");
483        let subs = generator.find_subroutines(&ast);
484        assert_eq!(subs.len(), 1);
485        assert_eq!(subs[0].name, "test_func");
486    }
487
488    #[test]
489    fn test_refactoring_suggestions() {
490        let analyzer = RefactoringAnalyzer::new();
491
492        // Create a subroutine with too many parameters
493        let ast = Node::new(
494            NodeKind::Subroutine {
495                name: Some("complex".to_string()),
496                name_span: None,
497                prototype: None,
498                signature: Some(Box::new(Node::new(
499                    NodeKind::Signature {
500                        parameters: vec![
501                            Node::new(
502                                NodeKind::MandatoryParameter {
503                                    variable: Box::new(Node::new(
504                                        NodeKind::Variable {
505                                            sigil: "$".to_string(),
506                                            name: "a".to_string(),
507                                        },
508                                        SourceLocation { start: 0, end: 0 },
509                                    )),
510                                },
511                                SourceLocation { start: 0, end: 0 },
512                            ),
513                            Node::new(
514                                NodeKind::MandatoryParameter {
515                                    variable: Box::new(Node::new(
516                                        NodeKind::Variable {
517                                            sigil: "$".to_string(),
518                                            name: "b".to_string(),
519                                        },
520                                        SourceLocation { start: 0, end: 0 },
521                                    )),
522                                },
523                                SourceLocation { start: 0, end: 0 },
524                            ),
525                            Node::new(
526                                NodeKind::MandatoryParameter {
527                                    variable: Box::new(Node::new(
528                                        NodeKind::Variable {
529                                            sigil: "$".to_string(),
530                                            name: "c".to_string(),
531                                        },
532                                        SourceLocation { start: 0, end: 0 },
533                                    )),
534                                },
535                                SourceLocation { start: 0, end: 0 },
536                            ),
537                            Node::new(
538                                NodeKind::MandatoryParameter {
539                                    variable: Box::new(Node::new(
540                                        NodeKind::Variable {
541                                            sigil: "$".to_string(),
542                                            name: "d".to_string(),
543                                        },
544                                        SourceLocation { start: 0, end: 0 },
545                                    )),
546                                },
547                                SourceLocation { start: 0, end: 0 },
548                            ),
549                            Node::new(
550                                NodeKind::MandatoryParameter {
551                                    variable: Box::new(Node::new(
552                                        NodeKind::Variable {
553                                            sigil: "$".to_string(),
554                                            name: "e".to_string(),
555                                        },
556                                        SourceLocation { start: 0, end: 0 },
557                                    )),
558                                },
559                                SourceLocation { start: 0, end: 0 },
560                            ),
561                            Node::new(
562                                NodeKind::MandatoryParameter {
563                                    variable: Box::new(Node::new(
564                                        NodeKind::Variable {
565                                            sigil: "$".to_string(),
566                                            name: "f".to_string(),
567                                        },
568                                        SourceLocation { start: 0, end: 0 },
569                                    )),
570                                },
571                                SourceLocation { start: 0, end: 0 },
572                            ),
573                            // 6 parameters - more than max_params (5)
574                        ],
575                    },
576                    SourceLocation { start: 0, end: 0 },
577                ))),
578                attributes: vec![],
579                body: Box::new(Node::new(
580                    NodeKind::Block { statements: vec![] },
581                    SourceLocation { start: 0, end: 0 },
582                )),
583            },
584            SourceLocation { start: 0, end: 0 },
585        );
586
587        let suggestions = analyzer.analyze(&ast, "sub complex($a, $b, $c, $d, $e, $f) { }");
588        assert!(!suggestions.is_empty());
589        assert!(suggestions.iter().any(|s| s.category == RefactoringCategory::TooManyParameters));
590    }
591
592    #[test]
593    fn test_tdd_workflow() {
594        let mut workflow = TddWorkflow::new("Test::More");
595
596        // Start cycle
597        let _result = workflow.start_cycle("add");
598        assert_eq!(workflow.state, TddState::Red);
599
600        // Run failing tests
601        let _result = workflow.run_tests(false);
602        assert_eq!(workflow.state, TddState::Red);
603
604        // Run passing tests
605        let _result = workflow.run_tests(true);
606        assert_eq!(workflow.state, TddState::Green);
607
608        // Start refactoring
609        let _result = workflow.start_refactor();
610        assert_eq!(workflow.state, TddState::Refactor);
611
612        // Complete cycle
613        let _result = workflow.complete_cycle();
614        assert_eq!(workflow.state, TddState::Idle);
615    }
616}