refactor(dynamic): add framework-aware route detection, improve Rust/Go handler resolution, and expand tests

This commit is contained in:
elipeter 2026-05-24 17:59:25 -05:00
parent baa9a36bc6
commit 43ab4aa469
9 changed files with 584 additions and 29 deletions

View file

@ -19,8 +19,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::go_routes::{
bind_go_path_params, collect_use_middleware, find_go_function, find_route_for_callee,
go_formal_names, source_imports_chi,
GoRouteFramework, bind_go_path_params, collect_use_middleware, find_go_function,
find_route_for_callee_in_framework, go_formal_names, source_imports_chi,
};
pub struct GoChiAdapter;
@ -45,7 +45,12 @@ impl FrameworkAdapter for GoChiAdapter {
if !source_imports_chi(file_bytes) {
return None;
}
let (method, path) = find_route_for_callee(ast, file_bytes, &summary.name)?;
let (method, path) = find_route_for_callee_in_framework(
ast,
file_bytes,
&summary.name,
GoRouteFramework::Chi,
)?;
let request_params = find_go_function(ast, file_bytes, &summary.name)
.map(|func| {
let formals = go_formal_names(func, file_bytes);

View file

@ -19,8 +19,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::go_routes::{
bind_go_path_params, collect_use_middleware, find_go_function, find_route_for_callee,
go_formal_names, source_imports_echo,
GoRouteFramework, bind_go_path_params, collect_use_middleware, find_go_function,
find_route_for_callee_in_framework, go_formal_names, source_imports_echo,
};
pub struct GoEchoAdapter;
@ -45,7 +45,12 @@ impl FrameworkAdapter for GoEchoAdapter {
if !source_imports_echo(file_bytes) {
return None;
}
let (method, path) = find_route_for_callee(ast, file_bytes, &summary.name)?;
let (method, path) = find_route_for_callee_in_framework(
ast,
file_bytes,
&summary.name,
GoRouteFramework::Echo,
)?;
let request_params = find_go_function(ast, file_bytes, &summary.name)
.map(|func| {
let formals = go_formal_names(func, file_bytes);

View file

@ -20,8 +20,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::go_routes::{
bind_go_path_params, collect_use_middleware, find_go_function, find_route_for_callee,
go_formal_names, source_imports_fiber,
GoRouteFramework, bind_go_path_params, collect_use_middleware, find_go_function,
find_route_for_callee_in_framework, go_formal_names, source_imports_fiber,
};
pub struct GoFiberAdapter;
@ -46,7 +46,12 @@ impl FrameworkAdapter for GoFiberAdapter {
if !source_imports_fiber(file_bytes) {
return None;
}
let (method, path) = find_route_for_callee(ast, file_bytes, &summary.name)?;
let (method, path) = find_route_for_callee_in_framework(
ast,
file_bytes,
&summary.name,
GoRouteFramework::Fiber,
)?;
let request_params = find_go_function(ast, file_bytes, &summary.name)
.map(|func| {
let formals = go_formal_names(func, file_bytes);

View file

@ -22,8 +22,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::go_routes::{
bind_go_path_params, collect_use_middleware, find_go_function, find_route_for_callee,
go_formal_names, source_imports_gin,
GoRouteFramework, bind_go_path_params, collect_use_middleware, find_go_function,
find_route_for_callee_in_framework, go_formal_names, source_imports_gin,
};
pub struct GoGinAdapter;
@ -48,7 +48,12 @@ impl FrameworkAdapter for GoGinAdapter {
if !source_imports_gin(file_bytes) {
return None;
}
let (method, path) = find_route_for_callee(ast, file_bytes, &summary.name)?;
let (method, path) = find_route_for_callee_in_framework(
ast,
file_bytes,
&summary.name,
GoRouteFramework::Gin,
)?;
let request_params = find_go_function(ast, file_bytes, &summary.name)
.map(|func| {
let formals = go_formal_names(func, file_bytes);

View file

@ -19,6 +19,7 @@
use crate::dynamic::framework::auth_markers;
use crate::dynamic::framework::{HttpMethod, MiddlewareShape, ParamBinding, ParamSource};
use crate::symbol::Lang;
use std::collections::HashSet;
use tree_sitter::Node;
/// True when `bytes` carries any of the well-known gin markers.
@ -229,6 +230,57 @@ fn is_implicit_formal(name: &str) -> bool {
matches!(name, "c" | "ctx" | "w" | "r" | "req" | "res" | "rw")
}
/// Go router family whose route-registration receiver can be checked
/// from local source context.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GoRouteFramework {
Gin,
Echo,
Fiber,
Chi,
}
impl GoRouteFramework {
fn marker_comment(self) -> &'static str {
match self {
Self::Gin => "// nyx-shape: gin",
Self::Echo => "// nyx-shape: echo",
Self::Fiber => "// nyx-shape: fiber",
Self::Chi => "// nyx-shape: chi",
}
}
fn constructor_markers(self) -> &'static [&'static str] {
match self {
Self::Gin => &["gin.Default(", "gin.New("],
Self::Echo => &["echo.New("],
Self::Fiber => &["fiber.New("],
Self::Chi => &["chi.NewRouter("],
}
}
fn type_markers(self) -> &'static [&'static str] {
match self {
Self::Gin => &[
"*gin.Engine",
"gin.Engine",
"*gin.RouterGroup",
"gin.RouterGroup",
],
Self::Echo => &["*echo.Echo", "echo.Echo", "*echo.Group", "echo.Group"],
Self::Fiber => &["*fiber.App", "fiber.App", "*fiber.Group", "fiber.Group"],
Self::Chi => &["chi.Router", "*chi.Mux", "chi.Mux"],
}
}
fn grouping_methods(self) -> &'static [&'static str] {
match self {
Self::Gin | Self::Echo | Self::Fiber => &["Group"],
Self::Chi => &["Group", "Route", "With"],
}
}
}
/// Parse Go verb-method names: `GET`, `POST`, `PUT`, `PATCH`,
/// `DELETE`, `HEAD`, `OPTIONS` (case-insensitive — gin uses upper,
/// echo / chi use upper, fiber uses pascal-cased like `Get`,
@ -355,28 +407,64 @@ pub fn find_route_for_callee<'a>(
target: &str,
) -> Option<(HttpMethod, String)> {
let mut hit: Option<(HttpMethod, String)> = None;
walk_routes(root, bytes, target, &mut hit);
walk_routes(root, bytes, target, None, &mut hit);
hit
}
/// Receiver-aware sibling of [`find_route_for_callee`].
///
/// A file can import a framework while also using ordinary objects with
/// route-like method names, for example `cache.Get(key)` or
/// `repo.Post(message)`. The broad helper above intentionally keeps
/// the old name-only behavior for legacy callers and unit tests; the
/// framework adapters call this variant so the registration receiver
/// must be a locally recognised router/app value.
pub fn find_route_for_callee_in_framework<'a>(
root: Node<'a>,
bytes: &'a [u8],
target: &str,
framework: GoRouteFramework,
) -> Option<(HttpMethod, String)> {
let receivers = collect_framework_receivers(root, bytes, framework);
let marker_fallback = receivers.is_empty()
&& std::str::from_utf8(bytes)
.map(|s| s.contains(framework.marker_comment()))
.unwrap_or(false);
let filter = RouteReceiverFilter {
framework,
receivers: &receivers,
marker_fallback,
};
let mut hit: Option<(HttpMethod, String)> = None;
walk_routes(root, bytes, target, Some(&filter), &mut hit);
hit
}
struct RouteReceiverFilter<'a> {
framework: GoRouteFramework,
receivers: &'a HashSet<String>,
marker_fallback: bool,
}
fn walk_routes<'a>(
node: Node<'a>,
bytes: &'a [u8],
target: &str,
receiver_filter: Option<&RouteReceiverFilter<'_>>,
out: &mut Option<(HttpMethod, String)>,
) {
if out.is_some() {
return;
}
if node.kind() == "call_expression"
&& let Some(found) = try_route_call(node, bytes, target)
&& let Some(found) = try_route_call(node, bytes, target, receiver_filter)
{
*out = Some(found);
return;
}
let mut cur = node.walk();
for child in node.children(&mut cur) {
walk_routes(child, bytes, target, out);
walk_routes(child, bytes, target, receiver_filter, out);
}
}
@ -384,6 +472,7 @@ fn try_route_call<'a>(
call: Node<'a>,
bytes: &'a [u8],
target: &str,
receiver_filter: Option<&RouteReceiverFilter<'_>>,
) -> Option<(HttpMethod, String)> {
let callee = call.child_by_field_name("function")?;
if callee.kind() != "selector_expression" {
@ -391,6 +480,11 @@ fn try_route_call<'a>(
}
let verb_node = callee.child_by_field_name("field")?.utf8_text(bytes).ok()?;
let method = verb_from_method(verb_node)?;
if let Some(filter) = receiver_filter
&& !route_receiver_matches(callee, bytes, filter)
{
return None;
}
let args = call.child_by_field_name("arguments")?;
let positional: Vec<Node<'_>> = {
let mut cur = args.walk();
@ -408,6 +502,199 @@ fn try_route_call<'a>(
Some((method, path))
}
fn route_receiver_matches(
selector: Node<'_>,
bytes: &[u8],
filter: &RouteReceiverFilter<'_>,
) -> bool {
let Some(receiver) = selector.child_by_field_name("operand") else {
return filter.marker_fallback;
};
let Ok(expr) = receiver.utf8_text(bytes) else {
return filter.marker_fallback;
};
receiver_expr_matches_framework(expr.trim(), filter.framework, filter.receivers)
|| filter.marker_fallback
}
fn receiver_expr_matches_framework(
expr: &str,
framework: GoRouteFramework,
receivers: &HashSet<String>,
) -> bool {
let expr = trim_wrapping_parens(expr.trim());
if receivers.contains(expr) {
return true;
}
if framework
.constructor_markers()
.iter()
.any(|marker| expr.starts_with(marker))
{
return true;
}
rhs_uses_known_router(expr, framework, receivers)
}
fn collect_framework_receivers(
root: Node<'_>,
bytes: &[u8],
framework: GoRouteFramework,
) -> HashSet<String> {
let mut receivers = HashSet::new();
let mut assignment_snippets = Vec::new();
collect_receiver_snippets(
root,
bytes,
framework,
&mut receivers,
&mut assignment_snippets,
);
let mut changed = true;
while changed {
changed = false;
for snippet in &assignment_snippets {
if assignment_rhs_matches_framework(snippet, framework, &receivers) {
for ident in assignment_lhs_identifiers(snippet) {
changed |= receivers.insert(ident);
}
}
}
}
receivers
}
fn collect_receiver_snippets(
node: Node<'_>,
bytes: &[u8],
framework: GoRouteFramework,
receivers: &mut HashSet<String>,
assignment_snippets: &mut Vec<String>,
) {
match node.kind() {
"parameter_declaration" | "var_spec" => {
if let Ok(text) = node.utf8_text(bytes) {
for ident in typed_decl_identifiers(text, framework) {
receivers.insert(ident);
}
if node.kind() == "var_spec" {
assignment_snippets.push(text.to_owned());
}
}
}
"short_var_declaration" | "assignment_statement" => {
if let Ok(text) = node.utf8_text(bytes) {
assignment_snippets.push(text.to_owned());
}
}
_ => {}
}
let mut cur = node.walk();
for child in node.children(&mut cur) {
collect_receiver_snippets(child, bytes, framework, receivers, assignment_snippets);
}
}
fn typed_decl_identifiers(text: &str, framework: GoRouteFramework) -> Vec<String> {
let Some(marker_pos) = framework
.type_markers()
.iter()
.filter_map(|marker| text.find(marker))
.min()
else {
return Vec::new();
};
identifiers_from_list(strip_var_prefix(&text[..marker_pos]))
}
fn assignment_rhs_matches_framework(
text: &str,
framework: GoRouteFramework,
receivers: &HashSet<String>,
) -> bool {
let Some((_, rhs)) = split_assignment(text) else {
return false;
};
let rhs = rhs.trim();
framework
.constructor_markers()
.iter()
.any(|marker| rhs.contains(marker))
|| rhs_uses_known_router(rhs, framework, receivers)
}
fn assignment_lhs_identifiers(text: &str) -> Vec<String> {
let Some((lhs, _)) = split_assignment(text) else {
return Vec::new();
};
identifiers_from_list(strip_var_prefix(lhs))
}
fn split_assignment(text: &str) -> Option<(&str, &str)> {
text.split_once(":=").or_else(|| text.split_once('='))
}
fn strip_var_prefix(text: &str) -> &str {
let text = text.trim();
text.strip_prefix("var ").unwrap_or(text).trim()
}
fn rhs_uses_known_router(
rhs: &str,
framework: GoRouteFramework,
receivers: &HashSet<String>,
) -> bool {
let rhs = trim_wrapping_parens(rhs.trim());
for receiver in receivers {
let Some(rest) = rhs.strip_prefix(receiver).and_then(|s| s.strip_prefix('.')) else {
continue;
};
if framework
.grouping_methods()
.iter()
.any(|method| rest.starts_with(&format!("{method}(")))
{
return true;
}
}
false
}
fn identifiers_from_list(text: &str) -> Vec<String> {
text.split(',')
.filter_map(|part| {
let token = part.split_whitespace().next()?.trim();
if is_go_identifier(token) {
Some(token.to_owned())
} else {
None
}
})
.collect()
}
fn is_go_identifier(s: &str) -> bool {
let mut chars = s.chars();
let Some(first) = chars.next() else {
return false;
};
(first == '_' || first.is_ascii_alphabetic())
&& chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
}
fn trim_wrapping_parens(mut s: &str) -> &str {
loop {
let trimmed = s.trim();
if trimmed.starts_with('(') && trimmed.ends_with(')') && trimmed.len() > 2 {
s = &trimmed[1..trimmed.len() - 1];
} else {
return trimmed;
}
}
}
/// Read a Go interpreted_string_literal's content, dropping the
/// surrounding `"` quotes. Returns `None` if `node` is not a string
/// literal.
@ -527,6 +814,67 @@ mod tests {
assert_eq!(path, "/x");
}
#[test]
fn receiver_aware_route_accepts_framework_constructor_receiver() {
let src: &[u8] =
b"package main\nfunc init() { r := gin.New(); r.GET(\"/x\", Show) }\nfunc Show(c interface{}) {}\n";
let tree = parse(src);
let (method, path) = find_route_for_callee_in_framework(
tree.root_node(),
src,
"Show",
GoRouteFramework::Gin,
)
.expect("hit");
assert_eq!(method, HttpMethod::GET);
assert_eq!(path, "/x");
}
#[test]
fn receiver_aware_route_rejects_cache_get_collision() {
let src: &[u8] = b"package main\nimport \"github.com/gin-gonic/gin\"\n\
func init() { r := gin.New(); _ = r; cache.Get(\"/x\", Show) }\n\
func Show(c interface{}) {}\n";
let tree = parse(src);
assert!(
find_route_for_callee_in_framework(
tree.root_node(),
src,
"Show",
GoRouteFramework::Gin,
)
.is_none()
);
}
#[test]
fn receiver_aware_route_accepts_typed_param_receiver() {
let src: &[u8] = b"package main\nfunc register(r *gin.Engine) { r.GET(\"/x\", Show) }\nfunc Show(c interface{}) {}\n";
let tree = parse(src);
let (_, path) = find_route_for_callee_in_framework(
tree.root_node(),
src,
"Show",
GoRouteFramework::Gin,
)
.expect("hit");
assert_eq!(path, "/x");
}
#[test]
fn receiver_aware_route_accepts_group_receiver_assignment() {
let src: &[u8] = b"package main\nfunc init() { r := chi.NewRouter(); auth := r.With(AuthMiddleware); auth.Get(\"/x\", Show) }\nfunc Show(w interface{}, r interface{}) {}\n";
let tree = parse(src);
let (_, path) = find_route_for_callee_in_framework(
tree.root_node(),
src,
"Show",
GoRouteFramework::Chi,
)
.expect("hit");
assert_eq!(path, "/x");
}
#[test]
fn formal_names_skip_types() {
let src: &[u8] = b"package main\nfunc Show(c *gin.Context, id string) {}\n";

View file

@ -19,8 +19,9 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::rust_routes::{
bind_rust_path_params, collect_rust_middleware, find_actix_route_chain, find_method_attribute,
find_rust_function, rust_formal_names, source_imports_actix,
RustRouteAttributeFramework, bind_rust_path_params, collect_rust_middleware,
find_actix_route_chain, find_method_attribute_for_framework, find_rust_function,
rust_formal_names, source_imports_actix,
};
pub struct RustActixAdapter;
@ -46,8 +47,12 @@ 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)
.or_else(|| find_actix_route_chain(ast, file_bytes, &summary.name))?;
let (method, path) = find_method_attribute_for_framework(
func,
file_bytes,
RustRouteAttributeFramework::Actix,
)
.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);
let middleware = collect_rust_middleware(ast, file_bytes);
@ -122,6 +127,27 @@ mod tests {
);
}
#[test]
fn skips_rocket_get_macro_in_actix_file() {
let src: &[u8] = b"use actix_web::HttpResponse;\nuse rocket::get;\n#[get(\"/u\")]\nasync fn show() -> HttpResponse { HttpResponse::Ok().finish() }\n";
let tree = parse(src);
assert!(
RustActixAdapter
.detect(&summary("show"), tree.root_node(), src)
.is_none()
);
}
#[test]
fn accepts_scoped_actix_get_macro() {
let src: &[u8] = b"use actix_web::HttpResponse;\n#[actix_web::get(\"/u\")]\nasync fn show() -> HttpResponse { HttpResponse::Ok().finish() }\n";
let tree = parse(src);
let binding = RustActixAdapter
.detect(&summary("show"), tree.root_node(), src)
.expect("binding");
assert_eq!(binding.route.unwrap().path, "/u");
}
#[test]
fn skips_when_attribute_missing() {
let src: &[u8] = b"use actix_web::App;\nfn helper(x: String) {}\n";

View file

@ -23,8 +23,9 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::rust_routes::{
bind_rust_path_params, collect_rust_middleware, find_method_attribute, find_rust_function,
rust_formal_names, source_imports_rocket,
RustRouteAttributeFramework, bind_rust_path_params, collect_rust_middleware,
find_method_attribute_for_framework, find_rust_function, rust_formal_names,
source_imports_rocket,
};
pub struct RustRocketAdapter;
@ -50,7 +51,11 @@ impl FrameworkAdapter for RustRocketAdapter {
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_for_framework(
func,
file_bytes,
RustRouteAttributeFramework::Rocket,
)?;
let formals = rust_formal_names(func, file_bytes);
let request_params = bind_rust_path_params(&formals, &path);
let middleware = collect_rust_middleware(ast, file_bytes);
@ -138,4 +143,26 @@ mod tests {
.is_none()
);
}
#[test]
fn skips_actix_get_macro_in_rocket_file() {
let src: &[u8] = b"use rocket::routes;\nuse actix_web::get;\n#[get(\"/u\")]\nfn show() -> &'static str { \"ok\" }\n";
let tree = parse(src);
assert!(
RustRocketAdapter
.detect(&summary("show"), tree.root_node(), src)
.is_none()
);
}
#[test]
fn accepts_scoped_rocket_get_macro() {
let src: &[u8] =
b"use rocket::routes;\n#[rocket::get(\"/u\")]\nfn show() -> &'static str { \"ok\" }\n";
let tree = parse(src);
let binding = RustRocketAdapter
.detect(&summary("show"), tree.root_node(), src)
.expect("binding");
assert_eq!(binding.route.unwrap().path, "/u");
}
}

