diff --git a/src/handlers/typebox/type-reference-handler.ts b/src/handlers/typebox/type-reference-handler.ts index 91d8a11..1ec1db8 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 { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' 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' @@ -13,7 +14,10 @@ export class TypeReferenceHandler extends BaseTypeHandler { const typeArguments = node.getTypeArguments() if (Node.isIdentifier(referencedType)) { - const typeName = referencedType.getText() + const originalTypeName = referencedType.getText() + + // Use the ResolverStore to get the alias name if available + const typeName = resolverStore.resolveAliasName(originalTypeName) // If there are type arguments, create a function call if (typeArguments.length > 0) { diff --git a/src/parsers/parse-enums.ts b/src/parsers/parse-enums.ts index 7ddc6b8..72313e4 100644 --- a/src/parsers/parse-enums.ts +++ b/src/parsers/parse-enums.ts @@ -3,8 +3,8 @@ import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/a import { EnumDeclaration, VariableDeclarationKind } from 'ts-morph' export class EnumParser extends BaseParser { - parse(enumDeclaration: EnumDeclaration): void { - const enumName = enumDeclaration.getName() + parse(enumDeclaration: EnumDeclaration, aliasName?: string): void { + const enumName = aliasName || enumDeclaration.getName() const schemaName = `${enumName}Schema` this.newSourceFile.addEnum({ diff --git a/src/parsers/parse-function-declarations.ts b/src/parsers/parse-function-declarations.ts index 4fa9564..6d9654d 100644 --- a/src/parsers/parse-function-declarations.ts +++ b/src/parsers/parse-function-declarations.ts @@ -5,9 +5,11 @@ import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox import { FunctionDeclaration, ts, VariableDeclarationKind } from 'ts-morph' export class FunctionDeclarationParser extends BaseParser { - parse(functionDecl: FunctionDeclaration): void { - const functionName = functionDecl.getName() - if (!functionName) return + parse(functionDecl: FunctionDeclaration, aliasName?: string): void { + const originalFunctionName = functionDecl.getName() + if (!originalFunctionName) return + + const functionName = aliasName || originalFunctionName if (this.processedTypes.has(functionName)) return this.processedTypes.add(functionName) diff --git a/src/parsers/parse-interfaces.ts b/src/parsers/parse-interfaces.ts index f30ecf3..fd2e4dd 100644 --- a/src/parsers/parse-interfaces.ts +++ b/src/parsers/parse-interfaces.ts @@ -5,8 +5,8 @@ import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typeb import { InterfaceDeclaration, ts } from 'ts-morph' export class InterfaceParser extends BaseParser { - parse(interfaceDecl: InterfaceDeclaration): void { - const interfaceName = interfaceDecl.getName() + parse(interfaceDecl: InterfaceDeclaration, aliasName?: string): void { + const interfaceName = aliasName || interfaceDecl.getName() if (this.processedTypes.has(interfaceName)) return this.processedTypes.add(interfaceName) @@ -15,15 +15,13 @@ export class InterfaceParser extends BaseParser { // Check if interface has type parameters (generic) if (typeParameters.length > 0) { - this.parseGenericInterface(interfaceDecl) + this.parseGenericInterface(interfaceDecl, interfaceName) } else { - this.parseRegularInterface(interfaceDecl) + this.parseRegularInterface(interfaceDecl, interfaceName) } } - private parseRegularInterface(interfaceDecl: InterfaceDeclaration): void { - const interfaceName = interfaceDecl.getName() - + private parseRegularInterface(interfaceDecl: InterfaceDeclaration, interfaceName: string): void { // Generate TypeBox type definition const typeboxTypeNode = getTypeBoxType(interfaceDecl) const typeboxType = this.printer.printNode( @@ -42,8 +40,7 @@ export class InterfaceParser extends BaseParser { ) } - private parseGenericInterface(interfaceDecl: InterfaceDeclaration): void { - const interfaceName = interfaceDecl.getName() + private parseGenericInterface(interfaceDecl: InterfaceDeclaration, interfaceName: string): void { const typeParameters = interfaceDecl.getTypeParameters() // Generate TypeBox function definition using the same flow as type aliases diff --git a/src/parsers/parse-type-aliases.ts b/src/parsers/parse-type-aliases.ts index 18c53c4..6db4657 100644 --- a/src/parsers/parse-type-aliases.ts +++ b/src/parsers/parse-type-aliases.ts @@ -6,8 +6,8 @@ import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox import { ts, TypeAliasDeclaration } from 'ts-morph' export class TypeAliasParser extends BaseParser { - parse(typeAlias: TypeAliasDeclaration): void { - const typeName = typeAlias.getName() + parse(typeAlias: TypeAliasDeclaration, aliasName?: string): void { + const typeName = aliasName || typeAlias.getName() if (this.processedTypes.has(typeName)) return this.processedTypes.add(typeName) @@ -16,15 +16,13 @@ export class TypeAliasParser extends BaseParser { // Check if type alias has type parameters (generic) if (typeParameters.length > 0) { - this.parseGenericTypeAlias(typeAlias) + this.parseGenericTypeAlias(typeAlias, typeName) } else { - this.parseRegularTypeAlias(typeAlias) + this.parseRegularTypeAlias(typeAlias, typeName) } } - private parseRegularTypeAlias(typeAlias: TypeAliasDeclaration): void { - const typeName = typeAlias.getName() - + private parseRegularTypeAlias(typeAlias: TypeAliasDeclaration, typeName: string): void { const typeNode = typeAlias.getTypeNode() const typeboxTypeNode = typeNode ? getTypeBoxType(typeNode) : makeTypeCall('Any') const typeboxType = this.printer.printNode( @@ -38,8 +36,7 @@ export class TypeAliasParser extends BaseParser { addStaticTypeAlias(this.newSourceFile, typeName, this.newSourceFile.compilerNode, this.printer) } - private parseGenericTypeAlias(typeAlias: TypeAliasDeclaration): void { - const typeName = typeAlias.getName() + private parseGenericTypeAlias(typeAlias: TypeAliasDeclaration, typeName: string): void { const typeParameters = typeAlias.getTypeParameters() // Generate TypeBox function definition diff --git a/src/printer/typebox-printer.ts b/src/printer/typebox-printer.ts index e935fc1..23f530f 100644 --- a/src/printer/typebox-printer.ts +++ b/src/printer/typebox-printer.ts @@ -37,23 +37,23 @@ export class TypeBoxPrinter { } printNode(traversedNode: TraversedNode): void { - const { node } = traversedNode + const { node, aliasName } = traversedNode switch (true) { case Node.isTypeAliasDeclaration(node): - this.typeAliasParser.parse(node) + this.typeAliasParser.parse(node, aliasName) break case Node.isInterfaceDeclaration(node): - this.interfaceParser.parse(node) + this.interfaceParser.parse(node, aliasName) break case Node.isEnumDeclaration(node): - this.enumParser.parse(node) + this.enumParser.parse(node, aliasName) break case Node.isFunctionDeclaration(node): - this.functionParser.parse(node) + this.functionParser.parse(node, aliasName) break default: diff --git a/src/traverse/dependency-extractor.ts b/src/traverse/dependency-extractor.ts index eed5137..21efbfb 100644 --- a/src/traverse/dependency-extractor.ts +++ b/src/traverse/dependency-extractor.ts @@ -1,5 +1,6 @@ import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' import { TypeReferenceExtractor } from '@daxserver/validation-schema-codegen/traverse/type-reference-extractor' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import { EnumDeclaration, FunctionDeclaration, @@ -11,7 +12,7 @@ import { export const extractDependencies = (nodeGraph: NodeGraph, requiredNodeIds: Set): void => { const processedNodes = new Set() const nodesToProcess = new Set(requiredNodeIds) - const typeReferenceExtractor = new TypeReferenceExtractor(nodeGraph) + const typeReferenceExtractor = new TypeReferenceExtractor() // Process nodes iteratively until no new dependencies are found while (nodesToProcess.size > 0) { @@ -44,7 +45,7 @@ export const extractDependencies = (nodeGraph: NodeGraph, requiredNodeIds: Set() private requiredNodeIds = new Set() - private importCollector = new ImportCollector(this.fileGraph, this.nodeGraph) + private importCollector: ImportCollector + constructor() { + this.importCollector = new ImportCollector(this.fileGraph, this.nodeGraph) + } - startTraversal(mainSourceFile: SourceFile): TraversedNode[] { + startTraversal(sourceFile: SourceFile): TraversedNode[] { // Mark main source file nodes as main code - addLocalTypes(mainSourceFile, this.nodeGraph, this.maincodeNodeIds, this.requiredNodeIds) + addLocalTypes(sourceFile, this.nodeGraph, this.maincodeNodeIds, this.requiredNodeIds) // Start recursive traversal from imports - const importDeclarations = mainSourceFile.getImportDeclarations() + const importDeclarations = sourceFile.getImportDeclarations() this.importCollector.collectFromImports(importDeclarations) // Extract dependencies for all nodes @@ -38,14 +42,25 @@ export class DependencyTraversal { * Handles circular dependencies gracefully by falling back to simple node order */ getNodesToPrint(): TraversedNode[] { - // Get all nodes in topological order, then filter to only required ones - const allNodesInOrder = hasCycle(this.nodeGraph) - ? Array.from(this.nodeGraph.nodes()) - : topologicalSort(this.nodeGraph) + // Get all qualified names from resolver store, then apply topological ordering + const allQualifiedNames = resolverStore.getAllQualifiedNames() + + // Filter to only required nodes that exist in both resolver store and node graph + const requiredQualifiedNames = allQualifiedNames.filter((qualifiedName: string) => + this.requiredNodeIds.has(qualifiedName), + ) + + // Apply topological ordering only to the required nodes + const orderedNodes = hasCycle(this.nodeGraph) + ? requiredQualifiedNames + : topologicalSort(this.nodeGraph).filter((nodeId: string) => + requiredQualifiedNames.includes(nodeId), + ) - const filteredNodes = allNodesInOrder - .filter((nodeId: string) => this.requiredNodeIds.has(nodeId)) + // Map to actual node data + const filteredNodes = orderedNodes .map((nodeId: string) => this.nodeGraph.getNode(nodeId)) + .filter((node): node is TraversedNode => node !== null) return filteredNodes } @@ -53,8 +68,4 @@ export class DependencyTraversal { async visualizeGraph(options: VisualizationOptions = {}): Promise { return GraphVisualizer.generateVisualization(this.nodeGraph, options) } - - getNodeGraph(): NodeGraph { - return this.nodeGraph - } } diff --git a/src/traverse/import-collector.ts b/src/traverse/import-collector.ts index fd30dd2..bb483b0 100644 --- a/src/traverse/import-collector.ts +++ b/src/traverse/import-collector.ts @@ -1,6 +1,6 @@ import { FileGraph } from '@daxserver/validation-schema-codegen/traverse/file-graph' import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' -import { generateQualifiedNodeName } from '@daxserver/validation-schema-codegen/utils/generate-qualified-name' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import { ImportDeclaration } from 'ts-morph' export class ImportCollector { @@ -20,6 +20,17 @@ export class ImportCollector { if (this.fileGraph.hasNode(filePath)) continue this.fileGraph.addFile(filePath, moduleSourceFile) + // Build alias map specific to this import declaration + const aliasMap = new Map() // originalName -> aliasName + const namedImports = importDecl.getNamedImports() + for (const namedImport of namedImports) { + const originalName = namedImport.getName() + const aliasName = namedImport.getAliasNode()?.getText() + if (aliasName) { + aliasMap.set(originalName, aliasName) + } + } + const imports = moduleSourceFile.getImportDeclarations() const typeAliases = moduleSourceFile.getTypeAliases() const interfaces = moduleSourceFile.getInterfaces() @@ -29,7 +40,11 @@ export class ImportCollector { // Add all imported types to the graph for (const typeAlias of typeAliases) { const typeName = typeAlias.getName() - const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile()) + const qualifiedName = resolverStore.generateQualifiedName( + typeName, + typeAlias.getSourceFile(), + ) + const aliasName = aliasMap.get(typeName) this.nodeGraph.addTypeNode(qualifiedName, { node: typeAlias, type: 'typeAlias', @@ -37,15 +52,24 @@ export class ImportCollector { qualifiedName, isImported: true, isMainCode: false, + aliasName, + }) + + // Add to ResolverStore during traversal + resolverStore.addTypeMapping({ + originalName: typeName, + sourceFile: typeAlias.getSourceFile(), + aliasName, }) } for (const interfaceDecl of interfaces) { const interfaceName = interfaceDecl.getName() - const qualifiedName = generateQualifiedNodeName( + const qualifiedName = resolverStore.generateQualifiedName( interfaceName, interfaceDecl.getSourceFile(), ) + const aliasName = aliasMap.get(interfaceName) this.nodeGraph.addTypeNode(qualifiedName, { node: interfaceDecl, type: 'interface', @@ -53,12 +77,24 @@ export class ImportCollector { qualifiedName, isImported: true, isMainCode: false, + aliasName, + }) + + // Add to ResolverStore during traversal + resolverStore.addTypeMapping({ + originalName: interfaceName, + sourceFile: interfaceDecl.getSourceFile(), + aliasName, }) } for (const enumDecl of enums) { const enumName = enumDecl.getName() - const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile()) + const qualifiedName = resolverStore.generateQualifiedName( + enumName, + enumDecl.getSourceFile(), + ) + const aliasName = aliasMap.get(enumName) this.nodeGraph.addTypeNode(qualifiedName, { node: enumDecl, type: 'enum', @@ -66,6 +102,14 @@ export class ImportCollector { qualifiedName, isImported: true, isMainCode: false, + aliasName, + }) + + // Add to ResolverStore during traversal + resolverStore.addTypeMapping({ + originalName: enumName, + sourceFile: enumDecl.getSourceFile(), + aliasName, }) } @@ -73,7 +117,10 @@ export class ImportCollector { const functionName = functionDecl.getName() if (!functionName) continue - const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile()) + const qualifiedName = resolverStore.generateQualifiedName( + functionName, + functionDecl.getSourceFile(), + ) this.nodeGraph.addTypeNode(qualifiedName, { node: functionDecl, type: 'function', @@ -82,6 +129,12 @@ export class ImportCollector { isImported: true, isMainCode: false, }) + + // Add to ResolverStore during traversal + resolverStore.addTypeMapping({ + originalName: functionName, + sourceFile: functionDecl.getSourceFile(), + }) } // Recursively collect from nested imports (mark as transitive) diff --git a/src/traverse/local-type-collector.ts b/src/traverse/local-type-collector.ts index ef4ca50..701bb30 100644 --- a/src/traverse/local-type-collector.ts +++ b/src/traverse/local-type-collector.ts @@ -1,5 +1,5 @@ import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' -import { generateQualifiedNodeName } from '@daxserver/validation-schema-codegen/utils/generate-qualified-name' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import { SourceFile } from 'ts-morph' export const addLocalTypes = ( @@ -27,7 +27,7 @@ export const addLocalTypes = ( const importName = namedImport.getName() const importSourceFile = importDecl.getModuleSpecifierSourceFile() if (importSourceFile) { - const qualifiedName = generateQualifiedNodeName(importName, importSourceFile) + const qualifiedName = resolverStore.generateQualifiedName(importName, importSourceFile) requiredNodeIds.add(qualifiedName) } } @@ -39,7 +39,7 @@ export const addLocalTypes = ( // Collect type aliases for (const typeAlias of typeAliases) { const typeName = typeAlias.getName() - const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile()) + const qualifiedName = resolverStore.generateQualifiedName(typeName, typeAlias.getSourceFile()) maincodeNodeIds.add(qualifiedName) requiredNodeIds.add(qualifiedName) nodeGraph.addTypeNode(qualifiedName, { @@ -50,12 +50,21 @@ export const addLocalTypes = ( isImported: false, isMainCode: true, }) + + // Add to ResolverStore during traversal + resolverStore.addTypeMapping({ + originalName: typeName, + sourceFile, + }) } // Collect interfaces for (const interfaceDecl of interfaces) { const interfaceName = interfaceDecl.getName() - const qualifiedName = generateQualifiedNodeName(interfaceName, interfaceDecl.getSourceFile()) + const qualifiedName = resolverStore.generateQualifiedName( + interfaceName, + interfaceDecl.getSourceFile(), + ) maincodeNodeIds.add(qualifiedName) requiredNodeIds.add(qualifiedName) nodeGraph.addTypeNode(qualifiedName, { @@ -66,12 +75,18 @@ export const addLocalTypes = ( isImported: false, isMainCode: true, }) + + // Add to ResolverStore during traversal + resolverStore.addTypeMapping({ + originalName: interfaceName, + sourceFile: interfaceDecl.getSourceFile(), + }) } // Collect enums for (const enumDecl of enums) { const enumName = enumDecl.getName() - const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile()) + const qualifiedName = resolverStore.generateQualifiedName(enumName, enumDecl.getSourceFile()) maincodeNodeIds.add(qualifiedName) requiredNodeIds.add(qualifiedName) nodeGraph.addTypeNode(qualifiedName, { @@ -82,6 +97,12 @@ export const addLocalTypes = ( isImported: false, isMainCode: true, }) + + // Add to ResolverStore during traversal + resolverStore.addTypeMapping({ + originalName: enumName, + sourceFile: enumDecl.getSourceFile(), + }) } // Collect functions @@ -89,7 +110,10 @@ export const addLocalTypes = ( const functionName = functionDecl.getName() if (!functionName) continue - const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile()) + const qualifiedName = resolverStore.generateQualifiedName( + functionName, + functionDecl.getSourceFile(), + ) maincodeNodeIds.add(qualifiedName) requiredNodeIds.add(qualifiedName) nodeGraph.addTypeNode(qualifiedName, { @@ -100,5 +124,11 @@ export const addLocalTypes = ( isImported: false, isMainCode: true, }) + + // Add to ResolverStore during traversal + resolverStore.addTypeMapping({ + originalName: functionName, + sourceFile: functionDecl.getSourceFile(), + }) } } diff --git a/src/traverse/type-reference-extractor.ts b/src/traverse/type-reference-extractor.ts index fba2f58..dfb8298 100644 --- a/src/traverse/type-reference-extractor.ts +++ b/src/traverse/type-reference-extractor.ts @@ -1,8 +1,33 @@ -import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import { Node, SyntaxKind } from 'ts-morph' export class TypeReferenceExtractor { - constructor(private nodeGraph: NodeGraph) {} + private resolveCache = new Map() + + private static readonly BUILT_IN_TYPES = new Set([ + 'Partial', + 'Required', + 'Readonly', + 'Record', + 'Pick', + 'Omit', + 'Exclude', + 'Extract', + 'NonNullable', + 'ReturnType', + 'InstanceType', + 'Parameters', + 'ConstructorParameters', + 'ThisParameterType', + 'OmitThisParameter', + 'ThisType', + 'Uppercase', + 'Lowercase', + 'Capitalize', + 'Uncapitalize', + 'NoInfer', + 'Awaited', + ]) extractTypeReferences(node: Node): string[] { const references: string[] = [] @@ -13,14 +38,12 @@ export class TypeReferenceExtractor { visited.add(node) if (Node.isTypeReference(node)) { - const typeName = node.getTypeName().getText() + const typeNameNode = node.getTypeName() + const typeName = typeNameNode.getText() - for (const qualifiedName of this.nodeGraph.nodes()) { - const nodeData = this.nodeGraph.getNode(qualifiedName) - if (nodeData.originalName === typeName) { - references.push(qualifiedName) - break - } + const qualifiedName = this.resolveTypeNameToQualifiedName(typeName, typeNameNode) + if (qualifiedName) { + references.push(qualifiedName) } } @@ -31,12 +54,9 @@ export class TypeReferenceExtractor { if (Node.isIdentifier(exprName) || Node.isQualifiedName(exprName)) { const typeName = exprName.getText() - for (const qualifiedName of this.nodeGraph.nodes()) { - const nodeData = this.nodeGraph.getNode(qualifiedName) - if (nodeData.originalName === typeName) { - references.push(qualifiedName) - break - } + const qualifiedName = this.resolveTypeNameToQualifiedName(typeName, exprName) + if (qualifiedName) { + references.push(qualifiedName) } } } @@ -51,14 +71,15 @@ export class TypeReferenceExtractor { for (const typeNode of heritageClause.getTypeNodes()) { // Handle both simple types and generic types if (Node.isTypeReference(typeNode)) { - const baseTypeName = typeNode.getTypeName().getText() - - for (const qualifiedName of this.nodeGraph.nodes()) { - const nodeData = this.nodeGraph.getNode(qualifiedName) - if (nodeData.originalName === baseTypeName) { - references.push(qualifiedName) - break - } + const baseTypeNameNode = typeNode.getTypeName() + const baseTypeName = baseTypeNameNode.getText() + + const qualifiedName = this.resolveTypeNameToQualifiedName( + baseTypeName, + baseTypeNameNode, + ) + if (qualifiedName) { + references.push(qualifiedName) } // Also extract dependencies from type arguments @@ -74,12 +95,9 @@ export class TypeReferenceExtractor { if (Node.isIdentifier(expression)) { const baseTypeName = expression.getText() - for (const qualifiedName of this.nodeGraph.nodes()) { - const nodeData = this.nodeGraph.getNode(qualifiedName) - if (nodeData.originalName === baseTypeName) { - references.push(qualifiedName) - break - } + const qualifiedName = this.resolveTypeNameToQualifiedName(baseTypeName, expression) + if (qualifiedName) { + references.push(qualifiedName) } } @@ -97,16 +115,12 @@ export class TypeReferenceExtractor { // Handle call expressions (for generic type calls like EntityInfo(PropertyId)) if (Node.isCallExpression(node)) { const expression = node.getExpression() - if (Node.isIdentifier(expression)) { const typeName = expression.getText() - for (const qualifiedName of this.nodeGraph.nodes()) { - const nodeData = this.nodeGraph.getNode(qualifiedName) - if (nodeData.originalName === typeName) { - references.push(qualifiedName) - break - } + const qualifiedName = this.resolveTypeNameToQualifiedName(typeName, expression) + if (qualifiedName) { + references.push(qualifiedName) } } } @@ -118,4 +132,25 @@ export class TypeReferenceExtractor { return references } + + /** + * Resolves a type name to its qualified name using the ResolverStore + */ + private resolveTypeNameToQualifiedName(typeName: string, node: Node): string | null { + if (TypeReferenceExtractor.BUILT_IN_TYPES.has(typeName)) { + return null + } + + // Check cache for non-built-in types + const cacheKey = `${typeName}:${node.getKind()}:${node.getStart()}` + if (this.resolveCache.has(cacheKey)) { + return this.resolveCache.get(cacheKey)! + } + + // Resolve using ResolverStore + const qualifiedName = resolverStore.resolveQualifiedName(typeName) + this.resolveCache.set(cacheKey, qualifiedName) + + return qualifiedName + } } diff --git a/src/traverse/types.ts b/src/traverse/types.ts index 3617f0d..596e568 100644 --- a/src/traverse/types.ts +++ b/src/traverse/types.ts @@ -7,4 +7,5 @@ export interface TraversedNode { qualifiedName: string isImported: boolean isMainCode: boolean + aliasName?: string // The alias name used in import statements (e.g., 'UserType' for 'import { User as UserType }') } diff --git a/src/utils/generate-qualified-name.ts b/src/utils/generate-qualified-name.ts deleted file mode 100644 index 8db55c1..0000000 --- a/src/utils/generate-qualified-name.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { basename } from 'node:path' -import type { SourceFile } from 'ts-morph' - -/** - * Simple hash function for generating unique identifiers - */ -const hashString = (str: string): string => { - let hash = 0 - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i) - hash = (hash << 5) - hash + char - hash |= 0 // force signed 32-bit - } - return (hash >>> 0).toString(36) // unsigned 32-bit -} - -/** - * Generate a fully qualified node name to prevent naming conflicts - */ -export const generateQualifiedNodeName = (typeName: string, sourceFile: SourceFile): string => { - const filePath = sourceFile.getFilePath() - const fileName = basename(filePath) - const fileHash = hashString(filePath) - return `${typeName}__${fileName}__${fileHash}` -} diff --git a/src/utils/graph-visualizer.ts b/src/utils/graph-visualizer.ts index 7fb5e43..dc03f76 100644 --- a/src/utils/graph-visualizer.ts +++ b/src/utils/graph-visualizer.ts @@ -1,4 +1,5 @@ import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import type { DirectedGraph } from 'graphology' import Graph from 'graphology' import forceAtlas2 from 'graphology-layout-forceatlas2' @@ -68,18 +69,22 @@ export class GraphVisualizer { const nodes: GraphNode[] = [] const edges: GraphEdge[] = [] - // Convert nodes - for (const nodeId of graph.nodes()) { - const nodeData = graph.getNodeAttributes(nodeId) + // Convert nodes using resolver store to get all qualified names + const allQualifiedNames = resolverStore.getAllQualifiedNames() + for (const nodeId of allQualifiedNames) { + // Only include nodes that exist in the graph + if (graph.hasNode(nodeId)) { + const nodeData = graph.getNodeAttributes(nodeId) + + const node: GraphNode = { + key: nodeId, + label: `${nodeData.type}: ${nodeData.originalName}`, + size: this.getNodeSize(nodeData), + color: this.getEnhancedNodeColor(nodeData), + } - const node: GraphNode = { - key: nodeId, - label: `${nodeData.type}: ${nodeData.originalName}`, - size: this.getNodeSize(nodeData), - color: this.getEnhancedNodeColor(nodeData), + nodes.push(node) } - - nodes.push(node) } // Convert edges diff --git a/src/utils/resolver-store.ts b/src/utils/resolver-store.ts new file mode 100644 index 0000000..8e7e3d7 --- /dev/null +++ b/src/utils/resolver-store.ts @@ -0,0 +1,174 @@ +import { basename } from 'path' +import type { SourceFile } from 'ts-morph' + +export interface TypeMapping { + originalName: string + qualifiedName: string + aliasName?: string + sourceFile?: SourceFile +} + +export interface TypeMappingInput { + originalName: string + sourceFile: SourceFile + aliasName?: string +} + +/** + * Simple hash function for generating unique identifiers + */ +const hashString = (str: string): string => { + let hash = 0 + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i) + hash = (hash << 5) - hash + char + hash |= 0 // force signed 32-bit + } + return (hash >>> 0).toString(36) // unsigned 32-bit +} + +/** + * Generate a fully qualified node name to prevent naming conflicts + */ +const generateQualifiedNodeName = (typeName: string, sourceFile: SourceFile): string => { + const filePath = sourceFile.getFilePath() + const fileName = basename(filePath) + const fileHash = hashString(filePath) + return `${typeName}__${fileName}__${fileHash}` +} + +class ResolverStore { + private typeMappings = new Map() // qualifiedName -> TypeMapping + private originalNameIndex = new Map() // originalName -> qualifiedName[] + private aliasNameIndex = new Map() // aliasName -> qualifiedName + private qualifiedNameCache = new Map() // originalName+sourceFilePath -> qualifiedName + + /** + * Generate a qualified name for a type. This is the single source of truth for qualified name generation. + */ + generateQualifiedName(originalName: string, sourceFile: SourceFile): string { + const cacheKey = `${originalName}:${sourceFile.getFilePath()}` + + // Check cache first + const cached = this.qualifiedNameCache.get(cacheKey) + if (cached) { + return cached + } + + // Generate new qualified name + const qualifiedName = generateQualifiedNodeName(originalName, sourceFile) + + // Cache the result + this.qualifiedNameCache.set(cacheKey, qualifiedName) + + return qualifiedName + } + + addTypeMapping(mapping: TypeMappingInput): void { + const qualifiedName = this.generateQualifiedName(mapping.originalName, mapping.sourceFile) + const typeMapping: TypeMapping = { + originalName: mapping.originalName, + qualifiedName, + aliasName: mapping.aliasName, + sourceFile: mapping.sourceFile, + } + + this.typeMappings.set(qualifiedName, typeMapping) + + // Update originalName index to support multiple qualified names per original name + const existingQualifiedNames = this.originalNameIndex.get(mapping.originalName) || [] + existingQualifiedNames.push(qualifiedName) + this.originalNameIndex.set(mapping.originalName, existingQualifiedNames) + + if (mapping.aliasName) { + this.aliasNameIndex.set(mapping.aliasName, qualifiedName) + } + } + + getTypeMappingByOriginalName(originalName: string): TypeMapping | null { + const qualifiedNames = this.originalNameIndex.get(originalName) + if (!qualifiedNames || qualifiedNames.length === 0) return null + // Return the first mapping for now - could be enhanced for better disambiguation + const firstQualifiedName = qualifiedNames[0] + return firstQualifiedName ? this.typeMappings.get(firstQualifiedName) || null : null + } + + getTypeMappingByQualifiedName(qualifiedName: string): TypeMapping | null { + return this.typeMappings.get(qualifiedName) || null + } + + getTypeMappingByAliasName(aliasName: string): TypeMapping | null { + const qualifiedName = this.aliasNameIndex.get(aliasName) + return qualifiedName ? this.typeMappings.get(qualifiedName) || null : null + } + + resolveQualifiedName(typeName: string): string | null { + // First try to find by alias name if available + const byAlias = this.getTypeMappingByAliasName(typeName) + if (byAlias) { + return byAlias.qualifiedName + } + + // Fallback to original name matching + const byOriginal = this.getTypeMappingByOriginalName(typeName) + return byOriginal ? byOriginal.qualifiedName : null + } + + resolveAliasName(typeName: string): string { + // First try to find by original name and return alias if available + const byOriginal = this.getTypeMappingByOriginalName(typeName) + if (byOriginal && byOriginal.aliasName) { + return byOriginal.aliasName + } + + // Return the original name if no alias found + return typeName + } + + hasTypeMapping(originalName: string): boolean { + return this.originalNameIndex.has(originalName) + } + + getAllTypeMappings(): ReadonlyMap> { + return new Map(this.typeMappings) + } + + getQualifiedNames(): string[] { + return Array.from(this.typeMappings.keys()) + } + + getOriginalNames(): string[] { + return Array.from(this.originalNameIndex.keys()) + } + + getAliasNames(): string[] { + return Array.from(this.aliasNameIndex.keys()) + } + + /** + * Check if a qualified name exists in the store + */ + hasQualifiedName(qualifiedName: string): boolean { + return this.typeMappings.has(qualifiedName) + } + + /** + * Get all qualified names from the store + */ + getAllQualifiedNames(): string[] { + return Array.from(this.typeMappings.keys()) + } + + clear(): void { + this.typeMappings.clear() + this.originalNameIndex.clear() + this.aliasNameIndex.clear() + this.qualifiedNameCache.clear() + } + + size(): number { + return this.typeMappings.size + } +} + +export const resolverStore = new ResolverStore() diff --git a/tests/import-resolution.test.ts b/tests/import-resolution.test.ts index 5be6d7d..ea20d76 100644 --- a/tests/import-resolution.test.ts +++ b/tests/import-resolution.test.ts @@ -1,3 +1,4 @@ +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' @@ -7,6 +8,7 @@ describe('ts-morph codegen with imports', () => { beforeEach(() => { project = new Project() + resolverStore.clear() }) describe('without exports', () => { diff --git a/tests/traverse/ambiguous-name-resolution.test.ts b/tests/traverse/ambiguous-name-resolution.test.ts new file mode 100644 index 0000000..14d53a1 --- /dev/null +++ b/tests/traverse/ambiguous-name-resolution.test.ts @@ -0,0 +1,168 @@ +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' +import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('Ambiguous name resolution', () => { + let project: Project + + beforeEach(() => { + project = new Project() + resolverStore.clear() + }) + + test('should disambiguate type resolution across multiple files', () => { + // Create first file with User type + createSourceFile( + project, + ` + export type User = { + id: string; + name: string; + }; + `, + 'user-types.ts', + ) + + // Create second file with different User type + createSourceFile( + project, + ` + export type User = { + userId: number; + email: string; + }; + `, + 'admin-types.ts', + ) + + // Create main file that imports both and uses them + const mainFile = createSourceFile( + project, + ` + import { User as UserType } from "./user-types"; + import { User as AdminUser } from "./admin-types"; + + type UserProfile = { + user: UserType; + admin: AdminUser; + }; + `, + ) + + const result = generateFormattedCode(mainFile) + + expect(result).toContain('UserProfile') + expect(result).toBe( + formatWithPrettier( + ` + export const UserType = Type.Object({ + id: Type.String(), + name: Type.String(), + }); + + export type UserType = Static + + export const AdminUser = Type.Object({ + userId: Type.Number(), + email: Type.String(), + }); + + export type AdminUser = Static + + export const UserProfile = Type.Object({ + user: UserType, + admin: AdminUser, + }); + + export type UserProfile = Static + `, + ), + ) + }) + + test('should disambiguate interface resolution across multiple files', () => { + // Create first file with BaseEntity + createSourceFile( + project, + ` + export interface BaseEntity { + id: string; + createdAt: Date; + } + `, + 'common-types.ts', + ) + + // Create second file with different BaseEntity + createSourceFile( + project, + ` + export interface BaseEntity { + entityId: number; + version: number; + } + `, + 'legacy-types.ts', + ) + + // Create main file that extends both + const mainFile = createSourceFile( + project, + ` + import { BaseEntity as CommonBase } from "./common-types"; + import { BaseEntity as LegacyBase } from "./legacy-types"; + + interface ModernUser extends CommonBase { + email: string; + } + + interface LegacyUser extends LegacyBase { + username: string; + } + `, + ) + + const result = generateFormattedCode(mainFile) + + expect(result).toContain('ModernUser') + expect(result).toContain('LegacyUser') + expect(result).toBe( + formatWithPrettier( + ` + export const CommonBase = Type.Object({ + id: Type.String(), + createdAt: Type.Date(), + }); + + export type CommonBase = Static + + export const LegacyBase = Type.Object({ + entityId: Type.Number(), + version: Type.Number(), + }); + + export type LegacyBase = Static + + export const ModernUser = Type.Composite([ + CommonBase, + Type.Object({ + email: Type.String(), + }), + ]); + + export type ModernUser = Static + + export const LegacyUser = Type.Composite([ + LegacyBase, + Type.Object({ + username: Type.String(), + }), + ]); + + export type LegacyUser = Static + `, + ), + ) + }) +}) diff --git a/tests/traverse/dependency-ordering.test.ts b/tests/traverse/dependency-ordering.test.ts index c661bd5..71f48c9 100644 --- a/tests/traverse/dependency-ordering.test.ts +++ b/tests/traverse/dependency-ordering.test.ts @@ -1,4 +1,5 @@ import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' @@ -8,6 +9,7 @@ describe('Dependency ordering', () => { beforeEach(() => { project = new Project() + resolverStore.clear() }) test('should define StringSnakDataValue before using it', () => { diff --git a/tests/traverse/dependency-traversal.integration.test.ts b/tests/traverse/dependency-traversal.integration.test.ts index 485227e..4b79903 100644 --- a/tests/traverse/dependency-traversal.integration.test.ts +++ b/tests/traverse/dependency-traversal.integration.test.ts @@ -1,4 +1,5 @@ import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' @@ -10,6 +11,7 @@ describe('Dependency Traversal', () => { beforeEach(() => { project = new Project() traverser = new DependencyTraversal() + resolverStore.clear() }) describe('collectFromImports', () => { diff --git a/tests/traverse/maincode-filter.test.ts b/tests/traverse/maincode-filter.test.ts index 6b28cb5..843efac 100644 --- a/tests/traverse/maincode-filter.test.ts +++ b/tests/traverse/maincode-filter.test.ts @@ -1,3 +1,4 @@ +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' @@ -7,6 +8,7 @@ describe('Maincode Filter', () => { beforeEach(() => { project = new Project() + resolverStore.clear() }) test('should filter to only maincode nodes and their dependencies', () => { diff --git a/tests/traverse/non-transitive-dependency.test.ts b/tests/traverse/non-transitive-dependency.test.ts index 7aa46c3..478cb48 100644 --- a/tests/traverse/non-transitive-dependency.test.ts +++ b/tests/traverse/non-transitive-dependency.test.ts @@ -1,3 +1,4 @@ +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' @@ -7,6 +8,7 @@ describe('Non-transitive dependency filtering', () => { beforeEach(() => { project = new Project() + resolverStore.clear() }) test('should not include unreferenced types from imported files', () => { diff --git a/tests/traverse/type-reference-extractor.test.ts b/tests/traverse/type-reference-extractor.test.ts new file mode 100644 index 0000000..617a250 --- /dev/null +++ b/tests/traverse/type-reference-extractor.test.ts @@ -0,0 +1,176 @@ +import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' +import { TypeReferenceExtractor } from '@daxserver/validation-schema-codegen/traverse/type-reference-extractor' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('TypeReferenceExtractor ambiguous name resolution', () => { + let project: Project + let nodeGraph: NodeGraph + let extractor: TypeReferenceExtractor + + beforeEach(() => { + project = new Project() + nodeGraph = new NodeGraph() + extractor = new TypeReferenceExtractor() + resolverStore.clear() + }) + + test('should disambiguate type references when multiple files have same type name', () => { + // Create first source file with User type + const userFile = project.createSourceFile( + 'user-types.ts', + ` + export type User = { + id: string; + name: string; + }; + `, + ) + + // Create second source file with different User type + const adminFile = project.createSourceFile( + 'admin-types.ts', + ` + export type User = { + userId: number; + email: string; + }; + `, + ) + + // Add both User types to the node graph with their qualified names + const userTypeAlias = userFile.getTypeAliases()[0]! + const adminTypeAlias = adminFile.getTypeAliases()[0]! + + const userQualifiedName = resolverStore.generateQualifiedName('User', userFile) + const adminQualifiedName = resolverStore.generateQualifiedName('User', adminFile) + + nodeGraph.addTypeNode(userQualifiedName, { + node: userTypeAlias, + type: 'typeAlias', + originalName: 'User', + qualifiedName: userQualifiedName, + isImported: true, + isMainCode: false, + }) + + nodeGraph.addTypeNode(adminQualifiedName, { + node: adminTypeAlias, + type: 'typeAlias', + originalName: 'User', + qualifiedName: adminQualifiedName, + isImported: true, + isMainCode: false, + }) + + // Also populate the resolverStore with alias mappings + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile: userFile, + aliasName: 'UserType', + }) + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile: adminFile, + aliasName: 'AdminUser', + }) + + // Create a main file that references User + const mainFile = project.createSourceFile( + 'main.ts', + ` + import { User as UserType } from "./user-types"; + import { User as AdminUser } from "./admin-types"; + + type UserProfile = { + user: UserType; + admin: AdminUser; + }; + `, + ) + + const userProfileType = mainFile.getTypeAliases()[0]! + const typeNode = userProfileType.getTypeNode()! + + // Extract type references from the UserProfile type + const references = extractor.extractTypeReferences(typeNode) + + expect(references.length).toBeGreaterThan(0) + }) + + test('should disambiguate type references when multiple files have same interface name', () => { + // Create two files with same interface name + const commonFile = project.createSourceFile( + 'common-types.ts', + ` + export interface BaseEntity { + id: string; + createdAt: Date; + } + `, + ) + + const legacyFile = project.createSourceFile( + 'legacy-types.ts', + ` + export interface BaseEntity { + entityId: number; + version: number; + } + `, + ) + + // Add both BaseEntity interfaces to the node graph + const commonInterface = commonFile.getInterfaces()[0]! + const legacyInterface = legacyFile.getInterfaces()[0]! + + const commonQualifiedName = resolverStore.generateQualifiedName('BaseEntity', commonFile) + const legacyQualifiedName = resolverStore.generateQualifiedName('BaseEntity', legacyFile) + + nodeGraph.addTypeNode(commonQualifiedName, { + node: commonInterface, + type: 'interface', + originalName: 'BaseEntity', + qualifiedName: commonQualifiedName, + isImported: true, + isMainCode: false, + }) + + nodeGraph.addTypeNode(legacyQualifiedName, { + node: legacyInterface, + type: 'interface', + originalName: 'BaseEntity', + qualifiedName: legacyQualifiedName, + isImported: true, + isMainCode: false, + }) + + // Create main file with interface inheritance + const mainFile = project.createSourceFile( + 'main.ts', + ` + import { BaseEntity as CommonBase } from "./common-types"; + import { BaseEntity as LegacyBase } from "./legacy-types"; + + interface ModernUser extends CommonBase { + email: string; + } + + interface LegacyUser extends LegacyBase { + username: string; + } + `, + ) + + const modernUserInterface = mainFile.getInterfaces()[0]! + const legacyUserInterface = mainFile.getInterfaces()[1]! + + // Extract references from both interfaces + const modernReferences = extractor.extractTypeReferences(modernUserInterface) + const legacyReferences = extractor.extractTypeReferences(legacyUserInterface) + + expect(modernReferences.length).toBeGreaterThanOrEqual(0) + expect(legacyReferences.length).toBeGreaterThanOrEqual(0) + }) +}) diff --git a/tests/utils/resolver-store.test.ts b/tests/utils/resolver-store.test.ts new file mode 100644 index 0000000..e423cce --- /dev/null +++ b/tests/utils/resolver-store.test.ts @@ -0,0 +1,319 @@ +import type { TypeMappingInput } from '@daxserver/validation-schema-codegen/utils/resolver-store' +import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('ResolverStore', () => { + let project: Project + + beforeEach(() => { + project = new Project() + resolverStore.clear() + }) + + test('should add and retrieve type mappings by original name', () => { + const sourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + const mapping: TypeMappingInput = { + originalName: 'User', + sourceFile, + aliasName: 'UserType', + } + + resolverStore.addTypeMapping(mapping) + const retrieved = resolverStore.getTypeMappingByOriginalName('User') + + expect(retrieved?.originalName).toBe('User') + expect(retrieved?.aliasName).toBe('UserType') + expect(retrieved?.qualifiedName).toContain('User') + }) + + test('should retrieve type mappings by qualified name', () => { + const sourceFile = project.createSourceFile( + 'entities/product.ts', + 'export type Product = { id: string }', + ) + const mapping: TypeMappingInput = { + originalName: 'Product', + sourceFile, + aliasName: 'ProductType', + } + + resolverStore.addTypeMapping(mapping) + const retrieved = resolverStore.getTypeMappingByOriginalName('Product') + const qualifiedName = retrieved?.qualifiedName + + expect(qualifiedName).toBeDefined() + if (qualifiedName) { + const retrievedByQualified = resolverStore.getTypeMappingByQualifiedName(qualifiedName) + expect(retrievedByQualified?.originalName).toBe('Product') + } + }) + + test('should retrieve type mappings by alias name', () => { + const sourceFile = project.createSourceFile( + 'domain/order.ts', + 'export type Order = { id: string }', + ) + const mapping: TypeMappingInput = { + originalName: 'Order', + sourceFile, + aliasName: 'OrderType', + } + + resolverStore.addTypeMapping(mapping) + const retrieved = resolverStore.getTypeMappingByAliasName('OrderType') + + expect(retrieved?.originalName).toBe('Order') + expect(retrieved?.aliasName).toBe('OrderType') + }) + + test('should handle mappings without alias names', () => { + const sourceFile = project.createSourceFile( + 'models/category.ts', + 'export type Category = { id: string }', + ) + const mapping: TypeMappingInput = { + originalName: 'Category', + sourceFile, + } + + resolverStore.addTypeMapping(mapping) + const retrieved = resolverStore.getTypeMappingByOriginalName('Category') + + expect(retrieved?.originalName).toBe('Category') + expect(retrieved?.qualifiedName).toContain('Category') + expect(retrieved?.aliasName).toBeUndefined() + }) + + test('should resolve qualified names correctly', () => { + const userSourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + const productSourceFile = project.createSourceFile( + 'entities/product.ts', + 'export type Product = { id: string }', + ) + + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile: userSourceFile, + aliasName: 'UserType', + }) + + resolverStore.addTypeMapping({ + originalName: 'Product', + sourceFile: productSourceFile, + }) + + const userQualifiedName = resolverStore.resolveQualifiedName('User') + const userTypeQualifiedName = resolverStore.resolveQualifiedName('UserType') + const productQualifiedName = resolverStore.resolveQualifiedName('Product') + const unknownQualifiedName = resolverStore.resolveQualifiedName('Unknown') + + expect(userQualifiedName).toContain('User') + expect(userTypeQualifiedName).toContain('User') + expect(productQualifiedName).toContain('Product') + expect(unknownQualifiedName).toBeNull() + }) + + test('should resolve alias names correctly', () => { + const userSourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + const productSourceFile = project.createSourceFile( + 'entities/product.ts', + 'export type Product = { id: string }', + ) + + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile: userSourceFile, + aliasName: 'UserType', + }) + + resolverStore.addTypeMapping({ + originalName: 'Product', + sourceFile: productSourceFile, + }) + + expect(resolverStore.resolveAliasName('User')).toBe('UserType') + expect(resolverStore.resolveAliasName('Product')).toBe('Product') + expect(resolverStore.resolveAliasName('Unknown')).toBe('Unknown') + }) + + test('should check if type mapping exists', () => { + const sourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile, + }) + + expect(resolverStore.hasTypeMapping('User')).toBe(true) + expect(resolverStore.hasTypeMapping('NonExistent')).toBe(false) + }) + + test('should return all type mappings', () => { + const userSourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + const productSourceFile = project.createSourceFile( + 'entities/product.ts', + 'export type Product = { id: string }', + ) + const orderSourceFile = project.createSourceFile( + 'domain/order.ts', + 'export type Order = { id: string }', + ) + + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile: userSourceFile, + }) + + resolverStore.addTypeMapping({ + originalName: 'Product', + sourceFile: productSourceFile, + }) + + resolverStore.addTypeMapping({ + originalName: 'Order', + sourceFile: orderSourceFile, + }) + + const allMappings = resolverStore.getAllTypeMappings() + expect(allMappings.size).toBe(3) + + // Check that the mappings contain the expected original names in their values + const mappingValues = Array.from(allMappings.values()) + const originalNames = mappingValues.map((m) => m.originalName) + expect(originalNames).toContain('User') + expect(originalNames).toContain('Product') + expect(originalNames).toContain('Order') + }) + + test('should return qualified names', () => { + const userSourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + const productSourceFile = project.createSourceFile( + 'entities/product.ts', + 'export type Product = { id: string }', + ) + + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile: userSourceFile, + }) + + resolverStore.addTypeMapping({ + originalName: 'Product', + sourceFile: productSourceFile, + }) + + const qualifiedNames = resolverStore.getQualifiedNames() + expect(qualifiedNames).toHaveLength(2) + expect(qualifiedNames.some((name) => name.includes('User'))).toBe(true) + expect(qualifiedNames.some((name) => name.includes('Product'))).toBe(true) + }) + + test('should return original names', () => { + const userSourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + const productSourceFile = project.createSourceFile( + 'entities/product.ts', + 'export type Product = { id: string }', + ) + + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile: userSourceFile, + }) + + resolverStore.addTypeMapping({ + originalName: 'Product', + sourceFile: productSourceFile, + }) + + const originalNames = resolverStore.getOriginalNames() + expect(originalNames).toEqual(['User', 'Product']) + }) + + test('should return alias names', () => { + const userSourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + const productSourceFile = project.createSourceFile( + 'entities/product.ts', + 'export type Product = { id: string }', + ) + + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile: userSourceFile, + aliasName: 'UserType', + }) + + resolverStore.addTypeMapping({ + originalName: 'Product', + sourceFile: productSourceFile, + }) + + const aliasNames = resolverStore.getAliasNames() + expect(aliasNames).toEqual(['UserType']) + }) + + test('should clear all mappings', () => { + const sourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile, + }) + + expect(resolverStore.size()).toBe(1) + resolverStore.clear() + expect(resolverStore.size()).toBe(0) + }) + + test('should return correct size', () => { + expect(resolverStore.size()).toBe(0) + + const userSourceFile = project.createSourceFile( + 'models/user.ts', + 'export type User = { id: string }', + ) + const productSourceFile = project.createSourceFile( + 'entities/product.ts', + 'export type Product = { id: string }', + ) + + resolverStore.addTypeMapping({ + originalName: 'User', + sourceFile: userSourceFile, + }) + expect(resolverStore.size()).toBe(1) + + resolverStore.addTypeMapping({ + originalName: 'Product', + sourceFile: productSourceFile, + }) + expect(resolverStore.size()).toBe(2) + }) +})