use std::collections::HashMap;
use super::{make_route_id, RouteEdge, RouteNode};
use crate::code_tree::models::{ConstantInfo, FunctionInfo};
const FRAMEWORK: &str = "django";
pub(super) fn detect(
constants: &[ConstantInfo],
functions: &[FunctionInfo],
) -> (Vec<RouteNode>, Vec<RouteEdge>) {
let mut by_name: HashMap<&str, Vec<&str>> = HashMap::new();
for f in functions {
by_name
.entry(f.name.as_str())
.or_default()
.push(f.qualified_name.as_str());
}
let mut nodes = Vec::new();
let mut edges = Vec::new();
for c in constants {
if c.name != "urlpatterns" {
continue;
}
let Some(preview) = c.value_preview.as_deref() else {
continue;
};
for entry in iter_path_calls(preview) {
let id = make_route_id(FRAMEWORK, &entry.method, &entry.path);
nodes.push(RouteNode {
id: id.clone(),
name: entry.path.clone(),
path: entry.path,
method: entry.method,
framework: FRAMEWORK.to_string(),
file_path: c.file_path.clone(),
line_number: c.line_number,
});
if let Some(view) = entry.view_name.as_deref() {
let bare = strip_to_bare_name(view);
if let Some(candidates) = by_name.get(bare) {
if candidates.len() == 1 {
edges.push(RouteEdge {
route_id: id,
function_qname: candidates[0].to_string(),
});
}
}
}
}
}
(nodes, edges)
}
struct PathCall {
method: String,
path: String,
view_name: Option<String>,
}
fn iter_path_calls(preview: &str) -> Vec<PathCall> {
let mut out = Vec::new();
let bytes = preview.as_bytes();
let mut i = 0;
while i < bytes.len() {
let kind_len = if preview[i..].starts_with("path(") {
"path(".len()
} else if preview[i..].starts_with("re_path(") {
"re_path(".len()
} else if preview[i..].starts_with("url(") {
"url(".len()
} else {
i += 1;
continue;
};
let body_start = i + kind_len;
let body_end = match scan_balanced(bytes, body_start, b'(', b')') {
Some(j) => j,
None => break, };
let args = &preview[body_start..body_end];
let Some(path) = first_call_arg_string(args) else {
i = body_end + 1;
continue;
};
let view_name = second_call_arg_ident(args);
out.push(PathCall {
method: "ANY".to_string(),
path,
view_name,
});
i = body_end + 1;
}
out
}
fn scan_balanced(bytes: &[u8], from: usize, open: u8, close: u8) -> Option<usize> {
let mut depth = 1i32;
let mut in_quote: Option<u8> = None;
let mut i = from;
while i < bytes.len() {
let c = bytes[i];
if let Some(q) = in_quote {
if c == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if c == q {
in_quote = None;
}
} else {
match c {
b'\'' | b'"' => in_quote = Some(c),
x if x == open => depth += 1,
x if x == close => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
i += 1;
}
None
}
fn first_call_arg_string(args: &str) -> Option<String> {
super::first_string_literal(args)
}
fn second_call_arg_ident(args: &str) -> Option<String> {
let bytes = args.as_bytes();
let first_end = skip_first_arg(bytes)?;
let mut i = first_end + 1;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b',') {
i += 1;
}
if i >= bytes.len() {
return None;
}
let mut depth = 0i32;
let mut in_quote: Option<u8> = None;
let mut j = i;
while j < bytes.len() {
let c = bytes[j];
if let Some(q) = in_quote {
if c == b'\\' && j + 1 < bytes.len() {
j += 2;
continue;
}
if c == q {
in_quote = None;
}
} else {
match c {
b'\'' | b'"' => in_quote = Some(c),
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b',' if depth == 0 => break,
_ => {}
}
}
j += 1;
}
let raw = args[i..j].trim();
if raw.starts_with("include(") {
return None;
}
Some(raw.to_string())
}
fn skip_first_arg(bytes: &[u8]) -> Option<usize> {
let mut i = 0;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= bytes.len() {
return None;
}
let q = bytes[i];
if q != b'\'' && q != b'"' {
return None;
}
let mut j = i + 1;
while j < bytes.len() && bytes[j] != q {
if bytes[j] == b'\\' && j + 1 < bytes.len() {
j += 2;
continue;
}
j += 1;
}
if j < bytes.len() {
Some(j)
} else {
None
}
}
fn strip_to_bare_name(view: &str) -> &str {
let head = view.split('(').next().unwrap_or(view);
head.rsplit('.').next().unwrap_or(head)
}