Skip to content

Commit dea3c9c

Browse files
authored
feat(traverse): improve type dependency analysis (#11)
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.
1 parent 88a99d4 commit dea3c9c

15 files changed

+502
-59
lines changed

ARCHITECTURE.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
- [TypeBoxTypeHandlers Optimizations](#typeboxtypehandlers-optimizations)
3434
- [Performance Testing](#performance-testing)
3535
- [Process Overview](#process-overview)
36-
- [Test-Driven Development (TDD) Approach](#test-driven-development-tdd-approach)
36+
- [Test-Driven Development](#test-driven-development)
3737
- [TDD Cycle](#tdd-cycle)
3838
- [Running Tests](#running-tests)
3939
- [TDD Workflow for New Features](#tdd-workflow-for-new-features)
@@ -77,7 +77,7 @@ The code generation process includes sophisticated import resolution and depende
7777

7878
#### DependencyCollector
7979

80-
The <mcfile name="dependency-collector.ts" path="src/utils/dependency-collector.ts"></mcfile> module implements a `DependencyCollector` class that:
80+
The <mcfile name="dependency-collector.ts" path="src/traverse/dependency-collector.ts"></mcfile> module implements a `DependencyCollector` class that:
8181

8282
- **Traverses Import Chains**: Recursively follows import declarations to collect all type dependencies from external files
8383
- **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
113113

114114
### Dependency-Ordered Processing
115115

116-
Interfaces are processed in dependency order using a topological sort algorithm implemented in <mcfile name="ts-morph-codegen.ts" path="src/ts-morph-codegen.ts"></mcfile>:
116+
The main codegen logic in <mcfile name="ts-morph-codegen.ts" path="src/ts-morph-codegen.ts"></mcfile> implements sophisticated processing order management:
117117

118-
1. **Dependency Analysis**: The `getInterfaceProcessingOrder` function analyzes all interfaces to identify inheritance relationships
119-
2. **Topological Sorting**: Interfaces are sorted to ensure base interfaces are processed before extended interfaces
120-
3. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully
118+
1. **Dependency Analysis**: Uses `InterfaceTypeDependencyAnalyzer` to analyze complex relationships between interfaces and type aliases
119+
2. **Conditional Processing**: Handles three scenarios:
120+
- Interfaces depending on type aliases only
121+
- Type aliases depending on interfaces only
122+
- Both dependencies present (three-phase processing)
123+
3. **Topological Sorting**: Ensures types are processed in correct dependency order to prevent "type not found" errors
124+
4. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully
121125

122126
### TypeBox Composite Generation
123127

@@ -126,11 +130,13 @@ Interface inheritance is implemented using TypeBox's `Type.Composite` functional
126130
- **Base Interface Reference**: Extended interfaces reference their base interfaces by name as identifiers
127131
- **Property Combination**: The `InterfaceTypeHandler` generates `Type.Composite([BaseInterface, Type.Object({...})])` for extended interfaces
128132
- **Type Safety**: Generated code maintains full TypeScript type safety through proper static type aliases
133+
- **Generic Type Parameter Handling**: Uses `TSchema` as the constraint for TypeBox compatibility instead of preserving original TypeScript constraints
129134

130135
### Implementation Details
131136

132137
- **Heritage Clause Processing**: The <mcfile name="interface-type-handler.ts" path="src/handlers/typebox/object/interface-type-handler.ts"></mcfile> processes `extends` clauses by extracting referenced type names
133138
- **Identifier Generation**: Base interface references are converted to TypeScript identifiers rather than attempting recursive type resolution
139+
- **TypeBox Constraint Normalization**: Generic type parameters use `TSchema` constraints for TypeBox schema compatibility
134140
- **Error Prevention**: The dependency ordering prevents "No handler found for type" errors that occur when extended interfaces are processed before their base interfaces
135141

136142
## Input Handling System
@@ -341,9 +347,9 @@ To ensure the dependency collection system performs efficiently under various sc
341347
5. **Static Type Generation**: Alongside each TypeBox schema, a TypeScript `type` alias is generated using `Static<typeof ...>` to provide compile-time type safety and seamless integration with existing TypeScript code.
342348
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.
343349

344-
## Test-Driven Development (TDD) Approach
350+
## Test-Driven Development
345351

346-
This project follows a Test-Driven Development methodology to ensure code quality, maintainability, and reliability. The TDD workflow consists of three main phases:
352+
This project follows a Test-Driven Development (TDD) methodology to ensure code quality, maintainability, and reliability. The TDD workflow consists of three main phases:
347353

348354
### TDD Cycle
349355

src/handlers/typebox/object/interface-type-handler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,13 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
6969
const functionTypeParams = typeParameters.map((typeParam) => {
7070
const paramName = typeParam.getName()
7171

72+
// Use TSchema as the constraint for TypeBox compatibility
73+
const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined)
74+
7275
return ts.factory.createTypeParameterDeclaration(
7376
undefined,
7477
ts.factory.createIdentifier(paramName),
75-
ts.factory.createTypeReferenceNode('TSchema', undefined),
78+
constraintNode,
7679
undefined,
7780
)
7881
})

src/handlers/typebox/type-reference-handler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler'
2+
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
23
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
34
import { Node, ts, TypeReferenceNode } from 'ts-morph'
45

@@ -9,9 +10,23 @@ export class TypeReferenceHandler extends BaseTypeHandler {
910

1011
handle(node: TypeReferenceNode): ts.Expression {
1112
const referencedType = node.getTypeName()
13+
const typeArguments = node.getTypeArguments()
1214

1315
if (Node.isIdentifier(referencedType)) {
1416
const typeName = referencedType.getText()
17+
18+
// If there are type arguments, create a function call
19+
if (typeArguments.length > 0) {
20+
const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg))
21+
22+
return ts.factory.createCallExpression(
23+
ts.factory.createIdentifier(typeName),
24+
undefined,
25+
typeBoxArgs,
26+
)
27+
}
28+
29+
// No type arguments, just return the identifier
1530
return ts.factory.createIdentifier(typeName)
1631
}
1732

src/parsers/parse-interfaces.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,13 @@ export class InterfaceParser extends BaseParser {
107107
// Create type parameters for the type alias
108108
const typeParamDeclarations = typeParameters.map((typeParam) => {
109109
const paramName = typeParam.getName()
110+
// Use TSchema as the constraint for TypeBox compatibility
111+
const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined)
112+
110113
return ts.factory.createTypeParameterDeclaration(
111114
undefined,
112115
ts.factory.createIdentifier(paramName),
113-
ts.factory.createTypeReferenceNode('TSchema', undefined),
116+
constraintNode,
114117
undefined,
115118
)
116119
})

src/traverse/ast-traversal.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { InterfaceDeclaration, Node, TypeAliasDeclaration, TypeReferenceNode } from 'ts-morph'
2+
3+
/**
4+
* AST traversal patterns used in dependency analysis
5+
*/
6+
export class ASTTraversal {
7+
private static cache = new Map<string, string[]>()
8+
9+
/**
10+
* Extract interface names referenced by a type alias
11+
*/
12+
static extractInterfaceReferences(
13+
typeAlias: TypeAliasDeclaration,
14+
interfaces: Map<string, InterfaceDeclaration>,
15+
): string[] {
16+
const typeNode = typeAlias.getTypeNode()
17+
if (!typeNode) return []
18+
19+
const cacheKey = `interface_refs_${typeNode.getText()}`
20+
const cached = ASTTraversal.cache.get(cacheKey)
21+
if (cached) return cached
22+
23+
const references: string[] = []
24+
const visited = new Set<Node>()
25+
26+
const traverse = (node: Node): void => {
27+
if (visited.has(node)) return
28+
visited.add(node)
29+
30+
// Handle type references
31+
if (Node.isTypeReference(node)) {
32+
const typeRefNode = node as TypeReferenceNode
33+
const typeName = typeRefNode.getTypeName().getText()
34+
35+
if (interfaces.has(typeName)) {
36+
references.push(typeName)
37+
}
38+
// Continue traversing to handle type arguments in generic instantiations
39+
}
40+
41+
// Use forEachChild for better performance
42+
node.forEachChild(traverse)
43+
}
44+
45+
traverse(typeNode)
46+
47+
// Cache the result
48+
ASTTraversal.cache.set(cacheKey, references)
49+
return references
50+
}
51+
52+
/**
53+
* Extract type alias names referenced by an interface (e.g., in type parameter constraints)
54+
*/
55+
static extractTypeAliasReferences(
56+
interfaceDecl: InterfaceDeclaration,
57+
typeAliases: Map<string, TypeAliasDeclaration>,
58+
): string[] {
59+
const cacheKey = `type_alias_refs_${interfaceDecl.getName()}_${interfaceDecl.getText()}`
60+
const cached = ASTTraversal.cache.get(cacheKey)
61+
if (cached) return cached
62+
63+
const references: string[] = []
64+
const visited = new Set<Node>()
65+
66+
const traverse = (node: Node): void => {
67+
if (visited.has(node)) return
68+
visited.add(node)
69+
70+
// Handle type references
71+
if (Node.isTypeReference(node)) {
72+
const typeRefNode = node as TypeReferenceNode
73+
const typeName = typeRefNode.getTypeName().getText()
74+
75+
if (typeAliases.has(typeName)) {
76+
references.push(typeName)
77+
}
78+
return // No need to traverse children of type references
79+
}
80+
81+
// Use forEachChild for better performance
82+
node.forEachChild(traverse)
83+
}
84+
85+
// Check type parameters for constraints
86+
for (const typeParam of interfaceDecl.getTypeParameters()) {
87+
const constraint = typeParam.getConstraint()
88+
if (constraint) {
89+
traverse(constraint)
90+
}
91+
}
92+
93+
// Check heritage clauses
94+
for (const heritageClause of interfaceDecl.getHeritageClauses()) {
95+
for (const typeNode of heritageClause.getTypeNodes()) {
96+
traverse(typeNode)
97+
}
98+
}
99+
100+
// Cache the result
101+
ASTTraversal.cache.set(cacheKey, references)
102+
return references
103+
}
104+
105+
/**
106+
* Clear the internal cache
107+
*/
108+
static clearCache(): void {
109+
ASTTraversal.cache.clear()
110+
}
111+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { ASTTraversal } from '@daxserver/validation-schema-codegen/traverse/ast-traversal'
2+
import { InterfaceDeclaration, TypeAliasDeclaration } from 'ts-morph'
3+
4+
/**
5+
* Dependency analyzer for determining processing order
6+
*/
7+
export class DependencyAnalyzer {
8+
/**
9+
* Extract interface names referenced by a type alias
10+
*/
11+
extractInterfaceReferences(
12+
typeAlias: TypeAliasDeclaration,
13+
interfaces: Map<string, InterfaceDeclaration>,
14+
): string[] {
15+
return ASTTraversal.extractInterfaceReferences(typeAlias, interfaces)
16+
}
17+
18+
/**
19+
* Extract type alias names referenced by an interface
20+
*/
21+
extractTypeAliasReferences(
22+
interfaceDecl: InterfaceDeclaration,
23+
typeAliases: Map<string, TypeAliasDeclaration>,
24+
): string[] {
25+
return ASTTraversal.extractTypeAliasReferences(interfaceDecl, typeAliases)
26+
}
27+
28+
/**
29+
* Check if any type aliases reference interfaces
30+
*/
31+
hasInterfaceReferences(
32+
typeAliases: TypeAliasDeclaration[],
33+
interfaces: InterfaceDeclaration[],
34+
): boolean {
35+
const interfaceMap = new Map<string, InterfaceDeclaration>()
36+
for (const iface of interfaces) {
37+
interfaceMap.set(iface.getName(), iface)
38+
}
39+
40+
for (const typeAlias of typeAliases) {
41+
const references = this.extractInterfaceReferences(typeAlias, interfaceMap)
42+
if (references.length > 0) {
43+
return true
44+
}
45+
}
46+
47+
return false
48+
}
49+
50+
/**
51+
* Get type aliases that reference interfaces, ordered by their dependencies
52+
*/
53+
getTypeAliasesReferencingInterfaces(
54+
typeAliases: TypeAliasDeclaration[],
55+
interfaces: InterfaceDeclaration[],
56+
): { typeAlias: TypeAliasDeclaration; referencedInterfaces: string[] }[] {
57+
const interfaceMap = new Map<string, InterfaceDeclaration>()
58+
for (const iface of interfaces) {
59+
interfaceMap.set(iface.getName(), iface)
60+
}
61+
62+
const result: { typeAlias: TypeAliasDeclaration; referencedInterfaces: string[] }[] = []
63+
64+
for (const typeAlias of typeAliases) {
65+
const references = this.extractInterfaceReferences(typeAlias, interfaceMap)
66+
if (references.length > 0) {
67+
result.push({
68+
typeAlias,
69+
referencedInterfaces: references,
70+
})
71+
}
72+
}
73+
74+
return result
75+
}
76+
77+
/**
78+
* Determine the correct processing order for interfaces and type aliases
79+
* Returns an object indicating which should be processed first
80+
*/
81+
analyzeProcessingOrder(
82+
typeAliases: TypeAliasDeclaration[],
83+
interfaces: InterfaceDeclaration[],
84+
): {
85+
processInterfacesFirst: boolean
86+
typeAliasesDependingOnInterfaces: string[]
87+
interfacesDependingOnTypeAliases: string[]
88+
} {
89+
const typeAliasMap = new Map<string, TypeAliasDeclaration>()
90+
const interfaceMap = new Map<string, InterfaceDeclaration>()
91+
92+
for (const typeAlias of typeAliases) {
93+
typeAliasMap.set(typeAlias.getName(), typeAlias)
94+
}
95+
96+
for (const interfaceDecl of interfaces) {
97+
interfaceMap.set(interfaceDecl.getName(), interfaceDecl)
98+
}
99+
100+
const typeAliasesDependingOnInterfaces: string[] = []
101+
const interfacesDependingOnTypeAliases: string[] = []
102+
103+
// Check type aliases that depend on interfaces
104+
for (const typeAlias of typeAliases) {
105+
const interfaceRefs = this.extractInterfaceReferences(typeAlias, interfaceMap)
106+
if (interfaceRefs.length > 0) {
107+
typeAliasesDependingOnInterfaces.push(typeAlias.getName())
108+
}
109+
}
110+
111+
// Check interfaces that depend on type aliases
112+
for (const interfaceDecl of interfaces) {
113+
const typeAliasRefs = this.extractTypeAliasReferences(interfaceDecl, typeAliasMap)
114+
if (typeAliasRefs.length > 0) {
115+
interfacesDependingOnTypeAliases.push(interfaceDecl.getName())
116+
}
117+
}
118+
119+
// Determine processing order:
120+
// If interfaces depend on type aliases, process type aliases first
121+
// If only type aliases depend on interfaces, process interfaces first
122+
// If both have dependencies, process type aliases that interfaces depend on first,
123+
// then interfaces, then type aliases that depend on interfaces
124+
const processInterfacesFirst =
125+
interfacesDependingOnTypeAliases.length === 0 && typeAliasesDependingOnInterfaces.length > 0
126+
127+
return {
128+
processInterfacesFirst,
129+
typeAliasesDependingOnInterfaces,
130+
interfacesDependingOnTypeAliases,
131+
}
132+
}
133+
134+
/**
135+
* Clear internal caches
136+
*/
137+
clearCache(): void {
138+
ASTTraversal.clearCache()
139+
}
140+
}

src/utils/dependency-collector.ts renamed to src/traverse/dependency-collector.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {
22
DefaultFileResolver,
33
type FileResolver,
4-
} from '@daxserver/validation-schema-codegen/utils/dependency-file-resolver'
4+
} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver'
55
import {
66
DefaultTypeReferenceExtractor,
77
type TypeReferenceExtractor,
8-
} from '@daxserver/validation-schema-codegen/utils/dependency-type'
8+
} from '@daxserver/validation-schema-codegen/traverse/dependency-type'
99
import { ImportDeclaration, SourceFile, TypeAliasDeclaration } from 'ts-morph'
1010

1111
export interface TypeDependency {
File renamed without changes.

src/utils/dependency-type.ts renamed to src/traverse/dependency-type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TypeDependency } from '@daxserver/validation-schema-codegen/utils/dependency-collector'
1+
import type { TypeDependency } from '@daxserver/validation-schema-codegen/traverse/dependency-collector'
22
import { Node, TypeReferenceNode } from 'ts-morph'
33

44
export interface TypeReferenceExtractor {

0 commit comments

Comments
 (0)