rusty_cpp/parser/
safety_annotations.rs

1use std::path::Path;
2use std::fs::File;
3use std::io::{BufRead, BufReader};
4use clang::Entity;
5use crate::debug_println;
6
7/// Helper function to check if a string starts with a safety annotation
8/// Accepts annotations with any suffix: @safe, @safe-XXX, @safe: note, etc.
9/// But rejects partial matches like @safety or @safeguard
10/// The annotation MUST be at the start of the text (already trimmed)
11fn contains_annotation(text: &str, annotation: &str) -> bool {
12    // The annotation must be at the start of the (already trimmed) text
13    if !text.starts_with(annotation) {
14        return false;
15    }
16
17    // Check what comes AFTER the annotation
18    let after_annotation = annotation.len();
19    if after_annotation >= text.len() {
20        // End of string - exact match
21        return true;
22    }
23
24    // Check the next character - it should NOT be alphanumeric
25    // This prevents matching @safety when looking for @safe
26    let next_char = text.chars().nth(after_annotation);
27    if let Some(ch) = next_char {
28        !ch.is_alphanumeric()
29    } else {
30        true
31    }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub enum SafetyMode {
36    Safe,   // Enforce borrow checking, can only call other @safe functions
37    Unsafe, // Skip borrow checking, default for unannotated code
38}
39
40/// Class annotation types for inheritance safety
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub enum ClassAnnotation {
43    Interface,  // @interface - pure virtual class (like Rust trait)
44    Safe,       // @safe - class methods are safe by default
45    Unsafe,     // @unsafe - class methods are unsafe by default
46}
47
48/// Function signature for disambiguating overloaded functions
49#[derive(Debug, Clone, PartialEq, Eq, Hash)]
50pub struct FunctionSignature {
51    pub name: String,
52    pub param_types: Option<Vec<String>>,  // None means match by name only
53}
54
55impl FunctionSignature {
56    fn new(name: String, param_types: Option<Vec<String>>) -> Self {
57        Self { name, param_types }
58    }
59
60    fn from_name_only(name: String) -> Self {
61        Self { name, param_types: None }
62    }
63
64    /// Check if this signature matches another (handles partial matches)
65    fn matches(&self, other: &FunctionSignature) -> bool {
66        // Names must match
67        if self.name != other.name {
68            return false;
69        }
70
71        // If either has no param types, match by name only
72        match (&self.param_types, &other.param_types) {
73            (None, _) | (_, None) => true,
74            (Some(a), Some(b)) => a == b,
75        }
76    }
77}
78
79#[derive(Debug, Clone)]
80pub struct SafetyContext {
81    pub file_default: SafetyMode,
82    pub function_overrides: Vec<(FunctionSignature, SafetyMode)>, // Function signature -> safety mode
83    pub source_file: Option<String>, // The source file where annotations were parsed from
84}
85
86
87impl SafetyContext {
88    pub fn new() -> Self {
89        Self {
90            file_default: SafetyMode::Unsafe,
91            function_overrides: Vec::new(),
92            source_file: None,
93        }
94    }
95    
96    /// Merge safety annotations from headers into this context
97    pub fn merge_header_annotations(&mut self, header_cache: &super::header_cache::HeaderCache) {
98        // For each function that has a safety annotation in a header,
99        // add it to our overrides if not already present
100        for (func_name, &safety_mode) in header_cache.safety_annotations.iter() {
101            // Check if we already have an override for this function
102            // Need to check both exact match and qualified/unqualified variations
103            let already_has_override = self.function_overrides.iter()
104                .any(|(sig, _)| {
105                    sig.name == *func_name ||
106                    sig.name.ends_with(&format!("::{}", func_name)) ||
107                    func_name.ends_with(&format!("::{}", sig.name))
108                });
109
110            if !already_has_override {
111                // Add the header's safety annotation (name only, no param types from header)
112                debug_println!("DEBUG SAFETY: Adding header annotation for '{}': {:?}", func_name, safety_mode);
113                let signature = FunctionSignature::from_name_only(func_name.clone());
114                self.function_overrides.push((signature, safety_mode));
115            } else {
116                debug_println!("DEBUG SAFETY: Function '{}' already has annotation, keeping source file version", func_name);
117            }
118            // If we already have an override from the source file, it takes precedence
119        }
120    }
121
122    /// Check if a specific function should be checked
123    pub fn should_check_function(&self, func_name: &str) -> bool {
124        self.get_function_safety(func_name) == SafetyMode::Safe
125    }
126
127    /// Check if a file path is from the source file where annotations were parsed
128    /// Returns true if the file path matches the source file, false otherwise
129    pub fn is_from_source_file(&self, file_path: &str) -> bool {
130        if let Some(ref source) = self.source_file {
131            // Compare file paths - handle both absolute and relative paths
132            // Check if either path ends with the other (to handle different path prefixes)
133            file_path == source ||
134            file_path.ends_with(source) ||
135            source.ends_with(file_path) ||
136            // Also check just the filename in case paths differ
137            std::path::Path::new(file_path).file_name() == std::path::Path::new(source).file_name()
138        } else {
139            // No source file set - assume everything is from source (backward compatibility)
140            true
141        }
142    }
143
144    /// Get the safety mode of a specific function
145    pub fn get_function_safety(&self, func_name: &str) -> SafetyMode {
146        let query = FunctionSignature::from_name_only(func_name.to_string());
147
148        // First check for exact match with function-specific override
149        for (sig, mode) in &self.function_overrides {
150            if sig.matches(&query) {
151                return *mode;
152            }
153
154            let sig_is_qualified = sig.name.contains("::");
155            let func_is_qualified = func_name.contains("::");
156
157            // Bug #8 fix: Careful suffix matching to avoid namespace collisions
158            // REMOVED Case 1: Qualified stored name, unqualified lookup - NO LONGER MATCH
159            //         This was causing false positives: an unqualified "get" would incorrectly
160            //         match "rusty::Cell::get" or any other qualified ::get annotation.
161            //         e.g., stored "rusty::Cell::get", lookup "get" -> NO MATCH (could be any get)
162            // Case 2: Both qualified - allow suffix matching on either side
163            //         e.g., stored "rrr::Timer::start", lookup "Timer::start" -> MATCH
164            // Case 3: Unqualified stored, qualified lookup - DON'T match (bug #8 scenario)
165            //         e.g., stored "Node", lookup "yaml::Node" -> NO MATCH (different namespaces)
166            // Note: If sig_is_qualified && !func_is_qualified, we DON'T match anymore.
167            //       This is stricter but prevents false matches from unqualified external function calls.
168            if sig_is_qualified && func_is_qualified {
169                // Both are qualified - allow suffix matching on either side
170                if sig.name.ends_with(&format!("::{}", func_name)) || func_name.ends_with(&format!("::{}", sig.name)) {
171                    return *mode;
172                }
173            }
174            // Note: if !sig_is_qualified && func_is_qualified, we DON'T match
175            // This prevents "Node" from matching "yaml::Node" (bug #8)
176        }
177
178        // If the function is a method (contains "::"), check if the class is annotated
179        // E.g., for "rrr::Alarm::add", check if "rrr::Alarm" or "Alarm" is annotated
180        if func_name.contains("::") {
181            // Try to extract the class name by removing the method name
182            if let Some(last_colon) = func_name.rfind("::") {
183                let class_name = &func_name[..last_colon];
184
185                // Check if the class has an annotation
186                let class_query = FunctionSignature::from_name_only(class_name.to_string());
187                for (sig, mode) in &self.function_overrides {
188                    if sig.matches(&class_query) {
189                        return *mode;
190                    }
191
192                    // Bug #8 fix: Careful suffix matching
193                    let sig_is_qualified = sig.name.contains("::");
194                    let class_is_qualified = class_name.contains("::");
195
196                    // Note: If sig_is_qualified && !class_is_qualified, we DON'T match anymore.
197                    // This prevents an unqualified "Node" from matching "yaml::Node" annotation.
198                    if sig_is_qualified && class_is_qualified {
199                        if sig.name.ends_with(&format!("::{}", class_name)) || class_name.ends_with(&format!("::{}", sig.name)) {
200                            return *mode;
201                        }
202                    }
203                }
204            }
205        }
206
207        // Fall back to file default
208        self.file_default
209    }
210
211    /// Get the safety mode of a specific class
212    /// This is similar to get_function_safety but specifically handles class-level annotations
213    pub fn get_class_safety(&self, class_name: &str) -> SafetyMode {
214        let query = FunctionSignature::from_name_only(class_name.to_string());
215
216        debug_println!("DEBUG SAFETY: Looking up class '{}'", class_name);
217        debug_println!("DEBUG SAFETY: Stored overrides ({} total):", self.function_overrides.len());
218        for (sig, mode) in &self.function_overrides {
219            debug_println!("DEBUG SAFETY:   - '{}' -> {:?}", sig.name, mode);
220        }
221
222        // Check for exact match
223        for (sig, mode) in &self.function_overrides {
224            if sig.matches(&query) {
225                debug_println!("DEBUG SAFETY: Exact match for class '{}' -> {:?}", class_name, mode);
226                return *mode;
227            }
228
229            // Bug #8 fix: Careful suffix matching to avoid namespace collisions
230            let sig_is_qualified = sig.name.contains("::");
231            let class_is_qualified = class_name.contains("::");
232
233            // REMOVED Case 1: Qualified stored, unqualified lookup - NO LONGER MATCH
234            //         This was causing false positives: an unqualified "Node" would incorrectly
235            //         match "rusty::Node" or any other qualified ::Node annotation.
236            // Case 2: Both qualified - allow suffix matching on either side
237            //         e.g., stored "rusty::Node", lookup "ns::rusty::Node" -> MATCH
238            // Case 3: Unqualified stored, qualified lookup - DON'T match (bug #8)
239            //         e.g., stored "Node", lookup "yaml::Node" -> NO MATCH
240            // Note: If sig_is_qualified && !class_is_qualified, we DON'T match anymore.
241            if sig_is_qualified && class_is_qualified {
242                // Both are qualified - suffix matching is sound
243                if sig.name.ends_with(&format!("::{}", class_name)) {
244                    debug_println!("DEBUG SAFETY: Suffix match for class '{}' (stored as '{}') -> {:?}", class_name, sig.name, mode);
245                    return *mode;
246                }
247
248                if class_name.ends_with(&format!("::{}", sig.name)) {
249                    debug_println!("DEBUG SAFETY: Prefix match for class '{}' (query has more qualifiers) -> {:?}", class_name, mode);
250                    return *mode;
251                }
252            }
253            // Note: if !sig_is_qualified && class_is_qualified, we DON'T match (bug #8)
254        }
255
256        debug_println!("DEBUG SAFETY: No match for class '{}', using file default: {:?}", class_name, self.file_default);
257        // Fall back to file default
258        self.file_default
259    }
260
261    /// Get the safety mode of a class, considering its source file location
262    ///
263    /// IMPORTANT: file_default only applies to classes from the source file being analyzed.
264    /// Classes from other files (system headers, external libraries) are treated as Undeclared
265    /// unless they have an explicit annotation.
266    ///
267    /// This fixes the namespace collision bug where a user's @safe namespace annotation
268    /// was incorrectly applying to STL classes from system headers.
269    pub fn get_class_safety_for_file(&self, class_name: &str, class_file: &str) -> SafetyMode {
270        let query = FunctionSignature::from_name_only(class_name.to_string());
271
272        debug_println!("DEBUG SAFETY: Looking up class '{}' from file '{}'", class_name, class_file);
273
274        // Check for explicit annotation (exact match or qualified match)
275        for (sig, mode) in &self.function_overrides {
276            if sig.matches(&query) {
277                debug_println!("DEBUG SAFETY: Exact match for class '{}' -> {:?}", class_name, mode);
278                return *mode;
279            }
280
281            let sig_is_qualified = sig.name.contains("::");
282            let class_is_qualified = class_name.contains("::");
283
284            if sig_is_qualified && class_is_qualified {
285                if sig.name.ends_with(&format!("::{}", class_name)) {
286                    debug_println!("DEBUG SAFETY: Suffix match for class '{}' -> {:?}", class_name, mode);
287                    return *mode;
288                }
289
290                if class_name.ends_with(&format!("::{}", sig.name)) {
291                    debug_println!("DEBUG SAFETY: Prefix match for class '{}' -> {:?}", class_name, mode);
292                    return *mode;
293                }
294            }
295        }
296
297        // No explicit annotation found
298        // Only apply file_default if the class is from the source file
299        if self.is_from_source_file(class_file) {
300            debug_println!("DEBUG SAFETY: Class '{}' is from source file, using file default: {:?}",
301                class_name, self.file_default);
302            self.file_default
303        } else {
304            // Class is from another file (header, system library, etc.)
305            // Treat as Undeclared - user must explicitly annotate external types
306            debug_println!("DEBUG SAFETY: Class '{}' is NOT from source file '{}', treating as Undeclared",
307                class_name, class_file);
308            SafetyMode::Unsafe
309        }
310    }
311
312    /// Get the safety mode of a function, considering its source file location
313    ///
314    /// IMPORTANT: file_default only applies to functions from the source file being analyzed.
315    /// Functions from other files are treated as Undeclared unless explicitly annotated.
316    pub fn get_function_safety_for_file(&self, func_name: &str, func_file: &str) -> SafetyMode {
317        let query = FunctionSignature::from_name_only(func_name.to_string());
318
319        // Check for explicit function-specific override
320        for (sig, mode) in &self.function_overrides {
321            if sig.matches(&query) {
322                return *mode;
323            }
324
325            let sig_is_qualified = sig.name.contains("::");
326            let func_is_qualified = func_name.contains("::");
327
328            if sig_is_qualified && func_is_qualified {
329                if sig.name.ends_with(&format!("::{}", func_name)) || func_name.ends_with(&format!("::{}", sig.name)) {
330                    return *mode;
331                }
332            }
333        }
334
335        // Check class-level annotation for methods
336        if func_name.contains("::") {
337            if let Some(last_colon) = func_name.rfind("::") {
338                let class_name = &func_name[..last_colon];
339
340                let class_query = FunctionSignature::from_name_only(class_name.to_string());
341                for (sig, mode) in &self.function_overrides {
342                    if sig.matches(&class_query) {
343                        return *mode;
344                    }
345
346                    let sig_is_qualified = sig.name.contains("::");
347                    let class_is_qualified = class_name.contains("::");
348
349                    if sig_is_qualified && class_is_qualified {
350                        if sig.name.ends_with(&format!("::{}", class_name)) || class_name.ends_with(&format!("::{}", sig.name)) {
351                            return *mode;
352                        }
353                    }
354                }
355            }
356        }
357
358        // No explicit annotation found
359        // Only apply file_default if the function is from the source file
360        if self.is_from_source_file(func_file) {
361            self.file_default
362        } else {
363            SafetyMode::Unsafe
364        }
365    }
366
367    /// Check if a function should be checked, considering its source file location
368    pub fn should_check_function_for_file(&self, func_name: &str, func_file: &str) -> bool {
369        self.get_function_safety_for_file(func_name, func_file) == SafetyMode::Safe
370    }
371}
372
373/// Parse safety annotations from a C++ file using the unified rule:
374/// @safe/@unsafe attaches to the next statement/block/function/namespace
375pub fn parse_safety_annotations(path: &Path) -> Result<SafetyContext, String> {
376    let file = File::open(path)
377        .map_err(|e| format!("Failed to open file for safety parsing: {}", e))?;
378
379    let reader = BufReader::new(file);
380    let mut context = SafetyContext::new();
381
382    // Store the source file path for later reference
383    // This is used to only apply file_default to code from this file
384    context.source_file = path.to_str().map(|s| s.to_string());
385
386    let mut pending_annotation: Option<SafetyMode> = None;
387    let mut in_comment_block = false;
388    let mut _current_line = 0;
389
390    let mut accumulated_line = String::new();
391    let mut accumulating_for_annotation = false;
392
393    // Bug #8 fix: Track class context for method annotations
394    let mut class_context_stack: Vec<String> = Vec::new();
395    let mut brace_depth = 0;
396    
397    for line_result in reader.lines() {
398        _current_line += 1;
399        let line = line_result.map_err(|e| format!("Failed to read line: {}", e))?;
400        let trimmed = line.trim();
401        
402        // Handle multi-line comments
403        if in_comment_block {
404            if trimmed.contains("*/") {
405                in_comment_block = false;
406            }
407            // Check for annotations in multi-line comments (must be on their own)
408            let cleaned = trimmed.trim_start_matches('*').trim();
409            if contains_annotation(cleaned, "@safe") {
410                pending_annotation = Some(SafetyMode::Safe);
411            } else if contains_annotation(cleaned, "@unsafe") {
412                pending_annotation = Some(SafetyMode::Unsafe);
413            }
414            continue;
415        }
416        
417        // Check for comment start
418        if trimmed.starts_with("/*") {
419            in_comment_block = true;
420            // Check if it's a single-line /* @safe */ or /* @unsafe */ comment
421            if let Some(end_pos) = trimmed.find("*/") {
422                let comment_content = trimmed[2..end_pos].trim();
423                if contains_annotation(comment_content, "@safe") {
424                    pending_annotation = Some(SafetyMode::Safe);
425                } else if contains_annotation(comment_content, "@unsafe") {
426                    pending_annotation = Some(SafetyMode::Unsafe);
427                }
428                in_comment_block = false;
429            }
430            continue;
431        }
432        
433        // Check single-line comments
434        if trimmed.starts_with("//") {
435            // Only look for annotations that are word boundaries (not part of other text)
436            let comment_text = trimmed[2..].trim();
437            if contains_annotation(comment_text, "@safe") {
438                pending_annotation = Some(SafetyMode::Safe);
439            } else if contains_annotation(comment_text, "@unsafe") {
440                pending_annotation = Some(SafetyMode::Unsafe);
441            }
442            continue;
443        }
444        
445        // Skip empty lines and preprocessor directives
446        if trimmed.is_empty() || trimmed.starts_with("#") {
447            continue;
448        }
449
450        // Bug #8 fix: Track braces to know when we exit a class
451        // Note: This is a simplified tracking that doesn't handle strings/comments perfectly
452        // but works for typical C++ code with annotations
453        let opens = trimmed.matches('{').count() as i32;
454        let closes = trimmed.matches('}').count() as i32;
455        brace_depth += opens - closes;
456
457        // Pop class context when we exit its scope
458        if brace_depth <= 0 && !class_context_stack.is_empty() {
459            class_context_stack.pop();
460            brace_depth = 0; // Reset to handle nested classes properly
461        }
462
463        // Bug #8 fix: Track class declarations even without annotations
464        // This ensures method annotations get qualified with class name
465        // NOTE: Only push non-annotated classes here; annotated classes are pushed
466        // in the annotation handling section below
467        let is_class_line = is_class_declaration(trimmed);
468        let needs_class_tracking = is_class_line && pending_annotation.is_none() && !accumulating_for_annotation;
469        if needs_class_tracking {
470            if let Some(class_name) = extract_class_name(trimmed) {
471                // Calculate brace depth for this class declaration
472                let class_brace_depth = trimmed.matches('{').count() as i32
473                            - trimmed.matches('}').count() as i32;
474                // Only push class to context if it's NOT complete on the same line
475                // A class complete on one line (like `struct Foo { int x; };`) has brace_depth == 0
476                // and should not be pushed to context stack
477                if class_brace_depth > 0 {
478                    class_context_stack.push(class_name);
479                    brace_depth = class_brace_depth;
480                }
481            }
482        }
483
484        // Track namespace declarations (even without annotations) for qualified name building
485        // This ensures function annotations inside namespaces get the proper qualified name
486        let is_namespace_line = (trimmed.starts_with("namespace ") || trimmed.contains(" namespace "))
487                                && !trimmed.contains("using ")
488                                && trimmed.contains('{');
489        let needs_namespace_tracking = is_namespace_line && pending_annotation.is_none() && !accumulating_for_annotation;
490        if needs_namespace_tracking {
491            if let Some(ns_name) = extract_namespace_name(trimmed) {
492                debug_println!("DEBUG SAFETY: Entering namespace '{}' for context", ns_name);
493                class_context_stack.push(ns_name);
494                // Reset brace depth to track this namespace's scope
495                brace_depth = trimmed.matches('{').count() as i32
496                            - trimmed.matches('}').count() as i32;
497            }
498        }
499
500        // If we have a pending annotation, start accumulating
501        if pending_annotation.is_some() && !accumulating_for_annotation {
502            accumulated_line.clear();
503            accumulating_for_annotation = true;
504        }
505        
506        // Only accumulate if we're looking for annotation target
507        if accumulating_for_annotation {
508            if !accumulated_line.is_empty() {
509                accumulated_line.push(' ');
510            }
511            accumulated_line.push_str(trimmed);
512            
513            // Check if we have a complete declaration to apply annotation to
514            // For namespaces: just needs to start with "namespace" and have opening brace
515            // For classes: needs "class"/"struct" keyword and opening brace
516            // For functions: needs parentheses
517            let is_namespace_decl = accumulated_line.starts_with("namespace") ||
518                                   (accumulated_line.contains("namespace") && !accumulated_line.contains("using"));
519            let is_class_decl = is_class_declaration(&accumulated_line);
520            let should_check_annotation = if is_namespace_decl || is_class_decl {
521                accumulated_line.contains('{')
522            } else {
523                accumulated_line.contains('(') &&
524                (accumulated_line.contains(')') || accumulated_line.contains('{'))
525            };
526
527            // CRITICAL FIX: Check if this is a forward declaration
528            // Forward declarations (class Foo;) should consume the annotation without applying it
529            // This prevents the annotation from carrying over to the next declaration
530            let is_forward_decl = is_forward_declaration(&accumulated_line);
531
532            if is_forward_decl && pending_annotation.is_some() {
533                // Forward declarations should NOT have annotations (they have no body)
534                // Consume the annotation without applying it to prevent it from affecting
535                // subsequent declarations (especially the full class definition)
536                debug_println!("DEBUG SAFETY: Ignoring annotation on forward declaration: {}",
537                               &accumulated_line);
538                pending_annotation.take();  // Consume the annotation
539                accumulated_line.clear();
540                accumulating_for_annotation = false;
541                continue;  // Skip to next line
542            }
543
544            // If we have a pending annotation and a complete declaration, apply it
545            if should_check_annotation {
546                if let Some(annotation) = pending_annotation.take() {
547                    debug_println!("DEBUG SAFETY: Applying {:?} annotation to: {}", annotation, &accumulated_line);
548                    // Check what kind of code element follows
549                    if accumulated_line.starts_with("namespace") ||
550                       (accumulated_line.contains("namespace") && !accumulated_line.contains("using")) {
551                        // Namespace declaration - applies to whole namespace contents
552                        context.file_default = annotation;
553                        debug_println!("DEBUG SAFETY: Set file default to {:?} (namespace)", annotation);
554                        // Also push namespace to context stack for qualifying nested function annotations
555                        if let Some(ns_name) = extract_namespace_name(&accumulated_line) {
556                            debug_println!("DEBUG SAFETY: Entering annotated namespace '{}' for context", ns_name);
557                            class_context_stack.push(ns_name);
558                            brace_depth = accumulated_line.matches('{').count() as i32
559                                        - accumulated_line.matches('}').count() as i32;
560                        }
561                    } else if is_class_declaration(&accumulated_line) {
562                        // Class/struct declaration - extract class name and store annotation
563                        if let Some(class_name) = extract_class_name(&accumulated_line) {
564                            // Bug #8 fix: Build qualified class name using context
565                            let qualified_name = if class_context_stack.is_empty() {
566                                class_name.clone()
567                            } else {
568                                format!("{}::{}", class_context_stack.join("::"), class_name)
569                            };
570                            let signature = FunctionSignature::from_name_only(qualified_name.clone());
571                            context.function_overrides.push((signature, annotation));
572                            debug_println!("DEBUG SAFETY: Set class '{}' to {:?}", qualified_name, annotation);
573
574                            // Push class to context for nested methods
575                            // Only push if the class is NOT complete on the same line
576                            let class_brace_depth = accumulated_line.matches('{').count() as i32
577                                        - accumulated_line.matches('}').count() as i32;
578                            if class_brace_depth > 0 {
579                                class_context_stack.push(class_name.clone());
580                                brace_depth = class_brace_depth;
581                            }
582                        }
583                    } else if is_function_declaration(&accumulated_line) {
584                        // Function declaration - extract function signature (name + params) and apply ONLY to this function
585                        if let Some(func_name) = extract_function_name(&accumulated_line) {
586                            // Bug #8 fix: Build qualified function name using class context
587                            let qualified_name = if class_context_stack.is_empty() {
588                                func_name.clone()
589                            } else {
590                                format!("{}::{}", class_context_stack.join("::"), func_name)
591                            };
592                            let param_types = extract_parameter_types(&accumulated_line);
593                            let signature = FunctionSignature::new(qualified_name.clone(), param_types.clone());
594                            context.function_overrides.push((signature, annotation));
595
596                            if let Some(ref params) = param_types {
597                                debug_println!("DEBUG SAFETY: Set function '{}({})' to {:?}",
598                                             qualified_name, params.join(", "), annotation);
599                            } else {
600                                debug_println!("DEBUG SAFETY: Set function '{}' to {:?}", qualified_name, annotation);
601                            }
602                        }
603                    } else {
604                        // Any other code - annotation was consumed but doesn't apply to whole file
605                        // It only applied to this single statement/declaration
606                        debug_println!("DEBUG SAFETY: Annotation consumed by single statement: {}", &accumulated_line);
607                    }
608                    accumulated_line.clear();
609                    accumulating_for_annotation = false;
610                }
611            }
612        }
613    }
614    
615    Ok(context)
616}
617
618/// Check if a line looks like a class/struct declaration
619fn is_class_declaration(line: &str) -> bool {
620    // Check if line contains class/struct keyword (at start or with space before)
621    let has_class = line.starts_with("class ") || line.starts_with("struct ") ||
622                    line.contains(" class ") || line.contains(" struct ");
623    // Check if line contains opening brace (may be after newlines in accumulated_line)
624    let has_brace = line.contains('{');
625    has_class && has_brace
626}
627
628/// Check if a line is a forward declaration (class/struct with ; but no {)
629/// Forward declarations should not have annotations applied to them
630fn is_forward_declaration(line: &str) -> bool {
631    let has_class_or_struct = line.starts_with("class ") || line.starts_with("struct ") ||
632                              line.contains(" class ") || line.contains(" struct ");
633    let has_semicolon = line.trim_end().ends_with(';');
634    let has_brace = line.contains('{');
635
636    // Must have class/struct keyword, must end with semicolon, must NOT have opening brace
637    has_class_or_struct && has_semicolon && !has_brace
638}
639
640/// Extract class name from a class/struct declaration
641fn extract_class_name(line: &str) -> Option<String> {
642    // Look for "class ClassName" or "struct StructName"
643    // Handle multi-line declarations by replacing newlines with spaces
644    let normalized = line.replace('\n', " ").replace('\r', " ");
645
646    // Try to find "class " or "struct " - prioritize start of line to avoid matching "friend class"
647    // Check patterns in priority order: start first, then middle
648    let class_patterns = [
649        ("class ", "class "),      // "class " at the start (highest priority)
650        ("struct ", "struct "),    // "struct " at the start
651        (" class ", " class "),    // " class " in the middle (lower priority)
652        (" struct ", " struct "),  // " struct " in the middle
653    ];
654
655    for (search_pattern, keyword) in &class_patterns {
656        if let Some(pos) = normalized.find(search_pattern) {
657            let after_keyword = &normalized[pos + keyword.len()..];
658            // Class name is the first word after "class" or "struct"
659            let parts: Vec<&str> = after_keyword.split_whitespace().collect();
660            if let Some(name) = parts.first() {
661                // Remove any template parameters or inheritance markers
662                let name = name.split('<').next().unwrap_or(name);
663                let name = name.split(':').next().unwrap_or(name);
664                let name = name.split('{').next().unwrap_or(name);
665                // Sanity check: the extracted name shouldn't be "rusty" (that's from "friend class rusty::Arc")
666                // This is a workaround for accumulated_line containing the full class body
667                if name != "rusty" && name != "Arc" && name != "std" && !name.is_empty() {
668                    return Some(name.to_string());
669                }
670            }
671        }
672    }
673    None
674}
675
676/// Extract namespace name from a namespace declaration
677fn extract_namespace_name(line: &str) -> Option<String> {
678    // Look for "namespace Name {"
679    // Handle multi-line declarations by replacing newlines with spaces
680    let normalized = line.replace('\n', " ").replace('\r', " ");
681
682    // Find "namespace " keyword
683    if let Some(pos) = normalized.find("namespace ") {
684        let after_keyword = &normalized[pos + "namespace ".len()..];
685        // Namespace name is the first word after "namespace"
686        let parts: Vec<&str> = after_keyword.split_whitespace().collect();
687        if let Some(name) = parts.first() {
688            // Remove opening brace if attached
689            let name = name.split('{').next().unwrap_or(name);
690            if !name.is_empty() {
691                return Some(name.to_string());
692            }
693        }
694    }
695    None
696}
697
698/// Check if a line looks like a function declaration
699fn is_function_declaration(line: &str) -> bool {
700    // First check if it's a template class/struct declaration (NOT a function)
701    // Pattern: "template<...> class ..." or "template<...> struct ..."
702    // We need to check if "class" or "struct" appears AFTER the template parameters (after '>')
703    if line.starts_with("template") || line.contains(" template") {
704        // Find the position of "template<" and match its closing '>'
705        if let Some(template_pos) = line.find("template<") {
706            // Find the matching '>' for "template<"
707            let mut depth = 0;
708            let mut template_end = None;
709            for (i, ch) in line[template_pos..].chars().enumerate() {
710                if ch == '<' {
711                    depth += 1;
712                } else if ch == '>' {
713                    depth -= 1;
714                    if depth == 0 {
715                        template_end = Some(template_pos + i);
716                        break;
717                    }
718                }
719            }
720
721            // Check what comes after the template parameters
722            if let Some(end_pos) = template_end {
723                let after_template = &line[end_pos + 1..].trim_start();
724                // Only return false if "class " or "struct " appears right after template params
725                if after_template.starts_with("class ") || after_template.starts_with("struct ") {
726                    return false;
727                }
728            }
729        }
730    }
731
732    // Simple heuristic - contains parentheses and common return types
733    // This is simplified and could be improved
734    let has_parens = line.contains('(') && line.contains(')');
735    let has_type = line.contains("void") || line.contains("int") ||
736                   line.contains("bool") || line.contains("auto") ||
737                   line.contains("const") || line.contains("static");
738
739    // Also recognize template functions: they start with a template parameter like "T " or "U "
740    // or contain template syntax
741    let is_template_function = {
742        // Check if line starts with a single capital letter followed by space (template param)
743        let trimmed = line.trim_start();
744        let starts_with_template_param = trimmed.len() >= 2 &&
745            trimmed.chars().next().map_or(false, |c| c.is_uppercase()) &&
746            trimmed.chars().nth(1) == Some(' ');
747
748        // Or contains template-related keywords/syntax
749        let has_template_syntax = line.contains("template") || line.contains('<') || line.contains('>');
750
751        starts_with_template_param || (has_template_syntax && has_parens)
752    };
753
754    has_parens && (has_type || line.contains("::") || is_template_function)
755}
756
757/// Extract function name from a declaration line (including qualified names)
758fn extract_function_name(line: &str) -> Option<String> {
759    // Find the function name before the opening parenthesis
760    if let Some(paren_pos) = line.find('(') {
761        let before_paren = &line[..paren_pos];
762        // Split by whitespace and get the last identifier (which may be qualified)
763        let parts: Vec<&str> = before_paren.split_whitespace().collect();
764        if let Some(last) = parts.last() {
765            // Remove any qualifiers like * or & but keep :: for qualified names
766            let name = last.trim_start_matches('*').trim_start_matches('&');
767            if !name.is_empty() {
768                // Check if THIS part (the last token) contains "::"
769                // If it does, it's a qualified method name like "MyClass::myMethod"
770                // If it doesn't but the line has "::", the "::" is in the return type
771                if name.contains("::") {
772                    // This is a qualified method name (e.g., "MyClass::myMethod")
773                    return Some(name.to_string());
774                }
775                // Otherwise, return the simple function name
776                return Some(name.to_string());
777            }
778        }
779    }
780    None
781}
782
783/// Extract parameter types from a function declaration line
784/// Returns None if parameters cannot be extracted, Some(Vec) otherwise
785fn extract_parameter_types(line: &str) -> Option<Vec<String>> {
786    // Find the opening and closing parentheses
787    let open_paren = line.find('(')?;
788
789    // Find the matching closing parenthesis
790    let mut depth = 0;
791    let mut close_paren = None;
792    for (i, ch) in line[open_paren..].chars().enumerate() {
793        if ch == '(' {
794            depth += 1;
795        } else if ch == ')' {
796            depth -= 1;
797            if depth == 0 {
798                close_paren = Some(open_paren + i);
799                break;
800            }
801        }
802    }
803
804    let close_paren = close_paren?;
805    let params_str = &line[open_paren + 1..close_paren].trim();
806
807    // Empty parameter list
808    if params_str.is_empty() {
809        return Some(Vec::new());
810    }
811
812    // Split parameters by comma (handling nested templates and parentheses)
813    let mut params = Vec::new();
814    let mut current_param = String::new();
815    let mut angle_depth = 0;
816    let mut paren_depth = 0;
817
818    for ch in params_str.chars() {
819        match ch {
820            '<' => {
821                angle_depth += 1;
822                current_param.push(ch);
823            }
824            '>' => {
825                angle_depth -= 1;
826                current_param.push(ch);
827            }
828            '(' => {
829                paren_depth += 1;
830                current_param.push(ch);
831            }
832            ')' => {
833                paren_depth -= 1;
834                current_param.push(ch);
835            }
836            ',' if angle_depth == 0 && paren_depth == 0 => {
837                // Found a parameter separator
838                if !current_param.trim().is_empty() {
839                    params.push(normalize_param_type(&current_param));
840                }
841                current_param.clear();
842            }
843            _ => {
844                current_param.push(ch);
845            }
846        }
847    }
848
849    // Don't forget the last parameter
850    if !current_param.trim().is_empty() {
851        params.push(normalize_param_type(&current_param));
852    }
853
854    Some(params)
855}
856
857/// Normalize a parameter type for comparison
858/// Removes parameter names, extra whitespace, and standardizes formatting
859fn normalize_param_type(param: &str) -> String {
860    let trimmed = param.trim();
861
862    // Remove default values (everything after '=')
863    let without_default = if let Some(eq_pos) = trimmed.find('=') {
864        &trimmed[..eq_pos]
865    } else {
866        trimmed
867    };
868
869    // Split by whitespace to handle "const Type&" or "Type *" etc.
870    let tokens: Vec<&str> = without_default.split_whitespace().collect();
871
872    if tokens.is_empty() {
873        return String::new();
874    }
875
876    // Find the type part (everything except the last token if it looks like a variable name)
877    // Heuristic: if last token has no special chars and previous token has <, >, ::, *, or &,
878    // it's probably a variable name
879    let type_tokens = if tokens.len() > 1 {
880        let last = tokens.last().unwrap();
881        let second_last = tokens[tokens.len() - 2];
882
883        // If last token looks like a variable name (no special chars) and
884        // second-to-last has type characters, drop the last token
885        if !last.contains('<') && !last.contains('>') && !last.contains("::") &&
886           !last.contains('*') && !last.contains('&') &&
887           (second_last.contains('<') || second_last.contains('>') ||
888            second_last.contains("::") || second_last.contains('*') ||
889            second_last.contains('&')) {
890            &tokens[..tokens.len() - 1]
891        } else {
892            &tokens[..]
893        }
894    } else {
895        &tokens[..]
896    };
897
898    // Join tokens with single space
899    type_tokens.join(" ")
900}
901
902/// Extract qualified function name (e.g., "MyClass::myMethod") from a declaration
903fn extract_qualified_function_name(before_paren: &str) -> Option<String> {
904    // Look for the pattern "ClassName::methodName" 
905    // This could be preceded by return type and other qualifiers
906    let parts: Vec<&str> = before_paren.split_whitespace().collect();
907    
908    for part in parts.iter().rev() {
909        if part.contains("::") {
910            // This is likely our qualified name
911            let clean_name = part.trim_start_matches('*').trim_start_matches('&');
912            return Some(clean_name.to_string());
913        }
914    }
915    
916    None
917}
918
919/// Parse safety annotation from entity comment (for clang AST)
920/// Bug fix: Only match @safe/@unsafe at the START of comment lines (or after prefix like //, /*, *)
921/// This prevents false matches like "No @safe annotation" being treated as @safe
922#[allow(dead_code)]
923pub fn parse_entity_safety(entity: &Entity) -> Option<SafetyMode> {
924    if let Some(comment) = entity.get_comment() {
925        // Parse each line of the comment and check for annotations at the start
926        for line in comment.lines() {
927            let trimmed = line.trim();
928            // Remove common comment prefixes
929            let content = if trimmed.starts_with("///") {
930                trimmed[3..].trim()
931            } else if trimmed.starts_with("//") {
932                trimmed[2..].trim()
933            } else if trimmed.starts_with("/*") {
934                trimmed[2..].trim()
935            } else if trimmed.starts_with("*") {
936                trimmed[1..].trim()
937            } else {
938                trimmed
939            };
940
941            // Use contains_annotation to properly check for annotations at start of line
942            if contains_annotation(content, "@safe") {
943                return Some(SafetyMode::Safe);
944            } else if contains_annotation(content, "@unsafe") {
945                return Some(SafetyMode::Unsafe);
946            }
947        }
948        None
949    } else {
950        None
951    }
952}
953
954/// Parse class annotation from entity comment (for clang AST)
955/// Returns @interface, @safe, or @unsafe annotation for a class
956#[allow(dead_code)]
957pub fn parse_class_annotation(entity: &Entity) -> Option<ClassAnnotation> {
958    if let Some(comment) = entity.get_comment() {
959        // Parse each line of the comment and check for annotations at the start
960        for line in comment.lines() {
961            let trimmed = line.trim();
962            // Remove common comment prefixes
963            let content = if trimmed.starts_with("///") {
964                trimmed[3..].trim()
965            } else if trimmed.starts_with("//") {
966                trimmed[2..].trim()
967            } else if trimmed.starts_with("/*") {
968                trimmed[2..].trim()
969            } else if trimmed.starts_with("*") {
970                trimmed[1..].trim()
971            } else {
972                trimmed
973            };
974
975            // Check for @interface first (more specific)
976            if contains_annotation(content, "@interface") {
977                return Some(ClassAnnotation::Interface);
978            } else if contains_annotation(content, "@safe") {
979                return Some(ClassAnnotation::Safe);
980            } else if contains_annotation(content, "@unsafe") {
981                return Some(ClassAnnotation::Unsafe);
982            }
983        }
984        None
985    } else {
986        None
987    }
988}
989
990/// Check if a class is marked as @interface by reading source file comments
991/// This is needed when libclang's get_comment() doesn't capture the annotation
992pub fn check_class_interface_annotation(entity: &Entity) -> bool {
993    use std::fs::File;
994    use std::io::{BufRead, BufReader};
995
996    // Try get_comment() first
997    if let Some(comment) = entity.get_comment() {
998        for line in comment.lines() {
999            let trimmed = line.trim();
1000            let content = if trimmed.starts_with("///") {
1001                trimmed[3..].trim()
1002            } else if trimmed.starts_with("//") {
1003                trimmed[2..].trim()
1004            } else if trimmed.starts_with("/*") {
1005                trimmed[2..].trim()
1006            } else if trimmed.starts_with("*") {
1007                trimmed[1..].trim()
1008            } else {
1009                trimmed
1010            };
1011            if contains_annotation(content, "@interface") {
1012                return true;
1013            }
1014        }
1015    }
1016
1017    // Fall back to reading source file directly
1018    let location = match entity.get_location() {
1019        Some(loc) => loc,
1020        None => return false,
1021    };
1022
1023    let file_location = location.get_file_location();
1024    let file = match file_location.file {
1025        Some(f) => f,
1026        None => return false,
1027    };
1028
1029    let file_path = file.get_path();
1030    let entity_line = file_location.line as usize;
1031
1032    let file_handle = match File::open(&file_path) {
1033        Ok(f) => f,
1034        Err(_) => return false,
1035    };
1036
1037    let reader = BufReader::new(file_handle);
1038    let mut prev_line = String::new();
1039    let mut current_line = 0;
1040
1041    for line_result in reader.lines() {
1042        current_line += 1;
1043        let line = match line_result {
1044            Ok(l) => l,
1045            Err(_) => continue,
1046        };
1047
1048        if current_line == entity_line {
1049            // Check if previous line has @interface
1050            let trimmed = prev_line.trim();
1051            if trimmed.starts_with("//") {
1052                let content = trimmed[2..].trim();
1053                if contains_annotation(content, "@interface") {
1054                    return true;
1055                }
1056            }
1057            return false;
1058        }
1059
1060        prev_line = line;
1061    }
1062
1063    false
1064}
1065
1066/// Check method safety annotation by reading source file comments
1067/// This is needed when libclang's get_comment() doesn't capture the annotation
1068/// for methods inside a class definition
1069pub fn check_method_safety_annotation(entity: &Entity) -> Option<SafetyMode> {
1070    use std::fs::File;
1071    use std::io::{BufRead, BufReader};
1072
1073    // Try get_comment() first
1074    if let Some(comment) = entity.get_comment() {
1075        for line in comment.lines() {
1076            let trimmed = line.trim();
1077            let content = if trimmed.starts_with("///") {
1078                trimmed[3..].trim()
1079            } else if trimmed.starts_with("//") {
1080                trimmed[2..].trim()
1081            } else if trimmed.starts_with("/*") {
1082                trimmed[2..].trim()
1083            } else if trimmed.starts_with("*") {
1084                trimmed[1..].trim()
1085            } else {
1086                trimmed
1087            };
1088            if contains_annotation(content, "@safe") {
1089                return Some(SafetyMode::Safe);
1090            } else if contains_annotation(content, "@unsafe") {
1091                return Some(SafetyMode::Unsafe);
1092            }
1093        }
1094    }
1095
1096    // Fall back to reading source file directly
1097    let location = match entity.get_location() {
1098        Some(loc) => loc,
1099        None => return None,
1100    };
1101
1102    let file_location = location.get_file_location();
1103    let file = match file_location.file {
1104        Some(f) => f,
1105        None => return None,
1106    };
1107
1108    let file_path = file.get_path();
1109    let entity_line = file_location.line as usize;
1110
1111    let file_handle = match File::open(&file_path) {
1112        Ok(f) => f,
1113        Err(_) => return None,
1114    };
1115
1116    let reader = BufReader::new(file_handle);
1117    let mut prev_line = String::new();
1118    let mut current_line = 0;
1119
1120    for line_result in reader.lines() {
1121        current_line += 1;
1122        let line = match line_result {
1123            Ok(l) => l,
1124            Err(_) => continue,
1125        };
1126
1127        if current_line == entity_line {
1128            // Check if previous line has @safe or @unsafe
1129            let trimmed = prev_line.trim();
1130            if trimmed.starts_with("//") {
1131                let content = trimmed[2..].trim();
1132                if contains_annotation(content, "@safe") {
1133                    return Some(SafetyMode::Safe);
1134                } else if contains_annotation(content, "@unsafe") {
1135                    return Some(SafetyMode::Unsafe);
1136                }
1137            }
1138            return None;
1139        }
1140
1141        prev_line = line;
1142    }
1143
1144    None
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149    use super::*;
1150    use tempfile::NamedTempFile;
1151    use std::io::Write;
1152    
1153    #[test]
1154    fn test_namespace_safe_annotation() {
1155        let code = r#"
1156// @safe
1157namespace myapp {
1158    void func1() {}
1159    void func2() {}
1160}
1161"#;
1162
1163        let mut file = NamedTempFile::with_suffix(".cpp").unwrap();
1164        file.write_all(code.as_bytes()).unwrap();
1165        file.flush().unwrap();
1166
1167        let context = parse_safety_annotations(file.path()).unwrap();
1168        assert_eq!(context.file_default, SafetyMode::Safe);
1169    }
1170    
1171    #[test]
1172    fn test_function_safe_annotation() {
1173        let code = r#"
1174// Default is unsafe
1175void unsafe_func() {}
1176
1177// @safe
1178void safe_func() {
1179    int x = 42;
1180}
1181
1182// @unsafe
1183void explicit_unsafe() {}
1184"#;
1185        
1186        let mut file = NamedTempFile::with_suffix(".cpp").unwrap();
1187        file.write_all(code.as_bytes()).unwrap();
1188        file.flush().unwrap();
1189        
1190        let context = parse_safety_annotations(file.path()).unwrap();
1191        
1192        assert!(!context.should_check_function("unsafe_func"));
1193        assert!(context.should_check_function("safe_func"));
1194        assert!(!context.should_check_function("explicit_unsafe"));
1195    }
1196    
1197    #[test]
1198    fn test_first_code_element_annotation() {
1199        let code = r#"
1200// @safe
1201int global_var = 42;
1202
1203void func() {}
1204"#;
1205        
1206        let mut file = NamedTempFile::with_suffix(".cpp").unwrap();
1207        file.write_all(code.as_bytes()).unwrap();
1208        file.flush().unwrap();
1209        
1210        let context = parse_safety_annotations(file.path()).unwrap();
1211        // @safe only applies to the next element (global_var), not the whole file
1212        assert_eq!(context.file_default, SafetyMode::Unsafe);
1213    }
1214}