@esportsplus/typescript 0.32.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +34 -49
  2. package/build/cli/tsc.d.ts +3 -2
  3. package/build/cli/tsc.js +15 -97
  4. package/build/{probe → guard}/adapter.d.ts +1 -2
  5. package/build/{probe → guard}/adapter.js +1 -4
  6. package/build/{probe → guard}/async/channel.d.ts +1 -1
  7. package/build/{probe → guard}/async/channel.js +11 -191
  8. package/build/{probe → guard}/async/value.d.ts +1 -2
  9. package/build/{probe → guard}/async/value.js +1 -4
  10. package/build/guard/channels.d.ts +4 -0
  11. package/build/guard/channels.js +13 -0
  12. package/build/{probe → guard}/exceptions/channel.d.ts +3 -2
  13. package/build/{probe → guard}/exceptions/channel.js +167 -107
  14. package/build/guard/exceptions/derive.d.ts +49 -0
  15. package/build/guard/exceptions/derive.js +478 -0
  16. package/build/guard/exceptions/resolve-js.d.ts +2 -0
  17. package/build/guard/exceptions/resolve-js.js +26 -0
  18. package/build/{probe → guard}/exceptions/value.d.ts +3 -2
  19. package/build/{probe → guard}/exceptions/value.js +12 -4
  20. package/build/{probe → guard}/kernel/analyze.d.ts +2 -1
  21. package/build/guard/kernel/analyze.js +84 -0
  22. package/build/guard/kernel/ast.d.ts +13 -0
  23. package/build/{probe → guard}/kernel/ast.js +22 -15
  24. package/build/guard/kernel/config.js +93 -0
  25. package/build/guard/kernel/fixpoint.d.ts +6 -0
  26. package/build/{probe → guard}/kernel/fixpoint.js +8 -48
  27. package/build/guard/kernel/graph.d.ts +4 -0
  28. package/build/guard/kernel/graph.js +268 -0
  29. package/build/{probe → guard}/kernel/ids.d.ts +2 -3
  30. package/build/{probe → guard}/kernel/ids.js +1 -6
  31. package/build/{probe → guard}/kernel/types.d.ts +3 -32
  32. package/build/{probe → guard}/overlay/base/async.jsonc +1 -1
  33. package/build/{probe → guard}/overlay/base/exceptions.jsonc +16 -3
  34. package/build/{probe → guard}/overlay/base/resources.jsonc +7 -8
  35. package/build/{probe → guard}/overlay/load.d.ts +3 -7
  36. package/build/{probe → guard}/overlay/load.js +37 -120
  37. package/build/{probe → guard}/resources/channel.d.ts +1 -1
  38. package/build/{probe → guard}/resources/channel.js +81 -159
  39. package/build/{probe → guard}/resources/value.d.ts +1 -2
  40. package/build/{probe → guard}/resources/value.js +1 -14
  41. package/build/lsp/diagnostics.d.ts +2 -2
  42. package/build/lsp/diagnostics.js +2 -2
  43. package/build/lsp/server.js +41 -3
  44. package/build/lsp/workspace.d.ts +6 -3
  45. package/build/lsp/workspace.js +19 -5
  46. package/build/tsconfig.d.ts +2 -1
  47. package/build/tsconfig.js +44 -102
  48. package/package.json +2 -2
  49. package/tsconfig.base.json +12 -19
  50. package/tsconfig.package.json +6 -1
  51. package/build/probe/channels.d.ts +0 -4
  52. package/build/probe/channels.js +0 -16
  53. package/build/probe/kernel/analyze.js +0 -68
  54. package/build/probe/kernel/ast.d.ts +0 -11
  55. package/build/probe/kernel/config.js +0 -209
  56. package/build/probe/kernel/fixpoint.d.ts +0 -6
  57. package/build/probe/kernel/graph.d.ts +0 -4
  58. package/build/probe/kernel/graph.js +0 -507
  59. package/build/probe/overlay/presets/express.jsonc +0 -25
  60. package/build/probe/overlay/presets/node.jsonc +0 -17
  61. /package/build/{probe → guard}/exceptions/jsdoc.d.ts +0 -0
  62. /package/build/{probe → guard}/exceptions/jsdoc.js +0 -0
  63. /package/build/{probe → guard}/kernel/config.d.ts +0 -0
  64. /package/build/{probe → guard}/kernel/format.d.ts +0 -0
  65. /package/build/{probe → guard}/kernel/format.js +0 -0
  66. /package/build/{probe → guard}/kernel/program.d.ts +0 -0
  67. /package/build/{probe → guard}/kernel/program.js +0 -0
  68. /package/build/{probe → guard}/kernel/types.js +0 -0
