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        Ok(collector.facts)
64    }
65}
66
67struct Collector<'source> {
68    path: &'source str,
69    facts: FileFacts,
70    owner: OwnerScope,
71    test_context: bool,
72    module_scope: ModuleScope,
73}
74
75fn is_public(visibility: &syn::Visibility) -> bool {
76    matches!(visibility, syn::Visibility::Public(_))
77}
78
79impl<'ast> Visit<'ast> for Collector<'_> {
80    fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
81        self.with_test_context(&node.attrs, |collector| {
82            let owner = collector.add_symbol(
83                &node.sig.ident,
84                NodeKind::Function,
85                node.span(),
86                is_public(&node.vis),
87            );
88            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
89                collector.add_attribute_endpoints(&node.attrs);
90                syn::visit::visit_item_fn(collector, node);
91            });
92        });
93    }
94
95    fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
96        self.with_test_context(&node.attrs, |collector| {
97            let type_name = collector.owner.type_name.clone();
98            let owner = collector.add_symbol(
99                &node.sig.ident,
100                NodeKind::Method,
101                node.span(),
102                is_public(&node.vis),
103            );
104            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
105                if let Some(type_name) = type_name {
106                    collector.add_reference(
107                        type_name,
108                        EdgeKind::References,
109                        false,
110                        node.sig.ident.span(),
111                    );
112                }
113                collector.add_attribute_endpoints(&node.attrs);
114                syn::visit::visit_impl_item_fn(collector, node);
115            });
116        });
117    }
118
119    fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
120        self.with_test_context(&node.attrs, |collector| {
121            let owner = collector.add_symbol(&node.sig.ident, NodeKind::Method, node.span(), false);
122            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
123                syn::visit::visit_trait_item_fn(collector, node);
124            });
125        });
126    }
127
128    fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) {
129        self.with_test_context(&node.attrs, |collector| {
130            collector.add_symbol(
131                &node.ident,
132                NodeKind::Struct,
133                node.span(),
134                is_public(&node.vis),
135            );
136            syn::visit::visit_item_struct(collector, node);
137        });
138    }
139
140    fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
141        self.with_test_context(&node.attrs, |collector| {
142            collector.add_symbol(
143                &node.ident,
144                NodeKind::Enum,
145                node.span(),
146                is_public(&node.vis),
147            );
148            syn::visit::visit_item_enum(collector, node);
149        });
150    }
151
152    fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
153        self.with_test_context(&node.attrs, |collector| {
154            collector.add_symbol(
155                &node.ident,
156                NodeKind::Trait,
157                node.span(),
158                is_public(&node.vis),
159            );
160            collector.with_owner(OwnerUpdate::Type(node.ident.to_string()), |collector| {
161                syn::visit::visit_item_trait(collector, node);
162            });
163        });
164    }
165
166    fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
167        self.with_test_context(&node.attrs, |collector| {
168            if let Some(owner) = impl_owner(&node.self_ty) {
169                collector.with_owner(OwnerUpdate::Type(owner), |collector| {
170                    syn::visit::visit_item_impl(collector, node);
171                });
172            } else {
173                syn::visit::visit_item_impl(collector, node);
174            }
175        });
176    }
177
178    fn visit_item_type(&mut self, node: &'ast syn::ItemType) {
179        self.with_test_context(&node.attrs, |collector| {
180            collector.add_symbol(
181                &node.ident,
182                NodeKind::TypeAlias,
183                node.span(),
184                is_public(&node.vis),
185            );
186            syn::visit::visit_item_type(collector, node);
187        });
188    }
189
190    fn visit_item_const(&mut self, node: &'ast syn::ItemConst) {
191        self.with_test_context(&node.attrs, |collector| {
192            collector.add_symbol(
193                &node.ident,
194                NodeKind::Constant,
195                node.span(),
196                is_public(&node.vis),
197            );
198            syn::visit::visit_item_const(collector, node);
199        });
200    }
201
202    fn visit_item_static(&mut self, node: &'ast syn::ItemStatic) {
203        self.with_test_context(&node.attrs, |collector| {
204            collector.add_symbol(
205                &node.ident,
206                NodeKind::Static,
207                node.span(),
208                is_public(&node.vis),
209            );
210            syn::visit::visit_item_static(collector, node);
211        });
212    }
213
214    fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
215        self.with_test_context(&node.attrs, |collector| {
216            collector.add_symbol(
217                &node.ident,
218                NodeKind::Module,
219                node.span(),
220                is_public(&node.vis),
221            );
222            if node.content.is_some() {
223                collector.module_scope.enter(node.ident.to_string());
224            } else {
225                // `mod x;` pulls in x.rs or x/mod.rs; keep those files reachable.
226                let target = collector
227                    .module_scope
228                    .declared_target(&node.ident.to_string());
229                collector.facts.imports.push(ImportFact::new(
230                    target,
231                    source_span(collector.path, node.span()),
232                ));
233            }
234            syn::visit::visit_item_mod(collector, node);
235            if node.content.is_some() {
236                collector.module_scope.leave();
237            }
238        });
239    }
240
241    fn visit_item_use(&mut self, node: &'ast syn::ItemUse) {
242        for target in use_tree_targets(&node.tree) {
243            let target = self.module_scope.target(&target);
244            let fact = ImportFact::new(target, source_span(self.path, node.span()));
245            if matches!(node.vis, syn::Visibility::Inherited) {
246                self.facts.imports.push(fact);
247            } else {
248                self.facts.reexports.push(fact);
249            }
250        }
251        syn::visit::visit_item_use(self, node);
252    }
253
254    fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
255        self.collect_call(node);
256        syn::visit::visit_expr_call(self, node);
257    }
258
259    fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
260        self.collect_method_call(node);
261        syn::visit::visit_expr_method_call(self, node);
262    }
263
264    fn visit_expr_macro(&mut self, node: &'ast syn::ExprMacro) {
265        macro_calls::for_each_standard_argument(node, |argument| self.visit_expr(argument));
266        syn::visit::visit_expr_macro(self, node);
267    }
268
269    fn visit_type_path(&mut self, node: &'ast syn::TypePath) {
270        // Never bind a qualified path by its final segment alone.
271        if node.qself.is_none()
272            && node.path.segments.len() == 1
273            && let Some(segment) = node.path.segments.last()
274        {
275            self.add_reference(
276                segment.ident.to_string(),
277                EdgeKind::References,
278                false,
279                node.span(),
280            );
281        }
282        syn::visit::visit_type_path(self, node);
283    }
284}
285#[cfg(test)]
286mod tests;