#![allow(clippy::all)]
use std::path::Path;
use swc_common::{FileName, SourceMap, Spanned, sync::Lrc};
use swc_ecma_ast::*;
use swc_ecma_parser::{EsSyntax, Parser, StringInput, Syntax, lexer::Lexer};
use swc_ecma_visit::{Visit, VisitWith};
use crate::recipes::express_to_fastify::analysis::{MigrationAnalysis, RouteInfo};
pub struct ExpressDetector {
analysis: MigrationAnalysis,
}
impl ExpressDetector {
pub fn new() -> Self {
Self {
analysis: MigrationAnalysis::default(),
}
}
pub fn detect(&mut self, path: &Path) -> Option<MigrationAnalysis> {
let source = std::fs::read_to_string(path).ok()?;
let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
let fm = cm.new_source_file(FileName::Real(path.to_path_buf()).into(), source);
let lexer = Lexer::new(
Syntax::Es(EsSyntax::default()),
Default::default(),
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let module = parser.parse_module().ok()?;
self.analyze_module(&module);
Some(self.analysis.clone())
}
fn analyze_module(&mut self, module: &Module) {
let mut visitor = ExpressVisitor::new();
visitor.visit_module(module);
self.analysis.routes = visitor.routes;
self.analysis.middleware_count = visitor.middleware_count;
self.analysis.has_body_parser = visitor.has_body_parser;
self.analysis.has_auth_middleware = visitor.has_auth_middleware;
self.analysis.has_error_handler = visitor.has_error_handler;
self.analysis.async_handlers = visitor.async_handlers;
self.analysis.express_apps = visitor.express_apps;
self.analysis.classify_complexity();
self.analysis.detect_risky_patterns();
self.analysis.suggest_recipes();
}
}
impl Default for ExpressDetector {
fn default() -> Self {
Self::new()
}
}
struct ExpressVisitor {
routes: Vec<RouteInfo>,
middleware_count: usize,
has_body_parser: bool,
has_auth_middleware: bool,
has_error_handler: bool,
async_handlers: usize,
express_apps: Vec<usize>,
}
impl ExpressVisitor {
fn new() -> Self {
Self {
routes: Vec::new(),
middleware_count: 0,
has_body_parser: false,
has_auth_middleware: false,
has_error_handler: false,
async_handlers: 0,
express_apps: Vec::new(),
}
}
}
impl Visit for ExpressVisitor {
#[allow(clippy::collapsible_if)]
fn visit_call_expr(&mut self, call: &CallExpr) {
if let Callee::Expr(expr) = &call.callee {
if let Expr::Member(MemberExpr { obj, prop, .. }) = expr.as_ref() {
if let Expr::Ident(i) = obj.as_ref() {
if let MemberProp::Ident(p) = prop {
let _obj_name = i.sym.as_ref();
let method = p.sym.as_ref();
let http_methods =
["get", "post", "put", "delete", "patch", "head", "options"];
if http_methods.contains(&method) {
let path = self.extract_path(call);
let is_async = self.is_async_handler(call);
if is_async {
self.async_handlers += 1;
}
self.routes.push(RouteInfo {
method: method.to_string(),
path: path.unwrap_or_else(|| "unknown".to_string()),
handler_type: if is_async {
"async".to_string()
} else {
"sync".to_string()
},
middleware_count: 0,
estimated_complexity: 1,
});
}
if method == "use" {
self.middleware_count += 1;
if let Some(arg) = call.args.first() {
if let Expr::Ident(ident) = arg.expr.as_ref() {
let name = ident.sym.as_ref();
if name.contains("body") || name.contains("parser") {
self.has_body_parser = true;
}
if name.contains("auth") || name.contains("jwt") {
self.has_auth_middleware = true;
}
}
}
}
}
}
}
}
call.visit_children_with(self);
}
fn visit_var_decl(&mut self, decl: &VarDecl) {
for declarator in &decl.decls {
if let Some(init) = &declarator.init {
if let Expr::Call(call) = init.as_ref() {
if let Callee::Expr(expr) = &call.callee {
if let Expr::Ident(i) = expr.as_ref() {
let name = i.sym.as_ref();
if name == "express" || name == "Router" || name.contains("express") {
self.express_apps.push(declarator.name.span().lo.0 as usize);
}
}
}
}
}
}
decl.visit_children_with(self);
}
}
impl ExpressVisitor {
fn extract_path(&self, call: &CallExpr) -> Option<String> {
if let Some(first) = call.args.first() {
if let Expr::Lit(Lit::Str(s)) = first.expr.as_ref() {
return Some(s.value.to_string());
}
}
None
}
fn is_async_handler(&self, call: &CallExpr) -> bool {
if let Some(last) = call.args.last() {
if let Expr::Fn(f) = last.expr.as_ref() {
if let Some(body) = &f.function.body {
for stmt in body.stmts.iter().rev() {
if let Stmt::Return(ret) = stmt {
if let Some(arg) = &ret.arg {
if matches!(arg.as_ref(), Expr::Await(_)) {
return true;
}
}
}
}
}
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_routes() {
let source = r#"
const express = require('express');
const app = express();
app.get('/users', (req, res) => res.json([]));
app.post('/users', (req, res) => res.json(req.body));
"#;
let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
let source_str = source.to_string();
let fm = cm.new_source_file(FileName::Custom("test.js".into()).into(), source_str);
let lexer = Lexer::new(
Syntax::Es(EsSyntax::default()),
Default::default(),
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let module = parser.parse_module().unwrap();
let mut detector = ExpressDetector::new();
detector.analyze_module(&module);
assert!(detector.analysis.routes.len() >= 2);
}
}