Skip to main content

infigraph_core/routes/
mod.rs

1mod csharp;
2mod elixir;
3mod generic;
4mod go;
5mod helpers;
6mod java;
7mod js_ts;
8mod php;
9mod python;
10mod ruby;
11mod rust_lang;
12
13use anyhow::Result;
14use serde::{Deserialize, Serialize};
15
16use crate::graph::GraphQuery;
17
18use helpers::{detect_from_docstring, language_from_file, Lang};
19
20use csharp::detect_csharp_route;
21use elixir::detect_elixir_route;
22use generic::detect_generic_route;
23use go::detect_go_route;
24use java::detect_java_route;
25use js_ts::detect_js_ts_route;
26use php::detect_php_route;
27use python::detect_python_route;
28use ruby::detect_ruby_route;
29use rust_lang::detect_rust_route;
30
31/// A detected HTTP route/endpoint in the codebase.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Route {
34    /// HTTP method (GET, POST, PUT, DELETE, PATCH, or UNKNOWN)
35    pub method: String,
36    /// Inferred URL path (best-effort from symbol/docstring heuristics)
37    pub path: String,
38    /// Symbol ID of the handler function
39    pub handler_id: String,
40    /// File containing the handler
41    pub file: String,
42    /// Detected web framework (e.g. "flask", "express", "spring", "actix")
43    pub framework: String,
44}
45
46/// Detect HTTP routes/endpoints from the indexed code graph using heuristics.
47///
48/// Queries symbols and applies language-aware pattern matching on names and
49/// docstrings to identify likely HTTP handlers. This is intentionally broad
50/// to catch routes across many web frameworks.
51pub fn detect_routes(gq: &GraphQuery) -> Result<Vec<Route>> {
52    let rows = gq.raw_query(
53        "MATCH (s:Symbol) WHERE s.kind IN ['Function', 'Method'] \
54         RETURN s.id, s.name, s.kind, s.file, s.docstring",
55    )?;
56
57    let mut routes = Vec::new();
58
59    for row in &rows {
60        let id = &row[0];
61        let name = &row[1];
62        let _kind = &row[2];
63        let file = &row[3];
64        let docstring = row.get(4).map(|s| s.as_str()).unwrap_or("");
65
66        if let Some(route) = detect_route_from_symbol(id, name, file, docstring) {
67            routes.push(route);
68        }
69    }
70
71    // Sort by file, then path for stable output
72    routes.sort_by(|a, b| a.file.cmp(&b.file).then(a.path.cmp(&b.path)));
73
74    Ok(routes)
75}
76
77/// Try to detect a route from a single symbol's name and docstring.
78fn detect_route_from_symbol(id: &str, name: &str, file: &str, docstring: &str) -> Option<Route> {
79    let name_lower = name.to_lowercase();
80    let doc_lower = docstring.to_lowercase();
81
82    // Determine language from file extension
83    let lang = language_from_file(file);
84
85    // Try docstring-based detection first (strongest signal — often contains
86    // explicit route/endpoint annotations captured as docstrings)
87    if let Some(route) = detect_from_docstring(id, name, file, &doc_lower) {
88        return Some(route);
89    }
90
91    // Then try name-based heuristics per language
92    match lang {
93        Lang::Python => detect_python_route(id, name, &name_lower, file, &doc_lower),
94        Lang::JavaScript | Lang::TypeScript => {
95            detect_js_ts_route(id, name, &name_lower, file, &doc_lower)
96        }
97        Lang::Go => detect_go_route(id, name, &name_lower, file, &doc_lower),
98        Lang::Java => detect_java_route(id, name, &name_lower, file, &doc_lower),
99        Lang::Rust => detect_rust_route(id, name, &name_lower, file, &doc_lower),
100        Lang::Ruby => detect_ruby_route(id, name, &name_lower, file, &doc_lower),
101        Lang::Php => detect_php_route(id, name, &name_lower, file, &doc_lower),
102        Lang::CSharp => detect_csharp_route(id, name, &name_lower, file, &doc_lower),
103        Lang::Elixir => detect_elixir_route(id, name, &name_lower, file, &doc_lower),
104        Lang::Other => detect_generic_route(id, name, &name_lower, file, &doc_lower),
105    }
106}
107
108/// Format routes as a displayable string.
109pub fn format_routes(routes: &[Route]) -> String {
110    if routes.is_empty() {
111        return "No HTTP routes detected.".to_string();
112    }
113
114    let mut out = format!("Detected {} HTTP route(s):\n\n", routes.len());
115
116    let mut current_file = "";
117    for route in routes {
118        if route.file != current_file {
119            current_file = &route.file;
120            out.push_str(&format!("  {}:\n", current_file));
121        }
122        out.push_str(&format!(
123            "    {:>7} {:30} [{:15}] [{}]\n",
124            route.method, route.path, route.framework, route.handler_id
125        ));
126    }
127
128    out
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use helpers::{camel_to_path, extract_path_from_text};
135
136    #[test]
137    fn test_python_get_prefix() {
138        let route = detect_route_from_symbol("views.py::get_users", "get_users", "views.py", "");
139        assert!(route.is_some());
140        let r = route.unwrap();
141        assert_eq!(r.method, "GET");
142        assert_eq!(r.path, "/users");
143    }
144
145    #[test]
146    fn test_python_post_prefix() {
147        let route = detect_route_from_symbol("views.py::post_order", "post_order", "views.py", "");
148        assert!(route.is_some());
149        let r = route.unwrap();
150        assert_eq!(r.method, "POST");
151        assert_eq!(r.path, "/order");
152    }
153
154    #[test]
155    fn test_python_handler_suffix() {
156        let route =
157            detect_route_from_symbol("views.py::user_handler", "user_handler", "views.py", "");
158        assert!(route.is_some());
159        let r = route.unwrap();
160        assert_eq!(r.path, "/user");
161    }
162
163    #[test]
164    fn test_go_handler_suffix() {
165        let route = detect_route_from_symbol("api.go::UsersHandler", "UsersHandler", "api.go", "");
166        assert!(route.is_some());
167        let r = route.unwrap();
168        assert!(r.path.contains("users"));
169    }
170
171    #[test]
172    fn test_go_serve_http() {
173        let route = detect_route_from_symbol(
174            "server.go::MyHandler::ServeHTTP",
175            "ServeHTTP",
176            "server.go",
177            "",
178        );
179        assert!(route.is_some());
180    }
181
182    #[test]
183    fn test_js_handler() {
184        let route =
185            detect_route_from_symbol("api/users.ts::handler", "handler", "api/users.ts", "");
186        assert!(route.is_some());
187    }
188
189    #[test]
190    fn test_docstring_route() {
191        let route = detect_route_from_symbol(
192            "app.py::list_items",
193            "list_items",
194            "app.py",
195            "GET /api/items endpoint",
196        );
197        assert!(route.is_some());
198        let r = route.unwrap();
199        assert_eq!(r.method, "GET");
200        assert_eq!(r.path, "/api/items");
201    }
202
203    #[test]
204    fn test_java_controller_file() {
205        let route = detect_route_from_symbol(
206            "UserController.java::UserController::getUsers",
207            "getUsers",
208            "com/example/controller/UserController.java",
209            "",
210        );
211        assert!(route.is_some());
212        let r = route.unwrap();
213        assert_eq!(r.method, "GET");
214    }
215
216    #[test]
217    fn test_no_false_positive_regular_function() {
218        let route =
219            detect_route_from_symbol("utils.py::format_string", "format_string", "utils.py", "");
220        assert!(route.is_none());
221    }
222
223    #[test]
224    fn test_extract_path_from_text() {
225        assert_eq!(
226            extract_path_from_text("route \"/api/users\""),
227            Some("/api/users".to_string())
228        );
229        assert_eq!(
230            extract_path_from_text("GET /api/items endpoint"),
231            Some("/api/items".to_string())
232        );
233    }
234
235    #[test]
236    fn test_camel_to_path() {
237        assert_eq!(camel_to_path("users"), "users");
238        assert_eq!(camel_to_path("user_profile"), "user/profile");
239    }
240}