Skip to main content

aptos_sdk/codegen/
move_parser.rs

1//! Move source code parser for extracting function signatures and documentation.
2//!
3//! This module parses Move source files to extract:
4//! - Function parameter names
5//! - Documentation comments
6//! - Struct field documentation
7//!
8//! This information is combined with ABI data to generate more readable bindings.
9
10use std::collections::HashMap;
11
12/// Information extracted from a Move function definition.
13#[derive(Debug, Clone, Default)]
14pub struct MoveFunctionInfo {
15    /// The function name.
16    pub name: String,
17    /// Documentation comment (from `///` comments).
18    pub doc: Option<String>,
19    /// Parameter names in order.
20    pub param_names: Vec<String>,
21    /// Type parameter names (e.g., `T`, `CoinType`).
22    pub type_param_names: Vec<String>,
23}
24
25/// Information extracted from a Move struct definition.
26#[derive(Debug, Clone, Default)]
27pub struct MoveStructInfo {
28    /// The struct name.
29    pub name: String,
30    /// Documentation comment.
31    pub doc: Option<String>,
32    /// Field names to documentation.
33    pub field_docs: HashMap<String, String>,
34}
35
36/// Information extracted from a Move module.
37#[derive(Debug, Clone, Default)]
38pub struct MoveModuleInfo {
39    /// Module documentation.
40    pub doc: Option<String>,
41    /// Functions by name.
42    pub functions: HashMap<String, MoveFunctionInfo>,
43    /// Structs by name.
44    pub structs: HashMap<String, MoveStructInfo>,
45}
46
47/// Parses Move source code to extract metadata.
48#[derive(Debug, Clone, Copy, Default)]
49pub struct MoveSourceParser;
50
51impl MoveSourceParser {
52    /// Parses Move source code and extracts module information.
53    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    /// Extracts leading documentation comments.
62    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    /// Parses all function definitions from the source.
88    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            // Look for function definitions
97            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    /// Checks if a line starts a function definition.
112    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    /// Parses a single function definition.
125    fn parse_function(lines: &[&str], start: usize) -> (MoveFunctionInfo, usize) {
126        let mut info = MoveFunctionInfo::default();
127        let mut consumed = 0;
128
129        // Look backwards for doc comments
130        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                // Skip empty lines and attributes
140            } else {
141                break;
142            }
143        }
144
145        if !doc_lines.is_empty() {
146            info.doc = Some(doc_lines.join("\n"));
147        }
148
149        // Collect the full function signature (may span multiple lines)
150        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            // Track parenthesis depth
162            for c in line.chars() {
163                match c {
164                    '(' => paren_depth += 1,
165                    ')' => paren_depth -= 1,
166                    _ => {}
167                }
168            }
169
170            // Stop when we've closed the parameter list and hit the body
171            if paren_depth == 0 && (line.contains('{') || line.ends_with(';')) {
172                break;
173            }
174
175            i += 1;
176        }
177
178        // Extract function name
179        if let Some(name) = Self::extract_function_name(&signature) {
180            info.name = name;
181        }
182
183        // Extract type parameters
184        info.type_param_names = Self::extract_type_params(&signature);
185
186        // Extract parameter names
187        info.param_names = Self::extract_param_names(&signature);
188
189        (info, consumed)
190    }
191
192    /// Extracts the function name from a signature.
193    fn extract_function_name(signature: &str) -> Option<String> {
194        // Look for "fun name" or "fun name<"
195        let fun_idx = signature.find("fun ")?;
196        let after_fun = &signature[fun_idx + 4..];
197        let after_fun = after_fun.trim_start();
198
199        // Get until the first non-identifier character
200        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    /// Extracts type parameter names from a signature.
209    fn extract_type_params(signature: &str) -> Vec<String> {
210        let mut params = Vec::new();
211
212        // Find the type params section after function name
213        if let Some(fun_idx) = signature.find("fun ") {
214            let after_fun = &signature[fun_idx..];
215
216            // Look for <...> before (
217            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                    // Extract just the name (before any constraints)
225                    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    /// Extracts parameter names from a function signature.
240    fn extract_param_names(signature: &str) -> Vec<String> {
241        let mut params = Vec::new();
242
243        // Find the parameter section (...) after function name
244        // We need to handle nested generics properly
245        let Some(paren_start) = signature.find('(') else {
246            return params;
247        };
248
249        let after_paren = &signature[paren_start + 1..];
250
251        // Find matching closing paren
252        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        // Split by comma, handling nested generics
271        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(&current_param) {
286                        params.push(name);
287                    }
288                    current_param.clear();
289                }
290                _ => current_param.push(c),
291            }
292        }
293
294        // Don't forget the last parameter
295        if let Some(name) = Self::extract_single_param_name(&current_param) {
296            params.push(name);
297        }
298
299        params
300    }
301
302    /// Extracts the parameter name from a single "name: Type" declaration.
303    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        // Handle "name: Type" format
310        if let Some(colon_idx) = param.find(':') {
311            let name = param[..colon_idx].trim();
312            // Remove any leading & for references
313            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    /// Parses all struct definitions from the source.
325    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            // Look for struct definitions
334            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    /// Parses a single struct definition.
349    fn parse_struct(lines: &[&str], start: usize) -> (MoveStructInfo, usize) {
350        let mut info = MoveStructInfo::default();
351        let mut consumed = 0;
352
353        // Look backwards for doc comments
354        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                // Skip empty lines and attributes
364            } else {
365                break;
366            }
367        }
368
369        if !doc_lines.is_empty() {
370            info.doc = Some(doc_lines.join("\n"));
371        }
372
373        // Extract struct name
374        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        // Parse struct body for field documentation
385        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                    // This is a field
403                    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/// Merges Move source information with ABI function parameters.
431#[derive(Debug, Clone)]
432pub struct EnrichedFunctionInfo {
433    /// Function name.
434    pub name: String,
435    /// Documentation from Move source.
436    pub doc: Option<String>,
437    /// Parameters with names and types.
438    pub params: Vec<EnrichedParam>,
439    /// Type parameter names.
440    pub type_param_names: Vec<String>,
441}
442
443/// A parameter with both name and type information.
444#[derive(Debug, Clone)]
445pub struct EnrichedParam {
446    /// Parameter name from Move source.
447    pub name: String,
448    /// Parameter type from ABI.
449    pub move_type: String,
450    /// Whether this is a signer parameter.
451    pub is_signer: bool,
452}
453
454impl EnrichedFunctionInfo {
455    /// Creates enriched function info by merging Move source and ABI data.
456    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        // Get parameter names from source, or generate defaults
470        let source_names = source_info
471            .map(|s| s.param_names.clone())
472            .unwrap_or_default();
473
474        // Get type parameter names
475        if let Some(src) = source_info {
476            info.type_param_names.clone_from(&src.type_param_names);
477        }
478        // Fill in missing type param names
479        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        // Create enriched params
485        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            // Get name from source if available
490            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                // Generate a meaningful name based on type
496                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    /// Generates a parameter name based on its type.
510    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    /// Returns non-signer parameters.
526    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        // Field docs
654        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        // Should generate reasonable names
698        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}