Skip to main content

weavatrix_rust/language/rust/
mod.rs

1use super::{
2    DomainFact, FileFacts, ImportFact, Language, LanguageAdapter, ReferenceFact, SourceFile,
3    SymbolFact, SymbolLocator,
4};
5use crate::model::{Diagnostic, Result};
6use syn::spanned::Spanned;
7use syn::visit::Visit;
8use weavatrix_graph::{EdgeKind, NodeKind};
9
10use endpoints::{
11    associated_owner_name, attribute_routes, bare_path_name, call_is_locally_scoped, callable_name,
12    route_call,
13};
14use module_scope::{ModuleScope, OwnerScope, OwnerUpdate, sort_facts};
15use syntax::{attributes_mark_test, impl_owner, source_span, use_tree_targets};
16
17mod collector;
18mod endpoints;
19mod macro_calls;
20mod module_scope;
21mod syntax;
22
23#[derive(Debug, Clone, Copy)]
24pub struct RustAdapter;
25
26impl LanguageAdapter for RustAdapter {
27    fn language(&self) -> Language {
28        Language::Rust
29    }
30
31    fn extensions(&self) -> &'static [&'static str] {
32        &["rs"]
33    }
34
35    fn extractor(&self) -> &'static str {
36        "weavatrix.rust.syn"
37    }
38
39    fn parse(&self, source: SourceFile<'_>) -> Result<FileFacts> {
40        let syntax = match syn::parse_file(source.text) {
41            Ok(syntax) => syntax,
42            Err(error) => {
43                return Ok(FileFacts {
44                    diagnostics: vec![Diagnostic {
45                        code: "rust.syntax_error".into(),
46                        message: error.to_string(),
47                        span: Some(source_span(source.path, error.span())),
48                    }],
49                    ..FileFacts::default()
50                });
51            }
52        };
53
54        let mut collector = Collector {
55            path: source.path,
56            facts: FileFacts::default(),
57            owner: OwnerScope::default(),
58            test_context: false,
59            module_scope: ModuleScope::for_file(&syntax),
60        };
61        collector.visit_file(&syntax);
62        sort_facts(&mut collector.facts);
63        if let Some(regs) = super::agent::analyze_rust(source.path, source.text) {
64            collector.facts.symbols.extend(regs.symbols);
65            collector.facts.domains.extend(regs.domains);
66            collector.facts.diagnostics.extend(regs.diagnostics);
67        }
68        Ok(collector.facts)
69    }
70}
71
72struct Collector<'source> {
73    path: &'source str,
74    facts: FileFacts,
75    owner: OwnerScope,
76    test_context: bool,
77    module_scope: ModuleScope,
78}
79
80fn is_public(visibility: &syn::Visibility) -> bool {
81    matches!(visibility, syn::Visibility::Public(_))
82}
83
84impl<'ast> Visit<'ast> for Collector<'_> {
85    fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
86        self.with_test_context(&node.attrs, |collector| {
87            let owner = collector.add_symbol(
88                &node.sig.ident,
89                NodeKind::Function,
90                node.span(),
91                is_public(&node.vis),
92            );
93            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
94                collector.add_attribute_endpoints(&node.attrs);
95                syn::visit::visit_item_fn(collector, node);
96            });
97        });
98    }
99
100    fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
101        self.with_test_context(&node.attrs, |collector| {
102            let type_name = collector.owner.type_name.clone();
103            let owner = collector.add_symbol(
104                &node.sig.ident,
105                NodeKind::Method,
106                node.span(),
107                is_public(&node.vis),
108            );
109            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
110                if let Some(type_name) = type_name {
111                    collector.add_reference(
112                        type_name,
113                        EdgeKind::References,
114                        false,
115                        node.sig.ident.span(),
116                    );
117                }
118                collector.add_attribute_endpoints(&node.attrs);
119                syn::visit::visit_impl_item_fn(collector, node);
120            });
121        });
122    }
123
124    fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
125        self.with_test_context(&node.attrs, |collector| {
126            let owner = collector.add_symbol(&node.sig.ident, NodeKind::Method, node.span(), false);
127            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
128                syn::visit::visit_trait_item_fn(collector, node);
129            });
130        });
131    }
132
133    fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) {
134        self.with_test_context(&node.attrs, |collector| {
135            collector.add_symbol(
136                &node.ident,
137                NodeKind::Struct,
138                node.span(),
139                is_public(&node.vis),
140            );
141            syn::visit::visit_item_struct(collector, node);
142        });
143    }
144
145    fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
146        self.with_test_context(&node.attrs, |collector| {
147            collector.add_symbol(
148                &node.ident,
149                NodeKind::Enum,
150                node.span(),
151                is_public(&node.vis),
152            );
153            syn::visit::visit_item_enum(collector, node);
154        });
155    }
156
157    fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
158        self.with_test_context(&node.attrs, |collector| {
159            collector.add_symbol(
160                &node.ident,
161                NodeKind::Trait,
162                node.span(),
163                is_public(&node.vis),
164            );
165            collector.with_owner(OwnerUpdate::Type(node.ident.to_string()), |collector| {
166                syn::visit::visit_item_trait(collector, node);
167            });
168        });
169    }
170
171    fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
172        self.with_test_context(&node.attrs, |collector| {
173            if let Some(owner) = impl_owner(&node.self_ty) {
174                collector.with_owner(OwnerUpdate::Type(owner), |collector| {
175                    syn::visit::visit_item_impl(collector, node);
176                });
177            } else {
178                syn::visit::visit_item_impl(collector, node);
179            }
180        });
181    }
182
183    fn visit_item_type(&mut self, node: &'ast syn::ItemType) {
184        self.with_test_context(&node.attrs, |collector| {
185            collector.add_symbol(
186                &node.ident,
187                NodeKind::TypeAlias,
188                node.span(),
189                is_public(&node.vis),
190            );
191            syn::visit::visit_item_type(collector, node);
192        });
193    }
194
195    fn visit_item_const(&mut self, node: &'ast syn::ItemConst) {
196        self.with_test_context(&node.attrs, |collector| {
197            collector.add_symbol(
198                &node.ident,
199                NodeKind::Constant,
200                node.span(),
201                is_public(&node.vis),
202            );
203            syn::visit::visit_item_const(collector, node);
204        });
205    }
206
207    fn visit_item_static(&mut self, node: &'ast syn::ItemStatic) {
208        self.with_test_context(&node.attrs, |collector| {
209            collector.add_symbol(
210                &node.ident,
211                NodeKind::Static,
212                node.span(),
213                is_public(&node.vis),
214            );
215            syn::visit::visit_item_static(collector, node);
216        });
217    }
218
219    fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
220        self.with_test_context(&node.attrs, |collector| {
221            collector.add_symbol(
222                &node.ident,
223                NodeKind::Module,
224                node.span(),
225                is_public(&node.vis),
226            );
227            if node.content.is_some() {
228                collector.module_scope.enter(node.ident.to_string());
229            } else {
230                // `mod x;` pulls in x.rs or x/mod.rs; keep those files reachable.
231                let target = collector
232                    .module_scope
233                    .declared_target(&node.ident.to_string());
234                collector.facts.imports.push(ImportFact::new(
235                    target,
236                    source_span(collector.path, node.span()),
237                ));
238            }
239            syn::visit::visit_item_mod(collector, node);
240            if node.content.is_some() {
241                collector.module_scope.leave();
242            }
243        });
244    }
245
246    fn visit_item_use(&mut self, node: &'ast syn::ItemUse) {
247        for target in use_tree_targets(&node.tree) {
248            let target = self.module_scope.target(&target);
249            let fact = ImportFact::new(target, source_span(self.path, node.span()));
250            if matches!(node.vis, syn::Visibility::Inherited) {
251                self.facts.imports.push(fact);
252            } else {
253                self.facts.reexports.push(fact);
254            }
255        }
256        syn::visit::visit_item_use(self, node);
257    }
258
259    fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
260        self.collect_call(node);
261        syn::visit::visit_expr_call(self, node);
262    }
263
264    fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
265        self.collect_method_call(node);
266        syn::visit::visit_expr_method_call(self, node);
267    }
268
269    fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
270        macro_calls::for_each_standard_argument(node, |argument| self.visit_expr(argument));
271        syn::visit::visit_expr_macro(self, node);
272    }
273
274    fn visit_type_path(&mut self, node: &'ast syn::TypePath) {
275        // Never bind a qualified path by its final segment alone.
276        if node.qself.is_none()
277            && node.path.segments.len() == 1
278            && let Some(segment) = node.path.segments.last()
279        {
280            self.add_reference(
281                segment.ident.to_string(),
282                EdgeKind::References,
283                false,
284                node.span(),
285            );
286        }
287        syn::visit::visit_type_path(self, node);
288    }
289}
290#[cfg(test)]
291mod tests;