diff --git a/packages/typert/generator/src/cordis-catalog.ts b/packages/typert/generator/src/cordis-catalog.ts index 9a0e8fd7e5..89c8449b1b 100644 --- a/packages/typert/generator/src/cordis-catalog.ts +++ b/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 diff --git a/packages/typert/generator/src/model.ts b/packages/typert/generator/src/model.ts index 4e59e68d65..c6b7ffbc87 100644 --- a/packages/typert/generator/src/model.ts +++ b/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)}`) +} diff --git a/packages/typert/generator/src/renderer.ts b/packages/typert/generator/src/renderer.ts index d2d993807a..8d9a3c4954 100644 --- a/packages/typert/generator/src/renderer.ts +++ b/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] : [] } diff --git a/packages/typert/generator/tests/renderer.spec.ts b/packages/typert/generator/tests/renderer.spec.ts index 4cca641e03..ae79a1c30c 100644 --- a/packages/typert/generator/tests/renderer.spec.ts +++ b/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 = {