package/README.md CHANGED
@@ -83,65 +83,38 @@ The CLI detects plugins in `tsconfig.json` `compilerOptions.plugins`, loads them
83
83
  - **`resources`** — acquired resources (timers, event listeners, file handles, sockets, `Disposable`s, …) that can leak: not released, transferred, or `using`-bound on every path — including throwing paths, which it derives from the `exceptions` channel's summaries.
84
84
  - **`async`** — unbounded `Promise.all(…)`-style fan-out, orphaned promises whose rejections go unhandled, and awaited cancellable calls that drop an `AbortSignal` the function holds.
85
85
 
86
- Only `exceptions` is on by default; enable the others per project. It rides the `tsc` passthrough (build/CI), and ships an LSP server so editors can render the same findings.
86
+ Only channels you configure run; a channel is otherwise off. It rides the `tsc` passthrough (build/CI), and ships an LSP server so editors can render the same findings.
87
87
 
88
88
  ### Enable
89
89
 
90
- Add an `analyze` entry to `compilerOptions.plugins`. Analysis covers every file the tsconfig includes (ignoring excludes); no config beyond the entry is required.
90
+ Add a top-level `tsc-guard` key to `tsconfig.json` (a sibling of `compilerOptions`, like `tsc-alias`). Analysis is always whole-project and covers every file the tsconfig includes (ignoring excludes). Its only root keys are the channel names — `exceptions`, `resources`, and `async` — with no `channels` wrapper. An absent channel is off; a present channel with no `severity` is `error`. Consequently, a bare `"tsc-guard": {}` analyzes nothing: the minimum useful config is the three-line per-channel shape below.
91
+
92
+ Unlike `compilerOptions.plugins` — which `extends` **replaces** wholesale — `tsc-guard` is **deep-merged down the `extends` chain**: a shared base can carry the defaults and a package override a single channel key (e.g. `tsc-guard.exceptions.report`) without restating the rest. Set an inherited key to `null` to delete it.
93
+
94
+ `enabled`, top-level `severity`/`failOnFindings`, `presets`, `overlays`, `sinks`, `handlerBoundaries`, `entryPoints`, and the `channels` wrapper are removed; leaving one in the config fails the build with a diagnostic naming its replacement. Platform effect models (Node/DOM globals, `node:fs`, and so on) are built in, so there is nothing to select. `dispatch` is recognised on every channel.
91
95
 
92
96
  ```jsonc
93
97
  {
94
- "compilerOptions": {
95
- "plugins": [
96
- {
97
- "name": "ts-probe",
98
- // Editor squiggle color; "warn" opts down. CLI/build ignore this.
99
- "severity": "error",
100
- // Fail the tsc/build run when there are findings.
101
- "failOnFindings": true,
102
- // Per-channel config. `enabled` and `dispatch` are recognised on
103
- // every channel; other keys are that channel's own options.
104
- "channels": {
105
- "exceptions": {
106
- "enabled": true,
107
- // "consumers" (default): uncaught calls only, and only when
108
- // the throwing callee is in another PACKAGE — a throw within
109
- // this package is its own contract, seen only where an
110
- // external consumer calls in.
111
- // "cross-module": like "consumers" but the boundary is the
112
- // FILE — a callee thrown from the caller's own file stays
113
- // silent; a call from another file reports.
114
- // "all": throws AND every uncaught call.
115
- "report": "consumers",
116
- // Flag `throw`s inside `catch` that drop the caught error's cause.
117
- "errorCause": true
118
- },
119
- "resources": {
120
- "enabled": true,
121
- // Untrackable handling when a resource escapes local analysis:
122
- // "optimist" assumes transfer (silent), "pessimist" reports it.
123
- "dispatch": "optimist",
124
- // Calls that take ownership of a passed resource argument.
125
- "ownership": [{ "callee": "registerCleanup", "params": [0] }]
126
- },
127
- "async": {
128
- "enabled": true,
129
- "fanOut": "warn", // "off" | "warn" | "error"
130
- "fanOutAllowLiteralUpTo": 16, // inline array/tuple size that is fine
131
- "poolFunctions": ["p-limit", "p-map"] // sanctioned concurrency wrappers
132
- }
133
- },
134
- // Model third-party throw behavior: "node", "express".
135
- "presets": ["node"]
136
- }
137
- ]
98
+ "compilerOptions": { /* … */ },
99
+ "tsc-guard": {
100
+ "exceptions": {
101
+ "severity": "error", // "error" reports + fails the build; "warn"/"info" report only; "off" skips
102
+ // "consumers": uncaught calls only, when the throwing callee is in another PACKAGE.
103
+ // "cross-module": like "consumers" but the boundary is the FILE.
104
+ // "all": throws AND every uncaught call.
105
+ "report": "cross-module" // "consumers" | "cross-module" | "all"
106
+ },
107
+ "resources": { "severity": "error" },
108
+ "async": { "severity": "warn" }
138
109
  }
139
110
  }
140
111
  ```
