morph_cli/recipes/express_to_fastify/
detect.rs1#![allow(clippy::all)]
2use std::path::Path;
3use swc_common::{FileName, SourceMap, Spanned, sync::Lrc};
4use swc_ecma_ast::*;
5use swc_ecma_parser::{EsSyntax, Parser, StringInput, Syntax, lexer::Lexer};
6use swc_ecma_visit::{Visit, VisitWith};
7
8use crate::recipes::express_to_fastify::analysis::{MigrationAnalysis, RouteInfo};
9
10pub struct ExpressDetector {
11 analysis: MigrationAnalysis,
12}
13
14impl ExpressDetector {
15 pub fn new() -> Self {
16 Self {
17 analysis: MigrationAnalysis::default(),
18 }
19 }
20
21 pub fn detect(&mut self, path: &Path) -> Option<MigrationAnalysis> {
22 let source = std::fs::read_to_string(path).ok()?;
23 let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
24 let fm = cm.new_source_file(FileName::Real(path.to_path_buf()).into(), source);
25
26 let lexer = Lexer::new(
27 Syntax::Es(EsSyntax::default()),
28 Default::default(),
29 StringInput::from(&*fm),
30 None,
31 );
32 let mut parser = Parser::new_from(lexer);
33 let module = parser.parse_module().ok()?;
34
35 self.analyze_module(&module);
36 Some(self.analysis.clone())
37 }
38
39 fn analyze_module(&mut self, module: &Module) {
40 let mut visitor = ExpressVisitor::new();
41 visitor.visit_module(module);
42
43 self.analysis.routes = visitor.routes;
44 self.analysis.middleware_count = visitor.middleware_count;
45 self.analysis.has_body_parser = visitor.has_body_parser;
46 self.analysis.has_auth_middleware = visitor.has_auth_middleware;
47 self.analysis.has_error_handler = visitor.has_error_handler;
48 self.analysis.async_handlers = visitor.async_handlers;
49 self.analysis.express_apps = visitor.express_apps;
50
51 self.analysis.classify_complexity();
52 self.analysis.detect_risky_patterns();
53 self.analysis.suggest_recipes();
54 }
55}
56
57impl Default for ExpressDetector {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63struct ExpressVisitor {
64 routes: Vec<RouteInfo>,
65 middleware_count: usize,
66 has_body_parser: bool,
67 has_auth_middleware: bool,
68 has_error_handler: bool,
69 async_handlers: usize,
70 express_apps: Vec<usize>,
71}
72
73impl ExpressVisitor {
74 fn new() -> Self {
75 Self {
76 routes: Vec::new(),
77 middleware_count: 0,
78 has_body_parser: false,
79 has_auth_middleware: false,
80 has_error_handler: false,
81 async_handlers: 0,
82 express_apps: Vec::new(),
83 }
84 }
85}
86
87impl Visit for ExpressVisitor {
88 #[allow(clippy::collapsible_if)]
89 fn visit_call_expr(&mut self, call: &CallExpr) {
90 if let Callee::Expr(expr) = &call.callee {
91 if let Expr::Member(MemberExpr { obj, prop, .. }) = expr.as_ref() {
92 if let Expr::Ident(i) = obj.as_ref() {
93 if let MemberProp::Ident(p) = prop {
94 let _obj_name = i.sym.as_ref();
95 let method = p.sym.as_ref();
96 let http_methods =
97 ["get", "post", "put", "delete", "patch", "head", "options"];
98
99 if http_methods.contains(&method) {
100 let path = self.extract_path(call);
101 let is_async = self.is_async_handler(call);
102 if is_async {
103 self.async_handlers += 1;
104 }
105 self.routes.push(RouteInfo {
106 method: method.to_string(),
107 path: path.unwrap_or_else(|| "unknown".to_string()),
108 handler_type: if is_async {
109 "async".to_string()
110 } else {
111 "sync".to_string()
112 },
113 middleware_count: 0,
114 estimated_complexity: 1,
115 });
116 }
117
118 if method == "use" {
119 self.middleware_count += 1;
120 if let Some(arg) = call.args.first() {
121 if let Expr::Ident(ident) = arg.expr.as_ref() {
122 let name = ident.sym.as_ref();
123 if name.contains("body") || name.contains("parser") {
124 self.has_body_parser = true;
125 }
126 if name.contains("auth") || name.contains("jwt") {
127 self.has_auth_middleware = true;
128 }
129 }
130 }
131 }
132 }
133 }
134 }
135 }
136
137 call.visit_children_with(self);
138 }
139
140 fn visit_var_decl(&mut self, decl: &VarDecl) {
141 for declarator in &decl.decls {
142 if let Some(init) = &declarator.init {
143 if let Expr::Call(call) = init.as_ref() {
144 if let Callee::Expr(expr) = &call.callee {
145 if let Expr::Ident(i) = expr.as_ref() {
146 let name = i.sym.as_ref();
147 if name == "express" || name == "Router" || name.contains("express") {
148 self.express_apps.push(declarator.name.span().lo.0 as usize);
149 }
150 }
151 }
152 }
153 }
154 }
155 decl.visit_children_with(self);
156 }
157}
158
159impl ExpressVisitor {
160 fn extract_path(&self, call: &CallExpr) -> Option<String> {
161 if let Some(first) = call.args.first() {
162 if let Expr::Lit(Lit::Str(s)) = first.expr.as_ref() {
163 return Some(s.value.to_string());
164 }
165 }
166 None
167 }
168
169 fn is_async_handler(&self, call: &CallExpr) -> bool {
170 if let Some(last) = call.args.last() {
171 if let Expr::Fn(f) = last.expr.as_ref() {
172 if let Some(body) = &f.function.body {
173 for stmt in body.stmts.iter().rev() {
174 if let Stmt::Return(ret) = stmt {
175 if let Some(arg) = &ret.arg {
176 if matches!(arg.as_ref(), Expr::Await(_)) {
177 return true;
178 }
179 }
180 }
181 }
182 }
183 }
184 }
185 false
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_detect_routes() {
195 let source = r#"
196 const express = require('express');
197 const app = express();
198 app.get('/users', (req, res) => res.json([]));
199 app.post('/users', (req, res) => res.json(req.body));
200 "#;
201 let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
202 let source_str = source.to_string();
203 let fm = cm.new_source_file(FileName::Custom("test.js".into()).into(), source_str);
204 let lexer = Lexer::new(
205 Syntax::Es(EsSyntax::default()),
206 Default::default(),
207 StringInput::from(&*fm),
208 None,
209 );
210 let mut parser = Parser::new_from(lexer);
211 let module = parser.parse_module().unwrap();
212
213 let mut detector = ExpressDetector::new();
214 detector.analyze_module(&module);
215 assert!(detector.analysis.routes.len() >= 2);
216 }
217}