diff --git a/docs/dependency-management.md b/docs/dependency-management.md index 1608c4a..d37f419 100644 --- a/docs/dependency-management.md +++ b/docs/dependency-management.md @@ -1,6 +1,6 @@ # Dependency Management -Graph-based dependency analysis for proper type processing order. +Graph-based dependency analysis for proper type processing order with intelligent filtering of unconnected dependencies. ## Components @@ -9,6 +9,7 @@ Graph-based dependency analysis for proper type processing order. Main orchestrator in `src/traverse/dependency-traversal.ts`: - Coordinates dependency collection +- Filters out unconnected dependencies - Returns topologically sorted nodes ### FileGraph @@ -28,11 +29,20 @@ Handles type-level dependencies in `src/traverse/node-graph.ts`: ## Process -1. Collect local types from main file -2. Process import chains recursively -3. Extract type dependencies -4. Build dependency graph -5. Topological sort for processing order +1. Collect local types from main file (automatically marked as required) +2. Process import chains recursively (adds types to graph) +3. Extract type dependencies (marks referenced types as required) +4. Filter to only connected dependencies +5. Build dependency graph +6. Topological sort for processing order + +## Dependency Filtering + +The system now filters out unconnected dependencies to optimize output: + +- **Local types**: Always included (defined in main file) +- **Imported types**: Only included if actually referenced by other types +- **Transitive dependencies**: Automatically included when their parent types are referenced ## Circular Dependencies diff --git a/docs/maincode-filtering.md b/docs/maincode-filtering.md new file mode 100644 index 0000000..54523d7 --- /dev/null +++ b/docs/maincode-filtering.md @@ -0,0 +1,115 @@ +# Maincode Filtering + +The code generator automatically filters the output to include only maincode nodes and their connected dependencies. This feature reduces the generated output by excluding imported types that are not actually used by the main code. + +## Overview + +The code generator includes only the following nodes in the output: + +1. **Maincode nodes**: Types defined in the main source file (not imported) +2. **Connected dependencies**: Types that are directly or indirectly referenced by maincode nodes + +Imported types that are not referenced anywhere in the dependency chain are automatically filtered out. + +## Example + +Consider the following file structure: + +**external.ts** + +```typescript +export type UsedType = { + value: string +} + +export type UnusedType = { + unused: boolean +} +``` + +**main.ts** + +```typescript +import { UsedType, UnusedType } from './external' + +export interface MainInterface { + data: UsedType // UsedType is referenced and will be included +} + +export type SimpleType = { + id: string +} + +// UnusedType is imported but not referenced, so it will be filtered out +``` + +### Generated Output + +```typescript +const result = generateCode({ + filePath: './main.ts', +}) +``` + +**Output automatically excludes unconnected imports:** + +```typescript +export const UsedType = Type.Object({ + value: Type.String(), +}) + +export const MainInterface = Type.Object({ + data: UsedType, +}) + +export const SimpleType = Type.Object({ + id: Type.String(), +}) + +// UnusedType is not included because it's not connected to any maincode node +``` + +## Dependency Chain Handling + +The filtering algorithm correctly handles deep dependency chains. If a maincode node references a type that has its own dependencies, all dependencies in the chain are included: + +**level1.ts** + +```typescript +export type Level1 = { + value: string +} +``` + +**level2.ts** + +```typescript +import { Level1 } from './level1' +export type Level2 = { + level1: Level1 +} +``` + +**main.ts** + +```typescript +import { Level2 } from './level2' + +export interface MainType { + data: Level2 +} +``` + +The output automatically includes `Level1`, `Level2`, and `MainType` because they form a complete dependency chain starting from the maincode node `MainType`. + +## Implementation Details + +The filtering algorithm: + +1. Identifies maincode nodes (nodes with `isImported: false`) and marks them as required +2. Analyzes type references to identify which imported types are actually used +3. Marks referenced types and their transitive dependencies as required +4. Filters the final output to include only required nodes +5. Returns the filtered nodes in topological order to maintain proper dependency ordering + +This approach ensures that only types that are part of a connected dependency graph starting from maincode nodes are included in the output. diff --git a/docs/overview.md b/docs/overview.md index dff28ac..75f610c 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -48,4 +48,5 @@ export type User = Static - [handler-system.md](./handler-system.md) - Type conversion - [compiler-configuration.md](./compiler-configuration.md) - Compiler options and script targets - [dependency-management.md](./dependency-management.md) - Dependency analysis +- [maincode-filtering.md](./maincode-filtering.md) - Filtering output to maincode and dependencies - [testing.md](./testing.md) - Testing diff --git a/src/traverse/dependency-extractor.ts b/src/traverse/dependency-extractor.ts new file mode 100644 index 0000000..eed5137 --- /dev/null +++ b/src/traverse/dependency-extractor.ts @@ -0,0 +1,57 @@ +import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' +import { TypeReferenceExtractor } from '@daxserver/validation-schema-codegen/traverse/type-reference-extractor' +import { + EnumDeclaration, + FunctionDeclaration, + InterfaceDeclaration, + Node, + TypeAliasDeclaration, +} from 'ts-morph' + +export const extractDependencies = (nodeGraph: NodeGraph, requiredNodeIds: Set): void => { + const processedNodes = new Set() + const nodesToProcess = new Set(requiredNodeIds) + const typeReferenceExtractor = new TypeReferenceExtractor(nodeGraph) + + // Process nodes iteratively until no new dependencies are found + while (nodesToProcess.size > 0) { + const currentNodeId = Array.from(nodesToProcess)[0] + if (!currentNodeId) break + + nodesToProcess.delete(currentNodeId) + + if (processedNodes.has(currentNodeId)) continue + processedNodes.add(currentNodeId) + + const nodeData = nodeGraph.getNode(currentNodeId) + if (!nodeData) continue + + let nodeToAnalyze: Node | undefined + + if (nodeData.type === 'typeAlias') { + const typeAlias = nodeData.node as TypeAliasDeclaration + nodeToAnalyze = typeAlias.getTypeNode() + } else if (nodeData.type === 'interface') { + nodeToAnalyze = nodeData.node as InterfaceDeclaration + } else if (nodeData.type === 'enum') { + nodeToAnalyze = nodeData.node as EnumDeclaration + } else if (nodeData.type === 'function') { + nodeToAnalyze = nodeData.node as FunctionDeclaration + } + + if (!nodeToAnalyze) continue + + const typeReferences = typeReferenceExtractor.extractTypeReferences(nodeToAnalyze) + + for (const referencedType of typeReferences) { + if (nodeGraph.hasNode(referencedType)) { + // Only add to required if not already processed + if (!requiredNodeIds.has(referencedType)) { + requiredNodeIds.add(referencedType) + nodesToProcess.add(referencedType) + } + nodeGraph.addDependency(referencedType, currentNodeId) + } + } + } +} diff --git a/src/traverse/dependency-traversal.ts b/src/traverse/dependency-traversal.ts index 0846dfa..16dbb66 100644 --- a/src/traverse/dependency-traversal.ts +++ b/src/traverse/dependency-traversal.ts @@ -1,324 +1,60 @@ +import { extractDependencies } from '@daxserver/validation-schema-codegen/traverse/dependency-extractor' import { FileGraph } from '@daxserver/validation-schema-codegen/traverse/file-graph' +import { ImportCollector } from '@daxserver/validation-schema-codegen/traverse/import-collector' +import { addLocalTypes } from '@daxserver/validation-schema-codegen/traverse/local-type-collector' import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' -import { generateQualifiedNodeName } from '@daxserver/validation-schema-codegen/utils/generate-qualified-name' import { GraphVisualizer, type VisualizationOptions, } from '@daxserver/validation-schema-codegen/utils/graph-visualizer' import { hasCycle, topologicalSort } from 'graphology-dag' -import { - EnumDeclaration, - FunctionDeclaration, - ImportDeclaration, - InterfaceDeclaration, - Node, - SourceFile, - SyntaxKind, - TypeAliasDeclaration, -} from 'ts-morph' +import { SourceFile } from 'ts-morph' -/** - * Dependency traversal class for AST traversal, dependency collection, and analysis - * Uses separate graphs for files and nodes for better separation of concerns - */ export class DependencyTraversal { private fileGraph = new FileGraph() private nodeGraph = new NodeGraph() + private maincodeNodeIds = new Set() + private requiredNodeIds = new Set() + private importCollector = new ImportCollector(this.fileGraph, this.nodeGraph) - /** - * Start the traversal process from the main source file - * This method handles the complete recursive traversal and returns sorted nodes - */ startTraversal(mainSourceFile: SourceFile): TraversedNode[] { // Mark main source file nodes as main code - this.addLocalTypes(mainSourceFile, true) + addLocalTypes(mainSourceFile, this.nodeGraph, this.maincodeNodeIds, this.requiredNodeIds) // Start recursive traversal from imports const importDeclarations = mainSourceFile.getImportDeclarations() - this.collectFromImports(importDeclarations, true) + this.importCollector.collectFromImports(importDeclarations) // Extract dependencies for all nodes - this.extractDependencies() + extractDependencies(this.nodeGraph, this.requiredNodeIds) - // Return topologically sorted nodes + // Return topologically sorted required nodes return this.getNodesToPrint() } - /** - * Add local types from a source file to the node graph - */ - addLocalTypes(sourceFile: SourceFile, isMainCode: boolean = false): void { - const typeAliases = sourceFile.getTypeAliases() - const interfaces = sourceFile.getInterfaces() - const enums = sourceFile.getEnums() - const functions = sourceFile.getFunctions() - - // First pass: Add all nodes to the graph without extracting dependencies - - // Collect type aliases - for (const typeAlias of typeAliases) { - const typeName = typeAlias.getName() - const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile()) - this.nodeGraph.addTypeNode(qualifiedName, { - node: typeAlias, - type: 'typeAlias', - originalName: typeName, - qualifiedName, - isImported: false, - isMainCode, - }) - } - - // Collect interfaces - for (const interfaceDecl of interfaces) { - const interfaceName = interfaceDecl.getName() - const qualifiedName = generateQualifiedNodeName(interfaceName, interfaceDecl.getSourceFile()) - this.nodeGraph.addTypeNode(qualifiedName, { - node: interfaceDecl, - type: 'interface', - originalName: interfaceName, - qualifiedName, - isImported: false, - isMainCode, - }) - } - - // Collect enums - for (const enumDecl of enums) { - const enumName = enumDecl.getName() - const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile()) - this.nodeGraph.addTypeNode(qualifiedName, { - node: enumDecl, - type: 'enum', - originalName: enumName, - qualifiedName, - isImported: false, - isMainCode, - }) - } - - // Collect functions - for (const functionDecl of functions) { - const functionName = functionDecl.getName() - if (!functionName) continue - - const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile()) - this.nodeGraph.addTypeNode(qualifiedName, { - node: functionDecl, - type: 'function', - originalName: functionName, - qualifiedName, - isImported: false, - isMainCode, - }) - } - } - - /** - * Extract dependencies for all nodes in the graph - */ - extractDependencies(): void { - for (const nodeId of this.nodeGraph.nodes()) { - const nodeData = this.nodeGraph.getNode(nodeId) - - let nodeToAnalyze: Node | undefined - - if (nodeData.type === 'typeAlias') { - const typeAlias = nodeData.node as TypeAliasDeclaration - nodeToAnalyze = typeAlias.getTypeNode() - } else if (nodeData.type === 'interface') { - nodeToAnalyze = nodeData.node as InterfaceDeclaration - } else if (nodeData.type === 'enum') { - nodeToAnalyze = nodeData.node as EnumDeclaration - } else if (nodeData.type === 'function') { - nodeToAnalyze = nodeData.node as FunctionDeclaration - } - - if (!nodeToAnalyze) continue - - const typeReferences = this.extractTypeReferences(nodeToAnalyze) - - for (const referencedType of typeReferences) { - if (this.nodeGraph.hasNode(referencedType)) { - this.nodeGraph.addDependency(referencedType, nodeId) - } - } - } - } - - /** - * Collect dependencies from import declarations - */ - collectFromImports(importDeclarations: ImportDeclaration[], isMainCode: boolean): void { - for (const importDecl of importDeclarations) { - const moduleSourceFile = importDecl.getModuleSpecifierSourceFile() - if (!moduleSourceFile) continue - - const filePath = moduleSourceFile.getFilePath() - - // Prevent infinite loops by tracking visited files - if (this.fileGraph.hasNode(filePath)) continue - this.fileGraph.addFile(filePath, moduleSourceFile) - - const imports = moduleSourceFile.getImportDeclarations() - const typeAliases = moduleSourceFile.getTypeAliases() - const interfaces = moduleSourceFile.getInterfaces() - const enums = moduleSourceFile.getEnums() - const functions = moduleSourceFile.getFunctions() - - // Add all imported types to the graph - for (const typeAlias of typeAliases) { - const typeName = typeAlias.getName() - const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile()) - this.nodeGraph.addTypeNode(qualifiedName, { - node: typeAlias, - type: 'typeAlias', - originalName: typeName, - qualifiedName, - isImported: true, - isMainCode, - }) - } - - for (const interfaceDecl of interfaces) { - const interfaceName = interfaceDecl.getName() - const qualifiedName = generateQualifiedNodeName( - interfaceName, - interfaceDecl.getSourceFile(), - ) - this.nodeGraph.addTypeNode(qualifiedName, { - node: interfaceDecl, - type: 'interface', - originalName: interfaceName, - qualifiedName, - isImported: true, - isMainCode, - }) - } - - for (const enumDecl of enums) { - const enumName = enumDecl.getName() - const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile()) - this.nodeGraph.addTypeNode(qualifiedName, { - node: enumDecl, - type: 'enum', - originalName: enumName, - qualifiedName, - isImported: true, - isMainCode, - }) - } - - for (const functionDecl of functions) { - const functionName = functionDecl.getName() - if (!functionName) continue - - const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile()) - this.nodeGraph.addTypeNode(qualifiedName, { - node: functionDecl, - type: 'function', - originalName: functionName, - qualifiedName, - isImported: true, - isMainCode, - }) - } - - // Recursively collect from nested imports (mark as transitive) - this.collectFromImports(imports, false) - } - } - /** * Get nodes in dependency order from graph * Handles circular dependencies gracefully by falling back to simple node order */ getNodesToPrint(): TraversedNode[] { - const nodes = hasCycle(this.nodeGraph) + // 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) - return nodes.map((nodeId: string) => this.nodeGraph.getNode(nodeId)) + const filteredNodes = allNodesInOrder + .filter((nodeId: string) => this.requiredNodeIds.has(nodeId)) + .map((nodeId: string) => this.nodeGraph.getNode(nodeId)) + + return filteredNodes } - /** - * Generate HTML visualization of the dependency graph - */ async visualizeGraph(options: VisualizationOptions = {}): Promise { return GraphVisualizer.generateVisualization(this.nodeGraph, options) } - /** - * Get the node graph for debugging purposes - */ getNodeGraph(): NodeGraph { return this.nodeGraph } - - private extractTypeReferences(node: Node): string[] { - const references: string[] = [] - const visited = new Set() - - const traverse = (node: Node): void => { - if (visited.has(node)) return - visited.add(node) - - if (Node.isTypeReference(node)) { - const typeName = node.getTypeName().getText() - - for (const qualifiedName of this.nodeGraph.nodes()) { - const nodeData = this.nodeGraph.getNode(qualifiedName) - if (nodeData.originalName === typeName) { - references.push(qualifiedName) - break - } - } - } - - // Handle typeof expressions (TypeQuery nodes) - if (Node.isTypeQuery(node)) { - const exprName = node.getExprName() - - 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 - } - } - } - } - - // Handle interface inheritance (extends clauses) - if (Node.isInterfaceDeclaration(node)) { - const heritageClauses = node.getHeritageClauses() - - for (const heritageClause of heritageClauses) { - if (heritageClause.getToken() !== SyntaxKind.ExtendsKeyword) continue - - for (const typeNode of heritageClause.getTypeNodes()) { - const typeName = typeNode.getText() - - for (const qualifiedName of this.nodeGraph.nodes()) { - const nodeData = this.nodeGraph.getNode(qualifiedName) - if (nodeData.originalName === typeName) { - references.push(qualifiedName) - break - } - } - } - } - } - - node.forEachChild(traverse) - } - - traverse(node) - - return references - } } diff --git a/src/traverse/import-collector.ts b/src/traverse/import-collector.ts new file mode 100644 index 0000000..fd30dd2 --- /dev/null +++ b/src/traverse/import-collector.ts @@ -0,0 +1,91 @@ +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 { ImportDeclaration } from 'ts-morph' + +export class ImportCollector { + constructor( + private fileGraph: FileGraph, + private nodeGraph: NodeGraph, + ) {} + + collectFromImports(importDeclarations: ImportDeclaration[]): void { + for (const importDecl of importDeclarations) { + const moduleSourceFile = importDecl.getModuleSpecifierSourceFile() + if (!moduleSourceFile) continue + + const filePath = moduleSourceFile.getFilePath() + + // Prevent infinite loops by tracking visited files + if (this.fileGraph.hasNode(filePath)) continue + this.fileGraph.addFile(filePath, moduleSourceFile) + + const imports = moduleSourceFile.getImportDeclarations() + const typeAliases = moduleSourceFile.getTypeAliases() + const interfaces = moduleSourceFile.getInterfaces() + const enums = moduleSourceFile.getEnums() + const functions = moduleSourceFile.getFunctions() + + // Add all imported types to the graph + for (const typeAlias of typeAliases) { + const typeName = typeAlias.getName() + const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile()) + this.nodeGraph.addTypeNode(qualifiedName, { + node: typeAlias, + type: 'typeAlias', + originalName: typeName, + qualifiedName, + isImported: true, + isMainCode: false, + }) + } + + for (const interfaceDecl of interfaces) { + const interfaceName = interfaceDecl.getName() + const qualifiedName = generateQualifiedNodeName( + interfaceName, + interfaceDecl.getSourceFile(), + ) + this.nodeGraph.addTypeNode(qualifiedName, { + node: interfaceDecl, + type: 'interface', + originalName: interfaceName, + qualifiedName, + isImported: true, + isMainCode: false, + }) + } + + for (const enumDecl of enums) { + const enumName = enumDecl.getName() + const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile()) + this.nodeGraph.addTypeNode(qualifiedName, { + node: enumDecl, + type: 'enum', + originalName: enumName, + qualifiedName, + isImported: true, + isMainCode: false, + }) + } + + for (const functionDecl of functions) { + const functionName = functionDecl.getName() + if (!functionName) continue + + const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile()) + this.nodeGraph.addTypeNode(qualifiedName, { + node: functionDecl, + type: 'function', + originalName: functionName, + qualifiedName, + isImported: true, + isMainCode: false, + }) + } + + // Recursively collect from nested imports (mark as transitive) + this.collectFromImports(imports) + } + } +} diff --git a/src/traverse/local-type-collector.ts b/src/traverse/local-type-collector.ts new file mode 100644 index 0000000..ef4ca50 --- /dev/null +++ b/src/traverse/local-type-collector.ts @@ -0,0 +1,104 @@ +import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' +import { generateQualifiedNodeName } from '@daxserver/validation-schema-codegen/utils/generate-qualified-name' +import { SourceFile } from 'ts-morph' + +export const addLocalTypes = ( + sourceFile: SourceFile, + nodeGraph: NodeGraph, + maincodeNodeIds: Set, + requiredNodeIds: Set, +): void => { + const typeAliases = sourceFile.getTypeAliases() + const interfaces = sourceFile.getInterfaces() + const enums = sourceFile.getEnums() + const functions = sourceFile.getFunctions() + + // If main file has no local types but has imports, add all imported types as required + if ( + typeAliases.length === 0 && + interfaces.length === 0 && + enums.length === 0 && + functions.length === 0 + ) { + const importDeclarations = sourceFile.getImportDeclarations() + for (const importDecl of importDeclarations) { + const namedImports = importDecl.getNamedImports() + for (const namedImport of namedImports) { + const importName = namedImport.getName() + const importSourceFile = importDecl.getModuleSpecifierSourceFile() + if (importSourceFile) { + const qualifiedName = generateQualifiedNodeName(importName, importSourceFile) + requiredNodeIds.add(qualifiedName) + } + } + } + + return + } + + // Collect type aliases + for (const typeAlias of typeAliases) { + const typeName = typeAlias.getName() + const qualifiedName = generateQualifiedNodeName(typeName, typeAlias.getSourceFile()) + maincodeNodeIds.add(qualifiedName) + requiredNodeIds.add(qualifiedName) + nodeGraph.addTypeNode(qualifiedName, { + node: typeAlias, + type: 'typeAlias', + originalName: typeName, + qualifiedName, + isImported: false, + isMainCode: true, + }) + } + + // Collect interfaces + for (const interfaceDecl of interfaces) { + const interfaceName = interfaceDecl.getName() + const qualifiedName = generateQualifiedNodeName(interfaceName, interfaceDecl.getSourceFile()) + maincodeNodeIds.add(qualifiedName) + requiredNodeIds.add(qualifiedName) + nodeGraph.addTypeNode(qualifiedName, { + node: interfaceDecl, + type: 'interface', + originalName: interfaceName, + qualifiedName, + isImported: false, + isMainCode: true, + }) + } + + // Collect enums + for (const enumDecl of enums) { + const enumName = enumDecl.getName() + const qualifiedName = generateQualifiedNodeName(enumName, enumDecl.getSourceFile()) + maincodeNodeIds.add(qualifiedName) + requiredNodeIds.add(qualifiedName) + nodeGraph.addTypeNode(qualifiedName, { + node: enumDecl, + type: 'enum', + originalName: enumName, + qualifiedName, + isImported: false, + isMainCode: true, + }) + } + + // Collect functions + for (const functionDecl of functions) { + const functionName = functionDecl.getName() + if (!functionName) continue + + const qualifiedName = generateQualifiedNodeName(functionName, functionDecl.getSourceFile()) + maincodeNodeIds.add(qualifiedName) + requiredNodeIds.add(qualifiedName) + nodeGraph.addTypeNode(qualifiedName, { + node: functionDecl, + type: 'function', + originalName: functionName, + qualifiedName, + isImported: false, + isMainCode: true, + }) + } +} diff --git a/src/traverse/type-reference-extractor.ts b/src/traverse/type-reference-extractor.ts new file mode 100644 index 0000000..fba2f58 --- /dev/null +++ b/src/traverse/type-reference-extractor.ts @@ -0,0 +1,121 @@ +import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph' +import { Node, SyntaxKind } from 'ts-morph' + +export class TypeReferenceExtractor { + constructor(private nodeGraph: NodeGraph) {} + + extractTypeReferences(node: Node): string[] { + const references: string[] = [] + const visited = new Set() + + const traverse = (node: Node): void => { + if (visited.has(node)) return + visited.add(node) + + if (Node.isTypeReference(node)) { + const typeName = node.getTypeName().getText() + + for (const qualifiedName of this.nodeGraph.nodes()) { + const nodeData = this.nodeGraph.getNode(qualifiedName) + if (nodeData.originalName === typeName) { + references.push(qualifiedName) + break + } + } + } + + // Handle typeof expressions (TypeQuery nodes) + if (Node.isTypeQuery(node)) { + const exprName = node.getExprName() + + 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 + } + } + } + } + + // Handle interface inheritance (extends clauses) + if (Node.isInterfaceDeclaration(node)) { + const heritageClauses = node.getHeritageClauses() + + for (const heritageClause of heritageClauses) { + if (heritageClause.getToken() !== SyntaxKind.ExtendsKeyword) continue + + 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 + } + } + + // Also extract dependencies from type arguments + const typeArguments = typeNode.getTypeArguments() + for (const typeArg of typeArguments) { + const argReferences = this.extractTypeReferences(typeArg) + references.push(...argReferences) + } + } else if (Node.isExpressionWithTypeArguments(typeNode)) { + // Handle ExpressionWithTypeArguments (e.g., EntityInfo) + const expression = typeNode.getExpression() + + 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 + } + } + } + + // Also extract dependencies from type arguments + const typeArguments = typeNode.getTypeArguments() + for (const typeArg of typeArguments) { + const argReferences = this.extractTypeReferences(typeArg) + references.push(...argReferences) + } + } + } + } + } + + // 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 + } + } + } + } + + node.forEachChild(traverse) + } + + traverse(node) + + return references + } +} diff --git a/tests/traverse/dependency-ordering.test.ts b/tests/traverse/dependency-ordering.test.ts index 2a47ddd..c661bd5 100644 --- a/tests/traverse/dependency-ordering.test.ts +++ b/tests/traverse/dependency-ordering.test.ts @@ -1,5 +1,5 @@ import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal' -import { formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' +import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' @@ -147,4 +147,265 @@ describe('Dependency ordering', () => { expect(entityInfoIndex).toBeLessThan(entityIndex) expect(entityIndex).toBeLessThan(entitiesIndex) }) + + test('should handle complex structure without including unused types', () => { + const project = new Project() + + // Create external dependency files + createSourceFile( + project, + ` + export type DataType = 'string' | 'url' | 'wikibase-item' + export type Claims = Record + `, + 'claim.ts', + ) + + createSourceFile( + project, + ` + export type Labels = Record + export type Descriptions = Record + export type Aliases = Record + `, + 'terms.ts', + ) + + createSourceFile( + project, + ` + export type Sitelinks = Record + export type UnusedSitelinkType = { unused: boolean } // Should not be included + `, + 'sitelinks.ts', + ) + + const sourceFile = createSourceFile( + project, + ` + import type { DataType, Claims } from './claim' + import type { Labels, Descriptions, Aliases } from './terms' + import type { Sitelinks } from './sitelinks' + + type NumericId = \`\${number}\` + type ItemId = \`Q\${number}\` + type PropertyId = \`P\${number}\` + type EntityId = ItemId | PropertyId + + interface EntityInfo { + id: T + title?: string + modified?: string + } + + interface Property extends EntityInfo { + type: 'property' + datatype?: DataType + labels?: Labels + descriptions?: Descriptions + aliases?: Aliases + claims?: Claims + } + + interface Item extends EntityInfo { + type: 'item' + labels?: Labels + descriptions?: Descriptions + aliases?: Aliases + claims?: Claims + sitelinks?: Sitelinks + } + + type Entity = Property | Item + type Entities = Record + `, + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier( + ` + export const NumericId = Type.TemplateLiteral([Type.Number()]) + + export type NumericId = Static + + export const ItemId = Type.TemplateLiteral([Type.Literal('Q'), Type.Number()]) + + export type ItemId = Static + + export const PropertyId = Type.TemplateLiteral([Type.Literal('P'), Type.Number()]) + + export type PropertyId = Static + + export const EntityInfo = (T: T) => Type.Object({ + id: T, + title: Type.Optional(Type.String()), + modified: Type.Optional(Type.String()), + }) + + export type EntityInfo = Static>> + + export const DataType = Type.Union([ + Type.Literal('string'), + Type.Literal('url'), + Type.Literal('wikibase-item'), + ]) + + export type DataType = Static + + export const Claims = Type.Record(Type.String(), Type.Any()) + + export type Claims = Static + + export const Labels = Type.Record(Type.String(), Type.String()) + + export type Labels = Static + + export const Descriptions = Type.Record(Type.String(), Type.String()) + + export type Descriptions = Static + + export const Aliases = Type.Record(Type.String(), Type.Array(Type.String())) + + export type Aliases = Static + + export const Sitelinks = Type.Record(Type.String(), Type.Any()) + + export type Sitelinks = Static + + export const EntityId = Type.Union([ItemId, PropertyId]) + + export type EntityId = Static + + export const Property = Type.Composite([ + EntityInfo(PropertyId), + Type.Object({ + type: Type.Literal('property'), + datatype: Type.Optional(DataType), + labels: Type.Optional(Labels), + descriptions: Type.Optional(Descriptions), + aliases: Type.Optional(Aliases), + claims: Type.Optional(Claims), + }), + ]) + + export type Property = Static + + export const Item = Type.Composite([ + EntityInfo(ItemId), + Type.Object({ + type: Type.Literal('item'), + labels: Type.Optional(Labels), + descriptions: Type.Optional(Descriptions), + aliases: Type.Optional(Aliases), + claims: Type.Optional(Claims), + sitelinks: Type.Optional(Sitelinks), + }), + ]) + + export type Item = Static + + export const Entity = Type.Union([Property, Item]) + + export type Entity = Static + + export const Entities = Type.Record(EntityId, Entity) + + export type Entities = Static + `, + true, + true, + ), + ) + }) + + test('should handle circular references', () => { + const project = new Project() + + const sourceFile = createSourceFile( + project, + ` + type Entities = Record + type SimplifiedEntities = Record + + interface EntityInfo { + id: T + } + + type EntityId = ItemId | PropertyId + type ItemId = \`Q\${number}\` + type PropertyId = \`P\${number}\` + + interface Item extends EntityInfo { + type: 'item' + } + + interface Property extends EntityInfo { + type: 'property' + } + + type Entity = Property | Item + type SimplifiedEntity = Property | Item + `, + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier( + ` + export const ItemId = Type.TemplateLiteral([Type.Literal('Q'), Type.Number()]) + + export type ItemId = Static + + export const PropertyId = Type.TemplateLiteral([Type.Literal('P'), Type.Number()]) + + export type PropertyId = Static + + export const EntityInfo = (T: T) => Type.Object({ + id: T, + }) + + export type EntityInfo = Static>> + + export const EntityId = Type.Union([ItemId, PropertyId]) + + export type EntityId = Static + + export const Item = Type.Composite([ + EntityInfo(ItemId), + Type.Object({ + type: Type.Literal('item'), + }), + ]) + + export type Item = Static + + export const Property = Type.Composite([ + EntityInfo(PropertyId), + Type.Object({ + type: Type.Literal('property'), + }), + ]) + + export type Property = Static + + export const Entity = Type.Union([Property, Item]) + + export type Entity = Static + + export const SimplifiedEntity = Type.Union([Property, Item]) + + export type SimplifiedEntity = Static + + export const Entities = Type.Record(EntityId, Entity) + + export type Entities = Static + + export const SimplifiedEntities = Type.Record(EntityId, SimplifiedEntity) + + export type SimplifiedEntities = Static + `, + true, + true, + ), + ) + }) }) diff --git a/tests/traverse/dependency-traversal.integration.test.ts b/tests/traverse/dependency-traversal.integration.test.ts index 354db8d..485227e 100644 --- a/tests/traverse/dependency-traversal.integration.test.ts +++ b/tests/traverse/dependency-traversal.integration.test.ts @@ -1,13 +1,8 @@ import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal' -import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' -import { createSourceFile } from '@test-fixtures/utils' +import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' import { beforeEach, describe, expect, test } from 'bun:test' import { Project } from 'ts-morph' -const getNodeName = (traversedNode: TraversedNode): string => { - return traversedNode.originalName -} - describe('Dependency Traversal', () => { let project: Project let traverser: DependencyTraversal @@ -30,14 +25,34 @@ describe('Dependency Traversal', () => { 'external.ts', ) - const mainFile = createSourceFile(project, 'import { User } from "./external";', 'main.ts') + const sourceFile = createSourceFile( + project, + ` + import { User } from "./external"; - traverser.startTraversal(mainFile) - const dependencies = traverser.getNodesToPrint() + type LocalType = { + user: User; + }; + `, + 'main.ts', + ) - expect(dependencies).toHaveLength(1) - expect(getNodeName(dependencies[0]!)).toBe('User') - expect(dependencies[0]!.isImported).toBe(true) + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const User = Type.Object({ + id: Type.String(), + name: Type.String(), + }); + + export type User = Static; + + export const LocalType = Type.Object({ + user: User, + }); + + export type LocalType = Static; + `), + ) }) test('should collect dependencies from multiple imports', () => { @@ -63,22 +78,44 @@ describe('Dependency Traversal', () => { 'product.ts', ) - const mainFile = createSourceFile( + const sourceFile = createSourceFile( project, ` import { User } from "./user"; import { Product } from "./product"; + + type LocalType = { + user: User; + product: Product; + }; `, 'main.ts', ) - traverser.startTraversal(mainFile) - const dependencies = traverser.getNodesToPrint() - - expect(dependencies).toHaveLength(2) - const typeNames = dependencies.map((d) => getNodeName(d)) - expect(typeNames).toContain('User') - expect(typeNames).toContain('Product') + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const User = Type.Object({ + id: Type.String(), + name: Type.String(), + }); + + export type User = Static; + + export const Product = Type.Object({ + id: Type.String(), + title: Type.String(), + }); + + export type Product = Static; + + export const LocalType = Type.Object({ + user: User, + product: Product, + }); + + export type LocalType = Static; + `), + ) }) test('should handle nested imports', () => { @@ -103,21 +140,48 @@ describe('Dependency Traversal', () => { 'user.ts', ) - const mainFile = createSourceFile(project, 'import { User } from "./user";', 'main.ts') + const sourceFile = createSourceFile( + project, + ` + import { User } from "./user"; - traverser.startTraversal(mainFile) - const dependencies = traverser.getNodesToPrint() + type LocalType = { + user: User; + }; + `, + 'main.ts', + ) - expect(dependencies).toHaveLength(2) - const typeNames = dependencies.map((d) => getNodeName(d)) - expect(typeNames).toContain('BaseType') - expect(typeNames).toContain('User') + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const BaseType = Type.Object({ + id: Type.String(), + }); + + export type BaseType = Static; + + export const User = Type.Intersect([ + BaseType, + Type.Object({ + name: Type.String(), + }), + ]); + + export type User = Static; + + export const LocalType = Type.Object({ + user: User, + }); + + export type LocalType = Static; + `), + ) }) test('should handle missing module specifier source file', () => { - const mainFile = createSourceFile(project, 'import { NonExistent } from "./non-existent";') + const sourceFile = createSourceFile(project, 'import { NonExistent } from "./non-existent";') - traverser.startTraversal(mainFile) + traverser.startTraversal(sourceFile) const dependencies = traverser.getNodesToPrint() expect(dependencies).toHaveLength(0) @@ -135,20 +199,37 @@ describe('Dependency Traversal', () => { 'user.ts', ) - const mainFile = createSourceFile( + const sourceFile = createSourceFile( project, ` import { User } from "./user"; import { User as UserAlias } from "./user"; + + type LocalType = { + user: User; + userAlias: UserAlias; + }; `, 'main.ts', ) - traverser.startTraversal(mainFile) - const dependencies = traverser.getNodesToPrint() - - expect(dependencies).toHaveLength(1) - expect(getNodeName(dependencies[0]!)).toBe('User') + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const User = Type.Object({ + id: Type.String(), + name: Type.String(), + }); + + export type User = Static; + + export const LocalType = Type.Object({ + user: User, + userAlias: UserAlias, + }); + + export type LocalType = Static; + `), + ) }) }) @@ -169,17 +250,23 @@ describe('Dependency Traversal', () => { `, ) - traverser.startTraversal(sourceFile) - const dependencies = traverser.getNodesToPrint() - - expect(dependencies).toHaveLength(2) - const typeNames = dependencies.map((d) => getNodeName(d)) - expect(typeNames).toContain('LocalUser') - expect(typeNames).toContain('LocalProduct') - - dependencies.forEach((dep) => { - expect(dep!.isImported).toBe(false) - }) + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const LocalUser = Type.Object({ + id: Type.String(), + name: Type.String(), + }); + + export type LocalUser = Static; + + export const LocalProduct = Type.Object({ + id: Type.String(), + title: Type.String(), + }); + + export type LocalProduct = Static; + `), + ) }) test('should not duplicate existing types', () => { @@ -193,12 +280,16 @@ describe('Dependency Traversal', () => { `, ) - traverser.addLocalTypes(sourceFile) - traverser.addLocalTypes(sourceFile) - const dependencies = traverser.getNodesToPrint() - - expect(dependencies).toHaveLength(1) - expect(getNodeName(dependencies[0]!)).toBe('User') + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const User = Type.Object({ + id: Type.String(), + name: Type.String(), + }); + + export type User = Static; + `), + ) }) }) @@ -225,14 +316,42 @@ describe('Dependency Traversal', () => { 'user.ts', ) - const mainFile = createSourceFile(project, 'import { User } from "./user";', 'main.ts') + const sourceFile = createSourceFile( + project, + ` + import { User } from "./user"; - traverser.startTraversal(mainFile) - const dependencies = traverser.getNodesToPrint() + type LocalType = { + user: User; + }; + `, + 'main.ts', + ) - expect(dependencies).toHaveLength(2) - expect(getNodeName(dependencies[0]!)).toBe('BaseType') - expect(getNodeName(dependencies[1]!)).toBe('User') + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const BaseType = Type.Object({ + id: Type.String(), + }); + + export type BaseType = Static; + + export const User = Type.Intersect([ + BaseType, + Type.Object({ + name: Type.String(), + }), + ]); + + export type User = Static; + + export const LocalType = Type.Object({ + user: User, + }); + + export type LocalType = Static; + `), + ) }) test('should handle complex dependency chains', () => { @@ -270,15 +389,47 @@ describe('Dependency Traversal', () => { 'c.ts', ) - const mainFile = createSourceFile(project, 'import { C } from "./c";', 'main.ts') + const sourceFile = createSourceFile( + project, + ` + import { C } from "./c"; - traverser.startTraversal(mainFile) - const dependencies = traverser.getNodesToPrint() + type LocalType = { + c: C; + }; + `, + 'main.ts', + ) - expect(dependencies).toHaveLength(3) - expect(getNodeName(dependencies[0]!)).toBe('A') - expect(getNodeName(dependencies[1]!)).toBe('B') - expect(getNodeName(dependencies[2]!)).toBe('C') + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const A = Type.Object({ + value: Type.String(), + }); + + export type A = Static; + + export const B = Type.Object({ + a: A, + name: Type.String(), + }); + + export type B = Static; + + export const C = Type.Object({ + b: B, + id: Type.Number(), + }); + + export type C = Static; + + export const LocalType = Type.Object({ + c: C, + }); + + export type LocalType = Static; + `), + ) }) test('should handle circular dependencies gracefully', () => { @@ -306,7 +457,7 @@ describe('Dependency Traversal', () => { 'b.ts', ) - const mainFile = createSourceFile( + const sourceFile = createSourceFile( project, ` import { A } from "./a"; @@ -315,13 +466,23 @@ describe('Dependency Traversal', () => { 'main.ts', ) - traverser.startTraversal(mainFile) - const dependencies = traverser.getNodesToPrint() - - expect(dependencies).toHaveLength(2) - const typeNames = dependencies.map((d) => getNodeName(d)) - expect(typeNames).toContain('A') - expect(typeNames).toContain('B') + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const A = Type.Object({ + b: Type.Optional(B), + value: Type.String(), + }); + + export type A = Static; + + export const B = Type.Object({ + a: Type.Optional(A), + name: Type.String(), + }); + + export type B = Static; + `), + ) }) test('should handle types with no dependencies', () => { @@ -336,17 +497,34 @@ describe('Dependency Traversal', () => { 'simple.ts', ) - const mainFile = createSourceFile( + const sourceFile = createSourceFile( project, - 'import { SimpleType } from "./simple";', + ` + import { SimpleType } from "./simple"; + + type LocalType = { + simple: SimpleType; + }; + `, 'main.ts', ) - traverser.startTraversal(mainFile) - const dependencies = traverser.getNodesToPrint() - - expect(dependencies).toHaveLength(1) - expect(getNodeName(dependencies[0]!)).toBe('SimpleType') + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const SimpleType = Type.Object({ + id: Type.String(), + name: Type.String(), + }); + + export type SimpleType = Static; + + export const LocalType = Type.Object({ + simple: SimpleType, + }); + + export type LocalType = Static; + `), + ) }) }) @@ -362,7 +540,7 @@ describe('Dependency Traversal', () => { 'external.ts', ) - const mainFile = createSourceFile( + const sourceFile = createSourceFile( project, ` import { ExternalType } from "./external"; @@ -375,19 +553,22 @@ describe('Dependency Traversal', () => { 'main.ts', ) - traverser.startTraversal(mainFile) - const dependencies = traverser.getNodesToPrint() - - expect(dependencies).toHaveLength(2) - - const externalDep = dependencies.find((d) => getNodeName(d) === 'ExternalType') - const localDep = dependencies.find((d) => getNodeName(d) === 'LocalType') - - expect(externalDep!.isImported).toBe(true) - expect(localDep!.isImported).toBe(false) - - expect(getNodeName(dependencies[0]!)).toBe('ExternalType') - expect(getNodeName(dependencies[1]!)).toBe('LocalType') + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const ExternalType = Type.Object({ + id: Type.String(), + }); + + export type ExternalType = Static; + + export const LocalType = Type.Object({ + external: ExternalType, + local: Type.String(), + }); + + export type LocalType = Static; + `), + ) }) }) }) diff --git a/tests/traverse/maincode-filter.test.ts b/tests/traverse/maincode-filter.test.ts new file mode 100644 index 0000000..6b28cb5 --- /dev/null +++ b/tests/traverse/maincode-filter.test.ts @@ -0,0 +1,232 @@ +import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('Maincode Filter', () => { + let project: Project + + beforeEach(() => { + project = new Project() + }) + + test('should filter to only maincode nodes and their dependencies', () => { + // Create external dependencies that are NOT used by maincode + createSourceFile( + project, + ` + export type UnusedExternal = { + id: string; + }; + `, + 'unused.ts', + ) + + // Create external dependencies that ARE used by maincode + createSourceFile( + project, + ` + export type UsedExternal = { + value: string; + }; + `, + 'used.ts', + ) + + // Create a dependency chain: Base -> Intermediate -> UsedExternal + createSourceFile( + project, + ` + export type Base = { + id: string; + }; + `, + 'base.ts', + ) + + createSourceFile( + project, + ` + import { Base } from "./base"; + export type Intermediate = Base & { + name: string; + }; + `, + 'intermediate.ts', + ) + + const sourceFile = createSourceFile( + project, + ` + import { UsedExternal } from "./used"; + import { Intermediate } from "./intermediate"; + import { UnusedExternal } from "./unused"; + + // This is maincode and uses UsedExternal and Intermediate + export interface MainType { + used: UsedExternal; + intermediate: Intermediate; + } + + // This is also maincode but doesn't use any imports + export type SimpleMainType = { + id: string; + name: string; + }; + + // UnusedExternal is imported but not used in any maincode types + `, + 'main.ts', + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const SimpleMainType = Type.Object({ + id: Type.String(), + name: Type.String(), + }); + + export type SimpleMainType = Static; + + export const UsedExternal = Type.Object({ + value: Type.String(), + }); + + export type UsedExternal = Static; + + export const Base = Type.Object({ + id: Type.String(), + }); + + export type Base = Static; + + export const Intermediate = Type.Intersect([Base, Type.Object({ + name: Type.String(), + })]); + + export type Intermediate = Static; + + export const MainType = Type.Object({ + used: UsedExternal, + intermediate: Intermediate, + }); + + export type MainType = Static; + `), + ) + }) + + test('should handle maincode with no dependencies', () => { + const sourceFile = createSourceFile( + project, + ` + export type SimpleType = { + id: string; + name: string; + }; + `, + 'main.ts', + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const SimpleType = Type.Object({ + id: Type.String(), + name: Type.String(), + }); + + export type SimpleType = Static; + `), + ) + }) + + test('should handle complex dependency chains from maincode', () => { + // Create a deep dependency chain + createSourceFile( + project, + ` + export type Level1 = { + value: string; + }; + `, + 'level1.ts', + ) + + createSourceFile( + project, + ` + import { Level1 } from "./level1"; + export type Level2 = { + level1: Level1; + name: string; + }; + `, + 'level2.ts', + ) + + createSourceFile( + project, + ` + import { Level2 } from "./level2"; + export type Level3 = { + level2: Level2; + id: number; + }; + `, + 'level3.ts', + ) + + // Create an unused import + createSourceFile( + project, + ` + export type Unused = { + unused: boolean; + }; + `, + 'unused.ts', + ) + + const sourceFile = createSourceFile( + project, + ` + import { Level3 } from "./level3"; + import { Unused } from "./unused"; + + export interface MainInterface { + data: Level3; + } + `, + 'main.ts', + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const Level1 = Type.Object({ + value: Type.String(), + }); + + export type Level1 = Static; + + export const Level2 = Type.Object({ + level1: Level1, + name: Type.String(), + }); + + export type Level2 = Static; + + export const Level3 = Type.Object({ + level2: Level2, + id: Type.Number(), + }); + + export type Level3 = Static; + + export const MainInterface = Type.Object({ + data: Level3, + }); + + export type MainInterface = Static; + `), + ) + }) +}) diff --git a/tests/traverse/non-transitive-dependency.test.ts b/tests/traverse/non-transitive-dependency.test.ts new file mode 100644 index 0000000..7aa46c3 --- /dev/null +++ b/tests/traverse/non-transitive-dependency.test.ts @@ -0,0 +1,116 @@ +import { createSourceFile, formatWithPrettier, generateFormattedCode } from '@test-fixtures/utils' +import { beforeEach, describe, expect, test } from 'bun:test' +import { Project } from 'ts-morph' + +describe('Non-transitive dependency filtering', () => { + let project: Project + + beforeEach(() => { + project = new Project() + }) + + test('should not include unreferenced types from imported files', () => { + createSourceFile( + project, + ` + type ApiQueryValue = string | number | true + export type ApiQueryParameters = Record + export type Url = string + export type BuildUrlFunction = (options: Readonly>>) => Url + export declare function buildUrlFactory(instanceApiEndpoint: Url): BuildUrlFunction + `, + 'build_url.d.ts', + ) + + createSourceFile( + project, + ` + import type { Url } from './build_url' + + export interface Sitelink { + site: string + title: string + url?: Url + } + `, + 'sitelinks.d.ts', + ) + + const sourceFile = createSourceFile( + project, + ` + import type { Sitelink } from './sitelinks' + + export interface Entity { + id: string + sitelinks?: Record + } + `, + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const Url = Type.String() + + export type Url = Static + + export const Sitelink = Type.Object({ + site: Type.String(), + title: Type.String(), + url: Type.Optional(Url), + }) + + export type Sitelink = Static + + export const Entity = Type.Object({ + id: Type.String(), + sitelinks: Type.Optional(Type.Record(Type.String(), Sitelink)), + }) + + export type Entity = Static + `), + ) + }) + + test('should include types that are actually referenced transitively', () => { + // Create a scenario where a type is legitimately referenced transitively + createSourceFile( + project, + ` + export type BaseId = string + export type UserId = BaseId + export type UnusedType = number // This should not be included + `, + 'types.d.ts', + ) + + const sourceFile = createSourceFile( + project, + ` + import type { UserId } from './types' + + export interface User { + id: UserId + } + `, + ) + + expect(generateFormattedCode(sourceFile)).toBe( + formatWithPrettier(` + export const BaseId = Type.String() + + export type BaseId = Static + + export const UserId = BaseId + + export type UserId = Static + + export const User = Type.Object({ + id: UserId, + }) + + export type User = Static + `), + ) + }) +})