/** * DI Graph Analyzer (pass 2) * * Walks constructor injection from a project's root classes down to leaves: * * - @inject(TOKEN) params → token lookup in the binding table → bound impl * - @multiInject(TOKEN) → fan-out edge to EVERY binding of that token * - bare typed params → checker resolves the type to a class (inject-by-type) * - toConstantValue/toDynamicValue bindings → leaf nodes, no recursion * - unresolvable tokens/types → kind "unresolved" nodes (generation never fails) * * Roots: @DocumentDesign() classes when the project has any; otherwise every * DI-registered class in the project that no other project class injects * (the tops of the local DAG). */ import * as ts from 'typescript'; import { DiDesign, DiGraph, DiNodeKind, DiScope } from './model'; import { BindingTable } from './bindings'; export declare class ParamInjection { expr: ts.Expression | null; multi: boolean; optional: boolean; unmanaged: boolean; constructor(expr: ts.Expression | null, multi: boolean, optional: boolean, unmanaged: boolean); } export declare function readParamDecorators(param: ts.ParameterDeclaration): ParamInjection; /** A `@DocumentDesign` class — the explicit DI-design root (server controller or designed-lib impl). */ export declare function isDocumentDesignClass(cls: ts.ClassDeclaration): boolean; /** * The node kind for a `@DocumentDesign` root, chosen by the analyzer's root mode * rather than the decorator (a single `@DocumentDesign` marks both). `server` → * `controller`, `designed-lib` → `apiImplementation`. */ export type DiRootMode = 'controller' | 'apiImplementation'; export declare function rootKindForMode(mode: DiRootMode): DiNodeKind; export declare function findConstructor(cls: ts.ClassDeclaration): ts.ConstructorDeclaration | null; /** All class declarations in files under the project root. */ export declare function projectClasses(program: ts.Program, workspaceRoot: string, projectRoot: string): ts.ClassDeclaration[]; /** * One normalized injection site, framework-agnostic — produced by a builder's * `collectInjections(cls)` hook and processed by the shared base. Inversify * emits `token`/`type` from constructor params; Angular emits `angularToken` * from constructor params AND `inject()` field initializers. * * Modes: * - `token` Inversify `@inject`/`@multiInject`: table lookup; an unbound * token becomes an `unresolved` node (no class fallback). * - `type` Inversify bare typed param: resolve the declared TYPE to a * class (inject-by-type); no table lookup. * - `angularToken` Angular `inject(T)` / `@Inject(T)` / bare typed ctor param: * table lookup FIRST (provider table), then fall back to * resolving the token expression as a class, else unresolved. */ export declare class Injection { mode: 'token' | 'type' | 'angularToken'; /** Token/type expression (modes `token`/`angularToken`, or the type identifier for `type`). */ expr: ts.Expression | ts.Identifier | null; multi: boolean; optional: boolean; paramName: string; paramType: string; constructor(mode: 'token' | 'type' | 'angularToken', expr: ts.Expression | ts.Identifier | null, paramName: string, paramType: string, multi?: boolean, optional?: boolean); } /** * Builds ONE self-contained `DiDesign` for a single root. A fresh instance is * created per root so its maps (visited/classIds/usedIds/...) are scoped to * that root's tree — a dependency shared by two roots is therefore walked * (and duplicated) into each root's design, not hidden under whichever root * reached it first. * * Framework-agnostic base: subclasses implement {@link collectInjections} (and * override {@link rootKindOf} for Angular components); everything else — node * ids, leaf/unresolved labeling, token→binding resolution, factory-dep edges, * level assignment — is shared so Inversify and Angular render identically. */ export declare abstract class DiDesignBuilder { protected readonly checker: ts.TypeChecker; protected readonly table: BindingTable; protected readonly workspaceRoot: string; protected readonly design: DiDesign; private readonly classIds; private readonly leafIds; private readonly unresolvedIds; private readonly usedIds; private readonly visited; protected rootClass: ts.ClassDeclaration | null; constructor(checker: ts.TypeChecker, table: BindingTable, workspaceRoot: string, design: DiDesign); /** Collect the injection sites for one class (constructor params, field inject(), ...). */ protected abstract collectInjections(cls: ts.ClassDeclaration): Injection[]; /** * Node kind for a root/reached class. Default: the ROOT box takes the design's * `rootKind` (`controller`/`apiImplementation`, chosen by root mode); every * reached dependency is a plain `class`. Angular overrides this to render * component classes as `component`. */ protected rootKindOf(cls: ts.ClassDeclaration): DiNodeKind; addRoot(cls: ts.ClassDeclaration): void; /** * Register (or fetch) the node for a class, returning its stable id. * `scopeHint` carries the scope of the module binding the class was reached * through (e.g. bind(TOKEN).to(X).inSingletonScope() where X is not self-bound). * `apiType` is the declared param/field type the class was injected as; when it * differs from the impl class name it becomes the node's `api` (rendered as the * primary box label, with the impl class in parens). Set on first reach. */ protected classNode(cls: ts.ClassDeclaration, scopeHint?: DiScope, apiType?: string): string; private claimId; /** Scope from the class's own decorator/module binding, or undefined if it has none. */ private classScope; /** * Leaf box for a constant/dynamic binding. B0: the box is labeled by the * DECLARED param TYPE (e.g. `FirestoreConfig`, `ClientConfig`) — the DI * contract — while the bound expression (`buildConfigFromEnv(...)` / * `TOKEN (dynamic)`) is kept as `detail`. A dynamic leaf also fans out to * each of its `useFactory` `deps` (Angular; empty for Inversify). */ private leafNode; /** Edges from a `useFactory` leaf to each declared `deps: [...]` token (Angular). */ private expandFactoryDeps; /** * `unresolved` placeholder box. B0: labeled by the declared param TYPE * (`className`) with the resolving token expression kept as `detail` (and * surfaced in `design.unresolved` for diagnostics). */ private unresolvedNode; protected walkClass(cls: ts.ClassDeclaration): void; /** Resolve one injection to a node and record the edge(s). Never throws. */ private processInjection; /** Inversify `@inject`/`@multiInject` and Angular `inject()`/`@Inject`/bare token. */ private processTokenInjection; /** * `@inject(TOKEN) p: Provider` draws `Consumer -> X`, NOT `Consumer -> Provider -> X`. * * A Provider is DI plumbing: it exists because `Provider` is erased at runtime and needs a * token. Nobody reading the design wants a box for it. What they want to see is that the * consumer gets Xs — and because X's own binding supplies the scope, a transient X renders as * the stacked "one instance per get()" box. * * Returns true when the token was a registered provider and the edge has been drawn. */ private processProviderInjection; private bindingTarget; /** Inversify bare typed param — resolve the declared type directly to a class. */ private processTypeInjection; } /** * Inversify builder: injection sites are constructor params — `@inject`/ * `@multiInject` tokens or bare typed (inject-by-type) params. */ export declare class InversifyDesignBuilder extends DiDesignBuilder { protected collectInjections(cls: ts.ClassDeclaration): Injection[]; } export declare function byClassName(a: ts.ClassDeclaration, b: ts.ClassDeclaration): number; /** * Assign each node its LONGEST-path depth from the design's root (root = level * 0, its direct injections = level 1, ...) and record the deepest level. A node * reached from multiple parents takes its DEEPEST depth, so it always sits below * everything that depends on it (a dependency is never on the same or a shallower * level than its dependent — e.g. a config injected by both a level-2 service and * a level-3 client lands at level 4, one below that client). Levels are computed * by edge relaxation bounded by the reachable node count, which terminates even * if the DI graph contains a cycle; the root is pinned at 0. */ export declare function assignLevels(design: DiDesign): void; /** * Build one self-contained downward design tree for a single root class, using * the builder produced by `makeBuilder` (Inversify or Angular). `rootKind` is * the node kind for the root box (`controller`/`component`/`class`). */ export declare function buildDesign(root: ts.ClassDeclaration, rootKind: DiNodeKind, workspaceRoot: string, makeBuilder: (design: DiDesign) => DiDesignBuilder): DiDesign; /** * Build the full Inversify DI graph for one project: one self-contained * `DiDesign` per @DocumentDesign root. `projectRoot` is workspace-relative. * * Both `rootMode`s root on @DocumentDesign classes; the mode only sets the root * box kind (`'controller'` for server, `'apiImplementation'` for designed-lib). * `includeLibraryRoots` (default false) lets a project with NO @DocumentDesign * class fall back to top-of-DAG DI classes (rendered as plain `class` roots). */ export declare function buildDiGraph(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string, includeLibraryRoots?: boolean, rootMode?: DiRootMode): DiGraph;