1use llmy_types::error::LLMYError;
9use tree_sitter::Node;
10
11use crate::extract::{
12 FileExtraction, GrammarSet, NodeUtil, RawCallSite, RawCallable, RawModule, RawState,
13 RawStateRef, SourceFile,
14};
15use crate::model::{CallableKind, Language, ModuleKind, StateKind};
16
17pub struct MoveAptosExtractor;
18
19impl MoveAptosExtractor {
20 pub fn extract(file: &SourceFile) -> Result<FileExtraction, LLMYError> {
21 let tree = GrammarSet::parse(Language::MoveAptos, &file.content)?;
22 let root = tree.root_node();
23 let parse_errors = GrammarSet::count_errors(root);
24 let source = &file.content;
25
26 let modules = root
27 .descendants_of_kinds(&["module_declaration"], false)
28 .into_iter()
29 .map(|node| Self::extract_module(node, source))
30 .collect();
31
32 Ok(FileExtraction {
33 file: file.relative.clone(),
34 language: Language::MoveAptos,
35 modules,
36 parse_errors,
37 })
38 }
39
40 fn extract_module(node: Node<'_>, source: &str) -> RawModule {
41 let name = node
42 .child_of_kind("module_identity")
43 .and_then(|identity| {
44 identity
45 .children_of_kind("identifier")
46 .last()
47 .map(|n| n.text_of(source))
48 })
49 .unwrap_or_else(|| "<module>".to_string());
50
51 let mut states = vec![];
52 let mut callables = vec![];
53 if let Some(body) = node.child_of_kind("module_body") {
54 for declaration in body.children_of_kind("struct_declaration") {
55 let Some(struct_name) = declaration
56 .child_of_kind("identifier")
57 .map(|n| n.text_of(source))
58 else {
59 continue;
60 };
61 let abilities: Vec<String> = declaration
62 .descendants_of_kinds(&["ability"], false)
63 .into_iter()
64 .map(|a| a.text_of(source))
65 .collect();
66 if !abilities.iter().any(|a| a == "key") {
67 continue;
68 }
69 states.push(RawState {
70 name: struct_name,
71 kind: StateKind::MoveResource,
72 type_text: format!("struct has {}", abilities.join(", ")),
73 span: declaration.line_span(),
74 });
75 }
76
77 for function in body.children_of_kind("function_declaration") {
78 callables.push(Self::extract_function(function, source));
79 }
80 }
81
82 RawModule {
83 name,
84 kind: ModuleKind::Module,
85 span: node.line_span(),
86 parents: vec![],
87 callables,
88 states,
89 }
90 }
91
92 fn extract_function(function: Node<'_>, source: &str) -> RawCallable {
93 let name = function
94 .child_of_kind("identifier")
95 .map(|n| n.text_of(source))
96 .unwrap_or_else(|| "<function>".to_string());
97 let kind = if function.child_of_kind("entry_modifier").is_some() {
98 CallableKind::Entry
99 } else {
100 CallableKind::Function
101 };
102 let signature = function.signature_head(&["block"], source);
103
104 let mut calls = vec![];
105 let mut state_refs = vec![];
106 if let Some(body) = function.child_of_kind("block") {
107 for call in body.descendants_of_kinds(&["call_expression"], true) {
108 let Some(chain) = call.child_of_kind("name_access_chain") else {
109 continue;
110 };
111 let parts: Vec<String> = chain
112 .children_of_kind("identifier")
113 .into_iter()
114 .map(|n| n.text_of(source))
115 .collect();
116 let Some(call_name) = parts.last().cloned() else {
117 continue;
118 };
119 let line = call.line_span().start_line;
120
121 let storage_write = match call_name.as_str() {
124 "move_to" | "move_from" | "borrow_global_mut" => Some(true),
125 "borrow_global" | "exists" => Some(false),
126 _ => None,
127 };
128 if let Some(write) = storage_write {
129 if let Some(resource) = call
130 .child_of_kind("type_arguments")
131 .and_then(|args| args.first_identifier(source))
132 {
133 state_refs.push(RawStateRef {
134 name: resource,
135 write,
136 line,
137 });
138 } else if call_name == "move_to" {
139 if let Some(packed) = call.child_of_kind("arg_list").and_then(|args| {
142 args.descendants_of_kinds(&["pack_expression"], false)
143 .first()
144 .and_then(|p| p.first_identifier(source))
145 }) {
146 state_refs.push(RawStateRef {
147 name: packed,
148 write: true,
149 line,
150 });
151 }
152 }
153 continue;
154 }
155
156 let qualifier = (parts.len() > 1).then(|| parts[0].clone());
157 calls.push(RawCallSite {
158 text: chain.text_of(source),
159 name: call_name,
160 qualifier,
161 line,
162 });
163 }
164 }
165
166 RawCallable {
167 name,
168 kind,
169 signature,
170 span: function.line_span(),
171 calls,
172 state_refs,
173 }
174 }
175}
176
177pub struct MoveSuiExtractor;
178
179impl MoveSuiExtractor {
180 pub fn extract(file: &SourceFile) -> Result<FileExtraction, LLMYError> {
181 let tree = GrammarSet::parse(Language::MoveSui, &file.content)?;
182 let root = tree.root_node();
183 let parse_errors = GrammarSet::count_errors(root);
184 let source = &file.content;
185
186 let modules = root
187 .descendants_of_kinds(&["module_definition"], false)
188 .into_iter()
189 .map(|node| Self::extract_module(node, source))
190 .collect();
191
192 Ok(FileExtraction {
193 file: file.relative.clone(),
194 language: Language::MoveSui,
195 modules,
196 parse_errors,
197 })
198 }
199
200 fn extract_module(node: Node<'_>, source: &str) -> RawModule {
201 let name = node
202 .child_of_kind("module_identity")
203 .and_then(|identity| {
204 identity
205 .children_of_kind("module_identifier")
206 .last()
207 .map(|n| n.text_of(source))
208 })
209 .unwrap_or_else(|| "<module>".to_string());
210
211 let mut states = vec![];
212 let mut callables = vec![];
213 if let Some(body) = node.child_of_kind("module_body") {
214 for declaration in body.children_of_kind("struct_definition") {
215 let Some(struct_name) = declaration
216 .child_of_kind("struct_identifier")
217 .map(|n| n.text_of(source))
218 else {
219 continue;
220 };
221 let abilities: Vec<String> = declaration
222 .descendants_of_kinds(&["ability"], false)
223 .into_iter()
224 .map(|a| a.text_of(source))
225 .collect();
226 if !abilities.iter().any(|a| a == "key") {
227 continue;
228 }
229 states.push(RawState {
230 name: struct_name,
231 kind: StateKind::SuiObject,
232 type_text: format!("struct has {}", abilities.join(", ")),
233 span: declaration.line_span(),
234 });
235 }
236
237 for function in body.descendants_of_kinds(
238 &[
239 "function_definition",
240 "native_function_definition",
241 "macro_function_definition",
242 ],
243 false,
244 ) {
245 callables.push(Self::extract_function(function, source));
246 }
247 }
248
249 RawModule {
250 name,
251 kind: ModuleKind::Module,
252 span: node.line_span(),
253 parents: vec![],
254 callables,
255 states,
256 }
257 }
258
259 fn extract_function(function: Node<'_>, source: &str) -> RawCallable {
260 let name = function
261 .child_of_kind("function_identifier")
262 .map(|n| n.text_of(source))
263 .unwrap_or_else(|| "<function>".to_string());
264 let is_entry = function
265 .children_of_kind("modifier")
266 .into_iter()
267 .any(|m| m.text_of(source) == "entry");
268 let kind = if is_entry {
269 CallableKind::Entry
270 } else {
271 CallableKind::Function
272 };
273 let signature = function.signature_head(&["block"], source);
274
275 let mut state_refs = vec![];
278 if let Some(parameters) = function.child_of_kind("function_parameters") {
279 for parameter in parameters.children_of_kind("function_parameter") {
280 let mut cursor = parameter.walk();
281 let Some(type_node) = parameter
282 .children(&mut cursor)
283 .filter(|c| c.is_named())
284 .nth(1)
285 else {
286 continue;
287 };
288 let line = parameter.line_span().start_line;
289 match type_node.kind() {
290 "ref_type" => {
291 let mutable = type_node.child_of_kind("mut_ref").is_some();
292 if let Some(type_name) = type_node
293 .child_of_kind("apply_type")
294 .and_then(|t| Self::last_identifier(t, source))
295 {
296 state_refs.push(RawStateRef {
297 name: type_name,
298 write: mutable,
299 line,
300 });
301 }
302 }
303 "apply_type" => {
304 if let Some(type_name) = Self::last_identifier(type_node, source) {
305 state_refs.push(RawStateRef {
306 name: type_name,
307 write: true,
308 line,
309 });
310 }
311 }
312 _ => {}
313 }
314 }
315 }
316
317 let mut calls = vec![];
318 if let Some(body) = function.child_of_kind("block") {
319 for call in body.descendants_of_kinds(&["call_expression"], true) {
320 let Some(access) = call
321 .child_of_kind("name_expression")
322 .and_then(|n| n.child_of_kind("module_access"))
323 else {
324 continue;
325 };
326 let mut cursor = access.walk();
327 let parts: Vec<(String, String)> = access
328 .children(&mut cursor)
329 .filter(|c| c.is_named())
330 .map(|c| (c.kind().to_string(), c.text_of(source)))
331 .collect();
332 let Some((_, call_name)) = parts
333 .iter()
334 .rev()
335 .find(|(kind, _)| kind == "identifier")
336 .cloned()
337 else {
338 continue;
339 };
340 let qualifier = parts
341 .iter()
342 .find(|(kind, _)| kind == "module_identifier")
343 .map(|(_, text)| text.clone());
344 calls.push(RawCallSite {
345 text: access.text_of(source),
346 name: call_name,
347 qualifier,
348 line: call.line_span().start_line,
349 });
350 }
351 }
352
353 RawCallable {
354 name,
355 kind,
356 signature,
357 span: function.line_span(),
358 calls,
359 state_refs,
360 }
361 }
362
363 fn last_identifier(node: Node<'_>, source: &str) -> Option<String> {
364 node.descendants_of_kinds(&["identifier"], false)
365 .last()
366 .map(|n| n.text_of(source))
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use std::path::PathBuf;
374
375 const APTOS: &str = r#"
376module counter_addr::counter {
377 use std::signer;
378
379 struct Counter has key {
380 value: u64,
381 }
382
383 public entry fun initialize(account: &signer) {
384 move_to(account, Counter { value: 0 });
385 }
386
387 public entry fun increment(account: &signer) acquires Counter {
388 let counter = borrow_global_mut<Counter>(signer::address_of(account));
389 counter.value = counter.value + 1;
390 helper(counter.value);
391 }
392
393 fun helper(v: u64): u64 {
394 let snapshot = borrow_global<Counter>(@counter_addr);
395 v + snapshot.value
396 }
397}
398"#;
399
400 const SUI: &str = r#"
401module counter::counter {
402 use sui::transfer;
403
404 public struct Counter has key {
405 id: UID,
406 value: u64,
407 }
408
409 public entry fun create(ctx: &mut TxContext) {
410 let counter = Counter { id: object::new(ctx), value: 0 };
411 transfer::share_object(counter);
412 }
413
414 public entry fun increment(counter: &mut Counter) {
415 counter.value = counter.value + 1;
416 helper(counter);
417 }
418
419 fun helper(counter: &Counter): u64 {
420 counter.value
421 }
422}
423"#;
424
425 #[test]
426 fn aptos_resources_calls_and_storage_ops_are_extracted() {
427 let extraction = MoveAptosExtractor::extract(&SourceFile {
428 relative: PathBuf::from("sources/counter.move"),
429 content: APTOS.to_string(),
430 })
431 .expect("extract");
432 assert_eq!(extraction.parse_errors, 0);
433
434 let module = &extraction.modules[0];
435 assert_eq!(module.name, "counter");
436 assert_eq!(module.states.len(), 1);
437 assert_eq!(module.states[0].name, "Counter");
438 assert_eq!(module.states[0].kind, StateKind::MoveResource);
439
440 let names: Vec<_> = module.callables.iter().map(|c| c.name.as_str()).collect();
441 assert_eq!(names, vec!["initialize", "increment", "helper"]);
442 assert_eq!(module.callables[0].kind, CallableKind::Entry);
443 assert_eq!(module.callables[2].kind, CallableKind::Function);
444
445 let initialize = &module.callables[0];
447 assert!(
448 initialize
449 .state_refs
450 .iter()
451 .any(|r| r.name == "Counter" && r.write)
452 );
453
454 let increment = &module.callables[1];
455 assert!(
456 increment
457 .state_refs
458 .iter()
459 .any(|r| r.name == "Counter" && r.write)
460 );
461 assert!(increment.calls.iter().any(|c| c.name == "helper"));
462 assert!(
463 increment
464 .calls
465 .iter()
466 .any(|c| c.name == "address_of" && c.qualifier.as_deref() == Some("signer"))
467 );
468 assert!(
470 !increment
471 .calls
472 .iter()
473 .any(|c| c.name == "borrow_global_mut")
474 );
475
476 let helper = &module.callables[2];
477 assert!(
478 helper
479 .state_refs
480 .iter()
481 .any(|r| r.name == "Counter" && !r.write)
482 );
483 }
484
485 #[test]
486 fn sui_objects_flow_through_parameters() {
487 let extraction = MoveSuiExtractor::extract(&SourceFile {
488 relative: PathBuf::from("sources/counter.move"),
489 content: SUI.to_string(),
490 })
491 .expect("extract");
492 assert_eq!(extraction.parse_errors, 0);
493
494 let module = &extraction.modules[0];
495 assert_eq!(module.name, "counter");
496 assert_eq!(module.states.len(), 1);
497 assert_eq!(module.states[0].kind, StateKind::SuiObject);
498
499 let names: Vec<_> = module.callables.iter().map(|c| c.name.as_str()).collect();
500 assert_eq!(names, vec!["create", "increment", "helper"]);
501 assert_eq!(module.callables[1].kind, CallableKind::Entry);
502
503 let increment = &module.callables[1];
504 assert!(
505 increment
506 .state_refs
507 .iter()
508 .any(|r| r.name == "Counter" && r.write)
509 );
510 assert!(increment.calls.iter().any(|c| c.name == "helper"));
511
512 let helper = &module.callables[2];
513 assert!(
514 helper
515 .state_refs
516 .iter()
517 .any(|r| r.name == "Counter" && !r.write)
518 );
519
520 let create = &module.callables[0];
521 assert!(
522 create
523 .calls
524 .iter()
525 .any(|c| c.name == "share_object" && c.qualifier.as_deref() == Some("transfer"))
526 );
527 }
528}