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: 16 additions & 6 deletions docs/dependency-management.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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

Comment on lines +39 to 46
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Docs/code mismatch: imported types are “referenced-only” in docs, but code includes all imports when the entry file has no local types

addLocalTypes currently marks every named import as required if the main file defines no local types, which contradicts this section (“Imported types: Only included if actually referenced”). Please align behavior or document the special-case explicitly.

Would you like to (A) change the code to only mark imported types as required when actually referenced, or (B) keep the current import-only seeding and document it as an exception?

🧰 Tools
🪛 LanguageTool

[grammar] ~43-~43: There might be a mistake here.
Context: ...: Always included (defined in main file) - Imported types: Only included if actua...

(QB_NEW_EN)


[grammar] ~44-~44: There might be a mistake here.
Context: ...ed if actually referenced by other types - Transitive dependencies: Automatically...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In docs/dependency-management.md around lines 39 to 46, the docs state imported
types should only be included if actually referenced, but addLocalTypes
currently marks every named import as required when the entry file has no local
types; change addLocalTypes so it does not unconditionally mark all named
imports required in that special-case—instead, detect actual references and mark
an imported type required only if it is referenced by other types (or by an
explicit option), update related tests to assert the referenced-only behavior,
and adjust or add a feature-flag/option if you want to preserve the previous
import-seeding behavior for backwards compatibility.

## Circular Dependencies

Expand Down
115 changes: 115 additions & 0 deletions docs/maincode-filtering.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ export type User = Static<typeof User>
- [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
57 changes: 57 additions & 0 deletions src/traverse/dependency-extractor.ts
Original file line number Diff line number Diff line change
@@ -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<string>): void => {
const processedNodes = new Set<string>()
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)
}
Comment on lines +44 to +54
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Name resolution is ambiguous across files with duplicate type names

TypeReferenceExtractor matches by originalName only, which can mis-resolve when multiple files export the same identifier (e.g., User). Consider symbol-based resolution via ts-morph TypeChecker or maintaining an import-binding map (specifier + local name → qualified node id) to disambiguate.

Happy to sketch a small resolver that uses getType().getSymbol() and getAliasedSymbol() to map references to the correct source file.

🤖 Prompt for AI Agents
In src/traverse/dependency-extractor.ts around lines 44 to 54, the extractor
currently matches type references by originalName only which can mis-resolve
when different files export the same identifier; change the resolution to use
symbol-based disambiguation: use ts-morph/TypeChecker (e.g.,
nodeToAnalyze.getType().getSymbol() and getAliasedSymbol() or
checker.getSymbolAtLocation on the type reference node) to obtain the symbol and
derive a unique file-qualified identifier (source file path + exported name)
before looking up nodeGraph; as an alternative or complement, build an
import-binding map during file parsing that maps (specifier + local name) →
qualified node id and consult it when a reference originates from an import;
update requiredNodeIds/nodesToProcess logic to use the qualified id returned by
the symbol/import resolver instead of originalName so duplicate names across
files are resolved correctly.

}
}
}
Loading