use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use anyhow::Result;
use swc_ecma_ast::*;
use swc_ecma_visit::{Visit, VisitWith};
use crate::core::ast::parser::parse_file;
use crate::core::ast::semantic::SemanticModel;
use crate::utils::terminal;
use colored::Colorize;
static VERIFY_CACHE_HITS: AtomicUsize = AtomicUsize::new(0);
static VERIFY_CACHE_MISSES: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct VerifyIssue {
pub severity: IssueSeverity,
pub title: String,
pub description: String,
pub confidence_reason: String,
pub hint: String,
pub follow_up: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum IssueSeverity {
Error,
Warning,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CachedVerifyEntry {
pub size: u64,
pub modified_secs: u64,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub issues: Vec<VerifyIssue>,
}
pub struct VerifySummary {
pub files_scanned: usize,
pub total_errors: usize,
pub total_warnings: usize,
pub file_reports: Vec<FileVerifyReport>,
}
fn load_verify_cache(path: &Path) -> Option<(Vec<String>, Vec<String>, Vec<VerifyIssue>)> {
let sanitize = |val: &str| {
val.chars()
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
.collect::<String>()
};
let cache_dir = Path::new(".morph-cli/cache/__verify__");
let cache_path = cache_dir.join(format!("{}.json", sanitize(&path.to_string_lossy())));
if !cache_path.exists() {
return None;
}
let content = std::fs::read_to_string(&cache_path).ok()?;
let cached = serde_json::from_str::<CachedVerifyEntry>(&content).ok()?;
if let Ok(meta) = std::fs::metadata(path) {
let modified_secs = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or_default();
if meta.len() == cached.size && modified_secs == cached.modified_secs {
VERIFY_CACHE_HITS.fetch_add(1, Ordering::Relaxed);
return Some((cached.errors, cached.warnings, cached.issues));
}
}
VERIFY_CACHE_MISSES.fetch_add(1, Ordering::Relaxed);
None
}
fn save_verify_cache(
path: &Path,
errors: Vec<String>,
warnings: Vec<String>,
issues: Vec<VerifyIssue>,
) -> Option<()> {
let sanitize = |val: &str| {
val.chars()
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
.collect::<String>()
};
let cache_dir = Path::new(".morph-cli/cache/__verify__");
let _ = std::fs::create_dir_all(cache_dir);
let cache_path = cache_dir.join(format!("{}.json", sanitize(&path.to_string_lossy())));
let meta = std::fs::metadata(path).ok()?;
let modified_secs = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or_default();
let entry = CachedVerifyEntry {
size: meta.len(),
modified_secs,
errors,
warnings,
issues,
};
let content = serde_json::to_string_pretty(&entry).ok()?;
let _ = std::fs::write(cache_path, content);
Some(())
}
pub fn execute(path: &Path) -> Result<()> {
let spinner = terminal::spinner("Verifying project integrity...");
let summary = run_verification(path)?;
spinner.finish_and_clear();
println!();
println!("{}", terminal::label("Verification Report"));
println!("{}", "─".repeat(60));
println!(" Files Scanned: {}", summary.files_scanned);
println!(
" Errors: {}",
if summary.total_errors > 0 {
summary.total_errors.to_string().red().bold().to_string()
} else {
"0".green().to_string()
}
);
println!(
" Warnings: {}",
if summary.total_warnings > 0 {
summary.total_warnings.to_string().yellow().bold().to_string()
} else {
"0".green().to_string()
}
);
println!("{}", "─".repeat(60));
for report in &summary.file_reports {
if report.issues.is_empty() {
continue;
}
println!();
println!(" File: {}", report.path.display().to_string().cyan().bold());
for issue in &report.issues {
let prefix = match issue.severity {
IssueSeverity::Error => "[ERROR]".red().bold(),
IssueSeverity::Warning => "[WARN]".yellow().bold(),
};
println!(" {} {} - {}", prefix, issue.title.bold(), issue.description);
println!(" ├─ {} {}", "Confidence:".dimmed(), issue.confidence_reason);
println!(" ├─ {} {}", "Hint:".dimmed(), issue.hint);
println!(" └─ {} {}", "Follow-Up:".dimmed(), issue.follow_up);
println!();
}
}
let reused = VERIFY_CACHE_HITS.load(Ordering::Relaxed);
let skipped_reparses = reused;
let time_saved_ms = reused * 25;
println!("{}", "─".repeat(60));
println!("{}", terminal::label("Performance & Caching Summary"));
println!(" Cached Files Reused: {}", reused);
println!(" Skipped Reparses: {}", skipped_reparses);
println!(" Est Time Savings: {}ms", time_saved_ms);
println!("{}", "─".repeat(60));
if summary.total_errors > 0 {
anyhow::bail!("Verification failed with {} errors", summary.total_errors);
} else {
println!("{} Verification passed!", terminal::success_prefix());
}
Ok(())
}
pub fn run_verification(path: &Path) -> Result<VerifySummary> {
let mut files_scanned = 0;
let mut total_errors = 0;
let mut total_warnings = 0;
let mut file_reports = Vec::new();
for entry in walkdir::WalkDir::new(path)
.into_iter()
.filter_entry(|e| {
let name = e.file_name().to_string_lossy();
name != "node_modules" && name != ".git" && name != "target" && name != "dist" && name != "build"
})
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let p = entry.path();
let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("");
if !["js", "ts", "jsx", "tsx", "mjs", "cjs"].contains(&ext) {
continue;
}
files_scanned += 1;
let mut report = FileVerifyReport::new(p.to_path_buf());
if let Some((errors, warnings, issues)) = load_verify_cache(p) {
report.errors = errors;
report.warnings = warnings;
report.issues = issues;
} else {
let source = std::fs::read_to_string(p).unwrap_or_default();
let has_todo = source.contains("TODO:") || source.contains("TODO") || source.contains("manual migration");
match parse_file(p) {
Ok(parsed) => {
let semantic = SemanticModel::new(&parsed.module);
let mut verifier = MigrationVerifier::new();
verifier.visit_module(&parsed.module);
let mut seen_imports = std::collections::HashSet::new();
for collision in &semantic.collisions {
let issue = VerifyIssue {
severity: IssueSeverity::Warning,
title: "Naming Collision".to_string(),
description: format!("Found multiple declarations for symbol: {}", collision),
confidence_reason: "Collisions lead to scope shadow bugs and unexpected variable resolution.".to_string(),
hint: "Check if the variable is defined twice or has overlapping scopes.".to_string(),
follow_up: "Rename one of the variables to ensure symbol uniqueness.".to_string(),
};
report.warnings.push(format!("{}: {}", issue.title, issue.description));
report.issues.push(issue);
}
for item in &parsed.module.body {
match item {
ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
let src = import.src.value.to_string();
if !seen_imports.insert(src.clone()) {
let issue = VerifyIssue {
severity: IssueSeverity::Warning,
title: "Duplicate Import Source".to_string(),
description: format!("The module '{}' was imported multiple times.", src),
confidence_reason: "Duplicate imports cause redundant dependency graph resolution.".to_string(),
hint: "Look for multiple import statements from the same source.".to_string(),
follow_up: "Merge the imported bindings into a single static import statement.".to_string(),
};
report.warnings.push(format!("{}: {}", issue.title, issue.description));
report.issues.push(issue);
}
if src.starts_with('.') {
if !resolve_local_import(p, &src) {
let issue = VerifyIssue {
severity: IssueSeverity::Error,
title: "Unresolved Local Import".to_string(),
description: format!("Local import '{}' could not be resolved to a file on disk.", src),
confidence_reason: "Unresolved paths break compilation and runtime bundling.".to_string(),
hint: "Verify that the relative path matches the file location and extension.".to_string(),
follow_up: "Locate the correct path and rename or adjust the import target.".to_string(),
};
report.errors.push(format!("{}: {}", issue.title, issue.description));
report.issues.push(issue);
}
}
}
ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
if let Some(src) = &export.src {
let src_val = src.value.to_string();
if src_val.starts_with('.') && !resolve_local_import(p, &src_val) {
let issue = VerifyIssue {
severity: IssueSeverity::Error,
title: "Unresolved Local Re-export".to_string(),
description: format!("Local re-export '{}' could not be resolved to a file on disk.", src_val),
confidence_reason: "Unresolved export references prevent dependent files from importing bindings.".to_string(),
hint: "Verify that the re-exported path exists and is spelt correctly.".to_string(),
follow_up: "Update the re-export target to point to a valid file.".to_string(),
};
report.errors.push(format!("{}: {}", issue.title, issue.description));
report.issues.push(issue);
}
}
}
_ => {}
}
}
if (verifier.has_esm_import || verifier.has_esm_export) && (verifier.has_cjs_require || verifier.has_cjs_export) {
let issue = VerifyIssue {
severity: IssueSeverity::Warning,
title: "Partially Migrated Paradigm".to_string(),
description: "Mixed ESM and CommonJS patterns detected in the same file.".to_string(),
confidence_reason: "Mixed syntax increases compiler ambiguity, often causing bundling failures.".to_string(),
hint: "Scan the file for residual require() or module.exports statements.".to_string(),
follow_up: "Refactor all remaining CommonJS elements to standard ESM imports/exports.".to_string(),
};
report.warnings.push(format!("{}: {}", issue.title, issue.description));
report.issues.push(issue);
}
if verifier.has_express && verifier.has_fastify {
let issue = VerifyIssue {
severity: IssueSeverity::Warning,
title: "Mixed Routing Frameworks".to_string(),
description: "Found references to both Express and Fastify in the same file.".to_string(),
confidence_reason: "Mixing paradigms in a single file makes the migration state inconsistent.".to_string(),
hint: "Inspect references to express or fastify in this file.".to_string(),
follow_up: "Complete the migration of Express routes to Fastify and remove Express references.".to_string(),
};
report.warnings.push(format!("{}: {}", issue.title, issue.description));
report.issues.push(issue);
}
if verifier.has_unsafe_mutation {
let issue = VerifyIssue {
severity: IssueSeverity::Warning,
title: "Unsafe Request/Response Mutation".to_string(),
description: "Found direct assignments to properties on req or res objects.".to_string(),
confidence_reason: "Modifying req/res directly breaks Fastify's schema optimization and is discouraged.".to_string(),
hint: "Look for assignments like req.property = value or res.property = value.".to_string(),
follow_up: "Refactor mutations to use Fastify request decorators or custom plugins.".to_string(),
};
report.warnings.push(format!("{}: {}", issue.title, issue.description));
report.issues.push(issue);
}
}
Err(e) => {
let issue = VerifyIssue {
severity: IssueSeverity::Error,
title: "Syntax Error".to_string(),
description: format!("The parser failed to compile the file: {}", e),
confidence_reason: "Code with syntax errors cannot be scanned, resolved, or run.".to_string(),
hint: "Check syntax or missing brackets/parentheses around the error location.".to_string(),
follow_up: "Fix the javascript syntax error before executing further transforms.".to_string(),
};
report.errors.push(format!("{}: {}", issue.title, issue.description));
report.issues.push(issue);
}
}
if has_todo {
let issue = VerifyIssue {
severity: IssueSeverity::Warning,
title: "Skipped Transform / Manual Action Required".to_string(),
description: "File contains TODO or manual migration markers indicating incomplete steps.".to_string(),
confidence_reason: "Automated recipes skip highly complex patterns to avoid regression, leaving placeholders.".to_string(),
hint: "Look for // TODO or 'manual migration' comments in this file.".to_string(),
follow_up: "Follow the comment instructions to implement the skipped block manually.".to_string(),
};
report.warnings.push(format!("{}: {}", issue.title, issue.description));
report.issues.push(issue);
}
let _ = save_verify_cache(p, report.errors.clone(), report.warnings.clone(), report.issues.clone());
}
if !report.is_empty() {
total_errors += report.errors.len();
total_warnings += report.warnings.len();
file_reports.push(report);
}
}
Ok(VerifySummary {
files_scanned,
total_errors,
total_warnings,
file_reports,
})
}
pub struct FileVerifyReport {
pub path: PathBuf,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub issues: Vec<VerifyIssue>,
}
impl FileVerifyReport {
pub fn new(path: PathBuf) -> Self {
Self {
path,
errors: Vec::new(),
warnings: Vec::new(),
issues: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty() && self.warnings.is_empty()
}
}
fn resolve_local_import(current_file: &Path, import_src: &str) -> bool {
crate::utils::path::resolve_relative_import(current_file, import_src).is_some()
}
struct MigrationVerifier {
pub has_esm_import: bool,
pub has_cjs_require: bool,
pub has_esm_export: bool,
pub has_cjs_export: bool,
pub has_express: bool,
pub has_fastify: bool,
pub has_unsafe_mutation: bool,
}
impl MigrationVerifier {
fn new() -> Self {
Self {
has_esm_import: false,
has_cjs_require: false,
has_esm_export: false,
has_cjs_export: false,
has_express: false,
has_fastify: false,
has_unsafe_mutation: false,
}
}
}
impl Visit for MigrationVerifier {
fn visit_call_expr(&mut self, call: &CallExpr) {
if let Callee::Expr(expr) = &call.callee {
if let Expr::Ident(i) = expr.as_ref() {
if i.sym.as_ref() == "require" {
self.has_cjs_require = true;
}
}
}
call.visit_children_with(self);
}
fn visit_member_expr(&mut self, expr: &MemberExpr) {
if let Expr::Ident(i) = expr.obj.as_ref() {
let name = i.sym.as_ref();
if name == "module" {
if let MemberProp::Ident(p) = &expr.prop {
if p.sym.as_ref() == "exports" {
self.has_cjs_export = true;
}
}
} else if name == "exports" {
self.has_cjs_export = true;
} else if name == "express" {
self.has_express = true;
} else if name == "fastify" {
self.has_fastify = true;
}
}
expr.visit_children_with(self);
}
fn visit_import_decl(&mut self, import: &ImportDecl) {
self.has_esm_import = true;
let src = import.src.value.to_string();
if src.contains("express") {
self.has_express = true;
} else if src.contains("fastify") {
self.has_fastify = true;
}
import.visit_children_with(self);
}
fn visit_export_decl(&mut self, export: &ExportDecl) {
self.has_esm_export = true;
export.visit_children_with(self);
}
fn visit_assign_expr(&mut self, expr: &AssignExpr) {
if let AssignTarget::Simple(simple) = &expr.left {
if let SimpleAssignTarget::Member(member) = simple {
if let Expr::Ident(i) = member.obj.as_ref() {
let name = i.sym.as_ref();
if name == "req" || name == "res" {
if let MemberProp::Ident(p) = &member.prop {
let prop = p.sym.as_ref();
if !["session", "user", "body", "query", "params", "headers"].contains(&prop) {
self.has_unsafe_mutation = true;
}
}
}
}
}
}
expr.visit_children_with(self);
}
}