ai_context_gen/
parser.rs

1//! Rust AST parsing module for the AI Context Generator.
2//!
3//! This module provides functionality to parse Rust source code and extract
4//! structural information such as modules, functions, structs, enums, and
5//! implementations using the `syn` crate.
6
7use anyhow::Result;
8use quote::ToTokens;
9use serde::{Deserialize, Serialize};
10use syn::{parse_file, Item, ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, Signature};
11
12/// Complete analysis result for a single Rust source file.
13///
14/// Contains all structural information extracted from parsing the file's AST,
15/// including modules, functions, structs, enums, and implementations.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct RustAnalysis {
18    /// Path to the analyzed file.
19    pub file_path: String,
20
21    /// List of modules defined in the file.
22    pub modules: Vec<ModuleInfo>,
23
24    /// List of functions defined in the file.
25    pub functions: Vec<FunctionInfo>,
26
27    /// List of structs defined in the file.
28    pub structs: Vec<StructInfo>,
29
30    /// List of enums defined in the file.
31    pub enums: Vec<EnumInfo>,
32
33    /// List of impl blocks defined in the file.
34    pub implementations: Vec<ImplInfo>,
35
36    /// Summary of the AST structure.
37    pub ast_summary: String,
38}
39
40/// Information about a module definition.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ModuleInfo {
43    /// Name of the module.
44    pub name: String,
45
46    /// Visibility modifier (pub, pub(crate), private, etc.).
47    pub visibility: String,
48
49    /// Number of items contained in the module.
50    pub items_count: usize,
51}
52
53/// Information about a function definition.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct FunctionInfo {
56    /// Name of the function.
57    pub name: String,
58
59    /// Visibility modifier (pub, pub(crate), private, etc.).
60    pub visibility: String,
61
62    /// Whether the function is async.
63    pub is_async: bool,
64
65    /// List of parameter types as strings.
66    pub parameters: Vec<String>,
67
68    /// Return type as a string, if any.
69    pub return_type: Option<String>,
70
71    /// Documentation comment, if present.
72    pub documentation: Option<String>,
73}
74
75/// Information about a struct definition.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct StructInfo {
78    /// Name of the struct.
79    pub name: String,
80
81    /// Visibility modifier (pub, pub(crate), private, etc.).
82    pub visibility: String,
83
84    /// List of fields in the struct.
85    pub fields: Vec<FieldInfo>,
86
87    /// Documentation comment, if present.
88    pub documentation: Option<String>,
89}
90
91/// Information about a struct field.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct FieldInfo {
94    /// Name of the field.
95    pub name: String,
96
97    /// Type of the field as a string.
98    pub field_type: String,
99
100    /// Visibility modifier for the field.
101    pub visibility: String,
102}
103
104/// Information about an enum definition.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct EnumInfo {
107    /// Name of the enum.
108    pub name: String,
109
110    /// Visibility modifier (pub, pub(crate), private, etc.).
111    pub visibility: String,
112
113    /// List of variant names.
114    pub variants: Vec<String>,
115
116    /// Documentation comment, if present.
117    pub documentation: Option<String>,
118}
119
120/// Information about an implementation block.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ImplInfo {
123    /// The type being implemented (e.g., "MyStruct", "Vec<T>").
124    pub target: String,
125
126    /// Name of the trait being implemented, if this is a trait impl.
127    pub trait_name: Option<String>,
128
129    /// List of methods defined in the implementation.
130    pub methods: Vec<FunctionInfo>,
131}
132
133/// Rust source code parser using syn for AST analysis.
134///
135/// Provides static methods for parsing Rust source files and extracting
136/// structural information about the code.
137pub struct RustParser;
138
139impl RustParser {
140    /// Parses a Rust source file and extracts structural information.
141    ///
142    /// This method uses the `syn` crate to parse Rust source code into an AST
143    /// and then extracts information about modules, functions, structs, enums,
144    /// and implementations.
145    ///
146    /// # Arguments
147    ///
148    /// * `file_path` - Path to the file being parsed (used for error reporting)
149    /// * `content` - Source code content as a string
150    ///
151    /// # Returns
152    ///
153    /// A `RustAnalysis` containing all extracted structural information.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the source code cannot be parsed as valid Rust syntax.
158    ///
159    /// # Examples
160    ///
161    /// ```rust
162    /// use ai_context_gen::parser::RustParser;
163    ///
164    /// let source = r#"
165    /// pub struct MyStruct {
166    ///     pub field: String,
167    /// }
168    ///
169    /// impl MyStruct {
170    ///     pub fn new() -> Self {
171    ///         Self { field: String::new() }
172    ///     }
173    /// }
174    /// "#;
175    ///
176    /// let analysis = RustParser::parse_rust_file("example.rs", source).unwrap();
177    /// assert_eq!(analysis.structs.len(), 1);
178    /// assert_eq!(analysis.implementations.len(), 1);
179    /// ```
180    pub fn parse_rust_file(file_path: &str, content: &str) -> Result<RustAnalysis> {
181        let syntax_tree = parse_file(content)?;
182
183        let mut analysis = RustAnalysis {
184            file_path: file_path.to_string(),
185            modules: Vec::new(),
186            functions: Vec::new(),
187            structs: Vec::new(),
188            enums: Vec::new(),
189            implementations: Vec::new(),
190            ast_summary: String::new(),
191        };
192
193        // Analyze each item in the file
194        for item in &syntax_tree.items {
195            match item {
196                Item::Mod(item_mod) => {
197                    analysis.modules.push(Self::parse_module(item_mod));
198                }
199                Item::Fn(item_fn) => {
200                    analysis.functions.push(Self::parse_function(item_fn));
201                }
202                Item::Struct(item_struct) => {
203                    analysis.structs.push(Self::parse_struct(item_struct));
204                }
205                Item::Enum(item_enum) => {
206                    analysis.enums.push(Self::parse_enum(item_enum));
207                }
208                Item::Impl(item_impl) => {
209                    analysis.implementations.push(Self::parse_impl(item_impl));
210                }
211                _ => {}
212            }
213        }
214
215        analysis.ast_summary = Self::generate_ast_summary(&analysis);
216
217        Ok(analysis)
218    }
219
220    fn parse_module(item: &ItemMod) -> ModuleInfo {
221        let items_count = item
222            .content
223            .as_ref()
224            .map(|(_, items)| items.len())
225            .unwrap_or(0);
226
227        ModuleInfo {
228            name: item.ident.to_string(),
229            visibility: Self::parse_visibility(&item.vis),
230            items_count,
231        }
232    }
233
234    fn parse_function(item: &ItemFn) -> FunctionInfo {
235        let sig = &item.sig;
236
237        FunctionInfo {
238            name: sig.ident.to_string(),
239            visibility: Self::parse_visibility(&item.vis),
240            is_async: sig.asyncness.is_some(),
241            parameters: Self::parse_parameters(sig),
242            return_type: Self::parse_return_type(sig),
243            documentation: Self::extract_doc_comments(&item.attrs),
244        }
245    }
246
247    fn parse_struct(item: &ItemStruct) -> StructInfo {
248        let fields = match &item.fields {
249            syn::Fields::Named(fields) => fields
250                .named
251                .iter()
252                .map(|f| FieldInfo {
253                    name: f.ident.as_ref().unwrap().to_string(),
254                    field_type: f.ty.to_token_stream().to_string(),
255                    visibility: Self::parse_visibility(&f.vis),
256                })
257                .collect(),
258            syn::Fields::Unnamed(fields) => fields
259                .unnamed
260                .iter()
261                .enumerate()
262                .map(|(i, f)| FieldInfo {
263                    name: format!("field_{i}"),
264                    field_type: f.ty.to_token_stream().to_string(),
265                    visibility: Self::parse_visibility(&f.vis),
266                })
267                .collect(),
268            syn::Fields::Unit => Vec::new(),
269        };
270
271        StructInfo {
272            name: item.ident.to_string(),
273            visibility: Self::parse_visibility(&item.vis),
274            fields,
275            documentation: Self::extract_doc_comments(&item.attrs),
276        }
277    }
278
279    fn parse_enum(item: &ItemEnum) -> EnumInfo {
280        let variants = item.variants.iter().map(|v| v.ident.to_string()).collect();
281
282        EnumInfo {
283            name: item.ident.to_string(),
284            visibility: Self::parse_visibility(&item.vis),
285            variants,
286            documentation: Self::extract_doc_comments(&item.attrs),
287        }
288    }
289
290    fn parse_impl(item: &ItemImpl) -> ImplInfo {
291        let target = item.self_ty.to_token_stream().to_string();
292        let trait_name = item
293            .trait_
294            .as_ref()
295            .map(|(_, path, _)| path.to_token_stream().to_string());
296
297        let methods = item
298            .items
299            .iter()
300            .filter_map(|item| {
301                if let syn::ImplItem::Fn(method) = item {
302                    Some(FunctionInfo {
303                        name: method.sig.ident.to_string(),
304                        visibility: Self::parse_visibility(&method.vis),
305                        is_async: method.sig.asyncness.is_some(),
306                        parameters: Self::parse_parameters(&method.sig),
307                        return_type: Self::parse_return_type(&method.sig),
308                        documentation: Self::extract_doc_comments(&method.attrs),
309                    })
310                } else {
311                    None
312                }
313            })
314            .collect();
315
316        ImplInfo {
317            target,
318            trait_name,
319            methods,
320        }
321    }
322
323    fn parse_visibility(vis: &syn::Visibility) -> String {
324        match vis {
325            syn::Visibility::Public(_) => "pub".to_string(),
326            syn::Visibility::Restricted(restricted) => {
327                format!("pub({})", restricted.path.to_token_stream())
328            }
329            syn::Visibility::Inherited => "private".to_string(),
330        }
331    }
332
333    fn parse_parameters(sig: &Signature) -> Vec<String> {
334        sig.inputs
335            .iter()
336            .map(|input| match input {
337                syn::FnArg::Receiver(receiver) => {
338                    if receiver.mutability.is_some() {
339                        "&mut self".to_string()
340                    } else {
341                        "&self".to_string()
342                    }
343                }
344                syn::FnArg::Typed(typed) => {
345                    format!(
346                        "{}: {}",
347                        typed.pat.to_token_stream(),
348                        typed.ty.to_token_stream()
349                    )
350                }
351            })
352            .collect()
353    }
354
355    fn parse_return_type(sig: &Signature) -> Option<String> {
356        match &sig.output {
357            syn::ReturnType::Default => None,
358            syn::ReturnType::Type(_, ty) => Some(ty.to_token_stream().to_string()),
359        }
360    }
361
362    fn extract_doc_comments(attrs: &[syn::Attribute]) -> Option<String> {
363        let mut doc_comments = Vec::new();
364
365        for attr in attrs {
366            if attr.path().is_ident("doc") {
367                if let Ok(syn::Lit::Str(lit_str)) = attr.parse_args() {
368                    doc_comments.push(lit_str.value());
369                }
370            }
371        }
372
373        if doc_comments.is_empty() {
374            None
375        } else {
376            Some(doc_comments.join("\n"))
377        }
378    }
379
380    fn generate_ast_summary(analysis: &RustAnalysis) -> String {
381        let mut summary = String::new();
382
383        summary.push_str(&format!("# AST Summary for {}\n\n", analysis.file_path));
384
385        if !analysis.modules.is_empty() {
386            summary.push_str("## Modules\n");
387            for module in &analysis.modules {
388                summary.push_str(&format!(
389                    "- `{}` ({}) - {} items\n",
390                    module.name, module.visibility, module.items_count
391                ));
392            }
393            summary.push('\n');
394        }
395
396        if !analysis.structs.is_empty() {
397            summary.push_str("## Structs\n");
398            for struct_info in &analysis.structs {
399                summary.push_str(&format!(
400                    "- `{}` ({}) - {} fields\n",
401                    struct_info.name,
402                    struct_info.visibility,
403                    struct_info.fields.len()
404                ));
405            }
406            summary.push('\n');
407        }
408
409        if !analysis.enums.is_empty() {
410            summary.push_str("## Enums\n");
411            for enum_info in &analysis.enums {
412                summary.push_str(&format!(
413                    "- `{}` ({}) - {} variants\n",
414                    enum_info.name,
415                    enum_info.visibility,
416                    enum_info.variants.len()
417                ));
418            }
419            summary.push('\n');
420        }
421
422        if !analysis.functions.is_empty() {
423            summary.push_str("## Functions\n");
424            for func in &analysis.functions {
425                let async_marker = if func.is_async { "async " } else { "" };
426                summary.push_str(&format!(
427                    "- `{}{}{}` ({})\n",
428                    async_marker,
429                    func.name,
430                    if func.parameters.is_empty() {
431                        "()"
432                    } else {
433                        "(...)"
434                    },
435                    func.visibility
436                ));
437            }
438            summary.push('\n');
439        }
440
441        if !analysis.implementations.is_empty() {
442            summary.push_str("## Implementations\n");
443            for impl_info in &analysis.implementations {
444                let trait_part = impl_info
445                    .trait_name
446                    .as_ref()
447                    .map(|t| format!("{t} for "))
448                    .unwrap_or_default();
449                summary.push_str(&format!("- `impl {}{}`\n", trait_part, impl_info.target));
450            }
451        }
452
453        summary
454    }
455}