#![allow(clippy::all)]
pub mod analysis;
pub mod detect;
pub mod transform;
use anyhow::Result;
use indicatif::ProgressBar;
use std::path::Path;
use crate::core::cache::{CachedDetectionOutcome, load_detection, record_miss, save_detection};
use crate::core::recipe::{
DetectionReport, FileAnalysis, FileClassification, Recipe, RecipeMetadata, TransformOptions,
TransformReport,
};
use crate::recipes::express_to_fastify::transform as tf;
const METADATA: RecipeMetadata = RecipeMetadata {
name: "express-to-fastify",
description: "Analyze Express.js applications for Fastify migration readiness",
supported_extensions: &["js", "ts", "jsx", "tsx"],
required_recipes: &[],
incompatible_recipes: &[],
should_run_before: &[],
should_run_after: &[],
maturity: crate::core::recipe::RecipeMaturity::Beta,
category: crate::core::recipe::RecipeCategory::Analysis,
tags: &["risky", "backend", "express", "fastify", "web", "router", "http"],
};
pub struct ExpressToFastifyRecipe;
impl ExpressToFastifyRecipe {
pub fn new() -> Self {
Self
}
}
impl Default for ExpressToFastifyRecipe {
fn default() -> Self {
Self::new()
}
}
impl Recipe for ExpressToFastifyRecipe {
fn metadata(&self) -> &'static RecipeMetadata {
&METADATA
}
fn detect(&self, root: &Path, progress: &ProgressBar) -> Result<DetectionReport> {
let mut report = DetectionReport::default();
for entry in walkdir::WalkDir::new(root)
.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())
{
let path = entry.path();
if !path.is_file() {
continue;
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !["js", "ts", "jsx", "tsx"].contains(&ext) {
continue;
}
progress.set_message(format!("Analyzing {}", path.display()));
report.total_files += 1;
if let Some(outcome) = load_detection(METADATA.name, path) {
apply_cached_outcome(&mut report, outcome);
continue;
}
record_miss();
let mut detector = detect::ExpressDetector::new();
let analysis = match detector.detect(path) {
Some(analysis) => analysis,
None => {
let skipped = path.to_path_buf();
let _ = save_detection(
METADATA.name,
path,
&CachedDetectionOutcome::Skipped(skipped.clone()),
);
report.skipped_files.push(skipped);
continue;
}
};
let is_relevant = !analysis.routes.is_empty() || !analysis.express_apps.is_empty();
if !is_relevant {
let skipped = path.to_path_buf();
let _ = save_detection(
METADATA.name,
path,
&CachedDetectionOutcome::Skipped(skipped.clone()),
);
report.skipped_files.push(skipped);
continue;
}
let is_safe = analysis.complexity == analysis::ComplexityLevel::Simple
|| analysis.complexity == analysis::ComplexityLevel::Moderate;
report.parseable_files += 1;
let file_analysis = FileAnalysis {
path: path.to_path_buf(),
detected_patterns: vec![format!("Complexity: {:?}", analysis.complexity)],
confidence_score: 100 - (analysis.risky_patterns.len() as u8 * 10).min(50),
classification: if is_safe {
FileClassification::Safe
} else {
FileClassification::Risky
},
is_transform_safe: is_safe,
tags: Default::default(),
};
let _ = save_detection(
METADATA.name,
path,
&CachedDetectionOutcome::Analysis(file_analysis.clone()),
);
report.analyses.push(file_analysis);
}
Ok(report)
}
fn transform(
&self,
report: &DetectionReport,
options: TransformOptions,
) -> Result<TransformReport> {
let mut transform_report = TransformReport::default();
for analysis in &report.analyses {
let source = std::fs::read_to_string(&analysis.path)?;
let mut transformer = tf::ExpressToFastifyTransform::new();
let result = transformer.transform_source(&source, &analysis.path);
let has_actual_changes = result.changed && result.transformed != source;
if has_actual_changes {
transform_report.changed_files.push(analysis.path.clone());
if options.mode == crate::core::recipe::TransformMode::Write {
std::fs::write(&analysis.path, &result.transformed)?;
}
} else {
transform_report.skipped_files.push(crate::core::recipe::SkippedTransform {
path: analysis.path.clone(),
reason: "no changes detected".to_string(),
});
}
let mut diagnostics = result.unsupported.clone();
diagnostics.extend(result.warnings.clone());
if !diagnostics.is_empty() {
transform_report.unsupported_patterns.push(crate::core::recipe::UnsupportedPatternReport {
path: analysis.path.clone(),
patterns: diagnostics,
});
}
}
Ok(transform_report)
}
}
fn apply_cached_outcome(report: &mut DetectionReport, outcome: CachedDetectionOutcome) {
match outcome {
CachedDetectionOutcome::Analysis(analysis) => {
report.parseable_files += 1;
report.analyses.push(analysis);
}
CachedDetectionOutcome::Skipped(path) => report.skipped_files.push(path),
CachedDetectionOutcome::Failure(failure) => report.failed_files.push(failure),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metadata() {
let recipe = ExpressToFastifyRecipe::new();
let meta = recipe.metadata();
assert_eq!(meta.name, "express-to-fastify");
}
#[test]
fn test_new() {
let recipe = ExpressToFastifyRecipe::new();
assert!(recipe.metadata().description.contains("Express"));
}
}