Skip to main content

tldr_cli/commands/patterns/
types.rs

1//! Shared types for Pattern Analysis commands
2//!
3//! This module defines all data types used across the patterns analysis
4//! commands. Types are designed for JSON serialization with serde.
5//!
6//! # Commands Using These Types
7//!
8//! - `cohesion`: LCOM4 class cohesion analysis
9//! - `coupling`: Cross-module coupling analysis
10//! - `interface`: Public API extraction
11//! - `purity`: Function purity/effect analysis
12//! - `temporal`: Temporal constraint mining
13//! - `behavioral`: Pre/postcondition extraction
14//! - `mutability`: Variable/parameter mutation tracking
15//! - `resources`: Resource lifecycle analysis
16
17use clap::ValueEnum;
18use serde::{Deserialize, Serialize};
19
20// =============================================================================
21// Confidence Level
22// =============================================================================
23
24/// Confidence level for inferred patterns and analysis results.
25///
26/// # Serialization
27///
28/// Serializes to snake_case: `"high"`, `"medium"`, `"low"`
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
30#[serde(rename_all = "snake_case")]
31pub enum Confidence {
32    /// High confidence - direct code evidence
33    High,
34    /// Medium confidence - inferred from patterns
35    #[default]
36    Medium,
37    /// Low confidence - heuristic or partial evidence
38    Low,
39}
40
41impl std::fmt::Display for Confidence {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::High => write!(f, "high"),
45            Self::Medium => write!(f, "medium"),
46            Self::Low => write!(f, "low"),
47        }
48    }
49}
50
51// =============================================================================
52// Docstring Style
53// =============================================================================
54
55/// Documentation style detected in source code.
56///
57/// Used by behavioral analysis to parse docstrings correctly.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
59#[serde(rename_all = "snake_case")]
60pub enum DocstringStyle {
61    /// Google-style docstrings (Args:, Returns:, Raises:)
62    Google,
63    /// NumPy-style docstrings (Parameters, Returns sections)
64    Numpy,
65    /// Sphinx/reST style docstrings (:param:, :returns:, :raises:)
66    Sphinx,
67    /// Plain docstrings without structured sections
68    #[default]
69    Plain,
70}
71
72impl std::fmt::Display for DocstringStyle {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            Self::Google => write!(f, "google"),
76            Self::Numpy => write!(f, "numpy"),
77            Self::Sphinx => write!(f, "sphinx"),
78            Self::Plain => write!(f, "plain"),
79        }
80    }
81}
82
83// =============================================================================
84// Effect Type
85// =============================================================================
86
87/// Type of side effect detected in code.
88///
89/// Used by purity and behavioral analysis.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum EffectType {
93    /// I/O operations (file, network, console)
94    Io,
95    /// Writing to global variables
96    GlobalWrite,
97    /// Writing to object attributes (self.x = ...)
98    AttributeWrite,
99    /// Modifying collections in place (list.append, dict.update)
100    CollectionModify,
101    /// Calling functions with potential side effects
102    Call,
103    /// Calling unknown/unresolved functions (purity cannot be determined)
104    UnknownCall,
105}
106
107impl std::fmt::Display for EffectType {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::Io => write!(f, "io"),
111            Self::GlobalWrite => write!(f, "global_write"),
112            Self::AttributeWrite => write!(f, "attribute_write"),
113            Self::CollectionModify => write!(f, "collection_modify"),
114            Self::Call => write!(f, "call"),
115            Self::UnknownCall => write!(f, "unknown_call"),
116        }
117    }
118}
119
120// =============================================================================
121// Condition Source
122// =============================================================================
123
124/// Source of a pre/postcondition constraint.
125///
126/// Tracks where a constraint was extracted from.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub enum ConditionSource {
130    /// Guard clause (if x < 0: raise ValueError)
131    Guard,
132    /// Docstring description
133    Docstring,
134    /// Type hint annotation
135    TypeHint,
136    /// Assert statement
137    Assertion,
138    /// icontract decorator (@require, @ensure)
139    Icontract,
140    /// deal library decorator
141    Deal,
142}
143
144impl std::fmt::Display for ConditionSource {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        match self {
147            Self::Guard => write!(f, "guard"),
148            Self::Docstring => write!(f, "docstring"),
149            Self::TypeHint => write!(f, "type_hint"),
150            Self::Assertion => write!(f, "assertion"),
151            Self::Icontract => write!(f, "icontract"),
152            Self::Deal => write!(f, "deal"),
153        }
154    }
155}
156
157// =============================================================================
158// Cohesion Types
159// =============================================================================
160
161/// Verdict for cohesion analysis.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
163#[serde(rename_all = "snake_case")]
164pub enum CohesionVerdict {
165    /// Class is cohesive (LCOM4 = 1)
166    Cohesive,
167    /// Class could be split (LCOM4 > 1)
168    SplitCandidate,
169}
170
171impl CohesionVerdict {
172    /// Determine verdict from LCOM4 value.
173    pub fn from_lcom4(lcom4: u32) -> Self {
174        if lcom4 <= 1 {
175            Self::Cohesive
176        } else {
177            Self::SplitCandidate
178        }
179    }
180}
181
182impl std::fmt::Display for CohesionVerdict {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        match self {
185            Self::Cohesive => write!(f, "cohesive"),
186            Self::SplitCandidate => write!(f, "split_candidate"),
187        }
188    }
189}
190
191/// Information about a connected component in LCOM4 analysis.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct ComponentInfo {
194    /// Methods in this component
195    pub methods: Vec<String>,
196    /// Fields accessed by this component
197    pub fields: Vec<String>,
198}
199
200/// Cohesion analysis result for a single class.
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202pub struct ClassCohesion {
203    /// Class name
204    pub class_name: String,
205    /// File path where class is defined
206    pub file_path: String,
207    /// Line number of class definition
208    pub line: u32,
209    /// LCOM4 value (1 = cohesive, >1 = split candidate)
210    pub lcom4: u32,
211    /// Number of methods analyzed
212    pub method_count: u32,
213    /// Number of fields detected
214    pub field_count: u32,
215    /// Verdict based on LCOM4 value
216    pub verdict: CohesionVerdict,
217    /// Suggestion for splitting if LCOM4 > 1
218    pub split_suggestion: Option<String>,
219    /// Connected components (if LCOM4 > 1)
220    pub components: Vec<ComponentInfo>,
221}
222
223/// Summary of cohesion analysis across multiple classes.
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225pub struct CohesionSummary {
226    /// Total classes analyzed
227    pub total_classes: u32,
228    /// Number of cohesive classes
229    pub cohesive: u32,
230    /// Number of split candidates
231    pub split_candidates: u32,
232    /// Average LCOM4 value
233    pub avg_lcom4: f64,
234}
235
236impl Default for CohesionSummary {
237    fn default() -> Self {
238        Self {
239            total_classes: 0,
240            cohesive: 0,
241            split_candidates: 0,
242            avg_lcom4: 0.0,
243        }
244    }
245}
246
247/// Full report from cohesion analysis.
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249pub struct CohesionReport {
250    /// Cohesion results per class
251    pub classes: Vec<ClassCohesion>,
252    /// Summary statistics
253    pub summary: CohesionSummary,
254}
255
256// =============================================================================
257// Coupling Types
258// =============================================================================
259
260/// Coupling verdict based on score.
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
262#[serde(rename_all = "snake_case")]
263pub enum CouplingVerdict {
264    /// Low coupling (0.0-0.2)
265    Low,
266    /// Moderate coupling (0.2-0.4)
267    Moderate,
268    /// High coupling (0.4-0.6)
269    High,
270    /// Very high coupling (0.6-1.0)
271    VeryHigh,
272}
273
274impl CouplingVerdict {
275    /// Determine verdict from coupling score.
276    pub fn from_score(score: f64) -> Self {
277        if score < 0.2 {
278            Self::Low
279        } else if score < 0.4 {
280            Self::Moderate
281        } else if score < 0.6 {
282            Self::High
283        } else {
284            Self::VeryHigh
285        }
286    }
287}
288
289impl std::fmt::Display for CouplingVerdict {
290    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291        match self {
292            Self::Low => write!(f, "low"),
293            Self::Moderate => write!(f, "moderate"),
294            Self::High => write!(f, "high"),
295            Self::VeryHigh => write!(f, "very_high"),
296        }
297    }
298}
299
300/// A single cross-module function call.
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302pub struct CrossCall {
303    /// Function making the call
304    pub caller: String,
305    /// Function being called
306    pub callee: String,
307    /// Line number of the call
308    pub line: u32,
309}
310
311/// Calls from one module to another.
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
313pub struct CrossCalls {
314    /// Individual call sites
315    pub calls: Vec<CrossCall>,
316    /// Total count of calls
317    pub count: u32,
318}
319
320/// Coupling analysis between two modules.
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
322pub struct CouplingReport {
323    /// Path to first module
324    pub path_a: String,
325    /// Path to second module
326    pub path_b: String,
327    /// Calls from A to B
328    pub a_to_b: CrossCalls,
329    /// Calls from B to A
330    pub b_to_a: CrossCalls,
331    /// Total cross-module calls
332    pub total_calls: u32,
333    /// Coupling score (0.0-1.0)
334    pub coupling_score: f64,
335    /// Verdict based on score
336    pub verdict: CouplingVerdict,
337}
338
339// =============================================================================
340// Purity Types
341// =============================================================================
342
343/// Purity analysis result for a single function.
344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
345pub struct FunctionPurity {
346    /// Function name
347    pub name: String,
348    /// Purity classification: "pure", "impure", or "unknown"
349    pub classification: String,
350    /// List of detected effects (empty if pure)
351    pub effects: Vec<String>,
352    /// Confidence level of the analysis
353    pub confidence: Confidence,
354}
355
356/// Purity report for a single file.
357#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
358pub struct FilePurityReport {
359    /// Source file path
360    pub source_file: String,
361    /// Purity results per function
362    pub functions: Vec<FunctionPurity>,
363    /// Count of pure functions
364    pub pure_count: u32,
365}
366
367/// Full purity report (may include multiple files for directory analysis).
368#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
369pub struct PurityReport {
370    /// Per-file reports
371    pub files: Vec<FilePurityReport>,
372    /// Total functions analyzed
373    pub total_functions: u32,
374    /// Total pure functions
375    pub total_pure: u32,
376}
377
378// =============================================================================
379// Temporal Types
380// =============================================================================
381
382/// Example location for a temporal constraint.
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
384pub struct TemporalExample {
385    /// File where the pattern was observed
386    pub file: String,
387    /// Line number
388    pub line: u32,
389}
390
391/// A temporal constraint (before -> after sequence).
392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
393pub struct TemporalConstraint {
394    /// Method that must come first
395    pub before: String,
396    /// Method that must come after
397    pub after: String,
398    /// Number of times this pattern was observed
399    pub support: u32,
400    /// Confidence (support / total sequences containing 'before')
401    pub confidence: f64,
402    /// Example locations where this pattern appears
403    pub examples: Vec<TemporalExample>,
404}
405
406/// A trigram (3-method sequence).
407#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
408pub struct Trigram {
409    /// The 3-method sequence
410    pub sequence: [String; 3],
411    /// Number of observations
412    pub support: u32,
413    /// Confidence score
414    pub confidence: f64,
415}
416
417/// Metadata about temporal mining.
418#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
419pub struct TemporalMetadata {
420    /// Number of files analyzed
421    pub files_analyzed: u32,
422    /// Total sequences extracted
423    pub sequences_extracted: u32,
424    /// Minimum support threshold used
425    pub min_support: u32,
426    /// Minimum confidence threshold used
427    pub min_confidence: f64,
428}
429
430impl Default for TemporalMetadata {
431    fn default() -> Self {
432        Self {
433            files_analyzed: 0,
434            sequences_extracted: 0,
435            min_support: 2,
436            min_confidence: 0.5,
437        }
438    }
439}
440
441/// Full temporal constraint report.
442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
443pub struct TemporalReport {
444    /// Discovered temporal constraints
445    pub constraints: Vec<TemporalConstraint>,
446    /// Discovered trigrams (if requested)
447    pub trigrams: Vec<Trigram>,
448    /// Analysis metadata
449    pub metadata: TemporalMetadata,
450}
451
452// =============================================================================
453// Interface Types
454// =============================================================================
455
456/// Information about a public function.
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
458pub struct FunctionInfo {
459    /// Function name
460    pub name: String,
461    /// Full signature (e.g., "def foo(x: int, y: str) -> bool")
462    pub signature: String,
463    /// Docstring if present
464    pub docstring: Option<String>,
465    /// Line number of definition
466    pub lineno: u32,
467    /// Whether the function is async
468    pub is_async: bool,
469}
470
471/// Information about a public method within a class.
472#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
473pub struct MethodInfo {
474    /// Method name
475    pub name: String,
476    /// Full signature
477    pub signature: String,
478    /// Whether the method is async
479    pub is_async: bool,
480}
481
482/// Information about a public class.
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484pub struct ClassInfo {
485    /// Class name
486    pub name: String,
487    /// Line number of definition
488    pub lineno: u32,
489    /// Base classes
490    pub bases: Vec<String>,
491    /// Public methods
492    pub methods: Vec<MethodInfo>,
493    /// Count of private methods (for completeness)
494    pub private_method_count: u32,
495}
496
497/// Interface (public API) for a single file.
498#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
499pub struct InterfaceInfo {
500    /// File path
501    pub file: String,
502    /// Public exports of the module.
503    ///
504    /// schema-cleanup-v1 BUG-22: previously `Option<Vec<String>>` and
505    /// emitted as `null` for any file without an explicit Python
506    /// `__all__` declaration. Now always a populated array — when
507    /// `__all__` is defined, its contents are used verbatim;
508    /// otherwise this falls back to the union of public function and
509    /// class names, mirroring what would be exported under "import *"
510    /// semantics. Empty modules return `[]` (empty array), never
511    /// `null`.
512    #[serde(default)]
513    pub all_exports: Vec<String>,
514    /// Public functions
515    pub functions: Vec<FunctionInfo>,
516    /// Public classes
517    pub classes: Vec<ClassInfo>,
518}
519
520// =============================================================================
521// Resource Types
522// =============================================================================
523
524/// Information about a detected resource.
525#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
526pub struct ResourceInfo {
527    /// Variable name holding the resource
528    pub name: String,
529    /// Type of resource (e.g., "file", "socket", "connection")
530    pub resource_type: String,
531    /// Line where resource was created/opened
532    pub line: u32,
533    /// Whether the resource is properly closed
534    pub closed: bool,
535}
536
537/// Information about a potential resource leak.
538#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
539pub struct LeakInfo {
540    /// Resource that may be leaked
541    pub resource: String,
542    /// Line where resource was created
543    pub line: u32,
544    /// Paths to the leak (if --show-paths enabled)
545    pub paths: Option<Vec<String>>,
546}
547
548/// Information about a double-close issue.
549#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550pub struct DoubleCloseInfo {
551    /// Resource being closed twice
552    pub resource: String,
553    /// Line of first close
554    pub first_close: u32,
555    /// Line of second close
556    pub second_close: u32,
557}
558
559/// Information about use-after-close issue.
560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
561pub struct UseAfterCloseInfo {
562    /// Resource being used after close
563    pub resource: String,
564    /// Line where resource was closed
565    pub close_line: u32,
566    /// Line where resource is used after close
567    pub use_line: u32,
568}
569
570/// Suggestion for using context manager.
571#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
572pub struct ContextSuggestion {
573    /// Resource that should use context manager
574    pub resource: String,
575    /// Suggested code pattern
576    pub suggestion: String,
577}
578
579/// LLM-ready constraint from resource analysis.
580#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
581pub struct ResourceConstraint {
582    /// The constraint rule
583    pub rule: String,
584    /// Context where it applies
585    pub context: String,
586    /// Confidence level
587    pub confidence: f64,
588}
589
590/// Summary of resource analysis.
591#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
592pub struct ResourceSummary {
593    /// Total resources detected
594    pub resources_detected: u32,
595    /// Number of leaks found
596    pub leaks_found: u32,
597    /// Number of double-close issues
598    pub double_closes_found: u32,
599    /// Number of use-after-close issues
600    pub use_after_closes_found: u32,
601}
602
603/// Full resource analysis report.
604#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
605pub struct ResourceReport {
606    /// File analyzed
607    pub file: String,
608    /// Language
609    pub language: String,
610    /// Function analyzed (if specific function requested)
611    pub function: Option<String>,
612    /// Detected resources
613    pub resources: Vec<ResourceInfo>,
614    /// Potential leaks
615    pub leaks: Vec<LeakInfo>,
616    /// Double-close issues
617    pub double_closes: Vec<DoubleCloseInfo>,
618    /// Use-after-close issues
619    pub use_after_closes: Vec<UseAfterCloseInfo>,
620    /// Context manager suggestions
621    pub suggestions: Vec<ContextSuggestion>,
622    /// LLM constraints (if --constraints enabled)
623    pub constraints: Vec<ResourceConstraint>,
624    /// Summary statistics
625    pub summary: ResourceSummary,
626    /// Analysis time in milliseconds
627    pub analysis_time_ms: u64,
628}
629
630// =============================================================================
631// Behavioral Types
632// =============================================================================
633
634/// A precondition on a function parameter.
635#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
636pub struct Precondition {
637    /// Parameter name
638    pub param: String,
639    /// Constraint expression (e.g., "x > 0")
640    pub expression: Option<String>,
641    /// Human-readable description from docstring
642    pub description: Option<String>,
643    /// Type hint if present
644    pub type_hint: Option<String>,
645    /// Source of this condition
646    pub source: ConditionSource,
647}
648
649/// A postcondition on function return.
650#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
651pub struct Postcondition {
652    /// Constraint expression
653    pub expression: Option<String>,
654    /// Human-readable description
655    pub description: Option<String>,
656    /// Return type hint
657    pub type_hint: Option<String>,
658}
659
660/// Information about an exception the function may raise.
661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
662pub struct ExceptionInfo {
663    /// Exception type (e.g., "ValueError")
664    pub exception_type: String,
665    /// Description of when it's raised
666    pub description: Option<String>,
667}
668
669/// Information about yield values (for generators).
670#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
671pub struct YieldInfo {
672    /// Type hint for yielded values
673    pub type_hint: Option<String>,
674    /// Description of yielded values
675    pub description: Option<String>,
676}
677
678/// Side effect detected in function.
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
680pub struct SideEffect {
681    /// Type of effect
682    pub effect_type: EffectType,
683    /// Target of the effect (e.g., "self.count", "global_var")
684    pub target: Option<String>,
685}
686
687/// Behavioral analysis for a single function.
688#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
689pub struct FunctionBehavior {
690    /// Function name
691    pub function_name: String,
692    /// File path
693    pub file_path: String,
694    /// Line number of function definition
695    pub line: u32,
696    /// Purity classification: "pure", "impure", or "unknown"
697    pub purity_classification: String,
698    /// Whether it's a generator
699    pub is_generator: bool,
700    /// Whether it's an async function
701    pub is_async: bool,
702    /// Preconditions on parameters
703    pub preconditions: Vec<Precondition>,
704    /// Postconditions on return
705    pub postconditions: Vec<Postcondition>,
706    /// Exceptions that may be raised
707    pub exceptions: Vec<ExceptionInfo>,
708    /// Yield information (if generator)
709    pub yields: Vec<YieldInfo>,
710    /// Detected side effects
711    pub side_effects: Vec<SideEffect>,
712}
713
714/// Class invariant.
715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
716pub struct ClassInvariant {
717    /// Invariant expression
718    pub expression: String,
719}
720
721/// Behavioral analysis for a class.
722#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
723pub struct ClassBehavior {
724    /// Class name
725    pub class_name: String,
726    /// Class invariants
727    pub invariants: Vec<ClassInvariant>,
728    /// Method behaviors
729    pub methods: Vec<FunctionBehavior>,
730}
731
732/// Full behavioral analysis report.
733#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
734pub struct BehavioralReport {
735    /// File analyzed
736    pub file_path: String,
737    /// Detected docstring style
738    pub docstring_style: DocstringStyle,
739    /// Whether icontract library is used
740    pub has_icontract: bool,
741    /// Whether deal library is used
742    pub has_deal: bool,
743    /// Function behaviors
744    pub functions: Vec<FunctionBehavior>,
745    /// Class behaviors
746    pub classes: Vec<ClassBehavior>,
747}
748
749// =============================================================================
750// Mutability Types
751// =============================================================================
752
753/// Mutability information for a variable.
754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
755pub struct VariableMutability {
756    /// Variable name
757    pub name: String,
758    /// Whether the variable is ever reassigned
759    pub mutable: bool,
760    /// Number of reassignments
761    pub reassignments: u32,
762    /// Number of in-place mutations
763    pub mutations: u32,
764}
765
766/// Mutability information for a function parameter.
767#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
768pub struct ParameterMutability {
769    /// Parameter name
770    pub name: String,
771    /// Whether the parameter is mutated
772    pub mutated: bool,
773    /// Lines where mutation occurs
774    pub mutation_sites: Vec<u32>,
775}
776
777/// Collection mutation detected.
778#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
779pub struct CollectionMutation {
780    /// Variable being mutated
781    pub variable: String,
782    /// Operation (e.g., "append", "update", "pop")
783    pub operation: String,
784    /// Line number
785    pub line: u32,
786}
787
788/// Mutability analysis for a function.
789#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
790pub struct FunctionMutability {
791    /// Function name
792    pub name: String,
793    /// Variable mutability info
794    pub variables: Vec<VariableMutability>,
795    /// Parameter mutability info
796    pub parameters: Vec<ParameterMutability>,
797    /// Collection mutations
798    pub collection_mutations: Vec<CollectionMutation>,
799}
800
801/// Field mutability for a class.
802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
803pub struct FieldMutability {
804    /// Field name
805    pub name: String,
806    /// Whether the field is mutable after __init__
807    pub mutable: bool,
808    /// Whether the field is only set in __init__
809    pub init_only: bool,
810}
811
812/// Mutability analysis for a class.
813#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
814pub struct ClassMutability {
815    /// Class name
816    pub name: String,
817    /// Field mutability info
818    pub fields: Vec<FieldMutability>,
819}
820
821/// Summary of mutability analysis.
822#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
823pub struct MutabilitySummary {
824    /// Functions analyzed
825    pub functions_analyzed: u32,
826    /// Classes analyzed
827    pub classes_analyzed: u32,
828    /// Total variables
829    pub total_variables: u32,
830    /// Mutable variables
831    pub mutable_variables: u32,
832    /// Immutable variables
833    pub immutable_variables: u32,
834    /// Percentage of immutable variables
835    pub immutable_pct: f64,
836    /// Parameters analyzed
837    pub parameters_analyzed: u32,
838    /// Mutated parameters
839    pub mutated_parameters: u32,
840    /// Percentage of unmutated parameters
841    pub unmutated_pct: f64,
842    /// Fields analyzed
843    pub fields_analyzed: u32,
844    /// Mutable fields
845    pub mutable_fields: u32,
846    /// Constraints generated (if --constraints)
847    pub constraints_generated: u32,
848}
849
850impl Default for MutabilitySummary {
851    fn default() -> Self {
852        Self {
853            functions_analyzed: 0,
854            classes_analyzed: 0,
855            total_variables: 0,
856            mutable_variables: 0,
857            immutable_variables: 0,
858            immutable_pct: 0.0,
859            parameters_analyzed: 0,
860            mutated_parameters: 0,
861            unmutated_pct: 0.0,
862            fields_analyzed: 0,
863            mutable_fields: 0,
864            constraints_generated: 0,
865        }
866    }
867}
868
869/// Full mutability report.
870#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
871pub struct MutabilityReport {
872    /// File analyzed
873    pub file: String,
874    /// Language
875    pub language: String,
876    /// Function mutability results
877    pub functions: Vec<FunctionMutability>,
878    /// Class mutability results
879    pub classes: Vec<ClassMutability>,
880    /// Summary statistics
881    pub summary: MutabilitySummary,
882    /// Analysis time in milliseconds
883    pub analysis_time_ms: u64,
884}
885
886// =============================================================================
887// Output Format
888// =============================================================================
889
890/// Output format for command results.
891#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
892pub enum OutputFormat {
893    /// JSON output (default)
894    #[default]
895    Json,
896
897    /// Human-readable text output
898    Text,
899}
900
901impl std::fmt::Display for OutputFormat {
902    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
903        match self {
904            Self::Json => write!(f, "json"),
905            Self::Text => write!(f, "text"),
906        }
907    }
908}
909
910// =============================================================================
911// Tests
912// =============================================================================
913
914#[cfg(test)]
915mod tests {
916    use super::*;
917
918    // -------------------------------------------------------------------------
919    // Confidence Tests
920    // -------------------------------------------------------------------------
921
922    #[test]
923    fn test_confidence_enum_serialization() {
924        let json = serde_json::to_string(&Confidence::High).unwrap();
925        assert_eq!(json, r#""high""#);
926
927        let json = serde_json::to_string(&Confidence::Medium).unwrap();
928        assert_eq!(json, r#""medium""#);
929
930        let json = serde_json::to_string(&Confidence::Low).unwrap();
931        assert_eq!(json, r#""low""#);
932    }
933
934    #[test]
935    fn test_confidence_enum_deserialization() {
936        let high: Confidence = serde_json::from_str(r#""high""#).unwrap();
937        assert_eq!(high, Confidence::High);
938
939        let medium: Confidence = serde_json::from_str(r#""medium""#).unwrap();
940        assert_eq!(medium, Confidence::Medium);
941
942        let low: Confidence = serde_json::from_str(r#""low""#).unwrap();
943        assert_eq!(low, Confidence::Low);
944    }
945
946    #[test]
947    fn test_confidence_display() {
948        assert_eq!(Confidence::High.to_string(), "high");
949        assert_eq!(Confidence::Medium.to_string(), "medium");
950        assert_eq!(Confidence::Low.to_string(), "low");
951    }
952
953    #[test]
954    fn test_confidence_default() {
955        assert_eq!(Confidence::default(), Confidence::Medium);
956    }
957
958    // -------------------------------------------------------------------------
959    // DocstringStyle Tests
960    // -------------------------------------------------------------------------
961
962    #[test]
963    fn test_docstring_style_serialization() {
964        let json = serde_json::to_string(&DocstringStyle::Google).unwrap();
965        assert_eq!(json, r#""google""#);
966
967        let json = serde_json::to_string(&DocstringStyle::Numpy).unwrap();
968        assert_eq!(json, r#""numpy""#);
969
970        let json = serde_json::to_string(&DocstringStyle::Sphinx).unwrap();
971        assert_eq!(json, r#""sphinx""#);
972
973        let json = serde_json::to_string(&DocstringStyle::Plain).unwrap();
974        assert_eq!(json, r#""plain""#);
975    }
976
977    #[test]
978    fn test_docstring_style_display() {
979        assert_eq!(DocstringStyle::Google.to_string(), "google");
980        assert_eq!(DocstringStyle::Numpy.to_string(), "numpy");
981        assert_eq!(DocstringStyle::Sphinx.to_string(), "sphinx");
982        assert_eq!(DocstringStyle::Plain.to_string(), "plain");
983    }
984
985    // -------------------------------------------------------------------------
986    // EffectType Tests
987    // -------------------------------------------------------------------------
988
989    #[test]
990    fn test_effect_type_serialization() {
991        let json = serde_json::to_string(&EffectType::Io).unwrap();
992        assert_eq!(json, r#""io""#);
993
994        let json = serde_json::to_string(&EffectType::GlobalWrite).unwrap();
995        assert_eq!(json, r#""global_write""#);
996
997        let json = serde_json::to_string(&EffectType::AttributeWrite).unwrap();
998        assert_eq!(json, r#""attribute_write""#);
999
1000        let json = serde_json::to_string(&EffectType::CollectionModify).unwrap();
1001        assert_eq!(json, r#""collection_modify""#);
1002
1003        let json = serde_json::to_string(&EffectType::Call).unwrap();
1004        assert_eq!(json, r#""call""#);
1005    }
1006
1007    // -------------------------------------------------------------------------
1008    // ConditionSource Tests
1009    // -------------------------------------------------------------------------
1010
1011    #[test]
1012    fn test_condition_source_serialization() {
1013        let json = serde_json::to_string(&ConditionSource::Guard).unwrap();
1014        assert_eq!(json, r#""guard""#);
1015
1016        let json = serde_json::to_string(&ConditionSource::Docstring).unwrap();
1017        assert_eq!(json, r#""docstring""#);
1018
1019        let json = serde_json::to_string(&ConditionSource::TypeHint).unwrap();
1020        assert_eq!(json, r#""type_hint""#);
1021
1022        let json = serde_json::to_string(&ConditionSource::Assertion).unwrap();
1023        assert_eq!(json, r#""assertion""#);
1024
1025        let json = serde_json::to_string(&ConditionSource::Icontract).unwrap();
1026        assert_eq!(json, r#""icontract""#);
1027
1028        let json = serde_json::to_string(&ConditionSource::Deal).unwrap();
1029        assert_eq!(json, r#""deal""#);
1030    }
1031
1032    // -------------------------------------------------------------------------
1033    // CohesionVerdict Tests
1034    // -------------------------------------------------------------------------
1035
1036    #[test]
1037    fn test_cohesion_verdict_from_lcom4() {
1038        assert_eq!(CohesionVerdict::from_lcom4(0), CohesionVerdict::Cohesive);
1039        assert_eq!(CohesionVerdict::from_lcom4(1), CohesionVerdict::Cohesive);
1040        assert_eq!(
1041            CohesionVerdict::from_lcom4(2),
1042            CohesionVerdict::SplitCandidate
1043        );
1044        assert_eq!(
1045            CohesionVerdict::from_lcom4(5),
1046            CohesionVerdict::SplitCandidate
1047        );
1048    }
1049
1050    // -------------------------------------------------------------------------
1051    // CouplingVerdict Tests
1052    // -------------------------------------------------------------------------
1053
1054    #[test]
1055    fn test_coupling_verdict_from_score() {
1056        assert_eq!(CouplingVerdict::from_score(0.0), CouplingVerdict::Low);
1057        assert_eq!(CouplingVerdict::from_score(0.1), CouplingVerdict::Low);
1058        assert_eq!(CouplingVerdict::from_score(0.2), CouplingVerdict::Moderate);
1059        assert_eq!(CouplingVerdict::from_score(0.3), CouplingVerdict::Moderate);
1060        assert_eq!(CouplingVerdict::from_score(0.4), CouplingVerdict::High);
1061        assert_eq!(CouplingVerdict::from_score(0.5), CouplingVerdict::High);
1062        assert_eq!(CouplingVerdict::from_score(0.6), CouplingVerdict::VeryHigh);
1063        assert_eq!(CouplingVerdict::from_score(1.0), CouplingVerdict::VeryHigh);
1064    }
1065
1066    // -------------------------------------------------------------------------
1067    // Report Serialization Tests
1068    // -------------------------------------------------------------------------
1069
1070    #[test]
1071    fn test_class_cohesion_serialization() {
1072        let cohesion = ClassCohesion {
1073            class_name: "MyClass".to_string(),
1074            file_path: "test.py".to_string(),
1075            line: 10,
1076            lcom4: 2,
1077            method_count: 4,
1078            field_count: 3,
1079            verdict: CohesionVerdict::SplitCandidate,
1080            split_suggestion: Some("Consider splitting".to_string()),
1081            components: vec![ComponentInfo {
1082                methods: vec!["method1".to_string()],
1083                fields: vec!["field1".to_string()],
1084            }],
1085        };
1086
1087        let json = serde_json::to_string(&cohesion).unwrap();
1088        let parsed: ClassCohesion = serde_json::from_str(&json).unwrap();
1089        assert_eq!(parsed.class_name, "MyClass");
1090        assert_eq!(parsed.lcom4, 2);
1091    }
1092
1093    #[test]
1094    fn test_coupling_report_serialization() {
1095        let report = CouplingReport {
1096            path_a: "module_a.py".to_string(),
1097            path_b: "module_b.py".to_string(),
1098            a_to_b: CrossCalls {
1099                calls: vec![CrossCall {
1100                    caller: "func_a".to_string(),
1101                    callee: "func_b".to_string(),
1102                    line: 10,
1103                }],
1104                count: 1,
1105            },
1106            b_to_a: CrossCalls::default(),
1107            total_calls: 1,
1108            coupling_score: 0.1,
1109            verdict: CouplingVerdict::Low,
1110        };
1111
1112        let json = serde_json::to_string(&report).unwrap();
1113        let parsed: CouplingReport = serde_json::from_str(&json).unwrap();
1114        assert_eq!(parsed.coupling_score, 0.1);
1115    }
1116
1117    #[test]
1118    fn test_purity_report_serialization() {
1119        let report = PurityReport {
1120            files: vec![FilePurityReport {
1121                source_file: "test.py".to_string(),
1122                functions: vec![FunctionPurity {
1123                    name: "pure_func".to_string(),
1124                    classification: "pure".to_string(),
1125                    effects: vec![],
1126                    confidence: Confidence::High,
1127                }],
1128                pure_count: 1,
1129            }],
1130            total_functions: 1,
1131            total_pure: 1,
1132        };
1133
1134        let json = serde_json::to_string(&report).unwrap();
1135        let parsed: PurityReport = serde_json::from_str(&json).unwrap();
1136        assert_eq!(parsed.total_pure, 1);
1137    }
1138
1139    #[test]
1140    fn test_temporal_report_serialization() {
1141        let report = TemporalReport {
1142            constraints: vec![TemporalConstraint {
1143                before: "open".to_string(),
1144                after: "close".to_string(),
1145                support: 5,
1146                confidence: 0.9,
1147                examples: vec![TemporalExample {
1148                    file: "test.py".to_string(),
1149                    line: 10,
1150                }],
1151            }],
1152            trigrams: vec![],
1153            metadata: TemporalMetadata::default(),
1154        };
1155
1156        let json = serde_json::to_string(&report).unwrap();
1157        let parsed: TemporalReport = serde_json::from_str(&json).unwrap();
1158        assert_eq!(parsed.constraints[0].before, "open");
1159    }
1160
1161    #[test]
1162    fn test_interface_info_serialization() {
1163        let info = InterfaceInfo {
1164            file: "test.py".to_string(),
1165            all_exports: vec!["func1".to_string()],
1166            functions: vec![FunctionInfo {
1167                name: "func1".to_string(),
1168                signature: "def func1(x: int) -> str".to_string(),
1169                docstring: Some("A function".to_string()),
1170                lineno: 5,
1171                is_async: false,
1172            }],
1173            classes: vec![],
1174        };
1175
1176        let json = serde_json::to_string(&info).unwrap();
1177        let parsed: InterfaceInfo = serde_json::from_str(&json).unwrap();
1178        assert_eq!(parsed.functions[0].name, "func1");
1179    }
1180
1181    #[test]
1182    fn test_resource_report_serialization() {
1183        let report = ResourceReport {
1184            file: "test.py".to_string(),
1185            language: "python".to_string(),
1186            function: Some("process".to_string()),
1187            resources: vec![ResourceInfo {
1188                name: "f".to_string(),
1189                resource_type: "file".to_string(),
1190                line: 10,
1191                closed: true,
1192            }],
1193            leaks: vec![],
1194            double_closes: vec![],
1195            use_after_closes: vec![],
1196            suggestions: vec![],
1197            constraints: vec![],
1198            summary: ResourceSummary::default(),
1199            analysis_time_ms: 50,
1200        };
1201
1202        let json = serde_json::to_string(&report).unwrap();
1203        let parsed: ResourceReport = serde_json::from_str(&json).unwrap();
1204        assert_eq!(parsed.resources[0].name, "f");
1205    }
1206
1207    #[test]
1208    fn test_behavioral_report_serialization() {
1209        let report = BehavioralReport {
1210            file_path: "test.py".to_string(),
1211            docstring_style: DocstringStyle::Google,
1212            has_icontract: false,
1213            has_deal: false,
1214            functions: vec![FunctionBehavior {
1215                function_name: "validate".to_string(),
1216                file_path: "test.py".to_string(),
1217                line: 10,
1218                purity_classification: "pure".to_string(),
1219                is_generator: false,
1220                is_async: false,
1221                preconditions: vec![Precondition {
1222                    param: "x".to_string(),
1223                    expression: Some("x > 0".to_string()),
1224                    description: None,
1225                    type_hint: Some("int".to_string()),
1226                    source: ConditionSource::Guard,
1227                }],
1228                postconditions: vec![],
1229                exceptions: vec![],
1230                yields: vec![],
1231                side_effects: vec![],
1232            }],
1233            classes: vec![],
1234        };
1235
1236        let json = serde_json::to_string(&report).unwrap();
1237        let parsed: BehavioralReport = serde_json::from_str(&json).unwrap();
1238        assert_eq!(parsed.functions[0].function_name, "validate");
1239    }
1240
1241    #[test]
1242    fn test_mutability_report_serialization() {
1243        let report = MutabilityReport {
1244            file: "test.py".to_string(),
1245            language: "python".to_string(),
1246            functions: vec![FunctionMutability {
1247                name: "process".to_string(),
1248                variables: vec![VariableMutability {
1249                    name: "count".to_string(),
1250                    mutable: true,
1251                    reassignments: 3,
1252                    mutations: 0,
1253                }],
1254                parameters: vec![],
1255                collection_mutations: vec![],
1256            }],
1257            classes: vec![],
1258            summary: MutabilitySummary::default(),
1259            analysis_time_ms: 30,
1260        };
1261
1262        let json = serde_json::to_string(&report).unwrap();
1263        let parsed: MutabilityReport = serde_json::from_str(&json).unwrap();
1264        assert_eq!(parsed.functions[0].name, "process");
1265    }
1266
1267    // -------------------------------------------------------------------------
1268    // OutputFormat Tests
1269    // -------------------------------------------------------------------------
1270
1271    #[test]
1272    fn test_output_format_display() {
1273        assert_eq!(OutputFormat::Json.to_string(), "json");
1274        assert_eq!(OutputFormat::Text.to_string(), "text");
1275    }
1276
1277    #[test]
1278    fn test_output_format_default() {
1279        assert_eq!(OutputFormat::default(), OutputFormat::Json);
1280    }
1281}