1use serde::{Deserialize, Serialize};
13use syn::{Block, Expr, ExprMethodCall, ItemFn, Local, Stmt};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct FunctionalAnalysisConfig {
18 pub min_pipeline_depth: usize,
20 pub max_closure_complexity: u32,
22 pub purity_threshold: f64,
24 pub composition_quality_threshold: f64,
26 pub min_function_complexity: u32,
28}
29
30impl Default for FunctionalAnalysisConfig {
31 fn default() -> Self {
32 Self::balanced()
33 }
34}
35
36impl FunctionalAnalysisConfig {
37 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 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 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 pub fn should_analyze(&self, complexity: u32) -> bool {
72 complexity >= self.min_function_complexity
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78pub enum PipelineStage {
79 Iterator { method: String },
81 Map {
83 closure_complexity: u32,
84 has_nested_pipeline: bool,
85 },
86 Filter {
88 closure_complexity: u32,
89 has_nested_pipeline: bool,
90 },
91 Fold {
93 init_complexity: u32,
94 fold_complexity: u32,
95 },
96 FlatMap {
98 closure_complexity: u32,
99 has_nested_pipeline: bool,
100 },
101 Inspect { closure_complexity: u32 },
103 AndThen { closure_complexity: u32 },
105 MapErr { closure_complexity: u32 },
107}
108
109#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct Pipeline {
125 pub stages: Vec<PipelineStage>,
127 pub depth: usize,
129 pub is_parallel: bool,
131 pub terminal_operation: Option<TerminalOp>,
133 pub nesting_level: usize,
135 pub builder_pattern: bool,
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub enum SideEffectKind {
142 Pure,
144 Benign,
146 Impure,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152pub struct PurityMetrics {
153 pub has_mutable_state: bool,
155 pub has_side_effects: bool,
157 pub immutability_ratio: f64,
159 pub is_const_fn: bool,
161 pub side_effect_kind: SideEffectKind,
163 pub score: f64,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169pub struct CompositionMetrics {
170 pub pipelines: Vec<Pipeline>,
172 pub purity_score: f64,
174 pub immutability_ratio: f64,
176 pub composition_quality: f64,
178 pub side_effect_kind: SideEffectKind,
180}
181
182#[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 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
209pub fn analyze_composition(
212 function: &ItemFn,
213 config: &FunctionalAnalysisConfig,
214) -> CompositionMetrics {
215 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
239pub 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
247fn collect_pipelines(
249 block: &Block,
250 config: &FunctionalAnalysisConfig,
251 nesting: usize,
252) -> Vec<Pipeline> {
253 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
265fn 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
289fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319enum MethodClassification {
320 ParallelIterator,
322 StandardIterator,
323 IteratorConstructor,
324 SliceIterator,
325 CollectionIterator,
326 StdIterConstructor,
327
328 Map,
330 Filter,
331 Fold,
332 FlatMap,
333 FilterMap,
334 AdapterMethod,
335 SimpleTransform,
336 OrderAdapter,
337
338 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 Unknown,
355}
356
357fn classify_method(method: &str) -> MethodClassification {
359 match method {
360 "par_iter" | "par_iter_mut" | "into_par_iter" | "par_bridge" => {
362 MethodClassification::ParallelIterator
363 }
364 "iter" | "into_iter" | "iter_mut" => MethodClassification::StandardIterator,
366 "lines" | "chars" | "bytes" | "split_whitespace" => {
368 MethodClassification::IteratorConstructor
369 }
370 "windows" | "chunks" | "chunks_exact" | "rchunks" | "split" | "rsplit"
372 | "split_terminator" => MethodClassification::SliceIterator,
373 "into_values" | "into_keys" | "values" | "keys" => MethodClassification::CollectionIterator,
375 "once" | "repeat" | "repeat_with" | "from_fn" | "successors" | "empty" => {
377 MethodClassification::StdIterConstructor
378 }
379 "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 "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 "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
412fn create_stage_from_classification(classification: MethodClassification) -> Option<PipelineStage> {
415 match classification {
416 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 MethodClassification::AdapterMethod
439 | MethodClassification::SimpleTransform
440 | MethodClassification::OrderAdapter => Some(PipelineStage::Map {
441 closure_complexity: 0,
442 has_nested_pipeline: false,
443 }),
444 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 _ => None,
469 }
470}
471
472fn 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 loop {
505 let method_str = current.method.to_string();
506 let classification = classify_method(&method_str);
507
508 if classification == MethodClassification::ParallelIterator {
510 is_parallel = true;
511 }
512
513 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 if is_iterator_constructor {
526 stages.push(PipelineStage::Iterator { method: method_str });
527 }
528
529 if let Some(stage) = create_stage_from_classification(classification) {
531 stages.push(stage);
532 }
533
534 if let Some(terminal) = extract_terminal_op(classification) {
536 terminal_op = Some(terminal);
537 }
538
539 match &*current.receiver {
541 Expr::MethodCall(next) => current = next,
542 _ => break,
543 }
544 }
545
546 stages.reverse();
548
549 if stages.is_empty() {
551 return Vec::new();
552 }
553
554 if !has_iterator_start(&stages) && !has_transformation_stage(&stages) {
557 return Vec::new();
558 }
559
560 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
577fn has_iterator_start(stages: &[PipelineStage]) -> bool {
579 stages
580 .first()
581 .map(|s| matches!(s, PipelineStage::Iterator { .. }))
582 .unwrap_or(false)
583}
584
585fn 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
602fn 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) )
615 }
618
619pub 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
643fn 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
654fn 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(), _ => PurityAccumulator::default(),
661 }
662}
663
664fn analyze_local_purity(local: &Local) -> PurityAccumulator {
666 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
689fn 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
715fn 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
736fn analyze_method_call_purity(method: &ExprMethodCall) -> PurityAccumulator {
738 let method_name = method.method.to_string();
739
740 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
755fn 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
766fn calculate_purity_score(acc: &PurityAccumulator, side_effect_kind: &SideEffectKind) -> f64 {
768 let mut score = 1.0;
769
770 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 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
793pub fn score_composition(
795 pipelines: &[Pipeline],
796 purity: &PurityMetrics,
797 config: &FunctionalAnalysisConfig,
798) -> f64 {
799 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
812fn score_pipelines(pipelines: &[Pipeline], config: &FunctionalAnalysisConfig) -> f64 {
814 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
830fn 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
841fn calculate_parallel_bonus(pipeline: &Pipeline) -> f64 {
843 if pipeline.is_parallel && pipeline.depth >= 3 {
844 0.2 } else {
846 0.0 }
848}
849
850fn 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 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
873fn 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 #[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 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 let stage = create_stage_from_classification(MethodClassification::StandardIterator);
1008 assert_eq!(stage, None);
1009
1010 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)); assert!(config.should_analyze(3)); assert!(config.should_analyze(10)); }
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 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 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}