pub(crate) const WASM_COMPLEXITY_NOOP_NOTE: &str =
"note: --wasm-complexity is a no-op — cyclomatic and cognitive complexity are measured for every parsed file";
#[allow(clippy::too_many_arguments)]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_analyze_assemblyscript(
project_path: PathBuf,
format: ComplexityOutputFormat,
wasm_complexity: bool,
memory_analysis: bool,
security: bool,
output: Option<PathBuf>,
_timeout: u64,
perf: bool,
top_files: usize,
) -> Result<()> {
crate::cli::ensure_analysis_path_exists(&project_path)?;
if wasm_complexity {
eprintln!("{WASM_COMPLEXITY_NOOP_NOTE}");
}
use crate::cli::colors as c;
crate::status_eprintln!("🔍 {}", c::label("Analyzing AssemblyScript code..."));
let start = std::time::Instant::now();
let mut sections = WasmSections {
security: security.then(Vec::new),
memory: memory_analysis.then(Vec::new),
complexity: None,
};
let results = process_assemblyscript_files(&project_path, &mut sections).await?;
let elapsed = start.elapsed();
crate::status_eprintln!(
"📊 Analysis complete in {}",
c::number(&format!("{:.2}s", elapsed.as_secs_f64()))
);
let output_text =
format_assemblyscript_results(&results, &format, perf, elapsed, top_files, §ions)?;
write_analysis_output(output_text, output).await?;
Ok(())
}
async fn process_assemblyscript_files(
project_path: &Path,
sections: &mut WasmSections,
) -> Result<Vec<(PathBuf, WasmComplexity)>> {
let detector = WasmLanguageDetector::new();
let mut parser = AssemblyScriptParser::new()?;
let mut results = Vec::new();
let as_files = collect_assemblyscript_files(project_path)?;
crate::status_eprintln!(
"📁 Found {} AssemblyScript files",
crate::cli::colors::number(&as_files.len().to_string())
);
for file_path in as_files {
if let Some(analysis_result) =
analyze_single_file(&file_path, &detector, &mut parser, sections).await?
{
results.push(analysis_result);
}
}
Ok(results)
}
async fn analyze_single_file(
file_path: &Path,
detector: &WasmLanguageDetector,
parser: &mut AssemblyScriptParser,
sections: &mut WasmSections,
) -> Result<Option<(PathBuf, WasmComplexity)>> {
let content = match tokio::fs::read_to_string(file_path).await {
Ok(content) => content,
Err(_) => return Ok(None),
};
if !detector.is_assemblyscript(&content) {
return Ok(None);
}
let ast = match parser.parse_file(file_path, &content).await {
Ok(ast) => ast,
Err(e) => {
eprintln!(
"{}",
crate::cli::colors::colored(
crate::cli::colors::RED,
&format!("❌ Failed to parse {}: {}", file_path.display(), e)
)
);
return Ok(None);
}
};
crate::status_eprintln!(
"✅ Parsed: {}",
crate::cli::colors::path(&file_path.display().to_string())
);
let result = process_parsed_ast(&ast, &content, file_path, sections)?;
Ok(result)
}
fn process_parsed_ast(
ast: &AstDag,
content: &str,
file_path: &Path,
sections: &mut WasmSections,
) -> Result<Option<(PathBuf, WasmComplexity)>> {
let complexity_analyzer = WasmComplexityAnalyzer::new();
let complexity = complexity_analyzer.analyze_ast(ast)?;
if let Some(rows) = sections.security.as_mut() {
rows.extend(source_security_findings(content, file_path));
}
if let Some(rows) = sections.memory.as_mut() {
rows.push(source_memory_finding(content, file_path));
}
Ok(Some((file_path.to_path_buf(), complexity)))
}
fn source_security_findings(content: &str, file_path: &Path) -> Vec<WasmFinding> {
let validator = WasmSecurityValidator::new();
let Ok(validation) = validator.validate_text(content) else {
return Vec::new();
};
security_findings(
file_path,
&validation,
"no issue found by the memory/resource rules",
)
}
fn source_memory_finding(content: &str, file_path: &Path) -> WasmFinding {
const SITES: &[(&str, &str)] = &[
("memory.grow", "memory.grow"),
("load<", "raw load<T>()"),
("store<", "raw store<T>()"),
("changetype<", "changetype<>"),
("new ", "`new` allocation"),
];
let counts: Vec<String> = SITES
.iter()
.map(|(needle, label)| {
let count = content.matches(needle).count();
format!("{label}: {count}")
})
.collect();
WasmFinding::info(file_path, "memory-sites", counts.join(", "))
}
#[cfg(test)]
mod assemblyscript_default_run_tests {
use super::*;
const FIXTURE: &str = "export function add(a: i32, b: i32): i32 {\n return a + b;\n}\n";
#[tokio::test]
async fn test_default_run_yields_one_result_per_parsed_file() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(dir.path().join("assembly")).expect("mkdir assembly");
std::fs::write(dir.path().join("assembly/index.ts"), FIXTURE).expect("write index.ts");
std::fs::write(dir.path().join("top.ts"), FIXTURE).expect("write top.ts");
let found = collect_assemblyscript_files(dir.path()).expect("collect");
assert_eq!(
found.len(),
2,
"fixture should present 2 AssemblyScript files"
);
let mut sections = WasmSections::default();
let results = process_assemblyscript_files(dir.path(), &mut sections)
.await
.expect("analysis");
assert_eq!(
results.len(),
found.len(),
"reported {} results for {} parsed files",
results.len(),
found.len()
);
}
}
async fn write_analysis_output(output_text: String, output_path: Option<PathBuf>) -> Result<()> {
if let Some(output_path) = output_path {
tokio::fs::write(&output_path, &output_text).await?;
crate::status_eprintln!(
"📝 Results written to: {}",
crate::cli::colors::path(&output_path.display().to_string())
);
} else {
println!("{output_text}");
}
Ok(())
}
#[cfg(test)]
mod wasm_complexity_noop_tests {
use super::*;
#[test]
fn the_noop_note_says_the_flag_is_a_no_op() {
assert!(WASM_COMPLEXITY_NOOP_NOTE.contains("--wasm-complexity"));
assert!(WASM_COMPLEXITY_NOOP_NOTE.contains("no-op"));
assert!(WASM_COMPLEXITY_NOOP_NOTE.contains("every parsed file"));
}
}