#![allow(clippy::all)]
use swc_common::{FileName, SourceMap, SourceMapper, Spanned, sync::Lrc};
use swc_ecma_ast::*;
use swc_ecma_parser::{Parser, StringInput, Syntax, lexer::Lexer};
use swc_ecma_visit::{Visit, VisitWith};
#[derive(Debug, Clone, Default)]
pub struct TransformStats {
pub express_init_count: usize,
pub route_count: usize,
pub router_count: usize,
pub async_handlers: usize,
pub middleware_count: usize,
pub res_json_calls: usize,
pub res_send_calls: usize,
}
#[derive(Clone)]
#[allow(dead_code)]
enum TransformChange {
ExpressInit,
RouterInit,
ImportMigration { _old: String, new: String },
BodyParserPlugin,
Replacement { original: String, replacement: String },
}
impl TransformChange {
fn apply(&self, source: &str) -> String {
match self {
Self::ExpressInit => source
.replace("express()", "fastify()")
.replace("require('express')", "require('fastify')")
.replace(" from 'express'", " from 'fastify'"),
Self::RouterInit => source.replace("Router()", "Router()"),
Self::ImportMigration { new, .. } => {
source.replace(" from 'express'", &format!(" from '{}'", new))
}
Self::BodyParserPlugin => source
.replace("bodyParser.json()", "// TODO: register @fastify/json")
.replace("body-parser", "// TODO: @fastify/json"),
Self::Replacement { original, replacement } => source.replace(original, replacement),
}
}
}
pub struct ExpressToFastifyTransform {
warnings: Vec<String>,
unsupported: Vec<String>,
stats: TransformStats,
}
impl ExpressToFastifyTransform {
pub fn new() -> Self {
Self {
warnings: Vec::new(),
unsupported: Vec::new(),
stats: TransformStats::default(),
}
}
pub fn transform_source(&mut self, source: &str, path: &std::path::Path) -> TransformOutcome {
let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
let fm = cm.new_source_file(
FileName::Real(path.to_path_buf()).into(),
source.to_string(),
);
let syntax = if path.to_string_lossy().ends_with(".ts")
|| path.to_string_lossy().ends_with(".tsx")
{
Syntax::Typescript(Default::default())
} else {
Syntax::Es(Default::default())
};
let lexer = Lexer::new(syntax, Default::default(), StringInput::from(&*fm), None);
let mut parser = Parser::new_from(lexer);
let module = match parser.parse_module() {
Ok(m) => m,
Err(_) => return TransformOutcome::unsupported("Parse error".to_string()),
};
let mut visitor = TransformVisitor::new(cm.clone());
visitor.visit_module(&module);
let changes = visitor.changes.clone();
let changes_empty = changes.is_empty();
self.stats = visitor.stats.clone();
self.warnings = visitor.warnings.clone();
self.unsupported = visitor.unsupported.clone();
let mut output = source.to_string();
for change in changes {
output = change.apply(&output);
}
output = output.replace(";;", ";");
TransformOutcome {
transformed: output,
changed: !changes_empty,
warnings: self.warnings.clone(),
unsupported: self.unsupported.clone(),
stats: self.stats.clone(),
}
}
}
impl Default for ExpressToFastifyTransform {
fn default() -> Self {
Self::new()
}
}
struct TransformVisitor {
changes: Vec<TransformChange>,
warnings: Vec<String>,
unsupported: Vec<String>,
stats: TransformStats,
cm: Lrc<SourceMap>,
processed_spans: Vec<swc_common::Span>,
}
struct RouteChain {
base_obj: String,
path: String,
calls: Vec<(String, Vec<String>)>,
_span: swc_common::Span,
}
fn extract_route_chain(call: &CallExpr, cm: &SourceMap) -> Option<RouteChain> {
let mut current_expr = Expr::Call(call.clone());
let mut calls = Vec::new();
let mut path = None;
let mut base_obj = None;
loop {
let temp_expr = current_expr.clone();
match temp_expr {
Expr::Call(c) => {
let mut is_route_method = false;
if let Callee::Expr(callee_expr) = &c.callee {
if let Expr::Member(MemberExpr { obj, prop, .. }) = callee_expr.as_ref() {
if let MemberProp::Ident(method_ident) = prop {
let method_name = method_ident.sym.as_ref().to_lowercase();
if ["get", "post", "put", "delete", "patch", "head", "options"].contains(&method_name.as_str()) {
is_route_method = true;
let args_list: Vec<String> = c.args.iter().map(|arg| {
cm.span_to_snippet(arg.span()).unwrap_or_default()
}).collect();
calls.push((method_name, args_list));
current_expr = *obj.clone();
}
}
}
}
if !is_route_method {
if let Callee::Expr(callee_expr) = &c.callee {
if let Expr::Member(MemberExpr { obj, prop, .. }) = callee_expr.as_ref() {
if let MemberProp::Ident(prop_ident) = prop {
if prop_ident.sym.as_ref() == "route" {
if let Expr::Ident(obj_ident) = obj.as_ref() {
base_obj = Some(obj_ident.sym.as_ref().to_string());
}
if let Some(first_arg) = c.args.first() {
path = Some(cm.span_to_snippet(first_arg.span()).unwrap_or_default());
}
}
}
}
}
break;
}
}
_ => break,
}
}
if let (Some(base), Some(p), false) = (base_obj, path, calls.is_empty()) {
calls.reverse();
Some(RouteChain {
base_obj: base,
path: p,
calls,
_span: call.span(),
})
} else {
None
}
}
fn migrate_handler(handler_src: &str) -> String {
let mut result = handler_src.to_string();
result = result.replace("(req, res, next)", "(req, reply, next)");
result = result.replace("(request, response, next)", "(request, reply, next)");
result = result.replace("(req, res)", "(req, reply)");
result = result.replace("(request, response)", "(request, reply)");
result = result.replace("async (req, res, next)", "async (req, reply, next)");
result = result.replace("async (request, response, next)", "async (request, reply, next)");
result = result.replace("async (req, res)", "async (req, reply)");
result = result.replace("async (request, response)", "async (request, reply)");
result = result.replace("res.status(", "reply.status(");
result = result.replace("response.status(", "reply.status(");
result = result.replace("res.send(", "reply.send(");
result = result.replace("response.send(", "reply.send(");
result = result.replace("res.json(", "reply.send(");
result = result.replace("response.json(", "reply.send(");
result = result.replace("res.sendStatus(", "reply.status(");
result = result.replace(".json(", ".send(");
result = result.replace("res.", "reply.");
result = result.replace("response.", "reply.");
let mut final_res = String::new();
let mut remaining = result.as_str();
while let Some(idx) = remaining.find("req.param(") {
final_res.push_str(&remaining[..idx]);
let rest = &remaining[idx + 10..];
if let Some(end_idx) = rest.find(')') {
let param_arg = rest[..end_idx].trim();
let clean_name = param_arg.trim_matches(|c| c == '\'' || c == '"');
final_res.push_str(&format!("req.params.{}", clean_name));
remaining = &rest[end_idx + 1..];
} else {
final_res.push_str("req.param(");
remaining = rest;
}
}
final_res.push_str(remaining);
result = final_res;
let mut final_res_req = String::new();
let mut remaining_req = result.as_str();
while let Some(idx) = remaining_req.find("request.param(") {
final_res_req.push_str(&remaining_req[..idx]);
let rest = &remaining_req[idx + 14..];
if let Some(end_idx) = rest.find(')') {
let param_arg = rest[..end_idx].trim();
let clean_name = param_arg.trim_matches(|c| c == '\'' || c == '"');
final_res_req.push_str(&format!("request.params.{}", clean_name));
remaining_req = &rest[end_idx + 1..];
} else {
final_res_req.push_str("request.param(");
remaining_req = rest;
}
}
final_res_req.push_str(remaining_req);
result = final_res_req;
result
}
fn migrate_route_call(
base: &str,
method: &str,
path: &str,
args: &[String],
) -> String {
if args.is_empty() {
return format!("{}.{}({})", base, method, path);
}
if args.len() == 1 {
let migrated_handler = migrate_handler(&args[0]);
return format!("{}.{}({}, {})", base, method, path, migrated_handler);
}
let middlewares = &args[0..args.len() - 1];
let handler = &args[args.len() - 1];
let migrated_handler = migrate_handler(handler);
format!(
"{}.{}({}, {{ preHandler: [{}] }}, {})",
base,
method,
path,
middlewares.join(", "),
migrated_handler
)
}
fn migrate_use_call(
base: &str,
args: &[String],
) -> String {
if args.is_empty() {
return format!("{}.register()", base);
}
if args.len() == 1 {
let mw = &args[0];
let mw_ref = if mw.ends_with("()") {
&mw[..mw.len() - 2]
} else {
mw
};
return format!("{}.register({})", base, mw_ref);
}
let path = &args[0];
let last_arg = &args[args.len() - 1];
let middlewares = &args[1..args.len() - 1];
if middlewares.is_empty() {
return format!("{}.register({}, {{ prefix: {} }})", base, last_arg, path);
}
let mw_refs: Vec<String> = middlewares.iter().map(|mw| {
if mw.ends_with("()") {
mw[..mw.len() - 2].to_string()
} else {
mw.clone()
}
}).collect();
format!(
"{}.register({}, {{ prefix: {}, preHandler: [{}] }})",
base,
last_arg,
path,
mw_refs.join(", ")
)
}
impl TransformVisitor {
fn new(cm: Lrc<SourceMap>) -> Self {
Self {
changes: Vec::new(),
warnings: Vec::new(),
unsupported: Vec::new(),
stats: TransformStats::default(),
cm,
processed_spans: Vec::new(),
}
}
fn add_express_init(&mut self) {
if !self
.changes
.iter()
.any(|c| matches!(c, TransformChange::ExpressInit))
{
self.changes.push(TransformChange::ExpressInit);
self.stats.express_init_count += 1;
}
}
fn check_express_helpers(&mut self, method_name: &str) {
match method_name {
"download" => {
self.warnings.push("res.download() is Express-specific. Use reply.send() or @fastify/static instead.".to_string());
}
"sendFile" => {
self.warnings.push("res.sendFile() is Express-specific. Use @fastify/static to serve static files.".to_string());
}
"render" => {
self.warnings.push("res.render() is Express-specific. Use @fastify/view to render templates.".to_string());
}
"redirect" => {
self.warnings.push("res.redirect() is Express-specific. Use reply.redirect() instead.".to_string());
}
"cookie" => {
self.warnings.push("res.cookie() is Express-specific. Use @fastify/cookie to set cookies.".to_string());
}
"clearCookie" => {
self.warnings.push("res.clearCookie() is Express-specific. Use @fastify/cookie to clear cookies.".to_string());
}
"attachment" => {
self.warnings.push("res.attachment() is Express-specific. Use custom reply headers instead.".to_string());
}
"format" => {
self.warnings.push("res.format() is Express-specific. Perform content negotiation manually in Fastify.".to_string());
}
"links" => {
self.warnings.push("res.links() is Express-specific. Set the Link header manually.".to_string());
}
"location" => {
self.warnings.push("res.location() is Express-specific. Use reply.redirect() or set Location header manually.".to_string());
}
"vary" => {
self.warnings.push("res.vary() is Express-specific. Use reply.header('Vary', ...) instead.".to_string());
}
"type" | "contentType" => {
self.warnings.push("res.type() is Express-specific. Use reply.type() instead.".to_string());
}
"append" => {
self.warnings.push("res.append() is Express-specific. Use reply.header() instead.".to_string());
}
"set" | "header" => {
self.warnings.push("res.set() / res.header() is Express-specific. Use reply.header() instead.".to_string());
}
"get" => {
self.warnings.push("res.get() is Express-specific. Use reply.getHeader() instead.".to_string());
}
"locals" => {
self.warnings.push("res.locals is Express-specific. Use request/reply decorators instead.".to_string());
}
"write" => {
self.warnings.push("res.write() is Express-specific low-level stream. Fastify handles streams by returning them from the handler.".to_string());
}
"end" => {
self.warnings.push("res.end() is Express-specific low-level stream. Use reply.send() instead.".to_string());
}
_ => {}
}
}
fn check_middleware_warnings(&mut self, middleware_src: &str) {
let lower = middleware_src.to_lowercase();
if lower.contains("passport") {
self.unsupported.push("passport - use @fastify/passport instead".to_string());
} else if lower.contains("session") {
self.warnings.push("session - use @fastify/session instead".to_string());
} else if lower.contains("multer") {
self.unsupported.push("multer - use @fastify/multipart instead".to_string());
} else if lower.contains("csrf") {
self.warnings.push("csrf - use @fastify/csrf-protection instead".to_string());
} else if lower.contains("helmet") {
self.warnings.push("helmet - use @fastify/helmet instead".to_string());
} else if lower.contains("cors") {
self.warnings.push("cors - use @fastify/cors instead".to_string());
} else if lower.contains("morgan") {
self.warnings.push("morgan - use fastify's built-in logger instead".to_string());
} else if lower.contains("cookieparser") || lower.contains("cookie-parser") {
self.warnings.push("cookie-parser - use @fastify/cookie instead".to_string());
} else if lower.contains("compression") {
self.warnings.push("compression - use @fastify/compress instead".to_string());
} else if lower.contains("body-parser") || lower.contains("bodyparser") {
self.warnings.push("body-parser - Fastify parses JSON bodies by default. For URL-encoded forms, use @fastify/formbody.".to_string());
} else if lower.contains("cookie-session") || lower.contains("cookiesession") {
self.warnings.push("cookie-session - use @fastify/session or @fastify/secure-session instead.".to_string());
} else if lower.contains("express-validator") || lower.contains("expressvalidator") {
self.warnings.push("express-validator - use fastify's built-in schema validation (AJV) instead.".to_string());
} else if lower.contains("serve-static") || lower.contains("servestatic") {
self.warnings.push("serve-static - use @fastify/static instead.".to_string());
} else if lower.contains("method-override") || lower.contains("methodoverride") {
self.warnings.push("method-override - use @fastify/method-override instead.".to_string());
} else if lower.contains("connect-flash") || lower.contains("flash") {
self.warnings.push("connect-flash - use @fastify/flash instead.".to_string());
}
}
}
impl Visit for TransformVisitor {
fn visit_import_decl(&mut self, import: &ImportDecl) {
let src_str = import.src.value.to_string();
if src_str.contains("express") && !src_str.contains("fastify") {
if src_str.contains("socket.io") || src_str.contains("ws") {
self.unsupported
.push("socket.io/ws - manual migration required".to_string());
} else {
self.changes.push(TransformChange::ImportMigration {
_old: src_str.clone(),
new: "fastify".to_string(),
});
self.add_express_init();
}
}
}
fn visit_call_expr(&mut self, call: &CallExpr) {
if let Some(chain) = extract_route_chain(call, &self.cm) {
let span = call.span();
if !self.processed_spans.iter().any(|ps| ps.contains(span)) {
self.processed_spans.push(span);
let mut replacement = String::new();
for (method, args) in &chain.calls {
let migrated_call = migrate_route_call(&chain.base_obj, method, &chain.path, args);
replacement.push_str(&migrated_call);
replacement.push_str(";\n");
}
let original = self.cm.span_to_snippet(span).unwrap_or_default();
self.changes.push(TransformChange::Replacement {
original,
replacement,
});
self.stats.route_count += chain.calls.len();
for (_method, args) in &chain.calls {
if args.iter().any(|arg| arg.contains("async ")) {
self.stats.async_handlers += 1;
}
}
}
call.visit_children_with(self);
return;
}
if let Callee::Expr(expr) = &call.callee {
match expr.as_ref() {
Expr::Ident(i) if i.sym.as_ref() == "express" => {
self.add_express_init();
}
Expr::Member(MemberExpr { obj, prop, .. }) => {
if let Expr::Ident(i) = obj.as_ref() {
let obj_name = i.sym.as_ref();
if let MemberProp::Ident(p) = prop {
let method = p.sym.as_ref();
if method == "Router" && (obj_name == "express" || obj_name == "Router")
{
self.changes.push(TransformChange::RouterInit);
self.stats.router_count += 1;
} else if ["get", "post", "put", "delete", "patch", "head", "options"]
.contains(&method)
{
if obj_name == "app" || obj_name == "router" || obj_name == "server" || obj_name == "api" {
let span = call.span();
if !self.processed_spans.iter().any(|ps| ps.contains(span)) {
self.processed_spans.push(span);
let original = self.cm.span_to_snippet(span).unwrap_or_default();
let args: Vec<String> = call.args.iter().map(|arg| {
self.cm.span_to_snippet(arg.span()).unwrap_or_default()
}).collect();
if !args.is_empty() {
let path = &args[0];
let rest_args = &args[1..];
let replacement = migrate_route_call(obj_name, method, path, rest_args);
self.changes.push(TransformChange::Replacement {
original,
replacement,
});
self.stats.route_count += 1;
if let Some(last) = call.args.last() {
if self.is_async(&last.expr) {
self.stats.async_handlers += 1;
}
}
}
}
}
} else if method == "use" && (obj_name == "app" || obj_name == "router" || obj_name == "server" || obj_name == "api")
{
let span = call.span();
if !self.processed_spans.iter().any(|ps| ps.contains(span)) {
self.processed_spans.push(span);
let original = self.cm.span_to_snippet(span).unwrap_or_default();
let args: Vec<String> = call.args.iter().map(|arg| {
self.cm.span_to_snippet(arg.span()).unwrap_or_default()
}).collect();
for arg in &args {
self.check_middleware_warnings(arg);
}
let replacement = migrate_use_call(obj_name, &args);
self.changes.push(TransformChange::Replacement {
original,
replacement,
});
self.stats.middleware_count += 1;
}
}
}
}
}
_ => {}
}
}
call.visit_children_with(self);
}
fn visit_member_expr(&mut self, expr: &MemberExpr) {
if let MemberProp::Ident(p) = &expr.prop {
let prop = p.sym.as_ref();
if let Expr::Ident(i) = expr.obj.as_ref() {
let obj_name = i.sym.as_ref();
if obj_name == "res" || obj_name == "response" {
if prop == "json" {
self.stats.res_json_calls += 1;
} else if prop == "send" {
self.stats.res_send_calls += 1;
}
self.check_express_helpers(prop);
} else if obj_name == "req" || obj_name == "request" {
if prop == "path" {
self.warnings.push("req.path is Express-specific. Use req.routerPath or req.url instead.".to_string());
} else if prop == "xhr" {
self.warnings.push("req.xhr is Express-specific. Check req.headers['x-requested-with'] instead.".to_string());
}
}
}
}
expr.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 == "request" || name == "res" || name == "response" {
if let MemberProp::Ident(p) = &member.prop {
let prop = p.sym.as_ref();
if !["session", "user", "body", "query", "params", "headers"].contains(&prop) {
self.warnings.push(format!(
"Unsafe route mutation: assigning directly to {}.{} is discouraged in Fastify. Use decorators instead.",
name, prop
));
}
}
}
}
}
}
expr.visit_children_with(self);
}
}
impl TransformVisitor {
fn is_async(&self, expr: &Expr) -> bool {
if let Expr::Arrow(arrow) = expr {
return arrow.is_async;
}
if let Expr::Fn(f) = expr {
return f.function.is_async;
}
false
}
}
#[derive(Debug, Clone)]
pub struct TransformOutcome {
pub transformed: String,
pub changed: bool,
pub warnings: Vec<String>,
pub unsupported: Vec<String>,
pub stats: TransformStats,
}
impl TransformOutcome {
fn unsupported(reason: String) -> Self {
Self {
transformed: String::new(),
changed: false,
warnings: vec![],
unsupported: vec![reason],
stats: TransformStats::default(),
}
}
pub fn confidence_summary(&self) -> String {
let supported = self.stats.express_init_count + self.stats.route_count;
let total = supported + self.unsupported.len();
if total == 0 {
"No Express patterns detected".to_string()
} else {
format!("Confidence: {}/{} transforms supported", supported, total)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_express_to_fastify_basic() {
let source = r#"const express = require('express');
const app = express();
app.get('/', (req, res) => res.json({ ok: true }));
app.listen(3000);"#;
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(result.changed);
assert!(result.transformed.contains("fastify()"));
}
#[test]
fn test_express_import() {
let source = r#"import express from 'express';
const app = express();"#;
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(result.changed);
assert!(result.transformed.contains("fastify"));
}
#[test]
fn test_router_init() {
let source = r#"const router = express.Router();"#;
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(result.changed);
}
#[test]
fn test_all_http_methods() {
let source = "app.get('/users', handler); app.post('/users', h);";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(result.changed);
assert!(result.stats.route_count >= 2);
}
#[test]
fn test_async_handler() {
let source = r#"app.get('/async', async (req, res) => {});"#;
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert_eq!(result.stats.async_handlers, 1);
}
#[test]
fn test_no_change_for_non_express() {
let source = "const x = 1;";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(!result.changed);
}
#[test]
fn test_middleware() {
let source = "app.use(cors());";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(result.stats.middleware_count >= 1);
}
#[test]
fn test_res_json() {
let source = "res.json({ ok: true });";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert_eq!(result.stats.res_json_calls, 1);
}
#[test]
fn test_res_send() {
let source = "res.send('hello');";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert_eq!(result.stats.res_send_calls, 1);
}
#[test]
fn test_confidence_summary() {
let source = "const app = express(); app.get('/', h);";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
let summary = result.confidence_summary();
assert!(summary.contains("Confidence"));
}
#[test]
fn test_typescript() {
let source = r#"import express, { Application } from 'express';
const app: Application = express();"#;
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.ts"));
assert!(result.changed);
}
#[test]
fn test_router_chaining() {
let source = "router.route('/users').get(h1).post(h2);";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(result.changed);
assert!(result.transformed.contains("router.get('/users', h1);"));
assert!(result.transformed.contains("router.post('/users', h2);"));
}
#[test]
fn test_router_chaining_middleware() {
let source = "router.route('/users').get(m1, h1).post(m2, m3, h2);";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(result.changed);
assert!(result.transformed.contains("router.get('/users', { preHandler: [m1] }, h1);"));
assert!(result.transformed.contains("router.post('/users', { preHandler: [m2, m3] }, h2);"));
}
#[test]
fn test_simple_middleware_and_handler() {
let source = "app.get('/users', m1, m2, (req, res) => { res.status(200).json({ ok: true }); });";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(result.changed);
assert!(result.transformed.contains("{ preHandler: [m1, m2] }"));
assert!(result.transformed.contains("(req, reply)"));
assert!(result.transformed.contains("reply.status(200).send({ ok: true })"));
}
#[test]
fn test_unsafe_route_mutations() {
let source = "app.get('/', (req, res) => { req.customProp = 'unsafe'; });";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(!result.warnings.is_empty());
assert!(result.warnings[0].contains("Unsafe route mutation"));
}
#[test]
fn test_query_params_access() {
let source = "app.get('/', (req, res) => { const name = req.param('name'); const id = req.query.id; });";
let mut transform = ExpressToFastifyTransform::new();
let result = transform.transform_source(source, std::path::Path::new("test.js"));
assert!(result.changed);
assert!(result.transformed.contains("req.params.name"));
assert!(result.transformed.contains("req.query.id"));
}
}