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