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