/** * API contract AST accessors * * The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint / * injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project * programs, the source index, relation accumulation) and had grown past the file-size limit. * * Everything here is parser-level on purpose. Decorators must be read exactly as written, and a * plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug * api-scanner's source pre-pass exists to guard against. */ import * as ts from 'typescript'; import { ApiClassInfo, ApiMethodMeta, ApiTransport, EmptiedApiContract, ExternalSystemDeclaration, NonLiteralDecoratorArg, UndeclaredExternalCaller, UnresolvedEndpointPath } from './api-relations'; /** * The module-scope `const NAME = ''` bindings of ONE source file. * * A contract that hoists its route to a constant (`@ApiPath(WHATSAPP_API_PATH)`) is good practice — * it lets a sibling contract and its callers share the symbol — but a decorator argument is read as * TEXT here, with no checker to constant-fold it. Without this table such an argument resolved to * nothing: the class lost its basePath, and a class whose every @Endpoint path was a constant * resolved to zero methods and was dropped from the graph entirely. * * Deliberately SAME-MODULE only. Following an import would mean resolving modules, which is exactly * what the source pre-pass avoids (it can be diverted to a decorator-erased `.d.ts`). A cross-module * constant is therefore still unresolvable — and is REPORTED rather than silently dropped, see * DecoratorArgDiagnostics. */ export declare class ModuleStringConstants { private readonly byName; constructor(byName: Map); lookup(name: string): string | null; } /** The module-scope string constants of `sourceFile`, parsed once per file. */ export declare function stringConstantsOf(sourceFile: ts.SourceFile): ModuleStringConstants; /** The string an initializer denotes, unwrapping `as const` / parentheses, else null. */ export declare function stringValueOf(expr: ts.Expression | undefined): string | null; /** * ONE decorator argument that had to be a string, and what came of it. * * `value` is the string when it was a literal or resolved through a same-module constant. * `unresolvedName` is the argument as written (`WHATSAPP_API_PATH`) when it is present but could not * be reduced — the case that must be reported, never silently dropped. Both are null when the * argument is simply absent. */ export declare class DecoratorArgValue { readonly value: string | null; readonly unresolvedName: string | null; constructor(value: string | null, unresolvedName: string | null); } /** Read one decorator argument as a string, resolving same-module constants. */ export declare function decoratorArgValue(expr: ts.Expression | undefined, constants: ModuleStringConstants): DecoratorArgValue; /** * Collects everything this parser-only pass had to drop: decorator arguments it could not reduce to * a string, plus the two of those that are FATAL rather than merely lossy. * * A same-module constant now resolves, but a cross-module one (`import { PATH } from './paths'`) * genuinely cannot — the source pre-pass has no checker by design. That gap used to be invisible: * the contract simply came out with no basePath, or with fewer methods, or not at all. Recording it * turns a silent drop into a named one, pointing at the exact file, line and identifier. * * Three sinks, because the consequences differ. `record` is the warning stream (a @Queue name falls * back to a derived one, so the graph is degraded, not wrong). `recordUnresolvedPath` and * `recordEmptiedContract` are collected so generation can FAIL — one aggregated error naming every * offender, because an author fixing five constants wants all five in one run. */ export declare class DecoratorArgDiagnostics { private readonly workspaceRoot; private readonly found; private readonly unresolvedPaths; private readonly emptied; private readonly undeclaredCallers; constructor(workspaceRoot: string); /** Record `argument` (as written) as unresolvable at `node`'s location. */ record(api: string, decorator: string, method: string | null, argument: string, node: ts.Node): void; /** Record an `@Endpoint` whose path argument is unreadable — fatal, see UnresolvedEndpointPathError. */ recordUnresolvedPath(api: string, method: string, argument: string, node: ts.Node): void; /** Record a class that declared `declared` `@Endpoint` methods and kept none of them. */ recordEmptiedContract(api: string, declared: number, node: ts.Node): void; /** Record an `external` `@Endpoint` whose caller is unreadable — fatal, see UndeclaredExternalCallerError. */ recordUndeclaredCaller(api: string, method: string, argument: string, node: ts.Node): void; all(): NonLiteralDecoratorArg[]; unresolvedEndpointPaths(): UnresolvedEndpointPath[]; emptiedContracts(): EmptiedApiContract[]; undeclaredExternalCallers(): UndeclaredExternalCaller[]; private locate; } /** {api, owner: `project`, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */ export declare function apiClassInfoFrom(cls: ts.ClassDeclaration, project: string, diagnostics?: DecoratorArgDiagnostics | null): ApiClassInfo | null; export declare function apiTransport(cls: ts.ClassDeclaration): ApiTransport; /** * Every `@Endpoint(path, kind)` method on a contract class, in declaration order. * * `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the * source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting * would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely * the blindness the required argument exists to remove. * * `path` is NOT skippable. It may be a same-module constant; an argument that is present but still * cannot be reduced is recorded on `diagnostics` as an UnresolvedEndpointPath, which FAILS generation * later. Upstream components need the URL — a client computes its request as `basePath + path` — so * dropping the method here shipped a contract missing routing information, and a class whose every * path was a constant lost every method and disappeared from the graph entirely. * * A class that declared endpoints and kept NONE of them is recorded too: `buildApiContracts` skips * zero-method classes, which is the door a gutted contract used to leave through unannounced. */ export declare function endpointMethodsOf(cls: ts.ClassDeclaration, api: string, constants?: ModuleStringConstants, diagnostics?: DecoratorArgDiagnostics | null): ApiMethodMeta[]; /** * The outcome of reading `@Endpoint(path, 'external', { calledBy, callerKind })`'s third argument: * either the resolved declaration, or the reason it could not be resolved (never both). */ export declare class ExternalCallerRead { readonly declaration: ExternalSystemDeclaration | null; /** What was wrong, as written, for the diagnostic. Null exactly when `declaration` is set. */ readonly problem: string | null; constructor(declaration: ExternalSystemDeclaration | null, /** What was wrong, as written, for the diagnostic. Null exactly when `declaration` is set. */ problem: string | null); } /** * Read the declared caller out of the @Endpoint OPTIONS OBJECT LITERAL — `args[2]`, not a positional * argument, because that is where `formPost` already lives and one options bag beats two. * * Everything unreadable is a PROBLEM, never a default: an unknown `callerKind` draws the wrong shape * (which teaches the reader something false), and a missing `calledBy` puts us back at a box that can * only name our own contract. The kind default applies ONLY to the case the API deliberately allows — * `calledBy` present, `callerKind` absent. */ export declare function externalCallerOf(arg: ts.Expression | undefined, constants: ModuleStringConstants): ExternalCallerRead; /** One property of an object literal, read as a string through the same constant folding as an argument. */ export declare function objectPropertyValue(literal: ts.ObjectLiteralExpression, name: string, constants: ModuleStringConstants): DecoratorArgValue; /** `@Queue('...')` override when present and resolvable, else the derived `${Api}-${method}`. */ export declare function queueNameOf(member: ts.MethodDeclaration, api: string, name: string, constants: ModuleStringConstants, diagnostics: DecoratorArgDiagnostics | null): string; /** Record an argument that is present but unresolvable; a resolved or absent one is silent. */ export declare function reportUnresolved(diagnostics: DecoratorArgDiagnostics | null, api: string, decorator: string, method: string | null, arg: DecoratorArgValue, node: ts.Node): void; /** The arguments of a decorator's call expression, or [] when it is a bare `@Foo` reference. */ export declare function decoratorArgs(decorator: ts.Decorator): ts.NodeArray | ts.Expression[]; /** The named decorator on a class member, or null. */ export declare function memberDecorator(member: ts.ClassElement, name: string): ts.Decorator | null; /** * The first argument of a class decorator as a string (`@ApiPath('/x')`, `@ApiPath(X_PATH)`), else * null. A same-module constant resolves; anything else is recorded on `diagnostics`. */ export declare function decoratorStringArg(cls: ts.ClassDeclaration, name: string, constants?: ModuleStringConstants, diagnostics?: DecoratorArgDiagnostics | null, api?: string): string | null; /** The constructor's parameters, or [] when the class declares no constructor. */ export declare function constructorParamsOf(cls: ts.ClassDeclaration): readonly ts.ParameterDeclaration[]; /** * The bare name of a type reference (`GmailApi`, or `gmail.GmailApi` -> `GmailApi`), else null. * Generic wrappers are deliberately NOT unwrapped: `Provider` hands out the contract * lazily, which is still a use, but it is not the shape any of these seams take today and guessing * at type arguments would start matching things that merely mention a contract. */ export declare function typeReferenceName(type: ts.TypeNode | undefined): string | null; /** Every type name in the class's `implements` clause — the contracts this class IS, not ones it calls. */ export declare function implementedTypeNames(cls: ts.ClassDeclaration): Set; export declare function isAbstractClass(cls: ts.ClassDeclaration): boolean; export declare function hasClassDecorator(cls: ts.ClassDeclaration, name: string): boolean; /** * The service a client-factory call aims at, from its config argument: * `createRpcClient(WarmupApi, new ClientConfig('helper-fsdb'))` → `'helper-fsdb'`. * * Only a `new ClientConfig('')` yields a name. A variable, a template string * or a computed expression yields null — the target is genuinely unknown at scan time, and the * runtime graph must fall back to fan-out (loudly) rather than guess. */ export declare function targetServiceOf(call: ts.CallExpression): string | null; export declare function calleeMethodName(call: ts.CallExpression): string | null; export declare function isTestFile(fileName: string): boolean; /** {api, owner, type:'rpc'|'pubsub'} for an in-repo contract class, else null. */ export declare function apiClassInfoFromNode(node: ts.Node, project: string, diagnostics?: DecoratorArgDiagnostics | null): ApiClassInfo | null; /** * {api, owner, type:'external'} for a VENDOR contract, else null. * * A vendor contract cannot be detected the way an in-repo one is. It carries no @ApiPath (there is * no route — the call leaves through a vendor SDK), and it is usually a plain `interface` bound to a * Symbol token, which is not even a class. So inside a project the workspace has DECLARED external * (`runtime-architecture.externalApiPaths`) the signal is structural instead: an exported * `interface`/`abstract class` whose name ends in `Api`. That deliberately picks up `GmailApi` and * `StorageApi` while leaving their DTOs, `*Config` types and `*Client` implementations alone. */ export declare function externalApiInfoFrom(node: ts.Node, project: string): ApiClassInfo | null; /** * The `@externalSystem [label]` JSDoc tag on a vendor contract, or null when absent. * * JSDoc rather than a decorator is not a style choice: these seams are TS `interface`s, and TS has * no interface decorators. Without the tag the contract still renders — as the generic dashed box it * always was — so this is purely additive and nothing needs migrating. * * The label defaults to the contract name minus its `Api` suffix (`FirestoreAdminApi` → * `FirestoreAdmin`), because the label is the node IDENTITY: two contracts that mean the same system * must be given the SAME explicit label to converge on one node. * * An unrecognised kind is ignored rather than defaulted. Silently drawing a `@externalSystem * databse` typo as a generic box is recoverable; drawing it as the wrong shape teaches the reader * something false about the architecture. */ export declare function externalSystemTagFrom(node: ts.Node, api: string): ExternalSystemDeclaration | null; /** True when the declaration carries an `export` modifier. */ export declare function isExported(node: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean; export declare function collectTsFiles(dir: string): string[];