141
112
 
113
+ Each channel's `severity` is `"error"`, `"warn"`, `"info"`, or `"off"` — `error` reports and fails the build; `warn`/`info` report without gating (a `warn` squiggle vs a subtler `info` one in the editor); `off` skips the channel. The only remaining channel option is `exceptions.report` (`"consumers"` | `"cross-module"` | `"all"`, default `"consumers"`). Everything else is inferred from the code and unconditional: the exceptions channel traces third-party throws from the installed `.js` and flags the catch-`{ cause }` smell; the resources channel infers ownership by tracing releases/transfers and reports any resource that escapes somewhere untrackable; the async channel flags orphaned promises, dropped `AbortSignal`s, and `Promise.all`/`race`/… over a **dynamically-sized** input (a statically-sized array literal or fixed tuple is always fine).
114
+
142
115
  ### CLI / build
143
116
 
144
- The `tsc` passthrough runs analyze after a successful compile and prints findings to stderr. With `failOnFindings: true`, a run with findings exits non-zero drop it into CI as a gate.
117
+ The `tsc` passthrough runs analyze after a successful compile and prints findings to stderr. It exits non-zero (`1`) when any finding is in a channel whose `severity` is `"error"`; `warn` channels never fail the build. A rejected or removed config key is a hard error and also exits `1`.
145
118
 
146
119
  ```bash
147
120
  tsc # compiles, resolves aliases, then reports analyze findings
@@ -149,7 +122,12 @@ tsc # compiles, resolves aliases, then reports analyze findings
149
122
 
150
123
  ### Editor (LSP)
151
124
 
152
- The package ships a standalone language server (`esportsplus-tsc-lsp` bin, or the `@esportsplus/typescript/lsp` export) that publishes analyze findings over LSP. A client spawns it beside the native TypeScript server and merges both diagnostic streams; `severity` drives the squiggle color. Analysis runs against saved files on open and save. Beyond diagnostics it serves **hovers** (the finding plus its origin→boundary chain) and **quick-fixes** — `void`/`await` an orphaned promise, forward an `AbortSignal`, or fix a leaked handle by converting it to `using` or wrapping the region in `try/finally`.
125
+ The package ships a standalone language server (`esportsplus-tsc-lsp` bin, or the `@esportsplus/typescript/lsp` export) that publishes analyze findings over LSP. It runs as a *second* server **beside** the TypeScript language server — it is not a tsserver plugin — and the editor merges both diagnostic streams; each finding's squiggle color comes from its own channel's `severity`. Analysis runs against saved files on open and save. Beyond diagnostics it serves **hovers** (the finding plus its origin→boundary chain) and **quick-fixes** — `void`/`await` an orphaned promise, forward an `AbortSignal`, or fix a leaked handle by converting it to `using` or wrapping the region in `try/finally`.
126
+
127
+ Wiring it up:
128
+
129
+ - **Config-driven LSP editors** (Neovim, Emacs, Helix, Zed, Sublime LSP): register `esportsplus-tsc-lsp` as an additional server for `typescript`/`typescriptreact`. It attaches to the same buffers as the TypeScript server; no tsconfig change.
130
+ - **VS Code**: install the client extension under [`editor/vscode`](editor/vscode) — it resolves and spawns the server from the workspace's own install.
153
131
 
154
132
  ```typescript
155
133
  import { startServer } from '@esportsplus/typescript/lsp';
@@ -157,6 +135,13 @@ import { startServer } from '@esportsplus/typescript/lsp';
157
135
  startServer(); // stdio LSP server
