diff --git a/package.json b/package.json index 5f9045f74..c8cbfe0c3 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "vega": "^5.31.0", "vega-lite": "^5.23.0" }, + "packageManager": "npm@10.8.2", "engines": { "node": ">=18.0.0" } diff --git a/src/ChainNodeParser.ts b/src/ChainNodeParser.ts index 3ff25d82a..2c6a2503b 100644 --- a/src/ChainNodeParser.ts +++ b/src/ChainNodeParser.ts @@ -31,6 +31,7 @@ export class ChainNodeParser implements SubNodeParser, MutableParser { } const contextCacheKey = context.getCacheKey(); let type = typeCache.get(contextCacheKey); + if (!type) { try { type = this.getNodeParser(node).createType(node, context, reference); @@ -41,6 +42,11 @@ export class ChainNodeParser implements SubNodeParser, MutableParser { typeCache.set(contextCacheKey, type); } } + + if (!type) { + throw new UnknownNodeError(node); + } + return type; } diff --git a/src/NodeParser/CallExpressionParser.ts b/src/NodeParser/CallExpressionParser.ts index a59a03c3d..f75a50816 100644 --- a/src/NodeParser/CallExpressionParser.ts +++ b/src/NodeParser/CallExpressionParser.ts @@ -7,6 +7,7 @@ import type { BaseType } from "../Type/BaseType.js"; import { UnionType } from "../Type/UnionType.js"; import { LiteralType } from "../Type/LiteralType.js"; import { SymbolType } from "../Type/SymbolType.js"; +import { UnknownNodeError } from "../Error/Errors.js"; export class CallExpressionParser implements SubNodeParser { public constructor( @@ -33,9 +34,21 @@ export class CallExpressionParser implements SubNodeParser { } const symbol = type.symbol || type.aliasSymbol; - const decl = symbol.valueDeclaration || symbol.declarations![0]; - const subContext = this.createSubContext(node, context); - return this.childNodeParser.createType(decl, subContext); + + // For funtions like (type: T) => T, there won't be any reference to the original + // type. Using type checker to infer the actual return type without mapping the whole + // function and back referencing its generic type based on parameter index is a better + // approach. + const decl = + this.typeChecker.typeToTypeNode(type, node, ts.NodeBuilderFlags.IgnoreErrors) || + symbol.valueDeclaration || + symbol.declarations?.[0]; + + if (!decl) { + throw new UnknownNodeError(node); + } + + return this.childNodeParser.createType(decl, this.createSubContext(node, context)); } protected createSubContext(node: ts.CallExpression, parentContext: Context): Context { diff --git a/src/NodeParser/IndexedAccessTypeNodeParser.ts b/src/NodeParser/IndexedAccessTypeNodeParser.ts index d7a481350..0a5445a06 100644 --- a/src/NodeParser/IndexedAccessTypeNodeParser.ts +++ b/src/NodeParser/IndexedAccessTypeNodeParser.ts @@ -1,4 +1,5 @@ import ts from "typescript"; +import { LogicError } from "../Error/Errors.js"; import type { Context, NodeParser } from "../NodeParser.js"; import type { SubNodeParser } from "../SubNodeParser.js"; import type { BaseType } from "../Type/BaseType.js"; @@ -9,9 +10,9 @@ import { ReferenceType } from "../Type/ReferenceType.js"; import { StringType } from "../Type/StringType.js"; import { TupleType } from "../Type/TupleType.js"; import { UnionType } from "../Type/UnionType.js"; +import { isErroredUnknownType } from "../Type/UnknownType.js"; import { derefType } from "../Utils/derefType.js"; import { getTypeByKey } from "../Utils/typeKeys.js"; -import { LogicError } from "../Error/Errors.js"; export class IndexedAccessTypeNodeParser implements SubNodeParser { public constructor( @@ -49,7 +50,7 @@ export class IndexedAccessTypeNodeParser implements SubNodeParser { const indexType = derefType(this.childNodeParser.createType(node.indexType, context)); const indexedType = this.createIndexedType(node.objectType, context, indexType); - if (indexedType) { + if (indexedType && !isErroredUnknownType(indexedType)) { return indexedType; } diff --git a/src/NodeParser/TypeReferenceNodeParser.ts b/src/NodeParser/TypeReferenceNodeParser.ts index 14535a243..f0372a2b3 100644 --- a/src/NodeParser/TypeReferenceNodeParser.ts +++ b/src/NodeParser/TypeReferenceNodeParser.ts @@ -6,6 +6,7 @@ import { AnyType } from "../Type/AnyType.js"; import { ArrayType } from "../Type/ArrayType.js"; import type { BaseType } from "../Type/BaseType.js"; import { StringType } from "../Type/StringType.js"; +import { UnknownType } from "../Type/UnknownType.js"; import { symbolAtNode } from "../Utils/symbolAtNode.js"; const invalidTypes: Record = { @@ -45,7 +46,7 @@ export class TypeReferenceNodeParser implements SubNodeParser { } if (typeSymbol.flags & ts.SymbolFlags.TypeParameter) { - return context.getArgument(typeSymbol.name); + return context.getArgument(typeSymbol.name) ?? new UnknownType(true); } // Wraps promise type to avoid resolving to a empty Object type. diff --git a/src/NodeParser/UnknownTypeNodeParser.ts b/src/NodeParser/UnknownTypeNodeParser.ts index ce0d7e0af..b89b20d55 100644 --- a/src/NodeParser/UnknownTypeNodeParser.ts +++ b/src/NodeParser/UnknownTypeNodeParser.ts @@ -1,5 +1,4 @@ import ts from "typescript"; -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"; @@ -8,7 +7,8 @@ export class UnknownTypeNodeParser implements SubNodeParser { public supportsNode(node: ts.KeywordTypeNode): boolean { return node.kind === ts.SyntaxKind.UnknownKeyword; } - public createType(node: ts.KeywordTypeNode, context: Context): BaseType { - return new UnknownType(); + + public createType(): BaseType { + return new UnknownType(false); } } diff --git a/src/Type/UnknownType.ts b/src/Type/UnknownType.ts index 6cce276ff..ec529f6ad 100644 --- a/src/Type/UnknownType.ts +++ b/src/Type/UnknownType.ts @@ -1,10 +1,23 @@ import { BaseType } from "./BaseType.js"; export class UnknownType extends BaseType { - constructor() { + constructor( + /** + * If the source for this UnknownType was from a failed operation than to an actual `unknown` type present in the source code. + */ + readonly erroredSource: boolean, + ) { super(); } + public getId(): string { return "unknown"; } } + +/** + * Checks for an UnknownType with an errored source. + */ +export function isErroredUnknownType(type: BaseType): type is UnknownType { + return type instanceof UnknownType && type.erroredSource; +} diff --git a/src/TypeFormatter/UnknownTypeFormatter.ts b/src/TypeFormatter/UnknownTypeFormatter.ts index da818ca9d..150d581fc 100644 --- a/src/TypeFormatter/UnknownTypeFormatter.ts +++ b/src/TypeFormatter/UnknownTypeFormatter.ts @@ -7,10 +7,16 @@ export class UnknownTypeFormatter implements SubTypeFormatter { public supportsType(type: BaseType): boolean { return type instanceof UnknownType; } + public getDefinition(type: UnknownType): Definition { + if (type.erroredSource) { + return { description: "Failed to correctly infer type" }; + } + return {}; } - public getChildren(type: UnknownType): BaseType[] { + + public getChildren(): BaseType[] { return []; } } diff --git a/test/valid-data-other.test.ts b/test/valid-data-other.test.ts index f41f09ff7..e5db54ef9 100644 --- a/test/valid-data-other.test.ts +++ b/test/valid-data-other.test.ts @@ -84,6 +84,7 @@ describe("valid-data-other", () => { it("array-function-generics", assertValidSchema("array-function-generics", "*")); it("array-max-items-optional", assertValidSchema("array-max-items-optional", "MyType")); it("shorthand-array", assertValidSchema("shorthand-array", "MyType")); + it("function-generic", assertValidSchema("function-generic", "MyType")); it( "object-required", diff --git a/test/valid-data/array-function-generics/schema.json b/test/valid-data/array-function-generics/schema.json index 6e82a59e4..b3a63c0fd 100644 --- a/test/valid-data/array-function-generics/schema.json +++ b/test/valid-data/array-function-generics/schema.json @@ -9,11 +9,15 @@ "additionalProperties": false, "properties": { "a": { - "items": {}, + "items": { + "description": "Failed to correctly infer type" + }, "type": "array" }, "b": { - "items": {}, + "items": { + "description": "Failed to correctly infer type" + }, "type": "array" } }, diff --git a/test/valid-data/function-generic/main.ts b/test/valid-data/function-generic/main.ts new file mode 100644 index 000000000..457ebd29c --- /dev/null +++ b/test/valid-data/function-generic/main.ts @@ -0,0 +1,12 @@ +function fn(a: A, b: B, c: C, any: A | B | C) { + return { a, b, c, any }; +} + +const value = { + litNum: fn(1, "2", true, 1), + litStr: fn("1", 2, true, "2"), + litBool: fn(true, 2, "3", true), + obj: fn({ a: 1 }, { b: "2" }, { c: true }, { a: 1 }), +}; + +export type MyType = typeof value; diff --git a/test/valid-data/function-generic/schema.json b/test/valid-data/function-generic/schema.json new file mode 100644 index 000000000..988aa7d9d --- /dev/null +++ b/test/valid-data/function-generic/schema.json @@ -0,0 +1,190 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "additionalProperties": false, + "properties": { + "litBool": { + "additionalProperties": false, + "properties": { + "a": { + "type": "boolean" + }, + "any": { + "type": [ + "string", + "number", + "boolean" + ] + }, + "b": { + "type": "number" + }, + "c": { + "type": "string" + } + }, + "required": [ + "a", + "b", + "c", + "any" + ], + "type": "object" + }, + "litNum": { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "any": { + "type": [ + "string", + "number", + "boolean" + ] + }, + "b": { + "type": "string" + }, + "c": { + "type": "boolean" + } + }, + "required": [ + "a", + "b", + "c", + "any" + ], + "type": "object" + }, + "litStr": { + "additionalProperties": false, + "properties": { + "a": { + "type": "string" + }, + "any": { + "type": [ + "string", + "number", + "boolean" + ] + }, + "b": { + "type": "number" + }, + "c": { + "type": "boolean" + } + }, + "required": [ + "a", + "b", + "c", + "any" + ], + "type": "object" + }, + "obj": { + "additionalProperties": false, + "properties": { + "a": { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + "any": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "b": { + "type": "string" + } + }, + "required": [ + "b" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "c": { + "type": "boolean" + } + }, + "required": [ + "c" + ], + "type": "object" + } + ] + }, + "b": { + "additionalProperties": false, + "properties": { + "b": { + "type": "string" + } + }, + "required": [ + "b" + ], + "type": "object" + }, + "c": { + "additionalProperties": false, + "properties": { + "c": { + "type": "boolean" + } + }, + "required": [ + "c" + ], + "type": "object" + } + }, + "required": [ + "a", + "b", + "c", + "any" + ], + "type": "object" + } + }, + "required": [ + "litNum", + "litStr", + "litBool", + "obj" + ], + "type": "object" + } + } +} diff --git a/ts-json-schema-generator.ts b/ts-json-schema-generator.ts index 52181cdec..29b0c452c 100644 --- a/ts-json-schema-generator.ts +++ b/ts-json-schema-generator.ts @@ -96,6 +96,8 @@ try { if (error.cause) { console.error(error.cause); + } else if (error.stack) { + console.debug(error.stack); } // Maybe we are being imported by another script