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