View file

@ -289,6 +289,36 @@ pub fn verb_from_ident(ident: &str) -> Option<HttpMethod> {
}
}
/// Framework that owns a bare or scoped Rust route attribute macro.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RustRouteAttributeFramework {
Actix,
Rocket,
}
impl RustRouteAttributeFramework {
fn scoped_prefix(self) -> &'static str {
match self {
Self::Actix => "actix_web::",
Self::Rocket => "rocket::",
}
}
fn marker_comment(self) -> &'static str {
match self {
Self::Actix => "// nyx-shape: actix",
Self::Rocket => "// nyx-shape: rocket",
}
}
fn import_roots(self) -> &'static [&'static str] {
match self {
Self::Actix => &["use actix_web::"],
Self::Rocket => &["use rocket::", "#[macro_use] extern crate rocket"],
}
}
}
/// Walk every method-chain call in the file whose field name is one
/// of the known middleware-attach verbs and collect argument
/// expressions whose names match a known Rust middleware marker (see
@ -461,6 +491,28 @@ pub fn rust_string_literal(node: Node<'_>, bytes: &[u8]) -> Option<String> {
/// Returns `(method, path)` on first match. Used by both actix-web
/// (`#[get("/path")]`) and rocket (same syntax).
pub fn find_method_attribute<'a>(func: Node<'a>, bytes: &'a [u8]) -> Option<(HttpMethod, String)> {
find_method_attribute_inner(func, bytes, None)
}
/// Framework-aware sibling of [`find_method_attribute`].
///
/// Actix and Rocket share bare `#[get("/x")]` / `#[post("/x")]`
/// macro names. This variant rejects a bare attribute unless the
/// source imports the matching framework's macro, and it rejects a
/// scoped attribute unless the scope belongs to that framework.
pub fn find_method_attribute_for_framework<'a>(
func: Node<'a>,
bytes: &'a [u8],
framework: RustRouteAttributeFramework,
) -> Option<(HttpMethod, String)> {
find_method_attribute_inner(func, bytes, Some(framework))
}
fn find_method_attribute_inner<'a>(
func: Node<'a>,
bytes: &'a [u8],
framework: Option<RustRouteAttributeFramework>,
) -> Option<(HttpMethod, String)> {
let parent = func.parent()?;
let mut cur = parent.walk();
let children: Vec<Node<'_>> = parent.children(&mut cur).collect();
@ -469,7 +521,7 @@ pub fn find_method_attribute<'a>(func: Node<'a>, bytes: &'a [u8]) -> Option<(Htt
// function declaration.
for child in children[..pos].iter().rev() {
if child.kind() == "attribute_item" {
if let Some(hit) = read_route_attribute(*child, bytes) {
if let Some(hit) = read_route_attribute(*child, bytes, framework) {
return Some(hit);
}
continue;
@ -492,7 +544,7 @@ pub fn find_method_attribute<'a>(func: Node<'a>, bytes: &'a [u8]) -> Option<(Htt
let mut cur = func.walk();
for c in func.children(&mut cur) {
if c.kind() == "attribute_item"
&& let Some(hit) = read_route_attribute(c, bytes)
&& let Some(hit) = read_route_attribute(c, bytes, framework)
{
return Some(hit);
}
@ -500,7 +552,11 @@ pub fn find_method_attribute<'a>(func: Node<'a>, bytes: &'a [u8]) -> Option<(Htt
None
}
fn read_route_attribute(attr: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, String)> {
fn read_route_attribute(
attr: Node<'_>,
bytes: &[u8],
framework: Option<RustRouteAttributeFramework>,
) -> Option<(HttpMethod, String)> {
let mut cur = attr.walk();
let attribute = attr
.named_children(&mut cur)
@ -513,11 +569,13 @@ fn read_route_attribute(attr: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, Str
let mut ac = attribute.walk();
let children: Vec<Node<'_>> = attribute.named_children(&mut ac).collect();
let head = children.first()?;
let verb_text = match head.kind() {
"identifier" => head.utf8_text(bytes).ok()?.to_owned(),
let (verb_text, scoped_head) = match head.kind() {
"identifier" => (head.utf8_text(bytes).ok()?.to_owned(), None),
"scoped_identifier" => {
let full = head.utf8_text(bytes).ok()?.to_owned();
let mut sc = head.walk();
head.named_children(&mut sc)
let leaf = head
.named_children(&mut sc)
.filter_map(|c| {
if c.kind() == "identifier" {
c.utf8_text(bytes).ok()
@ -526,11 +584,15 @@ fn read_route_attribute(attr: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, Str
}
})
.last()?
.to_owned()
.to_owned();
(leaf, Some(full))
}
_ => return None,
};
let method = verb_from_ident(&verb_text)?;
if !route_attribute_belongs_to_framework(&verb_text, scoped_head.as_deref(), bytes, framework) {
return None;
}
for child in &children[1..] {
if child.kind() == "token_tree" {
// Recurse to find the first string_literal under the
@ -547,6 +609,64 @@ fn read_route_attribute(attr: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, Str
None
}
fn route_attribute_belongs_to_framework(
verb: &str,
scoped_head: Option<&str>,
bytes: &[u8],
framework: Option<RustRouteAttributeFramework>,
) -> bool {
let Some(framework) = framework else {
return true;
};
if let Some(head) = scoped_head {
return head.starts_with(framework.scoped_prefix());
}
bare_route_attribute_imported_from_framework(bytes, verb, framework)
}
fn bare_route_attribute_imported_from_framework(
bytes: &[u8],
verb: &str,
framework: RustRouteAttributeFramework,
) -> bool {
let Ok(source) = std::str::from_utf8(bytes) else {
return false;
};
if source.contains(framework.marker_comment()) {
return true;
}
for line in source.lines().map(str::trim) {
for root in framework.import_roots() {
if *root == "#[macro_use] extern crate rocket" {
if line.contains(root) {
return true;
}
continue;
}
if !line.contains(root) {
continue;
}
if line.contains(&format!("{root}{verb};"))
|| line.contains(&format!("{root}{verb} as "))
{
return true;
}
if let Some((_, imports)) = line.split_once('{') {
let imports = imports.split('}').next().unwrap_or(imports);
if imports
.split(',')
.map(str::trim)
.filter_map(|part| part.split_ascii_whitespace().next())
.any(|name| name == verb)
{
return true;
}
}
}
}
false
}
fn first_string_in(node: Node<'_>, bytes: &[u8]) -> Option<String> {
if let Some(literal) = rust_string_literal(node, bytes) {
return Some(literal);