Skip to main content

weavatrix_rust/language/
rust.rs

1use super::{
2    DomainFact, FileFacts, ImportFact, Language, LanguageAdapter, ReferenceFact, SourceFile,
3    SymbolFact, SymbolLocator,
4};
5use crate::error::Result;
6use crate::snapshot::Diagnostic;
7use proc_macro2::{LineColumn, Span};
8use syn::UseTree;
9use syn::spanned::Spanned;
10use syn::visit::Visit;
11use weavatrix_graph::{EdgeKind, NodeKind, SourcePosition, SourceSpan};
12
13use super::rust_endpoint::{attribute_routes, callable_name, route_call};
14
15#[derive(Debug, Clone, Copy)]
16pub struct RustAdapter;
17
18impl LanguageAdapter for RustAdapter {
19    fn language(&self) -> Language {
20        Language::Rust
21    }
22
23    fn extensions(&self) -> &'static [&'static str] {
24        &["rs"]
25    }
26
27    fn extractor(&self) -> &'static str {
28        "weavatrix.rust.syn"
29    }
30
31    fn parse(&self, source: SourceFile<'_>) -> Result<FileFacts> {
32        let syntax = match syn::parse_file(source.text) {
33            Ok(syntax) => syntax,
34            Err(error) => {
35                return Ok(FileFacts {
36                    diagnostics: vec![Diagnostic {
37                        code: "rust.syntax_error".into(),
38                        message: error.to_string(),
39                        span: Some(source_span(source.path, error.span())),
40                    }],
41                    ..FileFacts::default()
42                });
43            }
44        };
45
46        let mut collector = Collector {
47            path: source.path,
48            facts: FileFacts::default(),
49            owner: OwnerScope::default(),
50            test_context: false,
51        };
52        collector.visit_file(&syntax);
53        collector
54            .facts
55            .symbols
56            .sort_by(|left, right| left.span.cmp(&right.span));
57        collector
58            .facts
59            .references
60            .sort_by(|left, right| left.span.cmp(&right.span));
61        collector
62            .facts
63            .imports
64            .sort_by(|left, right| left.span.cmp(&right.span));
65        Ok(collector.facts)
66    }
67}
68
69struct Collector<'source> {
70    path: &'source str,
71    facts: FileFacts,
72    owner: OwnerScope,
73    test_context: bool,
74}
75
76#[derive(Clone, Default)]
77struct OwnerScope {
78    symbol: Option<SymbolLocator>,
79    type_name: Option<String>,
80}
81
82enum OwnerUpdate {
83    Symbol(SymbolLocator),
84    Type(String),
85}
86
87impl Collector<'_> {
88    fn add_symbol(&mut self, name: &syn::Ident, kind: NodeKind, span: Span) -> SymbolLocator {
89        let locator = SymbolLocator {
90            name: name.to_string(),
91            kind,
92            span: source_span(self.path, span),
93        };
94        self.facts.symbols.push(SymbolFact {
95            name: locator.name.clone(),
96            kind: locator.kind.clone(),
97            span: locator.span.clone(),
98            test_only: self.test_context,
99            owner: (locator.kind == NodeKind::Method)
100                .then(|| self.owner.type_name.clone())
101                .flatten(),
102        });
103        locator
104    }
105
106    fn with_owner(&mut self, update: OwnerUpdate, visit: impl FnOnce(&mut Self)) {
107        let previous = self.owner.clone();
108        match update {
109            OwnerUpdate::Symbol(owner) => self.owner.symbol = Some(owner),
110            OwnerUpdate::Type(owner) => self.owner.type_name = Some(owner),
111        }
112        visit(self);
113        self.owner = previous;
114    }
115
116    fn with_test_context(&mut self, attributes: &[syn::Attribute], visit: impl FnOnce(&mut Self)) {
117        let previous = self.test_context;
118        self.test_context |= attributes_mark_test(attributes);
119        visit(self);
120        self.test_context = previous;
121    }
122
123    fn add_reference(&mut self, name: String, span: Span) {
124        self.facts.references.push(ReferenceFact {
125            name,
126            kind: EdgeKind::Calls,
127            receiver: None,
128            qualified: false,
129            span: source_span(self.path, span),
130            owner: self.owner.symbol.clone(),
131        });
132    }
133
134    fn add_endpoint(&mut self, method: &str, path: &str, span: Span) {
135        self.facts.domains.push(DomainFact {
136            name: format!("{method} {path}"),
137            kind: NodeKind::Endpoint,
138            relation: EdgeKind::Exposes,
139            span: source_span(self.path, span),
140            owner: self.owner.symbol.clone(),
141        });
142    }
143
144    fn add_attribute_endpoints(&mut self, attributes: &[syn::Attribute]) {
145        for (method, path, span) in attribute_routes(attributes) {
146            self.add_endpoint(method, &path, span);
147        }
148    }
149}
150
151impl<'ast> Visit<'ast> for Collector<'_> {
152    fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
153        self.with_test_context(&node.attrs, |collector| {
154            let owner =
155                collector.add_symbol(&node.sig.ident, NodeKind::Function, node.sig.ident.span());
156            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
157                collector.add_attribute_endpoints(&node.attrs);
158                syn::visit::visit_item_fn(collector, node);
159            });
160        });
161    }
162
163    fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
164        self.with_test_context(&node.attrs, |collector| {
165            let owner =
166                collector.add_symbol(&node.sig.ident, NodeKind::Method, node.sig.ident.span());
167            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
168                collector.add_attribute_endpoints(&node.attrs);
169                syn::visit::visit_impl_item_fn(collector, node);
170            });
171        });
172    }
173
174    fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
175        self.with_test_context(&node.attrs, |collector| {
176            let owner =
177                collector.add_symbol(&node.sig.ident, NodeKind::Method, node.sig.ident.span());
178            collector.with_owner(OwnerUpdate::Symbol(owner), |collector| {
179                syn::visit::visit_trait_item_fn(collector, node);
180            });
181        });
182    }
183
184    fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) {
185        self.with_test_context(&node.attrs, |collector| {
186            collector.add_symbol(&node.ident, NodeKind::Struct, node.ident.span());
187            syn::visit::visit_item_struct(collector, node);
188        });
189    }
190
191    fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
192        self.with_test_context(&node.attrs, |collector| {
193            collector.add_symbol(&node.ident, NodeKind::Enum, node.ident.span());
194            syn::visit::visit_item_enum(collector, node);
195        });
196    }
197
198    fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
199        self.with_test_context(&node.attrs, |collector| {
200            collector.add_symbol(&node.ident, NodeKind::Trait, node.ident.span());
201            collector.with_owner(OwnerUpdate::Type(node.ident.to_string()), |collector| {
202                syn::visit::visit_item_trait(collector, node);
203            });
204        });
205    }
206
207    fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
208        self.with_test_context(&node.attrs, |collector| {
209            if let Some(owner) = impl_owner(&node.self_ty) {
210                collector.with_owner(OwnerUpdate::Type(owner), |collector| {
211                    syn::visit::visit_item_impl(collector, node);
212                });
213            } else {
214                syn::visit::visit_item_impl(collector, node);
215            }
216        });
217    }
218
219    fn visit_item_type(&mut self, node: &'ast syn::ItemType) {
220        self.with_test_context(&node.attrs, |collector| {
221            collector.add_symbol(&node.ident, NodeKind::TypeAlias, node.ident.span());
222            syn::visit::visit_item_type(collector, node);
223        });
224    }
225
226    fn visit_item_const(&mut self, node: &'ast syn::ItemConst) {
227        self.with_test_context(&node.attrs, |collector| {
228            collector.add_symbol(&node.ident, NodeKind::Constant, node.ident.span());
229            syn::visit::visit_item_const(collector, node);
230        });
231    }
232
233    fn visit_item_static(&mut self, node: &'ast syn::ItemStatic) {
234        self.with_test_context(&node.attrs, |collector| {
235            collector.add_symbol(&node.ident, NodeKind::Static, node.ident.span());
236            syn::visit::visit_item_static(collector, node);
237        });
238    }
239
240    fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
241        self.with_test_context(&node.attrs, |collector| {
242            collector.add_symbol(&node.ident, NodeKind::Module, node.ident.span());
243            if node.content.is_none() {
244                // `mod x;` without a body pulls in x.rs or x/mod.rs. It is the
245                // only thing that makes those files part of the crate, so without
246                // this edge they look unreachable.
247                collector.facts.imports.push(ImportFact::new(
248                    format!("self::{}", node.ident),
249                    source_span(collector.path, node.span()),
250                ));
251            }
252            syn::visit::visit_item_mod(collector, node);
253        });
254    }
255
256    fn visit_item_use(&mut self, node: &'ast syn::ItemUse) {
257        let fact = ImportFact::new(
258            use_tree_text(&node.tree),
259            source_span(self.path, node.span()),
260        );
261        if matches!(node.vis, syn::Visibility::Inherited) {
262            self.facts.imports.push(fact);
263        } else {
264            self.facts.reexports.push(fact);
265        }
266        syn::visit::visit_item_use(self, node);
267    }
268
269    fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
270        if let Some(name) = callable_name(&node.func) {
271            self.add_reference(name, node.span());
272        }
273        syn::visit::visit_expr_call(self, node);
274    }
275
276    fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
277        self.add_reference(node.method.to_string(), node.span());
278        if node.method == "route"
279            && let Some((method, path)) = route_call(node)
280        {
281            self.add_endpoint(method, &path, node.span());
282        }
283        syn::visit::visit_expr_method_call(self, node);
284    }
285}
286
287fn attributes_mark_test(attributes: &[syn::Attribute]) -> bool {
288    attributes.iter().any(|attribute| {
289        let attribute_name = attribute
290            .path()
291            .segments
292            .last()
293            .map(|part| part.ident.to_string());
294        if attribute_name.as_deref().is_some_and(|name| {
295            matches!(
296                name,
297                "test" | "rstest" | "proptest" | "wasm_bindgen_test" | "test_case"
298            )
299        }) {
300            return true;
301        }
302        if !attribute.path().is_ident("cfg") {
303            return false;
304        }
305        let syn::Meta::List(meta_list) = &attribute.meta else {
306            return false;
307        };
308        cfg_list_marks_test(meta_list)
309    })
310}
311
312fn cfg_list_marks_test(list: &syn::MetaList) -> bool {
313    cfg_list_marks_test_with_negation(list, false)
314}
315
316fn cfg_list_marks_test_with_negation(list: &syn::MetaList, negated: bool) -> bool {
317    let nested_negated = if list.path.is_ident("not") {
318        !negated
319    } else {
320        negated
321    };
322    list.parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated)
323        .is_ok_and(|items| {
324            items
325                .iter()
326                .any(|meta| cfg_meta_marks_test(meta, nested_negated))
327        })
328}
329
330fn cfg_meta_marks_test(meta: &syn::Meta, negated: bool) -> bool {
331    match meta {
332        syn::Meta::Path(path) => path.is_ident("test") && !negated,
333        syn::Meta::List(list) => cfg_list_marks_test_with_negation(list, negated),
334        syn::Meta::NameValue(_) => false,
335    }
336}
337
338fn impl_owner(ty: &syn::Type) -> Option<String> {
339    let syn::Type::Path(path) = ty else {
340        return None;
341    };
342    path.path
343        .segments
344        .last()
345        .map(|segment| segment.ident.to_string())
346}
347
348fn use_tree_text(tree: &UseTree) -> String {
349    match tree {
350        UseTree::Path(path) => format!("{}::{}", path.ident, use_tree_text(&path.tree)),
351        UseTree::Name(name) => name.ident.to_string(),
352        UseTree::Rename(rename) => format!("{} as {}", rename.ident, rename.rename),
353        UseTree::Glob(_) => "*".into(),
354        UseTree::Group(group) => {
355            let items = group.items.iter().map(use_tree_text).collect::<Vec<_>>();
356            format!("{{{}}}", items.join(","))
357        }
358    }
359}
360
361fn source_span(path: &str, span: Span) -> SourceSpan {
362    SourceSpan {
363        file: path.to_owned(),
364        start: position(span.start()),
365        end: position(span.end()),
366    }
367}
368
369fn position(point: LineColumn) -> SourcePosition {
370    SourcePosition {
371        line: u32::try_from(point.line).unwrap_or(u32::MAX),
372        column: u32::try_from(point.column)
373            .unwrap_or(u32::MAX)
374            .saturating_add(1),
375    }
376}
377
378#[cfg(test)]
379mod tests;