From d69f59258e0ee1b3f67dc24c5285e7e0d0c6d286 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette <47537704+arthurfiorette@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:50:13 -0300 Subject: [PATCH] feat: add unhandled error to append useful information (#2063) --- src/ChainNodeParser.ts | 8 ++++++-- src/Error/Errors.ts | 23 +++++++++++++++++++++-- src/SchemaGenerator.ts | 35 ++++++++++++++++++++++++++--------- ts-json-schema-generator.ts | 16 +++++++++++----- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/ChainNodeParser.ts b/src/ChainNodeParser.ts index 069b6fa00..3ff25d82a 100644 --- a/src/ChainNodeParser.ts +++ b/src/ChainNodeParser.ts @@ -1,5 +1,5 @@ import type ts from "typescript"; -import { UnknownNodeError } from "./Error/Errors.js"; +import { UnhandledError, UnknownNodeError } from "./Error/Errors.js"; import type { MutableParser } from "./MutableParser.js"; import type { Context } from "./NodeParser.js"; import type { SubNodeParser } from "./SubNodeParser.js"; @@ -32,7 +32,11 @@ export class ChainNodeParser implements SubNodeParser, MutableParser { const contextCacheKey = context.getCacheKey(); let type = typeCache.get(contextCacheKey); if (!type) { - type = this.getNodeParser(node).createType(node, context, reference); + try { + type = this.getNodeParser(node).createType(node, context, reference); + } catch (error) { + throw UnhandledError.from("Unhandled error while creating Base Type.", node, error); + } if (!(type instanceof ReferenceType)) { typeCache.set(contextCacheKey, type); } diff --git a/src/Error/Errors.ts b/src/Error/Errors.ts index 2ef3b78cd..ef4161f5d 100644 --- a/src/Error/Errors.ts +++ b/src/Error/Errors.ts @@ -1,7 +1,7 @@ +import type { JSONSchema7 } from "json-schema"; import ts from "typescript"; -import { type PartialDiagnostic, BaseError } from "./BaseError.js"; import type { BaseType } from "../Type/BaseType.js"; -import type { JSONSchema7 } from "json-schema"; +import { BaseError, type PartialDiagnostic } from "./BaseError.js"; export class UnknownNodeError extends BaseError { constructor(readonly node: ts.Node) { @@ -102,3 +102,22 @@ export class BuildError extends BaseError { }); } } + +export class UnhandledError extends BaseError { + private constructor( + messageText: string, + node?: ts.Node, + readonly cause?: unknown, + ) { + super({ code: 109, messageText, node }); + } + + /** + * Creates a new error with a cause and optional node information and ensures it is not wrapped in another UnhandledError + */ + static from(message: string, node?: ts.Node, cause?: unknown) { + // This might be called deeply inside the parser chain + // this ensures it doesn't end up with error.cause.cause.cause... + return cause instanceof BaseError ? cause : new UnhandledError(message, node, cause); + } +} diff --git a/src/SchemaGenerator.ts b/src/SchemaGenerator.ts index 910edc3f3..7c990d479 100644 --- a/src/SchemaGenerator.ts +++ b/src/SchemaGenerator.ts @@ -1,6 +1,6 @@ import ts from "typescript"; import type { Config } from "./Config.js"; -import { MultipleDefinitionsError, RootlessError } from "./Error/Errors.js"; +import { MultipleDefinitionsError, RootlessError, UnhandledError } from "./Error/Errors.js"; import { Context, type NodeParser } from "./NodeParser.js"; import type { Definition } from "./Schema/Definition.js"; import type { Schema } from "./Schema/Schema.js"; @@ -26,15 +26,25 @@ export class SchemaGenerator { } public createSchemaFromNodes(rootNodes: ts.Node[]): Schema { - const rootTypes = rootNodes.map((rootNode) => { - return this.nodeParser.createType(rootNode, new Context()); - }); + const roots = rootNodes.map((rootNode) => ({ + rootNode: rootNode, + rootType: this.nodeParser.createType(rootNode, new Context()), + })); - const rootTypeDefinition = rootTypes.length === 1 ? this.getRootTypeDefinition(rootTypes[0]) : undefined; + const rootTypeDefinition = + roots.length === 1 ? this.getRootTypeDefinition(roots[0].rootType, roots[0].rootNode) : undefined; const definitions: StringMap = {}; - for (const rootType of rootTypes) { - this.appendRootChildDefinitions(rootType, definitions); + for (const root of roots) { + try { + this.appendRootChildDefinitions(root.rootType, definitions); + } catch (error) { + throw UnhandledError.from( + "Unhandled error while appending Child Type Definition.", + root.rootNode, + error, + ); + } } const reachableDefinitions = removeUnreachable(rootTypeDefinition, definitions); @@ -60,6 +70,7 @@ export class SchemaGenerator { this.appendTypes(rootSourceFiles, this.program.getTypeChecker(), rootNodes); return [...rootNodes.values()]; } + protected findNamedNode(fullName: string): ts.Node { const typeChecker = this.program.getTypeChecker(); const allTypes = new Map(); @@ -79,9 +90,15 @@ export class SchemaGenerator { throw new RootlessError(fullName); } - protected getRootTypeDefinition(rootType: BaseType): Definition { - return this.typeFormatter.getDefinition(rootType); + + protected getRootTypeDefinition(rootType: BaseType, rootNode: ts.Node): Definition { + try { + return this.typeFormatter.getDefinition(rootType); + } catch (error) { + throw UnhandledError.from("Unhandled error while creating Root Type Definition.", rootNode, error); + } } + protected appendRootChildDefinitions(rootType: BaseType, childDefinitions: StringMap): void { const seen = new Set(); diff --git a/ts-json-schema-generator.ts b/ts-json-schema-generator.ts index ea63ac28b..953929041 100644 --- a/ts-json-schema-generator.ts +++ b/ts-json-schema-generator.ts @@ -86,13 +86,19 @@ try { writeFileSync(args.out, schemaString); } else { // write to stdout - process.stdout.write(`${schemaString}\n`); + console.log(`${schemaString}\n`); } } catch (error) { if (error instanceof BaseError) { - process.stderr.write(error.format()); - process.exit(1); - } + console.error(error.format()); + + if (error.cause) { + console.error(error.cause); + } - throw error; + // Maybe we are being imported by another script + process.exitCode = 1; + } else { + throw error; + } }