Skip to content

Commit cef66dc

Browse files
authored
feat: Add resolver store for type name management (#21)
This change introduces a `ResolverStore` class to manage the mapping between original type names, qualified type names, and type aliases. The key features of this change are: - Generates a unique qualified name for each type based on the original name and the source file path to prevent naming conflicts. - Maintains indexes for looking up type mappings by original name, qualified name, and alias name. - Provides methods to add, retrieve, and resolve type mappings. - Caches the generated qualified names to improve performance. - Clears the resolver store in the test setup to ensure a clean state for each test. The `resolverStore` is used in the `DependencyTraversal` tests to ensure that types are properly resolved and ordered based on their dependencies.
1 parent b363896 commit cef66dc

23 files changed

Lines changed: 1085 additions & 127 deletions

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

Lines changed: 5 additions & 1 deletion
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 { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store'
23
import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call'
34
import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils'
45
import { Node, ts, TypeReferenceNode } from 'ts-morph'
@@ -13,7 +14,10 @@ export class TypeReferenceHandler extends BaseTypeHandler {
1314
const typeArguments = node.getTypeArguments()
1415

1516
if (Node.isIdentifier(referencedType)) {
16-
const typeName = referencedType.getText()
17+
const originalTypeName = referencedType.getText()
18+
19+
// Use the ResolverStore to get the alias name if available
20+
const typeName = resolverStore.resolveAliasName(originalTypeName)
1721

1822
// If there are type arguments, create a function call
1923
if (typeArguments.length > 0) {

src/parsers/parse-enums.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/a
33
import { EnumDeclaration, VariableDeclarationKind } from 'ts-morph'
44

55
export class EnumParser extends BaseParser {
6-
parse(enumDeclaration: EnumDeclaration): void {
7-
const enumName = enumDeclaration.getName()
6+
parse(enumDeclaration: EnumDeclaration, aliasName?: string): void {
7+
const enumName = aliasName || enumDeclaration.getName()
88
const schemaName = `${enumName}Schema`
99

1010
this.newSourceFile.addEnum({

src/parsers/parse-function-declarations.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox
55
import { FunctionDeclaration, ts, VariableDeclarationKind } from 'ts-morph'
66

77
export class FunctionDeclarationParser extends BaseParser {
8-
parse(functionDecl: FunctionDeclaration): void {
9-
const functionName = functionDecl.getName()
10-
if (!functionName) return
8+
parse(functionDecl: FunctionDeclaration, aliasName?: string): void {
9+
const originalFunctionName = functionDecl.getName()
10+
if (!originalFunctionName) return
11+
12+
const functionName = aliasName || originalFunctionName
1113

1214
if (this.processedTypes.has(functionName)) return
1315
this.processedTypes.add(functionName)

src/parsers/parse-interfaces.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typeb
55
import { InterfaceDeclaration, ts } from 'ts-morph'
66

77
export class InterfaceParser extends BaseParser {
8-
parse(interfaceDecl: InterfaceDeclaration): void {
9-
const interfaceName = interfaceDecl.getName()
8+
parse(interfaceDecl: InterfaceDeclaration, aliasName?: string): void {
9+
const interfaceName = aliasName || interfaceDecl.getName()
1010

1111
if (this.processedTypes.has(interfaceName)) return
1212
this.processedTypes.add(interfaceName)
@@ -15,15 +15,13 @@ export class InterfaceParser extends BaseParser {
1515

1616
// Check if interface has type parameters (generic)
1717
if (typeParameters.length > 0) {
18-
this.parseGenericInterface(interfaceDecl)
18+
this.parseGenericInterface(interfaceDecl, interfaceName)
1919
} else {
20-
this.parseRegularInterface(interfaceDecl)
20+
this.parseRegularInterface(interfaceDecl, interfaceName)
2121
}
2222
}
2323

24-
private parseRegularInterface(interfaceDecl: InterfaceDeclaration): void {
25-
const interfaceName = interfaceDecl.getName()
26-
24+
private parseRegularInterface(interfaceDecl: InterfaceDeclaration, interfaceName: string): void {
2725
// Generate TypeBox type definition
2826
const typeboxTypeNode = getTypeBoxType(interfaceDecl)
2927
const typeboxType = this.printer.printNode(
@@ -42,8 +40,7 @@ export class InterfaceParser extends BaseParser {
4240
)
4341
}
4442

45-
private parseGenericInterface(interfaceDecl: InterfaceDeclaration): void {
46-
const interfaceName = interfaceDecl.getName()
43+
private parseGenericInterface(interfaceDecl: InterfaceDeclaration, interfaceName: string): void {
4744
const typeParameters = interfaceDecl.getTypeParameters()
4845

4946
// Generate TypeBox function definition using the same flow as type aliases

src/parsers/parse-type-aliases.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox
66
import { ts, TypeAliasDeclaration } from 'ts-morph'
77

88
export class TypeAliasParser extends BaseParser {
9-
parse(typeAlias: TypeAliasDeclaration): void {
10-
const typeName = typeAlias.getName()
9+
parse(typeAlias: TypeAliasDeclaration, aliasName?: string): void {
10+
const typeName = aliasName || typeAlias.getName()
1111

1212
if (this.processedTypes.has(typeName)) return
1313
this.processedTypes.add(typeName)
@@ -16,15 +16,13 @@ export class TypeAliasParser extends BaseParser {
1616

1717
// Check if type alias has type parameters (generic)
1818
if (typeParameters.length > 0) {
19-
this.parseGenericTypeAlias(typeAlias)
19+
this.parseGenericTypeAlias(typeAlias, typeName)
2020
} else {
21-
this.parseRegularTypeAlias(typeAlias)
21+
this.parseRegularTypeAlias(typeAlias, typeName)
2222
}
2323
}
2424

25-
private parseRegularTypeAlias(typeAlias: TypeAliasDeclaration): void {
26-
const typeName = typeAlias.getName()
27-
25+
private parseRegularTypeAlias(typeAlias: TypeAliasDeclaration, typeName: string): void {
2826
const typeNode = typeAlias.getTypeNode()
2927
const typeboxTypeNode = typeNode ? getTypeBoxType(typeNode) : makeTypeCall('Any')
3028
const typeboxType = this.printer.printNode(
@@ -38,8 +36,7 @@ export class TypeAliasParser extends BaseParser {
3836
addStaticTypeAlias(this.newSourceFile, typeName, this.newSourceFile.compilerNode, this.printer)
3937
}
4038

41-
private parseGenericTypeAlias(typeAlias: TypeAliasDeclaration): void {
42-
const typeName = typeAlias.getName()
39+
private parseGenericTypeAlias(typeAlias: TypeAliasDeclaration, typeName: string): void {
4340
const typeParameters = typeAlias.getTypeParameters()
4441

4542
// Generate TypeBox function definition

src/printer/typebox-printer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,23 @@ export class TypeBoxPrinter {
3737
}
3838

3939
printNode(traversedNode: TraversedNode): void {
40-
const { node } = traversedNode
40+
const { node, aliasName } = traversedNode
4141

4242
switch (true) {
4343
case Node.isTypeAliasDeclaration(node):
44-
this.typeAliasParser.parse(node)
44+
this.typeAliasParser.parse(node, aliasName)
4545
break
4646

4747
case Node.isInterfaceDeclaration(node):
48-
this.interfaceParser.parse(node)
48+
this.interfaceParser.parse(node, aliasName)
4949
break
5050

5151
case Node.isEnumDeclaration(node):
52-
this.enumParser.parse(node)
52+
this.enumParser.parse(node, aliasName)
5353
break
5454

5555
case Node.isFunctionDeclaration(node):
56-
this.functionParser.parse(node)
56+
this.functionParser.parse(node, aliasName)
5757
break
5858

5959
default:

src/traverse/dependency-extractor.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph'
22
import { TypeReferenceExtractor } from '@daxserver/validation-schema-codegen/traverse/type-reference-extractor'
3+
import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store'
34
import {
45
EnumDeclaration,
56
FunctionDeclaration,
@@ -11,7 +12,7 @@ import {
1112
export const extractDependencies = (nodeGraph: NodeGraph, requiredNodeIds: Set<string>): void => {
1213
const processedNodes = new Set<string>()
1314
const nodesToProcess = new Set(requiredNodeIds)
14-
const typeReferenceExtractor = new TypeReferenceExtractor(nodeGraph)
15+
const typeReferenceExtractor = new TypeReferenceExtractor()
1516

1617
// Process nodes iteratively until no new dependencies are found
1718
while (nodesToProcess.size > 0) {
@@ -44,7 +45,7 @@ export const extractDependencies = (nodeGraph: NodeGraph, requiredNodeIds: Set<s
4445
const typeReferences = typeReferenceExtractor.extractTypeReferences(nodeToAnalyze)
4546

4647
for (const referencedType of typeReferences) {
47-
if (nodeGraph.hasNode(referencedType)) {
48+
if (resolverStore.hasQualifiedName(referencedType) && nodeGraph.hasNode(referencedType)) {
4849
// Only add to required if not already processed
4950
if (!requiredNodeIds.has(referencedType)) {
5051
requiredNodeIds.add(referencedType)

src/traverse/dependency-traversal.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
GraphVisualizer,
99
type VisualizationOptions,
1010
} from '@daxserver/validation-schema-codegen/utils/graph-visualizer'
11+
import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store'
1112
import { hasCycle, topologicalSort } from 'graphology-dag'
1213
import { SourceFile } from 'ts-morph'
1314

@@ -16,14 +17,17 @@ export class DependencyTraversal {
1617
private nodeGraph = new NodeGraph()
1718
private maincodeNodeIds = new Set<string>()
1819
private requiredNodeIds = new Set<string>()
19-
private importCollector = new ImportCollector(this.fileGraph, this.nodeGraph)
20+
private importCollector: ImportCollector
21+
constructor() {
22+
this.importCollector = new ImportCollector(this.fileGraph, this.nodeGraph)
23+
}
2024

21-
startTraversal(mainSourceFile: SourceFile): TraversedNode[] {
25+
startTraversal(sourceFile: SourceFile): TraversedNode[] {
2226
// Mark main source file nodes as main code
23-
addLocalTypes(mainSourceFile, this.nodeGraph, this.maincodeNodeIds, this.requiredNodeIds)
27+
addLocalTypes(sourceFile, this.nodeGraph, this.maincodeNodeIds, this.requiredNodeIds)
2428

2529
// Start recursive traversal from imports
26-
const importDeclarations = mainSourceFile.getImportDeclarations()
30+
const importDeclarations = sourceFile.getImportDeclarations()
2731
this.importCollector.collectFromImports(importDeclarations)
2832

2933
// Extract dependencies for all nodes
@@ -38,23 +42,30 @@ export class DependencyTraversal {
3842
* Handles circular dependencies gracefully by falling back to simple node order
3943
*/
4044
getNodesToPrint(): TraversedNode[] {
41-
// Get all nodes in topological order, then filter to only required ones
42-
const allNodesInOrder = hasCycle(this.nodeGraph)
43-
? Array.from(this.nodeGraph.nodes())
44-
: topologicalSort(this.nodeGraph)
45+
// Get all qualified names from resolver store, then apply topological ordering
46+
const allQualifiedNames = resolverStore.getAllQualifiedNames()
47+
48+
// Filter to only required nodes that exist in both resolver store and node graph
49+
const requiredQualifiedNames = allQualifiedNames.filter((qualifiedName: string) =>
50+
this.requiredNodeIds.has(qualifiedName),
51+
)
52+
53+
// Apply topological ordering only to the required nodes
54+
const orderedNodes = hasCycle(this.nodeGraph)
55+
? requiredQualifiedNames
56+
: topologicalSort(this.nodeGraph).filter((nodeId: string) =>
57+
requiredQualifiedNames.includes(nodeId),
58+
)
4559

46-
const filteredNodes = allNodesInOrder
47-
.filter((nodeId: string) => this.requiredNodeIds.has(nodeId))
60+
// Map to actual node data
61+
const filteredNodes = orderedNodes
4862
.map((nodeId: string) => this.nodeGraph.getNode(nodeId))
63+
.filter((node): node is TraversedNode => node !== null)
4964

5065
return filteredNodes
5166
}
5267

5368
async visualizeGraph(options: VisualizationOptions = {}): Promise<string> {
5469
return GraphVisualizer.generateVisualization(this.nodeGraph, options)
5570
}
56-
57-
getNodeGraph(): NodeGraph {
58-
return this.nodeGraph
59-
}
6071
}

src/traverse/import-collector.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FileGraph } from '@daxserver/validation-schema-codegen/traverse/file-graph'
22
import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph'
3-
import { generateQualifiedNodeName } from '@daxserver/validation-schema-codegen/utils/generate-qualified-name'
3+
import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store'
44
import { ImportDeclaration } from 'ts-morph'
55

66
export class ImportCollector {
@@ -20,6 +20,17 @@ export class ImportCollector {
2020
if (this.fileGraph.hasNode(filePath)) continue
2121
this.fileGraph.addFile(filePath, moduleSourceFile)
2222

23+
// Build alias map specific to this import declaration
24+
const aliasMap = new Map<string, string>() // originalName -> aliasName
25+
const namedImports = importDecl.getNamedImports()
26+
for (const namedImport of namedImports) {
27+
const originalName = namedImport.getName()
28+
const aliasName = namedImport.getAliasNode()?.getText()
29+
if (aliasName) {
30+
aliasMap.set(originalName, aliasName)
31+
}
32+
}
33+
2334
const imports = moduleSourceFile.getImportDeclarations()
2435
const typeAliases = moduleSourceFile.getTypeAliases()
2536
const interfaces = moduleSourceFile.getInterfaces()
@@ -29,51 +40,87 @@ export class ImportCollector {
2940
// Add all imported types to the graph
3041
for (const typeAlias of typeAliases) {
3142
const typeName = typeAlias.getName()
32-
const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile())
43+
const qualifiedName = resolverStore.generateQualifiedName(
44+
typeName,
45+
typeAlias.getSourceFile(),
46+
)
47+
const aliasName = aliasMap.get(typeName)
3348
this.nodeGraph.addTypeNode(qualifiedName, {
3449
node: typeAlias,
3550
type: 'typeAlias',
3651
originalName: typeName,
3752
qualifiedName,
3853
isImported: true,
3954
isMainCode: false,
55+
aliasName,
56+
})
57+
58+
// Add to ResolverStore during traversal
59+
resolverStore.addTypeMapping({
60+
originalName: typeName,
61+
sourceFile: typeAlias.getSourceFile(),
62+
aliasName,
4063
})
4164
}
4265

4366
for (const interfaceDecl of interfaces) {
4467
const interfaceName = interfaceDecl.getName()
45-
const qualifiedName = generateQualifiedNodeName(
68+
const qualifiedName = resolverStore.generateQualifiedName(
4669
interfaceName,
4770
interfaceDecl.getSourceFile(),
4871
)
72+
const aliasName = aliasMap.get(interfaceName)
4973
this.nodeGraph.addTypeNode(qualifiedName, {
5074
node: interfaceDecl,
5175
type: 'interface',
5276
originalName: interfaceName,
5377
qualifiedName,
5478
isImported: true,
5579
isMainCode: false,
80+
aliasName,
81+
})
82+
83+
// Add to ResolverStore during traversal
84+
resolverStore.addTypeMapping({
85+
originalName: interfaceName,
86+
sourceFile: interfaceDecl.getSourceFile(),
87+
aliasName,
5688
})
5789
}
5890

5991
for (const enumDecl of enums) {
6092
const enumName = enumDecl.getName()
61-
const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile())
93+
const qualifiedName = resolverStore.generateQualifiedName(
94+
enumName,
95+
enumDecl.getSourceFile(),
96+
)
97+
const aliasName = aliasMap.get(enumName)
6298
this.nodeGraph.addTypeNode(qualifiedName, {
6399
node: enumDecl,
64100
type: 'enum',
65101
originalName: enumName,
66102
qualifiedName,
67103
isImported: true,
68104
isMainCode: false,
105+
aliasName,
106+
})
107+
108+
// Add to ResolverStore during traversal
109+
resolverStore.addTypeMapping({
110+
originalName: enumName,
111+
sourceFile: enumDecl.getSourceFile(),
112+
aliasName,
69113
})
70114
}
71115

72116
for (const functionDecl of functions) {
73117
const functionName = functionDecl.getName()
74118
if (!functionName) continue
75119

76-
const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile())
120+
const qualifiedName = resolverStore.generateQualifiedName(
121+
functionName,
122+
functionDecl.getSourceFile(),
123+
)
77124
this.nodeGraph.addTypeNode(qualifiedName, {
78125
node: functionDecl,
79126
type: 'function',
@@ -82,6 +129,12 @@ export class ImportCollector {
82129
isImported: true,
83130
isMainCode: false,
84131
})
132+
133+
// Add to ResolverStore during traversal
134+
resolverStore.addTypeMapping({
135+
originalName: functionName,
136+
sourceFile: functionDecl.getSourceFile(),
137+
})
85138
}
86139

87140
// Recursively collect from nested imports (mark as transitive)

0 commit comments

Comments
 (0)