-
Notifications
You must be signed in to change notification settings - Fork 0
feat(graph): rework dependency traversal with graphology DAG #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { | ||
| createSourceFileFromInput, | ||
| type InputOptions, | ||
| } from '@daxserver/validation-schema-codegen/input-handler' | ||
| import { TypeBoxPrinter } from '@daxserver/validation-schema-codegen/printer/typebox-printer' | ||
| import { DependencyTraversal } from '@daxserver/validation-schema-codegen/traverse/dependency-traversal' | ||
| import type { TraversedNode } from '@daxserver/validation-schema-codegen/traverse/types' | ||
| import { Node, Project, SourceFile, ts } from 'ts-morph' | ||
|
|
||
| const createOutputFile = (hasGenericInterfaces: boolean) => { | ||
| const newSourceFile = new Project().createSourceFile('output.ts', '', { | ||
| overwrite: true, | ||
| }) | ||
|
|
||
| // Add imports | ||
| const namedImports = [ | ||
| 'Type', | ||
| { | ||
| name: 'Static', | ||
| isTypeOnly: true, | ||
| }, | ||
| ] | ||
|
|
||
| if (hasGenericInterfaces) { | ||
| namedImports.push({ | ||
| name: 'TSchema', | ||
| isTypeOnly: true, | ||
| }) | ||
| } | ||
|
|
||
| newSourceFile.addImportDeclaration({ | ||
| moduleSpecifier: '@sinclair/typebox', | ||
| namedImports, | ||
| }) | ||
|
|
||
| return newSourceFile | ||
| } | ||
|
|
||
| const printSortedNodes = (sortedTraversedNodes: TraversedNode[], newSourceFile: SourceFile) => { | ||
| const printer = new TypeBoxPrinter({ | ||
| newSourceFile, | ||
| printer: ts.createPrinter(), | ||
| }) | ||
|
|
||
| // Process nodes in topological order | ||
| for (const traversedNode of sortedTraversedNodes) { | ||
| printer.printNode(traversedNode) | ||
| } | ||
|
|
||
| return newSourceFile.getFullText() | ||
| } | ||
|
|
||
| export const generateCode = ({ sourceCode, filePath, ...options }: InputOptions): string => { | ||
| // Create source file from input | ||
| const sourceFile = createSourceFileFromInput({ | ||
| sourceCode, | ||
| filePath, | ||
| ...options, | ||
| }) | ||
|
|
||
| // Create dependency traversal and start traversal | ||
| const dependencyTraversal = new DependencyTraversal() | ||
| const traversedNodes = dependencyTraversal.startTraversal(sourceFile) | ||
|
|
||
| // Check if any interfaces have generic type parameters | ||
| const hasGenericInterfaces = traversedNodes.some( | ||
| (t) => Node.isInterfaceDeclaration(t.node) && t.node.getTypeParameters().length > 0, | ||
| ) | ||
|
|
||
| // Create output file with proper imports | ||
| const newSourceFile = createOutputFile(hasGenericInterfaces) | ||
|
|
||
| // Print sorted nodes to output | ||
| const result = printSortedNodes(traversedNodes, newSourceFile) | ||
|
|
||
| return result | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,25 +6,10 @@ import { FunctionDeclaration, ts, VariableDeclarationKind } from 'ts-morph' | |
|
|
||
| export class FunctionDeclarationParser extends BaseParser { | ||
| parse(functionDecl: FunctionDeclaration): void { | ||
| this.parseWithImportFlag(functionDecl, false) | ||
| } | ||
|
|
||
| parseWithImportFlag(functionDecl: FunctionDeclaration, isImported: boolean): void { | ||
| this.parseFunctionWithImportFlag(functionDecl, isImported) | ||
| } | ||
|
|
||
| private parseFunctionWithImportFlag( | ||
| functionDecl: FunctionDeclaration, | ||
| isImported: boolean, | ||
| ): void { | ||
| const functionName = functionDecl.getName() | ||
| if (!functionName) { | ||
| return | ||
| } | ||
| if (!functionName) return | ||
|
|
||
| if (this.processedTypes.has(functionName)) { | ||
| return | ||
| } | ||
| if (this.processedTypes.has(functionName)) return | ||
| this.processedTypes.add(functionName) | ||
|
Comment on lines
+10
to
13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dedupe key uses bare name; risks skipping same-named functions from different files processedTypes keyed by functionName causes false “already processed” when two files export the same name. Plumb a qualified key (e.g., file-hashed) or accept a provided output name from traversal/printing to avoid collisions and incorrect skips. I recommend updating BaseParser/TypeBoxPrinter to pass a collision-safe outputName (qualified) into parse(), and track processedTypes by that key. Would you like a follow-up patch touching BaseParser + all parsers? |
||
|
|
||
| // Get function parameters and return type | ||
|
|
@@ -70,10 +55,8 @@ export class FunctionDeclarationParser extends BaseParser { | |
| this.newSourceFile.compilerNode, | ||
| ) | ||
|
|
||
| const isExported = this.getIsExported(functionDecl, isImported) | ||
|
|
||
| this.newSourceFile.addVariableStatement({ | ||
| isExported, | ||
| isExported: true, | ||
| declarationKind: VariableDeclarationKind.Const, | ||
| declarations: [ | ||
| { | ||
|
|
@@ -88,7 +71,6 @@ export class FunctionDeclarationParser extends BaseParser { | |
| functionName, | ||
| this.newSourceFile.compilerNode, | ||
| this.printer, | ||
| isExported, | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,17 +10,6 @@ import { | |
|
|
||
| export class InterfaceParser extends BaseParser { | ||
| parse(interfaceDecl: InterfaceDeclaration): void { | ||
| this.parseWithImportFlag(interfaceDecl, false) | ||
| } | ||
|
|
||
| parseWithImportFlag(interfaceDecl: InterfaceDeclaration, isImported: boolean): void { | ||
| this.parseInterfaceWithImportFlag(interfaceDecl, isImported) | ||
| } | ||
|
|
||
| private parseInterfaceWithImportFlag( | ||
| interfaceDecl: InterfaceDeclaration, | ||
| isImported: boolean, | ||
| ): void { | ||
| const interfaceName = interfaceDecl.getName() | ||
|
|
||
| if (this.processedTypes.has(interfaceName)) { | ||
|
|
@@ -30,17 +19,16 @@ export class InterfaceParser extends BaseParser { | |
| this.processedTypes.add(interfaceName) | ||
|
|
||
| const typeParameters = interfaceDecl.getTypeParameters() | ||
| const isExported = this.getIsExported(interfaceDecl, isImported) | ||
|
|
||
| // Check if interface has type parameters (generic) | ||
| if (typeParameters.length > 0) { | ||
| this.parseGenericInterface(interfaceDecl, isExported) | ||
| this.parseGenericInterface(interfaceDecl) | ||
| } else { | ||
| this.parseRegularInterface(interfaceDecl, isExported) | ||
| this.parseRegularInterface(interfaceDecl) | ||
| } | ||
| } | ||
|
|
||
| private parseRegularInterface(interfaceDecl: InterfaceDeclaration, isExported: boolean): void { | ||
| private parseRegularInterface(interfaceDecl: InterfaceDeclaration): void { | ||
| const interfaceName = interfaceDecl.getName() | ||
|
|
||
| // Generate TypeBox type definition | ||
|
|
@@ -52,7 +40,7 @@ export class InterfaceParser extends BaseParser { | |
| ) | ||
|
|
||
| this.newSourceFile.addVariableStatement({ | ||
| isExported, | ||
| isExported: true, | ||
| declarationKind: VariableDeclarationKind.Const, | ||
| declarations: [ | ||
| { | ||
|
|
@@ -67,11 +55,10 @@ export class InterfaceParser extends BaseParser { | |
| interfaceName, | ||
| this.newSourceFile.compilerNode, | ||
| this.printer, | ||
| isExported, | ||
| ) | ||
| } | ||
|
|
||
| private parseGenericInterface(interfaceDecl: InterfaceDeclaration, isExported: boolean): void { | ||
| private parseGenericInterface(interfaceDecl: InterfaceDeclaration): void { | ||
| const interfaceName = interfaceDecl.getName() | ||
| const typeParameters = interfaceDecl.getTypeParameters() | ||
|
|
||
|
|
@@ -85,7 +72,7 @@ export class InterfaceParser extends BaseParser { | |
|
|
||
| // Add the function declaration | ||
| this.newSourceFile.addVariableStatement({ | ||
| isExported, | ||
| isExported: true, | ||
| declarationKind: VariableDeclarationKind.Const, | ||
| declarations: [ | ||
| { | ||
|
|
@@ -96,14 +83,10 @@ export class InterfaceParser extends BaseParser { | |
| }) | ||
|
|
||
| // Add generic type alias: type A<T extends TSchema> = Static<ReturnType<typeof A<T>>> | ||
| this.addGenericTypeAlias(interfaceName, typeParameters, isExported) | ||
| this.addGenericTypeAlias(interfaceName, typeParameters) | ||
| } | ||
|
Comment on lines
85
to
87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainPotentially invalid TypeScript: Creating a TypeQuery with type arguments ( Please run tsc to confirm. If invalid, switch to a safe pattern like:
I can produce a concrete patch once we confirm the preferred generic emission shape. Also applies to: 89-145 🏁 Script executed: #!/bin/bash
fd parse-interfaces.ts -t fLength of output: 61 🏁 Script executed: #!/bin/bash
sed -n '1,200p' src/parsers/parse-interfaces.tsLength of output: 4568 Replace invalid TypeScript type queries don’t support type arguments (queries accept only plain identifiers) (1library.net); emit a generic helper function (e.g. 🤖 Prompt for AI Agents |
||
|
|
||
| private addGenericTypeAlias( | ||
| name: string, | ||
| typeParameters: TypeParameterDeclaration[], | ||
| isExported: boolean, | ||
| ): void { | ||
| private addGenericTypeAlias(name: string, typeParameters: TypeParameterDeclaration[]): void { | ||
| // Create type parameters for the type alias | ||
| const typeParamDeclarations = typeParameters.map((typeParam) => { | ||
| const paramName = typeParam.getName() | ||
|
|
@@ -152,7 +135,7 @@ export class InterfaceParser extends BaseParser { | |
| ) | ||
|
|
||
| this.newSourceFile.addTypeAlias({ | ||
| isExported, | ||
| isExported: true, | ||
| name, | ||
| typeParameters: typeParamDeclarations.map((tp) => | ||
| this.printer.printNode(ts.EmitHint.Unspecified, tp, this.newSourceFile.compilerNode), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,18 +6,12 @@ import { ts, TypeAliasDeclaration, VariableDeclarationKind } from 'ts-morph' | |
|
|
||
| export class TypeAliasParser extends BaseParser { | ||
| parse(typeAlias: TypeAliasDeclaration): void { | ||
| this.parseWithImportFlag(typeAlias, false) | ||
| this.parseWithImportFlag(typeAlias) | ||
| } | ||
|
|
||
| parseWithImportFlag(typeAlias: TypeAliasDeclaration, isImported: boolean): void { | ||
| parseWithImportFlag(typeAlias: TypeAliasDeclaration): void { | ||
| const typeName = typeAlias.getName() | ||
|
|
||
| if (this.processedTypes.has(typeName)) { | ||
| return | ||
| } | ||
|
|
||
| this.processedTypes.add(typeName) | ||
|
|
||
| const typeNode = typeAlias.getTypeNode() | ||
|
Comment on lines
+12
to
15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainRemoved processedTypes dedup guard — verify no duplicate emissions. Run quick checks: 🏁 Script executed: #!/bin/bash
# Where else is processedTypes used?
rg -n "processedTypes" -C2
# Ensure TypeAliasParser no longer uses it
rg -n "class TypeAliasParser" -n -C5 src | sed -n '1,160p'Length of output: 4112 🏁 Script executed: #!/bin/bash
sed -n '1,200p' src/printer/typebox-printer.tsLength of output: 2165 🏁 Script executed: #!/bin/bash
sed -n '1,200p' src/parsers/parse-type-aliases.tsLength of output: 1379 Add processedTypes guard in TypeAliasParser if (this.processedTypes.has(typeName)) return;
this.processedTypes.add(typeName);🤖 Prompt for AI Agents |
||
| const typeboxTypeNode = typeNode ? getTypeBoxType(typeNode) : makeTypeCall('Any') | ||
| const typeboxType = this.printer.printNode( | ||
|
|
@@ -26,10 +20,8 @@ export class TypeAliasParser extends BaseParser { | |
| this.newSourceFile.compilerNode, | ||
| ) | ||
|
|
||
| const isExported = this.getIsExported(typeAlias, isImported) | ||
|
|
||
| this.newSourceFile.addVariableStatement({ | ||
| isExported, | ||
| isExported: true, | ||
| declarationKind: VariableDeclarationKind.Const, | ||
| declarations: [ | ||
| { | ||
|
|
@@ -39,12 +31,6 @@ export class TypeAliasParser extends BaseParser { | |
| ], | ||
| }) | ||
|
|
||
| addStaticTypeAlias( | ||
| this.newSourceFile, | ||
| typeName, | ||
| this.newSourceFile.compilerNode, | ||
| this.printer, | ||
| isExported, | ||
| ) | ||
| addStaticTypeAlias(this.newSourceFile, typeName, this.newSourceFile.compilerNode, this.printer) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add proactive collision detection before printing.
Given parsers emit original names, detect duplicate
originalNames across differentqualifiedNames and fail fast with guidance (see proposed diff in parse-type-aliases.ts comment). This prevents invalid, duplicate symbol emissions.🤖 Prompt for AI Agents