1use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28
29pub type FunctionId = String;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34pub enum Language {
35 Rust,
36 Python,
37 JavaScript,
38 TypeScript,
39}
40
41pub struct IoDetector {
43 patterns: HashMap<Language, IoPatternSet>,
45}
46
47impl IoDetector {
48 pub fn new() -> Self {
50 let mut patterns = HashMap::new();
51 patterns.insert(Language::Rust, IoPatternSet::for_rust());
52 patterns.insert(Language::Python, IoPatternSet::for_python());
53 patterns.insert(Language::JavaScript, IoPatternSet::for_javascript());
54 patterns.insert(Language::TypeScript, IoPatternSet::for_typescript());
55
56 Self { patterns }
57 }
58
59 pub fn detect_io(&self, code: &str, language: Language) -> IoProfile {
61 let pattern_set = self
62 .patterns
63 .get(&language)
64 .expect("Language not supported");
65
66 let mut profile = IoProfile::new();
67
68 for pattern in &pattern_set.file_ops {
70 if code.contains(pattern) {
71 profile
72 .file_operations
73 .push(IoOperation::FileRead { path_expr: None });
74 }
75 }
76
77 for pattern in &pattern_set.network_ops {
79 if code.contains(pattern) {
80 profile
81 .network_operations
82 .push(IoOperation::NetworkRequest { endpoint: None });
83 }
84 }
85
86 for pattern in &pattern_set.console_ops {
88 if code.contains(pattern) {
89 profile.console_operations.push(IoOperation::ConsoleOutput {
90 stream: OutputStream::Stdout,
91 });
92 }
93 }
94
95 for pattern in &pattern_set.db_ops {
97 if code.contains(pattern) {
98 profile
99 .database_operations
100 .push(IoOperation::DatabaseQuery {
101 query_type: QueryType::Select,
102 });
103 }
104 }
105
106 for pattern in &pattern_set.env_ops {
108 if code.contains(pattern) {
109 profile
110 .environment_operations
111 .push(IoOperation::EnvironmentAccess { var_name: None });
112 }
113 }
114
115 for pattern in &pattern_set.field_mutations {
117 if code.contains(pattern) {
118 profile.side_effects.push(SideEffect::FieldMutation {
119 target: "unknown".to_string(),
120 field: "unknown".to_string(),
121 });
122 }
123 }
124
125 for pattern in &pattern_set.global_mutations {
127 if code.contains(pattern) {
128 profile.side_effects.push(SideEffect::GlobalMutation {
129 name: "unknown".to_string(),
130 });
131 }
132 }
133
134 for pattern in &pattern_set.collection_mutations {
136 if code.contains(pattern) {
137 let op = Self::classify_collection_op(pattern);
138 profile
139 .side_effects
140 .push(SideEffect::CollectionMutation { operation: op });
141 }
142 }
143
144 profile.is_pure = profile.file_operations.is_empty()
146 && profile.network_operations.is_empty()
147 && profile.console_operations.is_empty()
148 && profile.database_operations.is_empty()
149 && profile.environment_operations.is_empty()
150 && profile.side_effects.is_empty();
151
152 profile
153 }
154
155 fn classify_collection_op(pattern: &str) -> CollectionOp {
157 if pattern.contains("push") {
158 CollectionOp::Push
159 } else if pattern.contains("pop") {
160 CollectionOp::Pop
161 } else if pattern.contains("insert") {
162 CollectionOp::Insert
163 } else if pattern.contains("remove") {
164 CollectionOp::Remove
165 } else if pattern.contains("clear") {
166 CollectionOp::Clear
167 } else {
168 CollectionOp::Push }
170 }
171}
172
173impl Default for IoDetector {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179#[derive(Debug, Clone)]
181pub struct IoPatternSet {
182 pub file_ops: Vec<String>,
183 pub network_ops: Vec<String>,
184 pub console_ops: Vec<String>,
185 pub db_ops: Vec<String>,
186 pub env_ops: Vec<String>,
187 pub field_mutations: Vec<String>,
188 pub global_mutations: Vec<String>,
189 pub collection_mutations: Vec<String>,
190}
191
192impl IoPatternSet {
193 pub fn for_rust() -> Self {
195 Self {
196 file_ops: vec![
197 "std::fs::read".to_string(),
198 "std::fs::write".to_string(),
199 "std::fs::File::open".to_string(),
200 "std::fs::File::create".to_string(),
201 "std::fs::OpenOptions".to_string(),
202 "std::fs::remove".to_string(),
203 "std::fs::copy".to_string(),
204 "std::fs::rename".to_string(),
205 "fs::read".to_string(),
206 "fs::write".to_string(),
207 "File::open".to_string(),
208 "File::create".to_string(),
209 "read_to_string".to_string(),
210 "write_all".to_string(),
211 ],
212 network_ops: vec![
213 "reqwest::".to_string(),
214 "hyper::".to_string(),
215 "std::net::TcpStream".to_string(),
216 "std::net::TcpListener".to_string(),
217 "std::net::UdpSocket".to_string(),
218 "TcpStream::connect".to_string(),
219 "TcpListener::bind".to_string(),
220 ],
221 console_ops: vec![
222 "println!".to_string(),
223 "print!".to_string(),
224 "eprintln!".to_string(),
225 "eprint!".to_string(),
226 "dbg!".to_string(),
227 ],
228 db_ops: vec![
229 "diesel::".to_string(),
230 "sqlx::".to_string(),
231 "rusqlite::".to_string(),
232 "execute".to_string(),
233 "query".to_string(),
234 ],
235 env_ops: vec![
236 "std::env::var".to_string(),
237 "std::env::set_var".to_string(),
238 "env::var".to_string(),
239 "env::set_var".to_string(),
240 ],
241 field_mutations: vec![
242 "self.".to_string(),
243 ".set_".to_string(),
244 ".update_".to_string(),
245 ],
246 global_mutations: vec![
247 "static mut".to_string(),
248 "GLOBAL_".to_string(),
249 "unsafe {".to_string(),
250 ],
251 collection_mutations: vec![
252 ".push(".to_string(),
253 ".pop(".to_string(),
254 ".insert(".to_string(),
255 ".remove(".to_string(),
256 ".clear(".to_string(),
257 ".append(".to_string(),
258 ".extend(".to_string(),
259 ],
260 }
261 }
262
263 pub fn for_python() -> Self {
265 Self {
266 file_ops: vec![
267 "open(".to_string(),
268 "pathlib.Path".to_string(),
269 ".read_text(".to_string(),
270 ".write_text(".to_string(),
271 ".read_bytes(".to_string(),
272 ".write_bytes(".to_string(),
273 "os.path.".to_string(),
274 "shutil.".to_string(),
275 ],
276 network_ops: vec![
277 "requests.".to_string(),
278 "urllib.".to_string(),
279 "http.client.".to_string(),
280 "socket.".to_string(),
281 "httpx.".to_string(),
282 ],
283 console_ops: vec![
284 "print(".to_string(),
285 "input(".to_string(),
286 "sys.stdout.".to_string(),
287 "sys.stderr.".to_string(),
288 ],
289 db_ops: vec![
290 "sqlite3.".to_string(),
291 "psycopg2.".to_string(),
292 "pymongo.".to_string(),
293 ".execute(".to_string(),
294 ".fetchall(".to_string(),
295 ".fetchone(".to_string(),
296 ],
297 env_ops: vec![
298 "os.environ".to_string(),
299 "os.getenv(".to_string(),
300 "os.putenv(".to_string(),
301 ],
302 field_mutations: vec![
303 "self.".to_string(),
304 ".set_".to_string(),
305 ".update_".to_string(),
306 ],
307 global_mutations: vec!["global ".to_string(), "GLOBAL_".to_string()],
308 collection_mutations: vec![
309 ".append(".to_string(),
310 ".pop(".to_string(),
311 ".insert(".to_string(),
312 ".remove(".to_string(),
313 ".clear(".to_string(),
314 ".extend(".to_string(),
315 ],
316 }
317 }
318
319 pub fn for_javascript() -> Self {
321 Self {
322 file_ops: vec![
323 "fs.readFile".to_string(),
324 "fs.writeFile".to_string(),
325 "fs.readFileSync".to_string(),
326 "fs.writeFileSync".to_string(),
327 "fs.promises.".to_string(),
328 "require('fs')".to_string(),
329 ],
330 network_ops: vec![
331 "fetch(".to_string(),
332 "axios.".to_string(),
333 "XMLHttpRequest".to_string(),
334 "http.request".to_string(),
335 "https.request".to_string(),
336 ],
337 console_ops: vec![
338 "console.log".to_string(),
339 "console.error".to_string(),
340 "console.warn".to_string(),
341 "console.debug".to_string(),
342 ],
343 db_ops: vec![
344 "mongoose.".to_string(),
345 "sequelize.".to_string(),
346 ".query(".to_string(),
347 ".find(".to_string(),
348 ".findOne(".to_string(),
349 ],
350 env_ops: vec!["process.env".to_string(), "process.env.".to_string()],
351 field_mutations: vec![
352 "this.".to_string(),
353 ".set(".to_string(),
354 ".update(".to_string(),
355 ],
356 global_mutations: vec![
357 "window.".to_string(),
358 "global.".to_string(),
359 "GLOBAL_".to_string(),
360 ],
361 collection_mutations: vec![
362 ".push(".to_string(),
363 ".pop(".to_string(),
364 ".splice(".to_string(),
365 ".shift(".to_string(),
366 ".unshift(".to_string(),
367 ],
368 }
369 }
370
371 pub fn for_typescript() -> Self {
373 Self::for_javascript()
374 }
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
379pub enum IoOperation {
380 FileRead { path_expr: Option<String> },
381 FileWrite { path_expr: Option<String> },
382 NetworkRequest { endpoint: Option<String> },
383 ConsoleOutput { stream: OutputStream },
384 DatabaseQuery { query_type: QueryType },
385 EnvironmentAccess { var_name: Option<String> },
386}
387
388#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
390pub enum OutputStream {
391 Stdout,
392 Stderr,
393}
394
395#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
397pub enum QueryType {
398 Select,
399 Insert,
400 Update,
401 Delete,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
406pub enum SideEffect {
407 FieldMutation { target: String, field: String },
409 GlobalMutation { name: String },
411 CollectionMutation { operation: CollectionOp },
413 ExternalState { description: String },
415}
416
417#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
419pub enum CollectionOp {
420 Push,
421 Pop,
422 Insert,
423 Remove,
424 Clear,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct IoProfile {
430 pub file_operations: Vec<IoOperation>,
431 pub network_operations: Vec<IoOperation>,
432 pub console_operations: Vec<IoOperation>,
433 pub database_operations: Vec<IoOperation>,
434 pub environment_operations: Vec<IoOperation>,
435 pub side_effects: Vec<SideEffect>,
436 pub is_pure: bool,
437}
438
439impl IoProfile {
440 pub fn new() -> Self {
442 Self {
443 file_operations: Vec::new(),
444 network_operations: Vec::new(),
445 console_operations: Vec::new(),
446 database_operations: Vec::new(),
447 environment_operations: Vec::new(),
448 side_effects: Vec::new(),
449 is_pure: true,
450 }
451 }
452
453 pub fn has_file_io(&self) -> bool {
455 !self.file_operations.is_empty()
456 }
457
458 pub fn has_network_io(&self) -> bool {
460 !self.network_operations.is_empty()
461 }
462
463 pub fn has_console_io(&self) -> bool {
465 !self.console_operations.is_empty()
466 }
467
468 pub fn has_database_io(&self) -> bool {
470 !self.database_operations.is_empty()
471 }
472
473 pub fn primary_responsibility(&self) -> Responsibility {
475 match (
476 self.file_operations.is_empty(),
477 self.network_operations.is_empty(),
478 self.console_operations.is_empty(),
479 self.database_operations.is_empty(),
480 self.is_pure,
481 ) {
482 (false, _, _, _, _) => Responsibility::FileIO,
483 (_, false, _, _, _) => Responsibility::NetworkIO,
484 (_, _, false, _, _) => Responsibility::ConsoleIO,
485 (_, _, _, false, _) => Responsibility::DatabaseIO,
486 (true, true, true, true, true) => Responsibility::PureComputation,
487 _ => Responsibility::MixedIO,
488 }
489 }
490
491 pub fn intensity(&self) -> f64 {
493 (self.file_operations.len()
494 + self.network_operations.len()
495 + self.console_operations.len()
496 + self.database_operations.len()
497 + self.environment_operations.len()) as f64
498 }
499
500 pub fn merge(&mut self, other: &IoProfile) {
502 self.file_operations.extend(other.file_operations.clone());
503 self.network_operations
504 .extend(other.network_operations.clone());
505 self.console_operations
506 .extend(other.console_operations.clone());
507 self.database_operations
508 .extend(other.database_operations.clone());
509 self.environment_operations
510 .extend(other.environment_operations.clone());
511 self.side_effects.extend(other.side_effects.clone());
512 self.is_pure = self.is_pure && other.is_pure;
513 }
514}
515
516impl Default for IoProfile {
517 fn default() -> Self {
518 Self::new()
519 }
520}
521
522#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
524pub enum Responsibility {
525 PureComputation,
526 FileIO,
527 NetworkIO,
528 ConsoleIO,
529 DatabaseIO,
530 MixedIO,
531 SideEffects,
532}
533
534impl Responsibility {
535 pub fn as_str(&self) -> &'static str {
537 match self {
538 Responsibility::PureComputation => "Pure Computation",
539 Responsibility::FileIO => "File I/O",
540 Responsibility::NetworkIO => "Network I/O",
541 Responsibility::ConsoleIO => "Console I/O",
542 Responsibility::DatabaseIO => "Database I/O",
543 Responsibility::MixedIO => "Mixed I/O",
544 Responsibility::SideEffects => "Side Effects",
545 }
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn test_rust_file_io_detection() {
555 let code = r#"
556 fn read_config() -> String {
557 std::fs::read_to_string("config.toml").unwrap()
558 }
559 "#;
560
561 let detector = IoDetector::new();
562 let profile = detector.detect_io(code, Language::Rust);
563
564 assert!(!profile.file_operations.is_empty());
566 assert_eq!(profile.primary_responsibility(), Responsibility::FileIO);
567 assert!(!profile.is_pure);
568 }
569
570 #[test]
571 fn test_rust_network_io_detection() {
572 let code = r#"
573 fn fetch_data() {
574 let client = reqwest::blocking::Client::new();
575 let response = client.get("https://api.example.com").send();
576 }
577 "#;
578
579 let detector = IoDetector::new();
580 let profile = detector.detect_io(code, Language::Rust);
581
582 assert_eq!(profile.network_operations.len(), 1);
583 assert_eq!(profile.primary_responsibility(), Responsibility::NetworkIO);
584 }
585
586 #[test]
587 fn test_rust_console_io_detection() {
588 let code = r#"
589 fn log_message(msg: &str) {
590 println!("Message: {}", msg);
591 }
592 "#;
593
594 let detector = IoDetector::new();
595 let profile = detector.detect_io(code, Language::Rust);
596
597 assert_eq!(profile.console_operations.len(), 1);
598 assert_eq!(profile.primary_responsibility(), Responsibility::ConsoleIO);
599 }
600
601 #[test]
602 fn test_pure_function_detection() {
603 let code = r#"
604 fn calculate_sum(a: i32, b: i32) -> i32 {
605 a + b
606 }
607 "#;
608
609 let detector = IoDetector::new();
610 let profile = detector.detect_io(code, Language::Rust);
611
612 assert!(profile.is_pure);
613 assert_eq!(
614 profile.primary_responsibility(),
615 Responsibility::PureComputation
616 );
617 assert_eq!(profile.intensity(), 0.0);
618 }
619
620 #[test]
621 fn test_python_file_io_detection() {
622 let code = r#"
623 def read_config():
624 with open('config.json') as f:
625 return f.read()
626 "#;
627
628 let detector = IoDetector::new();
629 let profile = detector.detect_io(code, Language::Python);
630
631 assert_eq!(profile.file_operations.len(), 1);
632 assert_eq!(profile.primary_responsibility(), Responsibility::FileIO);
633 }
634
635 #[test]
636 fn test_python_network_io_detection() {
637 let code = r#"
638 def fetch_data():
639 response = requests.get('https://api.example.com/data')
640 return response.json()
641 "#;
642
643 let detector = IoDetector::new();
644 let profile = detector.detect_io(code, Language::Python);
645
646 assert_eq!(profile.network_operations.len(), 1);
647 assert_eq!(profile.primary_responsibility(), Responsibility::NetworkIO);
648 }
649
650 #[test]
651 fn test_javascript_file_io_detection() {
652 let code = r#"
653 function readConfig() {
654 return fs.readFileSync('config.json', 'utf8');
655 }
656 "#;
657
658 let detector = IoDetector::new();
659 let profile = detector.detect_io(code, Language::JavaScript);
660
661 assert!(!profile.file_operations.is_empty());
663 assert_eq!(profile.primary_responsibility(), Responsibility::FileIO);
664 }
665
666 #[test]
667 fn test_javascript_network_io_detection() {
668 let code = r#"
669 async function fetchData() {
670 const response = await fetch('https://api.example.com');
671 return await response.json();
672 }
673 "#;
674
675 let detector = IoDetector::new();
676 let profile = detector.detect_io(code, Language::JavaScript);
677
678 assert_eq!(profile.network_operations.len(), 1);
679 assert_eq!(profile.primary_responsibility(), Responsibility::NetworkIO);
680 }
681
682 #[test]
683 fn test_mixed_io_detection() {
684 let code = r#"
685 fn process_and_log() {
686 let data = std::fs::read_to_string("input.txt").unwrap();
687 println!("Processing: {}", data);
688 }
689 "#;
690
691 let detector = IoDetector::new();
692 let profile = detector.detect_io(code, Language::Rust);
693
694 assert!(!profile.file_operations.is_empty());
695 assert!(!profile.console_operations.is_empty());
696 assert!(profile.intensity() > 1.0);
697 }
698
699 #[test]
700 fn test_io_profile_merge() {
701 let mut profile1 = IoProfile::new();
702 profile1
703 .file_operations
704 .push(IoOperation::FileRead { path_expr: None });
705
706 let mut profile2 = IoProfile::new();
707 profile2
708 .network_operations
709 .push(IoOperation::NetworkRequest { endpoint: None });
710
711 profile1.merge(&profile2);
712
713 assert_eq!(profile1.file_operations.len(), 1);
714 assert_eq!(profile1.network_operations.len(), 1);
715 }
716
717 #[test]
718 fn test_responsibility_as_str() {
719 assert_eq!(Responsibility::PureComputation.as_str(), "Pure Computation");
720 assert_eq!(Responsibility::FileIO.as_str(), "File I/O");
721 assert_eq!(Responsibility::NetworkIO.as_str(), "Network I/O");
722 assert_eq!(Responsibility::ConsoleIO.as_str(), "Console I/O");
723 assert_eq!(Responsibility::DatabaseIO.as_str(), "Database I/O");
724 assert_eq!(Responsibility::MixedIO.as_str(), "Mixed I/O");
725 assert_eq!(Responsibility::SideEffects.as_str(), "Side Effects");
726 }
727
728 #[test]
729 fn test_rust_field_mutation_detection() {
730 let code = r#"
731 fn update_counter(&mut self) {
732 self.count += 1;
733 }
734 "#;
735
736 let detector = IoDetector::new();
737 let profile = detector.detect_io(code, Language::Rust);
738
739 assert!(!profile.side_effects.is_empty());
740 assert!(!profile.is_pure);
741 assert!(matches!(
742 profile.side_effects[0],
743 SideEffect::FieldMutation { .. }
744 ));
745 }
746
747 #[test]
748 fn test_rust_collection_mutation_detection() {
749 let code = r#"
750 fn add_item(&mut self, item: i32) {
751 self.items.push(item);
752 }
753 "#;
754
755 let detector = IoDetector::new();
756 let profile = detector.detect_io(code, Language::Rust);
757
758 assert!(!profile.side_effects.is_empty());
759 assert!(!profile.is_pure);
760 assert!(profile.side_effects.iter().any(|e| matches!(
762 e,
763 SideEffect::CollectionMutation {
764 operation: CollectionOp::Push
765 }
766 )));
767 }
768
769 #[test]
770 fn test_rust_global_mutation_detection() {
771 let code = r#"
772 static mut GLOBAL_COUNTER: i32 = 0;
773 fn increment_global() {
774 unsafe {
775 GLOBAL_COUNTER += 1;
776 }
777 }
778 "#;
779
780 let detector = IoDetector::new();
781 let profile = detector.detect_io(code, Language::Rust);
782
783 assert!(!profile.side_effects.is_empty());
784 assert!(!profile.is_pure);
785 assert!(!profile.side_effects.is_empty());
787 }
788
789 #[test]
790 fn test_python_field_mutation_detection() {
791 let code = r#"
792 def update_counter(self):
793 self.count += 1
794 "#;
795
796 let detector = IoDetector::new();
797 let profile = detector.detect_io(code, Language::Python);
798
799 assert!(!profile.side_effects.is_empty());
800 assert!(!profile.is_pure);
801 }
802
803 #[test]
804 fn test_python_collection_mutation_detection() {
805 let code = r#"
806 def add_item(self, item):
807 self.items.append(item)
808 "#;
809
810 let detector = IoDetector::new();
811 let profile = detector.detect_io(code, Language::Python);
812
813 assert!(!profile.side_effects.is_empty());
814 assert!(!profile.is_pure);
815 assert!(profile.side_effects.iter().any(|e| matches!(
817 e,
818 SideEffect::CollectionMutation {
819 operation: CollectionOp::Push
820 }
821 )));
822 }
823
824 #[test]
825 fn test_python_global_mutation_detection() {
826 let code = r#"
827 def increment_global():
828 global counter
829 counter += 1
830 "#;
831
832 let detector = IoDetector::new();
833 let profile = detector.detect_io(code, Language::Python);
834
835 assert!(!profile.side_effects.is_empty());
836 assert!(!profile.is_pure);
837 }
838
839 #[test]
840 fn test_javascript_field_mutation_detection() {
841 let code = r#"
842 function updateCounter() {
843 this.count += 1;
844 }
845 "#;
846
847 let detector = IoDetector::new();
848 let profile = detector.detect_io(code, Language::JavaScript);
849
850 assert!(!profile.side_effects.is_empty());
851 assert!(!profile.is_pure);
852 }
853
854 #[test]
855 fn test_javascript_collection_mutation_detection() {
856 let code = r#"
857 function addItem(item) {
858 this.items.push(item);
859 }
860 "#;
861
862 let detector = IoDetector::new();
863 let profile = detector.detect_io(code, Language::JavaScript);
864
865 assert!(!profile.side_effects.is_empty());
866 assert!(!profile.is_pure);
867 assert!(profile.side_effects.iter().any(|e| matches!(
869 e,
870 SideEffect::CollectionMutation {
871 operation: CollectionOp::Push
872 }
873 )));
874 }
875
876 #[test]
877 fn test_javascript_global_mutation_detection() {
878 let code = r#"
879 function updateGlobal() {
880 window.globalCounter += 1;
881 }
882 "#;
883
884 let detector = IoDetector::new();
885 let profile = detector.detect_io(code, Language::JavaScript);
886
887 assert!(!profile.side_effects.is_empty());
888 assert!(!profile.is_pure);
889 }
890
891 #[test]
892 fn test_multiple_collection_operations() {
893 let code = r#"
894 fn manage_items(&mut self) {
895 self.items.push(1);
896 self.items.pop();
897 self.items.insert(0, 2);
898 self.items.remove(1);
899 self.items.clear();
900 }
901 "#;
902
903 let detector = IoDetector::new();
904 let profile = detector.detect_io(code, Language::Rust);
905
906 assert!(profile.side_effects.len() >= 5);
907 assert!(!profile.is_pure);
908 }
909
910 #[test]
911 fn test_pure_function_no_side_effects() {
912 let code = r#"
913 fn calculate_sum(a: i32, b: i32) -> i32 {
914 a + b
915 }
916 "#;
917
918 let detector = IoDetector::new();
919 let profile = detector.detect_io(code, Language::Rust);
920
921 assert!(profile.side_effects.is_empty());
922 assert!(profile.is_pure);
923 }
924
925 #[test]
926 fn test_side_effects_affect_purity() {
927 let code_pure = r#"fn pure(x: i32) -> i32 { x * 2 }"#;
928 let code_impure = r#"fn impure(&mut self) { self.value = 42; }"#;
929
930 let detector = IoDetector::new();
931 let profile_pure = detector.detect_io(code_pure, Language::Rust);
932 let profile_impure = detector.detect_io(code_impure, Language::Rust);
933
934 assert!(profile_pure.is_pure);
935 assert!(!profile_impure.is_pure);
936 }
937}