From ffe5781394a0559f23a6c8881a20d2b5a6a51d67 Mon Sep 17 00:00:00 2001 From: DaxServer Date: Tue, 26 Aug 2025 19:26:12 +0200 Subject: [PATCH] feat(traverse): improve type dependency analysis Improve the type dependency analysis by: - Using a dedicated `DependencyAnalyzer` class to handle the analysis - Extracting interface references from type aliases and type alias references from interfaces - Determining the correct processing order for interfaces and type aliases This ensures that the code generation process can handle complex type dependencies correctly, allowing for more robust and reliable schema generation. --- ARCHITECTURE.md | 22 ++- .../typebox/object/interface-type-handler.ts | 5 +- .../typebox/type-reference-handler.ts | 15 ++ src/parsers/parse-interfaces.ts | 5 +- src/traverse/ast-traversal.ts | 111 ++++++++++++++ src/traverse/dependency-analyzer.ts | 140 ++++++++++++++++++ .../dependency-collector.ts | 4 +- .../dependency-file-resolver.ts | 0 src/{utils => traverse}/dependency-type.ts | 2 +- src/ts-morph-codegen.ts | 126 ++++++++++++++-- .../dependency-collector.integration.test.ts | 2 +- .../dependency-collector.performance.test.ts | 2 +- tests/dependency-collector.unit.test.ts | 6 +- tests/handlers/typebox/interfaces.test.ts | 119 ++++++++++++--- tests/import-resolution.test.ts | 2 +- 15 files changed, 502 insertions(+), 59 deletions(-) create mode 100644 src/traverse/ast-traversal.ts create mode 100644 src/traverse/dependency-analyzer.ts rename src/{utils => traverse}/dependency-collector.ts (97%) rename src/{utils => traverse}/dependency-file-resolver.ts (100%) rename src/{utils => traverse}/dependency-type.ts (97%) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bda861c..575ec4c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -33,7 +33,7 @@ - [TypeBoxTypeHandlers Optimizations](#typeboxtypehandlers-optimizations) - [Performance Testing](#performance-testing) - [Process Overview](#process-overview) -- [Test-Driven Development (TDD) Approach](#test-driven-development-tdd-approach) +- [Test-Driven Development](#test-driven-development) - [TDD Cycle](#tdd-cycle) - [Running Tests](#running-tests) - [TDD Workflow for New Features](#tdd-workflow-for-new-features) @@ -77,7 +77,7 @@ The code generation process includes sophisticated import resolution and depende #### DependencyCollector -The module implements a `DependencyCollector` class that: +The module implements a `DependencyCollector` class that: - **Traverses Import Chains**: Recursively follows import declarations to collect all type dependencies from external files - **Builds Dependency Graph**: Creates a comprehensive map of type dependencies, tracking which types depend on which other types @@ -113,11 +113,15 @@ The codebase provides comprehensive support for TypeScript interface inheritance ### Dependency-Ordered Processing -Interfaces are processed in dependency order using a topological sort algorithm implemented in : +The main codegen logic in implements sophisticated processing order management: -1. **Dependency Analysis**: The `getInterfaceProcessingOrder` function analyzes all interfaces to identify inheritance relationships -2. **Topological Sorting**: Interfaces are sorted to ensure base interfaces are processed before extended interfaces -3. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully +1. **Dependency Analysis**: Uses `InterfaceTypeDependencyAnalyzer` to analyze complex relationships between interfaces and type aliases +2. **Conditional Processing**: Handles three scenarios: + - Interfaces depending on type aliases only + - Type aliases depending on interfaces only + - Both dependencies present (three-phase processing) +3. **Topological Sorting**: Ensures types are processed in correct dependency order to prevent "type not found" errors +4. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully ### TypeBox Composite Generation @@ -126,11 +130,13 @@ Interface inheritance is implemented using TypeBox's `Type.Composite` functional - **Base Interface Reference**: Extended interfaces reference their base interfaces by name as identifiers - **Property Combination**: The `InterfaceTypeHandler` generates `Type.Composite([BaseInterface, Type.Object({...})])` for extended interfaces - **Type Safety**: Generated code maintains full TypeScript type safety through proper static type aliases +- **Generic Type Parameter Handling**: Uses `TSchema` as the constraint for TypeBox compatibility instead of preserving original TypeScript constraints ### Implementation Details - **Heritage Clause Processing**: The processes `extends` clauses by extracting referenced type names - **Identifier Generation**: Base interface references are converted to TypeScript identifiers rather than attempting recursive type resolution +- **TypeBox Constraint Normalization**: Generic type parameters use `TSchema` constraints for TypeBox schema compatibility - **Error Prevention**: The dependency ordering prevents "No handler found for type" errors that occur when extended interfaces are processed before their base interfaces ## Input Handling System @@ -341,9 +347,9 @@ To ensure the dependency collection system performs efficiently under various sc 5. **Static Type Generation**: Alongside each TypeBox schema, a TypeScript `type` alias is generated using `Static` to provide compile-time type safety and seamless integration with existing TypeScript code. 6. **Output**: A new TypeScript file (as a string) containing the generated TypeBox schemas and static type aliases, ready to be written to disk or integrated into your application. -## Test-Driven Development (TDD) Approach +## Test-Driven Development -This project follows a Test-Driven Development methodology to ensure code quality, maintainability, and reliability. The TDD workflow consists of three main phases: +This project follows a Test-Driven Development (TDD) methodology to ensure code quality, maintainability, and reliability. The TDD workflow consists of three main phases: ### TDD Cycle diff --git a/src/handlers/typebox/object/interface-type-handler.ts b/src/handlers/typebox/object/interface-type-handler.ts index 91eae19..8603c57 100644 --- a/src/handlers/typebox/object/interface-type-handler.ts +++ b/src/handlers/typebox/object/interface-type-handler.ts @@ -69,10 +69,13 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler { const functionTypeParams = typeParameters.map((typeParam) => { const paramName = typeParam.getName() + // Use TSchema as the constraint for TypeBox compatibility + const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined) + return ts.factory.createTypeParameterDeclaration( undefined, ts.factory.createIdentifier(paramName), - ts.factory.createTypeReferenceNode('TSchema', undefined), + constraintNode, undefined, ) }) diff --git a/src/handlers/typebox/type-reference-handler.ts b/src/handlers/typebox/type-reference-handler.ts index e264b24..91d8a11 100644 --- a/src/handlers/typebox/type-reference-handler.ts +++ b/src/handlers/typebox/type-reference-handler.ts @@ -1,4 +1,5 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { Node, ts, TypeReferenceNode } from 'ts-morph' @@ -9,9 +10,23 @@ export class TypeReferenceHandler extends BaseTypeHandler { handle(node: TypeReferenceNode): ts.Expression { const referencedType = node.getTypeName() + const typeArguments = node.getTypeArguments() if (Node.isIdentifier(referencedType)) { const typeName = referencedType.getText() + + // If there are type arguments, create a function call + if (typeArguments.length > 0) { + const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg)) + + return ts.factory.createCallExpression( + ts.factory.createIdentifier(typeName), + undefined, + typeBoxArgs, + ) + } + + // No type arguments, just return the identifier return ts.factory.createIdentifier(typeName) } diff --git a/src/parsers/parse-interfaces.ts b/src/parsers/parse-interfaces.ts index a6360a3..d2a2a9c 100644 --- a/src/parsers/parse-interfaces.ts +++ b/src/parsers/parse-interfaces.ts @@ -107,10 +107,13 @@ export class InterfaceParser extends BaseParser { // Create type parameters for the type alias const typeParamDeclarations = typeParameters.map((typeParam) => { const paramName = typeParam.getName() + // Use TSchema as the constraint for TypeBox compatibility + const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined) + return ts.factory.createTypeParameterDeclaration( undefined, ts.factory.createIdentifier(paramName), - ts.factory.createTypeReferenceNode('TSchema', undefined), + constraintNode, undefined, ) }) diff --git a/src/traverse/ast-traversal.ts b/src/traverse/ast-traversal.ts new file mode 100644 index 0000000..83213fe --- /dev/null +++ b/src/traverse/ast-traversal.ts @@ -0,0 +1,111 @@ +import { InterfaceDeclaration, Node, TypeAliasDeclaration, TypeReferenceNode } from 'ts-morph' + +/** + * AST traversal patterns used in dependency analysis + */ +export class ASTTraversal { + private static cache = new Map() + + /** + * Extract interface names referenced by a type alias + */ + static extractInterfaceReferences( + typeAlias: TypeAliasDeclaration, + interfaces: Map, + ): string[] { + const typeNode = typeAlias.getTypeNode() + if (!typeNode) return [] + + const cacheKey = `interface_refs_${typeNode.getText()}` + const cached = ASTTraversal.cache.get(cacheKey) + if (cached) return cached + + const references: string[] = [] + const visited = new Set() + + const traverse = (node: Node): void => { + if (visited.has(node)) return + visited.add(node) + + // Handle type references + if (Node.isTypeReference(node)) { + const typeRefNode = node as TypeReferenceNode + const typeName = typeRefNode.getTypeName().getText() + + if (interfaces.has(typeName)) { + references.push(typeName) + } + // Continue traversing to handle type arguments in generic instantiations + } + + // Use forEachChild for better performance + node.forEachChild(traverse) + } + + traverse(typeNode) + + // Cache the result + ASTTraversal.cache.set(cacheKey, references) + return references + } + + /** + * Extract type alias names referenced by an interface (e.g., in type parameter constraints) + */ + static extractTypeAliasReferences( + interfaceDecl: InterfaceDeclaration, + typeAliases: Map, + ): string[] { + const cacheKey = `type_alias_refs_${interfaceDecl.getName()}_${interfaceDecl.getText()}` + const cached = ASTTraversal.cache.get(cacheKey) + if (cached) return cached + + const references: string[] = [] + const visited = new Set() + + const traverse = (node: Node): void => { + if (visited.has(node)) return + visited.add(node) + + // Handle type references + if (Node.isTypeReference(node)) { + const typeRefNode = node as TypeReferenceNode + const typeName = typeRefNode.getTypeName().getText() + + if (typeAliases.has(typeName)) { + references.push(typeName) + } + return // No need to traverse children of type references + } + + // Use forEachChild for better performance + node.forEachChild(traverse) + } + + // Check type parameters for constraints + for (const typeParam of interfaceDecl.getTypeParameters()) { + const constraint = typeParam.getConstraint() + if (constraint) { + traverse(constraint) + } + } + + // Check heritage clauses + for (const heritageClause of interfaceDecl.getHeritageClauses()) { + for (const typeNode of heritageClause.getTypeNodes()) { + traverse(typeNode) + } + } + + // Cache the result + ASTTraversal.cache.set(cacheKey, references) + return references + } + + /** + * Clear the internal cache + */ + static clearCache(): void { + ASTTraversal.cache.clear() + } +} diff --git a/src/traverse/dependency-analyzer.ts b/src/traverse/dependency-analyzer.ts new file mode 100644 index 0000000..951ed0f --- /dev/null +++ b/src/traverse/dependency-analyzer.ts @@ -0,0 +1,140 @@ +import { ASTTraversal } from '@daxserver/validation-schema-codegen/traverse/ast-traversal' +import { InterfaceDeclaration, TypeAliasDeclaration } from 'ts-morph' + +/** + * Dependency analyzer for determining processing order + */ +export class DependencyAnalyzer { + /** + * Extract interface names referenced by a type alias + */ + extractInterfaceReferences( + typeAlias: TypeAliasDeclaration, + interfaces: Map, + ): string[] { + return ASTTraversal.extractInterfaceReferences(typeAlias, interfaces) + } + + /** + * Extract type alias names referenced by an interface + */ + extractTypeAliasReferences( + interfaceDecl: InterfaceDeclaration, + typeAliases: Map, + ): string[] { + return ASTTraversal.extractTypeAliasReferences(interfaceDecl, typeAliases) + } + + /** + * Check if any type aliases reference interfaces + */ + hasInterfaceReferences( + typeAliases: TypeAliasDeclaration[], + interfaces: InterfaceDeclaration[], + ): boolean { + const interfaceMap = new Map() + for (const iface of interfaces) { + interfaceMap.set(iface.getName(), iface) + } + + for (const typeAlias of typeAliases) { + const references = this.extractInterfaceReferences(typeAlias, interfaceMap) + if (references.length > 0) { + return true + } + } + + return false + } + + /** + * Get type aliases that reference interfaces, ordered by their dependencies + */ + getTypeAliasesReferencingInterfaces( + typeAliases: TypeAliasDeclaration[], + interfaces: InterfaceDeclaration[], + ): { typeAlias: TypeAliasDeclaration; referencedInterfaces: string[] }[] { + const interfaceMap = new Map() + for (const iface of interfaces) { + interfaceMap.set(iface.getName(), iface) + } + + const result: { typeAlias: TypeAliasDeclaration; referencedInterfaces: string[] }[] = [] + + for (const typeAlias of typeAliases) { + const references = this.extractInterfaceReferences(typeAlias, interfaceMap) + if (references.length > 0) { + result.push({ + typeAlias, + referencedInterfaces: references, + }) + } + } + + return result + } + + /** + * Determine the correct processing order for interfaces and type aliases + * Returns an object indicating which should be processed first + */ + analyzeProcessingOrder( + typeAliases: TypeAliasDeclaration[], + interfaces: InterfaceDeclaration[], + ): { + processInterfacesFirst: boolean + typeAliasesDependingOnInterfaces: string[] + interfacesDependingOnTypeAliases: string[] + } { + const typeAliasMap = new Map() + const interfaceMap = new Map() + + for (const typeAlias of typeAliases) { + typeAliasMap.set(typeAlias.getName(), typeAlias) + } + + for (const interfaceDecl of interfaces) { + interfaceMap.set(interfaceDecl.getName(), interfaceDecl) + } + + const typeAliasesDependingOnInterfaces: string[] = [] + const interfacesDependingOnTypeAliases: string[] = [] + + // Check type aliases that depend on interfaces + for (const typeAlias of typeAliases) { + const interfaceRefs = this.extractInterfaceReferences(typeAlias, interfaceMap) + if (interfaceRefs.length > 0) { + typeAliasesDependingOnInterfaces.push(typeAlias.getName()) + } + } + + // Check interfaces that depend on type aliases + for (const interfaceDecl of interfaces) { + const typeAliasRefs = this.extractTypeAliasReferences(interfaceDecl, typeAliasMap) + if (typeAliasRefs.length > 0) { + interfacesDependingOnTypeAliases.push(interfaceDecl.getName()) + } + } + + // Determine processing order: + // If interfaces depend on type aliases, process type aliases first + // If only type aliases depend on interfaces, process interfaces first + // If both have dependencies, process type aliases that interfaces depend on first, + // then interfaces, then type aliases that depend on interfaces + const processInterfacesFirst = + interfacesDependingOnTypeAliases.length === 0 && typeAliasesDependingOnInterfaces.length > 0 + + return { + processInterfacesFirst, + typeAliasesDependingOnInterfaces, + interfacesDependingOnTypeAliases, + } + } + + /** + * Clear internal caches + */ + clearCache(): void { + ASTTraversal.clearCache() + } +} diff --git a/src/utils/dependency-collector.ts b/src/traverse/dependency-collector.ts similarity index 97% rename from src/utils/dependency-collector.ts rename to src/traverse/dependency-collector.ts index 24648ba..b3b0668 100644 --- a/src/utils/dependency-collector.ts +++ b/src/traverse/dependency-collector.ts @@ -1,11 +1,11 @@ import { DefaultFileResolver, type FileResolver, -} from '@daxserver/validation-schema-codegen/utils/dependency-file-resolver' +} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver' import { DefaultTypeReferenceExtractor, type TypeReferenceExtractor, -} from '@daxserver/validation-schema-codegen/utils/dependency-type' +} from '@daxserver/validation-schema-codegen/traverse/dependency-type' import { ImportDeclaration, SourceFile, TypeAliasDeclaration } from 'ts-morph' export interface TypeDependency { diff --git a/src/utils/dependency-file-resolver.ts b/src/traverse/dependency-file-resolver.ts similarity index 100% rename from src/utils/dependency-file-resolver.ts rename to src/traverse/dependency-file-resolver.ts diff --git a/src/utils/dependency-type.ts b/src/traverse/dependency-type.ts similarity index 97% rename from src/utils/dependency-type.ts rename to src/traverse/dependency-type.ts index 29ef00b..0bc1fb4 100644 --- a/src/utils/dependency-type.ts +++ b/src/traverse/dependency-type.ts @@ -1,4 +1,4 @@ -import type { TypeDependency } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import type { TypeDependency } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { Node, TypeReferenceNode } from 'ts-morph' export interface TypeReferenceExtractor { diff --git a/src/ts-morph-codegen.ts b/src/ts-morph-codegen.ts index 8105a5c..f6611ef 100644 --- a/src/ts-morph-codegen.ts +++ b/src/ts-morph-codegen.ts @@ -6,7 +6,8 @@ import { EnumParser } from '@daxserver/validation-schema-codegen/parsers/parse-e import { FunctionDeclarationParser } from '@daxserver/validation-schema-codegen/parsers/parse-function-declarations' import { InterfaceParser } from '@daxserver/validation-schema-codegen/parsers/parse-interfaces' import { TypeAliasParser } from '@daxserver/validation-schema-codegen/parsers/parse-type-aliases' -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyAnalyzer } from '@daxserver/validation-schema-codegen/traverse/dependency-analyzer' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { getInterfaceProcessingOrder } from '@daxserver/validation-schema-codegen/utils/interface-processing-order' import { Project, ts } from 'ts-morph' @@ -68,10 +69,38 @@ export const generateCode = async ({ const interfaceParser = new InterfaceParser(parserOptions) const functionDeclarationParser = new FunctionDeclarationParser(parserOptions) const dependencyCollector = new DependencyCollector() + const dependencyAnalyzer = new DependencyAnalyzer() // Collect all dependencies in correct order const importDeclarations = sourceFile.getImportDeclarations() const localTypeAliases = sourceFile.getTypeAliases() + const interfaces = sourceFile.getInterfaces() + + // Analyze cross-dependencies between interfaces and type aliases + const dependencyAnalysis = dependencyAnalyzer.analyzeProcessingOrder(localTypeAliases, interfaces) + + // Handle different dependency scenarios: + // 1. If interfaces depend on type aliases, process those type aliases first + // 2. If only type aliases depend on interfaces, process interfaces first + // 3. If both scenarios exist, process in order: type aliases interfaces depend on -> interfaces -> type aliases that depend on interfaces + + const hasInterfacesDependingOnTypeAliases = + dependencyAnalysis.interfacesDependingOnTypeAliases.length > 0 + const hasTypeAliasesDependingOnInterfaces = + dependencyAnalysis.typeAliasesDependingOnInterfaces.length > 0 + + if (hasInterfacesDependingOnTypeAliases && !hasTypeAliasesDependingOnInterfaces) { + // Case 1: Only interfaces depend on type aliases - process type aliases first (normal order) + // This will be handled by the normal dependency collection below + } else if (!hasInterfacesDependingOnTypeAliases && hasTypeAliasesDependingOnInterfaces) { + // Case 2: Only type aliases depend on interfaces - process interfaces first + getInterfaceProcessingOrder(interfaces).forEach((i) => { + interfaceParser.parse(i) + }) + } else if (hasInterfacesDependingOnTypeAliases && hasTypeAliasesDependingOnInterfaces) { + // Case 3: Both dependencies exist - process type aliases that interfaces depend on first + // This will be handled by the normal dependency collection below, then interfaces, then remaining type aliases + } // Always add local types first so they can be included in topological sort dependencyCollector.addLocalTypes(localTypeAliases, sourceFile) @@ -81,20 +110,57 @@ export const generateCode = async ({ exportEverything, ) - // Process all dependencies (both imported and local) in topological order - orderedDependencies.forEach((dependency) => { - if (!processedTypes.has(dependency.typeAlias.getName())) { - typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) - } - }) - - // Process any remaining local types that weren't included in the dependency graph - if (exportEverything) { - localTypeAliases.forEach((typeAlias) => { - if (!processedTypes.has(typeAlias.getName())) { - typeAliasParser.parseWithImportFlag(typeAlias, false) + if (!hasInterfacesDependingOnTypeAliases && hasTypeAliasesDependingOnInterfaces) { + // Case 2: Only process type aliases that don't depend on interfaces + orderedDependencies.forEach((dependency) => { + const dependsOnInterface = dependencyAnalysis.typeAliasesDependingOnInterfaces.includes( + dependency.typeAlias.getName(), + ) + if (!dependsOnInterface && !processedTypes.has(dependency.typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) } }) + } else if (hasInterfacesDependingOnTypeAliases && hasTypeAliasesDependingOnInterfaces) { + // Case 3: Process only type aliases that interfaces depend on (phase 1) + orderedDependencies.forEach((dependency) => { + const interfaceDependsOnThis = dependencyAnalysis.interfacesDependingOnTypeAliases.some( + (interfaceName) => { + const interfaceDecl = interfaces.find((i) => i.getName() === interfaceName) + if (!interfaceDecl) return false + const typeAliasRefs = dependencyAnalyzer.extractTypeAliasReferences( + interfaceDecl, + new Map(localTypeAliases.map((ta) => [ta.getName(), ta])), + ) + return typeAliasRefs.includes(dependency.typeAlias.getName()) + }, + ) + const dependsOnInterface = dependencyAnalysis.typeAliasesDependingOnInterfaces.includes( + dependency.typeAlias.getName(), + ) + if ( + interfaceDependsOnThis && + !dependsOnInterface && + !processedTypes.has(dependency.typeAlias.getName()) + ) { + typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) + } + }) + } else { + // Case 1: Process all dependencies (both imported and local) in topological order + orderedDependencies.forEach((dependency) => { + if (!processedTypes.has(dependency.typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) + } + }) + + // Process any remaining local types that weren't included in the dependency graph + if (exportEverything) { + localTypeAliases.forEach((typeAlias) => { + if (!processedTypes.has(typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(typeAlias, false) + } + }) + } } // Process enums @@ -103,9 +169,37 @@ export const generateCode = async ({ }) // Process interfaces in dependency order - getInterfaceProcessingOrder(sourceFile.getInterfaces()).forEach((i) => { - interfaceParser.parse(i) - }) + if ( + hasInterfacesDependingOnTypeAliases || + (!hasInterfacesDependingOnTypeAliases && !hasTypeAliasesDependingOnInterfaces) + ) { + // Case 1 and Case 3: Process interfaces after type aliases they depend on + getInterfaceProcessingOrder(interfaces).forEach((i) => { + interfaceParser.parse(i) + }) + } + // Case 2: Interfaces were already processed above + + // Process remaining type aliases that depend on interfaces (Case 2 and Case 3) + if (hasTypeAliasesDependingOnInterfaces) { + orderedDependencies.forEach((dependency) => { + const dependsOnInterface = dependencyAnalysis.typeAliasesDependingOnInterfaces.includes( + dependency.typeAlias.getName(), + ) + if (dependsOnInterface && !processedTypes.has(dependency.typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported) + } + }) + + // Process any remaining local types that weren't included in the dependency graph + if (exportEverything) { + localTypeAliases.forEach((typeAlias) => { + if (!processedTypes.has(typeAlias.getName())) { + typeAliasParser.parseWithImportFlag(typeAlias, false) + } + }) + } + } // Process function declarations sourceFile.getFunctions().forEach((f) => { diff --git a/tests/dependency-collector.integration.test.ts b/tests/dependency-collector.integration.test.ts index 1a8bcab..9132d3d 100644 --- a/tests/dependency-collector.integration.test.ts +++ b/tests/dependency-collector.integration.test.ts @@ -1,4 +1,4 @@ -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { createSourceFile } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' diff --git a/tests/dependency-collector.performance.test.ts b/tests/dependency-collector.performance.test.ts index 44e8099..a702723 100644 --- a/tests/dependency-collector.performance.test.ts +++ b/tests/dependency-collector.performance.test.ts @@ -1,4 +1,4 @@ -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { createSourceFile } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' diff --git a/tests/dependency-collector.unit.test.ts b/tests/dependency-collector.unit.test.ts index 963146f..a25e525 100644 --- a/tests/dependency-collector.unit.test.ts +++ b/tests/dependency-collector.unit.test.ts @@ -1,12 +1,12 @@ -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { DefaultFileResolver, type FileResolver, -} from '@daxserver/validation-schema-codegen/utils/dependency-file-resolver' +} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver' import { DefaultTypeReferenceExtractor, type TypeReferenceExtractor, -} from '@daxserver/validation-schema-codegen/utils/dependency-type' +} from '@daxserver/validation-schema-codegen/traverse/dependency-type' import { describe, expect, mock, test } from 'bun:test' import type { ImportDeclaration, SourceFile, TypeAliasDeclaration, TypeNode } from 'ts-morph' diff --git a/tests/handlers/typebox/interfaces.test.ts b/tests/handlers/typebox/interfaces.test.ts index 2be4b04..a8aa1aa 100644 --- a/tests/handlers/typebox/interfaces.test.ts +++ b/tests/handlers/typebox/interfaces.test.ts @@ -201,34 +201,105 @@ describe('Interfaces', () => { ) }) - test('generic types', () => { - const sourceFile = createSourceFile( - project, - ` - interface A { a: T } - interface B extends A { b: number } - `, - ) - - expect(generateFormattedCode(sourceFile)).resolves.toBe( - formatWithPrettier( + describe('generic types', () => { + test('generic types', () => { + const sourceFile = createSourceFile( + project, ` - const A = (T: T) => Type.Object({ - a: T - }); + interface A { a: T } + interface B extends A { b: number } + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier( + ` + const A = (T: T) => Type.Object({ + a: T + }); + + type A = Static>>; + + const B = Type.Composite([A(Type.Number()), Type.Object({ + b: Type.Number() + })]); + + type B = Static; + `, + true, + true, + ), + ) + }) + + test('generic types extension', () => { + const sourceFile = createSourceFile( + project, + ` + interface A { a: T } + interface B extends A { b: T } + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier( + ` + const A = (T: T) => Type.Object({ + a: T + }); + + type A = Static>>; + + const B = (T: T) => Type.Composite([A(T), Type.Object({ + b: T + })]); + + type B = Static>>; + `, + true, + true, + ), + ) + }) + + test('generic types with extended type', () => { + const sourceFile = createSourceFile( + project, + ` + declare const A: readonly ["a", "b"] + type A = typeof A[number] + interface B { a: T } + type C = B<'a'> + type D = B<'b'> + `, + ) - type A = Static>>; + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier( + ` + const A = Type.Union([Type.Literal('a'), Type.Literal('b')]) - const B = Type.Composite([A(Type.Number()), Type.Object({ - b: Type.Number() - })]); + type A = Static - type B = Static; - `, - true, - true, - ), - ) + const B = (T: T) => Type.Object({ + a: T + }) + + type B = Static>> + + const C = B(Type.Literal('a')) + + type C = Static + + const D = B(Type.Literal('b')) + + type D = Static + `, + true, + true, + ), + ) + }) }) }) }) diff --git a/tests/import-resolution.test.ts b/tests/import-resolution.test.ts index 173e343..a055bd8 100644 --- a/tests/import-resolution.test.ts +++ b/tests/import-resolution.test.ts @@ -1,4 +1,4 @@ -import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector' +import { DependencyCollector } from '@daxserver/validation-schema-codegen/traverse/dependency-collector' import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph'