rust_relations_explorer/parser/
mod.rs1use crate::errors::ParseError;
2use crate::graph::{FileMetrics, FileNode, Import, Item, ItemId, ItemType, Location, Visibility};
3use regex::Regex;
4use std::path::Path;
5use std::sync::Arc;
6
7#[derive(Debug, Default)]
8pub struct RustParser {
9 patterns: RegexPatterns,
10}
11
12#[derive(Debug)]
13pub struct RegexPatterns {
14 pub fn_sig: Regex,
15 pub struct_def: Regex,
16 pub enum_def: Regex,
17 pub vis_pub_in: Regex,
18 pub import_stmt: Regex,
19}
20
21impl RegexPatterns {
22 #[must_use]
27 pub fn compile() -> Self {
28 let fn_sig = Regex::new(r"(?m)^\s*(?P<vis>pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:const\s+)?fn\s+(?P<name>[a-zA-Z_][a-zA-Z0-9_]*)\s*\(").unwrap();
30 let struct_def = Regex::new(
31 r"(?m)^\s*(?P<vis>pub(?:\([^)]*\))?\s+)?struct\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*)",
32 )
33 .unwrap();
34 let enum_def = Regex::new(
35 r"(?m)^\s*(?P<vis>pub(?:\([^)]*\))?\s+)?enum\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*)",
36 )
37 .unwrap();
38 let vis_pub_in = Regex::new(r"^pub\((?P<sc>[^)]+)\)$").unwrap();
39 let import_stmt = Regex::new(
40 r"(?m)^\s*(?:pub\s+)?use\s+([^;{]+?)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?\s*;\s*$",
41 )
42 .unwrap();
43 Self { fn_sig, struct_def, enum_def, vis_pub_in, import_stmt }
44 }
45}
46
47impl Default for RegexPatterns {
48 fn default() -> Self {
49 Self::compile()
50 }
51}
52
53impl RustParser {
54 #[must_use]
55 pub fn new() -> Self {
56 Self { patterns: RegexPatterns::compile() }
57 }
58
59 pub fn parse_file(&self, content: &str, path: &Path) -> Result<FileNode, ParseError> {
64 let items = self.extract_items(content, path);
65 let imports = self.extract_imports(content);
66 let metrics = FileMetrics { item_count: items.len(), import_count: imports.len() };
67 Ok(FileNode { path: path.to_path_buf(), items, imports, metrics })
68 }
69
70 fn extract_items(&self, content: &str, path: &Path) -> Vec<Item> {
71 let fn_count = self.patterns.fn_sig.captures_iter(content).count();
73 let struct_count = self.patterns.struct_def.captures_iter(content).count();
74 let enum_count = self.patterns.enum_def.captures_iter(content).count();
75 let mut out = Vec::with_capacity(fn_count + struct_count + enum_count);
76
77 for cap in self.patterns.fn_sig.captures_iter(content) {
78 let name = Arc::from(cap.name("name").map_or("", |m| m.as_str()));
79 let vis = cap.name("vis").map_or("", |m| m.as_str().trim());
80 let visibility = parse_visibility(&self.patterns.vis_pub_in, vis);
81 let m0 = cap.get(0).unwrap();
82 let line = line_number_for(content, m0.start());
83 let span = m0.as_str();
84 out.push(Item {
85 id: ItemId(format!("fn:{name}:{line}")),
86 item_type: ItemType::Function {
87 is_async: span.contains("async "),
88 is_const: span.contains("const "),
89 },
90 name,
91 visibility,
92 location: Location { file: path.to_path_buf(), line_start: line, line_end: line },
93 attributes: vec![],
94 });
95 }
96
97 for cap in self.patterns.struct_def.captures_iter(content) {
98 let name = Arc::from(cap.name("name").map_or("", |m| m.as_str()));
99 let vis = cap.name("vis").map_or("", |m| m.as_str().trim());
100 let visibility = parse_visibility(&self.patterns.vis_pub_in, vis);
101 let line = line_number_for(content, cap.get(0).map_or(0, |m| m.start()));
102 out.push(Item {
103 id: ItemId(format!("struct:{name}:{line}")),
104 item_type: ItemType::Struct { is_tuple: false },
105 name,
106 visibility,
107 location: Location { file: path.to_path_buf(), line_start: line, line_end: line },
108 attributes: vec![],
109 });
110 }
111
112 for cap in self.patterns.enum_def.captures_iter(content) {
113 let name = Arc::from(cap.name("name").map_or("", |m| m.as_str()));
114 let vis = cap.name("vis").map_or("", |m| m.as_str().trim());
115 let visibility = parse_visibility(&self.patterns.vis_pub_in, vis);
116 let line = line_number_for(content, cap.get(0).map_or(0, |m| m.start()));
117 out.push(Item {
118 id: ItemId(format!("enum:{name}:{line}")),
119 item_type: ItemType::Enum { variant_count: 0 },
120 name,
121 visibility,
122 location: Location { file: path.to_path_buf(), line_start: line, line_end: line },
123 attributes: vec![],
124 });
125 }
126
127 out
128 }
129
130 fn extract_imports(&self, content: &str) -> Vec<Import> {
131 let import_count = self.patterns.import_stmt.captures_iter(content).count();
132 let mut out = Vec::with_capacity(import_count);
133 for cap in self.patterns.import_stmt.captures_iter(content) {
134 let path = Arc::from(cap.get(1).map_or("", |m| m.as_str().trim()));
135 let alias = cap.get(2).map(|m| Arc::from(m.as_str()));
136 out.push(Import { path, alias });
137 }
138 out
139 }
140}
141
142fn parse_visibility(vis_pub_in: &Regex, vis: &str) -> Visibility {
143 let v = vis.trim();
144 if v.is_empty() {
145 return Visibility::Private;
146 }
147 if v == "pub" {
148 return Visibility::Public;
149 }
150 if v == "pub(crate)" {
151 return Visibility::PubCrate;
152 }
153 if v == "pub(super)" {
154 return Visibility::PubSuper;
155 }
156 if let Some(c) = vis_pub_in.captures(v) {
157 return Visibility::PubIn(Arc::from(c.name("sc").map_or("", |m| m.as_str())));
158 }
159 Visibility::Private
160}
161
162fn line_number_for(content: &str, byte_idx: usize) -> usize {
163 content[..byte_idx].bytes().filter(|&b| b == b'\n').count() + 1
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_extract_fn_struct_enum_and_visibility() {
173 let src = r#"
174 pub fn top() {}
175 fn hidden() {}
176 pub(crate) struct S;
177 pub(super) enum E { A, B }
178 "#;
179 let parser = RustParser::new();
180 let file = std::path::Path::new("/tmp/test.rs");
181 let node = parser.parse_file(src, file).expect("parse");
182 assert_eq!(node.items.len(), 4);
184 let mut names: Vec<(String, Visibility)> =
186 node.items.iter().map(|i| (i.name.to_string(), i.visibility.clone())).collect();
187 names.sort_by(|a, b| a.0.cmp(&b.0));
188 assert!(names.iter().any(|(n, v)| n == "top" && matches!(v, Visibility::Public)));
189 assert!(names.iter().any(|(n, v)| n == "hidden" && matches!(v, Visibility::Private)));
190 assert!(names.iter().any(|(n, v)| n == "S" && matches!(v, Visibility::PubCrate)));
191 assert!(names.iter().any(|(n, v)| n == "E" && matches!(v, Visibility::PubSuper)));
192 }
193
194 #[test]
195 fn test_extract_imports_with_alias() {
196 let src = r#"
197 use std::collections::HashMap;
198 pub use crate::module::Thing as Alias;
199 "#;
200 let parser = RustParser::new();
201 let node = parser.parse_file(src, std::path::Path::new("/x.rs")).unwrap();
202 assert_eq!(node.imports.len(), 2);
203 assert!(node
204 .imports
205 .iter()
206 .any(|im| im.path.contains("std::collections::HashMap") && im.alias.is_none()));
207 assert!(node
208 .imports
209 .iter()
210 .any(|im| im.path.contains("crate::module::Thing")
211 && im.alias.as_deref() == Some("Alias")));
212 }
213
214 #[test]
215 fn test_async_const_functions_and_tuple_struct() {
216 let src = r#"
217 pub async fn af() {}
218 pub const fn cf() -> i32 { 0 }
219 pub struct TS(u32, u32);
220 pub(self) fn scoped() {}
221 "#;
222 let parser = RustParser::new();
223 let node = parser.parse_file(src, std::path::Path::new("/y.rs")).unwrap();
224 let names: Vec<_> = node.items.iter().map(|i| i.name.as_ref()).collect();
225 assert!(names.contains(&"af"));
226 assert!(names.contains(&"cf"));
227 assert!(names.contains(&"TS"));
228 let scoped =
230 node.items.iter().find(|i| i.name.as_ref() == "scoped").expect("scoped present");
231 match scoped.visibility {
232 Visibility::PubIn(ref s) => assert_eq!(s.as_ref(), "self"),
233 _ => panic!("expected Visibility::PubIn('self') for scoped"),
234 }
235 assert_eq!(node.items.len(), 4);
237 }
238}