mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
refactor(dynamic): add multi-method support to RouteShape, update framework bindings, and improve test coverage
This commit is contained in:
parent
4bcdec3a1b
commit
ca075a7141
55 changed files with 524 additions and 215 deletions
|
|
@ -140,7 +140,9 @@ mod tests {
|
|||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "sign".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("MessageDigest.getInstance")],
|
||||
callees: vec![crate::summary::CalleeSite::bare(
|
||||
"MessageDigest.getInstance",
|
||||
)],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
|
|
@ -169,7 +171,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn skips_plain_method() {
|
||||
let src: &[u8] = b"public class Plain { public static int add(int a, int b) { return a + b; } }\n";
|
||||
let src: &[u8] =
|
||||
b"public class Plain { public static int add(int a, int b) { return a + b; } }\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
|
|
|
|||
|
|
@ -122,7 +122,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn fires_on_math_random_key() {
|
||||
let src: &[u8] = b"function run(value) { return Math.random(); }\nmodule.exports = { run };\n";
|
||||
let src: &[u8] =
|
||||
b"function run(value) { return Math.random(); }\nmodule.exports = { run };\n";
|
||||
let tree = parse_js(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
|
|
|
|||
|
|
@ -128,8 +128,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn fires_on_md5() {
|
||||
let src: &[u8] =
|
||||
b"<?php\nfunction sign($value) {\n return md5($value);\n}\n";
|
||||
let src: &[u8] = b"<?php\nfunction sign($value) {\n return md5($value);\n}\n";
|
||||
let tree = parse_php(src);
|
||||
let summary = FuncSummary {
|
||||
name: "sign".into(),
|
||||
|
|
@ -180,8 +179,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn skips_when_sha256_hashing_present() {
|
||||
let src: &[u8] =
|
||||
b"<?php\nfunction sign($value) {\n return hash('sha256', $value);\n}\n";
|
||||
let src: &[u8] = b"<?php\nfunction sign($value) {\n return hash('sha256', $value);\n}\n";
|
||||
let tree = parse_php(src);
|
||||
let summary = FuncSummary {
|
||||
name: "sign".into(),
|
||||
|
|
|
|||
|
|
@ -159,7 +159,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn fires_on_md5_compute() {
|
||||
let src: &[u8] = b"use md5;\npub fn sign(value: &[u8]) -> md5::Digest {\n md5::compute(value)\n}\n";
|
||||
let src: &[u8] =
|
||||
b"use md5;\npub fn sign(value: &[u8]) -> md5::Digest {\n md5::compute(value)\n}\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "sign".into(),
|
||||
|
|
|
|||
|
|
@ -34,10 +34,7 @@ fn callee_is_outbound_http(name: &str) -> bool {
|
|||
}
|
||||
|
||||
fn source_imports_go_http_client(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"\"net/http\"",
|
||||
b"net/http\"",
|
||||
];
|
||||
const NEEDLES: &[&[u8]] = &[b"\"net/http\"", b"net/http\""];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
|
|
|
|||
|
|
@ -213,7 +213,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn skips_plain_method() {
|
||||
let src: &[u8] = b"public class Plain { public static int add(int a, int b) { return a + b; } }\n";
|
||||
let src: &[u8] =
|
||||
b"public class Plain { public static int add(int a, int b) { return a + b; } }\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
|
|
|
|||
|
|
@ -19,15 +19,7 @@ fn callee_is_outbound_http(name: &str) -> bool {
|
|||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"urlopen"
|
||||
| "get"
|
||||
| "post"
|
||||
| "put"
|
||||
| "patch"
|
||||
| "delete"
|
||||
| "request"
|
||||
| "Request"
|
||||
| "send"
|
||||
"urlopen" | "get" | "post" | "put" | "patch" | "delete" | "request" | "Request" | "send"
|
||||
) || matches!(
|
||||
name,
|
||||
"urllib.request.urlopen"
|
||||
|
|
|
|||
|
|
@ -19,33 +19,31 @@ const ADAPTER_NAME: &str = "data-exfil-ruby";
|
|||
fn callee_is_outbound_http(name: &str) -> bool {
|
||||
let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name);
|
||||
let last = last.rsplit_once('.').map(|(_, s)| s).unwrap_or(last);
|
||||
matches!(
|
||||
last,
|
||||
"get_response" | "post_form" | "request" | "start"
|
||||
) || matches!(
|
||||
name,
|
||||
"Net::HTTP.get"
|
||||
| "Net::HTTP.get_response"
|
||||
| "Net::HTTP.post_form"
|
||||
| "Net::HTTP.start"
|
||||
| "Net::HTTP::Get.new"
|
||||
| "Net::HTTP::Post.new"
|
||||
| "RestClient.get"
|
||||
| "RestClient.post"
|
||||
| "RestClient.put"
|
||||
| "RestClient.delete"
|
||||
| "RestClient::Request.execute"
|
||||
| "HTTParty.get"
|
||||
| "HTTParty.post"
|
||||
| "HTTParty.put"
|
||||
| "HTTParty.delete"
|
||||
| "Faraday.get"
|
||||
| "Faraday.post"
|
||||
| "Faraday.new"
|
||||
| "Faraday::Connection.get"
|
||||
| "URI.open"
|
||||
| "Kernel.open"
|
||||
)
|
||||
matches!(last, "get_response" | "post_form" | "request" | "start")
|
||||
|| matches!(
|
||||
name,
|
||||
"Net::HTTP.get"
|
||||
| "Net::HTTP.get_response"
|
||||
| "Net::HTTP.post_form"
|
||||
| "Net::HTTP.start"
|
||||
| "Net::HTTP::Get.new"
|
||||
| "Net::HTTP::Post.new"
|
||||
| "RestClient.get"
|
||||
| "RestClient.post"
|
||||
| "RestClient.put"
|
||||
| "RestClient.delete"
|
||||
| "RestClient::Request.execute"
|
||||
| "HTTParty.get"
|
||||
| "HTTParty.post"
|
||||
| "HTTParty.put"
|
||||
| "HTTParty.delete"
|
||||
| "Faraday.get"
|
||||
| "Faraday.post"
|
||||
| "Faraday.new"
|
||||
| "Faraday::Connection.get"
|
||||
| "URI.open"
|
||||
| "Kernel.open"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_ruby_http_client(file_bytes: &[u8]) -> bool {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ impl FrameworkAdapter for GoChiAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ impl FrameworkAdapter for GoEchoAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ impl FrameworkAdapter for GoFiberAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ impl FrameworkAdapter for GoGinAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -83,10 +83,7 @@ fn detect_micronaut(
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: http_method,
|
||||
path,
|
||||
}),
|
||||
route: Some(RouteShape::single(http_method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -87,10 +87,7 @@ fn detect_quarkus(
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: http_method,
|
||||
path,
|
||||
}),
|
||||
route: Some(RouteShape::single(http_method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -81,10 +81,7 @@ fn detect_servlet(
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: http_method,
|
||||
path,
|
||||
}),
|
||||
route: Some(RouteShape::single(http_method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -128,10 +128,7 @@ fn detect_spring(
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: http_method,
|
||||
path,
|
||||
}),
|
||||
route: Some(RouteShape::single(http_method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ fn detect_express(
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ fn detect_fastify(
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ fn detect_koa(
|
|||
return Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
@ -162,10 +162,7 @@ fn detect_koa(
|
|||
return Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: HttpMethod::GET,
|
||||
path: "/".to_owned(),
|
||||
}),
|
||||
route: Some(RouteShape::single(HttpMethod::GET, "/")),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: vec![MiddlewareShape {
|
||||
|
|
|
|||
|
|
@ -120,10 +120,7 @@ fn detect_nest(
|
|||
Some(FrameworkBinding {
|
||||
adapter: adapter_name.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method,
|
||||
path: full_path,
|
||||
}),
|
||||
route: Some(RouteShape::single(method, full_path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ impl FrameworkAdapter for PhpCodeIgniterAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@
|
|||
|
||||
#[cfg(test)]
|
||||
use crate::dynamic::framework::HttpMethod;
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape};
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
||||
use super::php_routes::{
|
||||
bind_php_path_params, collect_php_middleware, 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_shape,
|
||||
find_php_function, php_class_name, php_formal_names, source_imports_laravel,
|
||||
};
|
||||
|
||||
pub struct PhpLaravelAdapter;
|
||||
|
|
@ -50,16 +50,16 @@ impl FrameworkAdapter for PhpLaravelAdapter {
|
|||
let (func_node, class) = find_php_function(ast, file_bytes, &summary.name)?;
|
||||
let controller = class.and_then(|c| php_class_name(c, file_bytes));
|
||||
|
||||
let (method, path) = find_laravel_static_route(ast, file_bytes, &summary.name, controller)?;
|
||||
let route = find_laravel_static_route_shape(ast, file_bytes, &summary.name, controller)?;
|
||||
|
||||
let formals = php_formal_names(func_node, file_bytes);
|
||||
let request_params = bind_php_path_params(&formals, &path);
|
||||
let request_params = bind_php_path_params(&formals, &route.path);
|
||||
let middleware = collect_php_middleware(ast, file_bytes);
|
||||
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(route),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
@ -139,6 +139,22 @@ mod tests {
|
|||
assert_eq!(binding.route.unwrap().method, HttpMethod::DELETE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_match_route_methods() {
|
||||
let src: &[u8] = b"<?php\nuse Illuminate\\Support\\Facades\\Route;\nRoute::match(['POST', 'PATCH'], '/jobs/{id}', [JobController::class, 'run']);\nclass JobController {\n public function run($id) { return $id; }\n}\n";
|
||||
let tree = parse(src);
|
||||
let binding = PhpLaravelAdapter
|
||||
.detect(&summary("run"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
let route = binding.route.unwrap();
|
||||
assert_eq!(route.method, HttpMethod::POST);
|
||||
assert_eq!(
|
||||
route.reachable_methods(),
|
||||
vec![HttpMethod::POST, HttpMethod::PATCH]
|
||||
);
|
||||
assert_eq!(route.path, "/jobs/{id}");
|
||||
}
|
||||
|
||||
#[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";
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
//! framework share the same placeholder-binding semantics.
|
||||
|
||||
use crate::dynamic::framework::{
|
||||
HttpMethod, MiddlewareShape, ParamBinding, ParamSource, auth_markers,
|
||||
HttpMethod, MiddlewareShape, ParamBinding, ParamSource, RouteShape, auth_markers,
|
||||
};
|
||||
use crate::symbol::Lang;
|
||||
use tree_sitter::Node;
|
||||
|
|
@ -407,7 +407,19 @@ pub fn find_laravel_static_route<'a>(
|
|||
target: &str,
|
||||
controller: Option<&str>,
|
||||
) -> Option<(HttpMethod, String)> {
|
||||
let mut hit: Option<(HttpMethod, String)> = None;
|
||||
find_laravel_static_route_shape(root, bytes, target, controller)
|
||||
.map(|route| (route.method, route.path))
|
||||
}
|
||||
|
||||
/// Laravel route lookup that preserves multi-verb registrations such
|
||||
/// as `Route::any(...)` and `Route::match([...], ...)`.
|
||||
pub fn find_laravel_static_route_shape<'a>(
|
||||
root: Node<'a>,
|
||||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
controller: Option<&str>,
|
||||
) -> Option<RouteShape> {
|
||||
let mut hit: Option<RouteShape> = None;
|
||||
visit_laravel_routes(root, bytes, target, controller, &mut hit);
|
||||
hit
|
||||
}
|
||||
|
|
@ -417,7 +429,7 @@ fn visit_laravel_routes<'a>(
|
|||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
controller: Option<&str>,
|
||||
out: &mut Option<(HttpMethod, String)>,
|
||||
out: &mut Option<RouteShape>,
|
||||
) {
|
||||
if out.is_some() {
|
||||
return;
|
||||
|
|
@ -439,20 +451,81 @@ fn try_laravel_route<'a>(
|
|||
bytes: &'a [u8],
|
||||
target: &str,
|
||||
controller: Option<&str>,
|
||||
) -> Option<(HttpMethod, String)> {
|
||||
) -> Option<RouteShape> {
|
||||
let scope = call.child_by_field_name("scope")?.utf8_text(bytes).ok()?;
|
||||
let scope_leaf = scope.rsplit('\\').next().unwrap_or(scope);
|
||||
if scope_leaf != "Route" {
|
||||
return None;
|
||||
}
|
||||
let verb_node = call.child_by_field_name("name")?.utf8_text(bytes).ok()?;
|
||||
let method = verb_method(verb_node)?;
|
||||
let args = call.child_by_field_name("arguments")?;
|
||||
let path = first_php_string_arg(args, bytes)?;
|
||||
if !laravel_callable_matches(args, bytes, target, controller) {
|
||||
let methods = laravel_route_methods(verb_node, args, bytes)?;
|
||||
let path = laravel_route_path(verb_node, args, bytes)?;
|
||||
if !laravel_callable_matches(verb_node, args, bytes, target, controller) {
|
||||
return None;
|
||||
}
|
||||
Some((method, path))
|
||||
Some(if methods.len() > 1 {
|
||||
RouteShape::multi(methods, path)
|
||||
} else {
|
||||
RouteShape::single(methods[0], path)
|
||||
})
|
||||
}
|
||||
|
||||
fn laravel_route_methods(verb: &str, arguments: Node<'_>, bytes: &[u8]) -> Option<Vec<HttpMethod>> {
|
||||
match verb {
|
||||
"any" => Some(vec![
|
||||
HttpMethod::GET,
|
||||
HttpMethod::HEAD,
|
||||
HttpMethod::POST,
|
||||
HttpMethod::PUT,
|
||||
HttpMethod::PATCH,
|
||||
HttpMethod::DELETE,
|
||||
HttpMethod::OPTIONS,
|
||||
]),
|
||||
"match" => {
|
||||
let first = positional_arg_values(arguments).into_iter().next()?;
|
||||
let mut methods = Vec::new();
|
||||
collect_http_methods(first, bytes, &mut methods);
|
||||
if methods.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(methods)
|
||||
}
|
||||
}
|
||||
other => verb_method(other).map(|method| vec![method]),
|
||||
}
|
||||
}
|
||||
|
||||
fn laravel_route_path(verb: &str, arguments: Node<'_>, bytes: &[u8]) -> Option<String> {
|
||||
if verb != "match" {
|
||||
return first_php_string_arg(arguments, bytes);
|
||||
}
|
||||
positional_arg_values(arguments)
|
||||
.get(1)
|
||||
.and_then(|value| string_content(*value, bytes))
|
||||
}
|
||||
|
||||
fn positional_arg_values<'a>(arguments: Node<'a>) -> Vec<Node<'a>> {
|
||||
let mut cur = arguments.walk();
|
||||
arguments
|
||||
.named_children(&mut cur)
|
||||
.filter(|arg| arg.kind() == "argument" && arg.child_by_field_name("name").is_none())
|
||||
.filter_map(|arg| arg.named_child(0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_http_methods(node: Node<'_>, bytes: &[u8], out: &mut Vec<HttpMethod>) {
|
||||
if matches!(node.kind(), "string" | "encapsed_string")
|
||||
&& let Some(raw) = string_content(node, bytes)
|
||||
&& let Some(method) = HttpMethod::from_ident(&raw)
|
||||
&& !out.contains(&method)
|
||||
{
|
||||
out.push(method);
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
for child in node.named_children(&mut cur) {
|
||||
collect_http_methods(child, bytes, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the second positional arg of a `Route::verb('/x', ...)` call
|
||||
|
|
@ -462,26 +535,15 @@ fn try_laravel_route<'a>(
|
|||
/// - `'Controller@method'` / `'Controller::method'` strings
|
||||
/// - `[ Controller::class, 'method' ]` arrays
|
||||
fn laravel_callable_matches(
|
||||
verb: &str,
|
||||
arguments: Node<'_>,
|
||||
bytes: &[u8],
|
||||
target: &str,
|
||||
controller: Option<&str>,
|
||||
) -> bool {
|
||||
let mut cur = arguments.walk();
|
||||
let mut positional: Vec<Node<'_>> = Vec::new();
|
||||
for arg in arguments.named_children(&mut cur) {
|
||||
if arg.kind() != "argument" {
|
||||
continue;
|
||||
}
|
||||
if arg.child_by_field_name("name").is_some() {
|
||||
continue;
|
||||
}
|
||||
positional.push(arg);
|
||||
}
|
||||
let Some(callable_arg) = positional.get(1) else {
|
||||
return false;
|
||||
};
|
||||
let Some(value) = callable_arg.named_child(0) else {
|
||||
let callable_idx = if verb == "match" { 2 } else { 1 };
|
||||
let positional = positional_arg_values(arguments);
|
||||
let Some(value) = positional.get(callable_idx).copied() else {
|
||||
return false;
|
||||
};
|
||||
match value.kind() {
|
||||
|
|
@ -557,7 +619,6 @@ fn verb_method(verb: &str) -> Option<HttpMethod> {
|
|||
"delete" => Some(HttpMethod::DELETE),
|
||||
"options" => Some(HttpMethod::OPTIONS),
|
||||
"head" => Some(HttpMethod::HEAD),
|
||||
"any" | "match" => Some(HttpMethod::GET),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -895,6 +956,46 @@ mod tests {
|
|||
assert_eq!(hit.1, "/users");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_laravel_any_route_with_all_supported_methods() {
|
||||
let src: &[u8] =
|
||||
b"<?php\nRoute::any('/run', 'JobController@run');\nclass JobController { public function run() {} }\n";
|
||||
let tree = parse(src);
|
||||
let route =
|
||||
find_laravel_static_route_shape(tree.root_node(), src, "run", Some("JobController"))
|
||||
.unwrap();
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(
|
||||
route.reachable_methods(),
|
||||
vec![
|
||||
HttpMethod::GET,
|
||||
HttpMethod::HEAD,
|
||||
HttpMethod::POST,
|
||||
HttpMethod::PUT,
|
||||
HttpMethod::PATCH,
|
||||
HttpMethod::DELETE,
|
||||
HttpMethod::OPTIONS,
|
||||
]
|
||||
);
|
||||
assert_eq!(route.path, "/run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_laravel_match_route_with_declared_methods() {
|
||||
let src: &[u8] =
|
||||
b"<?php\nRoute::match(['POST', 'PUT'], '/run', [JobController::class, 'run']);\nclass JobController { public function run() {} }\n";
|
||||
let tree = parse(src);
|
||||
let route =
|
||||
find_laravel_static_route_shape(tree.root_node(), src, "run", Some("JobController"))
|
||||
.unwrap();
|
||||
assert_eq!(route.method, HttpMethod::POST);
|
||||
assert_eq!(
|
||||
route.reachable_methods(),
|
||||
vec![HttpMethod::POST, HttpMethod::PUT]
|
||||
);
|
||||
assert_eq!(route.path, "/run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_codeigniter_member_route() {
|
||||
let src: &[u8] = b"<?php\n$routes->get('users/(:num)', 'UserController::show');\n";
|
||||
|
|
|
|||
|
|
@ -89,10 +89,7 @@ impl FrameworkAdapter for PhpSymfonyAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: http_method,
|
||||
path,
|
||||
}),
|
||||
route: Some(RouteShape::single(http_method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ impl FrameworkAdapter for PythonDjangoAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ impl FrameworkAdapter for PythonFastApiAdapter {
|
|||
return Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ impl FrameworkAdapter for PythonFlaskAdapter {
|
|||
return Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ impl FrameworkAdapter for PythonStarletteAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -180,10 +180,7 @@ impl FrameworkAdapter for RubyHanamiAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: http_method,
|
||||
path,
|
||||
}),
|
||||
route: Some(RouteShape::single(http_method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -289,10 +289,7 @@ impl FrameworkAdapter for RubyRailsAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: http_method,
|
||||
path,
|
||||
}),
|
||||
route: Some(RouteShape::single(http_method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -153,10 +153,7 @@ impl FrameworkAdapter for RubySinatraAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: route.method,
|
||||
path: route.path.clone(),
|
||||
}),
|
||||
route: Some(RouteShape::single(route.method, route.path.clone())),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ impl FrameworkAdapter for RustActixAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ impl FrameworkAdapter for RustAxumAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ impl FrameworkAdapter for RustRocketAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ impl FrameworkAdapter for RustWarpAdapter {
|
|||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
route: Some(RouteShape::single(method, path)),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware,
|
||||
|
|
|
|||
|
|
@ -37,12 +37,56 @@ pub use crate::entry_points::HttpMethod;
|
|||
pub struct RouteShape {
|
||||
/// HTTP verb (`GET`, `POST`, …).
|
||||
pub method: HttpMethod,
|
||||
/// Additional HTTP verbs that reach the same handler. Empty for
|
||||
/// single-verb routes; when populated, [`Self::method`] is the
|
||||
/// first element for backward-compatible callers that still need a
|
||||
/// single representative method.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub methods: Vec<HttpMethod>,
|
||||
/// Route path template as registered with the framework (e.g.
|
||||
/// `"/users/{id}"`). Adapter-specific placeholder syntax is
|
||||
/// preserved verbatim.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl RouteShape {
|
||||
/// Construct a single-method route while preserving the legacy
|
||||
/// empty-`methods` representation.
|
||||
pub fn single(method: HttpMethod, path: impl Into<String>) -> Self {
|
||||
Self {
|
||||
method,
|
||||
methods: Vec::new(),
|
||||
path: path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a route reachable through multiple HTTP methods.
|
||||
pub fn multi(methods: Vec<HttpMethod>, path: impl Into<String>) -> Self {
|
||||
let mut deduped = Vec::new();
|
||||
for method in methods {
|
||||
if !deduped.contains(&method) {
|
||||
deduped.push(method);
|
||||
}
|
||||
}
|
||||
let method = deduped.first().copied().unwrap_or(HttpMethod::GET);
|
||||
Self {
|
||||
method,
|
||||
methods: deduped,
|
||||
path: path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return every method that reaches this route. Legacy single-method
|
||||
/// shapes return a one-element vector containing [`Self::method`].
|
||||
pub fn reachable_methods(&self) -> Vec<HttpMethod> {
|
||||
if self.methods.is_empty() {
|
||||
vec![self.method]
|
||||
} else {
|
||||
self.methods.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where on the external surface a function formal originates from.
|
||||
///
|
||||
/// Adapters classify each declared parameter into one of these
|
||||
|
|
@ -532,10 +576,7 @@ mod tests {
|
|||
let original = FrameworkBinding {
|
||||
adapter: "flask".into(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape {
|
||||
method: HttpMethod::POST,
|
||||
path: "/users/{id}".into(),
|
||||
}),
|
||||
route: Some(RouteShape::single(HttpMethod::POST, "/users/{id}")),
|
||||
request_params: vec![ParamBinding {
|
||||
index: 0,
|
||||
name: "id".into(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue