-
Notifications
You must be signed in to change notification settings - Fork 214
feat: add ReturnType parser #2317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from 5 commits
1bc0449
f379eac
af0482e
1008735
7c06489
e188df5
a266867
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
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"; | ||
import { ObjectType } from "../Type/ObjectType.js"; | ||
Check failure on line 6 in src/NodeParser/ReturnTypeNodeParser.ts
|
||
|
||
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; | ||
} | ||
|
||
// Check if it's a ReturnType reference | ||
try { | ||
const typeName = ts.isIdentifier(node.typeName) ? node.typeName.text : node.typeName.getText(); | ||
return typeName === "ReturnType" && node.typeArguments?.length === 1; | ||
} catch { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need a try catch? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the try/catch. |
||
return false; | ||
} | ||
} | ||
|
||
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<typeof functionName> | ||
// 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<SomeType["methodName"]> 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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof createTestStore>; | ||
export type TestAppState = ReturnType<TestAppStore["getState"]>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof implicitReturn>; | ||
arrow: ReturnType<typeof arrowImplicitReturn>; | ||
functionExpr: ReturnType<typeof functionExprImplicitReturn>; | ||
complex: ReturnType<typeof complexNestedReturn>; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export function implicitReturn() { | ||
return { message: "Hello", count: 42 }; | ||
} | ||
|
||
export type ImplicitReturnType = ReturnType<typeof implicitReturn>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove unnecessary comments that just explain what happens
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed