[pitboss/grind] deferred session-0014 (20260520T233019Z-6958)

This commit is contained in:
pitboss 2026-05-21 08:33:26 -05:00
parent d4fdd83578
commit ba0f83a855
2 changed files with 201 additions and 3 deletions

View file

@ -19,8 +19,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::rust_routes::{
bind_rust_path_params, find_method_attribute, find_rust_function, rust_formal_names,
source_imports_actix,
bind_rust_path_params, find_actix_route_chain, find_method_attribute, find_rust_function,
rust_formal_names, source_imports_actix,
};
pub struct RustActixAdapter;
@ -46,7 +46,8 @@ impl FrameworkAdapter for RustActixAdapter {
return None;
}
let func = find_rust_function(ast, file_bytes, &summary.name)?;
let (method, path) = find_method_attribute(func, file_bytes)?;
let (method, path) = find_method_attribute(func, file_bytes)
.or_else(|| find_actix_route_chain(ast, file_bytes, &summary.name))?;
let formals = rust_formal_names(func, file_bytes);
let request_params = bind_rust_path_params(&formals, &path);
Some(FrameworkBinding {
@ -126,4 +127,51 @@ mod tests {
.detect(&summary("helper"), tree.root_node(), src)
.is_none());
}
#[test]
fn fires_on_app_new_route_chain() {
let src: &[u8] = b"use actix_web::{App, web};\n\
fn build() -> App<()> { App::new().route(\"/u/{id}\", web::get().to(show)) }\n\
async fn show(id: String) -> String { id }\n";
let tree = parse(src);
let binding = RustActixAdapter
.detect(&summary("show"), tree.root_node(), src)
.expect("binding");
assert_eq!(binding.adapter, "rust-actix");
let route = binding.route.expect("route");
assert_eq!(route.method, HttpMethod::GET);
assert_eq!(route.path, "/u/{id}");
let id = binding
.request_params
.iter()
.find(|p| p.name == "id")
.unwrap();
assert!(matches!(id.source, ParamSource::PathSegment(_)));
}
#[test]
fn fires_on_web_resource_route_chain() {
let src: &[u8] = b"use actix_web::{App, web};\n\
fn build() -> App<()> { App::new().service(web::resource(\"/save\").route(web::post().to(save))) }\n\
async fn save(body: String) -> String { body }\n";
let tree = parse(src);
let binding = RustActixAdapter
.detect(&summary("save"), tree.root_node(), src)
.expect("binding");
let route = binding.route.expect("route");
assert_eq!(route.method, HttpMethod::POST);
assert_eq!(route.path, "/save");
}
#[test]
fn chained_builder_requires_handler_match() {
let src: &[u8] = b"use actix_web::{App, web};\n\
fn build() -> App<()> { App::new().route(\"/x\", web::get().to(other)) }\n\
async fn show() -> String { String::new() }\n\
async fn other() -> String { String::new() }\n";
let tree = parse(src);
assert!(RustActixAdapter
.detect(&summary("show"), tree.root_node(), src)
.is_none());
}
}

View file

