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