158
136
  ```
159
137
 
138
+ ### Accepted false negatives
139
+
140
+ - A library function that returns a resource the caller must release (for example, `db.connect()` → `.close()`) is not modeled unless it is in the built-in table.
141
+ - Third-party throws are derived by syntactically reading installed `.js`; cross-file/transitive throws beyond a small depth, dynamically dispatched calls, and non-standard export shapes are missed (optimistically, without false positives).
142
+ - Backing `.js` files that are minified or larger than 512 KiB are skipped.
143
+ - A bare `"tsc-guard": {}` is inert.
144
+
160
145
  ## API
161
146
 
162
147
  ### `@esportsplus/typescript`
@@ -242,4 +227,4 @@ This package targets `typescript@^7`, shipped as a regular `dependencies` entry.
242
227
  pnpm build # tsc && tsc-alias
243
228
  pnpm test # vitest run
244
229
  pnpm bench:run # vitest bench --run
245
- ```
230
+ ```
@@ -1,5 +1,5 @@
1
1
  import type { Plugin } from '../compiler/types.js';
2
- import { API } from 'typescript/unstable/sync';
2
+ import { API, type Checker, type Program } from 'typescript/unstable/sync';
3
3
  import { stripJsonc } from '../jsonc.js';
4
4
  type PluginConfig = {
5
5
  transform: string;
@@ -15,6 +15,7 @@ declare function loadPlugins(configs: PluginConfig[], root: string): Promise<Plu
15
15
  declare function main(): void;
16
16
  declare function normalizePath(fileName: string): string;
17
17
  declare function projectPath(args: string[]): string | null;
18
+ declare function runAnalyze(tsconfig: string, program?: Program, checker?: Checker): void;
18
19
  declare function resolvePluginConfigs(tsconfig: string): PluginConfig[];
19
20
  declare function runTscAlias(args: string[]): Promise<number>;
20
- export { build, classifyFlags, isPlugin, loadPlugins, main, normalizePath, projectPath, resolvePluginConfigs, runTscAlias, stripJsonc };
21
+ export { build, classifyFlags, isPlugin, loadPlugins, main, normalizePath, projectPath, resolvePluginConfigs, runAnalyze, runTscAlias, stripJsonc };
package/build/cli/tsc.js CHANGED
@@ -9,9 +9,9 @@ import fs from 'fs';
9
9
  import languageService from '../compiler/language-service.js';
10
10
  import path from 'path';
11
11
  import sourcemap from '../compiler/sourcemap.js';
12
- import { analyze, analyzeProgram } from '../probe/kernel/analyze.js';
13
- import { formatDiagnostics } from '../probe/kernel/format.js';
14
- import { loadConfigFromTsconfig } from '../probe/kernel/config.js';
12
+ import { analyze, analyzeProgram } from '../guard/kernel/analyze.js';
13
+ import { formatDiagnostics } from '../guard/kernel/format.js';
14
+ import { loadConfigFromTsconfig } from '../guard/kernel/config.js';
15
15
  import { stripJsonc } from '../jsonc.js';
16
16
  import { readPlugins } from '../tsconfig.js';
17
17
  const BACKSLASH_REGEX = /\\/g;
@@ -282,22 +282,25 @@ function runAnalyze(tsconfig, program, checker) {
282
282
  }
283
283
  catch (error) {
284
284
  console.error(`${PACKAGE_NAME}: analyze config error: ${error instanceof Error ? error.message : String(error)}`);
285
- return;
285
+ process.exit(1);
286
286
  }
287
287
  if (!config) {
288
288
  return;
289
289
  }
290
+ let result;
290
291
  try {
291
- let result = program && checker ? analyzeProgram(program, checker, config) : analyze(config);
292
- if (result.diagnostics.length > 0) {
293
- console.error(formatDiagnostics(result.diagnostics, config.projectRoot));
294
- if (config.failOnFindings) {
295
- process.exit(1);
296
- }
297
- }
292
+ result = program && checker ? analyzeProgram(program, checker, config) : analyze(config);
298
293
  }
299
294
  catch (error) {
300
295
  console.error(`${PACKAGE_NAME}: analyze failed: ${error instanceof Error ? error.message : String(error)}`);
296
+ return;
297
+ }
298
+ if (result.diagnostics.length === 0) {
299
+ return;
300
+ }
301
+ console.error(formatDiagnostics(result.diagnostics, config.projectRoot));
302
+ if (result.diagnostics.some((diagnostic) => config.channels[diagnostic.channel]?.severity === 'error')) {
303
+ process.exit(1);
301
304
  }
302
305
  }
303
306
  function passthrough() {
@@ -334,91 +337,6 @@ function spawnTsc(tscJs, args) {
334
337
  child.on('exit', (code) => resolve(code ?? 0));
335
338
  });
336
339
  }
337
- function legacyStripJsonc(text) {
338
- let escaped = false, inBlockComment = false, inLineComment = false, inString = false, stripped = '';
339
- for (let i = 0, n = text.length; i < n; i++) {
340
- let char = text[i], next = text[i + 1];
341
- if (inLineComment) {
342
- if (char === '\n') {
343
- inLineComment = false;
344
- stripped += char;
345
- }
346
- continue;
347
- }
348
- if (inBlockComment) {
349
- if (char === '*' && next === '/') {
350
- inBlockComment = false;
351
- i++;
352
- }
353
- continue;
354
- }
355
- if (inString) {
356
- stripped += char;
357
- if (escaped) {
358
- escaped = false;
359
- }
360
- else if (char === '\\') {
361
- escaped = true;
362
- }
363
- else if (char === '"') {
364
- inString = false;
365
- }
366
- continue;
367
- }
368
- if (char === '"') {
369
- inString = true;
370
- stripped += char;
371
- continue;
372
- }
373
- if (char === '/' && next === '/') {
374
- inLineComment = true;
375
- i++;
376
- continue;
377
- }
378
- if (char === '/' && next === '*') {
379
- inBlockComment = true;
380
- i++;
381
- continue;
382
- }
383
- stripped += char;
384
- }
385
- escaped = false;
386
- inString = false;
387
- let result = '';
388
- for (let i = 0, n = stripped.length; i < n; i++) {
389
- let char = stripped[i];
390
- if (inString) {
391
- result += char;
392
- if (escaped) {
393
- escaped = false;
394
- }
395
- else if (char === '\\') {
396
- escaped = true;
397
- }
398
- else if (char === '"') {
399
- inString = false;
400
- }
401
- continue;
402
- }
403
- if (char === '"') {
404
- inString = true;
405
- result += char;
406
- continue;
407
- }
408
- if (char === ',') {
409
- let j = i + 1;
410
- while (j < n && (stripped[j] === ' ' || stripped[j] === '\n' || stripped[j] === '\r' || stripped[j] === '\t')) {
411
- j++;
412
- }
413
- if (stripped[j] === ']' || stripped[j] === '}') {
414
- continue;
415
- }
416
- }
417
- result += char;
418
- }
419
- return result;
420
- }
421
- void legacyStripJsonc;
422
340
  function teardown(snapshot, configPath, owned) {
423
341
  if (owned) {
424
342
  languageService.dispose(configPath);
@@ -431,4 +349,4 @@ function teardown(snapshot, configPath, owned) {
431
349
  if (process.env.VITEST === undefined) {
432
350
  main();
433
351
  }
434
- export { build, classifyFlags, isPlugin, loadPlugins, main, normalizePath, projectPath, resolvePluginConfigs, runTscAlias, stripJsonc };
352
+ export { build, classifyFlags, isPlugin, loadPlugins, main, normalizePath, projectPath, resolvePluginConfigs, runAnalyze, runTscAlias, stripJsonc };
@@ -9,6 +9,5 @@ declare function forEachChild<T>(node: Node, cb: (child: Node) => T | undefined)
9
9
  declare function isClassLike(node: Node): boolean;
10
10
  declare function getSourceFiles(program: Program): SourceFile[];
11
11
  declare function symbolDeclarations(symbol: Symbol): Node[];
12
- declare function symbolValueDeclaration(symbol: Symbol): Node | undefined;
13
12
  declare function unionTypes(type: Type): readonly Type[] | undefined;
14
- export { forEachChild, getSourceFiles, isClassLike, symbolDeclarations, symbolValueDeclaration, unionTypes };
13
+ export { forEachChild, getSourceFiles, isClassLike, symbolDeclarations, unionTypes };
@@ -31,12 +31,9 @@ function symbolDeclarations(symbol) {
31
31
  }
32
32
  return out;
33
33
  }
34
- function symbolValueDeclaration(symbol) {
35
- return symbol.valueDeclaration?.resolve();
36
- }
37
34
  function unionTypes(type) {
38
35
  return (type.flags & TypeFlags.Union) !== 0
39
36
  ? type.getTypes()
40
37
  : undefined;
41
38
  }
42
- export { forEachChild, getSourceFiles, isClassLike, symbolDeclarations, symbolValueDeclaration, unionTypes };
39
+ export { forEachChild, getSourceFiles, isClassLike, symbolDeclarations, unionTypes };
@@ -1,4 +1,4 @@
1
1
  import type { Channel } from '../kernel/types.js';
2
2
  import { type AsyncValue } from './value.js';
3
- declare function createAsyncChannel(channelConfig: unknown): Channel<AsyncValue>;
3
+ declare function createAsyncChannel(): Channel<AsyncValue>;
4
4
  export { createAsyncChannel };
@@ -1,48 +1,10 @@
1
1
  import * as ts from '../adapter.js';
2
- import { bodyOf, calleeSelectors, unwrap } from '../kernel/ast.js';
2
+ import { bodyOf, constituents, hasModifier, isAwaited, unwrap } from '../kernel/ast.js';
3
3
  import { isFunctionLike, locationOf } from '../kernel/ids.js';
4
4
  import { bottom, equals, promise, widen } from './value.js';
5
5
  const AGGREGATORS = new Set(['all', 'allSettled', 'race', 'any']);
6
6
  const CHAIN_METHODS = new Set(['catch', 'finally', 'then']);
7
- const OPAQUE_SINKS = new Set(['add', 'push', 'set', 'unshift']);
8
7
  const UNBOUNDED_TUPLE_FLAGS = 12;
9
- function fail(message) {
10
- throw new Error(`async: ${message}`);
11
- }
12
- function parseOptions(raw) {
13
- const obj = typeof raw === 'object' && raw !== null
14
- ? raw
15
- : {};
16
- let fanOut = 'warn';
17
- if (obj['fanOut'] !== undefined) {
18
- if (obj['fanOut'] !== 'off' &&
19
- obj['fanOut'] !== 'warn' &&
20
- obj['fanOut'] !== 'error') {
21
- fail(`"fanOut" must be "off", "warn", or "error"`);
22
- }
23
- fanOut = obj['fanOut'];
24
- }
25
- let allow = 16;
26
- if (obj['fanOutAllowLiteralUpTo'] !== undefined) {
27
- const n = obj['fanOutAllowLiteralUpTo'];
28
- if (typeof n !== 'number' || !Number.isInteger(n) || n < 0) {
29
- fail(`"fanOutAllowLiteralUpTo" must be a non-negative integer`);
30
- }
31
- allow = n;
32
- }
33
- let poolFunctions = [];
34
- if (obj['poolFunctions'] !== undefined) {
35
- const p = obj['poolFunctions'];
36
- if (!Array.isArray(p) || p.some((v) => typeof v !== 'string')) {
37
- fail(`"poolFunctions" must be an array of strings`);
38
- }
39
- poolFunctions = p;
40
- }
41
- return { fanOut, fanOutAllowLiteralUpTo: allow, poolFunctions };
42
- }
43
- function constituents(type) {
44
- return ts.unionTypes(type) ?? [type];
45
- }
46
8
  function isPromiseType(checker, type) {
47
9
  if (!type) {
48
10
  return false;
@@ -59,22 +21,18 @@ function isPromiseType(checker, type) {
59
21
  }
60
22
  return false;
61
23
  }
62
- function hasAsyncModifier(node) {
63
- const mods = node.modifiers;
64
- return mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
65
- }
66
24
  function enclosingAsync(node) {
67
25
  let p = node.parent;
68
26
  while (p) {
69
27
  if (isFunctionLike(p)) {
70
- return hasAsyncModifier(p);
28
+ return hasModifier(p, ts.SyntaxKind.AsyncKeyword);
71
29
  }
72
30
  p = p.parent;
73
31
  }
74
32
  return false;
75
33
  }
76
34
  function returnsPromise(checker, node) {
77
- if (hasAsyncModifier(node)) {
35
+ if (hasModifier(node, ts.SyntaxKind.AsyncKeyword)) {
78
36
  return true;
79
37
  }
80
38
  const fnType = checker.getTypeAtLocation(node);
@@ -106,9 +64,6 @@ function aggregatorOf(call) {
106
64
  }
107
65
  return callee.name.text;
108
66
  }
109
- function isAggregatorCall(node) {
110
- return ts.isCallExpression(node) && aggregatorOf(node) !== undefined;
111
- }
112
67
  function chainRoot(call) {
113
68
  let cur = call;
114
69
  while (ts.isPropertyAccessExpression(cur.expression) &&
@@ -174,130 +129,28 @@ function isValueTransparent(node, child) {
174
129
  }
175
130
  return false;
176
131
  }
177
- function classifyOwnership(node) {
132
+ function isOrphan(node) {
178
133
  let child = node;
179
134
  let parent = node.parent;
180
135
  while (parent && isValueTransparent(parent, child)) {
181
136
  child = parent;
182
137
  parent = parent.parent;
183
138
  }
184
- if (!parent) {
185
- return 'opaque';
186
- }
187
- if (ts.isAwaitExpression(parent) || ts.isVoidExpression(parent)) {
188
- return 'owned';
189
- }
190
- if (ts.isReturnStatement(parent) || ts.isYieldExpression(parent)) {
191
- return 'owned';
192
- }
193
- if (ts.isArrowFunction(parent) && parent.body === child) {
194
- return 'owned';
195
- }
196
- if (ts.isVariableDeclaration(parent) && parent.initializer === child) {
197
- return 'owned';
198
- }
199
- if (ts.isPropertyDeclaration(parent) && parent.initializer === child) {
200
- return 'owned';
201
- }
202
- if (ts.isBinaryExpression(parent) &&
203
- parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
204
- parent.right === child) {
205
- return 'owned';
206
- }
207
- if (ts.isExpressionStatement(parent)) {
208
- return 'orphan';
209
- }
210
- if (ts.isCallExpression(parent) || ts.isNewExpression(parent)) {
211
- if (isAggregatorCall(parent)) {
212
- return 'owned';
213
- }
214
- const callee = parent.expression;
215
- if (ts.isPropertyAccessExpression(callee) &&
216
- OPAQUE_SINKS.has(callee.name.text)) {
217
- return 'opaque';
218
- }
219
- return 'owned';
220
- }
221
- if (ts.isArrayLiteralExpression(parent)) {
222
- const gp = parent.parent;
223
- if (gp && isAggregatorCall(gp)) {
224
- return 'owned';
225
- }
226
- return 'opaque';
227
- }
228
- return 'opaque';
139
+ return parent !== undefined && ts.isExpressionStatement(parent);
229
140
  }
230
141
  function isBoundedInput(env, arg) {
231
142
  const e = unwrap(arg);
232
143
  if (ts.isArrayLiteralExpression(e)) {
233
- if (e.elements.some((el) => ts.isSpreadElement(el))) {
234
- return false;
235
- }
236
- return e.elements.length <= env.options.fanOutAllowLiteralUpTo;
144
+ return !e.elements.some((el) => ts.isSpreadElement(el));
237
145
  }
238
146
  const type = env.checker.getTypeAtLocation(e);
239
147
  if (type && type.isTupleType()) {
240
148
  const flags = type.elementFlags ??
241
149
  [];
242
- if (flags.some((f) => (f & UNBOUNDED_TUPLE_FLAGS) !== 0)) {
243
- return false;
244
- }
245
- const len = type.fixedLength;
246
- return (typeof len === 'number' && len <= env.options.fanOutAllowLiteralUpTo);
247
- }
248
- return false;
249
- }
250
- function moduleMatches(env, call, pkg) {
251
- let sym = env.checker.getSymbolAtLocation(call.expression);
252
- if (sym && sym.flags & ts.SymbolFlags.Alias) {
253
- sym = env.checker.getAliasedSymbol(sym);
254
- }
255
- if (!sym) {
256
- return false;
257
- }
258
- for (const decl of ts.symbolDeclarations(sym)) {
259
- const path = decl.getSourceFile().fileName.replace(/\\/g, '/');
260
- if (path.includes(`node_modules/${pkg}/`)) {
261
- return true;
262
- }
150
+ return !flags.some((f) => (f & UNBOUNDED_TUPLE_FLAGS) !== 0);
263
151
  }
264
152
  return false;
265
153
  }
266
- function poolMatches(env, call) {
267
- const names = calleeSelectors(call.expression);
268
- for (const sel of env.options.poolFunctions) {
269
- const hash = sel.indexOf('#');
270
- if (hash >= 0) {
271
- const obj = sel.slice(0, hash);
272
- const method = sel.slice(hash + 1);
273
- if (names.has(method) ||
274
- names.has(`${obj}.${method}`) ||
275
- names.has(`${obj}#${method}`)) {
276
- return true;
277
- }
278
- continue;
279
- }
280
- if (names.has(sel) || moduleMatches(env, call, sel)) {
281
- return true;
282
- }
283
- }
284
- return false;
285
- }
286
- function routedThroughPool(env, arg) {
287
- let found = false;
288
- const visit = (n) => {
289
- if (found) {
290
- return;
291
- }
292
- if (ts.isCallExpression(n) && poolMatches(env, n)) {
293
- found = true;
294
- return;
295
- }
296
- ts.forEachChild(n, visit);
297
- };
298
- visit(arg);
299
- return found;
300
- }
301
154
  function isAbortSignalType(type) {
302
155
  if (!type) {
303
156
  return false;
@@ -346,19 +199,6 @@ function callRequiresSignal(env, call) {
346
199
  .resolveCall(call)
347
200
  .targets.some((t) => env.heldSignalNames(t).size > 0);
348
201
  }
349
- function isAwaited(call) {
350
- let child = call;
351
- let parent = call.parent;
352
- while (parent &&
353
- (ts.isParenthesizedExpression(parent) ||
354
- ts.isAsExpression(parent) ||
355
- ts.isNonNullExpression(parent)) &&
356
- parent.expression === child) {
357
- child = parent;
358
- parent = parent.parent;
359
- }
360
- return parent !== undefined && ts.isAwaitExpression(parent);
361
- }
362
202
  function forwardsSignal(call, held) {
363
203
  let found = false;
364
204
  const visit = (n) => {
@@ -404,14 +244,6 @@ function orphanDiagnostic(call) {
404
244
  fixes: orphanFixes(call),
405
245
  };
406
246
  }
407
- function degradedDiagnostic(call) {
408
- return {
409
- channel: 'async',
410
- message: `result of \`${calleeText(call)}()\` flows into an untracked structure — ownership cannot be verified`,
411
- location: locationOf(call, call.getSourceFile()),
412
- related: [],
413
- };
414
- }
415
247
  function fanOutDiagnostic(call, aggregator) {
416
248
  return {
417
249
  channel: 'async',
@@ -462,15 +294,12 @@ function cancellationDiagnostic(call, fixes) {
462
294
  };
463
295
  }
464
296
  function checkFanOut(env, call, out) {
465
- if (env.options.fanOut === 'off') {
466
- return;
467
- }
468
297
  const aggregator = aggregatorOf(call);
469
298
  if (!aggregator) {
470
299
  return;
471
300
  }
472
301
  const arg = call.arguments[0];
473
- if (!arg || isBoundedInput(env, arg) || routedThroughPool(env, arg)) {
302
+ if (!arg || isBoundedInput(env, arg)) {
474
303
  return;
475
304
  }
476
305
  out.push(fanOutDiagnostic(call, aggregator));
@@ -479,14 +308,8 @@ function checkOwnership(env, call, out) {
479
308
  if (isChainContinuation(call) || !producesPromise(env, call)) {
480
309
  return;
481
310
  }
482
- const ownership = classifyOwnership(call);
483
- if (ownership === 'orphan') {
484
- if (!chainHandlesRejection(call)) {
485
- out.push(orphanDiagnostic(call));
486
- }
487
- }
488
- else if (ownership === 'opaque' && env.dispatch === 'pessimist') {
489
- out.push(degradedDiagnostic(call));
311
+ if (isOrphan(call) && !chainHandlesRejection(call)) {
312
+ out.push(orphanDiagnostic(call));
490
313
  }
491
314
  }
492
315
  function checkCancellation(env, call, out) {
@@ -515,8 +338,7 @@ function walk(env, body, out) {
515
338
  };
516
339
  ts.forEachChild(body, visit);
517
340
  }
518
- function createAsyncChannel(channelConfig) {
519
- const options = parseOptions(channelConfig);
341
+ function createAsyncChannel() {
520
342
  const heldByFunction = new Map();
521
343
  const getHeldSignalNames = (checker, fn) => {
522
344
  let held = heldByFunction.get(fn);
@@ -544,8 +366,6 @@ function createAsyncChannel(channelConfig) {
544
366
  }
545
367
  const env = {
546
368
  checker: ctx.checker,
547
- dispatch: ctx.dispatch,
548
- options,
549
369
  summaryOf: ctx.summaryOf,
550
370
  resolveCall: ctx.resolveCall,
551
371
  heldSignalNames: (fn) => getHeldSignalNames(ctx.checker, fn),
@@ -3,7 +3,6 @@ type AsyncValue = {
3
3
  };
4
4
  declare function bottom(): AsyncValue;
5
5
  declare function promise(): AsyncValue;
6
- declare function join(a: AsyncValue, b: AsyncValue): AsyncValue;
7
6
  declare function equals(a: AsyncValue, b: AsyncValue): boolean;
8
7
  declare function widen(_prev: AsyncValue, next: AsyncValue, _round: number): AsyncValue;
9
- export { bottom, equals, join, promise, type AsyncValue, widen };
8
+ export { bottom, equals, promise, type AsyncValue, widen };
@@ -4,13 +4,10 @@ function bottom() {
4
4
  function promise() {
5
5
  return { returnsPromise: true };
6
6
  }
7
- function join(a, b) {
8
- return { returnsPromise: a.returnsPromise || b.returnsPromise };
9
- }
10
7
  function equals(a, b) {
11
8
  return a.returnsPromise === b.returnsPromise;
12
9
  }
13
10
  function widen(_prev, next, _round) {
14
11
  return next;
15
12
  }
16
- export { bottom, equals, join, promise, widen };
13
+ export { bottom, equals, promise, widen };
@@ -0,0 +1,4 @@
1
+ import type { Channel } from './kernel/types.js';
2
+ import type { ThrowIndex } from './exceptions/derive.js';
3
+ declare function channelFor(name: string, throwIndex: ThrowIndex): Channel<unknown> | undefined;
4
+ export { channelFor };
@@ -0,0 +1,13 @@
1
+ import { createAsyncChannel } from './async/channel.js';
2
+ import { createExceptionsChannel } from './exceptions/channel.js';
3
+ import { createResourcesChannel } from './resources/channel.js';
4
+ const FACTORIES = {
5
+ async: () => createAsyncChannel(),
6
+ exceptions: (throwIndex) => createExceptionsChannel(throwIndex),
7
+ resources: () => createResourcesChannel(),
8
+ };
9
+ function channelFor(name, throwIndex) {
10
+ const factory = FACTORIES[name];
11
+ return factory ? factory(throwIndex) : undefined;
12
+ }
13
+ export { channelFor };