1use crate::ast::{Node, NodeKind};
37use std::collections::HashMap;
38
39pub struct TestGenerator {
44 framework: TestFramework,
46 options: TestGeneratorOptions,
48}
49
50#[derive(Debug, Clone, PartialEq)]
54pub enum TestFramework {
55 TestMore,
57 Test2V0,
59 TestSimple,
61 TestClass,
63}
64
65#[derive(Debug, Clone)]
70pub struct TestGeneratorOptions {
71 pub test_private: bool,
73 pub edge_cases: bool,
75 pub use_mocks: bool,
77 pub data_driven: bool,
79 pub perf_tests: bool,
81 pub expected_values: HashMap<String, String>,
83 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#[derive(Debug, Clone)]
103pub struct TestCase {
104 pub name: String,
106 pub description: String,
108 pub code: String,
110 pub is_todo: bool,
112}
113
114impl TestGenerator {
115 pub fn new(framework: TestFramework) -> Self {
134 Self { framework, options: TestGeneratorOptions::default() }
135 }
136
137 pub fn with_options(framework: TestFramework, options: TestGeneratorOptions) -> Self {
144 Self { framework, options }
145 }
146
147 pub fn generate_tests(&self, ast: &Node, source: &str) -> Vec<TestCase> {
149 let mut tests = Vec::new();
150
151 let subs = self.find_subroutines(ast);
153
154 for sub in subs {
155 tests.push(self.generate_basic_test(&sub, source));
157
158 if self.options.edge_cases {
160 tests.extend(self.generate_edge_cases(&sub, source));
161 }
162
163 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 if self.options.perf_tests {
172 tests.push(self.generate_perf_test(&sub, source));
173 }
174 }
175
176 tests.extend(self.generate_module_tests(ast, source));
178
179 tests
180 }
181
182 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 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 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 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 tests.push(self.generate_undef_test(sub));
316
317 tests.push(self.generate_empty_test(sub));
319
320 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(), };
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 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, })
441 }
442
443 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 fn generate_module_tests(&self, ast: &Node, _source: &str) -> Vec<TestCase> {
477 let mut tests = Vec::new();
478
479 if let Some(package_name) = self.find_package_name(ast) {
481 tests.push(self.generate_module_load_test(&package_name));
483
484 if self.has_exports(ast) {
486 tests.push(self.generate_export_test(&package_name));
487 }
488
489 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 self.find_use_statement(node, "Exporter").is_some()
515 }
516
517 fn has_constructor(&self, node: &Node) -> bool {
518 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 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 ¶m.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
659pub struct TestRunner {
664 test_command: String,
666 watch_mode: bool,
668 coverage: bool,
670}
671
672impl Default for TestRunner {
673 fn default() -> Self {
674 Self::new()
675 }
676}
677
678impl TestRunner {
679 pub fn new() -> Self {
683 Self { test_command: "prove -l".to_string(), watch_mode: false, coverage: false }
684 }
685
686 pub fn with_command(command: String) -> Self {
692 Self { test_command: command, watch_mode: false, coverage: false }
693 }
694
695 pub fn run_tests(&self, test_files: &[String]) -> TestResults {
697 let mut results = TestResults::default();
698
699 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 results.total = test_files.len();
713 results.passed = test_files.len(); results
716 }
717
718 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 Ok(())
726 }
727
728 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#[derive(Debug, Default, Clone)]
745pub struct TestResults {
746 pub total: usize,
748 pub passed: usize,
750 pub failed: usize,
752 pub skipped: usize,
754 pub todo: usize,
756 pub errors: Vec<String>,
758}
759
760#[derive(Debug)]
762pub struct CoverageReport {
763 pub line_coverage: f64,
765 pub branch_coverage: f64,
767 pub function_coverage: f64,
769 pub uncovered_lines: Vec<usize>,
771}
772
773pub struct RefactoringSuggester {
778 suggestions: Vec<RefactoringSuggestion>,
780}
781
782#[derive(Debug, Clone)]
784pub struct RefactoringSuggestion {
785 pub title: String,
787 pub description: String,
789 pub priority: Priority,
791 pub category: RefactoringCategory,
793 pub code_action: Option<String>,
795}
796
797#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
799pub enum Priority {
800 Low,
802 Medium,
804 High,
806 Critical,
808}
809
810#[derive(Debug, Clone, PartialEq)]
812pub enum RefactoringCategory {
813 DuplicateCode,
815 ComplexMethod,
817 LongMethod,
819 TooManyParameters,
821 DeadCode,
823 Performance,
825 Naming,
827 Structure,
829}
830
831impl Default for RefactoringSuggester {
832 fn default() -> Self {
833 Self::new()
834 }
835}
836
837impl RefactoringSuggester {
838 pub fn new() -> Self {
840 Self { suggestions: Vec::new() }
841 }
842
843 pub fn analyze(&mut self, ast: &Node, source: &str) -> Vec<RefactoringSuggestion> {
845 self.suggestions.clear();
846
847 self.check_duplicate_code(ast, source);
849
850 self.check_complex_methods(ast, source);
852
853 self.check_long_methods(ast, source);
855
856 self.check_parameter_count(ast);
858
859 self.check_naming(ast);
861
862 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 }
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) = ¶ms {
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 name.len() > 2 && !name.chars().all(|c| c.is_uppercase())
1061 }
1062
1063 fn is_good_variable_name(&self, name: &str) -> bool {
1064 if name.len() == 1 {
1066 return matches!(name, "$i" | "$j" | "$k" | "$n" | "$_");
1067 }
1068
1069 let clean_name = name.trim_start_matches(['$', '@', '%', '*']);
1071 clean_name.len() > 1
1072 }
1073
1074 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 ¶m.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 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 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 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 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); }
1286}