1use std::collections::BTreeMap;
9
10use llmy_types::error::LLMYError;
11use tree_sitter::Node;
12
13use crate::extract::{
14 FileExtraction, GrammarSet, NodeUtil, RawCallSite, RawCallable, RawModule, RawState,
15 RawStateRef, SourceFile,
16};
17use crate::model::{CallableKind, Language, ModuleKind, StateKind};
18
19const CW_WRITE_METHODS: [&str; 4] = ["save", "update", "remove", "replace"];
20const CW_READ_METHODS: [&str; 7] = [
21 "load", "may_load", "has", "query", "range", "keys", "prefix",
22];
23
24#[derive(Debug, Clone)]
27struct AccountsField {
28 type_name: String,
29 mutable: bool,
30}
31
32pub struct RustExtractor;
33
34impl RustExtractor {
35 pub fn extract(file: &SourceFile) -> Result<FileExtraction, LLMYError> {
36 let tree = GrammarSet::parse(Language::Rust, &file.content)?;
37 let root = tree.root_node();
38 let parse_errors = GrammarSet::count_errors(root);
39 let source = &file.content;
40
41 let module_name = file
42 .relative
43 .with_extension("")
44 .display()
45 .to_string()
46 .replace('\\', "/");
47
48 let mut states = vec![];
49 let mut containers: BTreeMap<String, Vec<AccountsField>> = BTreeMap::new();
51
52 for item in root.descendants_of_kinds(&["struct_item", "const_item", "static_item"], false)
53 {
54 match item.kind() {
55 "struct_item" => {
56 let Some(name) = item
57 .child_of_kind("type_identifier")
58 .map(|n| n.text_of(source))
59 else {
60 continue;
61 };
62 let attributes = Self::preceding_attributes(item, source);
63 if attributes.iter().any(|a| a == "account") {
64 states.push(RawState {
65 name,
66 kind: StateKind::AnchorAccount,
67 type_text: "#[account] struct".to_string(),
68 span: item.line_span(),
69 });
70 } else if attributes.iter().any(|a| a.contains("Accounts")) {
71 containers.insert(name, Self::container_fields(item, source));
72 }
73 }
74 _ => {
75 let Some(name) = item.child_of_kind("identifier").map(|n| n.text_of(source))
76 else {
77 continue;
78 };
79 let Some(type_node) = item.child_of_kind("generic_type") else {
80 continue;
81 };
82 let head = type_node
83 .child_of_kind("type_identifier")
84 .map(|n| n.text_of(source))
85 .unwrap_or_default();
86 let kind = match head.as_str() {
87 "Item" => StateKind::CwItem,
88 "Map" => StateKind::CwMap,
89 _ => continue,
90 };
91 states.push(RawState {
92 name,
93 kind,
94 type_text: type_node.text_of(source),
95 span: item.line_span(),
96 });
97 }
98 }
99 }
100
101 let program_mods: Vec<Node<'_>> = root
104 .descendants_of_kinds(&["mod_item"], true)
105 .into_iter()
106 .filter(|m| {
107 Self::preceding_attributes(*m, source)
108 .iter()
109 .any(|a| a == "program")
110 })
111 .collect();
112
113 let cw_state_names: Vec<String> = states
114 .iter()
115 .filter(|s| matches!(s.kind, StateKind::CwItem | StateKind::CwMap))
116 .map(|s| s.name.clone())
117 .collect();
118
119 let mut callables = vec![];
120 for function in root.descendants_of_kinds(&["function_item"], false) {
121 callables.push(Self::extract_function(
122 function,
123 source,
124 &program_mods,
125 &containers,
126 &cw_state_names,
127 ));
128 }
129
130 Ok(FileExtraction {
131 file: file.relative.clone(),
132 language: Language::Rust,
133 modules: vec![RawModule {
134 name: module_name,
135 kind: ModuleKind::Module,
136 span: root.line_span(),
137 parents: vec![],
138 callables,
139 states,
140 }],
141 parse_errors,
142 })
143 }
144
145 fn preceding_attributes(item: Node<'_>, source: &str) -> Vec<String> {
148 let mut out = vec![];
149 let mut current = item;
150 while let Some(previous) = current.prev_named_sibling() {
151 if previous.kind() != "attribute_item" {
152 break;
153 }
154 if let Some(attribute) = previous.child_of_kind("attribute") {
155 out.push(attribute.text_of(source));
156 }
157 current = previous;
158 }
159 out
160 }
161
162 fn container_fields(item: Node<'_>, source: &str) -> Vec<AccountsField> {
163 let Some(fields) = item.child_of_kind("field_declaration_list") else {
164 return vec![];
165 };
166 let mut out = vec![];
167 for field in fields.children_of_kind("field_declaration") {
168 let mutable = Self::preceding_attributes(field, source)
169 .iter()
170 .any(|a| a.starts_with("account") && a.contains("mut"));
171 let type_names = field.descendants_of_kinds(&["type_identifier"], true);
174 if let Some(inner) = type_names.last() {
175 out.push(AccountsField {
176 type_name: inner.text_of(source),
177 mutable,
178 });
179 }
180 }
181 out
182 }
183
184 fn extract_function(
185 function: Node<'_>,
186 source: &str,
187 program_mods: &[Node<'_>],
188 containers: &BTreeMap<String, Vec<AccountsField>>,
189 cw_state_names: &[String],
190 ) -> RawCallable {
191 let name = function
192 .child_of_kind("identifier")
193 .map(|n| n.text_of(source))
194 .unwrap_or_else(|| "<function>".to_string());
195 let signature = function.signature_head(&["block"], source);
196
197 let attributes = Self::preceding_attributes(function, source);
198 let in_program_mod = program_mods.iter().any(|m| {
199 m.start_byte() <= function.start_byte() && function.end_byte() <= m.end_byte()
200 });
201 let kind = if in_program_mod || attributes.iter().any(|a| a.contains("entry_point")) {
202 CallableKind::Entry
203 } else {
204 CallableKind::Function
205 };
206
207 let mut calls = vec![];
208 let mut state_refs = vec![];
209
210 if let Some(parameters) = function.child_of_kind("parameters") {
213 for generic in parameters.descendants_of_kinds(&["generic_type"], true) {
214 let head = generic
215 .child_of_kind("type_identifier")
216 .map(|n| n.text_of(source))
217 .unwrap_or_default();
218 if head != "Context" {
219 continue;
220 }
221 let Some(arguments) = generic.child_of_kind("type_arguments") else {
222 continue;
223 };
224 let Some(container_name) = arguments
225 .descendants_of_kinds(&["type_identifier"], false)
226 .first()
227 .map(|n| n.text_of(source))
228 else {
229 continue;
230 };
231 for field in containers.get(&container_name).into_iter().flatten() {
232 state_refs.push(RawStateRef {
233 name: field.type_name.clone(),
234 write: field.mutable,
235 line: function.line_span().start_line,
236 });
237 }
238 }
239 }
240
241 if let Some(body) = function.child_of_kind("block") {
242 for call in body.descendants_of_kinds(&["call_expression"], true) {
243 let Some(target) = call.named_child(0) else {
244 continue;
245 };
246 let line = call.line_span().start_line;
247 match target.kind() {
248 "identifier" => {
249 let call_name = target.text_of(source);
250 calls.push(RawCallSite {
251 text: call_name.clone(),
252 name: call_name,
253 qualifier: None,
254 line,
255 });
256 }
257 "scoped_identifier" => {
258 let mut cursor = target.walk();
259 let parts: Vec<String> = target
260 .children(&mut cursor)
261 .filter(|c| c.is_named())
262 .map(|c| c.text_of(source))
263 .collect();
264 let Some(call_name) = parts.last().cloned() else {
265 continue;
266 };
267 let qualifier = (parts.len() > 1).then(|| parts[0].clone());
268 calls.push(RawCallSite {
269 text: target.text_of(source),
270 name: call_name,
271 qualifier,
272 line,
273 });
274 }
275 "field_expression" => {
276 let Some(method) = target
277 .child_of_kind("field_identifier")
278 .map(|n| n.text_of(source))
279 else {
280 continue;
281 };
282 let receiver = target
283 .named_child(0)
284 .map(|n| n.text_of(source))
285 .unwrap_or_default();
286
287 if cw_state_names.contains(&receiver) {
289 if CW_WRITE_METHODS.contains(&method.as_str()) {
290 state_refs.push(RawStateRef {
291 name: receiver.clone(),
292 write: true,
293 line,
294 });
295 continue;
296 }
297 if CW_READ_METHODS.contains(&method.as_str()) {
298 state_refs.push(RawStateRef {
299 name: receiver.clone(),
300 write: false,
301 line,
302 });
303 continue;
304 }
305 }
306
307 calls.push(RawCallSite {
308 text: target.text_of(source),
309 name: method,
310 qualifier: Some(receiver),
311 line,
312 });
313 }
314 _ => {}
315 }
316 }
317 }
318
319 RawCallable {
320 name,
321 kind,
322 signature,
323 span: function.line_span(),
324 calls,
325 state_refs,
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use std::path::PathBuf;
334
335 fn extract(source: &str) -> FileExtraction {
336 RustExtractor::extract(&SourceFile {
337 relative: PathBuf::from("src/lib.rs"),
338 content: source.to_string(),
339 })
340 .expect("extract")
341 }
342
343 const COSMWASM: &str = r#"
344use cw_storage_plus::{Item, Map};
345const STATE: Item<State> = Item::new("state");
346const BALANCES: Map<&Addr, u128> = Map::new("balances");
347
348#[entry_point]
349pub fn execute(deps: DepsMut) -> Result<Response, ContractError> {
350 let mut state = STATE.load(deps.storage)?;
351 state.count += 1;
352 STATE.save(deps.storage, &state)?;
353 helper(&state);
354 Ok(Response::new())
355}
356
357fn helper(state: &State) -> u64 { state.count }
358"#;
359
360 const ANCHOR: &str = r#"
361#[program]
362pub mod counter {
363 use super::*;
364 pub fn increment(ctx: Context<Increment>) -> Result<()> {
365 ctx.accounts.counter.count += 1;
366 Ok(())
367 }
368}
369
370#[account]
371pub struct Counter { pub count: u64 }
372
373#[derive(Accounts)]
374pub struct Increment<'info> {
375 #[account(mut)]
376 pub counter: Account<'info, Counter>,
377 pub user: Signer<'info>,
378}
379"#;
380
381 #[test]
382 fn cosmwasm_state_and_accesses_are_extracted() {
383 let extraction = extract(COSMWASM);
384 assert_eq!(extraction.parse_errors, 0);
385 let module = &extraction.modules[0];
386 assert_eq!(module.name, "src/lib");
387
388 let kinds: Vec<_> = module
389 .states
390 .iter()
391 .map(|s| (s.name.as_str(), s.kind))
392 .collect();
393 assert_eq!(
394 kinds,
395 vec![("STATE", StateKind::CwItem), ("BALANCES", StateKind::CwMap)]
396 );
397
398 let execute = &module.callables[0];
399 assert_eq!(execute.kind, CallableKind::Entry);
400 let refs: Vec<_> = execute
401 .state_refs
402 .iter()
403 .map(|r| (r.name.as_str(), r.write))
404 .collect();
405 assert!(refs.contains(&("STATE", false)), "{refs:?}");
406 assert!(refs.contains(&("STATE", true)), "{refs:?}");
407 assert!(execute.calls.iter().any(|c| c.name == "helper"));
408 assert!(!execute.calls.iter().any(|c| c.name == "save"));
410 }
411
412 #[test]
413 fn anchor_accounts_expand_through_context() {
414 let extraction = extract(ANCHOR);
415 assert_eq!(extraction.parse_errors, 0);
416 let module = &extraction.modules[0];
417
418 assert_eq!(module.states.len(), 1);
419 assert_eq!(module.states[0].name, "Counter");
420 assert_eq!(module.states[0].kind, StateKind::AnchorAccount);
421
422 let increment = &module.callables[0];
423 assert_eq!(increment.name, "increment");
424 assert_eq!(increment.kind, CallableKind::Entry);
425 let refs: Vec<_> = increment
426 .state_refs
427 .iter()
428 .map(|r| (r.name.as_str(), r.write))
429 .collect();
430 assert!(refs.contains(&("Counter", true)), "{refs:?}");
431 assert!(refs.contains(&("Signer", false)), "{refs:?}");
433 }
434}