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
331 let code = match self.framework {
332 TestFramework::TestMore => {
333 format!(
334 "use Test::More;\n\n\
335 subtest '{} with undef' => sub {{\n \
336 eval {{ {}(undef) }};\n \
337 ok(!$@, 'Handles undef gracefully');\n\
338 }};\n",
339 sub.name, sub.name
340 )
341 }
342 _ => String::new(), };
344
345 TestCase { name: test_name, description, code, is_todo: false }
346 }
347
348 fn generate_empty_test(&self, sub: &SubroutineInfo) -> TestCase {
349 let test_name = format!("test_{}_empty", sub.name);
350 let description = format!("Test {} with empty parameters", sub.name);
351
352 let code = match self.framework {
353 TestFramework::TestMore => {
354 format!(
355 "use Test::More;\n\n\
356 subtest '{} with empty params' => sub {{\n \
357 eval {{ {}('', [], {{}}) }};\n \
358 ok(!$@, 'Handles empty values');\n\
359 }};\n",
360 sub.name, sub.name
361 )
362 }
363 _ => String::new(),
364 };
365
366 TestCase { name: test_name, description, code, is_todo: false }
367 }
368
369 fn generate_type_test(&self, sub: &SubroutineInfo) -> TestCase {
370 let test_name = format!("test_{}_types", sub.name);
371 let description = format!("Test {} with different types", sub.name);
372
373 let code = match self.framework {
374 TestFramework::TestMore => {
375 format!(
376 "use Test::More;\n\n\
377 subtest '{} type checking' => sub {{\n \
378 # Test with different types\n \
379 eval {{ {}(123) }};\n \
380 eval {{ {}('string') }};\n \
381 eval {{ {}([1,2,3]) }};\n \
382 eval {{ {}({{a=>1}}) }};\n \
383 pass('Handles different types');\n\
384 }};\n",
385 sub.name, sub.name, sub.name, sub.name, sub.name
386 )
387 }
388 _ => String::new(),
389 };
390
391 TestCase { name: test_name, description, code, is_todo: false }
392 }
393
394 fn generate_data_driven_test(&self, sub: &SubroutineInfo, _source: &str) -> Option<TestCase> {
396 sub.params.as_ref()?;
397
398 let test_name = format!("test_{}_data_driven", sub.name);
399 let description = format!("Data-driven test for {}", sub.name);
400
401 let code = match self.framework {
402 TestFramework::TestMore => {
403 format!(
404 "use Test::More;\n\n\
405 my @test_cases = (\n \
406 {{ input => 'test1', expected => 'result1' }},\n \
407 {{ input => 'test2', expected => 'result2' }},\n \
408 {{ input => 'test3', expected => 'result3' }},\n\
409 );\n\n\
410 for my $case (@test_cases) {{\n \
411 my $result = {}($case->{{input}});\n \
412 is($result, $case->{{expected}}, \n \
413 \"{}($case->{{input}}) returns $case->{{expected}}\");\n\
414 }}\n",
415 sub.name, sub.name
416 )
417 }
418 _ => String::new(),
419 };
420
421 Some(TestCase {
422 name: test_name,
423 description,
424 code,
425 is_todo: true, })
427 }
428
429 fn generate_perf_test(&self, sub: &SubroutineInfo, _source: &str) -> TestCase {
431 let test_name = format!("test_{}_performance", sub.name);
432 let description = format!("Performance test for {}", sub.name);
433
434 let code = match self.framework {
435 TestFramework::TestMore => {
436 let mut snippet = String::new();
437 snippet.push_str("use Test::More;\n");
438 snippet.push_str("use Benchmark qw(timeit);\n\n");
439 snippet.push_str(&format!("subtest '{} performance' => sub {{\n", sub.name));
440 snippet.push_str(&format!(
441 " my $result = timeit(1000, sub {{ {}() }});\n",
442 sub.name
443 ));
444 if let Some(threshold) = self.options.perf_thresholds.get(&sub.name) {
445 snippet.push_str(&format!(
446 " cmp_ok($result->real, '<', {}, 'Executes under threshold');\n",
447 threshold
448 ));
449 } else {
450 snippet.push_str(" ok($result->real >= 0, 'Execution time recorded');\n");
451 }
452 snippet.push_str("};\n");
453 snippet
454 }
455 _ => String::new(),
456 };
457
458 TestCase { name: test_name, description, code, is_todo: true }
459 }
460
461 fn generate_module_tests(&self, ast: &Node, _source: &str) -> Vec<TestCase> {
463 let mut tests = Vec::new();
464
465 if let Some(package_name) = self.find_package_name(ast) {
467 tests.push(self.generate_module_load_test(&package_name));
469
470 if self.has_exports(ast) {
472 tests.push(self.generate_export_test(&package_name));
473 }
474
475 if self.has_constructor(ast) {
477 tests.push(self.generate_constructor_test(&package_name));
478 }
479 }
480
481 tests
482 }
483
484 fn find_package_name(&self, node: &Node) -> Option<String> {
485 match &node.kind {
486 NodeKind::Package { name, .. } => Some(name.clone()),
487 _ => {
488 for child in node.children() {
489 if let Some(name) = self.find_package_name(child) {
490 return Some(name);
491 }
492 }
493 None
494 }
495 }
496 }
497
498 fn has_exports(&self, node: &Node) -> bool {
499 self.find_use_statement(node, "Exporter").is_some()
501 }
502
503 fn has_constructor(&self, node: &Node) -> bool {
504 self.find_subroutine(node, "new").is_some()
506 }
507
508 fn find_use_statement(&self, node: &Node, module: &str) -> Option<Node> {
509 match &node.kind {
510 NodeKind::Use { module: m, .. } if m == module => Some(node.clone()),
511 _ => {
512 for child in node.children() {
513 if let Some(result) = self.find_use_statement(child, module) {
514 return Some(result);
515 }
516 }
517 None
518 }
519 }
520 }
521
522 fn find_subroutine(&self, node: &Node, name: &str) -> Option<Node> {
523 match &node.kind {
524 NodeKind::Subroutine { name: Some(n), .. } if n == name => Some(node.clone()),
525 _ => {
526 for child in node.children() {
527 if let Some(result) = self.find_subroutine(child, name) {
528 return Some(result);
529 }
530 }
531 None
532 }
533 }
534 }
535
536 fn generate_module_load_test(&self, package: &str) -> TestCase {
537 let test_name = "test_module_loads".to_string();
538 let description = format!("Test that {} loads correctly", package);
539
540 let code = match self.framework {
541 TestFramework::TestMore => {
542 format!(
543 "use Test::More;\n\n\
544 BEGIN {{\n \
545 use_ok('{}') || print \"Bail out!\\n\";\n\
546 }}\n\n\
547 diag(\"Testing {} ${}::VERSION, Perl $], $^X\");\n",
548 package, package, package
549 )
550 }
551 _ => String::new(),
552 };
553
554 TestCase { name: test_name, description, code, is_todo: false }
555 }
556
557 fn generate_export_test(&self, package: &str) -> TestCase {
558 let test_name = "test_exports".to_string();
559 let description = format!("Test {} exports", package);
560
561 let code = match self.framework {
562 TestFramework::TestMore => {
563 format!(
564 "use Test::More;\n\
565 use {};\n\n\
566 can_ok('main', @{}::EXPORT);\n",
567 package, package
568 )
569 }
570 _ => String::new(),
571 };
572
573 TestCase { name: test_name, description, code, is_todo: false }
574 }
575
576 fn generate_constructor_test(&self, package: &str) -> TestCase {
577 let test_name = "test_constructor".to_string();
578 let description = format!("Test {} constructor", package);
579
580 let code = match self.framework {
581 TestFramework::TestMore => {
582 format!(
583 "use Test::More;\n\
584 use {};\n\n\
585 subtest 'constructor' => sub {{\n \
586 my $obj = {}->new();\n \
587 isa_ok($obj, '{}');\n \
588 can_ok($obj, 'new');\n\
589 }};\n",
590 package, package, package
591 )
592 }
593 _ => String::new(),
594 };
595
596 TestCase { name: test_name, description, code, is_todo: false }
597 }
598
599 fn generate_sample_args(&self, count: usize) -> String {
600 let args: Vec<String> = (0..count).map(|i| format!("'arg{}'", i + 1)).collect();
601 args.join(", ")
602 }
603
604 fn extract_parameters(&self, signature: Option<&Node>) -> Option<Vec<String>> {
606 if let Some(sig) = signature {
607 if let NodeKind::Signature { parameters } = &sig.kind {
608 let mut param_names = Vec::new();
609 for param in parameters {
610 match ¶m.kind {
611 NodeKind::MandatoryParameter { variable }
612 | NodeKind::OptionalParameter { variable, .. } => {
613 if let NodeKind::Variable { name, .. } = &variable.kind {
614 param_names.push(name.clone());
615 }
616 }
617 _ => {}
618 }
619 }
620 if param_names.is_empty() { None } else { Some(param_names) }
621 } else {
622 None
623 }
624 } else {
625 None
626 }
627 }
628}
629
630#[derive(Debug, Clone)]
631struct SubroutineInfo {
632 name: String,
633 params: Option<Vec<String>>,
634 #[allow(dead_code)]
635 node: Node,
636 #[allow(dead_code)]
637 is_private: bool,
638}
639
640pub struct TestRunner {
645 test_command: String,
647 watch_mode: bool,
649 coverage: bool,
651}
652
653impl Default for TestRunner {
654 fn default() -> Self {
655 Self::new()
656 }
657}
658
659impl TestRunner {
660 pub fn new() -> Self {
664 Self { test_command: "prove -l".to_string(), watch_mode: false, coverage: false }
665 }
666
667 pub fn with_command(command: String) -> Self {
673 Self { test_command: command, watch_mode: false, coverage: false }
674 }
675
676 pub fn run_tests(&self, test_files: &[String]) -> TestResults {
678 let mut results = TestResults::default();
679
680 let mut cmd = self.test_command.clone();
682
683 if self.coverage {
684 cmd = format!("cover -test {}", cmd);
685 }
686
687 for file in test_files {
688 cmd.push(' ');
689 cmd.push_str(file);
690 }
691
692 results.total = test_files.len();
694 results.passed = test_files.len(); results
697 }
698
699 pub fn watch(&self, _test_files: &[String]) -> Result<(), String> {
701 if !self.watch_mode {
702 return Err("Watch mode not enabled".to_string());
703 }
704
705 Ok(())
707 }
708
709 pub fn get_coverage(&self) -> Option<CoverageReport> {
711 if !self.coverage {
712 return None;
713 }
714
715 Some(CoverageReport {
716 line_coverage: 85.0,
717 branch_coverage: 75.0,
718 function_coverage: 90.0,
719 uncovered_lines: vec![],
720 })
721 }
722}
723
724#[derive(Debug, Default, Clone)]
726pub struct TestResults {
727 pub total: usize,
729 pub passed: usize,
731 pub failed: usize,
733 pub skipped: usize,
735 pub todo: usize,
737 pub errors: Vec<String>,
739}
740
741#[derive(Debug)]
743pub struct CoverageReport {
744 pub line_coverage: f64,
746 pub branch_coverage: f64,
748 pub function_coverage: f64,
750 pub uncovered_lines: Vec<usize>,
752}
753
754pub struct RefactoringSuggester {
759 suggestions: Vec<RefactoringSuggestion>,
761}
762
763#[derive(Debug, Clone)]
765pub struct RefactoringSuggestion {
766 pub title: String,
768 pub description: String,
770 pub priority: Priority,
772 pub category: RefactoringCategory,
774 pub code_action: Option<String>,
776}
777
778#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
780pub enum Priority {
781 Low,
783 Medium,
785 High,
787 Critical,
789}
790
791#[derive(Debug, Clone, PartialEq)]
793pub enum RefactoringCategory {
794 DuplicateCode,
796 ComplexMethod,
798 LongMethod,
800 TooManyParameters,
802 DeadCode,
804 Performance,
806 Naming,
808 Structure,
810}
811
812impl Default for RefactoringSuggester {
813 fn default() -> Self {
814 Self::new()
815 }
816}
817
818impl RefactoringSuggester {
819 pub fn new() -> Self {
821 Self { suggestions: Vec::new() }
822 }
823
824 pub fn analyze(&mut self, ast: &Node, source: &str) -> Vec<RefactoringSuggestion> {
826 self.suggestions.clear();
827
828 self.check_duplicate_code(ast, source);
830
831 self.check_complex_methods(ast, source);
833
834 self.check_long_methods(ast, source);
836
837 self.check_parameter_count(ast);
839
840 self.check_naming(ast);
842
843 self.suggestions.sort_by_key(|s| s.priority.clone());
845 self.suggestions.reverse();
846
847 self.suggestions.clone()
848 }
849
850 fn check_duplicate_code(&mut self, _ast: &Node, _source: &str) {
851 }
854
855 fn check_complex_methods(&mut self, ast: &Node, _source: &str) {
856 self.check_complex_methods_recursive(ast);
857 }
858
859 fn check_complex_methods_recursive(&mut self, node: &Node) {
860 match &node.kind {
861 NodeKind::Subroutine { name, .. } => {
862 let complexity = self.calculate_cyclomatic_complexity(node);
863 if complexity > 10 {
864 self.suggestions.push(RefactoringSuggestion {
865 title: format!("High complexity in {}", name.as_ref().unwrap_or(&"anonymous".to_string())),
866 description: format!("Cyclomatic complexity is {}. Consider breaking into smaller functions.", complexity),
867 priority: if complexity > 20 { Priority::High } else { Priority::Medium },
868 category: RefactoringCategory::ComplexMethod,
869 code_action: Some("extract_method".to_string()),
870 });
871 }
872 }
873 _ => {
874 for child in node.children() {
875 self.check_complex_methods_recursive(child);
876 }
877 }
878 }
879 }
880
881 fn calculate_cyclomatic_complexity(&self, node: &Node) -> usize {
882 let mut complexity = 1;
883 self.count_decision_points(node, &mut complexity);
884 complexity
885 }
886
887 fn count_decision_points(&self, node: &Node, complexity: &mut usize) {
888 match &node.kind {
889 NodeKind::If { .. }
890 | NodeKind::While { .. }
891 | NodeKind::For { .. }
892 | NodeKind::Ternary { .. } => {
893 *complexity += 1;
894 }
895 NodeKind::Binary { op: operator, .. } => {
896 if operator == "&&" || operator == "||" || operator == "and" || operator == "or" {
897 *complexity += 1;
898 }
899 }
900 _ => {}
901 }
902
903 for child in node.children() {
904 self.count_decision_points(child, complexity);
905 }
906 }
907
908 fn check_long_methods(&mut self, ast: &Node, source: &str) {
909 self.check_long_methods_recursive(ast, source);
910 }
911
912 fn check_long_methods_recursive(&mut self, node: &Node, source: &str) {
913 match &node.kind {
914 NodeKind::Subroutine { name, .. } => {
915 let lines = self.count_lines(node, source);
916 if lines > 50 {
917 self.suggestions.push(RefactoringSuggestion {
918 title: format!(
919 "Long method: {}",
920 name.as_ref().unwrap_or(&"anonymous".to_string())
921 ),
922 description: format!(
923 "Method has {} lines. Consider breaking into smaller functions.",
924 lines
925 ),
926 priority: if lines > 100 { Priority::High } else { Priority::Medium },
927 category: RefactoringCategory::LongMethod,
928 code_action: Some("extract_method".to_string()),
929 });
930 }
931 }
932 _ => {
933 for child in node.children() {
934 self.check_long_methods_recursive(child, source);
935 }
936 }
937 }
938 }
939
940 fn count_lines(&self, node: &Node, source: &str) -> usize {
941 let start = node.location.start;
942 let end = node.location.end;
943
944 let text = &source[start..end.min(source.len())];
945 text.lines().count()
946 }
947
948 fn check_parameter_count(&mut self, ast: &Node) {
949 self.check_parameter_count_recursive(ast);
950 }
951
952 fn check_parameter_count_recursive(&mut self, node: &Node) {
953 match &node.kind {
954 NodeKind::Subroutine { name, signature, .. } => {
955 let params = self.extract_parameters(signature.as_deref());
956 if let Some(params) = ¶ms {
957 if params.len() > 5 {
958 self.suggestions.push(RefactoringSuggestion {
959 title: format!(
960 "Too many parameters in {}",
961 name.as_ref().unwrap_or(&"anonymous".to_string())
962 ),
963 description: format!(
964 "Function has {} parameters. Consider using a hash or object.",
965 params.len()
966 ),
967 priority: Priority::Medium,
968 category: RefactoringCategory::TooManyParameters,
969 code_action: Some("introduce_parameter_object".to_string()),
970 });
971 }
972 }
973 }
974 _ => {
975 for child in node.children() {
976 self.check_parameter_count_recursive(child);
977 }
978 }
979 }
980 }
981
982 fn check_naming(&mut self, ast: &Node) {
983 self.check_naming_recursive(ast);
984 }
985
986 fn check_naming_recursive(&mut self, node: &Node) {
987 match &node.kind {
988 NodeKind::Subroutine { name: Some(name), .. } => {
989 if !self.is_good_name(name) {
990 self.suggestions.push(RefactoringSuggestion {
991 title: format!("Poor naming: {}", name),
992 description: "Consider using a more descriptive name".to_string(),
993 priority: Priority::Low,
994 category: RefactoringCategory::Naming,
995 code_action: Some("rename".to_string()),
996 });
997 }
998 }
999 NodeKind::VariableDeclaration { variable, .. } => {
1000 if let NodeKind::Variable { name, .. } = &variable.kind {
1001 if !self.is_good_variable_name(name) {
1002 self.suggestions.push(RefactoringSuggestion {
1003 title: format!("Poor variable name: {}", name),
1004 description:
1005 "Single letter variables should only be used for loop counters"
1006 .to_string(),
1007 priority: Priority::Low,
1008 category: RefactoringCategory::Naming,
1009 code_action: Some("rename".to_string()),
1010 });
1011 }
1012 }
1013 }
1014 NodeKind::VariableListDeclaration { variables, .. } => {
1015 for var_node in variables {
1016 if let NodeKind::Variable { name, .. } = &var_node.kind {
1017 if !self.is_good_variable_name(name) {
1018 self.suggestions.push(RefactoringSuggestion {
1019 title: format!("Poor variable name: {}", name),
1020 description:
1021 "Single letter variables should only be used for loop counters"
1022 .to_string(),
1023 priority: Priority::Low,
1024 category: RefactoringCategory::Naming,
1025 code_action: Some("rename".to_string()),
1026 });
1027 }
1028 }
1029 }
1030 }
1031 _ => {
1032 for child in node.children() {
1033 self.check_naming_recursive(child);
1034 }
1035 }
1036 }
1037 }
1038
1039 fn is_good_name(&self, name: &str) -> bool {
1040 name.len() > 2 && !name.chars().all(|c| c.is_uppercase())
1042 }
1043
1044 fn is_good_variable_name(&self, name: &str) -> bool {
1045 if name.len() == 1 {
1047 return matches!(name, "$i" | "$j" | "$k" | "$n" | "$_");
1048 }
1049
1050 let clean_name = name.trim_start_matches(['$', '@', '%', '*']);
1052 clean_name.len() > 1
1053 }
1054
1055 fn extract_parameters(&self, signature: Option<&Node>) -> Option<Vec<String>> {
1057 if let Some(sig) = signature {
1058 if let NodeKind::Signature { parameters } = &sig.kind {
1059 let mut param_names = Vec::new();
1060 for param in parameters {
1061 match ¶m.kind {
1062 NodeKind::MandatoryParameter { variable }
1063 | NodeKind::OptionalParameter { variable, .. } => {
1064 if let NodeKind::Variable { name, .. } = &variable.kind {
1065 param_names.push(name.clone());
1066 }
1067 }
1068 _ => {}
1069 }
1070 }
1071 if param_names.is_empty() { None } else { Some(param_names) }
1072 } else {
1073 None
1074 }
1075 } else {
1076 None
1077 }
1078 }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083 use super::*;
1084
1085 #[test]
1086 fn test_generate_basic_test() {
1087 let generator = TestGenerator::new(TestFramework::TestMore);
1088 let ast = Node::new(
1089 NodeKind::Subroutine {
1090 name: Some("add".to_string()),
1091 name_span: None,
1092 signature: Some(Box::new(Node::new(
1093 NodeKind::Signature {
1094 parameters: vec![
1095 Node::new(
1096 NodeKind::MandatoryParameter {
1097 variable: Box::new(Node::new(
1098 NodeKind::Variable {
1099 name: "$a".to_string(),
1100 sigil: "$".to_string(),
1101 },
1102 crate::ast::SourceLocation { start: 0, end: 0 },
1103 )),
1104 },
1105 crate::ast::SourceLocation { start: 0, end: 0 },
1106 ),
1107 Node::new(
1108 NodeKind::MandatoryParameter {
1109 variable: Box::new(Node::new(
1110 NodeKind::Variable {
1111 name: "$b".to_string(),
1112 sigil: "$".to_string(),
1113 },
1114 crate::ast::SourceLocation { start: 0, end: 0 },
1115 )),
1116 },
1117 crate::ast::SourceLocation { start: 0, end: 0 },
1118 ),
1119 ],
1120 },
1121 crate::ast::SourceLocation { start: 0, end: 0 },
1122 ))),
1123 body: Box::new(Node::new(
1124 NodeKind::Block { statements: vec![] },
1125 crate::ast::SourceLocation { start: 0, end: 0 },
1126 )),
1127 attributes: vec![],
1128 prototype: None,
1129 },
1130 crate::ast::SourceLocation { start: 0, end: 0 },
1131 );
1132
1133 let tests = generator.generate_tests(&ast, "sub add { }");
1134 assert!(!tests.is_empty());
1135 assert!(tests[0].code.contains("Test::More"));
1136 assert!(tests[0].code.contains("add"));
1137 }
1138
1139 #[test]
1140 fn test_refactoring_suggestions() {
1141 let mut suggester = RefactoringSuggester::new();
1142
1143 let ast = Node::new(
1145 NodeKind::Subroutine {
1146 name: Some("complex_function".to_string()),
1147 name_span: None,
1148 signature: Some(Box::new(Node::new(
1149 NodeKind::Signature {
1150 parameters: (0..7)
1151 .map(|i| {
1152 Node::new(
1153 NodeKind::MandatoryParameter {
1154 variable: Box::new(Node::new(
1155 NodeKind::Variable {
1156 name: format!("$param{}", i),
1157 sigil: "$".to_string(),
1158 },
1159 crate::ast::SourceLocation { start: 0, end: 0 },
1160 )),
1161 },
1162 crate::ast::SourceLocation { start: 0, end: 0 },
1163 )
1164 })
1165 .collect(),
1166 },
1167 crate::ast::SourceLocation { start: 0, end: 0 },
1168 ))),
1169 body: Box::new(Node::new(
1170 NodeKind::Block {
1171 statements: vec![
1172 Node::new(
1174 NodeKind::If {
1175 condition: Box::new(Node::new(
1176 NodeKind::Variable {
1177 name: "$a".to_string(),
1178 sigil: "$".to_string(),
1179 },
1180 crate::ast::SourceLocation { start: 0, end: 0 },
1181 )),
1182 then_branch: Box::new(Node::new(
1183 NodeKind::Block { statements: vec![] },
1184 crate::ast::SourceLocation { start: 0, end: 0 },
1185 )),
1186 elsif_branches: vec![],
1187 else_branch: None,
1188 },
1189 crate::ast::SourceLocation { start: 0, end: 0 },
1190 ),
1191 ],
1192 },
1193 crate::ast::SourceLocation { start: 0, end: 0 },
1194 )),
1195 attributes: vec![],
1196 prototype: None,
1197 },
1198 crate::ast::SourceLocation { start: 0, end: 0 },
1199 );
1200
1201 let suggestions = suggester.analyze(&ast, "sub complex_function { }");
1202
1203 assert!(suggestions.iter().any(|s| s.category == RefactoringCategory::TooManyParameters));
1205 }
1206
1207 #[test]
1208 fn test_cyclomatic_complexity() {
1209 let suggester = RefactoringSuggester::new();
1210
1211 let node = Node::new(
1213 NodeKind::Block {
1214 statements: vec![Node::new(
1215 NodeKind::If {
1216 condition: Box::new(Node::new(
1217 NodeKind::Variable { name: "$x".to_string(), sigil: "$".to_string() },
1218 crate::ast::SourceLocation { start: 0, end: 0 },
1219 )),
1220 then_branch: Box::new(Node::new(
1221 NodeKind::Block { statements: vec![] },
1222 crate::ast::SourceLocation { start: 0, end: 0 },
1223 )),
1224 elsif_branches: vec![],
1225 else_branch: None,
1226 },
1227 crate::ast::SourceLocation { start: 0, end: 0 },
1228 )],
1229 },
1230 crate::ast::SourceLocation { start: 0, end: 0 },
1231 );
1232
1233 let complexity = suggester.calculate_cyclomatic_complexity(&node);
1234 assert_eq!(complexity, 2); }
1236}