Skip to main content

debtmap/priority/semantic_classifier/
mod.rs

1mod ast_analysis;
2mod classifiers;
3mod pattern_matchers;
4
5use crate::analyzers::rust_data_flow_analyzer::analyze_data_flow;
6use crate::core::FunctionMetrics;
7use crate::priority::call_graph::{CallGraph, FunctionId};
8use ast_analysis::is_simple_accessor_body;
9use classifiers::{
10    is_constructor_enhanced, is_debug_function, is_enum_converter_enhanced, is_io_wrapper,
11    is_orchestrator, is_pattern_matching_function,
12};
13use pattern_matchers::{is_entry_point_by_name, matches_accessor_name};
14use serde::{Deserialize, Serialize};
15
16#[cfg(test)]
17use classifiers::{
18    calculate_delegation_ratio, delegates_to_tested_functions, has_diagnostic_characteristics,
19    is_simple_constructor,
20};
21#[cfg(test)]
22use pattern_matchers::matches_debug_pattern;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub enum FunctionRole {
26    PureLogic,    // Business logic, high test priority
27    Orchestrator, // Coordinates other functions
28    IOWrapper,    // Thin I/O layer
29    EntryPoint,   // Main entry points
30    PatternMatch, // Pattern matching function (low complexity)
31    Debug,        // Debug/diagnostic functions (low test priority)
32    Unknown,      // Cannot classify
33}
34
35pub fn classify_function_role(
36    func: &FunctionMetrics,
37    func_id: &FunctionId,
38    call_graph: &CallGraph,
39) -> FunctionRole {
40    // Use a functional approach with classification rules
41    // Note: AST is not available at this level, so we pass None
42    // Full AST-based detection will be integrated when threading syn::ItemFn
43    classify_by_rules(func, func_id, call_graph, None).unwrap_or_else(|| {
44        // BUG-001 fix: Use purity analysis to prevent impure I/O functions from being
45        // classified as PureLogic. If function is impure, classify as Unknown instead.
46        if is_impure_function(func) {
47            FunctionRole::Unknown
48        } else {
49            FunctionRole::PureLogic
50        }
51    })
52}
53
54// Pure function that applies classification rules in order
55fn classify_by_rules(
56    func: &FunctionMetrics,
57    func_id: &FunctionId,
58    call_graph: &CallGraph,
59    syn_func: Option<&syn::ItemFn>,
60) -> Option<FunctionRole> {
61    // Entry point has highest precedence
62    if is_entry_point(func_id, call_graph) {
63        return Some(FunctionRole::EntryPoint);
64    }
65
66    // Check for debug/diagnostic functions early (Spec 119)
67    if is_debug_function(func) {
68        return Some(FunctionRole::Debug);
69    }
70
71    // Check for constructors BEFORE pattern matching (Spec 117 + 122)
72    if is_constructor_enhanced(func, syn_func) {
73        return Some(FunctionRole::IOWrapper);
74    }
75
76    // Check for enum converters (Spec 124)
77    if let Some(syn_func) = syn_func
78        && is_enum_converter_enhanced(func, syn_func)
79    {
80        return Some(FunctionRole::IOWrapper);
81    }
82
83    // Check for accessor methods (Spec 125)
84    // This should come after enum converter detection but before pattern matching
85    if is_accessor_method(func, syn_func) {
86        return Some(FunctionRole::IOWrapper);
87    }
88
89    // Check for data flow classification (Spec 126) - only if enabled and AST available
90    if let Some(syn_func) = syn_func {
91        let config = crate::config::get_data_flow_classification_config();
92
93        if config.enabled {
94            let profile = analyze_data_flow(syn_func);
95
96            // Only classify if high confidence
97            if profile.confidence >= config.min_confidence
98                && profile.transformation_ratio >= config.min_transformation_ratio
99                && profile.business_logic_ratio < config.max_business_logic_ratio
100            {
101                return Some(FunctionRole::Orchestrator);
102            }
103        }
104    }
105
106    // Check for pattern matching functions (like detect_file_type)
107    if is_pattern_matching_function(func, func_id) {
108        return Some(FunctionRole::PatternMatch);
109    }
110
111    // Check I/O wrapper BEFORE orchestration
112    if is_io_wrapper(func) {
113        return Some(FunctionRole::IOWrapper);
114    }
115
116    // Only then check orchestration patterns
117    if is_orchestrator(func, func_id, call_graph) {
118        return Some(FunctionRole::Orchestrator);
119    }
120
121    None // Will default to PureLogic
122}
123
124// Pure function to check if a function is an entry point
125fn is_entry_point(func_id: &FunctionId, call_graph: &CallGraph) -> bool {
126    call_graph.is_entry_point(func_id) || is_entry_point_by_name(&func_id.name)
127}
128
129/// Check if a function is impure based on purity analysis (BUG-001 fix).
130///
131/// Used to prevent impure functions from being classified as PureLogic.
132/// A function is considered impure if:
133/// - `purity_level` is `Some(Impure)`
134/// - `is_pure` is `Some(false)` (legacy field)
135fn is_impure_function(func: &FunctionMetrics) -> bool {
136    use crate::core::PurityLevel;
137
138    // Check new purity_level field first
139    if let Some(level) = func.purity_level {
140        return level == PurityLevel::Impure;
141    }
142
143    // Fallback to legacy is_pure field
144    func.is_pure == Some(false)
145}
146
147/// Detect simple accessor/getter methods (spec 125)
148///
149/// Identifies simple accessor and getter methods that should be classified as
150/// IOWrapper instead of PureLogic to reduce their priority score.
151///
152/// # Detection Strategy
153///
154/// 1. Check name matches accessor patterns (id, name, get_*, is_*, etc.)
155/// 2. Verify low complexity (cyclomatic ≤ 2, cognitive ≤ 1)
156/// 3. Check function is short (< 10 lines, nesting ≤ 1)
157/// 4. If AST available, verify body is simple accessor pattern
158///
159/// # Detected Patterns
160///
161/// **Simple accessors** (classified as IOWrapper):
162/// - Direct field access: `id()`, `name()` returning field values
163/// - Boolean checks: `is_active()`, `has_permission()` with simple conditions
164/// - Type conversions: `as_str()`, `to_string()` with minimal logic
165/// - Uses immutable `&self` reference only
166/// - Very low complexity (cyclomatic ≤ 2, cognitive ≤ 1)
167/// - Short length (< 10 lines, nesting ≤ 1)
168///
169/// **Complex methods** (NOT detected, remain PureLogic):
170/// - Methods with calculations, iterations, or aggregations
171/// - Methods calling other business logic functions
172/// - Methods with complex control flow or multiple branches
173fn is_accessor_method(func: &FunctionMetrics, syn_func: Option<&syn::ItemFn>) -> bool {
174    let config = crate::config::get_accessor_detection_config();
175
176    // Check if accessor detection is enabled
177    if !config.enabled {
178        return false;
179    }
180
181    // Check name matches accessor pattern
182    if !matches_accessor_name(&func.name, &config) {
183        return false;
184    }
185
186    // Check complexity is minimal
187    if func.cyclomatic > config.max_cyclomatic
188        || func.cognitive > config.max_cognitive
189        || func.length >= config.max_length
190        || func.nesting > config.max_nesting
191    {
192        return false;
193    }
194
195    // If AST available, verify body is simple
196    if let Some(syn_func) = syn_func
197        && !is_simple_accessor_body(syn_func)
198    {
199        return false;
200    }
201
202    true
203}
204
205pub fn get_role_multiplier(role: FunctionRole) -> f64 {
206    // Get multipliers from configuration
207    let config = crate::config::get_role_multipliers();
208
209    match role {
210        FunctionRole::PureLogic => config.pure_logic,
211        FunctionRole::Orchestrator => config.orchestrator,
212        FunctionRole::IOWrapper => config.io_wrapper,
213        FunctionRole::EntryPoint => config.entry_point,
214        FunctionRole::PatternMatch => config.pattern_match,
215        FunctionRole::Debug => config.debug,
216        FunctionRole::Unknown => config.unknown,
217    }
218}
219
220// Semantic priority calculation removed per spec 58
221// Role multipliers now provide the only role-based adjustment to avoid double penalties
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::core::FunctionMetrics;
227    use crate::priority::call_graph::{CallGraph, CallType, FunctionCall, FunctionId};
228    use std::path::PathBuf;
229
230    fn create_test_metrics(
231        name: &str,
232        cyclomatic: u32,
233        cognitive: u32,
234        lines: usize,
235    ) -> FunctionMetrics {
236        FunctionMetrics {
237            file: PathBuf::from("test.rs"),
238            name: name.to_string(),
239            line: 1,
240            length: lines,
241            cyclomatic,
242            cognitive,
243            nesting: 0,
244            is_test: false,
245            visibility: None,
246            is_trait_method: false,
247            in_test_module: false,
248            entropy_score: None,
249            is_pure: None,
250            purity_confidence: None,
251            purity_reason: None,
252            call_dependencies: None,
253            detected_patterns: None,
254            upstream_callers: None,
255            downstream_callees: None,
256            mapping_pattern_result: None,
257            adjusted_complexity: None,
258            composition_metrics: None,
259            language_specific: None,
260            purity_level: None,
261            error_swallowing_count: None,
262            error_swallowing_patterns: None,
263            entropy_analysis: None,
264        }
265    }
266
267    #[test]
268    fn test_entry_point_classification() {
269        let graph = CallGraph::new();
270        let func = create_test_metrics("main", 5, 8, 50);
271        let func_id = FunctionId::new(PathBuf::from("main.rs"), "main".to_string(), 1);
272
273        let role = classify_function_role(&func, &func_id, &graph);
274        assert_eq!(role, FunctionRole::EntryPoint);
275    }
276
277    #[test]
278    fn test_orchestrator_classification() {
279        let mut graph = CallGraph::new();
280        let func = create_test_metrics("coordinate_tasks", 2, 3, 15);
281        let func_id = FunctionId::new(
282            PathBuf::from("coord.rs"),
283            "coordinate_tasks".to_string(),
284            10,
285        );
286
287        // Add the orchestrator function
288        graph.add_function(func_id.clone(), false, false, 2, 15);
289
290        // Add some worker functions it calls
291        for i in 0..3 {
292            let worker_id =
293                FunctionId::new(PathBuf::from("worker.rs"), format!("worker_{i}"), i * 10);
294            graph.add_function(worker_id.clone(), false, false, 8, 40);
295            graph.add_call(crate::priority::call_graph::FunctionCall {
296                caller: func_id.clone(),
297                callee: worker_id,
298                call_type: crate::priority::call_graph::CallType::Delegate,
299            });
300        }
301
302        let role = classify_function_role(&func, &func_id, &graph);
303        assert_eq!(role, FunctionRole::Orchestrator);
304    }
305
306    #[test]
307    fn test_io_wrapper_classification() {
308        let graph = CallGraph::new();
309        let func = create_test_metrics("read_file", 1, 2, 10);
310        let func_id = FunctionId::new(PathBuf::from("io.rs"), "read_file".to_string(), 5);
311
312        let role = classify_function_role(&func, &func_id, &graph);
313        assert_eq!(role, FunctionRole::IOWrapper);
314    }
315
316    #[test]
317    fn test_io_orchestration_classification() {
318        let graph = CallGraph::new();
319
320        // Test case similar to output_unified_priorities:
321        // - Has "output_" prefix (strong I/O pattern)
322        // - 38 lines (within the 50 line limit)
323        // - Cyclomatic 12 (from format branching)
324        // - Nesting 3 (not deeply nested)
325        let mut func = create_test_metrics("output_unified_priorities", 12, 19, 38);
326        func.nesting = 3;
327
328        let func_id = FunctionId::new(
329            PathBuf::from("main.rs"),
330            "output_unified_priorities".to_string(),
331            861,
332        );
333
334        let role = classify_function_role(&func, &func_id, &graph);
335        assert_eq!(role, FunctionRole::IOWrapper);
336
337        // Test that high nesting disqualifies I/O orchestration
338        func.nesting = 4;
339        let role = classify_function_role(&func, &func_id, &graph);
340        assert_eq!(role, FunctionRole::PureLogic);
341    }
342
343    #[test]
344    fn test_pure_logic_classification() {
345        let graph = CallGraph::new();
346        let func = create_test_metrics("calculate_risk", 8, 12, 60);
347        let func_id = FunctionId::new(PathBuf::from("calc.rs"), "calculate_risk".to_string(), 20);
348
349        let role = classify_function_role(&func, &func_id, &graph);
350        assert_eq!(role, FunctionRole::PureLogic);
351    }
352
353    #[test]
354    fn test_role_multipliers() {
355        // Test with updated configuration values (BUG-001 fix: PureLogic now 0.7)
356        assert_eq!(get_role_multiplier(FunctionRole::PureLogic), 0.7);
357        assert_eq!(get_role_multiplier(FunctionRole::Orchestrator), 0.8);
358        assert_eq!(get_role_multiplier(FunctionRole::IOWrapper), 0.7);
359        assert_eq!(get_role_multiplier(FunctionRole::EntryPoint), 0.9);
360        assert_eq!(get_role_multiplier(FunctionRole::PatternMatch), 0.6);
361        assert_eq!(get_role_multiplier(FunctionRole::Unknown), 1.0);
362    }
363
364    #[test]
365    fn test_formatting_function_not_orchestrator() {
366        let mut graph = CallGraph::new();
367
368        // Create a function like format_recommendation_box_header
369        let func = create_test_metrics("format_recommendation_box_header", 1, 0, 9);
370        let func_id = FunctionId::new(
371            PathBuf::from("insights.rs"),
372            "format_recommendation_box_header".to_string(),
373            142,
374        );
375
376        // Add the function to the graph
377        graph.add_function(func_id.clone(), false, false, 1, 9);
378
379        // Add callees: calculate_dash_count and format (from macro)
380        let callee1 = FunctionId::new(
381            PathBuf::from("insights.rs"),
382            "calculate_dash_count".to_string(),
383            138,
384        );
385        let callee2 = FunctionId::new(PathBuf::from("std"), "format".to_string(), 1);
386
387        graph.add_function(callee1.clone(), false, false, 1, 3);
388        graph.add_function(callee2.clone(), false, false, 1, 1);
389
390        graph.add_call(FunctionCall {
391            caller: func_id.clone(),
392            callee: callee1,
393            call_type: CallType::Direct,
394        });
395        graph.add_call(FunctionCall {
396            caller: func_id.clone(),
397            callee: callee2,
398            call_type: CallType::Direct,
399        });
400
401        // Test that it's not classified as orchestrator
402        let role = classify_function_role(&func, &func_id, &graph);
403        assert_eq!(
404            role,
405            FunctionRole::PureLogic,
406            "Formatting function should be PureLogic, not Orchestrator"
407        );
408
409        // Verify it doesn't match delegation pattern
410        assert!(
411            !delegates_to_tested_functions(&func_id, &graph, 0.8),
412            "Should not be considered delegation when calling std functions"
413        );
414    }
415
416    #[test]
417    fn test_actual_orchestrator_with_meaningful_callees() {
418        let mut graph = CallGraph::new();
419
420        // Create an actual orchestrator function
421        let func = create_test_metrics("coordinate_workflow", 2, 3, 15);
422        let func_id = FunctionId::new(
423            PathBuf::from("workflow.rs"),
424            "coordinate_workflow".to_string(),
425            10,
426        );
427
428        graph.add_function(func_id.clone(), false, false, 2, 15);
429
430        // Add meaningful callees (not std library)
431        // Need at least 3 for the current config settings
432        let callee1 = FunctionId::new(
433            PathBuf::from("workflow.rs"),
434            "process_step_one".to_string(),
435            50,
436        );
437        let callee2 = FunctionId::new(
438            PathBuf::from("workflow.rs"),
439            "process_step_two".to_string(),
440            100,
441        );
442        let callee3 = FunctionId::new(
443            PathBuf::from("workflow.rs"),
444            "process_step_three".to_string(),
445            150,
446        );
447
448        graph.add_function(callee1.clone(), false, false, 5, 30);
449        graph.add_function(callee2.clone(), false, false, 5, 30);
450        graph.add_function(callee3.clone(), false, false, 5, 30);
451
452        graph.add_call(FunctionCall {
453            caller: func_id.clone(),
454            callee: callee1,
455            call_type: CallType::Direct,
456        });
457        graph.add_call(FunctionCall {
458            caller: func_id.clone(),
459            callee: callee2,
460            call_type: CallType::Direct,
461        });
462        graph.add_call(FunctionCall {
463            caller: func_id.clone(),
464            callee: callee3,
465            call_type: CallType::Direct,
466        });
467
468        // This should be classified as orchestrator
469        let role = classify_function_role(&func, &func_id, &graph);
470        assert_eq!(
471            role,
472            FunctionRole::Orchestrator,
473            "Function coordinating multiple steps should be Orchestrator"
474        );
475    }
476
477    #[test]
478    fn test_orchestrator_with_error_handling_complexity_5() {
479        let mut graph = CallGraph::new();
480
481        // Function with complexity 5 from Result handling (spec 117)
482        let func = create_test_metrics("coordinate_tasks", 5, 3, 20);
483        let func_id = FunctionId::new(
484            PathBuf::from("tasks.rs"),
485            "coordinate_tasks".to_string(),
486            10,
487        );
488
489        graph.add_function(func_id.clone(), false, false, 5, 20);
490
491        // Add 4 meaningful callees (20% of 20 lines = 4 calls for delegation ratio)
492        for i in 0..4 {
493            let callee_id = FunctionId::new(
494                PathBuf::from("worker.rs"),
495                format!("worker_task_{}", i),
496                i * 20,
497            );
498            graph.add_function(callee_id.clone(), false, false, 8, 40);
499            graph.add_call(FunctionCall {
500                caller: func_id.clone(),
501                callee: callee_id,
502                call_type: CallType::Direct,
503            });
504        }
505
506        // Should be classified as orchestrator despite higher complexity
507        let role = classify_function_role(&func, &func_id, &graph);
508        assert_eq!(
509            role,
510            FunctionRole::Orchestrator,
511            "Function with complexity 5 and good delegation ratio should be Orchestrator"
512        );
513    }
514
515    #[test]
516    fn test_delegation_ratio_calculation() {
517        let func = create_test_metrics("orchestrator", 4, 2, 20);
518
519        // Create test callees - 4 calls in 20 lines = 20% ratio
520        let callees_vec: Vec<FunctionId> = (0..4)
521            .map(|i| FunctionId::new(PathBuf::from("test.rs"), format!("callee_{}", i), i * 10))
522            .collect();
523
524        let callees: Vec<&FunctionId> = callees_vec.iter().collect();
525
526        let ratio = calculate_delegation_ratio(&func, &callees);
527        assert!(
528            (ratio - 0.2).abs() < 0.01,
529            "Expected delegation ratio of 0.2, got {}",
530            ratio
531        );
532    }
533
534    #[test]
535    fn test_high_complexity_not_orchestrator() {
536        let mut graph = CallGraph::new();
537
538        // Function with complexity > 5 should not be orchestrator
539        let func = create_test_metrics("complex_logic", 8, 10, 30);
540        let func_id = FunctionId::new(PathBuf::from("logic.rs"), "complex_logic".to_string(), 10);
541
542        graph.add_function(func_id.clone(), false, false, 8, 30);
543
544        // Even with callees, complexity > 5 means not orchestrator
545        for i in 0..3 {
546            let callee_id =
547                FunctionId::new(PathBuf::from("worker.rs"), format!("worker_{}", i), i * 20);
548            graph.add_function(callee_id.clone(), false, false, 5, 20);
549            graph.add_call(FunctionCall {
550                caller: func_id.clone(),
551                callee: callee_id,
552                call_type: CallType::Direct,
553            });
554        }
555
556        let role = classify_function_role(&func, &func_id, &graph);
557        assert_eq!(
558            role,
559            FunctionRole::PureLogic,
560            "Function with complexity > 5 should be PureLogic, not Orchestrator"
561        );
562    }
563
564    #[test]
565    fn test_simple_constructor_detection() {
566        // Test: ContextMatcher::any() case (spec 117)
567        let func = create_test_metrics("any", 1, 0, 9);
568        assert!(
569            is_simple_constructor(&func),
570            "any() should be detected as constructor"
571        );
572
573        // Test: Standard new() constructor
574        let func = create_test_metrics("new", 1, 0, 5);
575        assert!(
576            is_simple_constructor(&func),
577            "new() should be detected as constructor"
578        );
579
580        // Test: from_* constructor
581        let func = create_test_metrics("from_config", 1, 0, 8);
582        assert!(
583            is_simple_constructor(&func),
584            "from_config() should be detected as constructor"
585        );
586
587        // Test: with_* constructor
588        let func = create_test_metrics("with_defaults", 2, 1, 12);
589        assert!(
590            is_simple_constructor(&func),
591            "with_defaults() should be detected as constructor"
592        );
593
594        // Test: Complex factory should NOT match
595        let func = create_test_metrics("create_complex", 8, 12, 50);
596        assert!(
597            !is_simple_constructor(&func),
598            "Complex function should NOT be detected as constructor"
599        );
600
601        // Test: Long function should NOT match
602        let func = create_test_metrics("new_complex", 2, 2, 25);
603        assert!(
604            !is_simple_constructor(&func),
605            "Long function should NOT be detected as constructor"
606        );
607
608        // Test: High cognitive complexity should NOT match
609        let func = create_test_metrics("new_with_logic", 2, 8, 10);
610        assert!(
611            !is_simple_constructor(&func),
612            "High cognitive complexity should NOT be detected as constructor"
613        );
614    }
615
616    #[test]
617    fn test_constructor_classification_precedence() {
618        let graph = CallGraph::new();
619        let func = create_test_metrics("any", 1, 0, 9);
620        let func_id = FunctionId::new(PathBuf::from("context/rules.rs"), "any".to_string(), 52);
621
622        let role = classify_function_role(&func, &func_id, &graph);
623        assert_eq!(
624            role,
625            FunctionRole::IOWrapper,
626            "Simple constructor should be classified as IOWrapper, not PureLogic"
627        );
628    }
629
630    #[test]
631    fn test_constructor_name_patterns() {
632        // Test exact matches
633        assert!(is_simple_constructor(&create_test_metrics("new", 1, 0, 5)));
634        assert!(is_simple_constructor(&create_test_metrics(
635            "default", 1, 0, 5
636        )));
637        assert!(is_simple_constructor(&create_test_metrics(
638            "empty", 1, 0, 5
639        )));
640        assert!(is_simple_constructor(&create_test_metrics("zero", 1, 0, 5)));
641
642        // Test prefix matches
643        assert!(is_simple_constructor(&create_test_metrics(
644            "from_str", 1, 0, 8
645        )));
646        assert!(is_simple_constructor(&create_test_metrics(
647            "with_capacity",
648            2,
649            1,
650            10
651        )));
652        assert!(is_simple_constructor(&create_test_metrics(
653            "create_default",
654            1,
655            0,
656            7
657        )));
658        assert!(is_simple_constructor(&create_test_metrics(
659            "make_instance",
660            1,
661            0,
662            6
663        )));
664        assert!(is_simple_constructor(&create_test_metrics(
665            "build_config",
666            2,
667            2,
668            12
669        )));
670
671        // Test non-constructor names
672        let func = create_test_metrics("calculate_score", 1, 0, 5);
673        assert!(
674            !is_simple_constructor(&func),
675            "Non-constructor name should not match"
676        );
677    }
678
679    #[test]
680    fn test_constructor_complexity_thresholds() {
681        // Test at threshold boundaries
682        let func = create_test_metrics("new", 2, 3, 14);
683        assert!(
684            is_simple_constructor(&func),
685            "At threshold limits should match"
686        );
687
688        // Test just over cyclomatic threshold
689        let func = create_test_metrics("new", 3, 2, 10);
690        assert!(
691            !is_simple_constructor(&func),
692            "Over cyclomatic threshold should not match"
693        );
694
695        // Test just over cognitive threshold
696        let func = create_test_metrics("new", 1, 4, 10);
697        assert!(
698            !is_simple_constructor(&func),
699            "Over cognitive threshold should not match"
700        );
701
702        // Test just over length threshold
703        let func = create_test_metrics("new", 1, 2, 15);
704        assert!(
705            !is_simple_constructor(&func),
706            "Over length threshold should not match"
707        );
708
709        // Test nesting threshold
710        let mut func = create_test_metrics("new", 1, 2, 10);
711        func.nesting = 2;
712        assert!(
713            !is_simple_constructor(&func),
714            "Over nesting threshold should not match"
715        );
716    }
717
718    #[test]
719    fn test_ast_detects_non_standard_constructor() {
720        use syn::parse_quote;
721
722        let source: syn::ItemFn = parse_quote! {
723            pub fn create_default_client() -> Self {
724                Self {
725                    timeout: Duration::from_secs(30),
726                    retries: 3,
727                }
728            }
729        };
730
731        let func = create_test_metrics("create_default_client", 1, 0, 5);
732
733        // With AST: should be detected as constructor
734        assert!(
735            is_constructor_enhanced(&func, Some(&source)),
736            "AST should detect non-standard constructor name"
737        );
738
739        // Without AST: fallback to name-based (should also match due to create_ prefix)
740        assert!(
741            is_constructor_enhanced(&func, None),
742            "Should fallback to name-based detection"
743        );
744    }
745
746    #[test]
747    fn test_ast_detects_result_self_constructor() {
748        use syn::parse_quote;
749
750        let source: syn::ItemFn = parse_quote! {
751            pub fn try_new(value: i32) -> Result<Self, Error> {
752                if value > 0 {
753                    Ok(Self { value })
754                } else {
755                    Err(Error::InvalidValue)
756                }
757            }
758        };
759
760        let func = create_test_metrics("try_new", 2, 1, 8);
761
762        // With AST: should be detected as constructor (Result<Self>)
763        assert!(
764            is_constructor_enhanced(&func, Some(&source)),
765            "AST should detect Result<Self> constructor"
766        );
767    }
768
769    #[test]
770    fn test_ast_rejects_loop_in_constructor() {
771        use syn::parse_quote;
772
773        let source: syn::ItemFn = parse_quote! {
774            pub fn process_items() -> Self {
775                let mut result = Self::new();
776                for item in items {
777                    result.add(item);
778                }
779                result
780            }
781        };
782
783        let func = create_test_metrics("process_items", 2, 3, 8);
784
785        // Should NOT be detected as constructor due to loop
786        assert!(
787            !is_constructor_enhanced(&func, Some(&source)),
788            "AST should reject constructors with loops"
789        );
790    }
791
792    #[test]
793    fn test_ast_fallback_when_not_returning_self() {
794        use syn::parse_quote;
795
796        let source: syn::ItemFn = parse_quote! {
797            pub fn calculate_value() -> i32 {
798                42
799            }
800        };
801
802        let func = create_test_metrics("calculate_value", 1, 0, 3);
803
804        // Should fallback to name-based (which will reject non-constructor name)
805        assert!(
806            !is_constructor_enhanced(&func, Some(&source)),
807            "AST should fallback when not returning Self"
808        );
809    }
810
811    #[test]
812    fn test_enum_converter_framework_type_name() {
813        use syn::parse_quote;
814
815        let source: syn::ItemFn = parse_quote! {
816            pub fn name(&self) -> &'static str {
817                match self {
818                    FrameworkType::Django => "Django",
819                    FrameworkType::Flask => "Flask",
820                    FrameworkType::PyQt => "PyQt",
821                }
822            }
823        };
824
825        let func = create_test_metrics("name", 3, 0, 7);
826
827        // Should be detected as enum converter
828        assert!(
829            is_enum_converter_enhanced(&func, &source),
830            "FrameworkType::name should be detected as enum converter"
831        );
832    }
833
834    #[test]
835    fn test_enum_converter_builtin_exception_as_str() {
836        use syn::parse_quote;
837
838        let source: syn::ItemFn = parse_quote! {
839            fn as_str(&self) -> &str {
840                match self {
841                    Self::BaseException => "BaseException",
842                    Self::ValueError => "ValueError",
843                    Self::TypeError => "TypeError",
844                }
845            }
846        };
847
848        let func = create_test_metrics("as_str", 3, 0, 7);
849
850        // Should be detected as enum converter
851        assert!(
852            is_enum_converter_enhanced(&func, &source),
853            "BuiltinException::as_str should be detected as enum converter"
854        );
855    }
856
857    #[test]
858    fn test_enum_converter_with_function_calls_not_detected() {
859        use syn::parse_quote;
860
861        let source: syn::ItemFn = parse_quote! {
862            pub fn process(&self) -> String {
863                match self {
864                    Variant::A => format!("A"),
865                    Variant::B => format!("B"),
866                }
867            }
868        };
869
870        let func = create_test_metrics("process", 2, 1, 7);
871
872        // Should NOT be detected (has function calls)
873        assert!(
874            !is_enum_converter_enhanced(&func, &source),
875            "Converter with function calls should NOT be detected"
876        );
877    }
878
879    #[test]
880    fn test_enum_converter_high_cognitive_complexity_rejected() {
881        use syn::parse_quote;
882
883        let source: syn::ItemFn = parse_quote! {
884            pub fn name(&self) -> &'static str {
885                match self {
886                    Type::A => "A",
887                    Type::B => "B",
888                }
889            }
890        };
891
892        let func = create_test_metrics("name", 2, 5, 7); // cognitive = 5
893
894        // Should be rejected due to high cognitive complexity
895        assert!(
896            !is_enum_converter_enhanced(&func, &source),
897            "High cognitive complexity should be rejected"
898        );
899    }
900
901    #[test]
902    fn test_enum_converter_classification_precedence() {
903        use syn::parse_quote;
904
905        let graph = CallGraph::new();
906
907        let source: syn::ItemFn = parse_quote! {
908            pub fn name(&self) -> &'static str {
909                match self {
910                    FrameworkType::Django => "Django",
911                    FrameworkType::Flask => "Flask",
912                }
913            }
914        };
915
916        let func = create_test_metrics("name", 2, 0, 7);
917        let func_id = FunctionId::new(PathBuf::from("framework.rs"), "name".to_string(), 10);
918
919        let role = classify_by_rules(&func, &func_id, &graph, Some(&source));
920        assert_eq!(
921            role,
922            Some(FunctionRole::IOWrapper),
923            "Enum converter should be classified as IOWrapper"
924        );
925    }
926
927    #[test]
928    fn test_ast_detection_can_be_disabled() {
929        use syn::parse_quote;
930
931        let source: syn::ItemFn = parse_quote! {
932            pub fn create_default_client() -> Self {
933                Self { field: 0 }
934            }
935        };
936
937        let func = create_test_metrics("create_default_client", 1, 0, 5);
938
939        // When AST detection is disabled via config, should use name-based only
940        // NOTE: This test assumes config can be set, which currently uses a static global
941        // In a real implementation, we'd inject config or use a test-specific config
942
943        // With AST enabled (default), should detect
944        assert!(
945            is_constructor_enhanced(&func, Some(&source)),
946            "Should detect with AST when enabled"
947        );
948
949        // Without AST parameter (None), should fallback to name-based
950        assert!(
951            is_constructor_enhanced(&func, None),
952            "Should fallback to name-based when AST unavailable"
953        );
954    }
955
956    // Accessor detection tests (spec 125)
957
958    #[test]
959    fn test_simple_field_accessor_detected() {
960        let metrics = create_test_metrics("id", 1, 0, 3);
961        assert!(
962            is_accessor_method(&metrics, None),
963            "id() should be detected as accessor"
964        );
965
966        let metrics = create_test_metrics("get_name", 1, 0, 5);
967        assert!(
968            is_accessor_method(&metrics, None),
969            "get_name() should be detected as accessor"
970        );
971    }
972
973    #[test]
974    fn test_bool_accessor_detected() {
975        let metrics = create_test_metrics("is_active", 2, 1, 8);
976        assert!(
977            is_accessor_method(&metrics, None),
978            "is_active() should be detected as accessor"
979        );
980
981        let metrics = create_test_metrics("has_permission", 2, 0, 5);
982        assert!(
983            is_accessor_method(&metrics, None),
984            "has_permission() should be detected as accessor"
985        );
986    }
987
988    #[test]
989    fn test_converter_method_detected() {
990        let metrics = create_test_metrics("as_str", 1, 0, 3);
991        assert!(
992            is_accessor_method(&metrics, None),
993            "as_str() should be detected as accessor"
994        );
995
996        let metrics = create_test_metrics("to_string", 1, 0, 4);
997        assert!(
998            is_accessor_method(&metrics, None),
999            "to_string() should be detected as accessor"
1000        );
1001    }
1002
1003    #[test]
1004    fn test_complex_method_not_detected_as_accessor() {
1005        // High complexity despite accessor name
1006        let metrics = create_test_metrics("get_value", 5, 3, 20);
1007        assert!(
1008            !is_accessor_method(&metrics, None),
1009            "Complex method should not be detected as accessor"
1010        );
1011    }
1012
1013    #[test]
1014    fn test_business_logic_not_misclassified_as_accessor() {
1015        // Business logic method
1016        let metrics = create_test_metrics("calculate_total", 4, 2, 15);
1017        assert!(
1018            !is_accessor_method(&metrics, None),
1019            "Business logic should not be detected as accessor"
1020        );
1021    }
1022
1023    #[test]
1024    fn test_ast_body_validation_for_accessor() {
1025        use syn::parse_quote;
1026
1027        let code: syn::ItemFn = parse_quote! {
1028            pub fn id(&self) -> u32 {
1029                self.id
1030            }
1031        };
1032
1033        assert!(
1034            is_simple_accessor_body(&code),
1035            "Simple field access should be valid accessor body"
1036        );
1037    }
1038
1039    #[test]
1040    fn test_accessor_side_effect_rejected() {
1041        use syn::parse_quote;
1042
1043        let code: syn::ItemFn = parse_quote! {
1044            pub fn get_value(&self) -> i32 {
1045                self.counter.fetch_add(1, Ordering::SeqCst);
1046                self.value
1047            }
1048        };
1049
1050        assert!(
1051            !is_simple_accessor_body(&code),
1052            "Accessor with side effects should be rejected"
1053        );
1054    }
1055
1056    #[test]
1057    fn test_accessor_classification_integration() {
1058        let graph = CallGraph::new();
1059
1060        // Simple accessor should be IOWrapper
1061        let func = create_test_metrics("id", 1, 0, 3);
1062        let func_id = FunctionId::new(PathBuf::from("user.rs"), "id".to_string(), 10);
1063
1064        let role = classify_function_role(&func, &func_id, &graph);
1065        assert_eq!(
1066            role,
1067            FunctionRole::IOWrapper,
1068            "Simple accessor should be classified as IOWrapper"
1069        );
1070    }
1071
1072    #[test]
1073    fn test_accessor_with_reference_return() {
1074        use syn::parse_quote;
1075
1076        let code: syn::ItemFn = parse_quote! {
1077            pub fn name(&self) -> &str {
1078                &self.name
1079            }
1080        };
1081
1082        assert!(
1083            is_simple_accessor_body(&code),
1084            "Reference to field should be valid accessor body"
1085        );
1086    }
1087
1088    #[test]
1089    fn test_accessor_with_method_call() {
1090        use syn::parse_quote;
1091
1092        let code: syn::ItemFn = parse_quote! {
1093            pub fn name(&self) -> String {
1094                self.name.clone()
1095            }
1096        };
1097
1098        assert!(
1099            is_simple_accessor_body(&code),
1100            "Field with .clone() should be valid accessor body"
1101        );
1102    }
1103
1104    #[test]
1105    fn test_accessor_single_word_patterns() {
1106        // Test all single-word patterns
1107        for name in &[
1108            "id", "name", "value", "kind", "type", "status", "code", "key", "index",
1109        ] {
1110            let metrics = create_test_metrics(name, 1, 0, 3);
1111            assert!(
1112                is_accessor_method(&metrics, None),
1113                "{} should be detected as accessor",
1114                name
1115            );
1116        }
1117    }
1118
1119    #[test]
1120    fn test_accessor_prefix_patterns() {
1121        // Test all prefix patterns
1122        for (name, expected) in &[
1123            ("get_value", true),
1124            ("is_active", true),
1125            ("has_data", true),
1126            ("can_execute", true),
1127            ("should_run", true),
1128            ("as_str", true),
1129            ("to_string", true),
1130            ("into_iter", true),
1131        ] {
1132            let metrics = create_test_metrics(name, 1, 0, 5);
1133            assert_eq!(
1134                is_accessor_method(&metrics, None),
1135                *expected,
1136                "{} accessor detection mismatch",
1137                name
1138            );
1139        }
1140    }
1141
1142    #[test]
1143    fn test_accessor_complexity_thresholds() {
1144        // At threshold (should pass)
1145        let metrics = create_test_metrics("get_value", 2, 1, 9);
1146        assert!(
1147            is_accessor_method(&metrics, None),
1148            "At threshold should be detected"
1149        );
1150
1151        // Over cyclomatic threshold (should fail)
1152        let metrics = create_test_metrics("get_value", 3, 1, 9);
1153        assert!(
1154            !is_accessor_method(&metrics, None),
1155            "Over cyclomatic threshold should not be detected"
1156        );
1157
1158        // Over cognitive threshold (should fail)
1159        let metrics = create_test_metrics("get_value", 2, 2, 9);
1160        assert!(
1161            !is_accessor_method(&metrics, None),
1162            "Over cognitive threshold should not be detected"
1163        );
1164
1165        // Over length threshold (should fail)
1166        let metrics = create_test_metrics("get_value", 2, 1, 10);
1167        assert!(
1168            !is_accessor_method(&metrics, None),
1169            "At or over length threshold should not be detected"
1170        );
1171    }
1172
1173    #[test]
1174    fn test_accessor_requires_immutable_self() {
1175        use syn::parse_quote;
1176
1177        // Immutable self - should pass
1178        let code: syn::ItemFn = parse_quote! {
1179            pub fn value(&self) -> i32 {
1180                self.value
1181            }
1182        };
1183
1184        assert!(
1185            is_simple_accessor_body(&code),
1186            "Immutable self should be valid"
1187        );
1188
1189        // Mutable self - should fail
1190        let code: syn::ItemFn = parse_quote! {
1191            pub fn value(&mut self) -> i32 {
1192                self.value
1193            }
1194        };
1195
1196        assert!(
1197            !is_simple_accessor_body(&code),
1198            "Mutable self should be rejected"
1199        );
1200    }
1201
1202    #[test]
1203    fn test_accessor_precedence_in_classification() {
1204        use syn::parse_quote;
1205
1206        let graph = CallGraph::new();
1207
1208        let code: syn::ItemFn = parse_quote! {
1209            pub fn id(&self) -> u32 {
1210                self.id
1211            }
1212        };
1213
1214        let func = create_test_metrics("id", 1, 0, 3);
1215        let func_id = FunctionId::new(PathBuf::from("test.rs"), "id".to_string(), 10);
1216
1217        let role = classify_by_rules(&func, &func_id, &graph, Some(&code));
1218        assert_eq!(
1219            role,
1220            Some(FunctionRole::IOWrapper),
1221            "Accessor should be classified as IOWrapper"
1222        );
1223    }
1224
1225    // Debug role detection tests (spec 119)
1226
1227    #[test]
1228    fn test_debug_function_with_diagnostics_suffix() {
1229        let func = create_test_metrics("handle_call_graph_diagnostics", 5, 3, 20);
1230        assert!(
1231            is_debug_function(&func),
1232            "Function with _diagnostics suffix should be detected as debug"
1233        );
1234    }
1235
1236    #[test]
1237    fn test_debug_function_with_debug_prefix() {
1238        let func = create_test_metrics("debug_print_info", 3, 2, 15);
1239        assert!(
1240            is_debug_function(&func),
1241            "Function with debug_ prefix should be detected as debug"
1242        );
1243    }
1244
1245    #[test]
1246    fn test_print_function_detected_as_debug() {
1247        let func = create_test_metrics("print_statistics", 4, 3, 18);
1248        assert!(
1249            is_debug_function(&func),
1250            "Function with print_ prefix should be detected as debug"
1251        );
1252    }
1253
1254    #[test]
1255    fn test_dump_function_detected_as_debug() {
1256        let func = create_test_metrics("dump_state", 2, 1, 10);
1257        assert!(
1258            is_debug_function(&func),
1259            "Function with dump_ prefix should be detected as debug"
1260        );
1261    }
1262
1263    #[test]
1264    fn test_trace_function_detected_as_debug() {
1265        let func = create_test_metrics("trace_execution", 3, 2, 12);
1266        assert!(
1267            is_debug_function(&func),
1268            "Function with trace_ prefix should be detected as debug"
1269        );
1270    }
1271
1272    #[test]
1273    fn test_high_complexity_debug_name_not_debug() {
1274        // High complexity should prevent misclassification
1275        let func = create_test_metrics("debug_complex_algorithm", 15, 12, 50);
1276        assert!(
1277            !is_debug_function(&func),
1278            "High complexity function should not be classified as debug even with debug name"
1279        );
1280    }
1281
1282    #[test]
1283    fn test_debug_stats_function() {
1284        let func = create_test_metrics("calculate_stats", 4, 3, 20);
1285        assert!(
1286            is_debug_function(&func),
1287            "Function ending with _stats should be detected as debug"
1288        );
1289    }
1290
1291    #[test]
1292    fn test_debug_classification_integration() {
1293        let graph = CallGraph::new();
1294        // Use a function that clearly matches debug pattern without entry point pattern
1295        let func = create_test_metrics("print_call_graph_diagnostics", 5, 3, 20);
1296        let func_id = FunctionId::new(
1297            PathBuf::from("commands/analyze.rs"),
1298            "print_call_graph_diagnostics".to_string(),
1299            396,
1300        );
1301
1302        let role = classify_function_role(&func, &func_id, &graph);
1303        assert_eq!(
1304            role,
1305            FunctionRole::Debug,
1306            "Diagnostic function should be classified as Debug role"
1307        );
1308    }
1309
1310    #[test]
1311    fn test_entry_point_takes_precedence_over_debug() {
1312        let graph = CallGraph::new();
1313        // Function with both entry point and debug patterns - entry point wins
1314        let func = create_test_metrics("handle_diagnostics", 5, 3, 20);
1315        let func_id = FunctionId::new(
1316            PathBuf::from("commands/analyze.rs"),
1317            "handle_diagnostics".to_string(),
1318            396,
1319        );
1320
1321        let role = classify_function_role(&func, &func_id, &graph);
1322        assert_eq!(
1323            role,
1324            FunctionRole::EntryPoint,
1325            "Entry point pattern should take precedence over debug pattern"
1326        );
1327    }
1328
1329    #[test]
1330    fn test_business_logic_not_misclassified_as_debug() {
1331        let func = create_test_metrics("calculate_complexity", 8, 5, 30);
1332        assert!(
1333            !is_debug_function(&func),
1334            "Business logic function should not be detected as debug"
1335        );
1336    }
1337
1338    #[test]
1339    fn test_debug_role_multiplier() {
1340        let multiplier = get_role_multiplier(FunctionRole::Debug);
1341        assert_eq!(
1342            multiplier, 0.3,
1343            "Debug role should have lowest multiplier (0.3)"
1344        );
1345    }
1346
1347    #[test]
1348    fn test_matches_debug_pattern() {
1349        assert!(matches_debug_pattern("debug_foo"));
1350        assert!(matches_debug_pattern("print_bar"));
1351        assert!(matches_debug_pattern("dump_state"));
1352        assert!(matches_debug_pattern("trace_execution"));
1353        assert!(matches_debug_pattern("foo_diagnostics"));
1354        assert!(matches_debug_pattern("bar_debug"));
1355        assert!(matches_debug_pattern("baz_stats"));
1356        assert!(matches_debug_pattern("test_diagnostics_helper"));
1357
1358        // Should not match
1359        assert!(!matches_debug_pattern("calculate"));
1360        assert!(!matches_debug_pattern("process"));
1361        assert!(!matches_debug_pattern("validate"));
1362    }
1363
1364    #[test]
1365    fn test_diagnostic_characteristics() {
1366        // Should match: simple output function
1367        let func = create_test_metrics("print_output", 3, 2, 15);
1368        assert!(
1369            has_diagnostic_characteristics(&func),
1370            "Simple print function should have diagnostic characteristics"
1371        );
1372
1373        // Should not match: complex function
1374        let func = create_test_metrics("print_complex", 10, 8, 40);
1375        assert!(
1376            !has_diagnostic_characteristics(&func),
1377            "Complex function should not have diagnostic characteristics"
1378        );
1379
1380        // Should not match: no output I/O pattern
1381        let func = create_test_metrics("calculate_total", 3, 2, 15);
1382        assert!(
1383            !has_diagnostic_characteristics(&func),
1384            "Non-output function should not have diagnostic characteristics"
1385        );
1386
1387        // Should not match: read/write operations (not diagnostic output)
1388        let func = create_test_metrics("read_file", 3, 2, 15);
1389        assert!(
1390            !has_diagnostic_characteristics(&func),
1391            "Read/write operations should not have diagnostic characteristics"
1392        );
1393    }
1394}