import { TSESTree } from '@typescript-eslint/types'; import { ParserServices, TSESLint } from '@typescript-eslint/utils'; import * as ts from 'typescript'; import { ClassLike } from './signals'; /** * A Lumino connection leaks if and only if the **sender outlives the * receiver**. Connections are stored in `WeakMap`s keyed by both sender and * receiver, so a sender+receiver island that becomes unreachable together is * collected whether or not a `thisArg` was passed. * * The helpers in this module therefore classify the *sender* rather than * looking for absent cleanup on the receiver. Everything defaults to * `'unknown'` — the rules only report senders that are positively proven to * outlive their receiver. */ export type SenderLifetime = /** The sender is the enclosing instance itself (or something it intrinsically owns). */ 'self' /** The enclosing class constructs, adopts, or disposes the sender. */ | 'owned' /** A model/context in an MVC pair whose view is the enclosing class. */ | 'long-lived-model' /** An injected application service from the configured allowlist. */ | 'long-lived-service' /** Not provably anything — never reported. */ | 'unknown'; /** * Application-lifetime service types. A connection to a signal owned by one of * these outlives any per-document or per-widget receiver. * * Deliberately short and curated. Notably absent: `DocumentRegistry.Context` * and `JupyterFrontEnd` — both are routinely *shorter*-lived than the objects * that hold them, and including them re-introduces false positives. */ export declare const DEFAULT_LONG_LIVED_TYPES: readonly string[]; export interface SenderChain { /** The expression owning the signal — `a.b` in `a.b.changed.connect(...)`. */ sender: TSESTree.Expression; /** Sub-expressions from the root outward, ending with `sender`. */ prefixes: TSESTree.Expression[]; /** Base of the chain, or null when it is not a plain object path. */ root: TSESTree.ThisExpression | TSESTree.Identifier | null; /** Property names from the root to the sender; null for computed access. */ path: (string | null)[]; /** True when a call appears in the chain — not a stable object path. */ hasCall: boolean; /** The signal's own property name (`changed`), when statically known. */ signalName: string | null; } /** * Splits the receiver of a `.connect()` call into the signal name and the * object path that owns it. Returns null when the receiver is not a * `.` member expression (a bare `signal.connect(cb)` on a * local has no identifiable sender). */ export declare function buildSenderChain(signalExpr: TSESTree.Expression): SenderChain | null; /** * Per-class facts about member fields, gathered in one pass over the class * body. Nested classes are opaque. */ /** * One expression the class tears down sender-side. The node is kept alongside * its text because names are not unique across scopes: `@jupyterlab/outputarea` * has an `output` that is an `IOutputModel` in one loop and an adopted `Widget` * in another method, and matching on text alone conflates them. */ export interface TeardownRecord { text: string; node: TSESTree.Node; } export interface ClassOwnershipFacts { /** Field names (without `#`) the class constructs, adopts, or disposes. */ ownedFields: Set; /** * Expressions the class tears down sender-side: `x.dispose()`, * `Signal.clearData(x)`, `Signal.disconnectSender(x)`, `parent.addWidget(x)`. */ torndownSenders: TeardownRecord[]; /** Getter name → the field it forwards to, for `get editor() { return this._editor; }`. */ getterAliases: Map; } /** * Collects, for one class body, which fields it owns and which senders it * tears down. Both are *suppression* evidence: any hit means a connection to * that sender cannot outlive the receiver, so nothing is reported. */ export declare function collectClassOwnershipFacts(classNode: ClassLike, sourceCode: Readonly, signalLocalNames: ReadonlySet): ClassOwnershipFacts; /** * Does this class have somewhere to put a disconnect at all — a `dispose()` * method, an `isDisposed` member, or an `implements ...Disposable` clause? * * A class with no teardown protocol whatsoever is almost always a plugin-scope * singleton (`MermaidManager`, `ThemeManager`, `LayoutRestorer`): it is created * once in an `activate()` and lives as long as the services it connects to, so * the connection never leaks and there is no `dispose()` in which to write the * fix. It is indistinguishable, from inside the file, from a genuinely * short-lived class that forgot to implement disposal — so both stay silent. */ export declare function classHasDisposalProtocol(classNode: ClassLike): boolean; export interface LifetimeContext { classNode: ClassLike; facts: ClassOwnershipFacts; sourceCode: Readonly; scope: TSESLint.Scope.Scope; checker: ts.TypeChecker | null; services: ParserServices | null; longLivedTypes: ReadonlySet; } export interface SenderAnalysis { lifetime: SenderLifetime; /** Source text of the sender, for the diagnostic message. */ senderText: string; /** Allowlisted service type name, when `lifetime` is `long-lived-service`. */ typeName: string | null; } /** * Classifies the sender of a `.connect()` call. Everything that cannot be * proven long-lived comes back as `'unknown'`, `'self'`, or `'owned'` — all of * which the rules treat as silence. */ export declare function analyzeSender(signalExpr: TSESTree.Expression, context: LifetimeContext): SenderAnalysis; export type CallbackShape = 'bound' | 'inline' | 'referenced' | 'opaque'; /** * How a connected callback can be removed later. * * - `bound`: `fn.bind(this)` allocates a fresh function, so no * `disconnect(callback, ...)` can ever match it by identity. * - `inline`: an arrow or function expression — same problem, unless the * connection carries a `thisArg` that receiver-side cleanup can match. * - `referenced`: a stable identifier or member expression, removable by a * matching `disconnect()`. */ export declare function classifyCallbackShape(callback: TSESTree.CallExpressionArgument): CallbackShape; /** * Measure C: is there a `.disconnect()` anywhere in the file that removes this * callback? * * Searches the whole `Program` — including nested arrow functions, returned * `DisposableDelegate` bodies and helper closures — and matches on the * normalized text of the callback expression rather than on the sender chain. * `@jupyterlab/lsp`'s `registerProvider` connects * `registration.provider.sessionsChanged` and disconnects * `provider.sessionsChanged` with the same `registration.onSessionsChanged` * callback; comparing senders misses it, comparing callbacks does not. * * `arity` is the argument count the disconnect must have to match the connect: * Lumino matches on the exact `(signal, slot, thisArg)` triple, so a * one-argument `connect(cb)` is only undone by a one-argument * `disconnect(cb)`. */ export declare function hasMatchingDisconnect(program: TSESTree.Program, callback: TSESTree.CallExpressionArgument, sourceCode: Readonly, arity: 1 | 2): boolean; /** * Measure E: `disposed`-style wiring is cleaned up sender-side — the signal * fires exactly as its sender is torn down, and Lumino's own * `Widget.dispose()` clears the connection immediately afterwards. This is the * pattern the docs recommend, so neither rule may flag it. */ export declare function isSelfTerminatingSignal(chain: SenderChain | null): boolean;