From 945836bf88144ab045d87c126584c42bcfe0be26 Mon Sep 17 00:00:00 2001 From: pitboss Date: Thu, 21 May 2026 23:58:51 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0001 (20260522T043516Z-29b8) --- src/dynamic/framework/adapters/go_chi.rs | 20 ++- src/dynamic/framework/adapters/go_echo.rs | 20 ++- src/dynamic/framework/adapters/go_fiber.rs | 20 ++- src/dynamic/framework/adapters/go_gin.rs | 20 ++- src/dynamic/framework/adapters/go_routes.rs | 162 +++++++++++++++++++- 5 files changed, 229 insertions(+), 13 deletions(-) diff --git a/src/dynamic/framework/adapters/go_chi.rs b/src/dynamic/framework/adapters/go_chi.rs index c9203743..a7856bc1 100644 --- a/src/dynamic/framework/adapters/go_chi.rs +++ b/src/dynamic/framework/adapters/go_chi.rs @@ -19,8 +19,8 @@ use crate::symbol::Lang; use tree_sitter::Node; use super::go_routes::{ - bind_go_path_params, find_go_function, find_route_for_callee, go_formal_names, - source_imports_chi, + bind_go_path_params, collect_use_middleware, find_go_function, find_route_for_callee, + go_formal_names, source_imports_chi, }; pub struct GoChiAdapter; @@ -52,13 +52,14 @@ impl FrameworkAdapter for GoChiAdapter { bind_go_path_params(&formals, &path) }) .unwrap_or_default(); + let middleware = collect_use_middleware(ast, file_bytes); Some(FrameworkBinding { adapter: ADAPTER_NAME.to_owned(), kind: EntryKind::HttpRoute, route: Some(RouteShape { method, path }), request_params, response_writer: None, - middleware: Vec::new(), + middleware, }) } } @@ -115,6 +116,19 @@ mod tests { assert!(matches!(id.source, ParamSource::PathSegment(_))); } + #[test] + fn populates_middleware_from_with_chain() { + let src: &[u8] = b"package main\nimport (\"net/http\"; \"github.com/go-chi/chi/v5\")\n\ + func init() { r := chi.NewRouter(); r.With(jwtauth.Verifier).Get(\"/users/{id}\", Show) }\n\ + func Show(w http.ResponseWriter, r *http.Request) {}\n"; + let tree = parse(src); + let binding = GoChiAdapter + .detect(&summary("Show"), tree.root_node(), src) + .expect("binding"); + assert_eq!(binding.middleware.len(), 1); + assert_eq!(binding.middleware[0].name, "jwtauth.Verifier"); + } + #[test] fn skips_when_chi_not_imported() { let src: &[u8] = b"package main\nfunc Show() {}\n"; diff --git a/src/dynamic/framework/adapters/go_echo.rs b/src/dynamic/framework/adapters/go_echo.rs index 717c1737..bdaced55 100644 --- a/src/dynamic/framework/adapters/go_echo.rs +++ b/src/dynamic/framework/adapters/go_echo.rs @@ -19,8 +19,8 @@ use crate::symbol::Lang; use tree_sitter::Node; use super::go_routes::{ - bind_go_path_params, find_go_function, find_route_for_callee, go_formal_names, - source_imports_echo, + bind_go_path_params, collect_use_middleware, find_go_function, find_route_for_callee, + go_formal_names, source_imports_echo, }; pub struct GoEchoAdapter; @@ -52,13 +52,14 @@ impl FrameworkAdapter for GoEchoAdapter { bind_go_path_params(&formals, &path) }) .unwrap_or_default(); + let middleware = collect_use_middleware(ast, file_bytes); Some(FrameworkBinding { adapter: ADAPTER_NAME.to_owned(), kind: EntryKind::HttpRoute, route: Some(RouteShape { method, path }), request_params, response_writer: None, - middleware: Vec::new(), + middleware, }) } } @@ -116,6 +117,19 @@ mod tests { assert_eq!(binding.route.unwrap().method, HttpMethod::PUT); } + #[test] + fn populates_middleware_from_use_calls() { + let src: &[u8] = b"package main\nimport \"github.com/labstack/echo/v4\"\n\ + func init() { e := echo.New(); e.Use(middleware.JWT); e.GET(\"/u/:id\", Show) }\n\ + func Show(c echo.Context, id string) error { return nil }\n"; + let tree = parse(src); + let binding = GoEchoAdapter + .detect(&summary("Show"), tree.root_node(), src) + .expect("binding"); + assert_eq!(binding.middleware.len(), 1); + assert_eq!(binding.middleware[0].name, "middleware.JWT"); + } + #[test] fn skips_when_echo_not_imported() { let src: &[u8] = b"package main\nfunc Show() {}\n"; diff --git a/src/dynamic/framework/adapters/go_fiber.rs b/src/dynamic/framework/adapters/go_fiber.rs index 6c9dcbfd..63c2ecc3 100644 --- a/src/dynamic/framework/adapters/go_fiber.rs +++ b/src/dynamic/framework/adapters/go_fiber.rs @@ -20,8 +20,8 @@ use crate::symbol::Lang; use tree_sitter::Node; use super::go_routes::{ - bind_go_path_params, find_go_function, find_route_for_callee, go_formal_names, - source_imports_fiber, + bind_go_path_params, collect_use_middleware, find_go_function, find_route_for_callee, + go_formal_names, source_imports_fiber, }; pub struct GoFiberAdapter; @@ -53,13 +53,14 @@ impl FrameworkAdapter for GoFiberAdapter { bind_go_path_params(&formals, &path) }) .unwrap_or_default(); + let middleware = collect_use_middleware(ast, file_bytes); Some(FrameworkBinding { adapter: ADAPTER_NAME.to_owned(), kind: EntryKind::HttpRoute, route: Some(RouteShape { method, path }), request_params, response_writer: None, - middleware: Vec::new(), + middleware, }) } } @@ -122,6 +123,19 @@ mod tests { assert!(matches!(rest.source, ParamSource::PathSegment(_))); } + #[test] + fn populates_middleware_from_use_calls() { + let src: &[u8] = b"package main\nimport \"github.com/gofiber/fiber/v2\"\n\ + func init() { app := fiber.New(); app.Use(csrf.New(secret)); app.Get(\"/u/:id\", Show) }\n\ + func Show(c *fiber.Ctx, id string) error { return nil }\n"; + let tree = parse(src); + let binding = GoFiberAdapter + .detect(&summary("Show"), tree.root_node(), src) + .expect("binding"); + assert_eq!(binding.middleware.len(), 1); + assert_eq!(binding.middleware[0].name, "csrf.New"); + } + #[test] fn skips_when_fiber_not_imported() { let src: &[u8] = b"package main\nfunc Show() {}\n"; diff --git a/src/dynamic/framework/adapters/go_gin.rs b/src/dynamic/framework/adapters/go_gin.rs index daad0b96..b8c3bb77 100644 --- a/src/dynamic/framework/adapters/go_gin.rs +++ b/src/dynamic/framework/adapters/go_gin.rs @@ -22,8 +22,8 @@ use crate::symbol::Lang; use tree_sitter::Node; use super::go_routes::{ - bind_go_path_params, find_go_function, find_route_for_callee, go_formal_names, - source_imports_gin, + bind_go_path_params, collect_use_middleware, find_go_function, find_route_for_callee, + go_formal_names, source_imports_gin, }; pub struct GoGinAdapter; @@ -55,13 +55,14 @@ impl FrameworkAdapter for GoGinAdapter { bind_go_path_params(&formals, &path) }) .unwrap_or_default(); + let middleware = collect_use_middleware(ast, file_bytes); Some(FrameworkBinding { adapter: ADAPTER_NAME.to_owned(), kind: EntryKind::HttpRoute, route: Some(RouteShape { method, path }), request_params, response_writer: None, - middleware: Vec::new(), + middleware, }) } } @@ -143,6 +144,19 @@ mod tests { ); } + #[test] + fn populates_middleware_from_use_calls() { + let src: &[u8] = b"package main\nimport \"github.com/gin-gonic/gin\"\n\ + func init() { r := gin.Default(); r.Use(AuthMiddleware); r.GET(\"/u/:id\", Show) }\n\ + func Show(c *gin.Context, id string) {}\n"; + let tree = parse(src); + let binding = GoGinAdapter + .detect(&summary("Show"), tree.root_node(), src) + .expect("binding"); + assert_eq!(binding.middleware.len(), 1); + assert_eq!(binding.middleware[0].name, "AuthMiddleware"); + } + #[test] fn fires_on_marker_comment() { let src: &[u8] = diff --git a/src/dynamic/framework/adapters/go_routes.rs b/src/dynamic/framework/adapters/go_routes.rs index d43725c2..bf391906 100644 --- a/src/dynamic/framework/adapters/go_routes.rs +++ b/src/dynamic/framework/adapters/go_routes.rs @@ -16,7 +16,9 @@ //! //! [`extract_go_path_placeholders`] supports both syntaxes. -use crate::dynamic::framework::{HttpMethod, ParamBinding, ParamSource}; +use crate::dynamic::framework::auth_markers; +use crate::dynamic::framework::{HttpMethod, MiddlewareShape, ParamBinding, ParamSource}; +use crate::symbol::Lang; use tree_sitter::Node; /// True when `bytes` carries any of the well-known gin markers. @@ -245,6 +247,98 @@ pub fn verb_from_method(method: &str) -> Option { } } +/// Walk every `receiver.Use(...)` / `receiver.With(...)` call in the +/// file and collect the argument expressions whose names match a +/// known Go middleware marker (see +/// [`crate::dynamic::framework::auth_markers::is_protective`]). +/// +/// gin / echo / fiber attach middleware via `r.Use(mw1, mw2, ...)`; +/// chi attaches middleware inline via `r.Use(...)` or +/// `r.With(...).Get(...)`. Both verbs are accepted; mid-chain +/// `.With(...)` calls that follow a verb-method call (`.Get(...)` +/// returns no router so chained `.With` is impossible) are +/// conservatively still collected because tree-sitter has already +/// flattened the chain into the same `call_expression` shape. +/// +/// Argument rendering: +/// - bare identifier (`r.Use(authMiddleware)`) → `"authMiddleware"` +/// - selector expression (`r.Use(middleware.JWT)`) → +/// `"middleware.JWT"` +/// - call expression (`r.Use(csrf.New(secret))`) → `"csrf.New"` +/// (callee text, args dropped — the auth-markers table is keyed +/// on the factory function, not the constructed instance) +/// +/// De-duplicates within a single file; preserves declaration order. +/// Names the registry does not recognise are dropped silently — +/// callers can re-walk with a wider predicate if they need broader +/// inclusion. +pub fn collect_use_middleware(root: Node<'_>, bytes: &[u8]) -> Vec { + let mut raw: Vec = Vec::new(); + walk_use_calls(root, bytes, &mut raw); + let mut out: Vec = Vec::new(); + for name in raw { + if auth_markers::is_protective(Lang::Go, &name) + && !out.iter().any(|m| m.name == name) + { + out.push(MiddlewareShape { name }); + } + } + out +} + +fn walk_use_calls(node: Node<'_>, bytes: &[u8], out: &mut Vec) { + if node.kind() == "call_expression" { + try_collect_use_call(node, bytes, out); + } + let mut cur = node.walk(); + for child in node.children(&mut cur) { + walk_use_calls(child, bytes, out); + } +} + +fn try_collect_use_call(call: Node<'_>, bytes: &[u8], out: &mut Vec) { + let Some(callee) = call.child_by_field_name("function") else { + return; + }; + if callee.kind() != "selector_expression" { + return; + } + let Some(field) = callee.child_by_field_name("field") else { + return; + }; + let Ok(verb) = field.utf8_text(bytes) else { + return; + }; + if verb != "Use" && verb != "With" { + return; + } + let Some(args) = call.child_by_field_name("arguments") else { + return; + }; + let mut cur = args.walk(); + for arg in args.named_children(&mut cur) { + if arg.kind() == "comment" { + continue; + } + if let Some(name) = middleware_arg_name(arg, bytes) { + out.push(name); + } + } +} + +fn middleware_arg_name(node: Node<'_>, bytes: &[u8]) -> Option { + match node.kind() { + "identifier" | "selector_expression" => { + node.utf8_text(bytes).ok().map(|s| s.trim().to_owned()) + } + "call_expression" => { + let callee = node.child_by_field_name("function")?; + callee.utf8_text(bytes).ok().map(|s| s.trim().to_owned()) + } + _ => None, + } +} + /// Locate the `(method, path)` of a `receiver.Verb("/path", target)` /// call expression registered against `target` in the file. Walks /// every `call_expression` in `root` and inspects each one whose @@ -443,4 +537,70 @@ mod tests { let names = go_formal_names(f, src); assert_eq!(names, vec!["c", "id"]); } + + #[test] + fn collect_use_middleware_picks_bare_identifier() { + let src: &[u8] = b"package main\nfunc init() { r := gin.Default(); r.Use(AuthMiddleware) }\n"; + let tree = parse(src); + let mw = collect_use_middleware(tree.root_node(), src); + assert_eq!(mw.len(), 1); + assert_eq!(mw[0].name, "AuthMiddleware"); + } + + #[test] + fn collect_use_middleware_picks_selector_marker() { + let src: &[u8] = + b"package main\nfunc init() { e := echo.New(); e.Use(middleware.JWT) }\n"; + let tree = parse(src); + let mw = collect_use_middleware(tree.root_node(), src); + assert_eq!(mw.len(), 1); + assert_eq!(mw[0].name, "middleware.JWT"); + } + + #[test] + fn collect_use_middleware_picks_call_factory() { + let src: &[u8] = + b"package main\nfunc init() { r := chi.NewRouter(); r.Use(csrf.New(secret)) }\n"; + let tree = parse(src); + let mw = collect_use_middleware(tree.root_node(), src); + assert_eq!(mw.len(), 1); + assert_eq!(mw[0].name, "csrf.New"); + } + + #[test] + fn collect_use_middleware_accepts_chi_with() { + let src: &[u8] = + b"package main\nfunc init() { r := chi.NewRouter(); r.With(jwtauth.Verifier).Get(\"/x\", Show) }\n"; + let tree = parse(src); + let mw = collect_use_middleware(tree.root_node(), src); + assert_eq!(mw.len(), 1); + assert_eq!(mw[0].name, "jwtauth.Verifier"); + } + + #[test] + fn collect_use_middleware_drops_unknown_names() { + let src: &[u8] = + b"package main\nfunc init() { r := gin.Default(); r.Use(loggingHandler) }\n"; + let tree = parse(src); + let mw = collect_use_middleware(tree.root_node(), src); + assert!(mw.is_empty(), "loggingHandler is not a recognised marker"); + } + + #[test] + fn collect_use_middleware_dedupes_and_collects_multiple() { + let src: &[u8] = b"package main\nfunc init() { r := gin.Default(); r.Use(AuthMiddleware, csrf.New(s)); r.Use(AuthMiddleware) }\n"; + let tree = parse(src); + let mw = collect_use_middleware(tree.root_node(), src); + let names: Vec<&str> = mw.iter().map(|m| m.name.as_str()).collect(); + assert_eq!(names, vec!["AuthMiddleware", "csrf.New"]); + } + + #[test] + fn collect_use_middleware_returns_empty_when_none_recognised() { + let src: &[u8] = + b"package main\nfunc init() { r := gin.Default(); r.GET(\"/x\", Show) }\n"; + let tree = parse(src); + let mw = collect_use_middleware(tree.root_node(), src); + assert!(mw.is_empty()); + } }