rusty_cpp/parser/
annotations.rs1use clang::Entity;
2use regex::Regex;
3
4#[derive(Debug, Clone, PartialEq)]
5pub enum LifetimeAnnotation {
6 Lifetime(String),
8 Ref(String),
10 MutRef(String),
12 Owned,
14}
15
16impl LifetimeAnnotation {
17 pub fn is_owned(&self) -> bool {
18 matches!(self, LifetimeAnnotation::Owned)
19 }
20}
21
22#[derive(Debug, Clone, PartialEq)]
23pub enum SafetyAnnotation {
24 Safe, Unsafe, }
27
28#[derive(Debug, Clone)]
29pub struct FunctionSignature {
30 pub name: String,
31 pub return_lifetime: Option<LifetimeAnnotation>,
32 pub param_lifetimes: Vec<Option<LifetimeAnnotation>>,
33 pub lifetime_bounds: Vec<LifetimeBound>, pub safety: Option<SafetyAnnotation>, }
36
37#[derive(Debug, Clone)]
38pub struct LifetimeBound {
39 pub longer: String, pub shorter: String, }
42
43pub fn extract_annotations(entity: &Entity) -> Option<FunctionSignature> {
44 let name = entity.get_name()?;
45
46 if let Some(comment) = entity.get_comment() {
48 if let Some(sig) = parse_lifetime_annotations(&comment, name.clone()) {
49 return Some(sig);
50 }
51 }
53
54 if let Some(sig) = read_lifetime_from_source(entity, &name) {
57 return Some(sig);
58 }
59
60 None
61}
62
63fn read_lifetime_from_source(entity: &Entity, name: &str) -> Option<FunctionSignature> {
65 use std::fs::File;
66 use std::io::{BufRead, BufReader};
67
68 let location = entity.get_location()?;
69 let file_location = location.get_file_location();
70 let file = file_location.file?;
71 let file_path = file.get_path();
72 let entity_line = file_location.line as usize;
73
74 let file_handle = File::open(&file_path).ok()?;
76 let reader = BufReader::new(file_handle);
77
78 let mut annotations = String::new();
80 let mut current_line = 0;
81
82 for line_result in reader.lines() {
83 current_line += 1;
84 let line = line_result.ok()?;
85
86 if current_line >= entity_line {
88 if !annotations.is_empty() {
90 if let Some(sig) = parse_lifetime_annotations(&annotations, name.to_string()) {
91 return Some(sig);
92 }
93 }
94 return None;
95 }
96
97 let trimmed = line.trim();
99 if trimmed.starts_with("//") {
100 annotations.push_str(&line);
101 annotations.push('\n');
102 } else if !trimmed.is_empty() && !trimmed.starts_with("/*") {
103 annotations.clear();
105 }
106 }
107
108 None
109}
110
111fn parse_lifetime_annotations(comment: &str, func_name: String) -> Option<FunctionSignature> {
116 let safe_re = Regex::new(r"@safe\b").ok()?;
118 let unsafe_re = Regex::new(r"@unsafe\b").ok()?;
119
120 let safety = if safe_re.is_match(comment) {
121 Some(SafetyAnnotation::Safe)
122 } else if unsafe_re.is_match(comment) {
123 Some(SafetyAnnotation::Unsafe)
124 } else {
125 None
126 };
127
128 let lifetime_re = Regex::new(r"@lifetime:\s*(.+)").ok()?;
130
131 if let Some(captures) = lifetime_re.captures(comment) {
133 let annotation_str = captures.get(1)?.as_str();
134
135 let mut signature = FunctionSignature {
137 name: func_name.clone(),
138 return_lifetime: None,
139 param_lifetimes: Vec::new(),
140 lifetime_bounds: Vec::new(),
141 safety,
142 };
143
144 let parts: Vec<&str> = annotation_str.split("where").collect();
146 let main_part = parts[0].trim();
147
148 if parts.len() > 1 {
150 let bounds_str = parts[1].trim();
151 signature.lifetime_bounds = parse_lifetime_bounds(bounds_str);
152 }
153
154 if main_part.contains("->") {
156 let arrow_parts: Vec<&str> = main_part.split("->").collect();
158 if arrow_parts.len() == 2 {
159 let params_str = arrow_parts[0].trim();
160 let return_str = arrow_parts[1].trim();
161
162 signature.param_lifetimes = parse_param_lifetimes(params_str);
164
165 signature.return_lifetime = parse_single_lifetime(return_str);
167 }
168 } else {
169 signature.return_lifetime = parse_single_lifetime(main_part);
171 }
172
173 Some(signature)
174 } else if safety.is_some() {
175 Some(FunctionSignature {
177 name: func_name,
178 return_lifetime: None,
179 param_lifetimes: Vec::new(),
180 lifetime_bounds: Vec::new(),
181 safety,
182 })
183 } else {
184 None
185 }
186}
187
188fn parse_param_lifetimes(params_str: &str) -> Vec<Option<LifetimeAnnotation>> {
189 let mut result = Vec::new();
190
191 let cleaned = params_str.trim_start_matches('(').trim_end_matches(')');
193
194 for param in cleaned.split(',') {
196 result.push(parse_single_lifetime(param.trim()));
197 }
198
199 result
200}
201
202fn parse_single_lifetime(lifetime_str: &str) -> Option<LifetimeAnnotation> {
203 let trimmed = lifetime_str.trim();
204
205 if trimmed == "owned" {
206 Some(LifetimeAnnotation::Owned)
207 } else if trimmed.starts_with("&'") && trimmed.contains("mut") {
208 let lifetime_name = extract_lifetime_name(trimmed);
210 lifetime_name.map(|name| LifetimeAnnotation::MutRef(name))
211 } else if trimmed.starts_with("&'") {
212 let lifetime_name = extract_lifetime_name(trimmed);
214 lifetime_name.map(|name| LifetimeAnnotation::Ref(name))
215 } else if trimmed.starts_with('\'') {
216 Some(LifetimeAnnotation::Lifetime(trimmed.to_string()))
218 } else if trimmed.contains("<&'") {
219 if trimmed.contains("mut") {
222 let lifetime_name = extract_lifetime_name(trimmed);
223 lifetime_name.map(|name| LifetimeAnnotation::MutRef(name))
224 } else {
225 let lifetime_name = extract_lifetime_name(trimmed);
226 lifetime_name.map(|name| LifetimeAnnotation::Ref(name))
227 }
228 } else {
229 None
230 }
231}
232
233fn extract_lifetime_name(s: &str) -> Option<String> {
234 let re = Regex::new(r"'([a-z][a-z0-9]*)").ok()?;
235 re.captures(s)
236 .and_then(|cap| cap.get(1))
237 .map(|m| m.as_str().to_string())
238}
239
240fn parse_lifetime_bounds(bounds_str: &str) -> Vec<LifetimeBound> {
241 let mut bounds = Vec::new();
242
243 let bound_re = Regex::new(r"'([a-z][a-z0-9]*)\s*:\s*'([a-z][a-z0-9]*)").unwrap();
245
246 for cap in bound_re.captures_iter(bounds_str) {
247 if let (Some(longer), Some(shorter)) = (cap.get(1), cap.get(2)) {
248 bounds.push(LifetimeBound {
249 longer: longer.as_str().to_string(),
250 shorter: shorter.as_str().to_string(),
251 });
252 }
253 }
254
255 bounds
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_parse_simple_lifetime() {
264 let comment = "// @lifetime: &'a";
265 let sig = parse_lifetime_annotations(comment, "test".to_string()).unwrap();
266
267 assert_eq!(sig.name, "test");
268 assert_eq!(sig.return_lifetime, Some(LifetimeAnnotation::Ref("a".to_string())));
269 assert!(sig.param_lifetimes.is_empty());
270 }
271
272 #[test]
273 fn test_parse_owned() {
274 let comment = "// @lifetime: owned";
275 let sig = parse_lifetime_annotations(comment, "test".to_string()).unwrap();
276
277 assert_eq!(sig.return_lifetime, Some(LifetimeAnnotation::Owned));
278 }
279
280 #[test]
281 fn test_parse_with_params() {
282 let comment = "// @lifetime: (&'a, &'b) -> &'a where 'a: 'b";
283 let sig = parse_lifetime_annotations(comment, "test".to_string()).unwrap();
284
285 assert_eq!(sig.param_lifetimes.len(), 2);
286 assert_eq!(sig.param_lifetimes[0], Some(LifetimeAnnotation::Ref("a".to_string())));
287 assert_eq!(sig.param_lifetimes[1], Some(LifetimeAnnotation::Ref("b".to_string())));
288 assert_eq!(sig.return_lifetime, Some(LifetimeAnnotation::Ref("a".to_string())));
289 assert_eq!(sig.lifetime_bounds.len(), 1);
290 assert_eq!(sig.lifetime_bounds[0].longer, "a");
291 assert_eq!(sig.lifetime_bounds[0].shorter, "b");
292 }
293
294 #[test]
295 fn test_parse_mut_ref() {
296 let comment = "// @lifetime: &'a mut";
297 let sig = parse_lifetime_annotations(comment, "test".to_string()).unwrap();
298
299 assert_eq!(sig.return_lifetime, Some(LifetimeAnnotation::MutRef("a".to_string())));
300 }
301}