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
22 changes: 14 additions & 8 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
- [TypeBoxTypeHandlers Optimizations](#typeboxtypehandlers-optimizations)
- [Performance Testing](#performance-testing)
- [Process Overview](#process-overview)
- [Test-Driven Development (TDD) Approach](#test-driven-development-tdd-approach)
- [Test-Driven Development](#test-driven-development)
- [TDD Cycle](#tdd-cycle)
- [Running Tests](#running-tests)
- [TDD Workflow for New Features](#tdd-workflow-for-new-features)
Expand Down Expand Up @@ -77,7 +77,7 @@ The code generation process includes sophisticated import resolution and depende

#### DependencyCollector

The <mcfile name="dependency-collector.ts" path="src/utils/dependency-collector.ts"></mcfile> module implements a `DependencyCollector` class that:
The <mcfile name="dependency-collector.ts" path="src/traverse/dependency-collector.ts"></mcfile> module implements a `DependencyCollector` class that:

- **Traverses Import Chains**: Recursively follows import declarations to collect all type dependencies from external files
- **Builds Dependency Graph**: Creates a comprehensive map of type dependencies, tracking which types depend on which other types
Expand Down Expand Up @@ -113,11 +113,15 @@ The codebase provides comprehensive support for TypeScript interface inheritance

### Dependency-Ordered Processing

Interfaces are processed in dependency order using a topological sort algorithm implemented in <mcfile name="ts-morph-codegen.ts" path="src/ts-morph-codegen.ts"></mcfile>:
The main codegen logic in <mcfile name="ts-morph-codegen.ts" path="src/ts-morph-codegen.ts"></mcfile> implements sophisticated processing order management:

1. **Dependency Analysis**: The `getInterfaceProcessingOrder` function analyzes all interfaces to identify inheritance relationships
2. **Topological Sorting**: Interfaces are sorted to ensure base interfaces are processed before extended interfaces
3. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully
1. **Dependency Analysis**: Uses `InterfaceTypeDependencyAnalyzer` to analyze complex relationships between interfaces and type aliases
2. **Conditional Processing**: Handles three scenarios:
- Interfaces depending on type aliases only
- Type aliases depending on interfaces only
- Both dependencies present (three-phase processing)
3. **Topological Sorting**: Ensures types are processed in correct dependency order to prevent "type not found" errors
4. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully
Comment on lines +116 to +124
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix class name: DependencyAnalyzer (not InterfaceTypeDependencyAnalyzer) and clarify scenarios

The documented name doesn’t exist in the codebase and can mislead readers. Also tighten the bullet wording.

-1. **Dependency Analysis**: Uses `InterfaceTypeDependencyAnalyzer` to analyze complex relationships between interfaces and type aliases
-2. **Conditional Processing**: Handles three scenarios:
-   - Interfaces depending on type aliases only
-   - Type aliases depending on interfaces only
-   - Both dependencies present (three-phase processing)
+1. **Dependency Analysis**: Uses `DependencyAnalyzer` to analyze relationships between interfaces and type aliases
+2. **Conditional Processing**: Handles three scenarios:
+   - Interfaces depending on type aliases
+   - Type aliases depending on interfaces
+   - Both dependencies present (three-phase processing)
📝 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
The main codegen logic in <mcfile name="ts-morph-codegen.ts" path="src/ts-morph-codegen.ts"></mcfile> implements sophisticated processing order management:
1. **Dependency Analysis**: The `getInterfaceProcessingOrder` function analyzes all interfaces to identify inheritance relationships
2. **Topological Sorting**: Interfaces are sorted to ensure base interfaces are processed before extended interfaces
3. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully
1. **Dependency Analysis**: Uses `InterfaceTypeDependencyAnalyzer` to analyze complex relationships between interfaces and type aliases
2. **Conditional Processing**: Handles three scenarios:
- Interfaces depending on type aliases only
- Type aliases depending on interfaces only
- Both dependencies present (three-phase processing)
3. **Topological Sorting**: Ensures types are processed in correct dependency order to prevent "type not found" errors
4. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully
The main codegen logic in <mcfile name="ts-morph-codegen.ts" path="src/ts-morph-codegen.ts"></mcfile> implements sophisticated processing order management:
1. **Dependency Analysis**: Uses `DependencyAnalyzer` to analyze relationships between interfaces and type aliases
2. **Conditional Processing**: Handles three scenarios:
- Interfaces depending on type aliases
- Type aliases depending on interfaces
- Both dependencies present (three-phase processing)
3. **Topological Sorting**: Ensures types are processed in correct dependency order to prevent "type not found" errors
4. **Circular Dependency Detection**: The algorithm detects and handles circular inheritance scenarios gracefully
🧰 Tools
🪛 LanguageTool

[grammar] ~119-~119: There might be a mistake here.
Context: ...l Processing**: Handles three scenarios: - Interfaces depending on type aliases onl...

(QB_NEW_EN)


[style] ~121-~121: This adverb was used twice in the sentence. Consider removing one of them or replacing them with a synonym.
Context: ... - Type aliases depending on interfaces only - Both dependencies present (three-p...

(ADVERB_REPETITION_PREMIUM)

🤖 Prompt for AI Agents
In ARCHITECTURE.md around lines 116 to 124, the doc references a non-existent
class name and vague bullets: change the class reference from
InterfaceTypeDependencyAnalyzer to DependencyAnalyzer, and tighten the three
scenario bullets to concise wording that matches the implementation (interfaces
depend only on aliases; aliases depend only on interfaces; mixed dependencies
requiring phased processing). Also keep mentions of topological sorting and
circular dependency detection but rephrase succinctly to align with actual
behavior in src/ts-morph-codegen.ts.


### TypeBox Composite Generation

Expand All @@ -126,11 +130,13 @@ Interface inheritance is implemented using TypeBox's `Type.Composite` functional
- **Base Interface Reference**: Extended interfaces reference their base interfaces by name as identifiers
- **Property Combination**: The `InterfaceTypeHandler` generates `Type.Composite([BaseInterface, Type.Object({...})])` for extended interfaces
- **Type Safety**: Generated code maintains full TypeScript type safety through proper static type aliases
- **Generic Type Parameter Handling**: Uses `TSchema` as the constraint for TypeBox compatibility instead of preserving original TypeScript constraints

### Implementation Details

- **Heritage Clause Processing**: The <mcfile name="interface-type-handler.ts" path="src/handlers/typebox/object/interface-type-handler.ts"></mcfile> processes `extends` clauses by extracting referenced type names
- **Identifier Generation**: Base interface references are converted to TypeScript identifiers rather than attempting recursive type resolution
- **TypeBox Constraint Normalization**: Generic type parameters use `TSchema` constraints for TypeBox schema compatibility
- **Error Prevention**: The dependency ordering prevents "No handler found for type" errors that occur when extended interfaces are processed before their base interfaces

## Input Handling System
Expand Down Expand Up @@ -341,9 +347,9 @@ To ensure the dependency collection system performs efficiently under various sc
5. **Static Type Generation**: Alongside each TypeBox schema, a TypeScript `type` alias is generated using `Static<typeof ...>` to provide compile-time type safety and seamless integration with existing TypeScript code.
6. **Output**: A new TypeScript file (as a string) containing the generated TypeBox schemas and static type aliases, ready to be written to disk or integrated into your application.

## Test-Driven Development (TDD) Approach
## Test-Driven Development

This project follows a Test-Driven Development methodology to ensure code quality, maintainability, and reliability. The TDD workflow consists of three main phases:
This project follows a Test-Driven Development (TDD) methodology to ensure code quality, maintainability, and reliability. The TDD workflow consists of three main phases:

### TDD Cycle

Expand Down
5 changes: 4 additions & 1 deletion src/handlers/typebox/object/interface-type-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ export class InterfaceTypeHandler extends ObjectLikeBaseHandler {
const functionTypeParams = typeParameters.map((typeParam) => {
const paramName = typeParam.getName()

// Use TSchema as the constraint for TypeBox compatibility
const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined)

return ts.factory.createTypeParameterDeclaration(
undefined,
ts.factory.createIdentifier(paramName),
ts.factory.createTypeReferenceNode('TSchema', undefined),
constraintNode,
undefined,
)
})
Expand Down
15 changes: 15 additions & 0 deletions 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 { 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 @@ -9,9 +10,23 @@ export class TypeReferenceHandler extends BaseTypeHandler {

handle(node: TypeReferenceNode): ts.Expression {
const referencedType = node.getTypeName()
const typeArguments = node.getTypeArguments()

if (Node.isIdentifier(referencedType)) {
const typeName = referencedType.getText()

// If there are type arguments, create a function call
if (typeArguments.length > 0) {
const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg))

return ts.factory.createCallExpression(
ts.factory.createIdentifier(typeName),
undefined,
typeBoxArgs,
)
}
Comment on lines +18 to +27
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle qualified names (e.g., ns.Vector) when emitting calls.

The current path only supports identifiers. Qualified names will fall through or be emitted incorrectly if coerced into an Identifier. Build a property access chain from the EntityName for robust handling.

-      if (typeArguments.length > 0) {
-        const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg))
-
-        return ts.factory.createCallExpression(
-          ts.factory.createIdentifier(typeName),
-          undefined,
-          typeBoxArgs,
-        )
-      }
+      if (typeArguments.length > 0) {
+        const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg))
+        const entity = node.getTypeName()
+        const toExpr = (e: typeof entity): ts.Expression => {
+          const cn = e.compilerNode
+          if (ts.isIdentifier(cn)) return ts.factory.createIdentifier(cn.text)
+          // QualifiedName: recursively build ns.prop.prop...
+          const left = toExpr((e as any).getLeft())
+          const right = (e as any).getRight().getText()
+          return ts.factory.createPropertyAccessExpression(left, right)
+        }
+        return ts.factory.createCallExpression(toExpr(entity as any), undefined, typeBoxArgs)
+      }
📝 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
// If there are type arguments, create a function call
if (typeArguments.length > 0) {
const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg))
return ts.factory.createCallExpression(
ts.factory.createIdentifier(typeName),
undefined,
typeBoxArgs,
)
}
// If there are type arguments, create a function call
if (typeArguments.length > 0) {
const typeBoxArgs = typeArguments.map((arg) => getTypeBoxType(arg))
const entity = node.getTypeName()
const toExpr = (e: typeof entity): ts.Expression => {
const cn = e.compilerNode
if (ts.isIdentifier(cn)) return ts.factory.createIdentifier(cn.text)
// QualifiedName: recursively build ns.prop.prop...
const left = toExpr((e as any).getLeft())
const right = (e as any).getRight().getText()
return ts.factory.createPropertyAccessExpression(left, right)
}
return ts.factory.createCallExpression(toExpr(entity as any), undefined, typeBoxArgs)
}
🤖 Prompt for AI Agents
In src/handlers/typebox/type-reference-handler.ts around lines 18 to 27, the
current logic only handles Identifier names when emitting a call for a generic
type and will mis-handle QualifiedName (e.g., ns.Vector<T>); replace the direct
Identifier creation with a helper that converts an EntityName into a nested
PropertyAccessExpression by recursively walking QualifiedName nodes (or using
ts.isIdentifier/ts.isQualifiedName checks) to produce the correct left-hand
expression, then use that expression as the callee for
ts.factory.createCallExpression so calls for both identifiers and qualified
names are emitted correctly.


// No type arguments, just return the identifier
return ts.factory.createIdentifier(typeName)
}

Expand Down
5 changes: 4 additions & 1 deletion src/parsers/parse-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,13 @@ export class InterfaceParser extends BaseParser {
// Create type parameters for the type alias
const typeParamDeclarations = typeParameters.map((typeParam) => {
const paramName = typeParam.getName()
// Use TSchema as the constraint for TypeBox compatibility
const constraintNode = ts.factory.createTypeReferenceNode('TSchema', undefined)

return ts.factory.createTypeParameterDeclaration(
undefined,
ts.factory.createIdentifier(paramName),
ts.factory.createTypeReferenceNode('TSchema', undefined),
constraintNode,
undefined,
)
})
Expand Down
111 changes: 111 additions & 0 deletions src/traverse/ast-traversal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { InterfaceDeclaration, Node, TypeAliasDeclaration, TypeReferenceNode } from 'ts-morph'

/**
* AST traversal patterns used in dependency analysis
*/
export class ASTTraversal {
private static cache = new Map<string, string[]>()

/**
* Extract interface names referenced by a type alias
*/
static extractInterfaceReferences(
typeAlias: TypeAliasDeclaration,
interfaces: Map<string, InterfaceDeclaration>,
): string[] {
const typeNode = typeAlias.getTypeNode()
if (!typeNode) return []

const cacheKey = `interface_refs_${typeNode.getText()}`
const cached = ASTTraversal.cache.get(cacheKey)
if (cached) return cached

const references: string[] = []
const visited = new Set<Node>()

const traverse = (node: Node): void => {
if (visited.has(node)) return
visited.add(node)

// Handle type references
if (Node.isTypeReference(node)) {
const typeRefNode = node as TypeReferenceNode
const typeName = typeRefNode.getTypeName().getText()

if (interfaces.has(typeName)) {
references.push(typeName)
}
// Continue traversing to handle type arguments in generic instantiations
}

// Use forEachChild for better performance
node.forEachChild(traverse)
}

traverse(typeNode)

// Cache the result
ASTTraversal.cache.set(cacheKey, references)
return references
}
Comment on lines +23 to +50
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Deduplicate references and store copies in cache.

Traversals can encounter the same referenced name multiple times (e.g., repeated fields, unions). Use a Set to ensure uniqueness and store/return copies to maintain cache integrity.

-    const references: string[] = []
+    const references = new Set<string>()
     const visited = new Set<Node>()
@@
-      if (Node.isTypeReference(node)) {
+      if (Node.isTypeReference(node)) {
         const typeRefNode = node as TypeReferenceNode
         const typeName = typeRefNode.getTypeName().getText()
 
-        if (interfaces.has(typeName)) {
-          references.push(typeName)
-        }
+        if (interfaces.has(typeName)) {
+          references.add(typeName)
+        }
         // Continue traversing to handle type arguments in generic instantiations
       }
@@
-    ASTTraversal.cache.set(cacheKey, references)
-    return references
+    const out = Array.from(references)
+    ASTTraversal.cache.set(cacheKey, [...out])
+    return out
📝 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 references: string[] = []
const visited = new Set<Node>()
const traverse = (node: Node): void => {
if (visited.has(node)) return
visited.add(node)
// Handle type references
if (Node.isTypeReference(node)) {
const typeRefNode = node as TypeReferenceNode
const typeName = typeRefNode.getTypeName().getText()
if (interfaces.has(typeName)) {
references.push(typeName)
}
// Continue traversing to handle type arguments in generic instantiations
}
// Use forEachChild for better performance
node.forEachChild(traverse)
}
traverse(typeNode)
// Cache the result
ASTTraversal.cache.set(cacheKey, references)
return references
}
const references = new Set<string>()
const visited = new Set<Node>()
const traverse = (node: Node): void => {
if (visited.has(node)) return
visited.add(node)
// Handle type references
if (Node.isTypeReference(node)) {
const typeRefNode = node as TypeReferenceNode
const typeName = typeRefNode.getTypeName().getText()
if (interfaces.has(typeName)) {
references.add(typeName)
}
// Continue traversing to handle type arguments in generic instantiations
}
// Use forEachChild for better performance
node.forEachChild(traverse)
}
traverse(typeNode)
// Cache the result
const out = Array.from(references)
ASTTraversal.cache.set(cacheKey, [...out])
return out
🤖 Prompt for AI Agents
In src/traverse/ast-traversal.ts around lines 23 to 50, the traversal collects
referenced type names into an array that can contain duplicates and the array
instance is stored directly in the cache (allowing external mutation); change
the logic to collect names into a Set to ensure uniqueness during traversal, and
when caching and returning the result convert the Set into a fresh array copy
(e.g., Array.from(set)) so the cached value is an immutable copy and callers
receive their own array instances.


/**
* Extract type alias names referenced by an interface (e.g., in type parameter constraints)
*/
static extractTypeAliasReferences(
interfaceDecl: InterfaceDeclaration,
typeAliases: Map<string, TypeAliasDeclaration>,
): string[] {
const cacheKey = `type_alias_refs_${interfaceDecl.getName()}_${interfaceDecl.getText()}`
const cached = ASTTraversal.cache.get(cacheKey)
if (cached) return cached

const references: string[] = []
const visited = new Set<Node>()

const traverse = (node: Node): void => {
if (visited.has(node)) return
visited.add(node)

// Handle type references
if (Node.isTypeReference(node)) {
const typeRefNode = node as TypeReferenceNode
const typeName = typeRefNode.getTypeName().getText()

if (typeAliases.has(typeName)) {
references.push(typeName)
}
return // No need to traverse children of type references
}
Comment on lines +66 to +79
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t early-return on TypeReference; you’ll miss aliases in type arguments.

In extractTypeAliasReferences, returning immediately after handling a TypeReference prevents traversal into its type arguments. You’ll miss cases like Wrapper.

-      if (Node.isTypeReference(node)) {
+      if (Node.isTypeReference(node)) {
         const typeRefNode = node as TypeReferenceNode
         const typeName = typeRefNode.getTypeName().getText()
 
         if (typeAliases.has(typeName)) {
           references.push(typeName)
         }
-        return // No need to traverse children of type references
+        // Intentionally continue to traverse children to catch nested references
       }

Also consider switching to a Set here and returning a unique list, mirroring extractInterfaceReferences.

📝 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 traverse = (node: Node): void => {
if (visited.has(node)) return
visited.add(node)
// Handle type references
if (Node.isTypeReference(node)) {
const typeRefNode = node as TypeReferenceNode
const typeName = typeRefNode.getTypeName().getText()
if (typeAliases.has(typeName)) {
references.push(typeName)
}
return // No need to traverse children of type references
}
const traverse = (node: Node): void => {
if (visited.has(node)) return
visited.add(node)
// Handle type references
if (Node.isTypeReference(node)) {
const typeRefNode = node as TypeReferenceNode
const typeName = typeRefNode.getTypeName().getText()
if (typeAliases.has(typeName)) {
references.push(typeName)
}
// Intentionally continue to traverse children to catch nested references
}
// Continue traversal into child nodes
node.forEachChild(child => traverse(child))
}
🤖 Prompt for AI Agents
In src/traverse/ast-traversal.ts around lines 66 to 79, the traversal currently
returns immediately after handling a TypeReference which causes type arguments
(e.g., Wrapper<AliasX>) to be skipped and their aliases missed; change the logic
to not early-return for TypeReference nodes — record the referenced typeName if
it matches a typeAlias and then continue traversing the node's children
(including type arguments) so nested aliases are discovered; also change
references from an array to a Set (or deduplicate before returning) so the
result mirrors extractInterfaceReferences and only returns unique alias names.


// Use forEachChild for better performance
node.forEachChild(traverse)
}

// Check type parameters for constraints
for (const typeParam of interfaceDecl.getTypeParameters()) {
const constraint = typeParam.getConstraint()
if (constraint) {
traverse(constraint)
}
}

// Check heritage clauses
for (const heritageClause of interfaceDecl.getHeritageClauses()) {
for (const typeNode of heritageClause.getTypeNodes()) {
traverse(typeNode)
}
}

// Cache the result
ASTTraversal.cache.set(cacheKey, references)
return references
}

/**
* Clear the internal cache
*/
static clearCache(): void {
ASTTraversal.cache.clear()
}
}
140 changes: 140 additions & 0 deletions src/traverse/dependency-analyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ASTTraversal } from '@daxserver/validation-schema-codegen/traverse/ast-traversal'
import { InterfaceDeclaration, TypeAliasDeclaration } from 'ts-morph'

/**
* Dependency analyzer for determining processing order
*/
export class DependencyAnalyzer {
/**
* Extract interface names referenced by a type alias
*/
extractInterfaceReferences(
typeAlias: TypeAliasDeclaration,
interfaces: Map<string, InterfaceDeclaration>,
): string[] {
return ASTTraversal.extractInterfaceReferences(typeAlias, interfaces)
}

/**
* Extract type alias names referenced by an interface
*/
extractTypeAliasReferences(
interfaceDecl: InterfaceDeclaration,
typeAliases: Map<string, TypeAliasDeclaration>,
): string[] {
return ASTTraversal.extractTypeAliasReferences(interfaceDecl, typeAliases)
}

/**
* Check if any type aliases reference interfaces
*/
hasInterfaceReferences(
typeAliases: TypeAliasDeclaration[],
interfaces: InterfaceDeclaration[],
): boolean {
const interfaceMap = new Map<string, InterfaceDeclaration>()
for (const iface of interfaces) {
interfaceMap.set(iface.getName(), iface)
}

for (const typeAlias of typeAliases) {
const references = this.extractInterfaceReferences(typeAlias, interfaceMap)
if (references.length > 0) {
return true
}
}

return false
}

/**
* Get type aliases that reference interfaces, ordered by their dependencies
*/
getTypeAliasesReferencingInterfaces(
typeAliases: TypeAliasDeclaration[],
interfaces: InterfaceDeclaration[],
): { typeAlias: TypeAliasDeclaration; referencedInterfaces: string[] }[] {
const interfaceMap = new Map<string, InterfaceDeclaration>()
for (const iface of interfaces) {
interfaceMap.set(iface.getName(), iface)
}

const result: { typeAlias: TypeAliasDeclaration; referencedInterfaces: string[] }[] = []

for (const typeAlias of typeAliases) {
const references = this.extractInterfaceReferences(typeAlias, interfaceMap)
if (references.length > 0) {
result.push({
typeAlias,
referencedInterfaces: references,
})
}
}

return result
}

/**
* Determine the correct processing order for interfaces and type aliases
* Returns an object indicating which should be processed first
*/
analyzeProcessingOrder(
typeAliases: TypeAliasDeclaration[],
interfaces: InterfaceDeclaration[],
): {
processInterfacesFirst: boolean
typeAliasesDependingOnInterfaces: string[]
interfacesDependingOnTypeAliases: string[]
} {
const typeAliasMap = new Map<string, TypeAliasDeclaration>()
const interfaceMap = new Map<string, InterfaceDeclaration>()

for (const typeAlias of typeAliases) {
typeAliasMap.set(typeAlias.getName(), typeAlias)
}

for (const interfaceDecl of interfaces) {
interfaceMap.set(interfaceDecl.getName(), interfaceDecl)
}

const typeAliasesDependingOnInterfaces: string[] = []
const interfacesDependingOnTypeAliases: string[] = []

// Check type aliases that depend on interfaces
for (const typeAlias of typeAliases) {
const interfaceRefs = this.extractInterfaceReferences(typeAlias, interfaceMap)
if (interfaceRefs.length > 0) {
typeAliasesDependingOnInterfaces.push(typeAlias.getName())
}
}

// Check interfaces that depend on type aliases
for (const interfaceDecl of interfaces) {
const typeAliasRefs = this.extractTypeAliasReferences(interfaceDecl, typeAliasMap)
if (typeAliasRefs.length > 0) {
interfacesDependingOnTypeAliases.push(interfaceDecl.getName())
}
}

Comment on lines +111 to +118
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incomplete interface→type-alias reference extraction misses interface members

extractTypeAliasReferences (delegated to ASTTraversal) only inspects type parameter constraints and heritage clauses. It ignores interface member types (properties, methods, index signatures), which is the most common place an interface references a local type alias. This can produce incorrect ordering decisions and out-of-order generation.

Follow-up: extend ASTTraversal.extractTypeAliasReferences to traverse interface members. Example implementation:

// In src/traverse/ast-traversal.ts
static extractTypeAliasReferences(
  interfaceDecl: InterfaceDeclaration,
  typeAliases: Map<string, TypeAliasDeclaration>,
): string[] {
  const cacheKey = `type_alias_refs_${interfaceDecl.getName()}_${interfaceDecl.getText()}`
  const cached = ASTTraversal.cache.get(cacheKey)
  if (cached) return cached

  const references = new Set<string>()
  const visit = (node: Node): void => {
    if (Node.isTypeReference(node)) {
      const typeName = (node as TypeReferenceNode).getTypeName().getText()
      if (typeAliases.has(typeName)) references.add(typeName)
    }
    node.forEachChild(visit)
  }

  // 1) Type parameters (constraints)
  for (const tp of interfaceDecl.getTypeParameters()) {
    const c = tp.getConstraint()
    if (c) visit(c)
  }

  // 2) Heritage clauses
  for (const hc of interfaceDecl.getHeritageClauses()) {
    for (const t of hc.getTypeNodes()) visit(t)
  }

  // 3) Members: properties, methods, index signatures, call signatures
  for (const m of interfaceDecl.getMembers()) {
    if (Node.isPropertySignature(m) || Node.isMethodSignature(m) || Node.isIndexSignatureDeclaration(m)) {
      const t = m.getTypeNode()
      if (t) visit(t)
    }
    // Also inspect method parameters/return types
    if (Node.isMethodSignature(m)) {
      for (const p of m.getParameters()) {
        const pt = p.getTypeNode()
        if (pt) visit(pt)
      }
      const rt = m.getReturnTypeNode()
      if (rt) visit(rt)
    }
  }

  const result = Array.from(references)
  ASTTraversal.cache.set(cacheKey, result)
  return result
}

Additionally, consider de-duplicating reference arrays returned by traversal to avoid repeated work downstream.

🤖 Prompt for AI Agents
In src/traverse/dependency-analyzer.ts around lines 111-118, the current call to
extractTypeAliasReferences misses type-alias uses inside interface members
(properties, methods, index signatures, etc.), causing incorrect dependency
ordering; update ASTTraversal.extractTypeAliasReferences to recursively traverse
interface member nodes: visit type parameter constraints, heritage clauses, and
every member's type node (properties, index signatures, call signatures), and
for method signatures also visit each parameter type and the return type, using
a visitor that collects TypeReference nodes by name, deduplicate results, cache
by a stable key, and return an array of referenced type-alias names.

// Determine processing order:
// If interfaces depend on type aliases, process type aliases first
// If only type aliases depend on interfaces, process interfaces first
// If both have dependencies, process type aliases that interfaces depend on first,
// then interfaces, then type aliases that depend on interfaces
const processInterfacesFirst =
interfacesDependingOnTypeAliases.length === 0 && typeAliasesDependingOnInterfaces.length > 0

return {
processInterfacesFirst,
typeAliasesDependingOnInterfaces,
interfacesDependingOnTypeAliases,
}
}

/**
* Clear internal caches
*/
clearCache(): void {
ASTTraversal.clearCache()
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
DefaultFileResolver,
type FileResolver,
} from '@daxserver/validation-schema-codegen/utils/dependency-file-resolver'
} from '@daxserver/validation-schema-codegen/traverse/dependency-file-resolver'
import {
DefaultTypeReferenceExtractor,
type TypeReferenceExtractor,
} from '@daxserver/validation-schema-codegen/utils/dependency-type'
} from '@daxserver/validation-schema-codegen/traverse/dependency-type'
import { ImportDeclaration, SourceFile, TypeAliasDeclaration } from 'ts-morph'

export interface TypeDependency {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TypeDependency } from '@daxserver/validation-schema-codegen/utils/dependency-collector'
import type { TypeDependency } from '@daxserver/validation-schema-codegen/traverse/dependency-collector'
import { Node, TypeReferenceNode } from 'ts-morph'

export interface TypeReferenceExtractor {
Expand Down
Loading