@ -493,6 +493,156 @@ fn axum_callable_matches(node: Node<'_>, bytes: &[u8], target: &str) -> bool {
}
}
/// Walk `root` looking for an actix-web chained-builder route registration
/// (`App::new().route("/path", web::get().to(handler))` or
/// `web::resource("/path").route(web::get().to(handler))`) that wires
/// `target` as the handler. Returns `(method, path)` on first match.
pub fn find_actix_route_chain<'a>(
root: Node<'a>,
bytes: &'a [u8],
target: &str,
) -> Option<(HttpMethod, String)> {
let mut hit: Option<(HttpMethod, String)> = None;
walk_actix_chain(root, bytes, target, &mut hit);
hit
}
fn walk_actix_chain<'a>(
node: Node<'a>,
bytes: &'a [u8],
target: &str,
out: &mut Option<(HttpMethod, String)>,
) {
if out.is_some() {
return;
}
if node.kind() == "call_expression"
&& let Some(found) = try_actix_route_call(node, bytes, target)
{
*out = Some(found);
return;
}
let mut cur = node.walk();
for child in node.children(&mut cur) {
walk_actix_chain(child, bytes, target, out);
}
}
fn try_actix_route_call<'a>(
call: Node<'a>,
bytes: &'a [u8],
target: &str,
) -> Option<(HttpMethod, String)> {
let func = call.child_by_field_name("function")?;
if func.kind() != "field_expression" {
return None;
}
let field = func.child_by_field_name("field")?.utf8_text(bytes).ok()?;
if field != "route" {
return None;
}
let args = call.child_by_field_name("arguments")?;
let positional: Vec<Node<'_>> = {
let mut cur = args.walk();
args.named_children(&mut cur)
.filter(|c| !matches!(c.kind(), "line_comment" | "block_comment"))
.collect()
};
let (path, verb_node) = match positional.len() {
2 => {
let path = rust_string_literal(positional[0], bytes)?;
(path, positional[1])
}
1 => {
let receiver = func.child_by_field_name("value")?;
let path = find_actix_resource_path(receiver, bytes)?;
(path, positional[0])
}
_ => return None,
};
let (method, handler) = parse_actix_web_verb_to(verb_node, bytes)?;
if !axum_callable_matches(handler, bytes, target) {
return None;
}
Some((method, path))
}
/// Parse `web::get().to(handler)` / `web::post().to(handler)` /
/// `web::method(Method::PATCH).to(handler)` shapes. Returns
/// `(method, handler_node)` on the first matching `.to(...)` call.
fn parse_actix_web_verb_to<'a>(
node: Node<'a>,
bytes: &'a [u8],
) -> Option<(HttpMethod, Node<'a>)> {
if node.kind() != "call_expression" {
return None;
}
let func = node.child_by_field_name("function")?;
if func.kind() != "field_expression" {
return None;
}
let field = func.child_by_field_name("field")?.utf8_text(bytes).ok()?;
if field != "to" {
return None;
}
let args = node.child_by_field_name("arguments")?;
let handler = {
let mut cur = args.walk();
args.named_children(&mut cur)
.find(|c| !matches!(c.kind(), "line_comment" | "block_comment"))?
};
let recv = func.child_by_field_name("value")?;
if recv.kind() != "call_expression" {
return None;
}
let recv_func = recv.child_by_field_name("function")?;
let leaf = match recv_func.kind() {
"scoped_identifier" => recv_func
.child_by_field_name("name")?
.utf8_text(bytes)
.ok()?,
"identifier" => recv_func.utf8_text(bytes).ok()?,
_ => return None,
};
let method = verb_from_ident(leaf)?;
Some((method, handler))
}
/// Walk a receiver-chain backwards looking for the first
/// `web::resource(path)` / `web::scope(path)` call. Used when an actix
/// route is registered via `web::resource("/x").route(web::get().to(h))`
/// (no path argument on the `route` call itself).
fn find_actix_resource_path(node: Node<'_>, bytes: &[u8]) -> Option<String> {
let mut cur = node;
loop {
if cur.kind() == "call_expression" {
let func = cur.child_by_field_name("function")?;
let leaf = match func.kind() {
"scoped_identifier" => func
.child_by_field_name("name")
.and_then(|n| n.utf8_text(bytes).ok())
.unwrap_or(""),
"identifier" => func.utf8_text(bytes).ok().unwrap_or(""),
"field_expression" => {
cur = func.child_by_field_name("value")?;
continue;
}
_ => "",
};
if matches!(leaf, "resource" | "scope") {
let args = cur.child_by_field_name("arguments")?;
let mut cur_arg = args.walk();
let first = args
.named_children(&mut cur_arg)
.find(|c| !matches!(c.kind(), "line_comment" | "block_comment"))?;
return rust_string_literal(first, bytes);
}
return None;
}
return None;
}
}
/// Walk `root` looking for a `warp::path!("users" / u32)` macro
/// invocation that bridges to `target` via `.map(target)` /
/// `.and_then(target)`. Returns `(method, path)` on first match.