Skip to main content

avm_rs/cli/commands/
validate.rs

1//! Validate command implementation
2
3use crate::assembler::Assembler;
4use crate::cli::{ExecutionMode, GlobalOptions, ValidateCommand};
5use anyhow::{Context, Result};
6use std::fs;
7use std::path::Path;
8
9/// Handle the validate command
10pub fn handle(cmd: ValidateCommand, global: &GlobalOptions) -> Result<()> {
11    if !global.quiet && global.verbose {
12        println!("šŸ” Validating TEAL programs...");
13        println!("Files: {:?}", cmd.files);
14    }
15
16    let mut total_files = 0;
17    let mut valid_files = 0;
18    let mut warnings = 0;
19    let mut errors = 0;
20
21    for file_path in &cmd.files {
22        total_files += 1;
23
24        if !global.quiet && global.verbose {
25            println!("\nValidating: {file_path:?}");
26        }
27
28        match validate_file(file_path, &cmd, global) {
29            Ok(file_result) => {
30                valid_files += 1;
31                warnings += file_result.warnings;
32
33                if !global.quiet {
34                    if file_result.warnings > 0 {
35                        println!(
36                            "āš ļø  {:?}: Valid with {} warnings",
37                            file_path, file_result.warnings
38                        );
39                    } else {
40                        println!("āœ… {file_path:?}: Valid");
41                    }
42                }
43            }
44            Err(e) => {
45                errors += 1;
46                if !global.quiet {
47                    println!("āŒ {file_path:?}: {e}");
48                }
49            }
50        }
51    }
52
53    // Summary
54    if !global.quiet {
55        match global.format {
56            crate::cli::OutputFormat::Text => {
57                println!("\nšŸ“Š Validation Summary:");
58                println!("  Total files: {total_files}");
59                println!("  Valid files: {valid_files}");
60                println!("  Failed files: {errors}");
61                println!("  Total warnings: {warnings}");
62
63                if errors > 0 {
64                    println!("āŒ Validation failed for {errors} files");
65                } else if warnings > 0 && cmd.strict {
66                    println!("āš ļø  Validation passed but {warnings} warnings in strict mode");
67                } else {
68                    println!("āœ… All files validated successfully");
69                }
70            }
71            crate::cli::OutputFormat::Json => {
72                let summary = serde_json::json!({
73                    "total_files": total_files,
74                    "valid_files": valid_files,
75                    "failed_files": errors,
76                    "total_warnings": warnings,
77                    "success": errors == 0 && (!cmd.strict || warnings == 0)
78                });
79                println!("{}", serde_json::to_string_pretty(&summary)?);
80            }
81        }
82    }
83
84    // Exit with error if validation failed
85    if errors > 0 || (cmd.strict && warnings > 0) {
86        std::process::exit(1);
87    }
88
89    Ok(())
90}
91
92/// Result of validating a single file
93struct FileValidationResult {
94    warnings: usize,
95}
96
97/// Validate a single TEAL file
98fn validate_file(
99    file_path: &Path,
100    cmd: &ValidateCommand,
101    global: &GlobalOptions,
102) -> Result<FileValidationResult> {
103    // Read the file
104    let source = fs::read_to_string(file_path)
105        .with_context(|| format!("Failed to read file: {file_path:?}"))?;
106
107    // Parse and validate syntax
108    let mut assembler = Assembler::new();
109    let _bytecode = assembler
110        .assemble(&source)
111        .map_err(|e| anyhow::anyhow!("Assembly failed: {}", e))?;
112
113    // Additional validation checks
114    let mut warnings = 0;
115
116    // Check TEAL version compatibility
117    if let Some(target_version) = cmd.version {
118        warnings += check_version_compatibility(&source, target_version)?;
119    }
120
121    // Check execution mode compatibility
122    if let Some(target_mode) = &cmd.mode {
123        warnings += check_mode_compatibility(&source, target_mode)?;
124    }
125
126    // Perform detailed analysis if requested
127    if cmd.detailed {
128        warnings += perform_detailed_analysis(&source, global)?;
129    }
130
131    Ok(FileValidationResult { warnings })
132}
133
134/// Check version compatibility
135fn check_version_compatibility(source: &str, target_version: u8) -> Result<usize> {
136    let mut warnings = 0;
137
138    // Extract pragma version from source
139    let declared_version = extract_pragma_version(source)?;
140
141    if let Some(declared) = declared_version {
142        if declared > target_version {
143            warnings += 1;
144            eprintln!("Warning: File declares version {declared} but target is {target_version}");
145        }
146    } else {
147        warnings += 1;
148        eprintln!("Warning: No version pragma found, assuming latest version");
149    }
150
151    Ok(warnings)
152}
153
154/// Check execution mode compatibility
155fn check_mode_compatibility(source: &str, target_mode: &ExecutionMode) -> Result<usize> {
156    let mut warnings = 0;
157
158    // Check for mode-specific opcodes
159    match target_mode {
160        ExecutionMode::Signature => {
161            if source.contains("app_global_get") || source.contains("app_local_get") {
162                warnings += 1;
163                eprintln!("Warning: Application opcodes found in signature mode validation");
164            }
165        }
166        ExecutionMode::Application => {
167            // Application mode is more permissive
168        }
169    }
170
171    Ok(warnings)
172}
173
174/// Perform detailed analysis
175fn perform_detailed_analysis(source: &str, global: &GlobalOptions) -> Result<usize> {
176    let mut warnings = 0;
177
178    // Check for common issues
179    let lines: Vec<&str> = source.lines().collect();
180
181    for (line_num, line) in lines.iter().enumerate() {
182        let line = line.trim();
183
184        // Check for potential issues
185        if line.contains("int 0") && line.contains("==") {
186            warnings += 1;
187            if !global.quiet {
188                eprintln!(
189                    "Warning: Line {}: Consider using '!' instead of '== 0'",
190                    line_num + 1
191                );
192            }
193        }
194
195        if line.contains("b ") && !line.contains("bnz") && !line.contains("bz") && !global.quiet {
196            eprintln!("Info: Line {}: Unconditional branch found", line_num + 1);
197        }
198    }
199
200    // Check program structure
201    if !source.contains("return") {
202        warnings += 1;
203        if !global.quiet {
204            eprintln!("Warning: No 'return' statement found");
205        }
206    }
207
208    Ok(warnings)
209}
210
211/// Extract pragma version from source
212fn extract_pragma_version(source: &str) -> Result<Option<u8>> {
213    for line in source.lines() {
214        let line = line.trim();
215        if line.starts_with("#pragma version") {
216            let parts: Vec<&str> = line.split_whitespace().collect();
217            if parts.len() >= 3 {
218                return Ok(Some(parts[2].parse()?));
219            }
220        }
221    }
222    Ok(None)
223}