diff --git a/factory/parser.ts b/factory/parser.ts index 6bc089000..7bdc5e09d 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -61,6 +61,7 @@ import { SatisfiesNodeParser } from "../src/NodeParser/SatisfiesNodeParser.js"; import { PromiseNodeParser } from "../src/NodeParser/PromiseNodeParser.js"; import { SpreadElementNodeParser } from "../src/NodeParser/SpreadElementNodeParser.js"; import { IdentifierNodeParser } from "../src/NodeParser/IdentifierNodeParser.js"; +import { ReturnTypeNodeParser } from "../src/NodeParser/ReturnTypeNodeParser.js"; export type ParserAugmentor = (parser: MutableParser) => void; @@ -130,6 +131,7 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme .addNodeParser(new ParenthesizedNodeParser(chainNodeParser)) .addNodeParser(new PromiseNodeParser(typeChecker, chainNodeParser)) + .addNodeParser(new ReturnTypeNodeParser(chainNodeParser, typeChecker)) .addNodeParser(new TypeReferenceNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new ExpressionWithTypeArgumentsNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new IndexedAccessTypeNodeParser(typeChecker, chainNodeParser)) diff --git a/src/NodeParser/ReturnTypeNodeParser.ts b/src/NodeParser/ReturnTypeNodeParser.ts new file mode 100644 index 000000000..ad5e18579 --- /dev/null +++ b/src/NodeParser/ReturnTypeNodeParser.ts @@ -0,0 +1,116 @@ +import ts from "typescript"; +import type { Context, NodeParser } from "../NodeParser.js"; +import type { SubNodeParser } from "../SubNodeParser.js"; +import type { BaseType } from "../Type/BaseType.js"; +import { UnknownNodeError } from "../Error/Errors.js"; + +export class ReturnTypeNodeParser implements SubNodeParser { + constructor( + private readonly childNodeParser: NodeParser, + private readonly checker: ts.TypeChecker, + ) {} + + supportsNode(node: ts.Node): boolean { + if (!ts.isTypeReferenceNode(node)) { + return false; + } + + const typeName = ts.isIdentifier(node.typeName) ? node.typeName.text : node.typeName.getText(); + return typeName === "ReturnType" && node.typeArguments?.length === 1; + } + + createType(node: ts.TypeReferenceNode, context: Context): BaseType { + if (!node.typeArguments || node.typeArguments.length !== 1) { + throw new UnknownNodeError(node); + } + + const typeArg = node.typeArguments[0]; + + // Handle different types of type arguments + if (ts.isTypeQueryNode(typeArg)) { + // Case: ReturnType + // Get the symbol for the identifier + const symbol = this.checker.getSymbolAtLocation(typeArg.exprName); + if (!symbol) { + throw new UnknownNodeError(node); + } + + // Get the declarations of the symbol + const declarations = symbol.getDeclarations() || []; + + // Try multiple methods to extract return type + for (const decl of declarations) { + let returnTypeNode: ts.TypeNode | undefined; + + // If declaration is a function/method with explicit return type + if ( + (ts.isFunctionDeclaration(decl) || + ts.isMethodDeclaration(decl) || + ts.isArrowFunction(decl) || + ts.isFunctionExpression(decl)) && + decl.type + ) { + returnTypeNode = decl.type; + } + // If declaration is a variable with function type annotation + else if (ts.isVariableDeclaration(decl) && decl.type && ts.isFunctionTypeNode(decl.type)) { + returnTypeNode = decl.type.type; + } + + // If we found a return type node, process it + if (returnTypeNode) { + const baseType = this.childNodeParser.createType(returnTypeNode, context); + return baseType; + } + } + + // Fallback to type checking method + const type = this.checker.getTypeOfSymbolAtLocation(symbol, typeArg); + const result = extractReturnTypeFromSignatures(type, this.checker, this.childNodeParser, context); + if (result) { + return result; + } + } else { + // Case: ReturnType or other complex types + // Get the type directly from TypeScript's type system + const argType = this.checker.getTypeAtLocation(typeArg); + + // If it's a function type, get its return type + const result = extractReturnTypeFromSignatures(argType, this.checker, this.childNodeParser, context); + if (result) { + return result; + } + + // Final fallback: try to get type directly + const type = this.checker.getTypeAtLocation(typeArg); + const typeNode = this.checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation); + + if (typeNode) { + return this.childNodeParser.createType(typeNode, context); + } + } + + throw new UnknownNodeError(node); + } +} + +/** + * Helper function to extract return type from call signatures + */ +function extractReturnTypeFromSignatures( + type: ts.Type, + checker: ts.TypeChecker, + childNodeParser: NodeParser, + context: Context, +): BaseType | null { + const signatures = type.getCallSignatures(); + if (signatures.length > 0) { + const returnType = signatures[0].getReturnType(); + const returnTypeNode = checker.typeToTypeNode(returnType, undefined, ts.NodeBuilderFlags.NoTruncation); + + if (returnTypeNode) { + return childNodeParser.createType(returnTypeNode, context); + } + } + return null; +} diff --git a/test/valid-data-type.test.ts b/test/valid-data-type.test.ts index 519b48864..54ff588c0 100644 --- a/test/valid-data-type.test.ts +++ b/test/valid-data-type.test.ts @@ -156,4 +156,9 @@ describe("valid-data-type", () => { "export-star-prune-unreachable", assertValidSchema("export-star-prune-unreachable", "*", undefined, { mainTsOnly: true }), ); + it("type-return-type", assertValidSchema("type-return-type", "Greeting")); + it("type-return-type-complex", assertValidSchema("type-return-type-complex", "TestAppState")); + it("type-return-type-implicit", assertValidSchema("type-return-type-implicit", "ImplicitReturnType")); + it("type-return-type-function-parser", assertValidSchema("type-return-type-function", "FunctionReturnTypes")); + it("type-return-type-method", assertValidSchema("type-return-type-method", "MethodReturnTypes")); }); diff --git a/test/valid-data/type-return-type-complex/main.ts b/test/valid-data/type-return-type-complex/main.ts new file mode 100644 index 000000000..976f1099c --- /dev/null +++ b/test/valid-data/type-return-type-complex/main.ts @@ -0,0 +1,15 @@ +// Simulated Redux Toolkit scenario +export interface TestState { + counter: number; + name: string; +} + +export function createTestStore() { + return { + getState: () => ({ counter: 0, name: "test" }) as TestState, + dispatch: (action: any) => {}, + }; +} + +export type TestAppStore = ReturnType; +export type TestAppState = ReturnType; diff --git a/test/valid-data/type-return-type-complex/schema.json b/test/valid-data/type-return-type-complex/schema.json new file mode 100644 index 000000000..629ac0739 --- /dev/null +++ b/test/valid-data/type-return-type-complex/schema.json @@ -0,0 +1,25 @@ +{ + "$ref": "#/definitions/TestAppState", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "TestAppState": { + "$ref": "#/definitions/TestState" + }, + "TestState": { + "additionalProperties": false, + "properties": { + "counter": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "required": [ + "counter", + "name" + ], + "type": "object" + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-return-type-function/main.ts b/test/valid-data/type-return-type-function/main.ts new file mode 100644 index 000000000..ed6d63677 --- /dev/null +++ b/test/valid-data/type-return-type-function/main.ts @@ -0,0 +1,44 @@ +// Test cases to demonstrate ReturnType parsing with various function types + +// Implicit return type +export function implicitReturn() { + return { message: "Hello", count: 42 }; +} + +// Arrow function with implicit return +export const arrowImplicitReturn = () => ({ + nested: { + value: "test", + count: 123, + }, +}); + +// Function expression with implicit return +export const functionExprImplicitReturn = function () { + return { + dynamic: true, + payload: { id: 456, name: "example" }, + }; +}; + +// Complex nested return type with explicit annotation +export function complexNestedReturn(): { + meta: { + version: number; + type: string; + }; + data: string[]; +} { + return { + meta: { version: 1, type: "test" }, + data: ["item1", "item2"], + }; +} + +// Combined type that tests all function return types +export type FunctionReturnTypes = { + implicit: ReturnType; + arrow: ReturnType; + functionExpr: ReturnType; + complex: ReturnType; +}; diff --git a/test/valid-data/type-return-type-function/schema.json b/test/valid-data/type-return-type-function/schema.json new file mode 100644 index 000000000..0d6e3c116 --- /dev/null +++ b/test/valid-data/type-return-type-function/schema.json @@ -0,0 +1,120 @@ +{ + "$ref": "#/definitions/FunctionReturnTypes", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "FunctionReturnTypes": { + "additionalProperties": false, + "properties": { + "arrow": { + "additionalProperties": false, + "properties": { + "nested": { + "additionalProperties": false, + "properties": { + "count": { + "type": "number" + }, + "value": { + "type": "string" + } + }, + "required": [ + "value", + "count" + ], + "type": "object" + } + }, + "required": [ + "nested" + ], + "type": "object" + }, + "complex": { + "additionalProperties": false, + "properties": { + "data": { + "items": { + "type": "string" + }, + "type": "array" + }, + "meta": { + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": [ + "version", + "type" + ], + "type": "object" + } + }, + "required": [ + "meta", + "data" + ], + "type": "object" + }, + "functionExpr": { + "additionalProperties": false, + "properties": { + "dynamic": { + "type": "boolean" + }, + "payload": { + "additionalProperties": false, + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + } + }, + "required": [ + "dynamic", + "payload" + ], + "type": "object" + }, + "implicit": { + "additionalProperties": false, + "properties": { + "count": { + "type": "number" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message", + "count" + ], + "type": "object" + } + }, + "required": [ + "implicit", + "arrow", + "functionExpr", + "complex" + ], + "type": "object" + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-return-type-implicit/main.ts b/test/valid-data/type-return-type-implicit/main.ts new file mode 100644 index 000000000..fa66b8f1d --- /dev/null +++ b/test/valid-data/type-return-type-implicit/main.ts @@ -0,0 +1,5 @@ +export function implicitReturn() { + return { message: "Hello", count: 42 }; +} + +export type ImplicitReturnType = ReturnType; diff --git a/test/valid-data/type-return-type-implicit/schema.json b/test/valid-data/type-return-type-implicit/schema.json new file mode 100644 index 000000000..79ce6eb27 --- /dev/null +++ b/test/valid-data/type-return-type-implicit/schema.json @@ -0,0 +1,23 @@ +{ + "$ref": "#/definitions/ImplicitReturnType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ImplicitReturnType": { + "additionalProperties": false, + "properties": { + "count": { + "type": "number" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message", + "count" + ], + "type": "object" + } + } +} + diff --git a/test/valid-data/type-return-type-method/main.ts b/test/valid-data/type-return-type-method/main.ts new file mode 100644 index 000000000..a3cdf5250 --- /dev/null +++ b/test/valid-data/type-return-type-method/main.ts @@ -0,0 +1,33 @@ +export class MyClass { + // Method with explicit return type + getData(): { id: number; name: string } { + return { id: 1, name: "test" }; + } + + // Method with implicit return type + getStatus() { + return { active: true, count: 42 }; + } + + // Method with complex return type + getNestedData(): { + meta: { version: number; type: string }; + items: string[]; + } { + return { + meta: { version: 1, type: "test" }, + items: ["a", "b", "c"], + }; + } + + // Arrow method with implicit return + getSimple = () => ({ message: "hello" }); +} + +// Combined type that tests all method return types +export type MethodReturnTypes = { + explicit: ReturnType; + implicit: ReturnType; + complex: ReturnType; + arrow: ReturnType; +}; diff --git a/test/valid-data/type-return-type-method/schema.json b/test/valid-data/type-return-type-method/schema.json new file mode 100644 index 000000000..2522d72e3 --- /dev/null +++ b/test/valid-data/type-return-type-method/schema.json @@ -0,0 +1,94 @@ +{ + "$ref": "#/definitions/MethodReturnTypes", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MethodReturnTypes": { + "additionalProperties": false, + "properties": { + "arrow": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "complex": { + "additionalProperties": false, + "properties": { + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "meta": { + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": [ + "version", + "type" + ], + "type": "object" + } + }, + "required": [ + "meta", + "items" + ], + "type": "object" + }, + "explicit": { + "additionalProperties": false, + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "implicit": { + "additionalProperties": false, + "properties": { + "active": { + "type": "boolean" + }, + "count": { + "type": "number" + } + }, + "required": [ + "active", + "count" + ], + "type": "object" + } + }, + "required": [ + "explicit", + "implicit", + "complex", + "arrow" + ], + "type": "object" + } + } +} \ No newline at end of file diff --git a/test/valid-data/type-return-type/main.ts b/test/valid-data/type-return-type/main.ts new file mode 100644 index 000000000..97f13f196 --- /dev/null +++ b/test/valid-data/type-return-type/main.ts @@ -0,0 +1,5 @@ +export function greet(name: string): { message: string } { + return { message: `Hello, ${name}!` }; +} + +export type Greeting = ReturnType; diff --git a/test/valid-data/type-return-type/schema.json b/test/valid-data/type-return-type/schema.json new file mode 100644 index 000000000..1948bf15f --- /dev/null +++ b/test/valid-data/type-return-type/schema.json @@ -0,0 +1,18 @@ +{ + "$ref": "#/definitions/Greeting", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Greeting": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } +} \ No newline at end of file