use crate::analysis::io_detection::{IoDetector, IoProfile, Language, SideEffect};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PurityLevel {
StrictlyPure,
LocallyPure,
ReadOnly,
Impure,
}
impl PurityLevel {
pub fn as_str(&self) -> &'static str {
match self {
PurityLevel::StrictlyPure => "Strictly Pure",
PurityLevel::LocallyPure => "Locally Pure",
PurityLevel::ReadOnly => "Read-Only",
PurityLevel::Impure => "Impure",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PurityViolation {
IoOperation {
description: String,
line: Option<usize>,
},
StateMutation { target: String, line: Option<usize> },
NonDeterministic {
operation: String,
line: Option<usize>,
},
ImpureCall { callee: String, line: Option<usize> },
}
impl PurityViolation {
pub fn description(&self) -> String {
match self {
PurityViolation::IoOperation { description, .. } => {
format!("I/O operation: {}", description)
}
PurityViolation::StateMutation { target, .. } => {
format!("State mutation: {}", target)
}
PurityViolation::NonDeterministic { operation, .. } => {
format!("Non-deterministic operation: {}", operation)
}
PurityViolation::ImpureCall { callee, .. } => {
format!("Calls impure function: {}", callee)
}
}
}
pub fn line(&self) -> Option<usize> {
match self {
PurityViolation::IoOperation { line, .. }
| PurityViolation::StateMutation { line, .. }
| PurityViolation::NonDeterministic { line, .. }
| PurityViolation::ImpureCall { line, .. } => *line,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RefactoringType {
ExtractPureCore,
SeparateIoFromLogic,
ParameterizeNonDeterminism,
IsolateSingleViolation,
}
impl RefactoringType {
pub fn as_str(&self) -> &'static str {
match self {
RefactoringType::ExtractPureCore => "Extract Pure Core",
RefactoringType::SeparateIoFromLogic => "Separate I/O from Logic",
RefactoringType::ParameterizeNonDeterminism => "Parameterize Non-Determinism",
RefactoringType::IsolateSingleViolation => "Isolate Single Violation",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EffortLevel {
Low,
Medium,
High,
}
impl EffortLevel {
pub fn as_str(&self) -> &'static str {
match self {
EffortLevel::Low => "Low",
EffortLevel::Medium => "Medium",
EffortLevel::High => "High",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurityRefactoringOpportunity {
pub opportunity_type: RefactoringType,
pub description: String,
pub estimated_effort: EffortLevel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurityAnalysis {
pub purity: PurityLevel,
pub violations: Vec<PurityViolation>,
pub is_deterministic: bool,
pub can_be_pure: bool,
pub refactoring_opportunity: Option<PurityRefactoringOpportunity>,
}
impl PurityAnalysis {
pub fn strictly_pure() -> Self {
Self {
purity: PurityLevel::StrictlyPure,
violations: Vec::new(),
is_deterministic: true,
can_be_pure: false,
refactoring_opportunity: None,
}
}
pub fn impure(violations: Vec<PurityViolation>) -> Self {
let is_deterministic = !violations
.iter()
.any(|v| matches!(v, PurityViolation::NonDeterministic { .. }));
Self {
purity: PurityLevel::Impure,
violations,
is_deterministic,
can_be_pure: false,
refactoring_opportunity: None,
}
}
}
pub struct PurityAnalyzer {
io_detector: IoDetector,
non_determinism_patterns: HashMap<Language, Vec<String>>,
}
impl PurityAnalyzer {
pub fn new() -> Self {
Self {
io_detector: IoDetector::new(),
non_determinism_patterns: Self::build_non_determinism_patterns(),
}
}
pub fn analyze_code(&self, code: &str, language: Language) -> PurityAnalysis {
let io_profile = self.io_detector.detect_io(code, language);
let mut violations = Vec::new();
violations.extend(self.analyze_io_operations(&io_profile, code, language));
violations.extend(self.analyze_side_effects(&io_profile, code));
violations.extend(self.detect_non_determinism(code, language));
let purity = self.classify_purity(&violations, &io_profile, code, language);
let is_deterministic = !violations
.iter()
.any(|v| matches!(v, PurityViolation::NonDeterministic { .. }));
let can_be_pure = self.can_be_made_pure(&violations);
let refactoring_opportunity = self.suggest_refactoring(&violations);
PurityAnalysis {
purity,
violations,
is_deterministic,
can_be_pure,
refactoring_opportunity,
}
}
fn analyze_io_operations(
&self,
profile: &IoProfile,
_code: &str,
_language: Language,
) -> Vec<PurityViolation> {
let mut violations = Vec::new();
for _ in &profile.file_operations {
violations.push(PurityViolation::IoOperation {
description: "File I/O operation".to_string(),
line: None,
});
}
for _ in &profile.network_operations {
violations.push(PurityViolation::IoOperation {
description: "Network I/O operation".to_string(),
line: None,
});
}
for _ in &profile.console_operations {
violations.push(PurityViolation::IoOperation {
description: "Console I/O operation".to_string(),
line: None,
});
}
for _ in &profile.database_operations {
violations.push(PurityViolation::IoOperation {
description: "Database I/O operation".to_string(),
line: None,
});
}
for _ in &profile.environment_operations {
violations.push(PurityViolation::IoOperation {
description: "Environment variable access".to_string(),
line: None,
});
}
violations
}
fn analyze_side_effects(&self, profile: &IoProfile, code: &str) -> Vec<PurityViolation> {
let mut violations = Vec::new();
for side_effect in &profile.side_effects {
if !self.is_local_mutation(side_effect, code) {
match side_effect {
SideEffect::FieldMutation { target, field } => {
violations.push(PurityViolation::StateMutation {
target: format!("{}.{}", target, field),
line: None,
});
}
SideEffect::GlobalMutation { name } => {
violations.push(PurityViolation::StateMutation {
target: name.clone(),
line: None,
});
}
SideEffect::CollectionMutation { .. } => {
}
SideEffect::ExternalState { description } => {
violations.push(PurityViolation::StateMutation {
target: description.clone(),
line: None,
});
}
}
}
}
violations
}
fn is_local_mutation(&self, side_effect: &SideEffect, code: &str) -> bool {
match side_effect {
SideEffect::FieldMutation { target, .. } => {
target == "unknown" || !code.contains("self.")
}
SideEffect::GlobalMutation { .. } => false, SideEffect::CollectionMutation { .. } => {
true
}
SideEffect::ExternalState { .. } => false,
}
}
fn detect_non_determinism(&self, code: &str, language: Language) -> Vec<PurityViolation> {
let mut violations = Vec::new();
if let Some(patterns) = self.non_determinism_patterns.get(&language) {
for pattern in patterns {
if code.contains(pattern) {
violations.push(PurityViolation::NonDeterministic {
operation: pattern.clone(),
line: None,
});
}
}
}
violations
}
fn classify_purity(
&self,
violations: &[PurityViolation],
profile: &IoProfile,
code: &str,
language: Language,
) -> PurityLevel {
if violations.is_empty() {
return PurityLevel::StrictlyPure;
}
let only_local_mutations = violations
.iter()
.all(|v| matches!(v, PurityViolation::StateMutation { .. }))
&& !violations.is_empty();
if only_local_mutations {
return PurityLevel::LocallyPure;
}
let only_reads = self.only_has_read_operations(profile, code, language);
if only_reads
&& !violations
.iter()
.any(|v| matches!(v, PurityViolation::StateMutation { .. }))
{
return PurityLevel::ReadOnly;
}
PurityLevel::Impure
}
fn only_has_read_operations(
&self,
profile: &IoProfile,
code: &str,
language: Language,
) -> bool {
let has_file_ops = !profile.file_operations.is_empty();
let has_network = !profile.network_operations.is_empty();
let has_console = !profile.console_operations.is_empty();
let has_db = !profile.database_operations.is_empty();
let has_mutations = !profile.side_effects.is_empty();
if has_network || has_console || has_db || has_mutations {
return false;
}
if has_file_ops && self.has_write_operations(code, language) {
return false;
}
has_file_ops
}
fn has_write_operations(&self, code: &str, language: Language) -> bool {
match language {
Language::Rust => {
code.contains("::write")
|| code.contains("File::create")
|| code.contains("OpenOptions")
|| code.contains("write_all")
}
Language::Python => {
code.contains("write_text")
|| code.contains("write_bytes")
|| code.contains("open(") && code.contains("'w'")
|| code.contains("open(") && code.contains("\"w\"")
}
Language::JavaScript | Language::TypeScript => {
code.contains("writeFile")
|| code.contains("createWriteStream")
|| code.contains("appendFile")
}
}
}
fn can_be_made_pure(&self, violations: &[PurityViolation]) -> bool {
if violations.len() == 1 {
return true;
}
let all_io = violations
.iter()
.all(|v| matches!(v, PurityViolation::IoOperation { .. }));
if all_io && violations.len() <= 3 {
return true;
}
false
}
fn suggest_refactoring(
&self,
violations: &[PurityViolation],
) -> Option<PurityRefactoringOpportunity> {
if violations.len() == 1 {
let description = format!(
"Function has single purity violation: {}. Extract to make core logic pure.",
violations[0].description()
);
return Some(PurityRefactoringOpportunity {
opportunity_type: RefactoringType::IsolateSingleViolation,
description,
estimated_effort: EffortLevel::Low,
});
}
let all_io = violations
.iter()
.all(|v| matches!(v, PurityViolation::IoOperation { .. }));
if all_io {
return Some(PurityRefactoringOpportunity {
opportunity_type: RefactoringType::SeparateIoFromLogic,
description: "Separate I/O operations from business logic. Make computation pure."
.to_string(),
estimated_effort: EffortLevel::Medium,
});
}
let has_non_determinism = violations
.iter()
.any(|v| matches!(v, PurityViolation::NonDeterministic { .. }));
if has_non_determinism {
return Some(PurityRefactoringOpportunity {
opportunity_type: RefactoringType::ParameterizeNonDeterminism,
description: "Replace non-deterministic operations (time, random) with parameters for testability.".to_string(),
estimated_effort: EffortLevel::Low,
});
}
None
}
fn build_non_determinism_patterns() -> HashMap<Language, Vec<String>> {
let mut patterns = HashMap::new();
patterns.insert(
Language::Rust,
vec![
"std::time::Instant::now".to_string(),
"std::time::SystemTime::now".to_string(),
"Instant::now".to_string(),
"SystemTime::now".to_string(),
"rand::".to_string(),
"thread_rng".to_string(),
"uuid::Uuid::new_v4".to_string(),
"Uuid::new_v4".to_string(),
"HashMap::new".to_string(), "HashSet::new".to_string(), ],
);
patterns.insert(
Language::Python,
vec![
"random.".to_string(),
"datetime.now".to_string(),
"time.time".to_string(),
"uuid.uuid4".to_string(),
"time.monotonic".to_string(),
],
);
patterns.insert(
Language::JavaScript,
vec![
"Math.random".to_string(),
"Date.now".to_string(),
"new Date()".to_string(),
"crypto.randomUUID".to_string(),
"performance.now".to_string(),
],
);
patterns.insert(
Language::TypeScript,
patterns[&Language::JavaScript].clone(),
);
patterns
}
}
impl Default for PurityAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strictly_pure_function() {
let code = r#"
fn add(a: i32, b: i32) -> i32 {
a + b
}
"#;
let analyzer = PurityAnalyzer::new();
let analysis = analyzer.analyze_code(code, Language::Rust);
assert_eq!(analysis.purity, PurityLevel::StrictlyPure);
assert!(analysis.violations.is_empty());
assert!(analysis.is_deterministic);
}
#[test]
fn read_only_function() {
let code = r#"
fn read_config() -> String {
std::fs::read_to_string("config.toml").unwrap()
}
"#;
let analyzer = PurityAnalyzer::new();
let analysis = analyzer.analyze_code(code, Language::Rust);
assert_eq!(analysis.purity, PurityLevel::ReadOnly);
assert!(!analysis.violations.is_empty());
assert!(analysis
.violations
.iter()
.any(|v| { matches!(v, PurityViolation::IoOperation { .. }) }));
}
#[test]
fn impure_function() {
let code = r#"
fn save_data(data: &str) {
std::fs::write("output.txt", data).unwrap();
}
"#;
let analyzer = PurityAnalyzer::new();
let analysis = analyzer.analyze_code(code, Language::Rust);
assert_eq!(analysis.purity, PurityLevel::Impure);
assert!(!analysis.violations.is_empty());
}
#[test]
fn non_deterministic_detection() {
let code = r#"
fn generate_id() -> String {
uuid::Uuid::new_v4().to_string()
}
"#;
let analyzer = PurityAnalyzer::new();
let analysis = analyzer.analyze_code(code, Language::Rust);
assert!(!analysis.is_deterministic);
assert!(analysis
.violations
.iter()
.any(|v| { matches!(v, PurityViolation::NonDeterministic { .. }) }));
}
#[test]
fn almost_pure_refactoring_opportunity() {
let code = r#"
fn calculate_with_logging(a: i32, b: i32) -> i32 {
let result = a * b + a / b;
println!("Result: {}", result);
result
}
"#;
let analyzer = PurityAnalyzer::new();
let analysis = analyzer.analyze_code(code, Language::Rust);
assert!(analysis.can_be_pure);
assert!(analysis.refactoring_opportunity.is_some());
if let Some(opportunity) = &analysis.refactoring_opportunity {
assert!(matches!(
opportunity.opportunity_type,
RefactoringType::IsolateSingleViolation
));
}
}
#[test]
fn python_non_deterministic() {
let code = r#"
def generate_timestamp():
return datetime.now()
"#;
let analyzer = PurityAnalyzer::new();
let analysis = analyzer.analyze_code(code, Language::Python);
assert!(!analysis.is_deterministic);
assert!(analysis
.violations
.iter()
.any(|v| { matches!(v, PurityViolation::NonDeterministic { .. }) }));
}
#[test]
fn javascript_random() {
let code = r#"
function randomNumber() {
return Math.random();
}
"#;
let analyzer = PurityAnalyzer::new();
let analysis = analyzer.analyze_code(code, Language::JavaScript);
assert!(!analysis.is_deterministic);
assert!(analysis
.violations
.iter()
.any(|v| { matches!(v, PurityViolation::NonDeterministic { .. }) }));
}
#[test]
fn separate_io_refactoring() {
let code = r#"
fn process_file(path: &str) -> Result<i32, Error> {
let content = std::fs::read_to_string(path)?;
let data = parse_content(&content);
let result = calculate(&data);
std::fs::write("output.txt", &result.to_string())?;
Ok(result)
}
"#;
let analyzer = PurityAnalyzer::new();
let analysis = analyzer.analyze_code(code, Language::Rust);
assert_eq!(analysis.purity, PurityLevel::Impure);
if let Some(opportunity) = &analysis.refactoring_opportunity {
assert!(matches!(
opportunity.opportunity_type,
RefactoringType::SeparateIoFromLogic
));
}
}
}