Skip to content

Commit 63ae38f

Browse files
authored
feat(ts-morph-codegen): Implement interface processing order (#7)
This change introduces a new utility function `getInterfaceProcessingOrder` that determines the order in which interfaces should be processed. This is necessary to ensure that interfaces that depend on other interfaces are processed in the correct order. The changes include: - Importing the `getInterfaceProcessingOrder` function from the `@daxserver/validation-schema-codegen/utils/interface-processing-order` module. - Updating the code that processes interfaces to use the new function to determine the processing order. - Refactoring the interface processing loop to use `forEach` instead of a `for` loop. These changes improve the reliability and maintainability of the code generation process by ensuring that interfaces are processed in the correct order.
1 parent 58a5261 commit 63ae38f

File tree

9 files changed

+336
-44
lines changed

9 files changed

+336
-44
lines changed

ARCHITECTURE.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ The main logic for code generation resides in the <mcfile name="ts-morph-codegen
6161

6262
6. **Type Alias Processing**: The `TypeAliasParser` is instantiated and iterates through all `type alias` declarations in the input `sourceFile`. For each type alias, its underlying type node is converted into a TypeBox-compatible type representation, a TypeBox schema is generated, and a corresponding static type alias is added.
6363

64-
7. **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. For each interface, its properties and methods are converted into TypeBox object schemas with corresponding static type aliases.
64+
7. **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. Interfaces are processed in dependency order to handle inheritance properly. For each interface, its properties and methods are converted into TypeBox object schemas. Interfaces that extend other interfaces are generated using `Type.Intersect` to combine the base interface with additional properties, ensuring proper inheritance semantics.
6565

6666
8. **Function Declaration Processing**: The `FunctionDeclarationParser` is instantiated and iterates through all function declarations in the input `sourceFile`. For each function, its parameters, optional parameters, and return type are converted into TypeBox function schemas with corresponding static type aliases.
6767

@@ -103,6 +103,32 @@ The import resolution process works in two phases:
103103

104104
This approach ensures that complex import scenarios work correctly and generated code compiles without dependency errors.
105105

106+
## Interface Inheritance Support
107+
108+
The codebase provides comprehensive support for TypeScript interface inheritance through a sophisticated dependency resolution and code generation system:
109+
110+
### Dependency-Ordered Processing
111+
112+
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>:
113+
114+
1. **Dependency Analysis**: The `getInterfaceProcessingOrder` function analyzes all interfaces to identify inheritance relationships
115+
2. **Topological Sorting**: Interfaces are sorted to ensure base interfaces are processed before extended interfaces
116+
3. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully
117+
118+
### TypeBox.Composite Generation
119+
120+
Interface inheritance is implemented using TypeBox's `Type.Composite` functionality:
121+
122+
- **Base Interface Reference**: Extended interfaces reference their base interfaces by name as identifiers
123+
- **Property Combination**: The `InterfaceTypeHandler` generates `Type.Composite([BaseInterface, Type.Object({...})])` for extended interfaces
124+
- **Type Safety**: Generated code maintains full TypeScript type safety through proper static type aliases
125+
126+
### Implementation Details
127+
128+
- **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
129+
- **Identifier Generation**: Base interface references are converted to TypeScript identifiers rather than attempting recursive type resolution
130+
- **Error Prevention**: The dependency ordering prevents "No handler found for type" errors that occur when extended interfaces are processed before their base interfaces
131+
106132
## Input Handling System
107133

108134
The <mcfile name="input-handler.ts" path="src/input-handler.ts"></mcfile> module provides flexible input processing capabilities for the code generation system. It supports multiple input methods and handles various edge cases related to file resolution and import validation.
@@ -191,7 +217,7 @@ This directory contains a collection of specialized handler modules, each respon
191217
**Object-Like Type Handlers** (extend `ObjectLikeBaseHandler`):
192218

193219
- <mcfile name="object-type-handler.ts" path="src/handlers/typebox/object/object-type-handler.ts"></mcfile>: Handles TypeScript object types and type literals.
194-
- <mcfile name="interface-type-handler.ts" path="src/handlers/typebox/object/interface-type-handler.ts"></mcfile>: Handles TypeScript interface declarations.
220+
- <mcfile name="interface-type-handler.ts" path="src/handlers/typebox/object/interface-type-handler.ts"></mcfile>: Handles TypeScript interface declarations, including support for interface inheritance using `Type.Composite` to combine base interfaces with extended properties.
195221

196222
**Collection Type Handlers** (extend `CollectionBaseHandler`):
197223

@@ -227,6 +253,7 @@ This directory contains a collection of parser classes, each extending the `Base
227253
- <mcfile name="parse-imports.ts" path="src/parsers/parse-imports.ts"></mcfile>: Implements the `ImportParser` class, responsible for resolving and processing TypeScript import declarations.
228254
- <mcfile name="parse-type-aliases.ts" path="src/parsers/parse-type-aliases.ts"></mcfile>: Implements the `TypeAliasParser` class, responsible for processing TypeScript `type alias` declarations.
229255
- <mcfile name="parse-function-declarations.ts" path="src/parsers/parse-function-declarations.ts"></mcfile>: Implements the `FunctionDeclarationParser` class, responsible for processing TypeScript function declarations and converting them to TypeBox function schemas.
256+
- <mcfile name="parse-interfaces.ts" path="src/parsers/parse-interfaces.ts"></mcfile>: Implements the `InterfaceParser` class, responsible for processing TypeScript interface declarations with support for inheritance through dependency ordering and `Type.Composite` generation.
230257

231258
### Performance Considerations
232259

bun.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"devDependencies": {
77
"@eslint/js": "^9.33.0",
88
"@prettier/sync": "^0.6.1",
9+
"@sinclair/typebox-codegen": "^0.11.1",
910
"@types/bun": "^1.2.20",
1011
"@typescript-eslint/eslint-plugin": "^8.40.0",
1112
"@typescript-eslint/parser": "^8.40.0",
@@ -67,6 +68,10 @@
6768

6869
"@prettier/sync": ["@prettier/sync@0.6.1", "", { "dependencies": { "make-synchronized": "^0.8.0" }, "peerDependencies": { "prettier": "*" } }, "sha512-yF9G8vK/LYUTF3Cijd7VC9La3b20F20/J/fgoR4H0B8JGOWnZVZX6+I6+vODPosjmMcpdlUV+gUqJQZp3kLOcw=="],
6970

71+
"@sinclair/typebox": ["@sinclair/typebox@0.33.22", "", {}, "sha512-auUj4k+f4pyrIVf4GW5UKquSZFHJWri06QgARy9C0t9ZTjJLIuNIrr1yl9bWcJWJ1Gz1vOvYN1D+QPaIlNMVkQ=="],
72+
73+
"@sinclair/typebox-codegen": ["@sinclair/typebox-codegen@0.11.1", "", { "dependencies": { "@sinclair/typebox": "^0.33.1", "prettier": "^2.8.7", "typescript": "^5.4.5" } }, "sha512-Bckbrf1sJFTIVD88PvI0vWUfE3Sh/6pwu6Jov+6xyMrEqnabOxEFAmPSDWjB1FGPL5C1/HfdScwa1imwAtGi9w=="],
74+
7075
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
7176

7277
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
@@ -313,6 +318,8 @@
313318

314319
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
315320

321+
"@sinclair/typebox-codegen/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="],
322+
316323
"@ts-morph/common/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
317324

318325
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
"devDependencies": {
77
"@eslint/js": "^9.33.0",
88
"@prettier/sync": "^0.6.1",
9+
"@sinclair/typebox-codegen": "^0.11.1",
910
"@types/bun": "^1.2.20",
1011
"@typescript-eslint/eslint-plugin": "^8.40.0",
1112
"@typescript-eslint/parser": "^8.40.0",
1213
"eslint": "^9.33.0",
1314
"eslint-config-prettier": "^10.1.8",
14-
"eslint-plugin-prettier": "^5.5.4",
1515
"eslint-plugin-no-relative-import-paths": "^1.6.1",
16+
"eslint-plugin-prettier": "^5.5.4",
1617
"globals": "^16.3.0",
1718
"jiti": "^2.5.1",
1819
"prettier": "^3.6.2",
Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ObjectLikeBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-like-base-handler'
2+
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
23
import { InterfaceDeclaration, Node, ts } from 'ts-morph'
34

45
export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
@@ -7,6 +8,32 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
78
}
89

910
handle(node: InterfaceDeclaration): ts.Expression {
10-
return this.createObjectType(this.processProperties(node.getProperties()))
11+
const heritageClauses = node.getHeritageClauses()
12+
const baseObjectType = this.createObjectType(this.processProperties(node.getProperties()))
13+
14+
if (heritageClauses.length === 0) {
15+
return baseObjectType
16+
}
17+
18+
const extendedTypes: ts.Expression[] = []
19+
20+
for (const heritageClause of heritageClauses) {
21+
if (heritageClause.getToken() === ts.SyntaxKind.ExtendsKeyword) {
22+
for (const typeNode of heritageClause.getTypeNodes()) {
23+
// For interface inheritance, we reference the already processed interface by name
24+
const referencedTypeName = typeNode.getText()
25+
extendedTypes.push(ts.factory.createIdentifier(referencedTypeName))
26+
}
27+
}
28+
}
29+
30+
if (extendedTypes.length === 0) {
31+
return baseObjectType
32+
}
33+
34+
// Create composite with extended types first, then the current interface
35+
const allTypes = [...extendedTypes, baseObjectType]
36+
37+
return makeTypeCall('Composite', [ts.factory.createArrayLiteralExpression(allTypes, true)])
1138
}
1239
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ export class TypeBoxTypeHandlers {
6868
this.syntaxKindHandlers.set(SyntaxKind.TupleType, tupleTypeHandler)
6969
this.syntaxKindHandlers.set(SyntaxKind.UnionType, unionTypeHandler)
7070
this.syntaxKindHandlers.set(SyntaxKind.IntersectionType, intersectionTypeHandler)
71-
// TypeOperator handling moved to fallback handlers for specific operator types
7271
this.syntaxKindHandlers.set(SyntaxKind.IndexedAccessType, indexedAccessTypeHandler)
7372
this.syntaxKindHandlers.set(SyntaxKind.InterfaceDeclaration, interfaceTypeHandler)
7473
this.syntaxKindHandlers.set(SyntaxKind.FunctionType, functionTypeHandler)

src/ts-morph-codegen.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FunctionDeclarationParser } from '@daxserver/validation-schema-codegen/
77
import { InterfaceParser } from '@daxserver/validation-schema-codegen/parsers/parse-interfaces'
88
import { TypeAliasParser } from '@daxserver/validation-schema-codegen/parsers/parse-type-aliases'
99
import { DependencyCollector } from '@daxserver/validation-schema-codegen/utils/dependency-collector'
10+
import { getInterfaceProcessingOrder } from '@daxserver/validation-schema-codegen/utils/interface-processing-order'
1011
import { Project, ts } from 'ts-morph'
1112

1213
export interface GenerateCodeOptions extends InputOptions {
@@ -67,34 +68,34 @@ export const generateCode = async ({
6768
)
6869

6970
// Process all dependencies (both imported and local) in topological order
70-
for (const dependency of orderedDependencies) {
71+
orderedDependencies.forEach((dependency) => {
7172
if (!processedTypes.has(dependency.typeAlias.getName())) {
7273
typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported)
7374
}
74-
}
75+
})
7576

7677
// Process any remaining local types that weren't included in the dependency graph
7778
if (exportEverything) {
78-
for (const typeAlias of localTypeAliases) {
79+
localTypeAliases.forEach((typeAlias) => {
7980
if (!processedTypes.has(typeAlias.getName())) {
8081
typeAliasParser.parseWithImportFlag(typeAlias, false)
8182
}
82-
}
83+
})
8384
}
8485

8586
// Process enums
86-
sourceFile.getEnums().forEach((enumDeclaration) => {
87-
enumParser.parse(enumDeclaration)
87+
sourceFile.getEnums().forEach((e) => {
88+
enumParser.parse(e)
8889
})
8990

90-
// Process interfaces
91-
sourceFile.getInterfaces().forEach((interfaceDeclaration) => {
92-
interfaceParser.parse(interfaceDeclaration)
91+
// Process interfaces in dependency order
92+
getInterfaceProcessingOrder(sourceFile.getInterfaces()).forEach((i) => {
93+
interfaceParser.parse(i)
9394
})
9495

9596
// Process function declarations
96-
sourceFile.getFunctions().forEach((functionDeclaration) => {
97-
functionDeclarationParser.parse(functionDeclaration)
97+
sourceFile.getFunctions().forEach((f) => {
98+
functionDeclarationParser.parse(f)
9899
})
99100

100101
return newSourceFile.getFullText()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ts, type InterfaceDeclaration } from 'ts-morph'
2+
3+
export const getInterfaceProcessingOrder = (
4+
interfaces: InterfaceDeclaration[],
5+
): InterfaceDeclaration[] => {
6+
const interfaceMap = new Map<string, InterfaceDeclaration>()
7+
const visited = new Set<string>()
8+
const visiting = new Set<string>()
9+
const processingOrder: InterfaceDeclaration[] = []
10+
11+
// Build interface map
12+
interfaces.forEach((iface) => {
13+
interfaceMap.set(iface.getName(), iface)
14+
})
15+
16+
const visit = (interfaceName: string): void => {
17+
if (visited.has(interfaceName) || visiting.has(interfaceName)) {
18+
return
19+
}
20+
21+
const iface = interfaceMap.get(interfaceName)
22+
if (!iface) {
23+
return
24+
}
25+
26+
visiting.add(interfaceName)
27+
28+
// Process heritage clauses (extends)
29+
const heritageClauses = iface.getHeritageClauses()
30+
heritageClauses.forEach((heritageClause) => {
31+
if (heritageClause.getToken() !== ts.SyntaxKind.ExtendsKeyword) {
32+
return
33+
}
34+
35+
heritageClause.getTypeNodes().forEach((typeNode) => {
36+
const baseInterfaceName = typeNode.getText()
37+
if (interfaceMap.has(baseInterfaceName)) {
38+
visit(baseInterfaceName)
39+
}
40+
})
41+
})
42+
43+
visiting.delete(interfaceName)
44+
visited.add(interfaceName)
45+
processingOrder.push(iface)
46+
}
47+
48+
// Visit all interfaces
49+
interfaces.forEach((iface) => {
50+
visit(iface.getName())
51+
})
52+
53+
return processingOrder
54+
}

0 commit comments

Comments
 (0)