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 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;