mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
768 lines
27 KiB
Rust
768 lines
27 KiB
Rust
//! NestJS [`super::super::FrameworkAdapter`] (Phase 13 — Track L.11).
|
|
//!
|
|
//! Recognises Nest's controller-class decorator surface:
|
|
//! - `@Controller('users')` on the class establishes the route
|
|
//! prefix.
|
|
//! - `@Get(':id')` / `@Post()` / `@Put('/x')` / `@Patch()` /
|
|
//! `@Delete()` / `@Head()` / `@Options()` / `@All()` on the
|
|
//! method establishes the verb + sub-path; the full route is the
|
|
//! concatenation `prefix + path`.
|
|
//! - Parameter decorators (`@Param('id')`, `@Query('q')`,
|
|
//! `@Body()`, `@Headers()`, `@Req()`, `@Res()`) bind individual
|
|
//! formals to request slots.
|
|
//!
|
|
//! NestJS is TypeScript-first. The adapter is registered under both
|
|
//! [`Lang::TypeScript`] and [`Lang::JavaScript`] so Babel-transpiled
|
|
//! Nest projects (still common in the wild) are not silently
|
|
//! skipped — JS Nest projects emit the same decorator syntax via
|
|
//! `experimentalDecorators` / `legacyDecorators`. The lang-aware
|
|
//! tree-sitter parser is picked from `summary.lang`.
|
|
|
|
use crate::dynamic::framework::{
|
|
FrameworkAdapter, FrameworkBinding, HttpMethod, MiddlewareShape, ParamBinding, ParamSource,
|
|
RouteShape,
|
|
};
|
|
use crate::evidence::EntryKind;
|
|
use crate::summary::FuncSummary;
|
|
use crate::summary::ssa_summary::SsaFuncSummary;
|
|
use crate::symbol::Lang;
|
|
use tree_sitter::Node;
|
|
|
|
use super::js_routes::{
|
|
bind_path_params, extract_path_placeholders, function_formal_names, http_verb_from_method,
|
|
last_segment, source_imports_nest, source_imports_nest_common, strip_quotes,
|
|
};
|
|
|
|
pub struct JsNestAdapter;
|
|
pub struct TsNestAdapter;
|
|
|
|
const JS_ADAPTER_NAME: &str = "js-nest";
|
|
const TS_ADAPTER_NAME: &str = "ts-nest";
|
|
|
|
impl FrameworkAdapter for JsNestAdapter {
|
|
fn name(&self) -> &'static str {
|
|
JS_ADAPTER_NAME
|
|
}
|
|
|
|
fn lang(&self) -> Lang {
|
|
Lang::JavaScript
|
|
}
|
|
|
|
fn detect(
|
|
&self,
|
|
summary: &FuncSummary,
|
|
ast: Node<'_>,
|
|
file_bytes: &[u8],
|
|
) -> Option<FrameworkBinding> {
|
|
detect_nest(summary, ast, file_bytes, JS_ADAPTER_NAME)
|
|
}
|
|
|
|
fn detect_with_context(
|
|
&self,
|
|
summary: &FuncSummary,
|
|
_ssa_summary: Option<&SsaFuncSummary>,
|
|
ast: Node<'_>,
|
|
file_bytes: &[u8],
|
|
) -> Option<FrameworkBinding> {
|
|
detect_nest(summary, ast, file_bytes, JS_ADAPTER_NAME)
|
|
}
|
|
}
|
|
|
|
impl FrameworkAdapter for TsNestAdapter {
|
|
fn name(&self) -> &'static str {
|
|
TS_ADAPTER_NAME
|
|
}
|
|
|
|
fn lang(&self) -> Lang {
|
|
Lang::TypeScript
|
|
}
|
|
|
|
fn detect(
|
|
&self,
|
|
summary: &FuncSummary,
|
|
ast: Node<'_>,
|
|
file_bytes: &[u8],
|
|
) -> Option<FrameworkBinding> {
|
|
detect_nest(summary, ast, file_bytes, TS_ADAPTER_NAME)
|
|
}
|
|
|
|
fn detect_with_context(
|
|
&self,
|
|
summary: &FuncSummary,
|
|
_ssa_summary: Option<&SsaFuncSummary>,
|
|
ast: Node<'_>,
|
|
file_bytes: &[u8],
|
|
) -> Option<FrameworkBinding> {
|
|
detect_nest(summary, ast, file_bytes, TS_ADAPTER_NAME)
|
|
}
|
|
}
|
|
|
|
fn detect_nest(
|
|
summary: &FuncSummary,
|
|
ast: Node<'_>,
|
|
file_bytes: &[u8],
|
|
adapter_name: &'static str,
|
|
) -> Option<FrameworkBinding> {
|
|
if !source_imports_nest(file_bytes) || !source_imports_nest_common(file_bytes) {
|
|
return None;
|
|
}
|
|
let (class_node, method_node) = find_class_method(ast, file_bytes, &summary.name)?;
|
|
let prefix = class_controller_prefix(class_node, file_bytes)?;
|
|
let (method, sub_path) = method_verb_and_path(method_node, file_bytes)?;
|
|
let full_path = join_paths(&prefix, &sub_path);
|
|
let formals = method_node
|
|
.child_by_field_name("parameters")
|
|
.map(|p| function_formal_names(p, file_bytes))
|
|
.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,
|
|
route: Some(RouteShape::single(method, full_path)),
|
|
request_params,
|
|
response_writer: None,
|
|
middleware,
|
|
})
|
|
}
|
|
|
|
/// Find `(class_declaration, method_definition)` where the method's
|
|
/// `name` field equals `target` and the enclosing class is decorated
|
|
/// with `@Controller(...)`. Returns the first match in document
|
|
/// order.
|
|
fn find_class_method<'a>(
|
|
root: Node<'a>,
|
|
bytes: &[u8],
|
|
target: &str,
|
|
) -> Option<(Node<'a>, Node<'a>)> {
|
|
let mut hit: Option<(Node<'a>, Node<'a>)> = None;
|
|
walk_for_class_method(root, bytes, target, &mut hit);
|
|
hit
|
|
}
|
|
|
|
fn walk_for_class_method<'a>(
|
|
node: Node<'a>,
|
|
bytes: &[u8],
|
|
target: &str,
|
|
out: &mut Option<(Node<'a>, Node<'a>)>,
|
|
) {
|
|
if out.is_some() {
|
|
return;
|
|
}
|
|
if node.kind() == "class_declaration"
|
|
&& class_has_controller(node, bytes)
|
|
&& let Some(body) = node.child_by_field_name("body")
|
|
{
|
|
let mut cur = body.walk();
|
|
for child in body.named_children(&mut cur) {
|
|
if child.kind() == "method_definition"
|
|
&& let Some(name) = child
|
|
.child_by_field_name("name")
|
|
.and_then(|n| n.utf8_text(bytes).ok())
|
|
&& name == target
|
|
{
|
|
*out = Some((node, child));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
let mut cur = node.walk();
|
|
for child in node.children(&mut cur) {
|
|
walk_for_class_method(child, bytes, target, out);
|
|
}
|
|
}
|
|
|
|
/// True when `class_node` is preceded by (or contains, depending on
|
|
/// grammar version) an `@Controller(...)` decorator. The walk
|
|
/// inspects both the class's own `decorator` field children
|
|
/// (tree-sitter-typescript) and its preceding siblings in the parent
|
|
/// (tree-sitter-javascript with legacy decorator transform), so the
|
|
/// adapter fires regardless of the grammar's wrapping.
|
|
fn class_has_controller(class_node: Node<'_>, bytes: &[u8]) -> bool {
|
|
if decorator_named(class_node, bytes, "Controller", &mut |_| {}) {
|
|
return true;
|
|
}
|
|
let mut prev = class_node.prev_named_sibling();
|
|
while let Some(sib) = prev {
|
|
if sib.kind() == "decorator" {
|
|
if decorator_text_is(sib, bytes, "Controller") {
|
|
return true;
|
|
}
|
|
prev = sib.prev_named_sibling();
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Extract the controller-prefix string from a class's
|
|
/// `@Controller(<prefix>)` decorator. Returns `Some("")` when the
|
|
/// decorator carries no argument (`@Controller()` is valid Nest — it
|
|
/// mounts the controller at root).
|
|
fn class_controller_prefix(class_node: Node<'_>, bytes: &[u8]) -> Option<String> {
|
|
let mut found: Option<String> = None;
|
|
let mut catcher = |text: Option<&str>| {
|
|
if let Some(t) = text {
|
|
found = Some(t.to_owned());
|
|
} else if found.is_none() {
|
|
found = Some(String::new());
|
|
}
|
|
};
|
|
if decorator_named(class_node, bytes, "Controller", &mut catcher) {
|
|
return found;
|
|
}
|
|
let mut prev = class_node.prev_named_sibling();
|
|
while let Some(sib) = prev {
|
|
if sib.kind() == "decorator" {
|
|
if decorator_text_is(sib, bytes, "Controller") {
|
|
let arg = decorator_first_string_arg(sib, bytes);
|
|
return Some(arg.unwrap_or_default());
|
|
}
|
|
prev = sib.prev_named_sibling();
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Return `Some((verb, sub_path))` when `method_node` is decorated
|
|
/// with one of the Nest verb decorators (`@Get`, `@Post`, ...). The
|
|
/// `sub_path` is `""` when the decorator carries no argument
|
|
/// (`@Get()` mounts at the controller prefix root).
|
|
fn method_verb_and_path(method_node: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, String)> {
|
|
const VERBS: &[&str] = &[
|
|
"Get", "Head", "Post", "Put", "Patch", "Delete", "Options", "All",
|
|
];
|
|
for &verb in VERBS {
|
|
if decorator_named(method_node, bytes, verb, &mut |_| {})
|
|
&& let Some(method) = http_verb_from_method(verb)
|
|
{
|
|
let path = method_decorator_path(method_node, bytes, verb);
|
|
return Some((method, path));
|
|
}
|
|
}
|
|
// Phase 13 v1: also accept preceding-sibling decorators for
|
|
// grammar variants that hoist method decorators out of the
|
|
// method_definition node.
|
|
let mut prev = method_node.prev_named_sibling();
|
|
while let Some(sib) = prev {
|
|
if sib.kind() == "decorator" {
|
|
for &verb in VERBS {
|
|
if decorator_text_is(sib, bytes, verb)
|
|
&& let Some(method) = http_verb_from_method(verb)
|
|
{
|
|
let path = decorator_first_string_arg(sib, bytes).unwrap_or_default();
|
|
return Some((method, path));
|
|
}
|
|
}
|
|
prev = sib.prev_named_sibling();
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
None
|
|
}
|
|
|
|
fn method_decorator_path(method_node: Node<'_>, bytes: &[u8], verb: &str) -> String {
|
|
let mut cur = method_node.walk();
|
|
for d in method_node.children_by_field_name("decorator", &mut cur) {
|
|
if decorator_text_is(d, bytes, verb) {
|
|
return decorator_first_string_arg(d, bytes).unwrap_or_default();
|
|
}
|
|
}
|
|
String::new()
|
|
}
|
|
|
|
/// Walk `node`'s `decorator` field children invoking `callback` for
|
|
/// each decorator named `name`. Returns `true` when at least one
|
|
/// matching decorator was found. `callback` receives the first
|
|
/// string argument (or `None` when the decorator carries no
|
|
/// arguments).
|
|
fn decorator_named(
|
|
node: Node<'_>,
|
|
bytes: &[u8],
|
|
name: &str,
|
|
callback: &mut dyn FnMut(Option<&str>),
|
|
) -> bool {
|
|
let mut found = false;
|
|
let mut cur = node.walk();
|
|
for d in node.children_by_field_name("decorator", &mut cur) {
|
|
if decorator_text_is(d, bytes, name) {
|
|
found = true;
|
|
let arg = decorator_first_string_arg(d, bytes);
|
|
callback(arg.as_deref());
|
|
}
|
|
}
|
|
found
|
|
}
|
|
|
|
fn decorator_text_is(decorator: Node<'_>, bytes: &[u8], name: &str) -> bool {
|
|
let mut cur = decorator.walk();
|
|
for c in decorator.children(&mut cur) {
|
|
if c.kind() == "@" {
|
|
continue;
|
|
}
|
|
let text = c.utf8_text(bytes).unwrap_or("");
|
|
// Strip optional `(args)` so `@Get(':id')` matches the name `Get`.
|
|
let head = text.split('(').next().unwrap_or(text).trim();
|
|
if head == name {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn decorator_first_string_arg(decorator: Node<'_>, bytes: &[u8]) -> Option<String> {
|
|
let mut cur = decorator.walk();
|
|
for c in decorator.children(&mut cur) {
|
|
if c.kind() == "call_expression"
|
|
&& let Some(args) = c.child_by_field_name("arguments")
|
|
{
|
|
let mut ac = args.walk();
|
|
for a in args.named_children(&mut ac) {
|
|
if a.kind() == "string" || a.kind() == "template_string" {
|
|
let raw = a.utf8_text(bytes).ok()?;
|
|
return Some(strip_quotes(raw).to_owned());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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
|
|
/// [`ParamSource::JsonBody`]; a `@Param('x')` formal becomes
|
|
/// [`ParamSource::PathSegment`]; `@Query('q')` keeps
|
|
/// [`ParamSource::QueryParam`]; `@Req()` / `@Res()` becomes
|
|
/// [`ParamSource::Implicit`].
|
|
fn refine_with_param_decorators(
|
|
method_node: Node<'_>,
|
|
bytes: &[u8],
|
|
bindings: &mut [ParamBinding],
|
|
full_path: &str,
|
|
) {
|
|
let Some(params) = method_node.child_by_field_name("parameters") else {
|
|
return;
|
|
};
|
|
let mut cur = params.walk();
|
|
let placeholders = extract_path_placeholders(full_path);
|
|
let formal_param_nodes: Vec<Node<'_>> = params.named_children(&mut cur).collect();
|
|
for (idx, formal) in formal_param_nodes.iter().enumerate() {
|
|
if let Some(refinement) = classify_param_decorator(*formal, bytes, &placeholders)
|
|
&& let Some(slot) = bindings.get_mut(idx)
|
|
{
|
|
slot.source = refinement;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn classify_param_decorator(
|
|
formal: Node<'_>,
|
|
bytes: &[u8],
|
|
placeholders: &[String],
|
|
) -> Option<ParamSource> {
|
|
let mut cur = formal.walk();
|
|
for d in formal.children_by_field_name("decorator", &mut cur) {
|
|
if let Some(refinement) = decorator_to_param_source(d, bytes, placeholders) {
|
|
return Some(refinement);
|
|
}
|
|
}
|
|
// Some grammar variants attach the decorator as a preceding
|
|
// sibling inside the parameter list.
|
|
let mut prev = formal.prev_named_sibling();
|
|
while let Some(sib) = prev {
|
|
if sib.kind() == "decorator" {
|
|
if let Some(r) = decorator_to_param_source(sib, bytes, placeholders) {
|
|
return Some(r);
|
|
}
|
|
prev = sib.prev_named_sibling();
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
None
|
|
}
|
|
|
|
fn decorator_to_param_source(
|
|
decorator: Node<'_>,
|
|
bytes: &[u8],
|
|
placeholders: &[String],
|
|
) -> Option<ParamSource> {
|
|
let arg = decorator_first_string_arg(decorator, bytes);
|
|
if decorator_text_is(decorator, bytes, "Body") {
|
|
return Some(ParamSource::JsonBody);
|
|
}
|
|
if decorator_text_is(decorator, bytes, "Param") {
|
|
let name = arg.unwrap_or_else(|| {
|
|
placeholders
|
|
.first()
|
|
.cloned()
|
|
.unwrap_or_else(|| "id".to_owned())
|
|
});
|
|
return Some(ParamSource::PathSegment(name));
|
|
}
|
|
if decorator_text_is(decorator, bytes, "Query") {
|
|
let name = arg.unwrap_or_else(|| "q".to_owned());
|
|
return Some(ParamSource::QueryParam(name));
|
|
}
|
|
if decorator_text_is(decorator, bytes, "Headers") {
|
|
let name = arg.unwrap_or_else(|| "x-nyx".to_owned());
|
|
return Some(ParamSource::Header(name));
|
|
}
|
|
if decorator_text_is(decorator, bytes, "Req")
|
|
|| decorator_text_is(decorator, bytes, "Res")
|
|
|| decorator_text_is(decorator, bytes, "Request")
|
|
|| decorator_text_is(decorator, bytes, "Response")
|
|
|| decorator_text_is(decorator, bytes, "Next")
|
|
{
|
|
return Some(ParamSource::Implicit);
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Join a controller prefix and method path segment per Nest's own
|
|
/// path normalisation: collapse any double-slash run to a single
|
|
/// slash, ensure the result starts with `/`, and trim a trailing
|
|
/// slash unless the path is `/` itself.
|
|
fn join_paths(prefix: &str, sub_path: &str) -> String {
|
|
let mut combined = String::with_capacity(prefix.len() + sub_path.len() + 2);
|
|
if !prefix.starts_with('/') {
|
|
combined.push('/');
|
|
}
|
|
combined.push_str(prefix);
|
|
if !prefix.ends_with('/') && !sub_path.is_empty() && !sub_path.starts_with('/') {
|
|
combined.push('/');
|
|
}
|
|
combined.push_str(sub_path);
|
|
let collapsed = collapse_slashes(&combined);
|
|
if collapsed.is_empty() {
|
|
return "/".to_owned();
|
|
}
|
|
collapsed
|
|
}
|
|
|
|
fn collapse_slashes(s: &str) -> String {
|
|
let mut out = String::with_capacity(s.len());
|
|
let mut last_was_slash = false;
|
|
for c in s.chars() {
|
|
if c == '/' {
|
|
if !last_was_slash {
|
|
out.push('/');
|
|
}
|
|
last_was_slash = true;
|
|
} else {
|
|
out.push(c);
|
|
last_was_slash = false;
|
|
}
|
|
}
|
|
if out.len() > 1 {
|
|
while out.ends_with('/') {
|
|
out.pop();
|
|
}
|
|
}
|
|
if out.is_empty() {
|
|
return "/".to_owned();
|
|
}
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn parse_ts(src: &[u8]) -> tree_sitter::Tree {
|
|
let mut parser = tree_sitter::Parser::new();
|
|
let lang = tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT);
|
|
parser.set_language(&lang).unwrap();
|
|
parser.parse(src, None).unwrap()
|
|
}
|
|
|
|
fn summary(name: &str, lang: &str) -> FuncSummary {
|
|
FuncSummary {
|
|
name: name.into(),
|
|
lang: lang.into(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn collapse_slashes_normalises_join() {
|
|
assert_eq!(join_paths("users", "id"), "/users/id");
|
|
assert_eq!(join_paths("/users/", "/:id"), "/users/:id");
|
|
assert_eq!(join_paths("", ""), "/");
|
|
assert_eq!(join_paths("/", "/"), "/");
|
|
}
|
|
|
|
#[test]
|
|
fn fires_on_controller_get_decorator() {
|
|
let src: &[u8] = b"import { Controller, Get, Param } from '@nestjs/common';\n\
|
|
@Controller('users')\n\
|
|
export class UsersController {\n\
|
|
@Get(':id')\n\
|
|
getUser(@Param('id') id: string) { return id; }\n\
|
|
}\n";
|
|
let tree = parse_ts(src);
|
|
let binding = TsNestAdapter
|
|
.detect(&summary("getUser", "typescript"), tree.root_node(), src)
|
|
.expect("binding");
|
|
assert_eq!(binding.adapter, "ts-nest");
|
|
let route = binding.route.as_ref().unwrap();
|
|
assert_eq!(route.method, HttpMethod::GET);
|
|
assert_eq!(route.path, "/users/:id");
|
|
let id_binding = binding
|
|
.request_params
|
|
.iter()
|
|
.find(|p| p.name == "id")
|
|
.unwrap();
|
|
assert!(matches!(id_binding.source, ParamSource::PathSegment(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn fires_on_post_with_body_decorator() {
|
|
let src: &[u8] = b"import { Controller, Post, Body } from '@nestjs/common';\n\
|
|
@Controller('items')\n\
|
|
export class ItemsController {\n\
|
|
@Post()\n\
|
|
create(@Body() payload: any) { return payload; }\n\
|
|
}\n";
|
|
let tree = parse_ts(src);
|
|
let binding = TsNestAdapter
|
|
.detect(&summary("create", "typescript"), tree.root_node(), src)
|
|
.expect("binding");
|
|
let route = binding.route.unwrap();
|
|
assert_eq!(route.method, HttpMethod::POST);
|
|
assert_eq!(route.path, "/items");
|
|
let body_binding = binding
|
|
.request_params
|
|
.iter()
|
|
.find(|p| p.name == "payload")
|
|
.unwrap();
|
|
assert!(matches!(body_binding.source, ParamSource::JsonBody));
|
|
}
|
|
|
|
#[test]
|
|
fn fires_on_query_decorator() {
|
|
let src: &[u8] = b"import { Controller, Get, Query } from '@nestjs/common';\n\
|
|
@Controller()\n\
|
|
export class SearchController {\n\
|
|
@Get('search')\n\
|
|
search(@Query('q') q: string) { return q; }\n\
|
|
}\n";
|
|
let tree = parse_ts(src);
|
|
let binding = TsNestAdapter
|
|
.detect(&summary("search", "typescript"), tree.root_node(), src)
|
|
.expect("binding");
|
|
assert_eq!(binding.route.unwrap().path, "/search");
|
|
let q_binding = binding
|
|
.request_params
|
|
.iter()
|
|
.find(|p| p.name == "q")
|
|
.unwrap();
|
|
match &q_binding.source {
|
|
ParamSource::QueryParam(name) => assert_eq!(name, "q"),
|
|
other => panic!("expected QueryParam, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[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\
|
|
@Injectable()\n\
|
|
export class HelperService {\n\
|
|
compute(x: number) { return x + 1; }\n\
|
|
}\n";
|
|
let tree = parse_ts(src);
|
|
assert!(
|
|
TsNestAdapter
|
|
.detect(&summary("compute", "typescript"), tree.root_node(), src)
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn skips_unrelated_controller_decorator_without_nest_import() {
|
|
let src: &[u8] = b"function Controller(path: string) { return function(_: any) {}; }\n\
|
|
function Get(path: string) { return function(_: any, __: string) {}; }\n\
|
|
@Controller('users')\n\
|
|
export class UsersController {\n\
|
|
@Get(':id')\n\
|
|
getUser(id: string) { return id; }\n\
|
|
}\n";
|
|
let tree = parse_ts(src);
|
|
assert!(
|
|
TsNestAdapter
|
|
.detect(&summary("getUser", "typescript"), tree.root_node(), src)
|
|
.is_none()
|
|
);
|
|
}
|
|
}
|