[pitboss/grind] deferred session-0003 (20260522T043516Z-29b8)

This commit is contained in:
pitboss 2026-05-22 00:55:00 -05:00
parent ebe3a4fca0
commit 987fc1d89f
8 changed files with 539 additions and 19 deletions

View file

@ -20,8 +20,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::php_routes::{
bind_php_path_params, find_codeigniter_route, find_php_function, php_class_name,
php_formal_names, source_imports_codeigniter,
bind_php_path_params, collect_php_middleware, find_codeigniter_route, find_php_function,
php_class_name, php_formal_names, source_imports_codeigniter,
};
pub struct PhpCodeIgniterAdapter;
@ -53,6 +53,7 @@ impl FrameworkAdapter for PhpCodeIgniterAdapter {
let formals = php_formal_names(func_node, file_bytes);
let request_params = bind_php_path_params(&formals, &path);
let middleware = collect_php_middleware(ast, file_bytes);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
@ -60,7 +61,7 @@ impl FrameworkAdapter for PhpCodeIgniterAdapter {
route: Some(RouteShape { method, path }),
request_params,
response_writer: None,
middleware: Vec::new(),
middleware,
})
}
}

View file

@ -21,8 +21,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::php_routes::{
bind_php_path_params, find_laravel_static_route, find_php_function, php_class_name,
php_formal_names, source_imports_laravel,
bind_php_path_params, collect_php_middleware, find_laravel_static_route, find_php_function,
php_class_name, php_formal_names, source_imports_laravel,
};
pub struct PhpLaravelAdapter;
@ -54,6 +54,7 @@ impl FrameworkAdapter for PhpLaravelAdapter {
let formals = php_formal_names(func_node, file_bytes);
let request_params = bind_php_path_params(&formals, &path);
let middleware = collect_php_middleware(ast, file_bytes);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
@ -61,7 +62,7 @@ impl FrameworkAdapter for PhpLaravelAdapter {
route: Some(RouteShape { method, path }),
request_params,
response_writer: None,
middleware: Vec::new(),
middleware,
})
}
}
@ -138,6 +139,37 @@ mod tests {
assert_eq!(binding.route.unwrap().method, HttpMethod::DELETE);
}
#[test]
fn populates_middleware_from_chained_call() {
let src: &[u8] = b"<?php\nuse Illuminate\\Support\\Facades\\Route;\nRoute::get('/users/{id}', 'UserController@show')->middleware('auth');\nclass UserController {\n public function show($id) { return $id; }\n}\n";
let tree = parse(src);
let binding = PhpLaravelAdapter
.detect(&summary("show"), tree.root_node(), src)
.expect("binding");
assert!(
binding.middleware.iter().any(|m| m.name == "auth"),
"got {:?}",
binding.middleware
);
}
#[test]
fn populates_middleware_from_constructor_call() {
let src: &[u8] = b"<?php\nuse Illuminate\\Support\\Facades\\Route;\nRoute::get('/users', 'UserController@index');\nclass UserController {\n public function __construct() { $this->middleware('auth:sanctum'); }\n public function index() { return 1; }\n}\n";
let tree = parse(src);
let binding = PhpLaravelAdapter
.detect(&summary("index"), tree.root_node(), src)
.expect("binding");
assert!(
binding
.middleware
.iter()
.any(|m| m.name == "auth:sanctum"),
"got {:?}",
binding.middleware
);
}
#[test]
fn skips_when_laravel_not_imported() {
let src: &[u8] = b"<?php\nfunction f($x) { return $x; }\n";

View file

@ -10,7 +10,10 @@
//! helpers here keeps the three adapters terse and lets every
//! framework share the same placeholder-binding semantics.
use crate::dynamic::framework::{HttpMethod, ParamBinding, ParamSource};
use crate::dynamic::framework::{
HttpMethod, MiddlewareShape, ParamBinding, ParamSource, auth_markers,
};
use crate::symbol::Lang;
use tree_sitter::Node;
/// True when `bytes` carries any of the well-known Laravel import
@ -661,6 +664,117 @@ fn codeigniter_callable_matches(
}
}
/// Walk every PHP attach-site in `root` and collect arguments whose
/// names match a known PHP middleware marker (see
/// [`crate::dynamic::framework::auth_markers::is_protective`]).
///
/// Three attach idioms are recognised:
///
/// - **Chained `->middleware(...)` member calls** (Laravel):
/// `Route::get('/x', '...')->middleware('auth:sanctum')`,
/// `$this->middleware(['auth', 'verified'])` declared in a
/// controller constructor.
/// - **Static `Route::middleware(...)` scoped calls** (Laravel):
/// `Route::middleware(['auth'])->group(...)`.
/// - **Symfony PHP attributes** on `class_declaration` /
/// `method_declaration` / `function_definition`: `#[IsGranted]`,
/// `#[Security]`. Attribute leaf names are wrapped with the
/// `#[...]` brackets so they classify against the PHP marker
/// table (`#[IsGranted]`, `#[Security]`).
///
/// Argument rendering (for `->middleware(...)` / `Route::middleware(...)`):
/// - string literal → string content (e.g. `'auth:sanctum'`)
/// - array literal → each element string content, in order
/// - non-string args dropped silently
///
/// 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 broader inclusion is
/// needed. CodeIgniter `['filter' => 'auth-jwt']` array-key idiom is
/// out of scope for v1; revisit when a real-world CodeIgniter fixture
/// surfaces the gap.
pub fn collect_php_middleware(root: Node<'_>, bytes: &[u8]) -> Vec<MiddlewareShape> {
let mut raw: Vec<String> = Vec::new();
walk_php_middleware(root, bytes, &mut raw);
let mut out: Vec<MiddlewareShape> = Vec::new();
for name in raw {
if auth_markers::is_protective(Lang::Php, &name)
&& !out.iter().any(|m| m.name == name)
{
out.push(MiddlewareShape { name });
}
}
out
}
fn walk_php_middleware(node: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
match node.kind() {
"member_call_expression" | "scoped_call_expression" => {
collect_middleware_call(node, bytes, out);
}
"class_declaration" | "method_declaration" | "function_definition" => {
iter_php_attributes(node, bytes, |_ann, leaf| {
out.push(format!("#[{leaf}]"));
});
}
_ => {}
}
let mut cur = node.walk();
for child in node.children(&mut cur) {
walk_php_middleware(child, bytes, out);
}
}
fn collect_middleware_call(call: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
let Some(name_node) = call.child_by_field_name("name") else {
return;
};
let Ok(name) = name_node.utf8_text(bytes) else {
return;
};
if name != "middleware" {
return;
}
let Some(args) = call.child_by_field_name("arguments") else {
return;
};
let mut ac = args.walk();
for arg in args.named_children(&mut ac) {
if arg.kind() != "argument" {
continue;
}
if arg.child_by_field_name("name").is_some() {
continue;
}
let Some(value) = arg.named_child(0) else {
continue;
};
push_middleware_value(value, bytes, out);
}
}
fn push_middleware_value(node: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
match node.kind() {
"string" | "encapsed_string" => {
if let Some(s) = string_content(node, bytes) {
out.push(s);
}
}
"array_creation_expression" => {
let mut ac = node.walk();
for elem in node.named_children(&mut ac) {
if elem.kind() != "array_element_initializer" {
continue;
}
if let Some(value) = elem.named_child(0) {
push_middleware_value(value, bytes, out);
}
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -792,4 +906,81 @@ mod tests {
assert_eq!(hit.0, HttpMethod::GET);
assert_eq!(hit.1, "users/(:num)");
}
#[test]
fn collects_chained_middleware_string_arg() {
let src: &[u8] =
b"<?php\nRoute::get('/users', 'UserController@index')->middleware('auth');\n";
let tree = parse(src);
let mw = collect_php_middleware(tree.root_node(), src);
assert!(mw.iter().any(|m| m.name == "auth"), "got {mw:?}");
}
#[test]
fn collects_chained_middleware_with_sanctum_guard() {
let src: &[u8] = b"<?php\nRoute::get('/x', 'C@x')->middleware('auth:sanctum');\n";
let tree = parse(src);
let mw = collect_php_middleware(tree.root_node(), src);
assert!(mw.iter().any(|m| m.name == "auth:sanctum"), "got {mw:?}");
}
#[test]
fn collects_array_middleware_arg() {
let src: &[u8] =
b"<?php\nRoute::get('/x', 'C@x')->middleware(['auth', 'verified']);\n";
let tree = parse(src);
let mw = collect_php_middleware(tree.root_node(), src);
assert!(mw.iter().any(|m| m.name == "auth"), "got {mw:?}");
assert!(mw.iter().any(|m| m.name == "verified"), "got {mw:?}");
}
#[test]
fn collects_static_route_middleware_chain() {
let src: &[u8] = b"<?php\nRoute::middleware(['auth'])->group(function () {});\n";
let tree = parse(src);
let mw = collect_php_middleware(tree.root_node(), src);
assert!(mw.iter().any(|m| m.name == "auth"), "got {mw:?}");
}
#[test]
fn collects_controller_constructor_middleware() {
let src: &[u8] = b"<?php\nclass C {\n public function __construct() {\n $this->middleware('auth');\n }\n}\n";
let tree = parse(src);
let mw = collect_php_middleware(tree.root_node(), src);
assert!(mw.iter().any(|m| m.name == "auth"), "got {mw:?}");
}
#[test]
fn collects_symfony_is_granted_attribute() {
let src: &[u8] = b"<?php\nclass C {\n #[IsGranted('ROLE_USER')]\n public function show($id) { return $id; }\n}\n";
let tree = parse(src);
let mw = collect_php_middleware(tree.root_node(), src);
assert!(mw.iter().any(|m| m.name == "#[IsGranted]"), "got {mw:?}");
}
#[test]
fn collects_symfony_security_attribute_at_class_level() {
let src: &[u8] = b"<?php\n#[Security(\"is_granted('ROLE_ADMIN')\")]\nclass C {\n public function show() { return 1; }\n}\n";
let tree = parse(src);
let mw = collect_php_middleware(tree.root_node(), src);
assert!(mw.iter().any(|m| m.name == "#[Security]"), "got {mw:?}");
}
#[test]
fn drops_unknown_php_middleware_names() {
let src: &[u8] =
b"<?php\nRoute::get('/x', 'C@x')->middleware('custom-thing-not-in-table');\n";
let tree = parse(src);
let mw = collect_php_middleware(tree.root_node(), src);
assert!(mw.is_empty(), "got {mw:?}");
}
#[test]
fn dedupes_repeated_php_middleware() {
let src: &[u8] = b"<?php\nRoute::get('/a', 'C@a')->middleware('auth');\nRoute::get('/b', 'C@b')->middleware('auth');\n";
let tree = parse(src);
let mw = collect_php_middleware(tree.root_node(), src);
let auth_count = mw.iter().filter(|m| m.name == "auth").count();
assert_eq!(auth_count, 1, "got {mw:?}");
}
}

View file

@ -19,8 +19,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::php_routes::{
bind_php_path_params, find_php_function, first_php_string_arg, iter_php_attributes,
methods_named_arg, php_formal_names, source_imports_symfony,
bind_php_path_params, collect_php_middleware, find_php_function, first_php_string_arg,
iter_php_attributes, methods_named_arg, php_formal_names, source_imports_symfony,
};
pub struct PhpSymfonyAdapter;
@ -84,6 +84,7 @@ impl FrameworkAdapter for PhpSymfonyAdapter {
let path = join_route_path(&class_prefix, &method_path);
let formals = php_formal_names(func_node, file_bytes);
let request_params = bind_php_path_params(&formals, &path);
let middleware = collect_php_middleware(ast, file_bytes);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
@ -94,7 +95,7 @@ impl FrameworkAdapter for PhpSymfonyAdapter {
}),
request_params,
response_writer: None,
middleware: Vec::new(),
middleware,
})
}
}
@ -161,6 +162,23 @@ mod tests {
assert_eq!(route.path, "/x");
}
#[test]
fn populates_middleware_from_is_granted_attribute() {
let src: &[u8] = b"<?php\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nclass C {\n #[Route('/x')]\n #[IsGranted('ROLE_USER')]\n public function show() { return 1; }\n}\n";
let tree = parse(src);
let binding = PhpSymfonyAdapter
.detect(&summary("show"), tree.root_node(), src)
.expect("binding");
assert!(
binding
.middleware
.iter()
.any(|m| m.name == "#[IsGranted]"),
"got {:?}",
binding.middleware
);
}
#[test]
fn skips_when_symfony_not_imported() {
let src: &[u8] = b"<?php\n#[Route('/x')]\nfunction f() { return 1; }\n";

View file

@ -16,8 +16,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::ruby_routes::{
bind_path_params, class_extends, class_includes, class_name, find_class_with_method,
method_formal_names, source_imports_hanami,
bind_path_params, class_extends, class_includes, class_name, collect_ruby_middleware,
find_class_with_method, method_formal_names, source_imports_hanami,
};
pub struct RubyHanamiAdapter;
@ -172,6 +172,7 @@ impl FrameworkAdapter for RubyHanamiAdapter {
let (http_method, path) = route_for_class(file_bytes, cls_name, &default);
let formals = method_formal_names(method, file_bytes);
let request_params = bind_path_params(&formals, &path);
let middleware = collect_ruby_middleware(ast, file_bytes);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
@ -181,7 +182,7 @@ impl FrameworkAdapter for RubyHanamiAdapter {
}),
request_params,
response_writer: None,
middleware: Vec::new(),
middleware,
})
}
}
@ -356,6 +357,23 @@ mod tests {
assert_eq!(route.path, "/new");
}
#[test]
fn populates_middleware_from_before_action() {
let src: &[u8] = b"require 'hanami/action'\nclass Show < Hanami::Action\n before_action :authenticate_user!\n def call(req)\n 'ok'\n end\nend\n";
let tree = parse(src);
let binding = RubyHanamiAdapter
.detect(&summary("call"), tree.root_node(), src)
.expect("binding");
assert!(
binding
.middleware
.iter()
.any(|m| m.name == "authenticate_user!"),
"expected authenticate_user! marker, got {:?}",
binding.middleware
);
}
#[test]
fn skips_non_hanami_classes() {
let src: &[u8] =

View file

@ -17,8 +17,9 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::ruby_routes::{
bind_path_params, class_extends, class_name, find_class_with_method, first_string_arg,
first_symbol_arg, kwarg_string, method_formal_names, source_imports_rails, verb_from_ident,
bind_path_params, class_extends, class_name, collect_ruby_middleware, find_class_with_method,
first_string_arg, first_symbol_arg, kwarg_string, method_formal_names, source_imports_rails,
verb_from_ident,
};
pub struct RubyRailsAdapter;
@ -283,6 +284,7 @@ impl FrameworkAdapter for RubyRailsAdapter {
let formals = method_formal_names(method, file_bytes);
let request_params = bind_path_params(&formals, &path);
let middleware = collect_ruby_middleware(ast, file_bytes);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
@ -293,7 +295,7 @@ impl FrameworkAdapter for RubyRailsAdapter {
}),
request_params,
response_writer: None,
middleware: Vec::new(),
middleware,
})
}
}
@ -464,6 +466,34 @@ mod tests {
);
}
#[test]
fn populates_middleware_from_before_action() {
let src: &[u8] = b"class UsersController < ApplicationController\n before_action :authenticate_user!\n def index\n 'ok'\n end\nend\n";
let tree = parse(src);
let binding = RubyRailsAdapter
.detect(&summary("index"), tree.root_node(), src)
.expect("binding");
assert_eq!(binding.middleware.len(), 1);
assert_eq!(binding.middleware[0].name, "authenticate_user!");
}
#[test]
fn populates_middleware_from_protect_from_forgery() {
let src: &[u8] = b"class A < ApplicationController\n protect_from_forgery with: :exception\n def index\n 'ok'\n end\nend\n";
let tree = parse(src);
let binding = RubyRailsAdapter
.detect(&summary("index"), tree.root_node(), src)
.expect("binding");
assert!(
binding
.middleware
.iter()
.any(|m| m.name == "protect_from_forgery"),
"expected protect_from_forgery marker, got {:?}",
binding.middleware
);
}
#[test]
fn rails_controller_path_drops_suffix_and_snake_cases() {
assert_eq!(rails_controller_path("UsersController"), "users");

View file

@ -8,7 +8,8 @@
//! helpers here keeps the three adapters terse and lets every
//! framework share the same placeholder-binding semantics.
use crate::dynamic::framework::{HttpMethod, ParamBinding, ParamSource};
use crate::dynamic::framework::{HttpMethod, MiddlewareShape, ParamBinding, ParamSource, auth_markers};
use crate::symbol::Lang;
use tree_sitter::Node;
/// True when `bytes` carries any of the well-known Rails import
@ -448,6 +449,136 @@ pub fn verb_from_ident(ident: &str) -> Option<HttpMethod> {
}
}
/// Ruby attach-verb identifiers that introduce a middleware /
/// before-filter / output sanitiser declaration. Rails controllers
/// use `before_action :authenticate_user!`; Sinatra modular apps use
/// `use Rack::Auth::Basic`; both Rails and Hanami v1 also accept
/// `before :method_name`. Some verbs (`protect_from_forgery`) act as
/// self-naming markers with no positional argument.
const RUBY_ATTACH_VERBS: &[&str] = &[
"before_action",
"prepend_before_action",
"skip_before_action",
"around_action",
"append_before_action",
"before",
"use",
"protect_from_forgery",
];
/// Walk every Ruby `call` node whose identifier matches a known
/// middleware-attach verb and collect arguments whose names match a
/// known Ruby middleware marker (see
/// [`crate::dynamic::framework::auth_markers::is_protective`]).
///
/// Per-framework attach-verb idioms:
/// - Rails: `before_action :authenticate_user!`,
/// `protect_from_forgery with: :exception`,
/// `prepend_before_action :require_login`
/// - Sinatra: `use Rack::Auth::Basic`, `before do ... end`
/// - Hanami v1: `before :authenticate_user!`
///
/// Argument rendering:
/// - simple symbol (`:authenticate_user!`) → `"authenticate_user!"`
/// - bare identifier (`use AuthMiddleware`) → `"AuthMiddleware"`
/// - constant (`use Authenticate`) → `"Authenticate"`
/// - scoped constant (`use Rack::Auth::Basic`) → `"Rack::Auth::Basic"`
///
/// In addition the verb token itself is emitted as a candidate so
/// self-naming forms like `protect_from_forgery` (often invoked with
/// only kwargs) classify against the Ruby auth-markers table.
///
/// Recursion stops at `method` / `singleton_method` boundaries so a
/// stray `before_action :x` inside an unrelated method body is not
/// picked up. 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
/// broader inclusion is needed.
pub fn collect_ruby_middleware(root: Node<'_>, bytes: &[u8]) -> Vec<MiddlewareShape> {
let mut raw: Vec<String> = Vec::new();
walk_attach_calls(root, bytes, &mut raw);
let mut out: Vec<MiddlewareShape> = Vec::new();
for name in raw {
if auth_markers::is_protective(Lang::Ruby, &name)
&& !out.iter().any(|m| m.name == name)
{
out.push(MiddlewareShape { name });
}
}
out
}
fn walk_attach_calls(node: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
if node.kind() == "call" {
try_collect_attach_call(node, bytes, out);
}
// Middleware declarations live at class body / top level / routes
// block scope, not inside per-action method bodies. Skip descent
// into method nodes to avoid binding stray `before_action :x` calls
// hidden inside a helper method.
if matches!(node.kind(), "method" | "singleton_method") {
return;
}
let mut cur = node.walk();
for child in node.children(&mut cur) {
walk_attach_calls(child, bytes, out);
}
}
fn try_collect_attach_call(call: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
let mut cur = call.walk();
let mut verb: Option<&str> = None;
let mut args: Option<Node<'_>> = None;
for child in call.named_children(&mut cur) {
match child.kind() {
"identifier" => {
if verb.is_none()
&& let Ok(t) = child.utf8_text(bytes)
{
verb = Some(t);
}
}
"argument_list" => args = Some(child),
_ => {}
}
}
let Some(verb) = verb else { return };
if !RUBY_ATTACH_VERBS.contains(&verb) {
return;
}
// Emit the verb itself so self-naming forms classify (e.g.
// `protect_from_forgery with: :exception` → marker
// `protect_from_forgery`).
out.push(verb.to_owned());
let Some(args) = args else { return };
let mut ac = args.walk();
for arg in args.named_children(&mut ac) {
push_middleware_arg(arg, bytes, out);
}
}
fn push_middleware_arg(node: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
match node.kind() {
"simple_symbol" => {
if let Ok(t) = node.utf8_text(bytes) {
let trimmed = t.trim_start_matches(':').trim().to_owned();
if !trimmed.is_empty() {
out.push(trimmed);
}
}
}
"identifier" | "constant" | "scope_resolution" => {
if let Ok(t) = node.utf8_text(bytes) {
let name = t.trim().to_owned();
if !name.is_empty() {
out.push(name);
}
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -577,4 +708,84 @@ mod tests {
let args = call.child_by_field_name("arguments").unwrap();
assert_eq!(first_string_arg(args, src), Some("/run".into()));
}
#[test]
fn collects_rails_before_action_symbol() {
let src: &[u8] = b"class UsersController < ApplicationController\n before_action :authenticate_user!\n def index\n 'ok'\n end\nend\n";
let tree = parse(src);
let mw = collect_ruby_middleware(tree.root_node(), src);
assert_eq!(mw.len(), 1, "expected exactly one marker, got {mw:?}");
assert_eq!(mw[0].name, "authenticate_user!");
}
#[test]
fn collects_rails_protect_from_forgery_self_naming() {
// `protect_from_forgery with: :exception` carries no positional
// arg — the verb itself must be recognised as the marker.
let src: &[u8] = b"class A < ApplicationController\n protect_from_forgery with: :exception\nend\n";
let tree = parse(src);
let mw = collect_ruby_middleware(tree.root_node(), src);
assert!(
mw.iter().any(|m| m.name == "protect_from_forgery"),
"got {mw:?}"
);
}
#[test]
fn collects_sinatra_use_rack_auth_basic() {
let src: &[u8] = b"require 'sinatra/base'\nclass App < Sinatra::Base\n use Rack::Auth::Basic\n get '/x' do\n 'ok'\n end\nend\n";
let tree = parse(src);
let mw = collect_ruby_middleware(tree.root_node(), src);
assert!(
mw.iter().any(|m| m.name == "Rack::Auth::Basic"),
"got {mw:?}"
);
}
#[test]
fn collects_sinatra_use_rack_attack_rate_limit() {
let src: &[u8] =
b"require 'sinatra'\nuse Rack::Attack\nget '/x' do\n 'ok'\nend\n";
let tree = parse(src);
let mw = collect_ruby_middleware(tree.root_node(), src);
assert!(mw.iter().any(|m| m.name == "Rack::Attack"), "got {mw:?}");
}
#[test]
fn dedupes_repeated_markers() {
let src: &[u8] = b"class A < ApplicationController\n before_action :authenticate_user!\n before_action :authenticate_user!\nend\n";
let tree = parse(src);
let mw = collect_ruby_middleware(tree.root_node(), src);
assert_eq!(mw.len(), 1);
assert_eq!(mw[0].name, "authenticate_user!");
}
#[test]
fn drops_unknown_marker_names() {
let src: &[u8] = b"class A < ApplicationController\n before_action :do_something_custom\nend\n";
let tree = parse(src);
let mw = collect_ruby_middleware(tree.root_node(), src);
// `do_something_custom` is not in the Ruby auth-markers table.
// The verb itself (`before_action`) is also not registered as a
// standalone marker — it only flags the call to walk for args.
assert!(mw.is_empty(), "got {mw:?}");
}
#[test]
fn skips_middleware_call_hidden_inside_method_body() {
let src: &[u8] = b"class A < ApplicationController\n def helper\n before_action :authenticate_user!\n end\nend\n";
let tree = parse(src);
let mw = collect_ruby_middleware(tree.root_node(), src);
assert!(mw.is_empty(), "got {mw:?}");
}
#[test]
fn collects_multiple_distinct_markers() {
let src: &[u8] = b"class A < ApplicationController\n before_action :authenticate_user!\n protect_from_forgery with: :exception\nend\n";
let tree = parse(src);
let mw = collect_ruby_middleware(tree.root_node(), src);
assert_eq!(mw.len(), 2);
assert_eq!(mw[0].name, "authenticate_user!");
assert_eq!(mw[1].name, "protect_from_forgery");
}
}

View file

@ -19,7 +19,8 @@ use crate::symbol::Lang;
use tree_sitter::Node;
use super::ruby_routes::{
bind_path_params, first_string_arg, source_imports_sinatra, verb_from_ident,
bind_path_params, collect_ruby_middleware, first_string_arg, source_imports_sinatra,
verb_from_ident,
};
pub struct RubySinatraAdapter;
@ -148,6 +149,7 @@ impl FrameworkAdapter for RubySinatraAdapter {
.find(|r| path_stem(&r.path) == target)
.or_else(|| routes.first())?;
let request_params = bind_path_params(&route.block_params, &route.path);
let middleware = collect_ruby_middleware(ast, file_bytes);
Some(FrameworkBinding {
adapter: ADAPTER_NAME.to_owned(),
kind: EntryKind::HttpRoute,
@ -157,7 +159,7 @@ impl FrameworkAdapter for RubySinatraAdapter {
}),
request_params,
response_writer: None,
middleware: Vec::new(),
middleware,
})
}
}
@ -294,6 +296,23 @@ mod tests {
);
}
#[test]
fn populates_middleware_from_use_rack_attack() {
let src: &[u8] = b"require 'sinatra'\nuse Rack::Attack\nget '/run' do |payload|\n payload\nend\n";
let tree = parse(src);
let binding = RubySinatraAdapter
.detect(&summary("run"), tree.root_node(), src)
.expect("binding");
assert!(
binding
.middleware
.iter()
.any(|m| m.name == "Rack::Attack"),
"expected Rack::Attack marker, got {:?}",
binding.middleware
);
}
#[test]
fn path_stem_strips_sigils() {
assert_eq!(path_stem("/run"), "run");