From 3dc4f9a9bd4dbb50b51281819c4124d6f9547755 Mon Sep 17 00:00:00 2001 From: wildduck <108896341+wildduck2@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:02:30 +0200 Subject: [PATCH 1/3] fix: defensive symbol/type handling in MethodTransformer Fixes #1382. The get_name() and escape_promise() helpers used non-null assertions on values that the TypeScript API documents as potentially undefined. When a method return type resolves to a symbol without declarations (e.g. via import() type expressions on older TS versions), these helpers crashed with: TypeError: Cannot read properties of undefined (reading '0') or threw: Error on ImportAnalyzer.analyze(): invalid promise type. Replace the unsafe assertions with optional chaining and safe fallbacks, matching the pattern already used in ParameterDecoratorTransformer.ts:132-134 and DtoAnalyzer.ts:248-251 within the same codebase. --- .../src/transformers/MethodTransformer.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/core/src/transformers/MethodTransformer.ts b/packages/core/src/transformers/MethodTransformer.ts index 8f8c363b5f..7affff5a86 100644 --- a/packages/core/src/transformers/MethodTransformer.ts +++ b/packages/core/src/transformers/MethodTransformer.ts @@ -78,20 +78,22 @@ const get_escaped_type = const escape_promise = (checker: ts.TypeChecker) => (type: ts.Type): ts.Type => { - const generic: readonly ts.Type[] = checker.getTypeArguments( - type as ts.TypeReference, - ); - if (generic.length !== 1) - throw new Error( - "Error on ImportAnalyzer.analyze(): invalid promise type.", + try { + const generic: readonly ts.Type[] = checker.getTypeArguments( + type as ts.TypeReference, ); - return generic[0]!; + if (!generic?.length) return type; + return generic[0] ?? type; + } catch { + return type; + } }; -const get_name = (symbol: ts.Symbol): string => - explore_name(symbol.getDeclarations()![0]!.parent)( - symbol.escapedName.toString(), - ); +const get_name = (symbol: ts.Symbol): string => { + const parent: ts.Node | undefined = symbol.getDeclarations()?.[0]?.parent; + const name: string = symbol.escapedName?.toString() ?? ""; + return parent ? explore_name(parent)(name) : name; +}; const explore_name = (decl: ts.Node) => From d8d034e9fbdcc207672042c693408c68d9f3827e Mon Sep 17 00:00:00 2001 From: wildduck <108896341+wildduck2@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:54:44 +0200 Subject: [PATCH 2/3] test: add method-transformer-defensive demo project for #1382 Adds a new test feature that exercises the symbol and type paths previously guarded by non-null assertions in MethodTransformer. Covers: - inline/anonymous object return types wrapped in Promise - intersection of named interfaces - ReturnType utility types - infer keyword in conditional types - generic envelope types - type aliases over interfaces - import() type expressions in TypedRoute generic arguments Follows the structure of existing clone-based features under tests/test-sdk/features/ and is gitignored for generated output. --- tests/test-sdk/.gitignore | 1 + .../nestia.config.ts | 11 +++++ .../src/controllers/ImportTypeController.ts | 35 ++++++++++++++++ .../src/controllers/InlineReturnController.ts | 42 +++++++++++++++++++ .../tsconfig.json | 10 +++++ 5 files changed, 99 insertions(+) create mode 100644 tests/test-sdk/features/method-transformer-defensive/nestia.config.ts create mode 100644 tests/test-sdk/features/method-transformer-defensive/src/controllers/ImportTypeController.ts create mode 100644 tests/test-sdk/features/method-transformer-defensive/src/controllers/InlineReturnController.ts create mode 100644 tests/test-sdk/features/method-transformer-defensive/tsconfig.json diff --git a/tests/test-sdk/.gitignore b/tests/test-sdk/.gitignore index a562634892..bd7b71cec5 100644 --- a/tests/test-sdk/.gitignore +++ b/tests/test-sdk/.gitignore @@ -6,6 +6,7 @@ features/*/src/test/features/api/automated features/clone*/src/api/structures features/clone-type-create-duplicate/sdk +features/method-transformer-defensive/src/api/structures features/configurations/src/api/bbs features/configurations/src/api/common features/configurations/*.swagger.json \ No newline at end of file diff --git a/tests/test-sdk/features/method-transformer-defensive/nestia.config.ts b/tests/test-sdk/features/method-transformer-defensive/nestia.config.ts new file mode 100644 index 0000000000..f75934d1f7 --- /dev/null +++ b/tests/test-sdk/features/method-transformer-defensive/nestia.config.ts @@ -0,0 +1,11 @@ +import { INestiaConfig } from "@nestia/sdk"; + +export const NESTIA_CONFIG: INestiaConfig = { + input: ["src/controllers"], + output: "src/api", + clone: true, + swagger: { + output: "swagger.json", + }, +}; +export default NESTIA_CONFIG; diff --git a/tests/test-sdk/features/method-transformer-defensive/src/controllers/ImportTypeController.ts b/tests/test-sdk/features/method-transformer-defensive/src/controllers/ImportTypeController.ts new file mode 100644 index 0000000000..3ebf7739f1 --- /dev/null +++ b/tests/test-sdk/features/method-transformer-defensive/src/controllers/ImportTypeController.ts @@ -0,0 +1,35 @@ +import { TypedRoute } from "@nestia/core"; +import { Controller } from "@nestjs/common"; + +@Controller("import-type") +export class ImportTypeController { + @TypedRoute.Get("envelope") + public envelope(): IEnvelope { + return { message: "ok", data: { greeting: "hello" } }; + } + + @TypedRoute.Get("alias") + public alias(): AliasEnvelope { + return { message: "ok", data: { greeting: "hello" } }; + } + + @TypedRoute.Get< + import("./ImportTypeController").IEnvelope< + import("./ImportTypeController").IGreeting + > + >("decorator-generic") + public decoratorGeneric(): AliasEnvelope { + return { message: "ok", data: { greeting: "hello" } }; + } +} + +export interface IEnvelope { + message: string; + data: T | null; +} + +export interface IGreeting { + greeting: string; +} + +type AliasEnvelope = IEnvelope; diff --git a/tests/test-sdk/features/method-transformer-defensive/src/controllers/InlineReturnController.ts b/tests/test-sdk/features/method-transformer-defensive/src/controllers/InlineReturnController.ts new file mode 100644 index 0000000000..118c1a7caf --- /dev/null +++ b/tests/test-sdk/features/method-transformer-defensive/src/controllers/InlineReturnController.ts @@ -0,0 +1,42 @@ +import { TypedRoute } from "@nestia/core"; +import { Controller } from "@nestjs/common"; + +@Controller("inline-return") +export class InlineReturnController { + @TypedRoute.Get("anonymous") + public anonymous(): Promise<{ foo: string; bar?: number }> { + return Promise.resolve({ foo: "ok" }); + } + + @TypedRoute.Get("intersection") + public intersection(): Promise { + return Promise.resolve({ x: 1, y: "hi" }); + } + + @TypedRoute.Get("return-type") + public returnType(): Promise> { + return Promise.resolve(getItem()); + } + + @TypedRoute.Get("inferred") + public inferred(): Promise IItem>> { + return Promise.resolve({ id: 1, label: "item" }); + } +} + +interface IX { + x: number; +} +interface IY { + y: string; +} +interface IItem { + id: number; + label: string; +} + +type InferReturn = T extends (...args: any[]) => infer R ? R : never; + +function getItem(): IItem { + return { id: 1, label: "item" }; +} diff --git a/tests/test-sdk/features/method-transformer-defensive/tsconfig.json b/tests/test-sdk/features/method-transformer-defensive/tsconfig.json new file mode 100644 index 0000000000..ccd8aec38c --- /dev/null +++ b/tests/test-sdk/features/method-transformer-defensive/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../config/tsconfig.json", + "compilerOptions": { + "paths": { + "@api": ["./src/api"], + "@api/lib/*": ["./src/api/*"], + }, + }, + "include": ["src"], +} From a1e8b6cdd328cb3d5a88012cc2f81d5ce85909e8 Mon Sep 17 00:00:00 2001 From: wildduck <108896341+wildduck2@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:19:53 +0200 Subject: [PATCH 3/3] Update ignores and fix inline return typo fixture --- .gitignore | 4 +++- .../src/controllers/InlineReturnController.ts | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 58a63eb872..3d5bfe3e55 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ packages/*/*.tgz packages/*/docs package-lock.json *.log -.env \ No newline at end of file +.env +# bun.lock +playground/ diff --git a/tests/test-sdk/features/method-transformer-defensive/src/controllers/InlineReturnController.ts b/tests/test-sdk/features/method-transformer-defensive/src/controllers/InlineReturnController.ts index 118c1a7caf..94256a1159 100644 --- a/tests/test-sdk/features/method-transformer-defensive/src/controllers/InlineReturnController.ts +++ b/tests/test-sdk/features/method-transformer-defensive/src/controllers/InlineReturnController.ts @@ -9,7 +9,7 @@ export class InlineReturnController { } @TypedRoute.Get("intersection") - public intersection(): Promise { + public intersection(): Promise { return Promise.resolve({ x: 1, y: "hi" }); } @@ -24,10 +24,10 @@ export class InlineReturnController { } } -interface IX { +interface IWithX { x: number; } -interface IY { +interface IWithY { y: string; } interface IItem {