Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/handlers/typebox/type-reference-handler.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)

Comment on lines +17 to 21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add a safe fallback when no alias is registered.

Ensure we never emit an undefined identifier.

-      // Use the ResolverStore to get the alias name if available
-      const typeName = resolverStore.resolveAliasName(originalTypeName)
+      // Use the ResolverStore to get the alias name if available
+      const typeName = resolverStore.resolveAliasName(originalTypeName) ?? originalTypeName
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const originalTypeName = referencedType.getText()
// Use the ResolverStore to get the alias name if available
const typeName = resolverStore.resolveAliasName(originalTypeName)
const originalTypeName = referencedType.getText()
// Use the ResolverStore to get the alias name if available
const typeName = resolverStore.resolveAliasName(originalTypeName) ?? originalTypeName
🤖 Prompt for AI Agents
In src/handlers/typebox/type-reference-handler.ts around lines 17 to 21,
resolverStore.resolveAliasName(originalTypeName) can return undefined and may
cause an undefined identifier to be emitted; update the logic to provide a safe
fallback (e.g., use originalTypeName when no alias is registered, or a sanitized
default identifier) so that typeName is never undefined and downstream code
always receives a valid identifier.

// If there are type arguments, create a function call
if (typeArguments.length > 0) {
Expand Down
4 changes: 2 additions & 2 deletions src/parsers/parse-enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
8 changes: 5 additions & 3 deletions src/parsers/parse-function-declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 6 additions & 9 deletions src/parsers/parse-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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
Expand Down
15 changes: 6 additions & 9 deletions src/parsers/parse-type-aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Comment on lines +9 to 11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Sanitize aliasName to a valid TS identifier before use.

Aliases derived from paths may include invalid chars, breaking codegen.

-  parse(typeAlias: TypeAliasDeclaration, aliasName?: string): void {
-    const typeName = aliasName || typeAlias.getName()
+  parse(typeAlias: TypeAliasDeclaration, aliasName?: string): void {
+    const makeSafeIdentifier = (name: string) => name.replace(/[^A-Za-z0-9_$]/g, '_')
+    const resolved = aliasName ?? typeAlias.getName()
+    const typeName = ts.isIdentifierText(resolved, ts.ScriptTarget.Latest)
+      ? resolved
+      : makeSafeIdentifier(resolved)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
parse(typeAlias: TypeAliasDeclaration, aliasName?: string): void {
const typeName = aliasName || typeAlias.getName()
parse(typeAlias: TypeAliasDeclaration, aliasName?: string): void {
const makeSafeIdentifier = (name: string) => name.replace(/[^A-Za-z0-9_$]/g, '_')
const resolved = aliasName ?? typeAlias.getName()
const typeName = ts.isIdentifierText(resolved, ts.ScriptTarget.Latest)
? resolved
: makeSafeIdentifier(resolved)
🤖 Prompt for AI Agents
In src/parsers/parse-type-aliases.ts around lines 9–11, aliasName may contain
characters invalid in a TypeScript identifier (e.g., derived from paths);
sanitize aliasName before using it as typeName by: normalize aliasName to a safe
identifier (replace any character not in [A-Za-z0-9_$] with underscore), ensure
it does not start with a digit (prefix with '_' if it does), fall back to
typeAlias.getName() when aliasName is missing or becomes empty after
sanitization, and use that sanitized value as typeName so generated code never
contains invalid identifiers.

if (this.processedTypes.has(typeName)) return
this.processedTypes.add(typeName)
Expand All @@ -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(
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/printer/typebox-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions src/traverse/dependency-extractor.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,7 +12,7 @@ import {
export const extractDependencies = (nodeGraph: NodeGraph, requiredNodeIds: Set<string>): void => {
const processedNodes = new Set<string>()
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) {
Expand Down Expand Up @@ -44,7 +45,7 @@ export const extractDependencies = (nodeGraph: NodeGraph, requiredNodeIds: Set<s
const typeReferences = typeReferenceExtractor.extractTypeReferences(nodeToAnalyze)

for (const referencedType of typeReferences) {
if (nodeGraph.hasNode(referencedType)) {
if (resolverStore.hasQualifiedName(referencedType) && nodeGraph.hasNode(referencedType)) {
// Only add to required if not already processed
if (!requiredNodeIds.has(referencedType)) {
requiredNodeIds.add(referencedType)
Expand Down
39 changes: 25 additions & 14 deletions src/traverse/dependency-traversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
GraphVisualizer,
type VisualizationOptions,
} from '@daxserver/validation-schema-codegen/utils/graph-visualizer'
import { resolverStore } from '@daxserver/validation-schema-codegen/utils/resolver-store'
import { hasCycle, topologicalSort } from 'graphology-dag'
import { SourceFile } from 'ts-morph'

Expand All @@ -16,14 +17,17 @@ export class DependencyTraversal {
private nodeGraph = new NodeGraph()
private maincodeNodeIds = new Set<string>()
private requiredNodeIds = new Set<string>()
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
Expand All @@ -38,23 +42,30 @@ 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),
)
Comment on lines +49 to +58
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Topological sort should be based on the required subgraph, not global cycle presence.

A cycle elsewhere in the graph disables ordering for an acyclic required subset, degrading output determinism.

One pragmatic fix is to attempt sorting and fall back on failure:

-  const orderedNodes = hasCycle(this.nodeGraph)
-    ? requiredQualifiedNames
-    : topologicalSort(this.nodeGraph).filter((nodeId: string) =>
-        requiredQualifiedNames.includes(nodeId),
-      )
+  let orderedNodes: string[]
+  try {
+    const requiredSet = new Set(requiredQualifiedNames)
+    orderedNodes = topologicalSort(this.nodeGraph).filter((id: string) => requiredSet.has(id))
+  } catch {
+    // Graph has cycles; fall back to required insertion order
+    orderedNodes = requiredQualifiedNames
+  }

If feasible, prefer sorting an induced subgraph of required nodes for correctness and performance. I can sketch a small helper to build that subgraph if NodeGraph exposes the underlying Graphology graph.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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),
let orderedNodes: string[]
try {
// Attempt to sort the full graph, then filter to required nodes
const requiredSet = new Set(requiredQualifiedNames)
orderedNodes = topologicalSort(this.nodeGraph).filter((id: string) =>
requiredSet.has(id),
)
} catch {
// If sorting fails (due to cycles), fall back to original insertion order
orderedNodes = requiredQualifiedNames
}
🤖 Prompt for AI Agents
In src/traverse/dependency-traversal.ts around lines 49 to 58, the code
currently disables topological ordering when any cycle exists in the global
graph; instead create an induced subgraph containing only requiredQualifiedNames
(i.e., nodes in this.requiredNodeIds and their edges between those nodes) and
run topologicalSort against that subgraph; if the induced subgraph has a cycle
or topologicalSort throws, fall back to the original requiredQualifiedNames
order—do not rely on hasCycle(this.nodeGraph) for this decision. Ensure you
either use a helper to build the induced graph from NodeGraph's underlying graph
or filter edges appropriately, then replace the global-cycle check with a
cycle/sort check on the induced subgraph and a safe fallback.


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
}

async visualizeGraph(options: VisualizationOptions = {}): Promise<string> {
return GraphVisualizer.generateVisualization(this.nodeGraph, options)
}

getNodeGraph(): NodeGraph {
return this.nodeGraph
}
}
63 changes: 58 additions & 5 deletions src/traverse/import-collector.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<string, string>() // 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()
Expand All @@ -29,51 +40,87 @@ 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',
originalName: typeName,
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',
originalName: interfaceName,
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',
originalName: enumName,
qualifiedName,
isImported: true,
isMainCode: false,
aliasName,
})

// Add to ResolverStore during traversal
resolverStore.addTypeMapping({
originalName: enumName,
sourceFile: enumDecl.getSourceFile(),
aliasName,
})
}

for (const functionDecl of functions) {
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',
Expand All @@ -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)
Expand Down
Loading