diff --git a/factory/parser.ts b/factory/parser.ts index 6bc089000..ca8651fcd 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -114,7 +114,7 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme .addNodeParser(new ObjectTypeNodeParser()) .addNodeParser(new AsExpressionNodeParser(chainNodeParser)) .addNodeParser(new SatisfiesNodeParser(chainNodeParser)) - .addNodeParser(withJsDoc(new ParameterParser(chainNodeParser))) + .addNodeParser(withJsDoc(new ParameterParser(chainNodeParser, typeChecker))) .addNodeParser(new StringLiteralNodeParser()) .addNodeParser(new StringTemplateLiteralNodeParser(chainNodeParser)) .addNodeParser(new IntrinsicNodeParser()) diff --git a/src/NodeParser/FunctionNodeParser.ts b/src/NodeParser/FunctionNodeParser.ts index 7f5f06304..840e84165 100644 --- a/src/NodeParser/FunctionNodeParser.ts +++ b/src/NodeParser/FunctionNodeParser.ts @@ -64,12 +64,34 @@ export function getNamedArguments( // If it's missing a questionToken but has an initializer we can consider the property as not required const required = node.parameters[index].questionToken ? false : !node.parameters[index].initializer; - return new ObjectProperty(node.parameters[index].name.getText(), parameterType, required); + return new ObjectProperty(getParameterName(node.parameters[index].name, index), parameterType, required); }), false, ); } +function getParameterName(node: ts.BindingName, index: number) { + if (node.parent) { + return node.getText(); + } + + // for parameter type in inferred function type + if (ts.isIdentifier(node)) { + /** + * function foo(name: string) {} + * ^^^^ + */ + return node.escapedText as string; + } + + // otherwise, for BindingPattern + /** + * function foo([a, b, c]: number[]) {} + * ^^^^^^^^^ + */ + return `_${index}`; +} + export function getTypeName( node: | ts.FunctionTypeNode @@ -80,7 +102,8 @@ export function getTypeName( ): string | undefined { if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionTypeNode(node)) { const parent = node.parent; - if (ts.isVariableDeclaration(parent)) { + // there is no parent for inferred function type node. + if (parent && ts.isVariableDeclaration(parent)) { return parent.name.getText(); } } diff --git a/src/NodeParser/ParameterParser.ts b/src/NodeParser/ParameterParser.ts index edb05b192..954c68d39 100644 --- a/src/NodeParser/ParameterParser.ts +++ b/src/NodeParser/ParameterParser.ts @@ -3,14 +3,30 @@ import type { NodeParser } from "../NodeParser.js"; import type { Context } from "../NodeParser.js"; import type { SubNodeParser } from "../SubNodeParser.js"; import type { BaseType } from "../Type/BaseType.js"; +import { UnknownType } from "../Type/UnknownType.js"; export class ParameterParser implements SubNodeParser { - constructor(protected childNodeParser: NodeParser) {} + constructor( + protected childNodeParser: NodeParser, + protected checker: ts.TypeChecker, + ) {} - public supportsNode(node: ts.ParameterDeclaration): boolean { + public supportsNode(node: ts.Node): boolean { return node.kind === ts.SyntaxKind.Parameter; } - public createType(node: ts.FunctionTypeNode, context: Context): BaseType { - return this.childNodeParser.createType(node.type, context); + public createType(node: ts.ParameterDeclaration, context: Context): BaseType { + // If the parameter type is declared, use it directly to retain the location information. + if (node.type) { + return this.childNodeParser.createType(node.type, context); + } + + // otherwise, use inferred type + const paramType = this.checker.getTypeAtLocation(node.name); + const typeNode = this.checker.typeToTypeNode(paramType, node.parent, ts.NodeBuilderFlags.NoTruncation); + if (typeNode) { + return this.childNodeParser.createType(typeNode, context); + } + + return new UnknownType(true); } } diff --git a/src/Type/FunctionType.ts b/src/Type/FunctionType.ts index 485373f92..6339da23f 100644 --- a/src/Type/FunctionType.ts +++ b/src/Type/FunctionType.ts @@ -12,7 +12,11 @@ export class FunctionType extends BaseType { super(); if (node) { - this.comment = `(${node.parameters.map((p) => p.getFullText()).join(",")}) =>${node.type?.getFullText()}`; + if (node.parent) { + this.comment = `(${node.parameters.map((p) => p.getFullText()).join(",")}) =>${node.type?.getFullText()}`; + } else { + this.comment = "Function"; + } } } diff --git a/test/valid-data-other.test.ts b/test/valid-data-other.test.ts index 9d5cf7934..bee2a960e 100644 --- a/test/valid-data-other.test.ts +++ b/test/valid-data-other.test.ts @@ -13,6 +13,7 @@ describe("valid-data-other", () => { it("exported-enums-union", assertValidSchema("exported-enums-union", "MyObject")); it("function-parameters-default-value", assertValidSchema("function-parameters-default-value", "myFunction")); + it("function-parameters-infer-type", assertValidSchema("function-parameters-infer-type", "myFunction")); it("function-parameters-declaration", assertValidSchema("function-parameters-declaration", "myFunction")); it("function-parameters-jsdoc", assertValidSchema("function-parameters-jsdoc", "myFunction", { jsDoc: "basic" })); it("function-parameters-optional", assertValidSchema("function-parameters-optional", "myFunction")); diff --git a/test/valid-data/function-parameters-infer-type/main.ts b/test/valid-data/function-parameters-infer-type/main.ts new file mode 100644 index 000000000..7dfc34639 --- /dev/null +++ b/test/valid-data/function-parameters-infer-type/main.ts @@ -0,0 +1,16 @@ +import { myObj, MyClass, myArray, mFunction } from "./module"; + +export const myFunction = ( + str = "something", + num = 123, + bool = true, + [a, b, c] = [1, 2, 3], + obj = { a: 1, b: 2 }, + func = (a: number, b: number) => a + b, + object = myObj, + func1 = mFunction, + clas = new MyClass(), + arr = myArray, +) => { + return "whatever"; +}; diff --git a/test/valid-data/function-parameters-infer-type/module.ts b/test/valid-data/function-parameters-infer-type/module.ts new file mode 100644 index 000000000..f6dc13347 --- /dev/null +++ b/test/valid-data/function-parameters-infer-type/module.ts @@ -0,0 +1,15 @@ +export function mFunction(input = "something") { + return "whatever"; +} + +export class MyClass { + someField = "something"; +} + +export const myObj = { + str: "str", + num: 123, + func: ({ a, b } = { a: 1, b: "2" }) => "whatever", +}; + +export const myArray = ["str", 123, () => "whatever"]; diff --git a/test/valid-data/function-parameters-infer-type/schema.json b/test/valid-data/function-parameters-infer-type/schema.json new file mode 100644 index 000000000..9d502160f --- /dev/null +++ b/test/valid-data/function-parameters-infer-type/schema.json @@ -0,0 +1,164 @@ +{ + "$ref": "#/definitions/myFunction", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyClass": { + "additionalProperties": false, + "properties": { + "someField": { + "type": "string" + } + }, + "required": [ + "someField" + ], + "type": "object" + }, + "myFunction": { + "$comment": "(\n str = \"something\",\n num = 123,\n bool = true,\n [a, b, c] = [1, 2, 3],\n obj = { a: 1, b: 2 },\n func = (a: number, b: number) => a + b,\n object = myObj,\n func1 = mFunction,\n clas = new MyClass(),\n arr = myArray) =>undefined", + "properties": { + "namedArgs": { + "additionalProperties": false, + "properties": { + "[a, b, c]": { + "items": { + "type": "number" + }, + "maxItems": 3, + "minItems": 3, + "type": "array" + }, + "arr": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "$comment": "Function" + } + ] + }, + "type": "array" + }, + "bool": { + "type": "boolean" + }, + "clas": { + "$ref": "#/definitions/MyClass" + }, + "func": { + "$comment": "Function", + "properties": { + "namedArgs": { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "b": { + "type": "number" + } + }, + "required": [ + "a", + "b" + ], + "type": "object" + } + }, + "type": "object" + }, + "func1": { + "$comment": "Function", + "properties": { + "namedArgs": { + "additionalProperties": false, + "properties": { + "input": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "num": { + "type": "number" + }, + "obj": { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "b": { + "type": "number" + } + }, + "required": [ + "a", + "b" + ], + "type": "object" + }, + "object": { + "additionalProperties": false, + "properties": { + "func": { + "$comment": "Function", + "properties": { + "namedArgs": { + "additionalProperties": false, + "properties": { + "_0": { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "b": { + "type": "string" + } + }, + "required": [ + "a", + "b" + ], + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "num": { + "type": "number" + }, + "str": { + "type": "string" + } + }, + "required": [ + "str", + "num", + "func" + ], + "type": "object" + }, + "str": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + } +}