Skip to content

Commit

Permalink
feat: added diagnostics to all errors (#1963)
Browse files Browse the repository at this point in the history
* fix: errors for sourceless nodes

* initial promise support

* revert

* feat: final implementation

* fix: imports

* code

* lint

* code

* fix: tests

* expose internals

* i was wrong

* fix: entire check

* fix: lint

* type

* somehow tests were getting this out of order

* fix: removed unused method

* feat: added unnamed class test

* fix: test

* documented promise unwrapping

* Update src/NodeParser/FunctionNodeParser.ts

Co-authored-by: Dominik Moritz <domoritz@gmail.com>

* fix: removed allowUnionTypes

* chore: style

* feat: code

* fixes

* non tty errors

* remove useless test

* fix: keep same error message

* fix: remove TSJG prefixes

* fix yarn

* keep exact error message

---------

Co-authored-by: Dominik Moritz <domoritz@gmail.com>
  • Loading branch information
arthurfiorette and domoritz committed May 23, 2024
1 parent 4ac21b2 commit 49df2de
Show file tree
Hide file tree
Showing 33 changed files with 424 additions and 333 deletions.
77 changes: 43 additions & 34 deletions factory/program.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,47 @@
import * as glob from "glob";
import * as path from "path";
import ts from "typescript";
import * as path from "node:path";
import normalize from "normalize-path";

import { CompletedConfig, Config } from "../src/Config.js";
import { DiagnosticError } from "../src/Error/DiagnosticError.js";
import { LogicError } from "../src/Error/LogicError.js";
import { NoRootNamesError } from "../src/Error/NoRootNamesError.js";
import { NoTSConfigError } from "../src/Error/NoTSConfigError.js";
import ts from "typescript";
import type { CompletedConfig, Config } from "../src/Config.js";
import { BuildError } from "../src/Error/Errors.js";

function loadTsConfigFile(configFile: string) {
const raw = ts.sys.readFile(configFile);
if (raw) {
const config = ts.parseConfigFileTextToJson(configFile, raw);

if (config.error) {
throw new DiagnosticError([config.error]);
} else if (!config.config) {
throw new LogicError(`Invalid parsed config file "${configFile}"`);
}
if (!raw) {
throw new BuildError({
messageText: `Cannot read config file "${configFile}"`,
});
}

const parseResult = ts.parseJsonConfigFileContent(
config.config,
ts.sys,
path.resolve(path.dirname(configFile)),
{},
configFile,
);
parseResult.options.noEmit = true;
delete parseResult.options.out;
delete parseResult.options.outDir;
delete parseResult.options.outFile;
delete parseResult.options.declaration;
delete parseResult.options.declarationDir;
delete parseResult.options.declarationMap;
const config = ts.parseConfigFileTextToJson(configFile, raw);

return parseResult;
} else {
throw new NoTSConfigError();
if (config.error) {
throw new BuildError(config.error);
}

if (!config.config) {
throw new BuildError({
messageText: `Invalid parsed config file "${configFile}"`,
});
}

const parseResult = ts.parseJsonConfigFileContent(
config.config,
ts.sys,
path.resolve(path.dirname(configFile)),
{},
configFile,
);
parseResult.options.noEmit = true;
delete parseResult.options.out;
delete parseResult.options.outDir;
delete parseResult.options.outFile;
delete parseResult.options.declaration;
delete parseResult.options.declarationDir;
delete parseResult.options.declarationMap;

return parseResult;
}

function getTsConfig(config: Config) {
Expand Down Expand Up @@ -67,15 +70,21 @@ export function createProgram(config: CompletedConfig): ts.Program {
const rootNames = rootNamesFromPath.length ? rootNamesFromPath : tsconfig.fileNames;

if (!rootNames.length) {
throw new NoRootNamesError();
throw new BuildError({
messageText: "No input files",
});
}

const program: ts.Program = ts.createProgram(rootNames, tsconfig.options);

if (!config.skipTypeCheck) {
const diagnostics = ts.getPreEmitDiagnostics(program);

if (diagnostics.length) {
throw new DiagnosticError(diagnostics);
throw new BuildError({
messageText: "Type check error",
relatedInformation: [...diagnostics],
});
}
}

Expand Down
10 changes: 1 addition & 9 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
export * from "./src/Error/BaseError.js";
export * from "./src/Error/DiagnosticError.js";
export * from "./src/Error/LogicError.js";
export * from "./src/Error/NoRootNamesError.js";
export * from "./src/Error/NoRootTypeError.js";
export * from "./src/Error/NoTSConfigError.js";
export * from "./src/Error/UnknownNodeError.js";
export * from "./src/Error/UnknownTypeError.js";
export * from "./src/Error/Errors.js";

export * from "./src/Config.js";

export * from "./src/Utils/allOfDefinition.js";
export * from "./src/Utils/assert.js";
export * from "./src/Utils/deepMerge.js";
export * from "./src/Utils/derefType.js";
export * from "./src/Utils/extractLiterals.js";
export * from "./src/Utils/formatError.js";
export * from "./src/Utils/hasJsDocTag.js";
export * from "./src/Utils/intersectionOfArrays.js";
export * from "./src/Utils/isAssignableTo.js";
Expand Down
18 changes: 9 additions & 9 deletions src/ChainNodeParser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import ts from "typescript";
import { UnknownNodeError } from "./Error/UnknownNodeError.js";
import { MutableParser } from "./MutableParser.js";
import { Context } from "./NodeParser.js";
import { SubNodeParser } from "./SubNodeParser.js";
import { BaseType } from "./Type/BaseType.js";
import type ts from "typescript";
import { UnknownNodeError } from "./Error/Errors.js";
import type { MutableParser } from "./MutableParser.js";
import type { Context } from "./NodeParser.js";
import type { SubNodeParser } from "./SubNodeParser.js";
import type { BaseType } from "./Type/BaseType.js";
import { ReferenceType } from "./Type/ReferenceType.js";

export class ChainNodeParser implements SubNodeParser, MutableParser {
Expand Down Expand Up @@ -32,21 +32,21 @@ export class ChainNodeParser implements SubNodeParser, MutableParser {
const contextCacheKey = context.getCacheKey();
let type = typeCache.get(contextCacheKey);
if (!type) {
type = this.getNodeParser(node, context).createType(node, context, reference);
type = this.getNodeParser(node).createType(node, context, reference);
if (!(type instanceof ReferenceType)) {
typeCache.set(contextCacheKey, type);
}
}
return type;
}

protected getNodeParser(node: ts.Node, context: Context): SubNodeParser {
protected getNodeParser(node: ts.Node): SubNodeParser {
for (const nodeParser of this.nodeParsers) {
if (nodeParser.supportsNode(node)) {
return nodeParser;
}
}

throw new UnknownNodeError(node, context.getReference());
throw new UnknownNodeError(node);
}
}
10 changes: 5 additions & 5 deletions src/ChainTypeFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { UnknownTypeError } from "./Error/UnknownTypeError.js";
import { MutableTypeFormatter } from "./MutableTypeFormatter.js";
import { Definition } from "./Schema/Definition.js";
import { SubTypeFormatter } from "./SubTypeFormatter.js";
import { BaseType } from "./Type/BaseType.js";
import { UnknownTypeError } from "./Error/Errors.js";
import type { MutableTypeFormatter } from "./MutableTypeFormatter.js";
import type { Definition } from "./Schema/Definition.js";
import type { SubTypeFormatter } from "./SubTypeFormatter.js";
import type { BaseType } from "./Type/BaseType.js";

export class ChainTypeFormatter implements SubTypeFormatter, MutableTypeFormatter {
public constructor(protected typeFormatters: SubTypeFormatter[]) {}
Expand Down
62 changes: 60 additions & 2 deletions src/Error/BaseError.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
import ts from "typescript";

export type PartialDiagnostic = Omit<ts.Diagnostic, "category" | "file" | "start" | "length"> & {
file?: ts.SourceFile;
start?: number;
length?: number;

/** If we should populate `file`, `source`, `start` and `length` with this node information */
node?: ts.Node;

/** @default Error */
category?: ts.DiagnosticCategory;
};

const isTTY = process.env.TTY || process.stdout.isTTY;

/**
* Base error for ts-json-schema-generator
*/
export abstract class BaseError extends Error {
public constructor(message?: string) {
super(message);
readonly diagnostic: ts.Diagnostic;

constructor(diagnostic: PartialDiagnostic) {
super(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
this.diagnostic = BaseError.createDiagnostic(diagnostic);
}

static createDiagnostic(diagnostic: PartialDiagnostic): ts.Diagnostic {
// Swap the node for the file, source, start and length properties
// sourceless nodes cannot be referenced in the diagnostic
if (diagnostic.node && diagnostic.node.pos !== -1) {
diagnostic.file = diagnostic.node.getSourceFile();
diagnostic.start = diagnostic.node.getStart();
diagnostic.length = diagnostic.node.getWidth();

diagnostic.node = undefined;
}

// @ts-expect-error - Differentiates from errors from the TypeScript compiler
// error TSJ - 100: message
diagnostic.code = `J - ${diagnostic.code}`;

return Object.assign(
{
category: ts.DiagnosticCategory.Error,
file: undefined,
length: 0,
start: 0,
},
diagnostic,
);
}

format() {
const formatter = isTTY ? ts.formatDiagnosticsWithColorAndContext : ts.formatDiagnostics;

return formatter([this.diagnostic], {
getCanonicalFileName: (fileName) => fileName,
getCurrentDirectory: () => "",
getNewLine: () => "\n",
});
}
}
14 changes: 0 additions & 14 deletions src/Error/DiagnosticError.ts

This file was deleted.

103 changes: 103 additions & 0 deletions src/Error/Errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import ts from "typescript";
import { type PartialDiagnostic, BaseError } from "./BaseError.js";
import type { BaseType } from "../Type/BaseType.js";
import type { JSONSchema7 } from "json-schema";

export class UnknownNodeError extends BaseError {
constructor(readonly node: ts.Node) {
super({
code: 100,
node,
messageText: `Unknown node of kind "${ts.SyntaxKind[node.kind]}"`,
});
}
}

export class UnknownTypeError extends BaseError {
constructor(readonly type: BaseType) {
super({
code: 101,
messageText: `Unknown type "${type.getId()}"`,
});
}
}

export class RootlessError extends BaseError {
constructor(readonly fullName: string) {
super({
code: 102,
messageText: `No root type "${fullName}" found`,
});
}
}

export class MultipleDefinitionsError extends BaseError {
constructor(
readonly name: string,
readonly defA: BaseType,
readonly defB?: BaseType,
) {
super({
code: 103,
messageText: `Type "${name}" has multiple definitions.`,
});
}
}

export class LogicError extends BaseError {
constructor(
readonly node: ts.Node,
messageText: string,
) {
super({
code: 104,
messageText,
node,
});
}
}

export class ExpectationFailedError extends BaseError {
constructor(
messageText: string,
readonly node?: ts.Node,
) {
super({
code: 105,
messageText,
node,
});
}
}

export class JsonTypeError extends BaseError {
constructor(
messageText: string,
readonly type: BaseType,
) {
super({
code: 106,
messageText,
});
}
}

export class DefinitionError extends BaseError {
constructor(
messageText: string,
readonly definition: JSONSchema7,
) {
super({
code: 107,
messageText,
});
}
}
export class BuildError extends BaseError {
constructor(diag: Omit<PartialDiagnostic, "code">) {
super({
code: 108,
...diag,
});
}
}
7 changes: 0 additions & 7 deletions src/Error/LogicError.ts

This file was deleted.

10 changes: 0 additions & 10 deletions src/Error/NoRootNamesError.ts

This file was deleted.

11 changes: 0 additions & 11 deletions src/Error/NoRootTypeError.ts

This file was deleted.

Loading

0 comments on commit 49df2de

Please sign in to comment.