mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0009 (20260521T201327Z-3848)
This commit is contained in:
parent
ad15761042
commit
2548a4a802
4 changed files with 379 additions and 9 deletions
|
|
@ -21,8 +21,8 @@ use crate::symbol::Lang;
|
|||
use tree_sitter::Node;
|
||||
|
||||
use super::js_routes::{
|
||||
bind_path_params, find_function_params, find_route_registration, function_formal_names,
|
||||
source_imports_fastify,
|
||||
bind_path_params, extract_route_middleware, find_function_params, find_route_registration,
|
||||
function_formal_names, source_imports_fastify,
|
||||
};
|
||||
|
||||
pub struct JsFastifyAdapter;
|
||||
|
|
@ -63,13 +63,14 @@ impl FrameworkAdapter for JsFastifyAdapter {
|
|||
.map(|p| function_formal_names(p, file_bytes))
|
||||
.unwrap_or_default();
|
||||
let request_params = bind_path_params(&formals, &path);
|
||||
let middleware = extract_route_middleware(ast, file_bytes, &summary.name, &recv);
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
middleware,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -141,6 +142,53 @@ mod tests {
|
|||
assert_eq!(binding.route.unwrap().path, "/inner");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_chained_middleware_and_global_use() {
|
||||
let src: &[u8] = b"const fastify = require('fastify')();\n\
|
||||
fastify.use(helmet());\n\
|
||||
function authz(request, reply, done) { done(); }\n\
|
||||
function handler(request, reply) { reply.send('ok'); }\n\
|
||||
fastify.post('/save', authz, handler);\n";
|
||||
let tree = parse_js(src);
|
||||
let binding = JsFastifyAdapter
|
||||
.detect(&summary("handler"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
let names: Vec<_> = binding.middleware.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["helmet", "authz"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_options_object_pre_handler_hooks() {
|
||||
let src: &[u8] = b"const fastify = require('fastify')();\n\
|
||||
async function handler(request, reply) { reply.send('ok'); }\n\
|
||||
fastify.route({\n\
|
||||
method: 'PUT',\n\
|
||||
url: '/items/:id',\n\
|
||||
onRequest: tokenAuth,\n\
|
||||
preHandler: [authz, validate],\n\
|
||||
handler: handler,\n\
|
||||
});\n";
|
||||
let tree = parse_js(src);
|
||||
let binding = JsFastifyAdapter
|
||||
.detect(&summary("handler"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert_eq!(binding.route.as_ref().unwrap().method, HttpMethod::PUT);
|
||||
let names: Vec<_> = binding.middleware.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["tokenAuth", "authz", "validate"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middleware_empty_when_route_has_no_chain() {
|
||||
let src: &[u8] = b"const fastify = require('fastify')();\n\
|
||||
function handler(request, reply) { reply.send('ok'); }\n\
|
||||
fastify.get('/x', handler);\n";
|
||||
let tree = parse_js(src);
|
||||
let binding = JsFastifyAdapter
|
||||
.detect(&summary("handler"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert!(binding.middleware.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_fastify_not_imported() {
|
||||
let src: &[u8] = b"const express = require('express');\n\
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ use crate::symbol::Lang;
|
|||
use tree_sitter::Node;
|
||||
|
||||
use super::js_routes::{
|
||||
bind_path_params, find_function_params, find_route_registration, function_formal_names,
|
||||
last_segment, source_imports_koa, view_arg_references,
|
||||
bind_path_params, extract_route_middleware, find_function_params, find_route_registration,
|
||||
function_formal_names, last_segment, source_imports_koa, view_arg_references,
|
||||
};
|
||||
|
||||
pub struct JsKoaAdapter;
|
||||
|
|
@ -102,13 +102,14 @@ impl FrameworkAdapter for JsKoaAdapter {
|
|||
if let Some((method, path)) = find_route_registration(ast, file_bytes, &summary.name, &recv)
|
||||
{
|
||||
let request_params = formals_for(&path);
|
||||
let middleware = extract_route_middleware(ast, file_bytes, &summary.name, &recv);
|
||||
return Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
route: Some(RouteShape { method, path }),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
middleware,
|
||||
});
|
||||
}
|
||||
// Fall back to `app.use(handler)` middleware registration. No
|
||||
|
|
@ -192,6 +193,40 @@ mod tests {
|
|||
assert_eq!(binding.middleware[0].name, "koa.use");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_chained_middleware_and_global_app_use() {
|
||||
let src: &[u8] = b"const Koa = require('koa');\n\
|
||||
const Router = require('@koa/router');\n\
|
||||
const app = new Koa();\n\
|
||||
const router = new Router();\n\
|
||||
app.use(helmet());\n\
|
||||
app.use(logger);\n\
|
||||
async function authz(ctx, next) { await next(); }\n\
|
||||
async function validate(ctx, next) { await next(); }\n\
|
||||
async function handler(ctx) { ctx.body = 'ok'; }\n\
|
||||
router.post('/save', authz, validate, handler);\n";
|
||||
let tree = parse_js(src);
|
||||
let binding = JsKoaAdapter
|
||||
.detect(&summary("handler"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
let names: Vec<_> = binding.middleware.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["helmet", "logger", "authz", "validate"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middleware_empty_when_route_has_no_chain() {
|
||||
let src: &[u8] = b"const Koa = require('koa');\n\
|
||||
const Router = require('@koa/router');\n\
|
||||
const router = new Router();\n\
|
||||
async function handler(ctx) { ctx.body = 'ok'; }\n\
|
||||
router.get('/x', handler);\n";
|
||||
let tree = parse_js(src);
|
||||
let binding = JsKoaAdapter
|
||||
.detect(&summary("handler"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert!(binding.middleware.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_koa_not_imported() {
|
||||
let src: &[u8] = b"const express = require('express');\n\
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
//! tree-sitter parser is picked from `summary.lang`.
|
||||
|
||||
use crate::dynamic::framework::{
|
||||
FrameworkAdapter, FrameworkBinding, HttpMethod, ParamBinding, ParamSource, RouteShape,
|
||||
FrameworkAdapter, FrameworkBinding, HttpMethod, MiddlewareShape, ParamBinding, ParamSource,
|
||||
RouteShape,
|
||||
};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
|
|
@ -28,7 +29,7 @@ use tree_sitter::Node;
|
|||
|
||||
use super::js_routes::{
|
||||
bind_path_params, extract_path_placeholders, function_formal_names, http_verb_from_method,
|
||||
source_imports_nest, strip_quotes,
|
||||
last_segment, source_imports_nest, strip_quotes,
|
||||
};
|
||||
|
||||
pub struct JsNestAdapter;
|
||||
|
|
@ -94,6 +95,7 @@ fn detect_nest(
|
|||
.unwrap_or_default();
|
||||
let mut request_params = bind_path_params(&formals, &full_path);
|
||||
refine_with_param_decorators(method_node, file_bytes, &mut request_params, &full_path);
|
||||
let middleware = collect_nest_middleware(class_node, method_node, file_bytes);
|
||||
Some(FrameworkBinding {
|
||||
adapter: adapter_name.to_owned(),
|
||||
kind: EntryKind::HttpRoute,
|
||||
|
|
@ -103,7 +105,7 @@ fn detect_nest(
|
|||
}),
|
||||
request_params,
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
middleware,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -313,6 +315,115 @@ fn decorator_first_string_arg(decorator: Node<'_>, bytes: &[u8]) -> Option<Strin
|
|||
None
|
||||
}
|
||||
|
||||
/// Collect Nest middleware names from `@UseGuards(...)` /
|
||||
/// `@UseInterceptors(...)` / `@UseFilters(...)` / `@UsePipes(...)`
|
||||
/// decorators on the class **and** on the method. Class-level
|
||||
/// decorators fire before the method-level ones at runtime, so they
|
||||
/// are recorded first in the returned vector. Each decorator may
|
||||
/// carry one or more positional arguments (e.g.
|
||||
/// `@UseGuards(AuthGuard, RoleGuard)`); the recorded names are the
|
||||
/// last segment of each identifier / member-expression argument so
|
||||
/// `mod.AuthGuard` collapses to `AuthGuard`. Call-expression
|
||||
/// arguments (`@UseGuards(authGuard())`) record the called function's
|
||||
/// last segment.
|
||||
fn collect_nest_middleware(
|
||||
class_node: Node<'_>,
|
||||
method_node: Node<'_>,
|
||||
bytes: &[u8],
|
||||
) -> Vec<MiddlewareShape> {
|
||||
let mut out: Vec<MiddlewareShape> = Vec::new();
|
||||
for name in collect_use_decorators_for_node(class_node, bytes) {
|
||||
out.push(MiddlewareShape { name });
|
||||
}
|
||||
for name in collect_use_decorators_for_node(method_node, bytes) {
|
||||
out.push(MiddlewareShape { name });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Walk `node`'s own `decorator` field children plus its preceding
|
||||
/// `decorator` siblings, pulling argument names from any `@UseGuards`
|
||||
/// / `@UseInterceptors` / `@UseFilters` / `@UsePipes` decorator
|
||||
/// encountered. The two-source scan mirrors
|
||||
/// [`class_has_controller`] / [`method_verb_and_path`] so the helper
|
||||
/// behaves consistently across tree-sitter grammar variants that
|
||||
/// either nest decorators inside the class/method node or hoist them
|
||||
/// to preceding siblings.
|
||||
fn collect_use_decorators_for_node(node: Node<'_>, bytes: &[u8]) -> Vec<String> {
|
||||
const USE_DECORATORS: &[&str] = &["UseGuards", "UseInterceptors", "UseFilters", "UsePipes"];
|
||||
let mut field_form: Vec<String> = Vec::new();
|
||||
let mut cur = node.walk();
|
||||
for d in node.children_by_field_name("decorator", &mut cur) {
|
||||
for &use_name in USE_DECORATORS {
|
||||
if decorator_text_is(d, bytes, use_name) {
|
||||
collect_decorator_arg_names(d, bytes, &mut field_form);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut sibling_form_groups: Vec<Vec<String>> = Vec::new();
|
||||
let mut prev = node.prev_named_sibling();
|
||||
while let Some(sib) = prev {
|
||||
if sib.kind() == "decorator" {
|
||||
for &use_name in USE_DECORATORS {
|
||||
if decorator_text_is(sib, bytes, use_name) {
|
||||
let mut group: Vec<String> = Vec::new();
|
||||
collect_decorator_arg_names(sib, bytes, &mut group);
|
||||
sibling_form_groups.push(group);
|
||||
}
|
||||
}
|
||||
prev = sib.prev_named_sibling();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
let mut sibling_form: Vec<String> = Vec::new();
|
||||
for group in sibling_form_groups.into_iter().rev() {
|
||||
sibling_form.extend(group);
|
||||
}
|
||||
sibling_form.extend(field_form);
|
||||
sibling_form
|
||||
}
|
||||
|
||||
/// Append each positional argument's display name from a decorator's
|
||||
/// underlying `call_expression`. Identifiers contribute themselves;
|
||||
/// member expressions contribute the last `.`-segment; call
|
||||
/// expressions contribute the called function's last segment. Other
|
||||
/// argument kinds (string literals, object literals) are skipped.
|
||||
fn collect_decorator_arg_names(decorator: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
|
||||
let mut cur = decorator.walk();
|
||||
for c in decorator.children(&mut cur) {
|
||||
if c.kind() != "call_expression" {
|
||||
continue;
|
||||
}
|
||||
let Some(args) = c.child_by_field_name("arguments") else {
|
||||
continue;
|
||||
};
|
||||
let mut ac = args.walk();
|
||||
for a in args.named_children(&mut ac) {
|
||||
match a.kind() {
|
||||
"identifier" => {
|
||||
if let Ok(text) = a.utf8_text(bytes) {
|
||||
out.push(text.to_owned());
|
||||
}
|
||||
}
|
||||
"member_expression" => {
|
||||
if let Ok(text) = a.utf8_text(bytes) {
|
||||
out.push(last_segment(text).to_owned());
|
||||
}
|
||||
}
|
||||
"call_expression" => {
|
||||
if let Some(fn_node) = a.child_by_field_name("function")
|
||||
&& let Ok(text) = fn_node.utf8_text(bytes)
|
||||
{
|
||||
out.push(last_segment(text).to_owned());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Refine the per-formal binding shape using Nest's parameter
|
||||
/// decorators (`@Param('id')`, `@Query('q')`, `@Body()`, `@Headers()`,
|
||||
/// `@Req()` / `@Res()`). A `@Body()` formal becomes
|
||||
|
|
@ -549,6 +660,61 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_method_use_guards_decorator() {
|
||||
let src: &[u8] = b"import { Controller, Get, UseGuards } from '@nestjs/common';\n\
|
||||
import { AuthGuard } from './auth.guard';\n\
|
||||
@Controller('users')\n\
|
||||
export class UsersController {\n\
|
||||
@Get(':id')\n\
|
||||
@UseGuards(AuthGuard)\n\
|
||||
getUser(id: string) { return id; }\n\
|
||||
}\n";
|
||||
let tree = parse_ts(src);
|
||||
let binding = TsNestAdapter
|
||||
.detect(&summary("getUser", "typescript"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
let names: Vec<_> = binding.middleware.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["AuthGuard"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_class_and_method_use_decorators_in_order() {
|
||||
let src: &[u8] = b"import { Controller, Post, UseGuards, UseInterceptors } from '@nestjs/common';\n\
|
||||
import { AuthGuard } from './auth.guard';\n\
|
||||
import { LoggingInterceptor } from './logging.interceptor';\n\
|
||||
import { RoleGuard } from './role.guard';\n\
|
||||
@Controller('admin')\n\
|
||||
@UseGuards(AuthGuard)\n\
|
||||
@UseInterceptors(LoggingInterceptor)\n\
|
||||
export class AdminController {\n\
|
||||
@Post('drop')\n\
|
||||
@UseGuards(RoleGuard)\n\
|
||||
drop(payload: string) { return payload; }\n\
|
||||
}\n";
|
||||
let tree = parse_ts(src);
|
||||
let binding = TsNestAdapter
|
||||
.detect(&summary("drop", "typescript"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
let names: Vec<_> = binding.middleware.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["AuthGuard", "LoggingInterceptor", "RoleGuard"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middleware_empty_when_no_use_decorators() {
|
||||
let src: &[u8] = b"import { Controller, Get } from '@nestjs/common';\n\
|
||||
@Controller('open')\n\
|
||||
export class OpenController {\n\
|
||||
@Get('list')\n\
|
||||
list() { return []; }\n\
|
||||
}\n";
|
||||
let tree = parse_ts(src);
|
||||
let binding = TsNestAdapter
|
||||
.detect(&summary("list", "typescript"), tree.root_node(), src)
|
||||
.expect("binding");
|
||||
assert!(binding.middleware.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_when_not_a_nest_controller() {
|
||||
let src: &[u8] = b"import { Injectable } from '@nestjs/common';\n\
|
||||
|
|
|
|||
|
|
@ -550,6 +550,10 @@ fn walk_for_middleware<'a>(
|
|||
for name in collect_chain_middleware_names(args, bytes, target) {
|
||||
route_chain.push(MiddlewareShape { name });
|
||||
}
|
||||
} else if prop_text == "route" {
|
||||
for name in collect_options_middleware_names(args, bytes, target) {
|
||||
route_chain.push(MiddlewareShape { name });
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut cur = node.walk();
|
||||
|
|
@ -635,6 +639,93 @@ fn collect_use_arg_names(args: Node<'_>, bytes: &[u8]) -> Vec<String> {
|
|||
out
|
||||
}
|
||||
|
||||
/// Collect middleware names from a Fastify options-object call
|
||||
/// `fastify.route({ method, url, onRequest, preHandler, handler })`.
|
||||
/// Inspects the pre-handler hook keys (`onRequest`, `preParsing`,
|
||||
/// `preValidation`, `preHandler`) — each value may be a function
|
||||
/// reference (identifier or `member_expression`), a factory call, or
|
||||
/// an array literal of any of those. Returns the captured names in
|
||||
/// source order across the four hook keys. Only fires when the
|
||||
/// object's `handler:` property references `target`; otherwise an
|
||||
/// unrelated route's hooks would leak into the binding.
|
||||
fn collect_options_middleware_names(args: Node<'_>, bytes: &[u8], target: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut cur = args.walk();
|
||||
for c in args.named_children(&mut cur) {
|
||||
if c.kind() != "object" {
|
||||
continue;
|
||||
}
|
||||
let mut handler_matches = false;
|
||||
let mut hook_names: Vec<String> = Vec::new();
|
||||
let mut oc = c.walk();
|
||||
for pair in c.named_children(&mut oc) {
|
||||
if pair.kind() != "pair" {
|
||||
continue;
|
||||
}
|
||||
let Some(key_raw) = pair
|
||||
.child_by_field_name("key")
|
||||
.and_then(|n| n.utf8_text(bytes).ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(value) = pair.child_by_field_name("value") else {
|
||||
continue;
|
||||
};
|
||||
let key = key_raw.trim_matches(['\'', '"', '`']);
|
||||
match key {
|
||||
"handler" => {
|
||||
if view_arg_references(value, bytes, target) {
|
||||
handler_matches = true;
|
||||
}
|
||||
}
|
||||
"onRequest" | "preParsing" | "preValidation" | "preHandler" => {
|
||||
collect_hook_value_names(value, bytes, &mut hook_names);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if handler_matches {
|
||||
out.extend(hook_names);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Recursively collect identifier / member-expression / call / array
|
||||
/// references from a Fastify hook value into `out`. Used by
|
||||
/// [`collect_options_middleware_names`] — supports the three documented
|
||||
/// hook value shapes: a single function reference
|
||||
/// (`preHandler: authz`), a factory call (`preHandler: authz()`), or
|
||||
/// an array of references (`preHandler: [authz, validate]`).
|
||||
fn collect_hook_value_names(value: Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
|
||||
match value.kind() {
|
||||
"identifier" => {
|
||||
if let Ok(text) = value.utf8_text(bytes) {
|
||||
out.push(text.to_owned());
|
||||
}
|
||||
}
|
||||
"member_expression" => {
|
||||
if let Ok(text) = value.utf8_text(bytes) {
|
||||
out.push(last_segment(text).to_owned());
|
||||
}
|
||||
}
|
||||
"call_expression" => {
|
||||
if let Some(fn_node) = value.child_by_field_name("function")
|
||||
&& let Ok(text) = fn_node.utf8_text(bytes)
|
||||
{
|
||||
out.push(last_segment(text).to_owned());
|
||||
}
|
||||
}
|
||||
"array" => {
|
||||
let mut cur = value.walk();
|
||||
for c in value.named_children(&mut cur) {
|
||||
collect_hook_value_names(c, bytes, out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a Fastify options-object call `fastify.route({ method, url,
|
||||
/// handler })` returning the bound `(method, url)` when the
|
||||
/// `handler:` property references `target`.
|
||||
|
|
@ -820,6 +911,36 @@ mod tests {
|
|||
assert_eq!(names, vec!["csrf", "auth"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_middleware_picks_up_fastify_options_pre_handler() {
|
||||
let src: &[u8] = b"fastify.route({\n\
|
||||
method: 'POST',\n\
|
||||
url: '/items',\n\
|
||||
onRequest: tokenAuth,\n\
|
||||
preHandler: [authz, validate],\n\
|
||||
handler: handler,\n\
|
||||
});\n";
|
||||
let tree = parse_js(src);
|
||||
let recv = |n: &str| n == "fastify";
|
||||
let mw = extract_route_middleware(tree.root_node(), src, "handler", &recv);
|
||||
let names: Vec<_> = mw.iter().map(|m| m.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["tokenAuth", "authz", "validate"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_middleware_ignores_fastify_options_with_different_handler() {
|
||||
let src: &[u8] = b"fastify.route({\n\
|
||||
method: 'POST',\n\
|
||||
url: '/items',\n\
|
||||
preHandler: authz,\n\
|
||||
handler: other,\n\
|
||||
});\n";
|
||||
let tree = parse_js(src);
|
||||
let recv = |n: &str| n == "fastify";
|
||||
let mw = extract_route_middleware(tree.root_node(), src, "handler", &recv);
|
||||
assert!(mw.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_route_registration_matches_fastify_options_object() {
|
||||
let src: &[u8] =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue