Skip to main content

debtmap/analysis/
functional_composition.rs

1//! AST-Based Functional Pattern Detection
2//!
3//! This module provides deep AST analysis to detect actual functional composition patterns
4//! in Rust code, including:
5//! - Iterator pipelines (.iter(), .map(), .filter(), .collect())
6//! - Purity analysis (no mutable state, no side effects)
7//! - Functional composition quality metrics
8//! - Integration with orchestration quality assessment
9//!
10//! Implements Specification 111: AST-Based Functional Pattern Detection
11
12use serde::{Deserialize, Serialize};
13use syn::{Block, Expr, ExprMethodCall, ItemFn, Local, Stmt};
14
15/// Configuration for functional pattern analysis with three profiles
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct FunctionalAnalysisConfig {
18    /// Minimum pipeline depth to consider (default: 2)
19    pub min_pipeline_depth: usize,
20    /// Maximum acceptable closure complexity (default: 5)
21    pub max_closure_complexity: u32,
22    /// Minimum purity score for "pure" label (default: 0.8)
23    pub purity_threshold: f64,
24    /// Minimum quality for score boost (default: 0.6)
25    pub composition_quality_threshold: f64,
26    /// Skip analysis for trivial functions (default: 3)
27    pub min_function_complexity: u32,
28}
29
30impl Default for FunctionalAnalysisConfig {
31    fn default() -> Self {
32        Self::balanced()
33    }
34}
35
36impl FunctionalAnalysisConfig {
37    /// Strict configuration for codebases emphasizing functional purity
38    pub fn strict() -> Self {
39        Self {
40            min_pipeline_depth: 3,
41            max_closure_complexity: 3,
42            purity_threshold: 0.9,
43            composition_quality_threshold: 0.7,
44            min_function_complexity: 2,
45        }
46    }
47
48    /// Balanced configuration (default) for typical Rust codebases
49    pub fn balanced() -> Self {
50        Self {
51            min_pipeline_depth: 2,
52            max_closure_complexity: 5,
53            purity_threshold: 0.8,
54            composition_quality_threshold: 0.6,
55            min_function_complexity: 3,
56        }
57    }
58
59    /// Lenient configuration for imperative-heavy codebases
60    pub fn lenient() -> Self {
61        Self {
62            min_pipeline_depth: 2,
63            max_closure_complexity: 10,
64            purity_threshold: 0.5,
65            composition_quality_threshold: 0.4,
66            min_function_complexity: 5,
67        }
68    }
69
70    /// Check if a function should be analyzed based on complexity threshold
71    pub fn should_analyze(&self, complexity: u32) -> bool {
72        complexity >= self.min_function_complexity
73    }
74}
75
76/// Pipeline stage in a functional composition
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78pub enum PipelineStage {
79    /// Iterator initialization (.iter(), .into_iter(), .iter_mut())
80    Iterator { method: String },
81    /// Map transformation
82    Map {
83        closure_complexity: u32,
84        has_nested_pipeline: bool,
85    },
86    /// Filter predicate
87    Filter {
88        closure_complexity: u32,
89        has_nested_pipeline: bool,
90    },
91    /// Fold/reduce aggregation
92    Fold {
93        init_complexity: u32,
94        fold_complexity: u32,
95    },
96    /// FlatMap transformation
97    FlatMap {
98        closure_complexity: u32,
99        has_nested_pipeline: bool,
100    },
101    /// Inspect (side-effect aware)
102    Inspect { closure_complexity: u32 },
103    /// AndThen for Result/Option chaining
104    AndThen { closure_complexity: u32 },
105    /// MapErr for error transformation
106    MapErr { closure_complexity: u32 },
107}
108
109/// Terminal operation in a pipeline
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
111pub enum TerminalOp {
112    Collect,
113    Sum,
114    Count,
115    Any,
116    All,
117    Find,
118    Reduce,
119    ForEach,
120}
121
122/// A functional pipeline detected in code
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct Pipeline {
125    /// Stages in the pipeline
126    pub stages: Vec<PipelineStage>,
127    /// Depth of the pipeline (number of stages)
128    pub depth: usize,
129    /// Whether this uses parallel iteration
130    pub is_parallel: bool,
131    /// Terminal operation if any
132    pub terminal_operation: Option<TerminalOp>,
133    /// Nesting level (0 for top-level, >0 for nested pipelines)
134    pub nesting_level: usize,
135    /// Whether this is a builder pattern (not a functional pipeline)
136    pub builder_pattern: bool,
137}
138
139/// Classification of side effects
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub enum SideEffectKind {
142    /// No side effects
143    Pure,
144    /// Only logging/tracing/metrics (small penalty)
145    Benign,
146    /// I/O, mutation, network (large penalty)
147    Impure,
148}
149
150/// Purity metrics for a function
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152pub struct PurityMetrics {
153    /// Has mutable state
154    pub has_mutable_state: bool,
155    /// Has side effects (I/O, global mutation)
156    pub has_side_effects: bool,
157    /// Ratio of immutable bindings to total
158    pub immutability_ratio: f64,
159    /// Is declared as const fn
160    pub is_const_fn: bool,
161    /// Classification of side effects
162    pub side_effect_kind: SideEffectKind,
163    /// Purity score (0.0 impure to 1.0 pure)
164    pub score: f64,
165}
166
167/// Complete composition metrics for a function
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169pub struct CompositionMetrics {
170    /// Detected pipelines
171    pub pipelines: Vec<Pipeline>,
172    /// Purity score
173    pub purity_score: f64,
174    /// Immutability ratio
175    pub immutability_ratio: f64,
176    /// Overall composition quality (0.0-1.0)
177    pub composition_quality: f64,
178    /// Side effect classification
179    pub side_effect_kind: SideEffectKind,
180}
181
182/// Internal accumulator for purity analysis (functional pattern)
183#[derive(Default, Clone, Debug)]
184struct PurityAccumulator {
185    mutable_bindings: usize,
186    immutable_bindings: usize,
187    io_operations: Vec<String>,
188    global_mutations: Vec<String>,
189    benign_side_effects: Vec<String>,
190}
191
192impl PurityAccumulator {
193    /// Merge two accumulators (functional composition)
194    fn merge(self, other: Self) -> Self {
195        Self {
196            mutable_bindings: self.mutable_bindings + other.mutable_bindings,
197            immutable_bindings: self.immutable_bindings + other.immutable_bindings,
198            io_operations: [self.io_operations, other.io_operations].concat(),
199            global_mutations: [self.global_mutations, other.global_mutations].concat(),
200            benign_side_effects: [self.benign_side_effects, other.benign_side_effects].concat(),
201        }
202    }
203
204    fn total_bindings(&self) -> usize {
205        self.mutable_bindings + self.immutable_bindings
206    }
207}
208
209/// Main entry point for functional composition analysis
210/// Uses pure functions for stateless analysis
211pub fn analyze_composition(
212    function: &ItemFn,
213    config: &FunctionalAnalysisConfig,
214) -> CompositionMetrics {
215    // Early exit for empty functions
216    if function.block.stmts.is_empty() {
217        return CompositionMetrics {
218            pipelines: Vec::new(),
219            purity_score: 1.0,
220            immutability_ratio: 1.0,
221            composition_quality: 0.5,
222            side_effect_kind: SideEffectKind::Pure,
223        };
224    }
225
226    let pipelines = detect_pipelines(function, config);
227    let purity = analyze_purity(function, config);
228    let quality = score_composition(&pipelines, &purity, config);
229
230    CompositionMetrics {
231        pipelines,
232        purity_score: purity.score,
233        immutability_ratio: purity.immutability_ratio,
234        composition_quality: quality,
235        side_effect_kind: purity.side_effect_kind,
236    }
237}
238
239/// Detect functional pipelines in a function
240pub fn detect_pipelines(function: &ItemFn, config: &FunctionalAnalysisConfig) -> Vec<Pipeline> {
241    collect_pipelines(&function.block, config, 0)
242        .into_iter()
243        .filter(|p| p.depth >= config.min_pipeline_depth)
244        .collect()
245}
246
247/// Collect pipelines from a block (recursive)
248fn collect_pipelines(
249    block: &Block,
250    config: &FunctionalAnalysisConfig,
251    nesting: usize,
252) -> Vec<Pipeline> {
253    // Early exit for empty blocks
254    if block.stmts.is_empty() {
255        return Vec::new();
256    }
257
258    block
259        .stmts
260        .iter()
261        .flat_map(|stmt| extract_pipeline_from_stmt(stmt, config, nesting))
262        .collect()
263}
264
265/// Extract pipeline from a statement
266fn extract_pipeline_from_stmt(
267    stmt: &Stmt,
268    config: &FunctionalAnalysisConfig,
269    nesting: usize,
270) -> Vec<Pipeline> {
271    match stmt {
272        Stmt::Local(local) => {
273            if let Some(init) = &local.init {
274                extract_pipeline_from_expr(&init.expr, config, nesting)
275            } else {
276                vec![]
277            }
278        }
279        Stmt::Expr(expr, _) => extract_pipeline_from_expr(expr, config, nesting),
280        Stmt::Macro(mac) => extract_pipeline_from_expr(
281            &syn::parse2(mac.mac.tokens.clone()).unwrap_or_else(|_| syn::parse_quote!(())),
282            config,
283            nesting,
284        ),
285        _ => vec![],
286    }
287}
288
289/// Extract pipeline from an expression
290fn extract_pipeline_from_expr(
291    expr: &Expr,
292    config: &FunctionalAnalysisConfig,
293    nesting: usize,
294) -> Vec<Pipeline> {
295    match expr {
296        Expr::MethodCall(method_call) => {
297            extract_pipeline_from_method_call(method_call, config, nesting)
298        }
299        Expr::Block(block) => collect_pipelines(&block.block, config, nesting + 1),
300        Expr::If(if_expr) => {
301            let mut pipelines = collect_pipelines(&if_expr.then_branch, config, nesting + 1);
302            if let Some((_, else_expr)) = &if_expr.else_branch {
303                pipelines.extend(extract_pipeline_from_expr(else_expr, config, nesting + 1));
304            }
305            pipelines
306        }
307        Expr::Match(match_expr) => match_expr
308            .arms
309            .iter()
310            .flat_map(|arm| extract_pipeline_from_expr(&arm.body, config, nesting + 1))
311            .collect(),
312        _ => vec![],
313    }
314}
315
316/// Extract pipeline from a method call chain
317/// Classification of iterator methods by their semantic role
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319enum MethodClassification {
320    // Iterator constructors
321    ParallelIterator,
322    StandardIterator,
323    IteratorConstructor,
324    SliceIterator,
325    CollectionIterator,
326    StdIterConstructor,
327
328    // Transformation stages
329    Map,
330    Filter,
331    Fold,
332    FlatMap,
333    FilterMap,
334    AdapterMethod,
335    SimpleTransform,
336    OrderAdapter,
337
338    // Terminal operations (with or without transformation)
339    TerminalCollect,
340    TerminalSum,
341    TerminalCount,
342    TerminalAny,
343    TerminalAll,
344    TerminalFind,
345    TerminalForEach,
346    TerminalPartition,
347    TerminalUnzip,
348    TerminalReduce,
349    TerminalPosition,
350    TerminalElementAccess,
351    TerminalProduct,
352
353    // Not recognized
354    Unknown,
355}
356
357/// Classify an iterator method by its semantic role
358fn classify_method(method: &str) -> MethodClassification {
359    match method {
360        // Parallel iterators
361        "par_iter" | "par_iter_mut" | "into_par_iter" | "par_bridge" => {
362            MethodClassification::ParallelIterator
363        }
364        // Standard iterators
365        "iter" | "into_iter" | "iter_mut" => MethodClassification::StandardIterator,
366        // Iterator constructors (these ARE iterators, not receivers)
367        "lines" | "chars" | "bytes" | "split_whitespace" => {
368            MethodClassification::IteratorConstructor
369        }
370        // Slice/collection iterators
371        "windows" | "chunks" | "chunks_exact" | "rchunks" | "split" | "rsplit"
372        | "split_terminator" => MethodClassification::SliceIterator,
373        // Collection-specific iterators
374        "into_values" | "into_keys" | "values" | "keys" => MethodClassification::CollectionIterator,
375        // std::iter constructors
376        "once" | "repeat" | "repeat_with" | "from_fn" | "successors" | "empty" => {
377            MethodClassification::StdIterConstructor
378        }
379        // Core transformation stages
380        "map" => MethodClassification::Map,
381        "filter" => MethodClassification::Filter,
382        "fold" | "reduce" | "scan" | "try_fold" | "try_for_each" => MethodClassification::Fold,
383        "flat_map" => MethodClassification::FlatMap,
384        "filter_map" => MethodClassification::FilterMap,
385        // Adapter methods
386        "take" | "skip" | "step_by" | "chain" | "zip" | "enumerate" | "peekable" | "fuse"
387        | "take_while" | "skip_while" | "map_while" | "by_ref" | "inspect" | "flatten" => {
388            MethodClassification::AdapterMethod
389        }
390        "cloned" | "copied" => MethodClassification::SimpleTransform,
391        "rev" | "cycle" => MethodClassification::OrderAdapter,
392        // Terminal operations
393        "collect" => MethodClassification::TerminalCollect,
394        "sum" => MethodClassification::TerminalSum,
395        "count" => MethodClassification::TerminalCount,
396        "any" => MethodClassification::TerminalAny,
397        "all" => MethodClassification::TerminalAll,
398        "find" => MethodClassification::TerminalFind,
399        "for_each" => MethodClassification::TerminalForEach,
400        "partition" => MethodClassification::TerminalPartition,
401        "unzip" => MethodClassification::TerminalUnzip,
402        "max" | "min" | "max_by" | "min_by" | "max_by_key" | "min_by_key" => {
403            MethodClassification::TerminalReduce
404        }
405        "position" | "rposition" => MethodClassification::TerminalPosition,
406        "nth" | "last" => MethodClassification::TerminalElementAccess,
407        "product" => MethodClassification::TerminalProduct,
408        _ => MethodClassification::Unknown,
409    }
410}
411
412/// Create pipeline stage from method classification
413/// Returns Some(stage) if the classification should add a transformation stage
414fn create_stage_from_classification(classification: MethodClassification) -> Option<PipelineStage> {
415    match classification {
416        // Core transformation stages
417        MethodClassification::Map => Some(PipelineStage::Map {
418            closure_complexity: 1,
419            has_nested_pipeline: false,
420        }),
421        MethodClassification::Filter => Some(PipelineStage::Filter {
422            closure_complexity: 1,
423            has_nested_pipeline: false,
424        }),
425        MethodClassification::Fold => Some(PipelineStage::Fold {
426            init_complexity: 1,
427            fold_complexity: 1,
428        }),
429        MethodClassification::FlatMap => Some(PipelineStage::FlatMap {
430            closure_complexity: 1,
431            has_nested_pipeline: false,
432        }),
433        MethodClassification::FilterMap => Some(PipelineStage::FlatMap {
434            closure_complexity: 1,
435            has_nested_pipeline: false,
436        }),
437        // Adapter methods map with no closure
438        MethodClassification::AdapterMethod
439        | MethodClassification::SimpleTransform
440        | MethodClassification::OrderAdapter => Some(PipelineStage::Map {
441            closure_complexity: 0,
442            has_nested_pipeline: false,
443        }),
444        // Terminal operations with transformation stage
445        MethodClassification::TerminalSum | MethodClassification::TerminalCount => {
446            Some(PipelineStage::Fold {
447                init_complexity: 0,
448                fold_complexity: 0,
449            })
450        }
451        MethodClassification::TerminalAny
452        | MethodClassification::TerminalAll
453        | MethodClassification::TerminalFind
454        | MethodClassification::TerminalPartition
455        | MethodClassification::TerminalPosition => Some(PipelineStage::Filter {
456            closure_complexity: 1,
457            has_nested_pipeline: false,
458        }),
459        MethodClassification::TerminalUnzip => Some(PipelineStage::Map {
460            closure_complexity: 0,
461            has_nested_pipeline: false,
462        }),
463        MethodClassification::TerminalProduct => Some(PipelineStage::Fold {
464            init_complexity: 0,
465            fold_complexity: 0,
466        }),
467        // Iterator constructors, terminal ops without stages, and unknown don't add stages
468        _ => None,
469    }
470}
471
472/// Extract terminal operation from method classification
473fn extract_terminal_op(classification: MethodClassification) -> Option<TerminalOp> {
474    match classification {
475        MethodClassification::TerminalCollect
476        | MethodClassification::TerminalPartition
477        | MethodClassification::TerminalUnzip => Some(TerminalOp::Collect),
478        MethodClassification::TerminalSum | MethodClassification::TerminalProduct => {
479            Some(TerminalOp::Sum)
480        }
481        MethodClassification::TerminalCount => Some(TerminalOp::Count),
482        MethodClassification::TerminalAny => Some(TerminalOp::Any),
483        MethodClassification::TerminalAll => Some(TerminalOp::All),
484        MethodClassification::TerminalFind
485        | MethodClassification::TerminalPosition
486        | MethodClassification::TerminalElementAccess => Some(TerminalOp::Find),
487        MethodClassification::TerminalForEach => Some(TerminalOp::ForEach),
488        MethodClassification::TerminalReduce => Some(TerminalOp::Reduce),
489        _ => None,
490    }
491}
492
493fn extract_pipeline_from_method_call(
494    method_call: &ExprMethodCall,
495    _config: &FunctionalAnalysisConfig,
496    nesting: usize,
497) -> Vec<Pipeline> {
498    let mut stages = Vec::new();
499    let mut current = method_call;
500    let mut is_parallel = false;
501    let mut terminal_op = None;
502
503    // Walk backwards through the chain
504    loop {
505        let method_str = current.method.to_string();
506        let classification = classify_method(&method_str);
507
508        // Handle parallel iterators (special case for tracking is_parallel)
509        if classification == MethodClassification::ParallelIterator {
510            is_parallel = true;
511        }
512
513        // Check if this is an iterator constructor
514        let is_iterator_constructor = matches!(
515            classification,
516            MethodClassification::ParallelIterator
517                | MethodClassification::StandardIterator
518                | MethodClassification::IteratorConstructor
519                | MethodClassification::SliceIterator
520                | MethodClassification::CollectionIterator
521                | MethodClassification::StdIterConstructor
522        );
523
524        // Iterator constructors become Iterator stages
525        if is_iterator_constructor {
526            stages.push(PipelineStage::Iterator { method: method_str });
527        }
528
529        // Add transformation stage if the classification requires one
530        if let Some(stage) = create_stage_from_classification(classification) {
531            stages.push(stage);
532        }
533
534        // Set terminal operation if present
535        if let Some(terminal) = extract_terminal_op(classification) {
536            terminal_op = Some(terminal);
537        }
538
539        // Move to the receiver
540        match &*current.receiver {
541            Expr::MethodCall(next) => current = next,
542            _ => break,
543        }
544    }
545
546    // Reverse stages to get correct order
547    stages.reverse();
548
549    // Early exit if no valid pipeline
550    if stages.is_empty() {
551        return Vec::new();
552    }
553
554    // Must start with either an iterator OR a transformation stage
555    // (Range, Option, Result don't need explicit .iter() calls)
556    if !has_iterator_start(&stages) && !has_transformation_stage(&stages) {
557        return Vec::new();
558    }
559
560    // Require at least one transformation stage (map, filter, fold, etc.)
561    // UNLESS we have a meaningful terminal operation (sum, any, find, etc.)
562    // These terminals provide functional value even without intermediate transformations
563    if !has_transformation_stage(&stages) && !has_meaningful_terminal(&terminal_op) {
564        return Vec::new();
565    }
566
567    vec![Pipeline {
568        depth: stages.len(),
569        stages,
570        is_parallel,
571        terminal_operation: terminal_op,
572        nesting_level: nesting,
573        builder_pattern: false,
574    }]
575}
576
577/// Check if stages start with an iterator
578fn has_iterator_start(stages: &[PipelineStage]) -> bool {
579    stages
580        .first()
581        .map(|s| matches!(s, PipelineStage::Iterator { .. }))
582        .unwrap_or(false)
583}
584
585/// Check if pipeline has at least one transformation stage
586/// (not just iterator initialization)
587fn has_transformation_stage(stages: &[PipelineStage]) -> bool {
588    stages.iter().any(|stage| {
589        matches!(
590            stage,
591            PipelineStage::Map { .. }
592                | PipelineStage::Filter { .. }
593                | PipelineStage::Fold { .. }
594                | PipelineStage::FlatMap { .. }
595                | PipelineStage::AndThen { .. }
596                | PipelineStage::MapErr { .. }
597                | PipelineStage::Inspect { .. }
598        )
599    })
600}
601
602/// Check if terminal operation is meaningful enough to constitute a functional pattern
603/// even without intermediate transformations (e.g., `items.iter().sum()` is functional)
604fn has_meaningful_terminal(terminal: &Option<TerminalOp>) -> bool {
605    matches!(
606        terminal,
607        Some(TerminalOp::Sum)
608            | Some(TerminalOp::Count)
609            | Some(TerminalOp::Any)
610            | Some(TerminalOp::All)
611            | Some(TerminalOp::Find)
612            | Some(TerminalOp::Reduce)
613            | Some(TerminalOp::Collect) // partition, unzip, etc.
614    )
615    // Note: Collect alone (without transformations) is NOT meaningful
616    // But we include it because partition/unzip set terminal to Collect
617}
618
619/// Analyze function purity using functional accumulation
620pub fn analyze_purity(function: &ItemFn, _config: &FunctionalAnalysisConfig) -> PurityMetrics {
621    let metrics = analyze_block_purity(&function.block);
622    let is_const_fn = function.sig.constness.is_some();
623
624    let immutability_ratio = if metrics.total_bindings() == 0 {
625        1.0
626    } else {
627        metrics.immutable_bindings as f64 / metrics.total_bindings() as f64
628    };
629
630    let side_effect_kind = classify_side_effects(&metrics);
631    let score = calculate_purity_score(&metrics, &side_effect_kind);
632
633    PurityMetrics {
634        has_mutable_state: metrics.mutable_bindings > 0,
635        has_side_effects: matches!(side_effect_kind, SideEffectKind::Impure),
636        immutability_ratio,
637        is_const_fn,
638        side_effect_kind,
639        score,
640    }
641}
642
643/// Analyze block purity using functional fold pattern
644fn analyze_block_purity(block: &Block) -> PurityAccumulator {
645    block
646        .stmts
647        .iter()
648        .map(analyze_stmt_purity)
649        .fold(PurityAccumulator::default(), |acc, metrics| {
650            acc.merge(metrics)
651        })
652}
653
654/// Analyze statement purity
655fn analyze_stmt_purity(stmt: &Stmt) -> PurityAccumulator {
656    match stmt {
657        Stmt::Local(local) => analyze_local_purity(local),
658        Stmt::Expr(expr, _) => analyze_expr_purity(expr),
659        Stmt::Macro(_) => PurityAccumulator::default(), // Macros analyzed elsewhere
660        _ => PurityAccumulator::default(),
661    }
662}
663
664/// Analyze local binding purity
665fn analyze_local_purity(local: &Local) -> PurityAccumulator {
666    // Check mutability without string conversion
667    let is_mutable =
668        matches!(&local.pat, syn::Pat::Ident(pat_ident) if pat_ident.mutability.is_some());
669
670    let mut acc = if is_mutable {
671        PurityAccumulator {
672            mutable_bindings: 1,
673            ..Default::default()
674        }
675    } else {
676        PurityAccumulator {
677            immutable_bindings: 1,
678            ..Default::default()
679        }
680    };
681
682    if let Some(init) = &local.init {
683        acc = acc.merge(analyze_expr_purity(&init.expr));
684    }
685
686    acc
687}
688
689/// Analyze expression purity
690fn analyze_expr_purity(expr: &Expr) -> PurityAccumulator {
691    match expr {
692        Expr::Macro(mac) => classify_macro_side_effect(&mac.mac),
693        Expr::Block(block) => analyze_block_purity(&block.block),
694        Expr::If(if_expr) => {
695            let then_branch = analyze_block_purity(&if_expr.then_branch);
696            let else_branch = if_expr
697                .else_branch
698                .as_ref()
699                .map(|(_, expr)| analyze_expr_purity(expr))
700                .unwrap_or_default();
701            then_branch.merge(else_branch)
702        }
703        Expr::Match(match_expr) => match_expr
704            .arms
705            .iter()
706            .map(|arm| analyze_expr_purity(&arm.body))
707            .fold(PurityAccumulator::default(), |acc, metrics| {
708                acc.merge(metrics)
709            }),
710        Expr::MethodCall(method) => analyze_method_call_purity(method),
711        _ => PurityAccumulator::default(),
712    }
713}
714
715/// Classify macro side effects
716fn classify_macro_side_effect(mac: &syn::Macro) -> PurityAccumulator {
717    let Some(last_segment) = mac.path.segments.last() else {
718        return PurityAccumulator::default();
719    };
720
721    let ident_str = last_segment.ident.to_string();
722
723    match ident_str.as_str() {
724        "println" | "eprintln" | "print" | "eprint" => PurityAccumulator {
725            io_operations: vec!["console_output".to_string()],
726            ..Default::default()
727        },
728        "debug" | "info" | "warn" | "error" | "trace" | "log" => PurityAccumulator {
729            benign_side_effects: vec![format!("logging::{}", ident_str)],
730            ..Default::default()
731        },
732        _ => PurityAccumulator::default(),
733    }
734}
735
736/// Analyze method call purity
737fn analyze_method_call_purity(method: &ExprMethodCall) -> PurityAccumulator {
738    let method_name = method.method.to_string();
739
740    // Detect known mutating methods
741    if method_name.starts_with("push")
742        || method_name.starts_with("insert")
743        || method_name.starts_with("remove")
744        || method_name.starts_with("clear")
745    {
746        PurityAccumulator {
747            global_mutations: vec![format!("mutation::{}", method_name)],
748            ..Default::default()
749        }
750    } else {
751        PurityAccumulator::default()
752    }
753}
754
755/// Classify side effects into Pure/Benign/Impure
756fn classify_side_effects(acc: &PurityAccumulator) -> SideEffectKind {
757    if !acc.io_operations.is_empty() || !acc.global_mutations.is_empty() {
758        SideEffectKind::Impure
759    } else if !acc.benign_side_effects.is_empty() {
760        SideEffectKind::Benign
761    } else {
762        SideEffectKind::Pure
763    }
764}
765
766/// Calculate purity score (0.0 impure to 1.0 pure)
767fn calculate_purity_score(acc: &PurityAccumulator, side_effect_kind: &SideEffectKind) -> f64 {
768    let mut score = 1.0;
769
770    // Side effect penalties
771    match side_effect_kind {
772        SideEffectKind::Pure => {}
773        SideEffectKind::Benign => score -= 0.1,
774        SideEffectKind::Impure => {
775            if !acc.io_operations.is_empty() {
776                score -= 0.4;
777            }
778            if !acc.global_mutations.is_empty() {
779                score -= 0.3;
780            }
781        }
782    }
783
784    // Mutability penalty
785    if acc.mutable_bindings > 0 && acc.total_bindings() > 0 {
786        let mutability_ratio = acc.mutable_bindings as f64 / acc.total_bindings() as f64;
787        score -= 0.3 * mutability_ratio;
788    }
789
790    score.max(0.0)
791}
792
793/// Score composition quality (0.0-1.0)
794pub fn score_composition(
795    pipelines: &[Pipeline],
796    purity: &PurityMetrics,
797    config: &FunctionalAnalysisConfig,
798) -> f64 {
799    // No functional pipelines = not functional code, regardless of purity
800    // Purity alone doesn't make code "functional" - it needs transformation pipelines
801    if pipelines.is_empty() {
802        return 0.0;
803    }
804
805    let pipeline_score = score_pipelines(pipelines, config);
806    let purity_weight = 0.4;
807    let pipeline_weight = 0.6;
808
809    (purity.score * purity_weight) + (pipeline_score * pipeline_weight)
810}
811
812/// Score all pipelines
813fn score_pipelines(pipelines: &[Pipeline], config: &FunctionalAnalysisConfig) -> f64 {
814    // Filter out builder patterns
815    let functional_pipelines: Vec<&Pipeline> =
816        pipelines.iter().filter(|p| !p.builder_pattern).collect();
817
818    if functional_pipelines.is_empty() {
819        return 0.0;
820    }
821
822    let total_score: f64 = functional_pipelines
823        .iter()
824        .map(|p| score_single_pipeline(p, config))
825        .sum();
826
827    (total_score / functional_pipelines.len() as f64).min(1.0)
828}
829
830/// Score a single pipeline
831fn score_single_pipeline(pipeline: &Pipeline, config: &FunctionalAnalysisConfig) -> f64 {
832    let base_score = 0.5;
833    let depth_bonus = (pipeline.depth as f64 * 0.1).min(0.3);
834    let parallel_bonus = calculate_parallel_bonus(pipeline);
835    let complexity_penalty = calculate_closure_penalty(pipeline, config);
836    let nesting_bonus = if pipeline.nesting_level > 0 { 0.1 } else { 0.0 };
837
838    (base_score + depth_bonus + parallel_bonus + nesting_bonus - complexity_penalty).clamp(0.0, 1.0)
839}
840
841/// Calculate parallel bonus (only for pipelines with sufficient depth)
842fn calculate_parallel_bonus(pipeline: &Pipeline) -> f64 {
843    if pipeline.is_parallel && pipeline.depth >= 3 {
844        0.2 // Likely worth parallelization
845    } else {
846        0.0 // Overhead may outweigh benefit
847    }
848}
849
850/// Calculate closure complexity penalty
851fn calculate_closure_penalty(pipeline: &Pipeline, config: &FunctionalAnalysisConfig) -> f64 {
852    let complexities: Vec<u32> = pipeline
853        .stages
854        .iter()
855        .filter_map(extract_closure_complexity)
856        .collect();
857
858    if complexities.is_empty() {
859        return 0.0;
860    }
861
862    let avg_complexity = complexities.iter().sum::<u32>() as f64 / complexities.len() as f64;
863    let expected_complexity = (pipeline.depth as u32 * 2).min(config.max_closure_complexity);
864
865    // Penalty based on how much closure complexity exceeds expectations
866    if avg_complexity > expected_complexity as f64 {
867        ((avg_complexity - expected_complexity as f64) * 0.05).min(0.3)
868    } else {
869        0.0
870    }
871}
872
873/// Extract closure complexity from a pipeline stage
874fn extract_closure_complexity(stage: &PipelineStage) -> Option<u32> {
875    match stage {
876        PipelineStage::Map {
877            closure_complexity, ..
878        } => Some(*closure_complexity),
879        PipelineStage::Filter {
880            closure_complexity, ..
881        } => Some(*closure_complexity),
882        PipelineStage::FlatMap {
883            closure_complexity, ..
884        } => Some(*closure_complexity),
885        PipelineStage::AndThen { closure_complexity } => Some(*closure_complexity),
886        PipelineStage::MapErr { closure_complexity } => Some(*closure_complexity),
887        PipelineStage::Fold {
888            fold_complexity, ..
889        } => Some(*fold_complexity),
890        _ => None,
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897    use syn::parse_quote;
898
899    // Tests for extracted helper functions
900    #[test]
901    fn test_classify_method_parallel_iterators() {
902        assert_eq!(
903            classify_method("par_iter"),
904            MethodClassification::ParallelIterator
905        );
906        assert_eq!(
907            classify_method("into_par_iter"),
908            MethodClassification::ParallelIterator
909        );
910    }
911
912    #[test]
913    fn test_classify_method_standard_iterators() {
914        assert_eq!(
915            classify_method("iter"),
916            MethodClassification::StandardIterator
917        );
918        assert_eq!(
919            classify_method("into_iter"),
920            MethodClassification::StandardIterator
921        );
922    }
923
924    #[test]
925    fn test_classify_method_transformations() {
926        assert_eq!(classify_method("map"), MethodClassification::Map);
927        assert_eq!(classify_method("filter"), MethodClassification::Filter);
928        assert_eq!(classify_method("fold"), MethodClassification::Fold);
929        assert_eq!(classify_method("flat_map"), MethodClassification::FlatMap);
930        assert_eq!(
931            classify_method("filter_map"),
932            MethodClassification::FilterMap
933        );
934    }
935
936    #[test]
937    fn test_classify_method_terminals() {
938        assert_eq!(
939            classify_method("collect"),
940            MethodClassification::TerminalCollect
941        );
942        assert_eq!(classify_method("sum"), MethodClassification::TerminalSum);
943        assert_eq!(classify_method("any"), MethodClassification::TerminalAny);
944        assert_eq!(classify_method("find"), MethodClassification::TerminalFind);
945    }
946
947    #[test]
948    fn test_classify_method_unknown() {
949        assert_eq!(
950            classify_method("unknown_method"),
951            MethodClassification::Unknown
952        );
953    }
954
955    #[test]
956    fn test_create_stage_from_classification_map() {
957        let stage = create_stage_from_classification(MethodClassification::Map);
958        assert!(matches!(
959            stage,
960            Some(PipelineStage::Map {
961                closure_complexity: 1,
962                has_nested_pipeline: false
963            })
964        ));
965    }
966
967    #[test]
968    fn test_create_stage_from_classification_filter() {
969        let stage = create_stage_from_classification(MethodClassification::Filter);
970        assert!(matches!(
971            stage,
972            Some(PipelineStage::Filter {
973                closure_complexity: 1,
974                has_nested_pipeline: false
975            })
976        ));
977    }
978
979    #[test]
980    fn test_create_stage_from_classification_fold() {
981        let stage = create_stage_from_classification(MethodClassification::Fold);
982        assert!(matches!(
983            stage,
984            Some(PipelineStage::Fold {
985                init_complexity: 1,
986                fold_complexity: 1
987            })
988        ));
989    }
990
991    #[test]
992    fn test_create_stage_from_classification_terminal_with_stage() {
993        // Terminal operations like sum should add a Fold stage
994        let stage = create_stage_from_classification(MethodClassification::TerminalSum);
995        assert!(matches!(
996            stage,
997            Some(PipelineStage::Fold {
998                init_complexity: 0,
999                fold_complexity: 0
1000            })
1001        ));
1002    }
1003
1004    #[test]
1005    fn test_create_stage_from_classification_no_stage() {
1006        // Iterator constructors don't create transformation stages
1007        let stage = create_stage_from_classification(MethodClassification::StandardIterator);
1008        assert_eq!(stage, None);
1009
1010        // Pure terminals without transformation don't create stages
1011        let stage = create_stage_from_classification(MethodClassification::TerminalCollect);
1012        assert_eq!(stage, None);
1013    }
1014
1015    #[test]
1016    fn test_extract_terminal_op_collect() {
1017        assert_eq!(
1018            extract_terminal_op(MethodClassification::TerminalCollect),
1019            Some(TerminalOp::Collect)
1020        );
1021    }
1022
1023    #[test]
1024    fn test_extract_terminal_op_sum() {
1025        assert_eq!(
1026            extract_terminal_op(MethodClassification::TerminalSum),
1027            Some(TerminalOp::Sum)
1028        );
1029        assert_eq!(
1030            extract_terminal_op(MethodClassification::TerminalProduct),
1031            Some(TerminalOp::Sum)
1032        );
1033    }
1034
1035    #[test]
1036    fn test_extract_terminal_op_find() {
1037        assert_eq!(
1038            extract_terminal_op(MethodClassification::TerminalFind),
1039            Some(TerminalOp::Find)
1040        );
1041        assert_eq!(
1042            extract_terminal_op(MethodClassification::TerminalPosition),
1043            Some(TerminalOp::Find)
1044        );
1045    }
1046
1047    #[test]
1048    fn test_extract_terminal_op_none() {
1049        assert_eq!(extract_terminal_op(MethodClassification::Map), None);
1050        assert_eq!(
1051            extract_terminal_op(MethodClassification::StandardIterator),
1052            None
1053        );
1054    }
1055
1056    #[test]
1057    fn test_config_profiles() {
1058        let strict = FunctionalAnalysisConfig::strict();
1059        assert_eq!(strict.min_pipeline_depth, 3);
1060        assert_eq!(strict.max_closure_complexity, 3);
1061
1062        let balanced = FunctionalAnalysisConfig::balanced();
1063        assert_eq!(balanced.min_pipeline_depth, 2);
1064        assert_eq!(balanced.max_closure_complexity, 5);
1065
1066        let lenient = FunctionalAnalysisConfig::lenient();
1067        assert_eq!(lenient.min_pipeline_depth, 2);
1068        assert_eq!(lenient.max_closure_complexity, 10);
1069    }
1070
1071    #[test]
1072    fn test_should_analyze() {
1073        let config = FunctionalAnalysisConfig::balanced();
1074        assert!(!config.should_analyze(2)); // Below threshold
1075        assert!(config.should_analyze(3)); // At threshold
1076        assert!(config.should_analyze(10)); // Above threshold
1077    }
1078
1079    #[test]
1080    fn test_detect_simple_iterator_chain() {
1081        let function: ItemFn = parse_quote! {
1082            fn process_items(items: Vec<i32>) -> Vec<i32> {
1083                items.iter()
1084                    .map(|x| x * 2)
1085                    .filter(|x| x > &10)
1086                    .collect()
1087            }
1088        };
1089
1090        let config = FunctionalAnalysisConfig::balanced();
1091        let pipelines = detect_pipelines(&function, &config);
1092
1093        assert_eq!(pipelines.len(), 1);
1094        // Depth is 3: iter, map, filter (collect is terminal, not a stage)
1095        assert_eq!(pipelines[0].depth, 3);
1096        assert!(!pipelines[0].is_parallel);
1097        assert_eq!(pipelines[0].terminal_operation, Some(TerminalOp::Collect));
1098    }
1099
1100    #[test]
1101    fn test_purity_analysis_pure_function() {
1102        let function: ItemFn = parse_quote! {
1103            fn pure_calculation(x: i32, y: i32) -> i32 {
1104                let sum = x + y;
1105                let product = x * y;
1106                sum + product
1107            }
1108        };
1109
1110        let config = FunctionalAnalysisConfig::balanced();
1111        let metrics = analyze_purity(&function, &config);
1112
1113        assert!(!metrics.has_mutable_state);
1114        assert_eq!(metrics.immutability_ratio, 1.0);
1115        assert!(metrics.score > 0.9);
1116        assert_eq!(metrics.side_effect_kind, SideEffectKind::Pure);
1117    }
1118
1119    #[test]
1120    fn test_purity_analysis_impure_function() {
1121        let function: ItemFn = parse_quote! {
1122            fn impure_function(x: i32) -> i32 {
1123                let mut counter = 0;
1124                counter += x;
1125                println!("Counter: {}", counter);
1126                counter
1127            }
1128        };
1129
1130        let config = FunctionalAnalysisConfig::balanced();
1131        let metrics = analyze_purity(&function, &config);
1132
1133        assert!(metrics.has_mutable_state);
1134        // Note: println! detection is simplified and may not always detect I/O
1135        // The score reflects mutable state penalty of ~0.3, giving us 0.7
1136        assert!(metrics.score < 0.8);
1137    }
1138
1139    #[test]
1140    fn test_composition_scoring_high_quality() {
1141        let pipeline = Pipeline {
1142            stages: vec![
1143                PipelineStage::Iterator {
1144                    method: "iter".to_string(),
1145                },
1146                PipelineStage::Map {
1147                    closure_complexity: 2,
1148                    has_nested_pipeline: false,
1149                },
1150                PipelineStage::Filter {
1151                    closure_complexity: 1,
1152                    has_nested_pipeline: false,
1153                },
1154            ],
1155            depth: 3,
1156            is_parallel: false,
1157            terminal_operation: Some(TerminalOp::Collect),
1158            nesting_level: 0,
1159            builder_pattern: false,
1160        };
1161
1162        let purity = PurityMetrics {
1163            has_mutable_state: false,
1164            has_side_effects: false,
1165            immutability_ratio: 1.0,
1166            is_const_fn: false,
1167            score: 1.0,
1168            side_effect_kind: SideEffectKind::Pure,
1169        };
1170
1171        let config = FunctionalAnalysisConfig::balanced();
1172        let quality = score_composition(&[pipeline], &purity, &config);
1173
1174        assert!(quality > 0.7);
1175    }
1176
1177    #[test]
1178    fn test_parallel_bonus() {
1179        let shallow_parallel = Pipeline {
1180            stages: vec![PipelineStage::Iterator {
1181                method: "par_iter".to_string(),
1182            }],
1183            depth: 2,
1184            is_parallel: true,
1185            terminal_operation: None,
1186            nesting_level: 0,
1187            builder_pattern: false,
1188        };
1189        assert_eq!(calculate_parallel_bonus(&shallow_parallel), 0.0);
1190
1191        let deep_parallel = Pipeline {
1192            stages: vec![PipelineStage::Iterator {
1193                method: "par_iter".to_string(),
1194            }],
1195            depth: 4,
1196            is_parallel: true,
1197            terminal_operation: None,
1198            nesting_level: 0,
1199            builder_pattern: false,
1200        };
1201        assert_eq!(calculate_parallel_bonus(&deep_parallel), 0.2);
1202    }
1203}