Sfoglia il codice sorgente

refactor(typert): share type node traversal

imccyu 2 mesi fa
parent
commit
b5ec489c9f

+ 2 - 35
packages/typert/generator/src/cordis-catalog.ts

@@ -6,6 +6,7 @@
  */
 
 import { WorkspaceAnalyzer } from './analyzer.ts'
+import { childTypeNodeIds } from './model.ts'
 import { TypeGraphRenderer } from './renderer.ts'
 import type {
   FaceModel,
@@ -15,7 +16,6 @@ import type {
   SourceDeclarationModel,
   SourceLocation,
   TypeNodeId,
-  TypeNodeModel,
 } from './model.ts'
 
 type Mode = 'emit' | 'waterfall' | 'parallel' | 'serial'
@@ -514,7 +514,7 @@ function signatureTypeNames(renderer: TypeGraphRenderer, signature: SignatureMod
     const node = renderer.node(id)
     if (node.kind === 'reference' && node.target.kind !== 'type-parameter') names.add(node.name)
     if (node.kind === 'type-query') names.add(node.expression)
-    for (const child of childTypes(node)) visit(child)
+    for (const child of childTypeNodeIds(node)) visit(child)
     if (node.kind === 'object') for (const member of node.members) visitMember(member)
     if (node.kind === 'function' || node.kind === 'constructor') visitSignature(node.signature)
   }
@@ -522,39 +522,6 @@ function signatureTypeNames(renderer: TypeGraphRenderer, signature: SignatureMod
   return [...names].sort()
 }
 
-function childTypes(node: TypeNodeModel): TypeNodeId[] {
-  switch (node.kind) {
-    case 'parenthesized': return [node.type]
-    case 'reference': return [...node.arguments]
-    case 'union':
-    case 'intersection': return [...node.types]
-    case 'array': return [node.element]
-    case 'tuple': return node.elements.map(element => element.type)
-    case 'indexed-access': return [node.object, node.index]
-    case 'operator': return [node.type]
-    case 'conditional': return [node.check, node.extends, node.whenTrue, node.whenFalse]
-    case 'infer': return [node.parameter.constraint, node.parameter.default]
-      .filter((value): value is TypeNodeId => value !== undefined)
-    case 'mapped': return [node.parameter.constraint, node.parameter.default, node.nameType, node.value]
-      .filter((value): value is TypeNodeId => value !== undefined)
-    case 'template-literal': return node.spans.map(span => span.type)
-    case 'type-query':
-    case 'import-type': return [...node.arguments]
-    case 'predicate': return node.type === undefined ? [] : [node.type]
-    case 'keyword':
-    case 'literal':
-    case 'object':
-    case 'function':
-    case 'constructor':
-    case 'this': return []
-    default: return assertNever(node)
-  }
-}
-
-function assertNever(value: never): never {
-  throw new Error(`unhandled TypeGraph node: ${JSON.stringify(value)}`)
-}
-
 /** Declarations longer than this render as a truncated stub. */
 const MAX_DECL_CHARS = 1500
 

+ 44 - 0
packages/typert/generator/src/model.ts

@@ -324,8 +324,52 @@ export type TypeNodeModel =
   | { readonly id: TypeNodeId; readonly kind: 'predicate'; readonly asserts: boolean; readonly parameter: string; readonly type?: TypeNodeId }
   | { readonly id: TypeNodeId; readonly kind: 'this' }
 
+/**
+ * Return the direct type-expression edges owned by one node.
+ * @param node - compiler-independent type node to inspect.
+ * @returns graph-local ids of its direct child type nodes.
+ */
+export function childTypeNodeIds(node: TypeNodeModel): TypeNodeId[] {
+  switch (node.kind) {
+    case 'parenthesized':
+    case 'operator': return [node.type]
+    case 'reference': return [...node.arguments]
+    case 'union':
+    case 'intersection': return [...node.types]
+    case 'array': return [node.element]
+    case 'tuple': return node.elements.map(element => element.type)
+    case 'indexed-access': return [node.object, node.index]
+    case 'conditional': return [node.check, node.extends, node.whenTrue, node.whenFalse]
+    case 'mapped': return [
+      ...(node.parameter.constraint === undefined ? [] : [node.parameter.constraint]),
+      ...(node.parameter.default === undefined ? [] : [node.parameter.default]),
+      ...(node.nameType === undefined ? [] : [node.nameType]),
+      ...(node.value === undefined ? [] : [node.value]),
+    ]
+    case 'template-literal': return node.spans.map(span => span.type)
+    case 'type-query':
+    case 'import-type': return [...node.arguments]
+    case 'predicate': return node.type === undefined ? [] : [node.type]
+    case 'infer': return [
+      ...(node.parameter.constraint === undefined ? [] : [node.parameter.constraint]),
+      ...(node.parameter.default === undefined ? [] : [node.parameter.default]),
+    ]
+    case 'keyword':
+    case 'literal':
+    case 'object':
+    case 'function':
+    case 'constructor':
+    case 'this': return []
+    default: return assertNever(node)
+  }
+}
+
 /** Type declarations and expressions owned by one face. */
 export interface TypeGraph {
   readonly declarations: readonly TypeDeclarationModel[]
   readonly nodes: readonly TypeNodeModel[]
 }
+
+function assertNever(value: never): never {
+  throw new Error(`unsupported model variant ${JSON.stringify(value)}`)
+}

+ 2 - 36
packages/typert/generator/src/renderer.ts

@@ -4,6 +4,7 @@
  * @module @deepseek-ai/dsh-typert-generator/renderer
  */
 
+import { childTypeNodeIds } from './model.ts'
 import type {
   MemberModel,
   ParameterModel,
@@ -254,7 +255,7 @@ export class TypeGraphRenderer {
       const node = this.node(id)
       if (node.kind === 'reference' && node.target.kind === 'declaration') visitDeclaration(node.target.symbol)
       if (node.kind === 'import-type' && node.target?.kind === 'declaration') visitDeclaration(node.target.symbol)
-      for (const child of childTypeNodes(node)) visitNode(child)
+      for (const child of childTypeNodeIds(node)) visitNode(child)
       for (const signature of nodeSignatures(node)) visitSignature(signature)
       if (node.kind === 'object') for (const member of node.members) visitMember(member)
     }
@@ -328,41 +329,6 @@ export class TypeGraphRenderer {
   }
 }
 
-function childTypeNodes(node: TypeNodeModel): TypeNodeId[] {
-  switch (node.kind) {
-    case 'parenthesized':
-    case 'operator': return [node.type]
-    case 'reference': return [...node.arguments]
-    case 'union':
-    case 'intersection': return [...node.types]
-    case 'array': return [node.element]
-    case 'tuple': return node.elements.map(element => element.type)
-    case 'indexed-access': return [node.object, node.index]
-    case 'conditional': return [node.check, node.extends, node.whenTrue, node.whenFalse]
-    case 'mapped': return [
-      ...(node.parameter.constraint === undefined ? [] : [node.parameter.constraint]),
-      ...(node.parameter.default === undefined ? [] : [node.parameter.default]),
-      ...(node.nameType === undefined ? [] : [node.nameType]),
-      ...(node.value === undefined ? [] : [node.value]),
-    ]
-    case 'template-literal': return node.spans.map(span => span.type)
-    case 'type-query':
-    case 'import-type': return [...node.arguments]
-    case 'predicate': return node.type === undefined ? [] : [node.type]
-    case 'infer': return [
-      ...(node.parameter.constraint === undefined ? [] : [node.parameter.constraint]),
-      ...(node.parameter.default === undefined ? [] : [node.parameter.default]),
-    ]
-    case 'keyword':
-    case 'literal':
-    case 'object':
-    case 'function':
-    case 'constructor':
-    case 'this': return []
-    default: return assertNever(node)
-  }
-}
-
 function nodeSignatures(node: TypeNodeModel): SignatureModel[] {
   return node.kind === 'function' || node.kind === 'constructor' ? [node.signature] : []
 }

+ 33 - 0
packages/typert/generator/tests/renderer.spec.ts

@@ -6,12 +6,45 @@ import type {
   TypeGraph,
   TypeNodeModel,
 } from '../src/model.ts'
+import { childTypeNodeIds } from '../src/model.ts'
 import { TypeGraphRenderError, TypeGraphRenderer } from '../src/renderer.ts'
 
 const location = { file: 'fixture.ts', line: 1, column: 1 } as const
 const documentation = { tags: [] } as const
 
 describe('TypeGraphRenderer defensive and optional shapes', () => {
+  it('enumerates direct child edges for every type node kind', () => {
+    const signature = { typeParameters: [], parameters: [], returns: 'leaf' } as const
+    const cases: readonly (readonly [TypeNodeModel, readonly string[]])[] = [
+      [keyword('keyword', 'string'), []],
+      [{ id: 'literal', kind: 'literal', value: 1, text: '1' }, []],
+      [{ id: 'parenthesized', kind: 'parenthesized', type: 'leaf' }, ['leaf']],
+      [{ id: 'reference', kind: 'reference', name: 'Ref', target: { kind: 'standard', name: 'Ref' }, arguments: ['left', 'right'] }, ['left', 'right']],
+      [{ id: 'union', kind: 'union', types: ['left', 'right'] }, ['left', 'right']],
+      [{ id: 'intersection', kind: 'intersection', types: ['left', 'right'] }, ['left', 'right']],
+      [{ id: 'array', kind: 'array', element: 'leaf' }, ['leaf']],
+      [{ id: 'tuple', kind: 'tuple', elements: [{ type: 'leaf', optional: false, rest: false }] }, ['leaf']],
+      [{ id: 'object', kind: 'object', members: [] }, []],
+      [{ id: 'function', kind: 'function', signature }, []],
+      [{ id: 'constructor', kind: 'constructor', abstract: false, signature }, []],
+      [{ id: 'indexed', kind: 'indexed-access', object: 'left', index: 'right' }, ['left', 'right']],
+      [{ id: 'operator', kind: 'operator', operator: 'keyof', type: 'leaf' }, ['leaf']],
+      [{ id: 'conditional', kind: 'conditional', check: 'check', extends: 'extends', whenTrue: 'yes', whenFalse: 'no' }, ['check', 'extends', 'yes', 'no']],
+      [{ id: 'infer-full', kind: 'infer', parameter: { id: 'infer', name: 'Value', const: false, constraint: 'constraint', default: 'fallback' } }, ['constraint', 'fallback']],
+      [{ id: 'infer-empty', kind: 'infer', parameter: { id: 'infer', name: 'Value', const: false } }, []],
+      [{ id: 'mapped-full', kind: 'mapped', parameter: { id: 'key', name: 'Key', const: false, constraint: 'constraint', default: 'fallback' }, nameType: 'name', value: 'value', readonly: 'preserve', optional: 'preserve' }, ['constraint', 'fallback', 'name', 'value']],
+      [{ id: 'mapped-empty', kind: 'mapped', parameter: { id: 'key', name: 'Key', const: false }, readonly: 'preserve', optional: 'preserve' }, []],
+      [{ id: 'template', kind: 'template-literal', head: '', spans: [{ type: 'leaf', text: '' }] }, ['leaf']],
+      [{ id: 'query', kind: 'type-query', expression: 'value', arguments: ['leaf'] }, ['leaf']],
+      [{ id: 'import', kind: 'import-type', module: 'fixture', arguments: ['leaf'], typeof: false }, ['leaf']],
+      [{ id: 'predicate-full', kind: 'predicate', asserts: false, parameter: 'value', type: 'leaf' }, ['leaf']],
+      [{ id: 'predicate-empty', kind: 'predicate', asserts: true, parameter: 'value' }, []],
+      [{ id: 'this', kind: 'this' }, []],
+    ]
+
+    for (const [node, expected] of cases) expect(childTypeNodeIds(node)).toEqual(expected)
+  })
+
   it('renders optional source shapes and traverses every optional closure edge', () => {
     const dependency = declaration('dependency', 'Dependency', 'interface')
     const graph: TypeGraph = {