/** * Project the runner's `LintFileResult[]` into the exact wire shape Go * decodes. Both host paths use this so Go receives a byte-stable set of * fields. */ export declare function buildPluginLintResult(results: LintFileResult[]): EslintPluginLintResult; /** * Build per-file {@link LintTask}s from an EslintPluginLintRequest. Each * task carries the file's `configKey` verbatim; the worker uses it to * pick the right `LoadedPlugins` from its per-config map. * * If a file's `configKey` is empty OR missing from `configDirSet`, we * still emit a task (with the empty/unknown key) and let the worker * report the failure via `parseError` — keeping wire-format consistency. */ export declare function buildPluginLintTasks(input: EslintPluginLintRequest, options: BuildPluginLintTasksOptions): LintTask[]; export declare interface BuildPluginLintTasksOptions { /** * Set of `configDirectory` strings the worker pool was initialized * with. Used purely to detect "unknown configKey on the wire" — the * host's invariant is that every file's `configKey` was previously * declared in `WorkerPoolOptions.configs[]`. A miss is an internal * bug surfaced through {@link onUnknownConfigKey}. */ configDirSet: ReadonlySet; /** * Invoked once per file whose `configKey` is not in `configDirSet`. * The helper otherwise forwards `configKey` verbatim — the worker * reports the failure via `parseError`. */ onUnknownConfigKey?: (filePath: string, configKey: string) => void; } declare interface Comment { type: 'Line' | 'Block' | 'Shebang'; value: string; range: [number, number]; loc: SourceLocation; } /** * Types shared between the eslint-plugin host and its lint workers. * * Wire-format / IPC frame types (the Go↔Node frame contract) live in * `src/ipc/protocol.ts` — the single source the CLI host consumes. */ /** * Per-config descriptor handed to the worker pool. Each worker imports * every descriptor's `configPath` once at init, then routes per-file * lint tasks via `configKey === configDirectory` to the right plugin * instances. The `configDirectory` here MUST match the value Go writes * into `EslintPluginLintFile.ConfigKey` byte-for-byte; the worker uses * it as a Map key for per-file dispatch. */ export declare interface ConfigDescriptor { /** Absolute filesystem path of the rslint config file (`rslint.config.{js,mjs,ts,mts}`). */ configPath: string; /** Absolute filesystem path of the directory holding the config file. * Matches the `ConfigKey` Go emits per file during plugin-lint dispatch. */ configDirectory: string; } /** * Build a {@link PluginLintHost} over a freshly-initialized WorkerPool. * * `configs` empty ⇒ the pool spawns no workers (no-op fast path); a * `lint` call then returns empty per-file results. Init rejects if a * referenced plugin fails to import — the caller decides how loud to be * (CLI fails the run; LSP logs and serves empty). */ export declare function createPluginLintHost(configs: ConfigDescriptor[], onLog?: WorkerPoolOptions['onLog'], singleThreaded?: boolean): Promise; /** A single diagnostic emitted by a rule call to `context.report`. */ export declare interface Diagnostic { ruleName: string; messageId?: string; message: string; /** * Position offsets into the source text. INSIDE the runner these * are UTF-16 code-unit indices (matching native JS string * indexing). On the IPC boundary in `ecma-language-plugin.ts` they * are converted to UTF-8 BYTE offsets before shipping to Go — Go's * `scanner.GetECMALineAndUTF16CharacterOfPosition` takes bytes. * Callers that drain diagnostics via the public lintFile path * therefore observe byte offsets; the in-process runner code paths * still work in UTF-16 units. */ startPos: number; endPos: number; fixes?: Fix[]; suggestions?: SuggestionDescriptor[]; } /** * Wire-format lint request as it leaves Go and arrives at the host. * * Mirrors Go's `internal/linter.EslintPluginLintRequest`. Field types * are permissive (`unknown` for opaque pass-through fields) so the host * doesn't need to re-validate Go's serialization. */ export declare interface EslintPluginLintRequest { files: ReadonlyArray<{ path: string; /** * Optional file content override. The CLI host leaves it absent — * the worker reads from disk via `readFileSync` (and re-reads * post-fix content across `--fix` passes). The LSP host sends it so * an unsaved editor buffer's overlay text is linted instead of the * stale on-disk copy. Also used by in-process test harnesses. */ text?: string; /** * Per-file `languageOptions`, computed by Go via `GetConfigForFile` * (flat-config files-glob match + deep merge). Opaque here; the * worker reads `sourceType`/`globals`/`parserOptions.ecmaFeatures`. */ languageOptions?: unknown; settings?: Record; /** * The owning config's directory in the SAME form the host used as * its `ConfigDescriptor.configDirectory` (CLI: fs path; LSP: URI). * The worker uses it to pick the right `LoadedPlugins`. Empty when * no JS config governs the file. */ configKey?: string; }>; rules?: Record; /** Collect autofixes (driven by Go's `--fix`). */ fix?: boolean; suggestionsMode?: 'off' | 'eager'; } /** * Wire-format lint result, returned to Go. Mirrors Go's * `internal/linter.EslintPluginFileResult`. Pure projection — drops the * runner-internal aggregate fields (`fixes`, `suggestionsCount`) that * Go doesn't decode. */ export declare interface EslintPluginLintResult { results: Array<{ filePath: string; diagnostics: unknown[]; parseError?: string; cancelled?: boolean; ruleErrors?: Array<{ rule: string; message: string; }>; }>; } /** Compact node shape — what plugin code reads through `context.sourceCode`. */ export declare interface ESTreeNode { type: string; range: [number, number]; loc: SourceLocation; parent?: ESTreeNode; [key: string]: unknown; } /** * The `fixer` object passed to ESLint plugin code via `descriptor.fix(fixer)` * (and `descriptor.suggest[i].fix(fixer)`). Plugins call methods on this * object to express their intended edit; each method returns a `Fix` * record that the runner ships back to Go for application. * * The full ESLint fixer surface is 8 methods. Most plugins use the * insert/remove variants, not just `replaceText`, so leaving any out * breaks real plugins. */ /** * A single fix edit. Mirrors ESLint's `Fix` interface: byte range + * replacement text. Empty text + non-empty range = remove. Empty range * (start === end) + non-empty text = insert. */ declare interface Fix { /** * Offset range `[start, end)` into the source text. * * INSIDE the runner these are UTF-16 code-unit indices (matching * native JS string indexing). On the IPC boundary they get * converted to UTF-8 BYTE offsets — Go's TextRange consumes bytes. * The conversion lives in `ecma-language-plugin.ts` next to the * diagnostic drain; do not double-convert from rule code. */ range: [number, number]; /** Replacement text (may be empty for remove). */ text: string; } /** Minimal node shape — the fixer only needs `range`. */ declare interface HasRange { range: [number, number]; } /** * The forwarded subset of ESLint flat-config `languageOptions` exposed * to plugin rules as `context.languageOptions`. Plugin rules read it * to branch on `languageOptions.parserOptions.ecmaFeatures.jsx`, * `parserOptions.sourceType`, or `languageOptions.globals`. * * Field set matches what the wire payload from Go carries in each file's * `languageOptions` (computed by Go via `GetConfigForFile`); out-of-band * fields the runner doesn't reproduce (custom `parser`, * `parserOptions.project`, `parserOptions.tsconfigRootDir`) are * intentionally absent. This is the contract the linter package and the * plugin runtime share — any change here must update both * `ecma-language-plugin.ts:LintFileRequest` and Go's * `EslintPluginLintFile.LanguageOptions`. */ declare interface LanguageOptions { /** * ECMAScript version target — top-level in ESLint v10. Plugin rules * read this as `ctx.languageOptions.ecmaVersion`. The v8-era * `parserOptions.ecmaVersion` nesting is intentionally NOT mirrored * here: rslint targets v10 cleanly, and we don't expose this field * to user config, so there is no legacy v8 surface to preserve. */ ecmaVersion?: number | 'latest'; /** Module / script / commonjs — top-level in ESLint v10. Same rationale as `ecmaVersion`. */ sourceType?: 'module' | 'script' | 'commonjs'; globals?: Record; /** * Parser-specific extras. Only `ecmaFeatures` lives here in v10 * (rules that gate on `jsx` / `globalReturn` / `impliedStrict` read * via `ctx.languageOptions.parserOptions.ecmaFeatures.*`). Custom * `parser` instances aren't supported by the runner, so we don't * include that field. */ parserOptions?: { ecmaFeatures?: { jsx?: boolean; globalReturn?: boolean; impliedStrict?: boolean; }; }; } /** * Lint a single file. Stateless — `loadedPlugins` is owned by the * Worker; this function does not mutate it. */ export declare function lintFile(req: LintFileRequest, loadedPlugins: LoadedPlugins): LintFileResult; /** Per-file lint request input (called by Worker dispatcher per task). * * The Worker reads source text from disk via `fs.readFileSync(filePath)` * by default — text is intentionally NOT carried over IPC. This drops * the structuredClone cost of shipping every file's contents across * the worker_threads boundary (~60 MB on a 5000-file repo). * * Multi-pass --fix coherence is preserved because cmd/rslint's * applyFixPass writes fixes to disk BEFORE re-dispatching the next * lint pass — the worker reads the post-fix contents. * * `text` here is an in-process override for unit tests that want to * exercise `lintFile` against an in-memory source. The wire shape * (engine.ts → worker postMessage) NEVER carries text. */ export declare interface LintFileRequest { filePath: string; /** In-process override for tests. The IPC wire shape never carries this. */ text?: string; /** * Forwarded subset of user `languageOptions`. Only the fields the * runner actually consumes are typed here; the rest of the user's * `languageOptions` (parser custom hooks, plugin-specific extensions) * is intentionally dropped at the IPC boundary because the worker * doesn't reproduce ESLint's full language plugin pipeline. * * Per ESLint v10's flat-config spec * (https://eslint.org/docs/v10.x/use/configure/language-options), * `ecmaVersion` / `sourceType` / `globals` are TOP-LEVEL properties * of `languageOptions`. `parserOptions` is reserved for parser- * specific extras (`ecmaFeatures`, `allowReserved`). The v8-era * positions (`parserOptions.ecmaVersion` / `parserOptions.sourceType`) * are NOT accepted — rslint targets v10 cleanly. */ languageOptions?: { ecmaVersion?: number | 'latest'; sourceType?: 'module' | 'script' | 'commonjs'; globals?: Record; parserOptions?: { ecmaFeatures?: { jsx?: boolean; globalReturn?: boolean; impliedStrict?: boolean; }; }; }; /** Merged flat-config `settings` for plugin consumption (e.g. `react.version`). */ settings?: Record; /** Map of fully-qualified rule name → config. Already enabled-filtered. */ rules: Record; /** * Whether to materialise plugin `descriptor.fix(fixer)` into the * diagnostic's `fixes` payload. The runner never APPLIES fixes — * application is the caller's job (CLI fix-loop, LSP code-action / * fixAll). CLI sets this whenever `--fix` is on; LSP always sets it so * Quick Fix / source.fixAll see plugin-rule fixes the same way they * see native-rule ones. */ collectFixes: boolean; suggestionsMode: SuggestionsMode; /** Optional Int32Array(SharedArrayBuffer) cancel flag, length-1, for per-node Atomics polling. */ cancelFlag?: Int32Array; /** * Identity of the rslint config that owns THIS file — the * `configDirectory` Go writes into `EslintPluginLintFile.ConfigKey`. The * worker uses this to pick the right `LoadedPlugins` from its * per-config map (`Map`). * * The `lintFile` pipeline never reads this field directly — the * worker has already selected `LoadedPlugins` by the time `lintFile` * runs. It exists on the request only so the worker dispatcher can * route the task before delegating. */ configKey?: string; } /** Per-file lint response — one record per LintFileRequest. */ export declare interface LintFileResult { filePath: string; diagnostics: Diagnostic[]; /** Aggregated fixes (sum of diagnostic.fixes for ApplyRuleFixes). For convenience; same data as diagnostics[].fixes. */ fixes: Fix[]; /** Aggregated suggestions, same convenience as fixes. */ suggestionsCount: number; /** True iff the visit was cancelled mid-flight (cancelFlag observed). */ cancelled: boolean; /** Set when the native parser failed; diagnostics is empty in this case. */ parseError?: string; /** Per-rule errors caught during create() / listener execution. */ ruleErrors?: Array<{ rule: string; message: string; }>; } /** A single task as the pool sees it; equivalent to LintFileRequest minus cancelFlag (pool injects). */ export declare type LintTask = Omit; /** * Loaded plugin shape: only the fields the runner consumes. Plugins may * carry far more (configs, processors, etc.) — we keep references intact * for downstream needs but type only what we use. */ declare interface LoadedPlugin { prefix: string; /** The unwrapped plugin module, ready for `plugin.rules['ruleName']`. */ plugin: { meta?: { name?: string; version?: string; }; name?: string; rules?: Record; configs?: Record; [key: string]: unknown; }; } /** * Loaded plugin set for a single rslint config. `rules` is keyed by * `/` and is the only lookup `lintFile` consults * — the worker has already picked the right `LoadedPlugins` for this * file via its `configKey` map before calling `lintFile`, so there's * no cross-config prefix collision to worry about. */ export declare interface LoadedPlugins { plugins: LoadedPlugin[]; rules: Map; } /** * Load every plugin in `entries` and return them keyed by prefix, with a * Import the user's rslint config file directly and extract plugin * instances from its object-form `plugins` map(s). Each worker calls * this through {@link loadPluginsFromConfigs} * once per assigned config at init. * * Each worker independently imports the config (and transitively its * plugins), naturally anchoring `node_modules` walks at the config's * own location — which is what makes monorepo setups Just Work: a * sub-package config gets its sub-package's node_modules, not the * root's. * * @throws PluginLoaderError on config-import failure or a Node version * too old to satisfy {@link MIN_NODE_MAJOR}. */ export declare function loadPluginsFromConfigFile(configFilePath: string): Promise; /** * Import every config in `configs` and return a map keyed by each * config's directory. This is the worker-side entry point for the new * config-loads-in-worker flow: each worker calls this once at init, * caches the result for its entire lifetime, and per-file lint tasks * pick the right `LoadedPlugins` via `request.configKey === configDirectory`. * * Fail-fast on the first config import failure — same contract as the * old `loadPlugins(entries, baseUrl)` path. Surfacing a partial success * silently degrades lint quality across the workspace (files under the * failing config get no plugin rules), so we prefer a clean, loud * failure that the user can fix and retry. */ export declare function loadPluginsFromConfigs(configs: readonly ConfigDescriptor[]): Promise>; /** * Normalize native parser output to ESLint-shape ESTree in a single DFS: * 1. add `range` + `loc` to every node (parser offsets are UTF-16 code units); * 2. add `parent` references so rules can walk up the tree; * 3. apply the TS-specific shape fixes scope-manager / TS plugins expect. * * Mutates the AST in place; the parser returns a fresh tree per parse. */ declare interface LocPosition { line: number; column: number; } export declare interface PluginLintHost { /** * Run one reverse batch: build per-file tasks, lint, project results. An * optional AbortSignal cancels the dispatched worker tasks — the LSP path * wires it to a superseding keystroke / document close so the worker stops * instead of running to completion. */ lint(req: EslintPluginLintRequest, signal?: AbortSignal): Promise; /** Drain in-flight tasks and terminate the worker pool. Idempotent. */ shutdown(): Promise; } declare interface ReportDescriptor { node?: ESTreeNode; loc?: ReportLoc; message?: string; messageId?: string; data?: Record; fix?: (fixer: RuleFixer) => Fix | Fix[] | null | undefined | Iterable; suggest?: SuggestionInput[]; } /** * Modern descriptor form: `context.report({ node | loc, message | * messageId, data?, fix?, suggest? })`. This is the recommended form. * * For the legacy positional form ESLint still accepts — * `report(node, message, data?, fix?)` or * `report(node, loc, message, data?, fix?)` — see * `normalizeReportArgs` below. ESLint's * `lib/linter/file-report.js`'s `normalizeMultiArgReportCall` is our * reference implementation. */ /** * `loc` shape accepted on a report descriptor. Wider than the AST's * own {@link SourceLocation} on purpose: ESLint's `context.report({ loc })` * permits any of: * * - `{ line, column }` — a single position (treated as zero-width) * - `{ start, end }` — a full range * - `{ start }` — partial range; `end` defaults to `start` * * The third form is what real plugins write when reporting at a single * point but using the `{start: ...}` shape they already build for ranges * elsewhere. ESLint v9's `lib/linter/file-report.js` accepts it; rslint * matches. */ declare type ReportLoc = LocPosition | { start: LocPosition; end?: LocPosition; }; /** Per-rule configuration as it reaches the Worker. Already filtered to enabled rules; severity is reattached Go-side. */ export declare interface RuleConfig { /** Rule options — typically [optionsObject] or [], pre-schema-defaults. */ options: readonly unknown[]; /** * `rule.meta` is opaque to the runner except for: * - schema → applyOptionDefaults * - messages → context.report messageId lookup * - fixable → currently informational only; collectFixes gating is * done at the lintBatch level (internal/linter) */ meta?: { schema?: unknown; messages?: Record; fixable?: 'code' | 'whitespace'; hasSuggestions?: boolean; }; } /** * The shape of the context object passed to `rule.create(ctx)`. ESLint's * actual type has many more fields; we expose what real plugins read. * * Surface tracks **ESLint v10** exactly — empirically pinned against * `eslint@10.x` so any v10 plugin reads the same property set whether * it runs under ESLint or rslint. */ export declare interface RuleContext { id: string; options: readonly unknown[]; settings: Record; /** * The full forwarded `languageOptions` object. Always present (never * undefined) — when the user's config didn't set anything, nested * fields stay undefined but the wrapper itself is here so a rule's * `ctx.languageOptions.globals` access doesn't crash. */ languageOptions: LanguageOptions; filename: string; /** * Physical disk path of the file. For non-processor files this equals * `filename`. Distinct from `filename` only when ESLint applies a * processor that yields virtual sub-files; the runner doesn't run * processors today, so they're equal. */ physicalFilename: string; cwd: string; sourceCode: SourceCode; report(descriptor: ReportDescriptor): void; /** @internal */ _drainDiagnostics(): Diagnostic[]; } /** * The fixer object plugins call into. Eight methods, one record per call. * Stateless — each method returns a fresh Fix; the caller (RuleContext's * `report`) collects them into the diagnostic. */ declare interface RuleFixer { replaceText(node: HasRange, text: string): Fix; replaceTextRange(range: [number, number], text: string): Fix; insertTextBefore(node: HasRange, text: string): Fix; insertTextBeforeRange(range: [number, number], text: string): Fix; insertTextAfter(node: HasRange, text: string): Fix; insertTextAfterRange(range: [number, number], text: string): Fix; remove(node: HasRange): Fix; removeRange(range: [number, number]): Fix; } declare interface SourceCode { text: string; ast: ESTreeNode; isESTree: boolean; lines: string[]; hasBOM: boolean; scopeManager: unknown; /** * ESTree visitor keys map (`{ NodeType: [...childKeys] }`). v10 * exposes this so rules can do their own traversal without pulling * `eslint-visitor-keys`. rslint ships the standard ESTree key set; * the runner doesn't yet support custom parsers, so this is static. */ visitorKeys: Record; /** * Parser-supplied services. Empty `{}` in plain JS (matches v10). * Real ESLint would populate this with the TS parser's `program`, * `esTreeNodeToTSNodeMap`, etc.; the runner doesn't proxy ts-go * type info through, so plugin rules that need TS types should * guard via `if (!services.program) return {}`. */ parserServices: Record; getText(node?: ESTreeNode, beforeCount?: number, afterCount?: number): string; getLines(): string[]; getLocFromIndex(index: number): LocPosition; getIndexFromLoc(loc: LocPosition): number; getRange(node: ESTreeNode): [number, number]; getLoc(node: ESTreeNode): SourceLocation; getAncestors(node: ESTreeNode): ESTreeNode[]; getNodeByRangeIndex(index: number): ESTreeNode | null; getTokenBefore(node: ESTreeNode, opts?: TokenSkipOpts): Token | null; getTokenAfter(node: ESTreeNode, opts?: TokenSkipOpts): Token | null; getFirstToken(node: ESTreeNode, opts?: TokenSkipOpts): Token | null; getLastToken(node: ESTreeNode, opts?: TokenSkipOpts): Token | null; getTokens(node: ESTreeNode, opts?: TokenFilterOpts, afterCount?: number): Token[]; getTokensBetween(left: ESTreeNode, right: ESTreeNode, opts?: TokenFilterOpts): Token[]; getFirstTokenBetween(left: ESTreeNode, right: ESTreeNode, opts?: TokenSkipOpts): Token | null; getFirstTokensBetween(left: ESTreeNode, right: ESTreeNode, opts?: TokenCountOpts): Token[]; getLastTokenBetween(left: ESTreeNode, right: ESTreeNode, opts?: TokenSkipOpts): Token | null; getLastTokensBetween(left: ESTreeNode, right: ESTreeNode, opts?: TokenCountOpts): Token[]; getFirstTokens(node: ESTreeNode, opts?: TokenCountOpts): Token[]; getLastTokens(node: ESTreeNode, opts?: TokenCountOpts): Token[]; getTokensBefore(node: ESTreeNode, opts?: TokenCountOpts): Token[]; getTokensAfter(node: ESTreeNode, opts?: TokenCountOpts): Token[]; getTokenByRangeStart(start: number, opts?: { includeComments?: boolean; }): Token | null; getCommentsBefore(node: ESTreeNode): Comment[]; getCommentsAfter(node: ESTreeNode): Comment[]; getCommentsInside(node: ESTreeNode): Comment[]; getAllComments(): Comment[]; /** * All code tokens AND comments merged into one stream sorted by `range[0]` * — ESLint's `SourceCode#tokensAndComments`. Stylistic whitespace rules * (`comma-spacing`, `no-multi-spaces`, `indent`, `indent-binary-ops`, * `space-in-parens`, ...) read this array directly. */ readonly tokensAndComments: readonly Token[]; commentsExistBetween(left: ESTreeNode, right: ESTreeNode): boolean; isSpaceBetween(left: ESTreeNode, right: ESTreeNode): boolean; getScope(node?: ESTreeNode): unknown; getDeclaredVariables(node: ESTreeNode): unknown[]; markVariableAsUsed(name: string, node?: ESTreeNode): boolean; /** * ESLint v9 `sourceCode.isGlobalReference(node)`. Returns true iff * `node` is an Identifier referencing a variable that lives in the * global scope AND has no in-source definition. Widely used by * community rules (unicorn, ESLint built-ins). See impl for the * exact semantics. */ isGlobalReference(node: ESTreeNode): boolean; getInlineConfigNodes(): Comment[]; /** * ESLint v10's `sourceCode.getDisableDirectives()`. Returns the * parsed `eslint-disable*` / `eslint-enable` directives in the file * plus any parse problems (e.g. multi-line `eslint-disable-line`). * Plugin rules like `unicorn/no-abusive-eslint-disable` consume it * to report on directive shape. */ getDisableDirectives(): { problems: Array<{ ruleId: null; message: string; loc: { start: LocPosition; end: LocPosition; }; }>; directives: Array<{ type: 'disable' | 'enable' | 'disable-next-line' | 'disable-line'; node: Comment; value: string; justification: string; }>; }; } declare interface SourceLocation { start: LocPosition; end: LocPosition; } /** * Suggestion descriptor as returned to Go. `fixes` is null in `'off'` * mode (we record the descriptor but didn't run `fix(fixer)`); a * populated array in `'eager'` mode. */ export declare interface SuggestionDescriptor { messageId?: string; desc?: string; fixes: Fix[] | null; } declare interface SuggestionInput { messageId?: string; desc?: string; data?: Record; fix: (fixer: RuleFixer) => Fix | Fix[] | null | undefined | Iterable; } /** Mode controlling whether suggestion `fix(fixer)` is invoked at report time. */ export declare type SuggestionsMode = 'off' | 'eager'; declare interface Token { type: TokenType; value: string; range: [number, number]; loc: SourceLocation; /** * Only on `RegularExpression` tokens — espree's shape. Plugins (`eslint-plugin-regexp`, * core `no-invalid-regexp` / `prefer-regex-literals`) read `token.regex.{pattern,flags}`. */ regex?: { pattern: string; flags: string; }; } /** * Plural-token API options. Mirrors ESLint: * - `number` → maximum number of tokens to return * - `(t: Token) => boolean` → per-token predicate * - object → `{ count?, filter?, includeComments? }` * * `includeComments: true` interleaves comments with code tokens in * source order (the same order a single-pass lexer would emit them). * Plugins use this to inspect formatting (e.g. whether a comment * appears between two tokens). */ declare type TokenCountOpts = number | ((t: Token) => boolean) | { count?: number; filter?: (t: Token) => boolean; includeComments?: boolean; }; declare type TokenFilterOpts = { filter?: (t: Token) => boolean; includeComments?: boolean; } | ((t: Token) => boolean) | number; /** * Singular-token API options. Mirrors ESLint: * - `number` → `skip` count (return the (skip+1)-th matching token) * - `(t: Token) => boolean` → per-token predicate * - object → `{ skip?, filter?, includeComments? }` * * The `skip` semantics caught us in the audit: `getFirstToken(node, 2)` * does NOT mean "first 2 tokens" — it means "skip the first 2 matches * and return the 3rd". Plugin code that uses bare numbers here is * almost always doing skip; treating the number as anything else * silently picks the wrong token. (Verified against ESLint: * `getFirstToken('const x = 1;', 2)` returns `=`.) */ declare type TokenSkipOpts = number | ((t: Token) => boolean) | { skip?: number; filter?: (t: Token) => boolean; includeComments?: boolean; }; declare type TokenType = 'Identifier' | 'PrivateIdentifier' | 'Keyword' | 'Punctuator' | 'String' | 'Numeric' | 'RegularExpression' | 'Template' | 'Boolean' | 'Null' | 'JSXIdentifier' | 'JSXText'; export declare class WorkerPool { private readonly opts; private readonly cancelPool; private workers; private nextTaskId; private closed; /** Pool-level backlog. `kickQueue` moves entries to idle workers * one at a time. Each worker carries at most ONE inflight task — * the cap exists so per-task timeouts only measure actual * execution time, not "stuck behind 30 other tasks on the worker's * postMessage queue" wait time. Otherwise a 100-file batch on 8 * workers makes the last ~12 tasks per worker race a 30 s timer * before the worker even reaches them, terminating the worker and * marking everything else inflight on that worker as `worker_crashed`. */ private pendingQueue; /** * In-flight respawn promises. A crashed worker's `'exit'` handler * kicks off `spawnWorker(id)` asynchronously; `shutdown()` awaits * these so it doesn't return while a freshly-spawned replacement * thread is still booting (which would leave an orphan worker alive * past `await pool.shutdown()`). Each promise resolves only after * the replacement is either adopted or — when `closed` raced ahead * — fully terminated. */ private readonly respawns; constructor(opts: WorkerPoolOptions); /** * Spawn workers and wait until all report 'ready'. Rejects on the * first worker init failure — the entire pool is unusable if any * plugin fails to load (the user's config references rules from a * plugin that didn't import, so every subsequent lintBatch would * surface "rule not found"). * * `workerCount=0` is a no-op fast path used when there are zero * ESLint plugin entries: the Go side never reaches the dispatcher * (no rule has IsEslintPluginRule=true), so a real worker pool is * wasted overhead. We still go through the IPC handshake — this lets * the CLI keep ONE code path regardless of whether plugins are * configured. */ init(): Promise; /** * Dispatch tasks round-robin to workers; resolve as a per-task result array. * * @param onTaskDispatched optional callback invoked synchronously with * each task's internal taskId after the task has been fully tracked * (cancelSlot acquired, `inflight` populated) but BEFORE the task * is posted to the worker. Two use cases: * * 1. **ID bookkeeping** — callers that need a list of dispatched * ids (e.g. the VS Code extension host mapping LSP * `$/cancelRequest` reqId → taskId) collect them here. * * 2. **Cancel-before-start** — callers that have observed a * cancel signal mid-dispatch can call `cancelTask(taskId)` from * inside this callback. Because `inflight` is already populated, * the lookup succeeds and the SAB cancel flag is set BEFORE * postMessage delivers the task. The worker sees flag=1 on its * first poll and bails immediately without running any rule. * * The callback runs before any await, so the caller's bookkeeping * is guaranteed populated before any task can complete. */ lintBatch(tasks: LintTask[], onTaskDispatched?: (taskId: number) => void): Promise; /** * Move queued tasks onto idle workers. Each worker takes AT MOST ONE * task — the per-worker concurrency cap is what makes the per-task * timeout meaningful (it measures real execution time, not backlog * wait + execution). Called from: * * - `lintBatch` after enqueue * - the `result` message handler (`attachOngoingHandlers`) after a * task completes * - the `exit` handler after a respawn, so the replacement worker * can pick up the backlog * * Idempotent + cheap: bails on the first non-idle worker for each * loop pass and on an empty queue. * * Pre-cancelled entries (set by `cancelTask` while still queued) * are resolved here without ever being posted to a worker. */ private kickQueue; /** Cancel a task by taskId. Best-effort. * * - In-flight (already on a worker): set the SAB cancel flag — * worker bails at the next per-node visit. * - Queued (not yet dispatched): set `cancelled = true` so * `kickQueue` resolves it as cancelled and never posts to a * worker. * - Otherwise (already completed / never existed): no-op, returns * false. * * RACE NOTE: a `true` return does NOT guarantee the task's result is * suppressed. Cancellation is cooperative (polled per-node in * `listener-merge`), so a worker that has already finished its * traversal but whose result is still in flight will deliver that * result with `cancelled: false` and complete diagnostics — the flag * is no longer read. The returned result is itself correct/complete; * callers that need to drop a cancelled task's output must key off * each result's own `cancelled` field, not this method's return. */ cancelTask(taskId: number): boolean; /** Graceful shutdown — message all workers, wait for exit. */ shutdown(): Promise; /** * Spawn one worker and wait for its 'ready' (or 'init-error') message. * On init error or init-timeout, throws — caller (init) reports up. */ private spawnWorker; /** * Wire post-init handlers: result routing, log-forwarding, and crash recovery. */ private attachOngoingHandlers; /** * Last-chance drain for the "every worker exhausted its respawn * cap" terminal state. Iterates `pendingQueue`, releases each * cancel-slot, and resolves with `parseError: 'pool_degraded'` so * the host distinguishes this case from a normal `shutdown` / * `worker_crashed` per-task failure. Idempotent: if `pendingQueue` * is already empty, the loop is a no-op. */ private drainQueueIfAllSlotsDegraded; /** * Post one queued task to a specific (idle) worker slot. Caller is * `kickQueue`, which has already verified the slot is ready + * idle. taskId / cancelSlot were allocated at `lintBatch` enqueue * time, so cancellation works against queued AND inflight tasks. * * The per-task timeout starts HERE — when the worker actually * takes the task off the queue — not at enqueue time. That's the * whole point of the queue model: the previous design ran every * task's timer from `lintBatch` time, so the last task on each * worker (sitting in the worker's postMessage backlog) raced a * 30 s deadline before the worker even reached it. */ private dispatchToWorker; } export declare interface WorkerPoolOptions { /** * Each user rslint config file passed directly to the worker; the * worker imports each one once at init via * `loadPluginsFromConfigs`, caches the resulting `LoadedPlugins` per * `configDirectory`, and per-file lint tasks pick the right one via * the task's `configKey`. * * Empty array means "no plugin work" — the pool spawns no workers * (`workerCount=0` fast path) and `lintBatch` short-circuits to * empty per-file results. */ configs: ConfigDescriptor[]; /** Worker count. 1 honors --singleThreaded; default min(cpus, 8). */ workerCount?: number; /** Per-task soft deadline (ms). Default 30_000. */ taskTimeoutMs?: number; /** Worker init timeout (ms). Default 60_000. */ workerInitTimeoutMs?: number; /** Max worker respawns per crashed worker before giving up. Default 3. */ retryCap?: number; /** Hook called when a runner-side log is received (`console.*` from plugins / pool diagnostics). */ onLog?: (rec: { level: string; source: 'plugin' | 'runner'; text: string; }) => void; } export { }