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