refactor(typert): share type node traversal

This commit is contained in:
imccyu
2026-07-29 23:55:13 +08:00
parent 49fbfc2249
commit b5ec489c9f
4 changed files with 81 additions and 71 deletions
@@ -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
View File
@@ -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
View File
@@ -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] : []
}
@@ -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 = {