1use std::collections::HashMap;
11
12#[derive(Debug, Clone, Default)]
14pub struct MoveFunctionInfo {
15 pub name: String,
17 pub doc: Option<String>,
19 pub param_names: Vec<String>,
21 pub type_param_names: Vec<String>,
23}
24
25#[derive(Debug, Clone, Default)]
27pub struct MoveStructInfo {
28 pub name: String,
30 pub doc: Option<String>,
32 pub field_docs: HashMap<String, String>,
34}
35
36#[derive(Debug, Clone, Default)]
38pub struct MoveModuleInfo {
39 pub doc: Option<String>,
41 pub functions: HashMap<String, MoveFunctionInfo>,
43 pub structs: HashMap<String, MoveStructInfo>,
45}
46
47#[derive(Debug, Clone, Copy, Default)]
49pub struct MoveSourceParser;
50
51impl MoveSourceParser {
52 pub fn parse(source: &str) -> MoveModuleInfo {
54 MoveModuleInfo {
55 doc: Self::extract_leading_doc(source),
56 functions: Self::parse_functions(source),
57 structs: Self::parse_structs(source),
58 }
59 }
60
61 fn extract_leading_doc(source: &str) -> Option<String> {
63 let mut doc_lines = Vec::new();
64 let mut in_doc = false;
65
66 for line in source.lines() {
67 let trimmed = line.trim();
68 if trimmed.starts_with("///") {
69 in_doc = true;
70 let doc_content = trimmed.strip_prefix("///").unwrap_or("").trim();
71 doc_lines.push(doc_content.to_string());
72 } else if trimmed.starts_with("module ")
73 || trimmed.starts_with("script ")
74 || (in_doc && !trimmed.is_empty() && !trimmed.starts_with("//"))
75 {
76 break;
77 }
78 }
79
80 if doc_lines.is_empty() {
81 None
82 } else {
83 Some(doc_lines.join("\n"))
84 }
85 }
86
87 fn parse_functions(source: &str) -> HashMap<String, MoveFunctionInfo> {
89 let mut functions = HashMap::new();
90 let lines: Vec<&str> = source.lines().collect();
91
92 let mut i = 0;
93 while i < lines.len() {
94 let line = lines[i].trim();
95
96 if Self::is_function_start(line) {
98 let (func_info, consumed) = Self::parse_function(&lines, i);
99 if !func_info.name.is_empty() {
100 functions.insert(func_info.name.clone(), func_info);
101 }
102 i += consumed.max(1);
103 } else {
104 i += 1;
105 }
106 }
107
108 functions
109 }
110
111 fn is_function_start(line: &str) -> bool {
113 let patterns = [
114 "public fun ",
115 "public entry fun ",
116 "public(friend) fun ",
117 "entry fun ",
118 "fun ",
119 "#[view]",
120 ];
121 patterns.iter().any(|p| line.contains(p))
122 }
123
124 fn parse_function(lines: &[&str], start: usize) -> (MoveFunctionInfo, usize) {
126 let mut info = MoveFunctionInfo::default();
127 let mut consumed = 0;
128
129 let mut doc_lines = Vec::new();
131 let mut j = start;
132 while j > 0 {
133 j -= 1;
134 let prev_line = lines[j].trim();
135 if prev_line.starts_with("///") {
136 let doc_content = prev_line.strip_prefix("///").unwrap_or("").trim();
137 doc_lines.insert(0, doc_content.to_string());
138 } else if prev_line.is_empty() || prev_line.starts_with("#[") {
139 } else {
141 break;
142 }
143 }
144
145 if !doc_lines.is_empty() {
146 info.doc = Some(doc_lines.join("\n"));
147 }
148
149 let mut signature = String::new();
151 let mut i = start;
152 let mut paren_depth = 0;
153
154 while i < lines.len() {
155 let line = lines[i].trim();
156 consumed += 1;
157
158 signature.push_str(line);
159 signature.push(' ');
160
161 for c in line.chars() {
163 match c {
164 '(' => paren_depth += 1,
165 ')' => paren_depth -= 1,
166 _ => {}
167 }
168 }
169
170 if paren_depth == 0 && (line.contains('{') || line.ends_with(';')) {
172 break;
173 }
174
175 i += 1;
176 }
177
178 if let Some(name) = Self::extract_function_name(&signature) {
180 info.name = name;
181 }
182
183 info.type_param_names = Self::extract_type_params(&signature);
185
186 info.param_names = Self::extract_param_names(&signature);
188
189 (info, consumed)
190 }
191
192 fn extract_function_name(signature: &str) -> Option<String> {
194 let fun_idx = signature.find("fun ")?;
196 let after_fun = &signature[fun_idx + 4..];
197 let after_fun = after_fun.trim_start();
198
199 let name: String = after_fun
201 .chars()
202 .take_while(|c| c.is_alphanumeric() || *c == '_')
203 .collect();
204
205 if name.is_empty() { None } else { Some(name) }
206 }
207
208 fn extract_type_params(signature: &str) -> Vec<String> {
210 let mut params = Vec::new();
211
212 if let Some(fun_idx) = signature.find("fun ") {
214 let after_fun = &signature[fun_idx..];
215
216 if let Some(lt_idx) = after_fun.find('<')
218 && let Some(gt_idx) = after_fun.find('>')
219 && lt_idx < gt_idx
220 {
221 let type_params = &after_fun[lt_idx + 1..gt_idx];
222 for param in type_params.split(',') {
223 let param = param.trim();
224 let name: String = param
226 .chars()
227 .take_while(|c| c.is_alphanumeric() || *c == '_')
228 .collect();
229 if !name.is_empty() {
230 params.push(name);
231 }
232 }
233 }
234 }
235
236 params
237 }
238
239 fn extract_param_names(signature: &str) -> Vec<String> {
241 let mut params = Vec::new();
242
243 let Some(paren_start) = signature.find('(') else {
246 return params;
247 };
248
249 let after_paren = &signature[paren_start + 1..];
250
251 let mut depth = 1;
253 let mut end_idx = 0;
254 for (i, c) in after_paren.chars().enumerate() {
255 match c {
256 '(' => depth += 1,
257 ')' => {
258 depth -= 1;
259 if depth == 0 {
260 end_idx = i;
261 break;
262 }
263 }
264 _ => {}
265 }
266 }
267
268 let params_str = &after_paren[..end_idx];
269
270 let mut current_param = String::new();
272 let mut angle_depth = 0;
273
274 for c in params_str.chars() {
275 match c {
276 '<' => {
277 angle_depth += 1;
278 current_param.push(c);
279 }
280 '>' => {
281 angle_depth -= 1;
282 current_param.push(c);
283 }
284 ',' if angle_depth == 0 => {
285 if let Some(name) = Self::extract_single_param_name(¤t_param) {
286 params.push(name);
287 }
288 current_param.clear();
289 }
290 _ => current_param.push(c),
291 }
292 }
293
294 if let Some(name) = Self::extract_single_param_name(¤t_param) {
296 params.push(name);
297 }
298
299 params
300 }
301
302 fn extract_single_param_name(param: &str) -> Option<String> {
304 let param = param.trim();
305 if param.is_empty() {
306 return None;
307 }
308
309 if let Some(colon_idx) = param.find(':') {
311 let name = param[..colon_idx].trim();
312 let name = name.trim_start_matches('&').trim();
314 if name.is_empty() || name == "_" {
315 None
316 } else {
317 Some(name.to_string())
318 }
319 } else {
320 None
321 }
322 }
323
324 fn parse_structs(source: &str) -> HashMap<String, MoveStructInfo> {
326 let mut structs = HashMap::new();
327 let lines: Vec<&str> = source.lines().collect();
328
329 let mut i = 0;
330 while i < lines.len() {
331 let line = lines[i].trim();
332
333 if line.contains("struct ") && (line.contains(" has ") || line.contains('{')) {
335 let (struct_info, consumed) = Self::parse_struct(&lines, i);
336 if !struct_info.name.is_empty() {
337 structs.insert(struct_info.name.clone(), struct_info);
338 }
339 i += consumed.max(1);
340 } else {
341 i += 1;
342 }
343 }
344
345 structs
346 }
347
348 fn parse_struct(lines: &[&str], start: usize) -> (MoveStructInfo, usize) {
350 let mut info = MoveStructInfo::default();
351 let mut consumed = 0;
352
353 let mut doc_lines = Vec::new();
355 let mut j = start;
356 while j > 0 {
357 j -= 1;
358 let prev_line = lines[j].trim();
359 if prev_line.starts_with("///") {
360 let doc_content = prev_line.strip_prefix("///").unwrap_or("").trim();
361 doc_lines.insert(0, doc_content.to_string());
362 } else if prev_line.is_empty() || prev_line.starts_with("#[") {
363 } else {
365 break;
366 }
367 }
368
369 if !doc_lines.is_empty() {
370 info.doc = Some(doc_lines.join("\n"));
371 }
372
373 let line = lines[start].trim();
375 if let Some(struct_idx) = line.find("struct ") {
376 let after_struct = &line[struct_idx + 7..];
377 let name: String = after_struct
378 .chars()
379 .take_while(|c| c.is_alphanumeric() || *c == '_')
380 .collect();
381 info.name = name;
382 }
383
384 let mut i = start;
386 let mut in_struct = false;
387 let mut current_doc: Option<String> = None;
388
389 while i < lines.len() {
390 let line = lines[i].trim();
391 consumed += 1;
392
393 if line.contains('{') {
394 in_struct = true;
395 }
396
397 if in_struct {
398 if line.starts_with("///") {
399 let doc = line.strip_prefix("///").unwrap_or("").trim();
400 current_doc = Some(doc.to_string());
401 } else if line.contains(':') && !line.starts_with("//") {
402 let field_name: String = line
404 .trim()
405 .chars()
406 .take_while(|c| c.is_alphanumeric() || *c == '_')
407 .collect();
408
409 if !field_name.is_empty()
410 && let Some(doc) = current_doc.take()
411 {
412 info.field_docs.insert(field_name, doc);
413 }
414 } else if !line.starts_with("//") && !line.is_empty() {
415 current_doc = None;
416 }
417
418 if line.contains('}') {
419 break;
420 }
421 }
422
423 i += 1;
424 }
425
426 (info, consumed)
427 }
428}
429
430#[derive(Debug, Clone)]
432pub struct EnrichedFunctionInfo {
433 pub name: String,
435 pub doc: Option<String>,
437 pub params: Vec<EnrichedParam>,
439 pub type_param_names: Vec<String>,
441}
442
443#[derive(Debug, Clone)]
445pub struct EnrichedParam {
446 pub name: String,
448 pub move_type: String,
450 pub is_signer: bool,
452}
453
454impl EnrichedFunctionInfo {
455 pub fn from_abi_and_source(
457 func_name: &str,
458 abi_params: &[String],
459 abi_type_params_count: usize,
460 source_info: Option<&MoveFunctionInfo>,
461 ) -> Self {
462 let mut info = Self {
463 name: func_name.to_string(),
464 doc: source_info.and_then(|s| s.doc.clone()),
465 params: Vec::new(),
466 type_param_names: Vec::new(),
467 };
468
469 let source_names = source_info
471 .map(|s| s.param_names.clone())
472 .unwrap_or_default();
473
474 if let Some(src) = source_info {
476 info.type_param_names.clone_from(&src.type_param_names);
477 }
478 while info.type_param_names.len() < abi_type_params_count {
480 info.type_param_names
481 .push(format!("T{}", info.type_param_names.len()));
482 }
483
484 let mut source_idx = 0;
486 for (i, move_type) in abi_params.iter().enumerate() {
487 let is_signer = move_type == "&signer" || move_type == "signer";
488
489 let name = if source_idx < source_names.len() {
491 let name = source_names[source_idx].clone();
492 source_idx += 1;
493 name
494 } else {
495 Self::generate_param_name(move_type, i)
497 };
498
499 info.params.push(EnrichedParam {
500 name,
501 move_type: move_type.clone(),
502 is_signer,
503 });
504 }
505
506 info
507 }
508
509 fn generate_param_name(move_type: &str, index: usize) -> String {
511 match move_type {
512 "&signer" | "signer" => "account".to_string(),
513 "address" => "addr".to_string(),
514 "u8" | "u16" | "u32" | "u64" | "u128" | "u256" => "amount".to_string(),
515 "bool" => "flag".to_string(),
516 t if t.starts_with("vector<u8>") => "bytes".to_string(),
517 t if t.starts_with("vector<") => "items".to_string(),
518 t if t.contains("::string::String") => "name".to_string(),
519 t if t.contains("::object::Object") => "object".to_string(),
520 t if t.contains("::option::Option") => "maybe_value".to_string(),
521 _ => format!("arg{index}"),
522 }
523 }
524
525 pub fn non_signer_params(&self) -> Vec<&EnrichedParam> {
527 self.params.iter().filter(|p| !p.is_signer).collect()
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 const SAMPLE_MOVE_SOURCE: &str = r"
536/// A module for managing tokens.
537///
538/// This module provides functionality for minting and transferring tokens.
539module my_addr::my_token {
540 use std::string::String;
541 use aptos_framework::object::Object;
542
543 /// Represents token information.
544 struct TokenInfo has key {
545 /// The name of the token.
546 name: String,
547 /// The symbol of the token.
548 symbol: String,
549 /// Number of decimal places.
550 decimals: u8,
551 }
552
553 /// Mints new tokens to a recipient.
554 ///
555 /// # Arguments
556 /// * `admin` - The admin account
557 /// * `recipient` - The address to receive tokens
558 /// * `amount` - The amount to mint
559 public entry fun mint(
560 admin: &signer,
561 recipient: address,
562 amount: u64,
563 ) acquires TokenInfo {
564 // implementation
565 }
566
567 /// Transfers tokens between accounts.
568 public entry fun transfer<CoinType>(
569 sender: &signer,
570 to: address,
571 amount: u64,
572 ) {
573 // implementation
574 }
575
576 /// Gets the balance of an account.
577 #[view]
578 public fun balance(owner: address): u64 {
579 0
580 }
581
582 /// Gets the total supply.
583 #[view]
584 public fun total_supply(): u64 {
585 0
586 }
587}
588";
589
590 #[test]
591 fn test_parse_module_doc() {
592 let info = MoveSourceParser::parse(SAMPLE_MOVE_SOURCE);
593 assert!(info.doc.is_some());
594 assert!(info.doc.unwrap().contains("managing tokens"));
595 }
596
597 #[test]
598 fn test_parse_function_names() {
599 let info = MoveSourceParser::parse(SAMPLE_MOVE_SOURCE);
600
601 assert!(info.functions.contains_key("mint"));
602 assert!(info.functions.contains_key("transfer"));
603 assert!(info.functions.contains_key("balance"));
604 assert!(info.functions.contains_key("total_supply"));
605 }
606
607 #[test]
608 fn test_parse_function_params() {
609 let info = MoveSourceParser::parse(SAMPLE_MOVE_SOURCE);
610
611 let mint = info.functions.get("mint").unwrap();
612 assert_eq!(mint.param_names, vec!["admin", "recipient", "amount"]);
613
614 let transfer = info.functions.get("transfer").unwrap();
615 assert_eq!(transfer.param_names, vec!["sender", "to", "amount"]);
616
617 let balance = info.functions.get("balance").unwrap();
618 assert_eq!(balance.param_names, vec!["owner"]);
619 }
620
621 #[test]
622 fn test_parse_type_params() {
623 let info = MoveSourceParser::parse(SAMPLE_MOVE_SOURCE);
624
625 let transfer = info.functions.get("transfer").unwrap();
626 assert_eq!(transfer.type_param_names, vec!["CoinType"]);
627 }
628
629 #[test]
630 fn test_parse_function_docs() {
631 let info = MoveSourceParser::parse(SAMPLE_MOVE_SOURCE);
632
633 let mint = info.functions.get("mint").unwrap();
634 assert!(mint.doc.is_some());
635 assert!(mint.doc.as_ref().unwrap().contains("Mints new tokens"));
636 }
637
638 #[test]
639 fn test_parse_struct() {
640 let info = MoveSourceParser::parse(SAMPLE_MOVE_SOURCE);
641
642 assert!(info.structs.contains_key("TokenInfo"));
643 let token_info = info.structs.get("TokenInfo").unwrap();
644 assert!(token_info.doc.is_some());
645 assert!(
646 token_info
647 .doc
648 .as_ref()
649 .unwrap()
650 .contains("token information")
651 );
652
653 assert!(token_info.field_docs.contains_key("name"));
655 assert!(
656 token_info
657 .field_docs
658 .get("name")
659 .unwrap()
660 .contains("name of the token")
661 );
662 }
663
664 #[test]
665 fn test_enriched_function() {
666 let info = MoveSourceParser::parse(SAMPLE_MOVE_SOURCE);
667 let mint_source = info.functions.get("mint");
668
669 let abi_params = vec![
670 "&signer".to_string(),
671 "address".to_string(),
672 "u64".to_string(),
673 ];
674
675 let enriched =
676 EnrichedFunctionInfo::from_abi_and_source("mint", &abi_params, 0, mint_source);
677
678 assert_eq!(enriched.params[0].name, "admin");
679 assert!(enriched.params[0].is_signer);
680 assert_eq!(enriched.params[1].name, "recipient");
681 assert_eq!(enriched.params[2].name, "amount");
682
683 let non_signers = enriched.non_signer_params();
684 assert_eq!(non_signers.len(), 2);
685 }
686
687 #[test]
688 fn test_enriched_function_without_source() {
689 let abi_params = vec![
690 "&signer".to_string(),
691 "address".to_string(),
692 "u64".to_string(),
693 ];
694
695 let enriched = EnrichedFunctionInfo::from_abi_and_source("transfer", &abi_params, 0, None);
696
697 assert_eq!(enriched.params[0].name, "account");
699 assert_eq!(enriched.params[1].name, "addr");
700 assert_eq!(enriched.params[2].name, "amount");
701 }
702}