@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.
- package/README.md +34 -49
- package/build/cli/tsc.d.ts +3 -2
- package/build/cli/tsc.js +15 -97
- package/build/{probe → guard}/adapter.d.ts +1 -2
- package/build/{probe → guard}/adapter.js +1 -4
- package/build/{probe → guard}/async/channel.d.ts +1 -1
- package/build/{probe → guard}/async/channel.js +11 -191
- package/build/{probe → guard}/async/value.d.ts +1 -2
- package/build/{probe → guard}/async/value.js +1 -4
- package/build/guard/channels.d.ts +4 -0
- package/build/guard/channels.js +13 -0
- package/build/{probe → guard}/exceptions/channel.d.ts +3 -2
- package/build/{probe → guard}/exceptions/channel.js +167 -107
- package/build/guard/exceptions/derive.d.ts +49 -0
- package/build/guard/exceptions/derive.js +478 -0
- package/build/guard/exceptions/resolve-js.d.ts +2 -0
- package/build/guard/exceptions/resolve-js.js +26 -0
- package/build/{probe → guard}/exceptions/value.d.ts +3 -2
- package/build/{probe → guard}/exceptions/value.js +12 -4
- package/build/{probe → guard}/kernel/analyze.d.ts +2 -1
- package/build/guard/kernel/analyze.js +84 -0
- package/build/guard/kernel/ast.d.ts +13 -0
- package/build/{probe → guard}/kernel/ast.js +22 -15
- package/build/guard/kernel/config.js +93 -0
- package/build/guard/kernel/fixpoint.d.ts +6 -0
- package/build/{probe → guard}/kernel/fixpoint.js +8 -48
- package/build/guard/kernel/graph.d.ts +4 -0
- package/build/guard/kernel/graph.js +268 -0
- package/build/{probe → guard}/kernel/ids.d.ts +2 -3
- package/build/{probe → guard}/kernel/ids.js +1 -6
- package/build/{probe → guard}/kernel/types.d.ts +3 -32
- package/build/{probe → guard}/overlay/base/async.jsonc +1 -1
- package/build/{probe → guard}/overlay/base/exceptions.jsonc +16 -3
- package/build/{probe → guard}/overlay/base/resources.jsonc +7 -8
- package/build/{probe → guard}/overlay/load.d.ts +3 -7
- package/build/{probe → guard}/overlay/load.js +37 -120
- package/build/{probe → guard}/resources/channel.d.ts +1 -1
- package/build/{probe → guard}/resources/channel.js +81 -159
- package/build/{probe → guard}/resources/value.d.ts +1 -2
- package/build/{probe → guard}/resources/value.js +1 -14
- package/build/lsp/diagnostics.d.ts +2 -2
- package/build/lsp/diagnostics.js +2 -2
- package/build/lsp/server.js +41 -3
- package/build/lsp/workspace.d.ts +6 -3
- package/build/lsp/workspace.js +19 -5
- package/build/tsconfig.d.ts +2 -1
- package/build/tsconfig.js +44 -102
- package/package.json +2 -2
- package/tsconfig.base.json +12 -19
- package/tsconfig.package.json +6 -1
- package/build/probe/channels.d.ts +0 -4
- package/build/probe/channels.js +0 -16
- package/build/probe/kernel/analyze.js +0 -68
- package/build/probe/kernel/ast.d.ts +0 -11
- package/build/probe/kernel/config.js +0 -209
- package/build/probe/kernel/fixpoint.d.ts +0 -6
- package/build/probe/kernel/graph.d.ts +0 -4
- package/build/probe/kernel/graph.js +0 -507
- package/build/probe/overlay/presets/express.jsonc +0 -25
- package/build/probe/overlay/presets/node.jsonc +0 -17
- /package/build/{probe → guard}/exceptions/jsdoc.d.ts +0 -0
- /package/build/{probe → guard}/exceptions/jsdoc.js +0 -0
- /package/build/{probe → guard}/kernel/config.d.ts +0 -0
- /package/build/{probe → guard}/kernel/format.d.ts +0 -0
- /package/build/{probe → guard}/kernel/format.js +0 -0
- /package/build/{probe → guard}/kernel/program.d.ts +0 -0
- /package/build/{probe → guard}/kernel/program.js +0 -0
- /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
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
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.
|
|
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
|
+
```
|
package/build/cli/tsc.d.ts
CHANGED
|
@@ -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 '../
|
|
13
|
-
import { formatDiagnostics } from '../
|
|
14
|
-
import { loadConfigFromTsconfig } from '../
|
|
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
|
-
|
|
285
|
+
process.exit(1);
|
|
286
286
|
}
|
|
287
287
|
if (!config) {
|
|
288
288
|
return;
|
|
289
289
|
}
|
|
290
|
+
let result;
|
|
290
291
|
try {
|
|
291
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
483
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
13
|
+
export { bottom, equals, promise, widen };
|
|
@@ -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 };
|