rusty_cpp/parser/
mod.rs

1use clang::{Clang, Entity, EntityKind, Index};
2use std::path::Path;
3
4pub mod ast_visitor;
5pub mod annotations;
6pub mod header_cache;
7pub mod safety_annotations;
8pub mod template_context;
9pub mod type_annotations;
10pub mod external_annotations;
11
12pub use ast_visitor::{CppAst, Function, Statement, Expression, MethodQualifier, MoveKind};
13pub use header_cache::HeaderCache;
14pub use template_context::TemplateContext;
15#[allow(unused_imports)]
16pub use ast_visitor::{Variable, SourceLocation};
17
18use std::fs;
19use std::io::{BufRead, BufReader};
20
21#[allow(dead_code)]
22pub fn parse_cpp_file(path: &Path) -> Result<CppAst, String> {
23    parse_cpp_file_with_includes(path, &[])
24}
25
26#[allow(dead_code)]
27pub fn parse_cpp_file_with_includes(path: &Path, include_paths: &[std::path::PathBuf]) -> Result<CppAst, String> {
28    parse_cpp_file_with_includes_and_defines(path, include_paths, &[])
29}
30
31pub fn parse_cpp_file_with_includes_and_defines(path: &Path, include_paths: &[std::path::PathBuf], defines: &[String]) -> Result<CppAst, String> {
32    // Initialize Clang
33    let clang = Clang::new()
34        .map_err(|e| format!("Failed to initialize Clang: {:?}", e))?;
35    
36    let index = Index::new(&clang, false, false);
37    
38    // Build arguments with include paths and defines
39    let mut args = vec![
40        "-std=c++20".to_string(),
41        "-xc++".to_string(),
42        // Add flags to make parsing more lenient
43        "-fno-delayed-template-parsing".to_string(),
44        "-fparse-all-comments".to_string(),
45        // Suppress certain errors that don't affect borrow checking
46        "-Wno-everything".to_string(),
47        // Don't fail on missing includes
48        "-Wno-error".to_string(),
49    ];
50    
51    // Add include paths
52    for include_path in include_paths {
53        args.push(format!("-I{}", include_path.display()));
54    }
55    
56    // Add preprocessor definitions
57    for define in defines {
58        args.push(format!("-D{}", define));
59    }
60    
61    // Parse the translation unit with skip function bodies option for better error recovery
62    let tu = index
63        .parser(path)
64        .arguments(&args.iter().map(|s| s.as_str()).collect::<Vec<_>>())
65        .detailed_preprocessing_record(true)
66        .skip_function_bodies(false)  // We need function bodies for analysis
67        .incomplete(true)  // Allow incomplete translation units
68        .parse()
69        .map_err(|e| format!("Failed to parse file: {:?}", e))?;
70    
71    // Check for diagnostics but only fail on fatal errors
72    let diagnostics = tu.get_diagnostics();
73    let mut has_fatal = false;
74    if !diagnostics.is_empty() {
75        for diag in &diagnostics {
76            // Only fail on fatal errors, ignore regular errors
77            if diag.get_severity() >= clang::diagnostic::Severity::Fatal {
78                has_fatal = true;
79                eprintln!("Fatal error: {}", diag.get_text());
80            } else if diag.get_severity() >= clang::diagnostic::Severity::Error {
81                // Log errors but don't fail
82                eprintln!("Warning (suppressed error): {}", diag.get_text());
83            }
84        }
85    }
86    
87    if has_fatal {
88        return Err("Fatal parsing errors encountered".to_string());
89    }
90    
91    // Visit the AST
92    let mut ast = CppAst::new();
93    let root = tu.get_entity();
94    let main_file_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
95    visit_entity(&root, &mut ast, &main_file_path);
96
97    Ok(ast)
98}
99
100fn visit_entity(entity: &Entity, ast: &mut CppAst, main_file: &Path) {
101    use crate::debug_println;
102    // Only debug function and class-related entities
103    if matches!(entity.get_kind(), EntityKind::FunctionDecl | EntityKind::Method | EntityKind::FunctionTemplate | EntityKind::ClassTemplate) {
104        // Check if this is a template specialization
105        let template_kind = entity.get_template_kind();
106        let template_args = entity.get_template_arguments();
107        debug_println!("DEBUG PARSE: Visiting entity: kind={:?}, name={:?}, is_definition={}, template_kind={:?}, has_template_args={}",
108            entity.get_kind(), entity.get_name(), entity.is_definition(), template_kind, template_args.is_some());
109    }
110
111    // Extract entities from all files (main file and headers)
112    // The analysis phase will distinguish between system headers and user code
113    // System headers: track for safety status, but skip borrow checking
114    // User code: full borrow checking and safety analysis
115    let _main_file = main_file; // Keep parameter for future use
116
117    match entity.get_kind() {
118        EntityKind::FunctionDecl | EntityKind::Method => {
119            debug_println!("DEBUG PARSE: Found FunctionDecl: name={:?}, is_definition={}, kind={:?}",
120                entity.get_name(), entity.is_definition(), entity.get_kind());
121            // Extract all function definitions (from main file and headers)
122            if entity.is_definition() {
123                let func = ast_visitor::extract_function(entity);
124                ast.functions.push(func);
125            }
126        }
127        EntityKind::FunctionTemplate => {
128            // Template free functions: extract the template declaration to analyze with generic types
129            // We don't need instantiations - our borrow/move checking works on generic types!
130            debug_println!("DEBUG PARSE: Found FunctionTemplate: {:?}, is_definition={}",
131                entity.get_name(), entity.is_definition());
132
133            // The FunctionTemplateDecl IS the function entity in LibClang
134            // Its children are: TemplateTypeParameter, ParmDecl, CompoundStmt
135            // We extract the function directly from this entity
136            if entity.is_definition() {
137                debug_println!("DEBUG PARSE: Extracting template function from FunctionTemplate entity");
138                let func = ast_visitor::extract_function(entity);
139                debug_println!("DEBUG PARSE: Extracted template function: {}", func.name);
140                ast.functions.push(func);
141            }
142        }
143        EntityKind::ClassTemplate => {
144            // Phase 3: Template classes
145            debug_println!("DEBUG PARSE: Found ClassTemplate: {:?}, is_definition={}",
146                entity.get_name(), entity.is_definition());
147
148            // ClassTemplateDecl works similarly to FunctionTemplateDecl
149            // Its children are: TemplateTypeParameter, CXXRecordDecl
150            // Extract the class directly from this entity
151            if entity.is_definition() {
152                debug_println!("DEBUG PARSE: Extracting template class from ClassTemplate entity");
153                let class = ast_visitor::extract_class(entity);
154                debug_println!("DEBUG PARSE: Extracted template class: {}", class.name);
155                ast.classes.push(class);
156            }
157        }
158        EntityKind::ClassDecl | EntityKind::StructDecl => {
159            // Regular (non-template) classes and structs
160            debug_println!("DEBUG PARSE: Found ClassDecl/StructDecl: {:?}, is_definition={}",
161                entity.get_name(), entity.is_definition());
162
163            if entity.is_definition() {
164                debug_println!("DEBUG PARSE: Extracting regular class from ClassDecl entity");
165                let class = ast_visitor::extract_class(entity);
166                debug_println!("DEBUG PARSE: Extracted class: {}", class.name);
167                ast.classes.push(class);
168            }
169        }
170        EntityKind::CallExpr => {
171            // Note: We don't need to handle template instantiations here.
172            // Template functions are analyzed via their declarations (with generic types).
173            // CallExpr references to instantiations don't have bodies in LibClang anyway.
174        }
175        EntityKind::VarDecl => {
176            // Extract all global variables (from main file and headers)
177            let var = ast_visitor::extract_variable(entity);
178            ast.global_variables.push(var);
179        }
180        _ => {}
181    }
182
183    // Recursively visit children
184    for child in entity.get_children() {
185        visit_entity(&child, ast, main_file);
186    }
187}
188
189/// Check if the file has @safe annotation at the beginning
190#[allow(dead_code)]
191pub fn check_file_safety_annotation(path: &Path) -> Result<bool, String> {
192    let file = fs::File::open(path)
193        .map_err(|e| format!("Failed to open file for safety check: {}", e))?;
194    
195    let reader = BufReader::new(file);
196    
197    // Check first 20 lines for @safe annotation (before any code)
198    for (line_num, line_result) in reader.lines().enumerate() {
199        if line_num > 20 {
200            break; // Don't look too far
201        }
202        
203        let line = line_result.map_err(|e| format!("Failed to read line: {}", e))?;
204        let trimmed = line.trim();
205        
206        // Skip empty lines
207        if trimmed.is_empty() {
208            continue;
209        }
210        
211        // Check for @safe annotation in comments
212        if trimmed.starts_with("//") {
213            if trimmed.contains("@safe") {
214                return Ok(true);
215            }
216        } else if trimmed.starts_with("/*") {
217            // Check multi-line comment for @safe
218            if line.contains("@safe") {
219                return Ok(true);
220            }
221        } else if !trimmed.starts_with("#") {
222            // Found actual code (not preprocessor), stop looking
223            break;
224        }
225    }
226    
227    Ok(false) // No @safe annotation found
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use tempfile::NamedTempFile;
234    use std::io::Write;
235
236    fn create_temp_cpp_file(content: &str) -> NamedTempFile {
237        let mut file = NamedTempFile::with_suffix(".cpp").unwrap();
238        file.write_all(content.as_bytes()).unwrap();
239        file.flush().unwrap();
240        file
241    }
242    
243    #[allow(dead_code)]
244    fn is_libclang_available() -> bool {
245        // Try to initialize Clang to check if libclang is available
246        // Note: This might fail if another test already initialized Clang
247        true  // Assume it's available and let individual tests handle errors
248    }
249
250    #[test]
251    fn test_parse_simple_function() {
252        let code = r#"
253        void test_function() {
254            int x = 42;
255        }
256        "#;
257        
258        let temp_file = create_temp_cpp_file(code);
259        let result = parse_cpp_file(temp_file.path());
260        
261        match result {
262            Ok(ast) => {
263                assert_eq!(ast.functions.len(), 1);
264                assert_eq!(ast.functions[0].name, "test_function");
265            }
266            Err(e) if e.contains("already exists") => {
267                // Skip if Clang is already initialized by another test
268                eprintln!("Skipping test: Clang already initialized by another test");
269            }
270            Err(e) if e.contains("Failed to initialize Clang") => {
271                // Skip if libclang is not available
272                eprintln!("Skipping test: libclang not available");
273            }
274            Err(e) => {
275                panic!("Unexpected error: {}", e);
276            }
277        }
278    }
279
280    #[test]
281    fn test_parse_function_with_parameters() {
282        let code = r#"
283        int add(int a, int b) {
284            return a + b;
285        }
286        "#;
287        
288        let temp_file = create_temp_cpp_file(code);
289        let result = parse_cpp_file(temp_file.path());
290        
291        match result {
292            Ok(ast) => {
293                assert_eq!(ast.functions.len(), 1);
294                assert_eq!(ast.functions[0].name, "add");
295                assert_eq!(ast.functions[0].parameters.len(), 2);
296            }
297            Err(e) if e.contains("already exists") => {
298                eprintln!("Skipping test: Clang already initialized by another test");
299            }
300            Err(e) if e.contains("Failed to initialize Clang") => {
301                eprintln!("Skipping test: libclang not available");
302            }
303            Err(e) => {
304                panic!("Unexpected error: {}", e);
305            }
306        }
307    }
308
309    #[test]
310    fn test_parse_global_variable() {
311        let code = r#"
312        int global_var = 100;
313        
314        void func() {}
315        "#;
316        
317        let temp_file = create_temp_cpp_file(code);
318        let result = parse_cpp_file(temp_file.path());
319        
320        match result {
321            Ok(ast) => {
322                assert_eq!(ast.global_variables.len(), 1);
323                assert_eq!(ast.global_variables[0].name, "global_var");
324            }
325            Err(e) if e.contains("already exists") => {
326                eprintln!("Skipping test: Clang already initialized by another test");
327            }
328            Err(e) if e.contains("Failed to initialize Clang") => {
329                eprintln!("Skipping test: libclang not available");
330            }
331            Err(e) => {
332                panic!("Unexpected error: {}", e);
333            }
334        }
335    }
336
337    #[test]
338    fn test_parse_invalid_file() {
339        let temp_dir = tempfile::tempdir().unwrap();
340        let invalid_path = temp_dir.path().join("nonexistent.cpp");
341        
342        let result = parse_cpp_file(&invalid_path);
343        assert!(result.is_err());
344    }
345}