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(
73        &mut self,
74        name: &syn::Ident,
75        kind: NodeKind,
76        definition_span: Span,
77    ) -> SymbolLocator {
78        let mut span = source_span(self.path, name.span());
79        span.end = source_span(self.path, definition_span).end;
80        let locator = SymbolLocator {
81            name: name.to_string(),
82            kind,
83            span,
84        };
85        self.facts.symbols.push(SymbolFact {
86            name: locator.name.clone(),
87            kind: locator.kind.clone(),
88            span: locator.span.clone(),
89            test_only: self.test_context,
90            owner: (locator.kind == NodeKind::Method)
91                .then(|| self.owner.type_name.clone())
92                .flatten(),
93        });
94        locator
95    }
96
97    fn with_owner(&mut self, update: OwnerUpdate, visit: impl FnOnce(&mut Self)) {
98        let previous = self.owner.apply(update);
99        visit(self);
100        self.owner = previous;
101    }
102
103    fn with_test_context(&mut self, attributes: &[syn::Attribute], visit: impl FnOnce(&mut Self)) {
104        let previous = self.test_context;
105        self.test_context |= attributes_mark_test(attributes);
106        visit(self);
107        self.test_context = previous;
108    }
109
110    fn add_reference(&mut self, name: String, kind: EdgeKind, qualified: bool, span: Span) {
111        self.facts.references.push(ReferenceFact {
112            name,
113            kind,
114            receiver: None,
115            qualified,
116            span: source_span(self.path, span),
117            owner: self.owner.symbol.clone(),
118        });
119    }
120
121    fn add_endpoint(&mut self, method: &str, path: &str, span: Span) {
122        self.facts.domains.push(DomainFact {
123            name: format!("{method} {path}"),
124            kind: NodeKind::Endpoint,
125            relation: EdgeKind::Exposes,
126            span: source_span(self.path, span),
127            owner: self.owner.symbol.clone(),
128        });
129    }
130
131    fn add_attribute_endpoints(&mut self, attributes: &[syn::Attribute]) {
132        for (method, path, span) in attribute_routes(attributes) {
133            self.add_endpoint(method, &path, span);
134        }
135    }
136}
137
138impl<'ast> Visit<'ast> for Collector<'_> {
139    fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
140        self.with_test_context(&node.attrs, |collector| {
141            let owner = collector.add_symbol(&node.sig.ident, NodeKind::Function, node.span());
142            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
143                collector.add_attribute_endpoints(&node.attrs);
144                syn::visit::visit_item_fn(collector, node);
145            });
146        });
147    }
148
149    fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
150        self.with_test_context(&node.attrs, |collector| {
151            let owner = collector.add_symbol(&node.sig.ident, NodeKind::Method, node.span());
152            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
153                collector.add_attribute_endpoints(&node.attrs);
154                syn::visit::visit_impl_item_fn(collector, node);
155            });
156        });
157    }
158
159    fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
160        self.with_test_context(&node.attrs, |collector| {
161            let owner = collector.add_symbol(&node.sig.ident, NodeKind::Method, node.span());
162            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
163                syn::visit::visit_trait_item_fn(collector, node);
164            });
165        });
166    }
167
168    fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) {
169        self.with_test_context(&node.attrs, |collector| {
170            collector.add_symbol(&node.ident, NodeKind::Struct, node.span());
171            syn::visit::visit_item_struct(collector, node);
172        });
173    }
174
175    fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
176        self.with_test_context(&node.attrs, |collector| {
177            collector.add_symbol(&node.ident, NodeKind::Enum, node.span());
178            syn::visit::visit_item_enum(collector, node);
179        });
180    }
181
182    fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
183        self.with_test_context(&node.attrs, |collector| {
184            collector.add_symbol(&node.ident, NodeKind::Trait, node.span());
185            collector.with_owner(OwnerUpdate::Type(node.ident.to_string()), |collector| {
186                syn::visit::visit_item_trait(collector, node);
187            });
188        });
189    }
190
191    fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
192        self.with_test_context(&node.attrs, |collector| {
193            if let Some(owner) = impl_owner(&node.self_ty) {
194                collector.with_owner(OwnerUpdate::Type(owner), |collector| {
195                    syn::visit::visit_item_impl(collector, node);
196                });
197            } else {
198                syn::visit::visit_item_impl(collector, node);
199            }
200        });
201    }
202
203    fn visit_item_type(&mut self, node: &'ast syn::ItemType) {
204        self.with_test_context(&node.attrs, |collector| {
205            collector.add_symbol(&node.ident, NodeKind::TypeAlias, node.span());
206            syn::visit::visit_item_type(collector, node);
207        });
208    }
209
210    fn visit_item_const(&mut self, node: &'ast syn::ItemConst) {
211        self.with_test_context(&node.attrs, |collector| {
212            collector.add_symbol(&node.ident, NodeKind::Constant, node.span());
213            syn::visit::visit_item_const(collector, node);
214        });
215    }
216
217    fn visit_item_static(&mut self, node: &'ast syn::ItemStatic) {
218        self.with_test_context(&node.attrs, |collector| {
219            collector.add_symbol(&node.ident, NodeKind::Static, node.span());
220            syn::visit::visit_item_static(collector, node);
221        });
222    }
223
224    fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
225        self.with_test_context(&node.attrs, |collector| {
226            collector.add_symbol(&node.ident, NodeKind::Module, node.span());
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. Without this edge those
231                // files look unreachable.
232                let target = collector
233                    .module_scope
234                    .target(&format!("self::{}", node.ident));
235                collector.facts.imports.push(ImportFact::new(
236                    target,
237                    source_span(collector.path, node.span()),
238                ));
239            }
240            syn::visit::visit_item_mod(collector, node);
241            if node.content.is_some() {
242                collector.module_scope.leave();
243            }
244        });
245    }
246
247    fn visit_item_use(&mut self, node: &'ast syn::ItemUse) {
248        for target in use_tree_targets(&node.tree) {
249            let target = self.module_scope.target(&target);
250            let fact = ImportFact::new(target, source_span(self.path, node.span()));
251            if matches!(node.vis, syn::Visibility::Inherited) {
252                self.facts.imports.push(fact);
253            } else {
254                self.facts.reexports.push(fact);
255            }
256        }
257        syn::visit::visit_item_use(self, node);
258    }
259
260    fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
261        if let Some(name) = callable_name(&node.func) {
262            self.add_reference(name, EdgeKind::Calls, false, node.span());
263        }
264        syn::visit::visit_expr_call(self, node);
265    }
266
267    fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
268        self.add_reference(node.method.to_string(), EdgeKind::Calls, false, node.span());
269        if node.method == "route" {
270            for (method, path) in route_call(node) {
271                self.add_endpoint(method, &path, node.span());
272            }
273        }
274        syn::visit::visit_expr_method_call(self, node);
275    }
276
277    fn visit_type_path(&mut self, node: &'ast syn::TypePath) {
278        // A final segment alone is not enough evidence for a qualified Rust
279        // path: `std::io::Result` must not bind to an unrelated local
280        // `Result`. Keep exact single-name type references now; path-aware
281        // module resolution can add qualified references without guessing.
282        if node.qself.is_none()
283            && node.path.segments.len() == 1
284            && let Some(segment) = node.path.segments.last()
285        {
286            self.add_reference(
287                segment.ident.to_string(),
288                EdgeKind::References,
289                false,
290                node.span(),
291            );
292        }
293        syn::visit::visit_type_path(self, node);
294    }
295}
296
297#[cfg(test)]
298mod tests;