Skip to main content

morph_cli/recipes/express_to_fastify/
detect.rs

1#![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}