Skip to content

Commit 6075948

Browse files
authored
feat: Improve dependency traversal for interface inheritance (#14)
This commit introduces several improvements to the dependency traversal logic, specifically handling interface inheritance scenarios: 1. Correctly extracts dependencies from interfaces that inherit from other interfaces, ensuring the correct order of dependencies. 2. Handles multiple interface inheritance, where an interface extends multiple other interfaces. 3. Supports nested interface inheritance, where an interface extends an interface that extends another interface. These changes ensure that the dependency traversal algorithm can accurately identify and order the dependencies for complex interface inheritance structures, which is crucial for generating correct code.
1 parent 6f520f1 commit 6075948

14 files changed

+1017
-79
lines changed

ARCHITECTURE.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,35 @@ The dependency system consists of three main components:
7070

7171
1. **Local Type Collection**: Adds all types from the main source file
7272
2. **Import Processing**: Recursively processes import declarations
73-
3. **Dependency Extraction**: Analyzes type references to build dependency graph
73+
3. **Dependency Extraction**: Analyzes type references to build dependency graph for all supported TypeScript constructs:
74+
- **Type Aliases**: Extracts dependencies from type alias declarations
75+
- **Interfaces**: Analyzes interface property types and heritage clauses
76+
- **Enums**: Processes enum member value dependencies
77+
- **Functions**: Extracts dependencies from parameter types, return types, and type parameters
7478
4. **Topological Sorting**: Returns nodes in proper dependency order
7579

80+
#### Graph Visualization
81+
82+
The system includes interactive graph visualization capabilities through the `GraphVisualizer` utility:
83+
84+
- **Sigma.js Integration**: Uses ES6 modules with unpkg CDN for interactive HTML-based graph visualization
85+
- **Custom Node Shapes**: Implements WebGL-based custom node programs for different TypeScript constructs:
86+
- **Diamond**: Interface types (rotated square program)
87+
- **Square**: Type alias declarations
88+
- **Triangle**: Enum declarations
89+
- **Star**: Function declarations
90+
- **Circle**: Default/other types
91+
- **ForceAtlas2 Layout**: Single optimized layout algorithm for automatic node positioning with gravity and scaling controls
92+
- **Enhanced Node Differentiation**:
93+
- **Size Variation**: Different node sizes based on type and importance (main code vs imported)
94+
- **Color Coding**: Type-specific colors with intensity variation based on import nesting level
95+
- **Shape Mapping**: Direct visual shape differentiation using custom WebGL node programs
96+
- **Visual Legend**: CSS-styled legend showing actual shape representations
97+
- **Import Nesting Visualization**: Color intensity reflects import depth, with main code having brightest colors
98+
- **Interactive Features**: Click events for node details, zoom/pan capabilities, and hover tooltips
99+
- **Export Options**: Generates standalone HTML files with embedded visualization and complete styling
100+
- **WebGL Programs**: Custom node renderers extending `@sigma/node-square` and `@sigma/node-border` packages
101+
76102
### Parser System
77103

78104
The parser system is built around a base class architecture in <mcfile name="parsers" path="src/parsers"></mcfile>:

bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"dependencies": {
77
"graphology": "^0.26.0",
88
"graphology-dag": "^0.4.1",
9+
"graphology-layout-forceatlas2": "^0.10.1",
910
"graphology-traversal": "^0.3.1",
1011
},
1112
"devDependencies": {
@@ -208,6 +209,8 @@
208209

209210
"graphology-indices": ["graphology-indices@0.17.0", "", { "dependencies": { "graphology-utils": "^2.4.2", "mnemonist": "^0.39.0" }, "peerDependencies": { "graphology-types": ">=0.20.0" } }, "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ=="],
210211

212+
"graphology-layout-forceatlas2": ["graphology-layout-forceatlas2@0.10.1", "", { "dependencies": { "graphology-utils": "^2.1.0" }, "peerDependencies": { "graphology-types": ">=0.19.0" } }, "sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ=="],
213+
211214
"graphology-traversal": ["graphology-traversal@0.3.1", "", { "dependencies": { "graphology-indices": "^0.17.0", "graphology-utils": "^2.0.0" }, "peerDependencies": { "graphology-types": ">=0.20.0" } }, "sha512-lGLrLKEDKtNgAKgHVhVftKf3cb/nuWwuVPQZHXRnN90JWn0RSjco/s+NB2ARSlMapEMlbnPgv6j++427yTnU3Q=="],
212215

213216
"graphology-types": ["graphology-types@0.24.8", "", {}, "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q=="],

package.json

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
{
22
"name": "@daxserver/validation-schema-codegen",
33
"version": "0.1.0",
4+
"description": "Codegen for validation schemas",
5+
"private": true,
46
"main": "src/index.ts",
57
"module": "src/index.ts",
8+
"type": "module",
9+
"dependencies": {
10+
"graphology": "^0.26.0",
11+
"graphology-dag": "^0.4.1",
12+
"graphology-layout-forceatlas2": "^0.10.1",
13+
"graphology-traversal": "^0.3.1"
14+
},
615
"devDependencies": {
716
"@eslint/js": "^9.34.0",
817
"@prettier/sync": "^0.6.1",
@@ -26,17 +35,9 @@
2635
"peerDependencies": {
2736
"typescript": "~5.9.2"
2837
},
29-
"description": "Codegen for validation schemas",
30-
"private": true,
3138
"scripts": {
3239
"format": "prettier --cache --write .",
3340
"typecheck": "tsc --noEmit",
3441
"lint": "eslint"
35-
},
36-
"type": "module",
37-
"dependencies": {
38-
"graphology": "^0.26.0",
39-
"graphology-dag": "^0.4.1",
40-
"graphology-traversal": "^0.3.1"
4142
}
4243
}

src/index.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import { TypeBoxPrinter } from '@daxserver/validation-schema-codegen/printer/typebox-printer'
66
import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal'
77
import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types'
8+
import type { VisualizationOptions } from '@daxserver/validation-schema-codegen/utils/graph-visualizer'
89
import { Node, Project, SourceFile, ts } from 'ts-morph'
910

1011
const createOutputFile = (hasGenericInterfaces: boolean) => {
@@ -50,13 +51,25 @@ const printSortedNodes = (sortedTraversedNodes: TraversedNode[], newSourceFile:
5051
return newSourceFile.getFullText()
5152
}
5253

53-
export const generateCode = ({ sourceCode, filePath, ...options }: InputOptions): string => {
54+
export interface CodeGenerationOptions extends InputOptions {
55+
visualizationOptions?: VisualizationOptions
56+
}
57+
58+
export const generateVisualization = async (options: CodeGenerationOptions): Promise<string> => {
5459
// Create source file from input
55-
const sourceFile = createSourceFileFromInput({
56-
sourceCode,
57-
filePath,
58-
...options,
59-
})
60+
const sourceFile = createSourceFileFromInput(options)
61+
62+
// Create dependency traversal and start traversal
63+
const dependencyTraversal = new DependencyTraversal()
64+
dependencyTraversal.startTraversal(sourceFile)
65+
66+
// Generate visualization
67+
return await dependencyTraversal.visualizeGraph(options.visualizationOptions)
68+
}
69+
70+
export const generateCode = (options: InputOptions): string => {
71+
// Create source file from input
72+
const sourceFile = createSourceFileFromInput(options)
6073

6174
// Create dependency traversal and start traversal
6275
const dependencyTraversal = new DependencyTraversal()

src/traverse/dependency-traversal.ts

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ import { FileGraph } from '@daxserver/validation-schema-codegen/traverse/file-gr
22
import { NodeGraph } from '@daxserver/validation-schema-codegen/traverse/node-graph'
33
import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types'
44
import { generateQualifiedNodeName } from '@daxserver/validation-schema-codegen/utils/generate-qualified-name'
5-
import { topologicalSort } from 'graphology-dag'
65
import {
6+
GraphVisualizer,
7+
type VisualizationOptions,
8+
} from '@daxserver/validation-schema-codegen/utils/graph-visualizer'
9+
import { hasCycle, topologicalSort } from 'graphology-dag'
10+
import {
11+
EnumDeclaration,
12+
FunctionDeclaration,
713
ImportDeclaration,
814
InterfaceDeclaration,
915
Node,
1016
SourceFile,
17+
SyntaxKind,
1118
TypeAliasDeclaration,
12-
TypeReferenceNode,
1319
} from 'ts-morph'
1420

1521
/**
@@ -113,32 +119,29 @@ export class DependencyTraversal {
113119
* Extract dependencies for all nodes in the graph
114120
*/
115121
extractDependencies(): void {
116-
// Extract dependencies for all nodes in the graph
117122
for (const nodeId of this.nodeGraph.nodes()) {
118123
const nodeData = this.nodeGraph.getNode(nodeId)
119124

125+
let nodeToAnalyze: Node | undefined
126+
120127
if (nodeData.type === 'typeAlias') {
121128
const typeAlias = nodeData.node as TypeAliasDeclaration
122-
const typeNode = typeAlias.getTypeNode()
123-
if (!typeNode) continue
129+
nodeToAnalyze = typeAlias.getTypeNode()
130+
} else if (nodeData.type === 'interface') {
131+
nodeToAnalyze = nodeData.node as InterfaceDeclaration
132+
} else if (nodeData.type === 'enum') {
133+
nodeToAnalyze = nodeData.node as EnumDeclaration
134+
} else if (nodeData.type === 'function') {
135+
nodeToAnalyze = nodeData.node as FunctionDeclaration
136+
}
124137

125-
const typeReferences = this.extractTypeReferences(typeNode)
138+
if (!nodeToAnalyze) continue
126139

127-
// Add edges for dependencies
128-
for (const referencedType of typeReferences) {
129-
if (this.nodeGraph.hasNode(referencedType)) {
130-
this.nodeGraph.addDependency(referencedType, nodeId)
131-
}
132-
}
133-
} else if (nodeData.type === 'interface') {
134-
const interfaceDecl = nodeData.node as InterfaceDeclaration
135-
const typeReferences = this.extractTypeReferences(interfaceDecl)
140+
const typeReferences = this.extractTypeReferences(nodeToAnalyze)
136141

137-
// Add edges for dependencies
138-
for (const referencedType of typeReferences) {
139-
if (this.nodeGraph.hasNode(referencedType)) {
140-
this.nodeGraph.addDependency(referencedType, nodeId)
141-
}
142+
for (const referencedType of typeReferences) {
143+
if (this.nodeGraph.hasNode(referencedType)) {
144+
this.nodeGraph.addDependency(referencedType, nodeId)
142145
}
143146
}
144147
}
@@ -229,21 +232,29 @@ export class DependencyTraversal {
229232
}
230233

231234
/**
232-
* Get nodes in dependency order (dependencies first)
233-
* Retrieved from the graph, not from SourceFile
235+
* Get nodes in dependency order from graph
236+
* Handles circular dependencies gracefully by falling back to simple node order
234237
*/
235238
getNodesToPrint(): TraversedNode[] {
236-
try {
237-
// Use topological sort to ensure dependencies are printed first
238-
const sortedNodeIds = topologicalSort(this.nodeGraph)
239-
return sortedNodeIds.map((nodeId: string) => this.nodeGraph.getNodeAttributes(nodeId))
240-
} catch {
241-
// Handle circular dependencies by returning nodes in insertion order
242-
// This ensures dependencies are still processed before dependents when possible
243-
return Array.from(this.nodeGraph.nodes()).map((nodeId: string) =>
244-
this.nodeGraph.getNodeAttributes(nodeId),
245-
)
246-
}
239+
const nodes = hasCycle(this.nodeGraph)
240+
? Array.from(this.nodeGraph.nodes())
241+
: topologicalSort(this.nodeGraph)
242+
243+
return nodes.map((nodeId: string) => this.nodeGraph.getNode(nodeId))
244+
}
245+
246+
/**
247+
* Generate HTML visualization of the dependency graph
248+
*/
249+
async visualizeGraph(options: VisualizationOptions = {}): Promise<string> {
250+
return GraphVisualizer.generateVisualization(this.nodeGraph, options)
251+
}
252+
253+
/**
254+
* Get the node graph for debugging purposes
255+
*/
256+
getNodeGraph(): NodeGraph {
257+
return this.nodeGraph
247258
}
248259

249260
private extractTypeReferences(node: Node): string[] {
@@ -255,8 +266,7 @@ export class DependencyTraversal {
255266
visited.add(node)
256267

257268
if (Node.isTypeReference(node)) {
258-
const typeRefNode = node as TypeReferenceNode
259-
const typeName = typeRefNode.getTypeName().getText()
269+
const typeName = node.getTypeName().getText()
260270

261271
for (const qualifiedName of this.nodeGraph.nodes()) {
262272
const nodeData = this.nodeGraph.getNode(qualifiedName)
@@ -265,8 +275,44 @@ export class DependencyTraversal {
265275
break
266276
}
267277
}
278+
}
268279

269-
return
280+
// Handle typeof expressions (TypeQuery nodes)
281+
if (Node.isTypeQuery(node)) {
282+
const exprName = node.getExprName()
283+
284+
if (Node.isIdentifier(exprName) || Node.isQualifiedName(exprName)) {
285+
const typeName = exprName.getText()
286+
287+
for (const qualifiedName of this.nodeGraph.nodes()) {
288+
const nodeData = this.nodeGraph.getNode(qualifiedName)
289+
if (nodeData.originalName === typeName) {
290+
references.push(qualifiedName)
291+
break
292+
}
293+
}
294+
}
295+
}
296+
297+
// Handle interface inheritance (extends clauses)
298+
if (Node.isInterfaceDeclaration(node)) {
299+
const heritageClauses = node.getHeritageClauses()
300+
301+
for (const heritageClause of heritageClauses) {
302+
if (heritageClause.getToken() !== SyntaxKind.ExtendsKeyword) continue
303+
304+
for (const typeNode of heritageClause.getTypeNodes()) {
305+
const typeName = typeNode.getText()
306+
307+
for (const qualifiedName of this.nodeGraph.nodes()) {
308+
const nodeData = this.nodeGraph.getNode(qualifiedName)
309+
if (nodeData.originalName === typeName) {
310+
references.push(qualifiedName)
311+
break
312+
}
313+
}
314+
}
315+
}
270316
}
271317

272318
node.forEachChild(traverse)

src/traverse/node-graph.ts

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,19 @@ export class NodeGraph extends DirectedGraph<TraversedNode> {
2222
return this.getNodeAttributes(qualifiedName) as TraversedNode
2323
}
2424

25-
/**
26-
* Remove unused imported nodes that have no outgoing edges
27-
* Never removes root imports (types directly imported from the main file)
28-
*/
29-
removeUnusedImportedNodes(): void {
30-
const nodesToRemove: string[] = []
31-
32-
for (const nodeId of this.nodes()) {
33-
const nodeData = this.getNodeAttributes(nodeId)
34-
if (nodeData?.isImported && !nodeData?.isMainCode) {
35-
// Check if this imported type has any outgoing edges (other nodes depend on it)
36-
const outgoingEdges = this.outboundNeighbors(nodeId)
37-
if (outgoingEdges.length === 0) {
38-
nodesToRemove.push(nodeId)
39-
}
40-
}
41-
}
42-
43-
// Remove unused imported types
44-
for (const nodeId of nodesToRemove) {
45-
this.dropNode(nodeId)
46-
}
47-
}
48-
4925
/**
5026
* Add dependency edge between two nodes
5127
*/
5228
addDependency(fromNode: string, toNode: string): void {
53-
if (this.hasNode(fromNode) && this.hasNode(toNode)) {
54-
this.addDirectedEdge(fromNode, toNode)
29+
if (
30+
!this.hasNode(fromNode) ||
31+
!this.hasNode(toNode) ||
32+
fromNode === toNode ||
33+
this.hasDirectedEdge(fromNode, toNode)
34+
) {
35+
return
5536
}
37+
38+
this.addDirectedEdge(fromNode, toNode)
5639
}
5740
}

0 commit comments

Comments
 (0)