From f75dbaf522f8b21d973ff383483839c690fc21b3 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Wed, 22 May 2024 18:47:43 -0300 Subject: [PATCH 1/7] code --- src/SchemaGenerator.ts | 47 +++++++++++++++++++++++++++++ test/sourceless-nodes/index.test.ts | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/SchemaGenerator.ts b/src/SchemaGenerator.ts index 723aaa7c9..d8be8606c 100644 --- a/src/SchemaGenerator.ts +++ b/src/SchemaGenerator.ts @@ -187,6 +187,53 @@ export class SchemaGenerator { } return; } + case ts.SyntaxKind.ExportDeclaration: { + if (!ts.isExportDeclaration(node)) { + return; + } + + // export { variable } clauses + if (!node.moduleSpecifier) { + return; + } + + const symbol = typeChecker.getSymbolAtLocation(node.moduleSpecifier); + + // should never hit this (maybe type error in user's code) + if (!symbol || !symbol.declarations) { + return; + } + + // module augmentation can result in more than one source file + for (const source of symbol.declarations) { + const sourceSymbol = typeChecker.getSymbolAtLocation(source); + + if (!sourceSymbol) { + throw new Error( + `Could not find symbol for SourceFile at ${(source as ts.SourceFile).fileName}`, + ); + } + + const moduleExports = typeChecker.getExportsOfModule(sourceSymbol); + + for (const moduleExport of moduleExports) { + const nodes = + moduleExport.declarations || + (!!moduleExport.valueDeclaration && [moduleExport.valueDeclaration]); + + // should never hit this (maybe type error in user's code) + if (!nodes) { + return; + } + + for (const node of nodes) { + this.inspectNode(node, typeChecker, allTypes); + } + } + } + + return; + } default: ts.forEachChild(node, (subnode) => this.inspectNode(subnode, typeChecker, allTypes)); return; diff --git a/test/sourceless-nodes/index.test.ts b/test/sourceless-nodes/index.test.ts index cb73297f5..90c2e6369 100644 --- a/test/sourceless-nodes/index.test.ts +++ b/test/sourceless-nodes/index.test.ts @@ -41,7 +41,7 @@ describe("sourceless-nodes", () => { }); }); -// From github.com/arthurfiorette/kita/blob/main/packages/generator/src/util/type-resolver.ts +// From https://github.com/kitajs/kitajs/blob/ebf23297de07887c78becff52120f941e69386ec/packages/parser/src/util/nodes.ts#L64 function getReturnType(node: ts.SignatureDeclaration, typeChecker: ts.TypeChecker) { const signature = typeChecker.getSignatureFromDeclaration(node); const implicitType = typeChecker.getReturnTypeOfSignature(signature!); From 319fcfee7b1b41165877a3d08332ae02a5bc5649 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Wed, 22 May 2024 19:10:42 -0300 Subject: [PATCH 2/7] tests --- test/valid-data-type.test.ts | 2 ++ test/valid-data/export-star/literal.ts | 5 +++ test/valid-data/export-star/main.ts | 4 +++ test/valid-data/export-star/object.ts | 11 +++++++ test/valid-data/export-star/schema.json | 43 +++++++++++++++++++++++++ 5 files changed, 65 insertions(+) create mode 100644 test/valid-data/export-star/literal.ts create mode 100644 test/valid-data/export-star/main.ts create mode 100644 test/valid-data/export-star/object.ts create mode 100644 test/valid-data/export-star/schema.json diff --git a/test/valid-data-type.test.ts b/test/valid-data-type.test.ts index c1699a58e..39e626b47 100644 --- a/test/valid-data-type.test.ts +++ b/test/valid-data-type.test.ts @@ -145,4 +145,6 @@ describe("valid-data-type", () => { it("lowercase", assertValidSchema("lowercase", "MyType")); it("promise-extensions", assertValidSchema("promise-extensions", "*")); + + it("export-star", assertValidSchema("export-star", "*", undefined, { mainTsOnly: true })); }); diff --git a/test/valid-data/export-star/literal.ts b/test/valid-data/export-star/literal.ts new file mode 100644 index 000000000..e7a9fe6eb --- /dev/null +++ b/test/valid-data/export-star/literal.ts @@ -0,0 +1,5 @@ +export type A = 1; + +export type B = "string"; + +type C = "internal"; diff --git a/test/valid-data/export-star/main.ts b/test/valid-data/export-star/main.ts new file mode 100644 index 000000000..5333cbd85 --- /dev/null +++ b/test/valid-data/export-star/main.ts @@ -0,0 +1,4 @@ +export * from "./literal"; +export * from "./object"; + +export type External = 1; diff --git a/test/valid-data/export-star/object.ts b/test/valid-data/export-star/object.ts new file mode 100644 index 000000000..e9ebcacea --- /dev/null +++ b/test/valid-data/export-star/object.ts @@ -0,0 +1,11 @@ +export interface D { + a: 1; +} + +export class E { + b: 2; +} + +interface F { + internal: true; +} diff --git a/test/valid-data/export-star/schema.json b/test/valid-data/export-star/schema.json new file mode 100644 index 000000000..0c34121c2 --- /dev/null +++ b/test/valid-data/export-star/schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "A": { + "const": 1, + "type": "number" + }, + "B": { + "const": "string", + "type": "string" + }, + "D": { + "additionalProperties": false, + "properties": { + "a": { + "const": 1, + "type": "number" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + "E": { + "additionalProperties": false, + "properties": { + "b": { + "const": 2, + "type": "number" + } + }, + "required": [ + "b" + ], + "type": "object" + }, + "External": { + "const": 1, + "type": "number" + } + } +} From a699a4dffbceb24fef143000860201a60303b4fc Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Wed, 22 May 2024 19:47:11 -0300 Subject: [PATCH 3/7] test fix --- test/valid-data/export-star/main.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/valid-data/export-star/main.ts b/test/valid-data/export-star/main.ts index 5333cbd85..524c70e91 100644 --- a/test/valid-data/export-star/main.ts +++ b/test/valid-data/export-star/main.ts @@ -2,3 +2,5 @@ export * from "./literal"; export * from "./object"; export type External = 1; + +type Internal = 2; From 5d16d78a3aaec7d9925ca27fe62490eba374cab1 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Wed, 22 May 2024 19:50:32 -0300 Subject: [PATCH 4/7] lint --- src/SchemaGenerator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SchemaGenerator.ts b/src/SchemaGenerator.ts index d8be8606c..044e2347f 100644 --- a/src/SchemaGenerator.ts +++ b/src/SchemaGenerator.ts @@ -226,8 +226,8 @@ export class SchemaGenerator { return; } - for (const node of nodes) { - this.inspectNode(node, typeChecker, allTypes); + for (const subnodes of nodes) { + this.inspectNode(subnodes, typeChecker, allTypes); } } } From f997aeeb2efc3b9ce35eb617dc470dee20336d01 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Wed, 22 May 2024 20:02:15 -0300 Subject: [PATCH 5/7] refactors --- jest.config.cjs | 1 + src/SchemaGenerator.ts | 222 ++++++++++++++++++++------------------ src/Utils/symbolAtNode.ts | 7 +- 3 files changed, 122 insertions(+), 108 deletions(-) diff --git a/jest.config.cjs b/jest.config.cjs index 9b551660c..f104b325e 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -6,6 +6,7 @@ const config = { coverageDirectory: "./coverage/", collectCoverage: false, testEnvironment: "node", + maxWorkers: '50%', // speeds up tests transform: { ".*": "babel-jest", }, diff --git a/src/SchemaGenerator.ts b/src/SchemaGenerator.ts index 044e2347f..1dcadbf40 100644 --- a/src/SchemaGenerator.ts +++ b/src/SchemaGenerator.ts @@ -1,16 +1,16 @@ import ts from "typescript"; +import type { Config } from "./Config.js"; import { NoRootTypeError } from "./Error/NoRootTypeError.js"; -import { Context, NodeParser } from "./NodeParser.js"; -import { Definition } from "./Schema/Definition.js"; -import { Schema } from "./Schema/Schema.js"; -import { BaseType } from "./Type/BaseType.js"; +import { Context, type NodeParser } from "./NodeParser.js"; +import type { Definition } from "./Schema/Definition.js"; +import type { Schema } from "./Schema/Schema.js"; +import type { BaseType } from "./Type/BaseType.js"; import { DefinitionType } from "./Type/DefinitionType.js"; -import { TypeFormatter } from "./TypeFormatter.js"; -import { StringMap } from "./Utils/StringMap.js"; -import { localSymbolAtNode, symbolAtNode } from "./Utils/symbolAtNode.js"; -import { removeUnreachable } from "./Utils/removeUnreachable.js"; -import { Config } from "./Config.js"; +import type { TypeFormatter } from "./TypeFormatter.js"; +import type { StringMap } from "./Utils/StringMap.js"; import { hasJsDocTag } from "./Utils/hasJsDocTag.js"; +import { removeUnreachable } from "./Utils/removeUnreachable.js"; +import { symbolAtNode } from "./Utils/symbolAtNode.js"; export class SchemaGenerator { public constructor( @@ -32,7 +32,10 @@ export class SchemaGenerator { const rootTypeDefinition = rootTypes.length === 1 ? this.getRootTypeDefinition(rootTypes[0]) : undefined; const definitions: StringMap = {}; - rootTypes.forEach((rootType) => this.appendRootChildDefinitions(rootType, definitions)); + + for (const rootType of rootTypes) { + this.appendRootChildDefinitions(rootType, definitions); + } const reachableDefinitions = removeUnreachable(rootTypeDefinition, definitions); @@ -47,15 +50,15 @@ export class SchemaGenerator { protected getRootNodes(fullName: string | undefined): ts.Node[] { if (fullName && fullName !== "*") { return [this.findNamedNode(fullName)]; - } else { - const rootFileNames = this.program.getRootFileNames(); - const rootSourceFiles = this.program - .getSourceFiles() - .filter((sourceFile) => rootFileNames.includes(sourceFile.fileName)); - const rootNodes = new Map(); - this.appendTypes(rootSourceFiles, this.program.getTypeChecker(), rootNodes); - return [...rootNodes.values()]; } + + const rootFileNames = this.program.getRootFileNames(); + const rootSourceFiles = this.program + .getSourceFiles() + .filter((sourceFile) => rootFileNames.includes(sourceFile.fileName)); + const rootNodes = new Map(); + this.appendTypes(rootSourceFiles, this.program.getTypeChecker(), rootNodes); + return [...rootNodes.values()]; } protected findNamedNode(fullName: string): ts.Node { const typeChecker = this.program.getTypeChecker(); @@ -129,6 +132,7 @@ export class SchemaGenerator { return { projectFiles, externalFiles }; } + protected appendTypes( sourceFiles: readonly ts.SourceFile[], typeChecker: ts.TypeChecker, @@ -138,119 +142,131 @@ export class SchemaGenerator { this.inspectNode(sourceFile, typeChecker, types); } } + protected inspectNode(node: ts.Node, typeChecker: ts.TypeChecker, allTypes: Map): void { - switch (node.kind) { - case ts.SyntaxKind.VariableDeclaration: { - const variableDeclarationNode = node as ts.VariableDeclaration; - if ( - variableDeclarationNode.initializer?.kind === ts.SyntaxKind.ArrowFunction || - variableDeclarationNode.initializer?.kind === ts.SyntaxKind.FunctionExpression - ) { - this.inspectNode(variableDeclarationNode.initializer, typeChecker, allTypes); - } - return; + if (ts.isVariableDeclaration(node)) { + if ( + node.initializer?.kind === ts.SyntaxKind.ArrowFunction || + node.initializer?.kind === ts.SyntaxKind.FunctionExpression + ) { + this.inspectNode(node.initializer, typeChecker, allTypes); } - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.TypeAliasDeclaration: - if ( - this.config?.expose === "all" || - (this.isExportType(node) && !this.isGenericType(node as ts.TypeAliasDeclaration)) - ) { - allTypes.set(this.getFullName(node, typeChecker), node); - return; - } - return; - case ts.SyntaxKind.ConstructorType: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.ArrowFunction: + + return; + } + + if ( + ts.isInterfaceDeclaration(node) || + ts.isClassDeclaration(node) || + ts.isEnumDeclaration(node) || + ts.isTypeAliasDeclaration(node) + ) { + if ( + this.config?.expose === "all" || + (this.isExportType(node) && !this.isGenericType(node as ts.TypeAliasDeclaration)) + ) { allTypes.set(this.getFullName(node, typeChecker), node); return; - case ts.SyntaxKind.ExportSpecifier: { - const exportSpecifierNode = node as ts.ExportSpecifier; - const symbol = typeChecker.getExportSpecifierLocalTargetSymbol(exportSpecifierNode); - if (symbol?.declarations?.length === 1) { - const declaration = symbol.declarations[0]; - if (declaration.kind === ts.SyntaxKind.ImportSpecifier) { - // Handling the `Foo` in `import { Foo } from "./lib"; export { Foo };` - const importSpecifierNode = declaration as ts.ImportSpecifier; - const type = typeChecker.getTypeAtLocation(importSpecifierNode); - if (type.symbol?.declarations?.length === 1) { - this.inspectNode(type.symbol.declarations[0], typeChecker, allTypes); - } - } else { - // Handling the `Bar` in `export { Bar } from './lib';` - this.inspectNode(declaration, typeChecker, allTypes); + } + return; + } + + if ( + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isConstructorTypeNode(node) + ) { + allTypes.set(this.getFullName(node, typeChecker), node); + return; + } + + if (ts.isExportSpecifier(node)) { + const symbol = typeChecker.getExportSpecifierLocalTargetSymbol(node); + + if (symbol?.declarations?.length === 1) { + const declaration = symbol.declarations[0]; + + if (ts.isImportSpecifier(declaration)) { + // Handling the `Foo` in `import { Foo } from "./lib"; export { Foo };` + const importSpecifierNode = declaration as ts.ImportSpecifier; + const type = typeChecker.getTypeAtLocation(importSpecifierNode); + if (type.symbol?.declarations?.length === 1) { + this.inspectNode(type.symbol.declarations[0], typeChecker, allTypes); } + } else { + // Handling the `Bar` in `export { Bar } from './lib';` + this.inspectNode(declaration, typeChecker, allTypes); } + } + + return; + } + + if (ts.isExportDeclaration(node)) { + if (!ts.isExportDeclaration(node)) { return; } - case ts.SyntaxKind.ExportDeclaration: { - if (!ts.isExportDeclaration(node)) { - return; - } - // export { variable } clauses - if (!node.moduleSpecifier) { - return; - } + // export { variable } clauses + if (!node.moduleSpecifier) { + return; + } - const symbol = typeChecker.getSymbolAtLocation(node.moduleSpecifier); + const symbol = typeChecker.getSymbolAtLocation(node.moduleSpecifier); - // should never hit this (maybe type error in user's code) - if (!symbol || !symbol.declarations) { - return; - } + // should never hit this (maybe type error in user's code) + if (!symbol || !symbol.declarations) { + return; + } - // module augmentation can result in more than one source file - for (const source of symbol.declarations) { - const sourceSymbol = typeChecker.getSymbolAtLocation(source); + // module augmentation can result in more than one source file + for (const source of symbol.declarations) { + const sourceSymbol = typeChecker.getSymbolAtLocation(source); - if (!sourceSymbol) { - throw new Error( - `Could not find symbol for SourceFile at ${(source as ts.SourceFile).fileName}`, - ); - } + if (!sourceSymbol) { + return; + } - const moduleExports = typeChecker.getExportsOfModule(sourceSymbol); + const moduleExports = typeChecker.getExportsOfModule(sourceSymbol); - for (const moduleExport of moduleExports) { - const nodes = - moduleExport.declarations || - (!!moduleExport.valueDeclaration && [moduleExport.valueDeclaration]); + for (const moduleExport of moduleExports) { + const nodes = + moduleExport.declarations || + (!!moduleExport.valueDeclaration && [moduleExport.valueDeclaration]); - // should never hit this (maybe type error in user's code) - if (!nodes) { - return; - } + // should never hit this (maybe type error in user's code) + if (!nodes) { + return; + } - for (const subnodes of nodes) { - this.inspectNode(subnodes, typeChecker, allTypes); - } + for (const subnodes of nodes) { + this.inspectNode(subnodes, typeChecker, allTypes); } } - - return; } - default: - ts.forEachChild(node, (subnode) => this.inspectNode(subnode, typeChecker, allTypes)); - return; + + return; } + + ts.forEachChild(node, (subnode) => this.inspectNode(subnode, typeChecker, allTypes)); } - protected isExportType(node: ts.Node): boolean { + + protected isExportType( + node: ts.InterfaceDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.TypeAliasDeclaration, + ): boolean { if (this.config?.jsDoc !== "none" && hasJsDocTag(node, "internal")) { return false; } - const localSymbol = localSymbolAtNode(node); - return localSymbol ? "exportSymbol" in localSymbol : false; + + return !!node.localSymbol?.exportSymbol; } + protected isGenericType(node: ts.TypeAliasDeclaration): boolean { return !!(node.typeParameters && node.typeParameters.length > 0); } - protected getFullName(node: ts.Node, typeChecker: ts.TypeChecker): string { - const symbol = symbolAtNode(node)!; - return typeChecker.getFullyQualifiedName(symbol).replace(/".*"\./, ""); + + protected getFullName(node: ts.Declaration, typeChecker: ts.TypeChecker): string { + return typeChecker.getFullyQualifiedName(node.symbol).replace(/".*"\./, ""); } } diff --git a/src/Utils/symbolAtNode.ts b/src/Utils/symbolAtNode.ts index ab0335e15..1a6988d2b 100644 --- a/src/Utils/symbolAtNode.ts +++ b/src/Utils/symbolAtNode.ts @@ -1,8 +1,5 @@ -import ts from "typescript"; +import type ts from "typescript"; export function symbolAtNode(node: ts.Node): ts.Symbol | undefined { - return (node as any).symbol; -} -export function localSymbolAtNode(node: ts.Node): ts.Symbol | undefined { - return (node as any).localSymbol; + return (node as ts.Declaration).symbol; } From 93a6b49f757c8c61dc1cd791726eb8ecdb851475 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Wed, 22 May 2024 20:05:57 -0300 Subject: [PATCH 6/7] lint --- src/SchemaGenerator.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SchemaGenerator.ts b/src/SchemaGenerator.ts index 1dcadbf40..2d3fbfcaf 100644 --- a/src/SchemaGenerator.ts +++ b/src/SchemaGenerator.ts @@ -10,7 +10,6 @@ import type { TypeFormatter } from "./TypeFormatter.js"; import type { StringMap } from "./Utils/StringMap.js"; import { hasJsDocTag } from "./Utils/hasJsDocTag.js"; import { removeUnreachable } from "./Utils/removeUnreachable.js"; -import { symbolAtNode } from "./Utils/symbolAtNode.js"; export class SchemaGenerator { public constructor( @@ -189,8 +188,8 @@ export class SchemaGenerator { if (ts.isImportSpecifier(declaration)) { // Handling the `Foo` in `import { Foo } from "./lib"; export { Foo };` - const importSpecifierNode = declaration as ts.ImportSpecifier; - const type = typeChecker.getTypeAtLocation(importSpecifierNode); + const type = typeChecker.getTypeAtLocation(declaration); + if (type.symbol?.declarations?.length === 1) { this.inspectNode(type.symbol.declarations[0], typeChecker, allTypes); } From eb0aeae52b201d1f3b3670046d6236fc43734eb4 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Wed, 22 May 2024 21:40:31 -0300 Subject: [PATCH 7/7] revert --- jest.config.cjs | 1 - 1 file changed, 1 deletion(-) diff --git a/jest.config.cjs b/jest.config.cjs index f104b325e..9b551660c 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -6,7 +6,6 @@ const config = { coverageDirectory: "./coverage/", collectCoverage: false, testEnvironment: "node", - maxWorkers: '50%', // speeds up tests transform: { ".*": "babel-jest", },