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