Skip to main content

memscope_rs/analysis/
ownership_analyzer.rs

1//! Ownership Analyzer
2//!
3//! This module provides static analysis capabilities for Rust ownership semantics.
4//! It uses a four-layer architecture:
5//! 1. rustdoc JSON - type information
6//! 2. syn - AST analysis
7//! 3. inference engine - ownership state machine
8//! 4. runtime tracing (optional)
9//!
10//! This analyzer can detect:
11//! - Move operations
12//! - Borrow operations (shared and mutable)
13//! - Use-after-move errors
14//! - Borrow conflicts
15
16use std::collections::HashMap;
17use std::path::PathBuf;
18
19/// Type information extracted from rustdoc JSON
20#[derive(Debug, Clone)]
21pub struct TypeInfo {
22    pub name: String,
23    pub is_copy: bool,
24    pub is_clone: bool,
25    pub is_drop: bool,
26    pub size: Option<usize>,
27}
28
29/// Database of type information from rustdoc JSON
30#[derive(Debug, Clone)]
31pub struct RustdocDatabase {
32    pub types: HashMap<String, TypeInfo>,
33    pub impls: HashMap<String, Vec<ImplInfo>>,
34}
35
36/// Implementation information
37#[derive(Debug, Clone)]
38pub struct ImplInfo {
39    pub type_name: String,
40    pub trait_name: String,
41}
42
43/// Ownership operation detected by AST analysis
44#[derive(Debug, Clone, PartialEq)]
45pub enum OwnershipOp {
46    Move {
47        target: String,
48        source: String,
49        line: usize,
50    },
51    CallMove {
52        arg_name: String,
53        func_name: String,
54        arg_index: usize,
55        line: usize,
56    },
57    Borrow {
58        target: String,
59        is_mut: bool,
60        line: usize,
61    },
62}
63
64/// Rustdoc JSON extractor
65pub struct RustdocExtractor {
66    _json_path: PathBuf,
67}
68
69impl RustdocExtractor {
70    pub fn new(json_path: PathBuf) -> Self {
71        Self {
72            _json_path: json_path,
73        }
74    }
75
76    /// Extract type information from rustdoc JSON
77    pub fn extract(&self) -> crate::error::MemScopeResult<RustdocDatabase> {
78        // TODO: Implement actual rustdoc JSON parsing
79        // For now, return a placeholder database
80        let mut db = RustdocDatabase {
81            types: HashMap::new(),
82            impls: HashMap::new(),
83        };
84
85        // Add common types with known Copy semantics
86        db.types.insert(
87            "i32".to_string(),
88            TypeInfo {
89                name: "i32".to_string(),
90                is_copy: true,
91                is_clone: true,
92                is_drop: false,
93                size: Some(4),
94            },
95        );
96        db.types.insert(
97            "i64".to_string(),
98            TypeInfo {
99                name: "i64".to_string(),
100                is_copy: true,
101                is_clone: true,
102                is_drop: false,
103                size: Some(8),
104            },
105        );
106        db.types.insert(
107            "f32".to_string(),
108            TypeInfo {
109                name: "f32".to_string(),
110                is_copy: true,
111                is_clone: true,
112                is_drop: false,
113                size: Some(4),
114            },
115        );
116        db.types.insert(
117            "f64".to_string(),
118            TypeInfo {
119                name: "f64".to_string(),
120                is_copy: true,
121                is_clone: true,
122                is_drop: false,
123                size: Some(8),
124            },
125        );
126        db.types.insert(
127            "bool".to_string(),
128            TypeInfo {
129                name: "bool".to_string(),
130                is_copy: true,
131                is_clone: true,
132                is_drop: false,
133                size: Some(1),
134            },
135        );
136        db.types.insert(
137            "usize".to_string(),
138            TypeInfo {
139                name: "usize".to_string(),
140                is_copy: true,
141                is_clone: true,
142                is_drop: false,
143                size: Some(8),
144            },
145        );
146        db.types.insert(
147            "String".to_string(),
148            TypeInfo {
149                name: "String".to_string(),
150                is_copy: false,
151                is_clone: true,
152                is_drop: true,
153                size: Some(24),
154            },
155        );
156        db.types.insert(
157            "Vec".to_string(),
158            TypeInfo {
159                name: "Vec".to_string(),
160                is_copy: false,
161                is_clone: true,
162                is_drop: true,
163                size: Some(24),
164            },
165        );
166
167        Ok(db)
168    }
169}
170
171/// AST analyzer for parsing Rust source code
172pub struct AstAnalyzer {
173    source_code: String,
174}
175
176impl AstAnalyzer {
177    pub fn new(source_code: String) -> Self {
178        Self { source_code }
179    }
180
181    /// Analyze source code to extract ownership operations
182    pub fn analyze(&self) -> Vec<OwnershipOp> {
183        let mut ops = Vec::new();
184
185        // Simple line-based analysis for now
186        // TODO: Use syn crate for proper AST parsing
187        for (line_num, line) in self.source_code.lines().enumerate() {
188            self.detect_move_operations(line, line_num, &mut ops);
189            self.detect_borrow_operations(line, line_num, &mut ops);
190            self.detect_function_calls(line, line_num, &mut ops);
191        }
192
193        ops
194    }
195
196    fn detect_move_operations(&self, line: &str, line_num: usize, ops: &mut Vec<OwnershipOp>) {
197        // Detect variable assignments that may involve moves
198        if line.contains('=') && !line.contains('&') {
199            let parts: Vec<&str> = line.split('=').collect();
200            if parts.len() == 2 {
201                let target = parts[0].split_whitespace().next().unwrap_or("");
202                let source = parts[1].split_whitespace().next().unwrap_or("");
203
204                // Skip if it's a literal or copy type
205                if !source.is_empty()
206                    && source.parse::<i32>().is_err()
207                    && source.parse::<f64>().is_err()
208                    && !source.starts_with('"')
209                {
210                    ops.push(OwnershipOp::Move {
211                        target: target.to_string(),
212                        source: source.to_string(),
213                        line: line_num,
214                    });
215                }
216            }
217        }
218    }
219
220    fn detect_borrow_operations(&self, line: &str, line_num: usize, ops: &mut Vec<OwnershipOp>) {
221        // Detect borrow operations (& and &mut)
222        if line.contains('&') {
223            let is_mut = line.contains("&mut");
224            // Extract the variable being borrowed
225            let var = line
226                .split('&')
227                .nth(1)
228                .and_then(|s| s.split_whitespace().next())
229                .unwrap_or("");
230
231            if !var.is_empty() {
232                ops.push(OwnershipOp::Borrow {
233                    target: var.to_string(),
234                    is_mut,
235                    line: line_num,
236                });
237            }
238        }
239    }
240
241    fn detect_function_calls(&self, line: &str, line_num: usize, ops: &mut Vec<OwnershipOp>) {
242        // Detect function calls that may move arguments
243        if line.contains('(') && line.contains(')') {
244            let func_name = line
245                .split('(')
246                .next()
247                .and_then(|s| s.split_whitespace().last())
248                .unwrap_or("");
249
250            let args = line
251                .split('(')
252                .nth(1)
253                .and_then(|s| s.split(')').next())
254                .unwrap_or("");
255
256            // Check if function takes ownership (heuristic)
257            if self.is_ownership_taking_function(func_name) {
258                for (arg_idx, arg) in args.split(',').enumerate() {
259                    let arg_name = arg.split_whitespace().next().unwrap_or("");
260                    if !arg_name.is_empty() {
261                        ops.push(OwnershipOp::CallMove {
262                            arg_name: arg_name.to_string(),
263                            func_name: func_name.to_string(),
264                            arg_index: arg_idx,
265                            line: line_num,
266                        });
267                    }
268                }
269            }
270        }
271    }
272
273    fn is_ownership_taking_function(&self, func_name: &str) -> bool {
274        // Heuristic: functions that typically take ownership
275        // TODO: Use rustdoc JSON to determine actual function signatures
276        matches!(
277            func_name,
278            "into_iter" | "into_vec" | "into_string" | "into_boxed_slice" | "collect" | "consume"
279        )
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_rustdoc_extractor() {
289        let extractor = RustdocExtractor::new(PathBuf::from("test.json"));
290        let db = extractor.extract().unwrap();
291
292        assert!(db.types.contains_key("i32"));
293        assert!(db.types.contains_key("String"));
294    }
295
296    #[test]
297    fn test_type_info_copy() {
298        let extractor = RustdocExtractor::new(PathBuf::from("test.json"));
299        let db = extractor.extract().unwrap();
300
301        let i32_info = db.types.get("i32").unwrap();
302        assert!(i32_info.is_copy);
303    }
304
305    #[test]
306    fn test_type_info_string_not_copy() {
307        let extractor = RustdocExtractor::new(PathBuf::from("test.json"));
308        let db = extractor.extract().unwrap();
309
310        let string_info = db.types.get("String").unwrap();
311        assert!(!string_info.is_copy);
312        assert!(string_info.is_clone);
313    }
314
315    #[test]
316    fn test_ast_analyzer_move() {
317        let source = "let y = x;";
318        let analyzer = AstAnalyzer::new(source.to_string());
319        let ops = analyzer.analyze();
320
321        assert!(!ops.is_empty());
322        assert!(matches!(ops[0], OwnershipOp::Move { .. }));
323    }
324
325    #[test]
326    fn test_ast_analyzer_borrow() {
327        let source = "let y = &x;";
328        let analyzer = AstAnalyzer::new(source.to_string());
329        let ops = analyzer.analyze();
330
331        assert!(!ops.is_empty());
332        assert!(matches!(ops[0], OwnershipOp::Borrow { .. }));
333    }
334
335    #[test]
336    fn test_ast_analyzer_mut_borrow() {
337        let source = "let y = &mut x;";
338        let analyzer = AstAnalyzer::new(source.to_string());
339        let ops = analyzer.analyze();
340
341        assert!(!ops.is_empty());
342        if let OwnershipOp::Borrow { is_mut, .. } = &ops[0] {
343            assert!(*is_mut);
344        }
345    }
346
347    #[test]
348    fn test_ast_analyzer_function_call() {
349        let source = "let iter = vec.into_iter();";
350        let analyzer = AstAnalyzer::new(source.to_string());
351        let ops = analyzer.analyze();
352
353        // Should detect the into_iter call
354        assert!(!ops.is_empty());
355    }
356}