@esportsplus/typescript 0.29.6 → 0.31.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 +97 -3
- package/bin/tsc-alias +1 -1
- package/bin/tsc-lsp +3 -0
- package/build/cli/diagnostics.d.ts +8 -0
- package/build/cli/diagnostics.js +95 -0
- package/build/cli/tsc.d.ts +12 -2
- package/build/cli/tsc.js +343 -52
- package/build/compiler/ast.d.ts +7 -5
- package/build/compiler/ast.js +6 -6
- package/build/compiler/code.d.ts +5 -4
- package/build/compiler/code.js +12 -1
- package/build/compiler/coordinator.d.ts +16 -7
- package/build/compiler/coordinator.js +75 -41
- package/build/compiler/imports.d.ts +8 -3
- package/build/compiler/imports.js +34 -24
- package/build/compiler/index.d.ts +3 -0
- package/build/compiler/index.js +1 -0
- package/build/compiler/language-service.d.ts +36 -6
- package/build/compiler/language-service.js +252 -58
- package/build/compiler/plugins/index.d.ts +4 -2
- package/build/compiler/plugins/tsc.d.ts +1 -1
- package/build/compiler/plugins/vite.d.ts +6 -3
- package/build/compiler/plugins/vite.js +15 -8
- package/build/compiler/sourcemap.d.ts +52 -0
- package/build/compiler/sourcemap.js +265 -0
- package/build/compiler/types.d.ts +7 -6
- package/build/compiler/uid.d.ts +5 -2
- package/build/compiler/uid.js +33 -4
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/jsonc.d.ts +2 -0
- package/build/jsonc.js +85 -0
- package/build/lsp/bin.d.ts +1 -0
- package/build/lsp/bin.js +2 -0
- package/build/lsp/diagnostics.d.ts +10 -0
- package/build/lsp/diagnostics.js +69 -0
- package/build/lsp/index.d.ts +2 -0
- package/build/lsp/index.js +2 -0
- package/build/lsp/server.d.ts +4 -0
- package/build/lsp/server.js +130 -0
- package/build/lsp/workspace.d.ts +14 -0
- package/build/lsp/workspace.js +46 -0
- package/build/probe/adapter.d.ts +14 -0
- package/build/probe/adapter.js +42 -0
- package/build/probe/async/channel.d.ts +4 -0
- package/build/probe/async/channel.js +560 -0
- package/build/probe/async/value.d.ts +9 -0
- package/build/probe/async/value.js +16 -0
- package/build/probe/channels.d.ts +4 -0
- package/build/probe/channels.js +16 -0
- package/build/probe/exceptions/channel.d.ts +5 -0
- package/build/probe/exceptions/channel.js +657 -0
- package/build/probe/exceptions/jsdoc.d.ts +8 -0
- package/build/probe/exceptions/jsdoc.js +34 -0
- package/build/probe/exceptions/value.d.ts +39 -0
- package/build/probe/exceptions/value.js +158 -0
- package/build/probe/kernel/analyze.d.ts +8 -0
- package/build/probe/kernel/analyze.js +68 -0
- package/build/probe/kernel/ast.d.ts +11 -0
- package/build/probe/kernel/ast.js +49 -0
- package/build/probe/kernel/config.d.ts +5 -0
- package/build/probe/kernel/config.js +209 -0
- package/build/probe/kernel/fixpoint.d.ts +6 -0
- package/build/probe/kernel/fixpoint.js +206 -0
- package/build/probe/kernel/format.d.ts +4 -0
- package/build/probe/kernel/format.js +32 -0
- package/build/probe/kernel/graph.d.ts +4 -0
- package/build/probe/kernel/graph.js +507 -0
- package/build/probe/kernel/ids.d.ts +8 -0
- package/build/probe/kernel/ids.js +66 -0
- package/build/probe/kernel/program.d.ts +8 -0
- package/build/probe/kernel/program.js +13 -0
- package/build/probe/kernel/types.d.ts +134 -0
- package/build/probe/kernel/types.js +1 -0
- package/build/probe/overlay/base/async.jsonc +13 -0
- package/build/probe/overlay/base/exceptions.jsonc +54 -0
- package/build/probe/overlay/base/resources.jsonc +57 -0
- package/build/probe/overlay/load.d.ts +18 -0
- package/build/probe/overlay/load.js +302 -0
- package/build/probe/overlay/presets/express.jsonc +25 -0
- package/build/probe/overlay/presets/node.jsonc +17 -0
- package/build/probe/resources/channel.d.ts +4 -0
- package/build/probe/resources/channel.js +798 -0
- package/build/probe/resources/value.d.ts +9 -0
- package/build/probe/resources/value.js +36 -0
- package/build/ts.d.ts +3 -0
- package/build/ts.js +3 -0
- package/build/tsconfig.d.ts +2 -0
- package/build/tsconfig.js +150 -0
- package/package.json +29 -9
- package/tsconfig.base.json +19 -0
- package/tsconfig.dev.json +13 -0
- package/tsconfig.package.json +4 -1
- package/.claude/skills/code-audit/registry-typescript.json +0 -326
- package/.editorconfig +0 -9
- package/.gitattributes +0 -2
- package/.github/dependabot.yml +0 -25
- package/.github/workflows/bump.yml +0 -27
- package/.github/workflows/dependabot.yml +0 -58
- package/.github/workflows/publish.yml +0 -42
- package/.github/workflows/templates/bump.yml +0 -9
- package/.github/workflows/templates/dependabot.yml +0 -12
- package/.github/workflows/templates/publish.yml +0 -16
- package/pnpm-workspace.yaml +0 -6
- package/src/cli/tsc.ts +0 -235
- package/src/compiler/ast.ts +0 -60
- package/src/compiler/code.ts +0 -27
- package/src/compiler/coordinator.ts +0 -284
- package/src/compiler/imports.ts +0 -185
- package/src/compiler/index.ts +0 -7
- package/src/compiler/language-service.ts +0 -121
- package/src/compiler/plugins/index.ts +0 -5
- package/src/compiler/plugins/tsc.ts +0 -6
- package/src/compiler/plugins/vite.ts +0 -91
- package/src/compiler/types.ts +0 -83
- package/src/compiler/uid.ts +0 -10
- package/src/constants.ts +0 -4
- package/src/index.ts +0 -1
- package/tests/cli/tsc.test.ts +0 -155
- package/tests/compiler/ast.test.ts +0 -131
- package/tests/compiler/code.test.ts +0 -73
- package/tests/compiler/coordinator.bench.ts +0 -101
- package/tests/compiler/coordinator.test.ts +0 -872
- package/tests/compiler/imports.test.ts +0 -167
- package/tests/compiler/language-service.test.ts +0 -90
- package/tests/compiler/plugins.test.ts +0 -172
- package/tests/compiler/uid.test.ts +0 -70
- package/tsconfig.json +0 -3
- package/vitest.config.ts +0 -14
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnalyzeResult } from '../probe/kernel/analyze.js';
|
|
2
|
+
import type { AnalyzeConfig } from '../probe/kernel/types.js';
|
|
3
|
+
declare class AnalyzeWorkspace {
|
|
4
|
+
private api;
|
|
5
|
+
private configPath;
|
|
6
|
+
private snapshot;
|
|
7
|
+
config: AnalyzeConfig | undefined;
|
|
8
|
+
constructor(tsconfigPath: string);
|
|
9
|
+
reloadConfig(): void;
|
|
10
|
+
refresh(changed: ReadonlyArray<string>): void;
|
|
11
|
+
analyze(): AnalyzeResult | undefined;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
14
|
+
export { AnalyzeWorkspace };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as NodePath from 'node:path';
|
|
2
|
+
import { analyzeProgram } from '../probe/kernel/analyze.js';
|
|
3
|
+
import { loadConfigFromTsconfig } from '../probe/kernel/config.js';
|
|
4
|
+
import { open } from '../compiler/language-service.js';
|
|
5
|
+
class AnalyzeWorkspace {
|
|
6
|
+
api;
|
|
7
|
+
configPath;
|
|
8
|
+
snapshot;
|
|
9
|
+
config;
|
|
10
|
+
constructor(tsconfigPath) {
|
|
11
|
+
this.configPath = NodePath.resolve(tsconfigPath);
|
|
12
|
+
this.config = loadConfigFromTsconfig(this.configPath);
|
|
13
|
+
let opened = open(this.configPath);
|
|
14
|
+
this.api = opened.api;
|
|
15
|
+
this.snapshot = opened.snapshot;
|
|
16
|
+
}
|
|
17
|
+
reloadConfig() {
|
|
18
|
+
this.config = loadConfigFromTsconfig(this.configPath);
|
|
19
|
+
}
|
|
20
|
+
refresh(changed) {
|
|
21
|
+
let previous = this.snapshot;
|
|
22
|
+
this.snapshot = this.api.updateSnapshot(changed.length === 0
|
|
23
|
+
? { fileChanges: { invalidateAll: true } }
|
|
24
|
+
: { fileChanges: { changed: changed.map((fileName) => NodePath.resolve(fileName)) } });
|
|
25
|
+
if (previous && !previous.isDisposed()) {
|
|
26
|
+
previous.dispose();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
analyze() {
|
|
30
|
+
if (!this.config || !this.snapshot) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
let project = this.snapshot.getProject(this.configPath);
|
|
34
|
+
if (!project) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return analyzeProgram(project.program, project.checker, this.config);
|
|
38
|
+
}
|
|
39
|
+
dispose() {
|
|
40
|
+
if (this.snapshot && !this.snapshot.isDisposed()) {
|
|
41
|
+
this.snapshot.dispose();
|
|
42
|
+
}
|
|
43
|
+
this.api.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export { AnalyzeWorkspace };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from 'typescript/unstable/ast';
|
|
2
|
+
export * from 'typescript/unstable/ast/is';
|
|
3
|
+
export * from 'typescript/unstable/sync';
|
|
4
|
+
export { ModifierFlags } from 'typescript/unstable/sync';
|
|
5
|
+
import { type Node, type SourceFile } from 'typescript/unstable/ast';
|
|
6
|
+
import { type Checker, type Program, type Symbol, type Type } from 'typescript/unstable/sync';
|
|
7
|
+
export type TypeChecker = Checker;
|
|
8
|
+
declare function forEachChild<T>(node: Node, cb: (child: Node) => T | undefined): T | undefined;
|
|
9
|
+
declare function isClassLike(node: Node): boolean;
|
|
10
|
+
declare function getSourceFiles(program: Program): SourceFile[];
|
|
11
|
+
declare function symbolDeclarations(symbol: Symbol): Node[];
|
|
12
|
+
declare function symbolValueDeclaration(symbol: Symbol): Node | undefined;
|
|
13
|
+
declare function unionTypes(type: Type): readonly Type[] | undefined;
|
|
14
|
+
export { forEachChild, getSourceFiles, isClassLike, symbolDeclarations, symbolValueDeclaration, unionTypes };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export * from 'typescript/unstable/ast';
|
|
2
|
+
export * from 'typescript/unstable/ast/is';
|
|
3
|
+
export * from 'typescript/unstable/sync';
|
|
4
|
+
export { ModifierFlags } from 'typescript/unstable/sync';
|
|
5
|
+
import { SyntaxKind, } from 'typescript/unstable/ast';
|
|
6
|
+
import { TypeFlags, } from 'typescript/unstable/sync';
|
|
7
|
+
function forEachChild(node, cb) {
|
|
8
|
+
return node.forEachChild(cb);
|
|
9
|
+
}
|
|
10
|
+
function isClassLike(node) {
|
|
11
|
+
return (node.kind === SyntaxKind.ClassDeclaration ||
|
|
12
|
+
node.kind === SyntaxKind.ClassExpression);
|
|
13
|
+
}
|
|
14
|
+
function getSourceFiles(program) {
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const name of program.getSourceFileNames()) {
|
|
17
|
+
const sf = program.getSourceFile(name);
|
|
18
|
+
if (sf) {
|
|
19
|
+
out.push(sf);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function symbolDeclarations(symbol) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const handle of symbol.declarations ?? []) {
|
|
27
|
+
const node = handle.resolve();
|
|
28
|
+
if (node) {
|
|
29
|
+
out.push(node);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function symbolValueDeclaration(symbol) {
|
|
35
|
+
return symbol.valueDeclaration?.resolve();
|
|
36
|
+
}
|
|
37
|
+
function unionTypes(type) {
|
|
38
|
+
return (type.flags & TypeFlags.Union) !== 0
|
|
39
|
+
? type.getTypes()
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
42
|
+
export { forEachChild, getSourceFiles, isClassLike, symbolDeclarations, symbolValueDeclaration, unionTypes };
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import * as ts from '../../probe/adapter.js';
|
|
2
|
+
import { bodyOf, calleeSelectors, unwrap } from '../kernel/ast.js';
|
|
3
|
+
import { isFunctionLike, locationOf } from '../kernel/ids.js';
|
|
4
|
+
import { bottom, equals, promise, widen } from './value.js';
|
|
5
|
+
const AGGREGATORS = new Set(['all', 'allSettled', 'race', 'any']);
|
|
6
|
+
const CHAIN_METHODS = new Set(['catch', 'finally', 'then']);
|
|
7
|
+
const OPAQUE_SINKS = new Set(['add', 'push', 'set', 'unshift']);
|
|
8
|
+
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
|
+
function isPromiseType(checker, type) {
|
|
47
|
+
if (!type) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
for (const c of constituents(type)) {
|
|
51
|
+
const sym = c.getSymbol() ?? c.getAliasSymbol();
|
|
52
|
+
if (sym && sym.name === 'Promise') {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const apparent = checker.getApparentType(c) ?? c;
|
|
56
|
+
if (checker.getPropertyOfType(apparent, 'then')) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function hasAsyncModifier(node) {
|
|
63
|
+
const mods = node.modifiers;
|
|
64
|
+
return mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
|
|
65
|
+
}
|
|
66
|
+
function enclosingAsync(node) {
|
|
67
|
+
let p = node.parent;
|
|
68
|
+
while (p) {
|
|
69
|
+
if (isFunctionLike(p)) {
|
|
70
|
+
return hasAsyncModifier(p);
|
|
71
|
+
}
|
|
72
|
+
p = p.parent;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function returnsPromise(checker, node) {
|
|
77
|
+
if (hasAsyncModifier(node)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
const fnType = checker.getTypeAtLocation(node);
|
|
81
|
+
if (!fnType) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const sig = checker.getSignaturesOfType(fnType, ts.SignatureKind.Call)[0];
|
|
85
|
+
if (!sig) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return isPromiseType(checker, checker.getReturnTypeOfSignature(sig));
|
|
89
|
+
}
|
|
90
|
+
function producesPromise(env, call) {
|
|
91
|
+
const res = env.resolveCall(call);
|
|
92
|
+
if (res.targets.length > 0) {
|
|
93
|
+
return res.targets.some((t) => env.summaryOf(t).value.returnsPromise);
|
|
94
|
+
}
|
|
95
|
+
return isPromiseType(env.checker, env.checker.getTypeAtLocation(call));
|
|
96
|
+
}
|
|
97
|
+
function aggregatorOf(call) {
|
|
98
|
+
const callee = call.expression;
|
|
99
|
+
if (!ts.isPropertyAccessExpression(callee) ||
|
|
100
|
+
!ts.isIdentifier(callee.expression)) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
if (callee.expression.text !== 'Promise' ||
|
|
104
|
+
!AGGREGATORS.has(callee.name.text)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return callee.name.text;
|
|
108
|
+
}
|
|
109
|
+
function isAggregatorCall(node) {
|
|
110
|
+
return ts.isCallExpression(node) && aggregatorOf(node) !== undefined;
|
|
111
|
+
}
|
|
112
|
+
function chainRoot(call) {
|
|
113
|
+
let cur = call;
|
|
114
|
+
while (ts.isPropertyAccessExpression(cur.expression) &&
|
|
115
|
+
CHAIN_METHODS.has(cur.expression.name.text)) {
|
|
116
|
+
const receiver = cur.expression.expression;
|
|
117
|
+
if (!ts.isCallExpression(receiver) && !ts.isNewExpression(receiver)) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
cur = receiver;
|
|
121
|
+
}
|
|
122
|
+
return cur;
|
|
123
|
+
}
|
|
124
|
+
function calleeText(node) {
|
|
125
|
+
const call = chainRoot(node);
|
|
126
|
+
const callee = call.expression;
|
|
127
|
+
if (ts.isIdentifier(callee)) {
|
|
128
|
+
return callee.text;
|
|
129
|
+
}
|
|
130
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
131
|
+
return callee.name.text;
|
|
132
|
+
}
|
|
133
|
+
return callee.getText(callee.getSourceFile());
|
|
134
|
+
}
|
|
135
|
+
function isChainContinuation(call) {
|
|
136
|
+
const parent = call.parent;
|
|
137
|
+
if (!parent ||
|
|
138
|
+
!ts.isPropertyAccessExpression(parent) ||
|
|
139
|
+
parent.expression !== call) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return (CHAIN_METHODS.has(parent.name.text) &&
|
|
143
|
+
ts.isCallExpression(parent.parent) &&
|
|
144
|
+
parent.parent.expression === parent);
|
|
145
|
+
}
|
|
146
|
+
function chainHandlesRejection(call) {
|
|
147
|
+
let cur = call;
|
|
148
|
+
while (ts.isCallExpression(cur) &&
|
|
149
|
+
ts.isPropertyAccessExpression(cur.expression)) {
|
|
150
|
+
const member = cur.expression.name.text;
|
|
151
|
+
if (member === 'catch' && cur.arguments.length >= 1) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (member === 'then' && cur.arguments.length >= 2) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
cur = cur.expression.expression;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
function isValueTransparent(node, child) {
|
|
162
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (ts.isConditionalExpression(node)) {
|
|
166
|
+
return node.whenTrue === child || node.whenFalse === child;
|
|
167
|
+
}
|
|
168
|
+
if (ts.isBinaryExpression(node) && node.right === child) {
|
|
169
|
+
const k = node.operatorToken.kind;
|
|
170
|
+
return (k === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
171
|
+
k === ts.SyntaxKind.BarBarToken ||
|
|
172
|
+
k === ts.SyntaxKind.QuestionQuestionToken ||
|
|
173
|
+
k === ts.SyntaxKind.CommaToken);
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
function classifyOwnership(node) {
|
|
178
|
+
let child = node;
|
|
179
|
+
let parent = node.parent;
|
|
180
|
+
while (parent && isValueTransparent(parent, child)) {
|
|
181
|
+
child = parent;
|
|
182
|
+
parent = parent.parent;
|
|
183
|
+
}
|
|
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';
|
|
229
|
+
}
|
|
230
|
+
function isBoundedInput(env, arg) {
|
|
231
|
+
const e = unwrap(arg);
|
|
232
|
+
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;
|
|
237
|
+
}
|
|
238
|
+
const type = env.checker.getTypeAtLocation(e);
|
|
239
|
+
if (type && type.isTupleType()) {
|
|
240
|
+
const flags = type.elementFlags ??
|
|
241
|
+
[];
|
|
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
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
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
|
+
function isAbortSignalType(type) {
|
|
302
|
+
if (!type) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
for (const c of constituents(type)) {
|
|
306
|
+
const sym = c.getSymbol() ?? c.getAliasSymbol();
|
|
307
|
+
if (sym && sym.name === 'AbortSignal') {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
function collectSignalNames(checker, name, out) {
|
|
314
|
+
if (ts.isIdentifier(name)) {
|
|
315
|
+
if (isAbortSignalType(checker.getTypeAtLocation(name))) {
|
|
316
|
+
out.add(name.text);
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (ts.isObjectBindingPattern(name)) {
|
|
321
|
+
for (const el of name.elements) {
|
|
322
|
+
if (el.name) {
|
|
323
|
+
collectSignalNames(checker, el.name, out);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function heldSignalNames(checker, node) {
|
|
329
|
+
const out = new Set();
|
|
330
|
+
const params = node
|
|
331
|
+
.parameters ?? [];
|
|
332
|
+
for (const p of params) {
|
|
333
|
+
collectSignalNames(checker, p.name, out);
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
function overlayCancellable(env, call) {
|
|
338
|
+
const entry = env.resolveCall(call).overlay?.entry;
|
|
339
|
+
return entry?.cancellable === true;
|
|
340
|
+
}
|
|
341
|
+
function callRequiresSignal(env, call) {
|
|
342
|
+
if (overlayCancellable(env, call)) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
return env
|
|
346
|
+
.resolveCall(call)
|
|
347
|
+
.targets.some((t) => env.heldSignalNames(t).size > 0);
|
|
348
|
+
}
|
|
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
|
+
function forwardsSignal(call, held) {
|
|
363
|
+
let found = false;
|
|
364
|
+
const visit = (n) => {
|
|
365
|
+
if (found) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (ts.isIdentifier(n) && held.has(n.text)) {
|
|
369
|
+
found = true;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
ts.forEachChild(n, visit);
|
|
373
|
+
};
|
|
374
|
+
for (const arg of call.arguments) {
|
|
375
|
+
visit(arg);
|
|
376
|
+
}
|
|
377
|
+
return found;
|
|
378
|
+
}
|
|
379
|
+
function orphanFixes(call) {
|
|
380
|
+
const sf = call.getSourceFile();
|
|
381
|
+
const pos = call.getStart(sf);
|
|
382
|
+
const fixes = [
|
|
383
|
+
{
|
|
384
|
+
title: 'Ignore the result with `void`',
|
|
385
|
+
edits: [{ fileName: sf.fileName, pos, end: pos, newText: 'void ' }],
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
if (enclosingAsync(call)) {
|
|
389
|
+
fixes.push({
|
|
390
|
+
title: 'Await the promise',
|
|
391
|
+
edits: [
|
|
392
|
+
{ fileName: sf.fileName, pos, end: pos, newText: 'await ' },
|
|
393
|
+
],
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return fixes;
|
|
397
|
+
}
|
|
398
|
+
function orphanDiagnostic(call) {
|
|
399
|
+
return {
|
|
400
|
+
channel: 'async',
|
|
401
|
+
message: `result of \`${calleeText(call)}()\` is neither awaited nor voided — rejections will be unhandled`,
|
|
402
|
+
location: locationOf(call, call.getSourceFile()),
|
|
403
|
+
related: [],
|
|
404
|
+
fixes: orphanFixes(call),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
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
|
+
function fanOutDiagnostic(call, aggregator) {
|
|
416
|
+
return {
|
|
417
|
+
channel: 'async',
|
|
418
|
+
message: `unbounded fan-out: \`Promise.${aggregator}\` over a dynamically-sized input — cap concurrency with a pool or bound the input`,
|
|
419
|
+
location: locationOf(call, call.getSourceFile()),
|
|
420
|
+
related: [],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function signalFix(env, call) {
|
|
424
|
+
if (!overlayCancellable(env, call)) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
const name = env.held.values().next().value;
|
|
428
|
+
if (!name) {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
const sf = call.getSourceFile();
|
|
432
|
+
const prop = name === 'signal' ? 'signal' : `signal: ${name}`;
|
|
433
|
+
const last = call.arguments.length > 0
|
|
434
|
+
? call.arguments[call.arguments.length - 1]
|
|
435
|
+
: undefined;
|
|
436
|
+
if (last && ts.isObjectLiteralExpression(last)) {
|
|
437
|
+
const at = last.getStart(sf) + 1;
|
|
438
|
+
const newText = last.properties.length > 0 ? ` ${prop},` : ` ${prop} `;
|
|
439
|
+
return [
|
|
440
|
+
{
|
|
441
|
+
title: 'Forward the AbortSignal',
|
|
442
|
+
edits: [{ fileName: sf.fileName, pos: at, end: at, newText }],
|
|
443
|
+
},
|
|
444
|
+
];
|
|
445
|
+
}
|
|
446
|
+
const close = call.getEnd() - 1;
|
|
447
|
+
const newText = call.arguments.length > 0 ? `, { ${prop} }` : `{ ${prop} }`;
|
|
448
|
+
return [
|
|
449
|
+
{
|
|
450
|
+
title: 'Forward the AbortSignal',
|
|
451
|
+
edits: [{ fileName: sf.fileName, pos: close, end: close, newText }],
|
|
452
|
+
},
|
|
453
|
+
];
|
|
454
|
+
}
|
|
455
|
+
function cancellationDiagnostic(call, fixes) {
|
|
456
|
+
return {
|
|
457
|
+
channel: 'async',
|
|
458
|
+
message: `\`${calleeText(call)}()\` is awaited without the AbortSignal this function holds — the work cannot be cancelled`,
|
|
459
|
+
location: locationOf(call, call.getSourceFile()),
|
|
460
|
+
related: [],
|
|
461
|
+
fixes,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function checkFanOut(env, call, out) {
|
|
465
|
+
if (env.options.fanOut === 'off') {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const aggregator = aggregatorOf(call);
|
|
469
|
+
if (!aggregator) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const arg = call.arguments[0];
|
|
473
|
+
if (!arg || isBoundedInput(env, arg) || routedThroughPool(env, arg)) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
out.push(fanOutDiagnostic(call, aggregator));
|
|
477
|
+
}
|
|
478
|
+
function checkOwnership(env, call, out) {
|
|
479
|
+
if (isChainContinuation(call) || !producesPromise(env, call)) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
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));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function checkCancellation(env, call, out) {
|
|
493
|
+
if (env.held.size === 0 ||
|
|
494
|
+
!callRequiresSignal(env, call) ||
|
|
495
|
+
!isAwaited(call)) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (!forwardsSignal(call, env.held)) {
|
|
499
|
+
out.push(cancellationDiagnostic(call, signalFix(env, call)));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function walk(env, body, out) {
|
|
503
|
+
const visit = (n) => {
|
|
504
|
+
if (isFunctionLike(n)) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (ts.isCallExpression(n)) {
|
|
508
|
+
checkFanOut(env, n, out);
|
|
509
|
+
checkCancellation(env, n, out);
|
|
510
|
+
}
|
|
511
|
+
if (ts.isCallExpression(n) || ts.isNewExpression(n)) {
|
|
512
|
+
checkOwnership(env, n, out);
|
|
513
|
+
}
|
|
514
|
+
ts.forEachChild(n, visit);
|
|
515
|
+
};
|
|
516
|
+
ts.forEachChild(body, visit);
|
|
517
|
+
}
|
|
518
|
+
function createAsyncChannel(channelConfig) {
|
|
519
|
+
const options = parseOptions(channelConfig);
|
|
520
|
+
const heldByFunction = new Map();
|
|
521
|
+
const getHeldSignalNames = (checker, fn) => {
|
|
522
|
+
let held = heldByFunction.get(fn);
|
|
523
|
+
if (!held) {
|
|
524
|
+
held = heldSignalNames(checker, fn.node);
|
|
525
|
+
heldByFunction.set(fn, held);
|
|
526
|
+
}
|
|
527
|
+
return held;
|
|
528
|
+
};
|
|
529
|
+
return {
|
|
530
|
+
name: 'async',
|
|
531
|
+
bottom,
|
|
532
|
+
equals,
|
|
533
|
+
widen,
|
|
534
|
+
transfer(ctx) {
|
|
535
|
+
const value = returnsPromise(ctx.checker, ctx.fn.node)
|
|
536
|
+
? promise()
|
|
537
|
+
: bottom();
|
|
538
|
+
return { value, fromCallbacks: new Set() };
|
|
539
|
+
},
|
|
540
|
+
diagnose(ctx) {
|
|
541
|
+
const body = bodyOf(ctx.fn.node);
|
|
542
|
+
if (!body) {
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
const env = {
|
|
546
|
+
checker: ctx.checker,
|
|
547
|
+
dispatch: ctx.dispatch,
|
|
548
|
+
options,
|
|
549
|
+
summaryOf: ctx.summaryOf,
|
|
550
|
+
resolveCall: ctx.resolveCall,
|
|
551
|
+
heldSignalNames: (fn) => getHeldSignalNames(ctx.checker, fn),
|
|
552
|
+
held: getHeldSignalNames(ctx.checker, ctx.fn),
|
|
553
|
+
};
|
|
554
|
+
const out = [];
|
|
555
|
+
walk(env, body, out);
|
|
556
|
+
return out;
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
export { createAsyncChannel };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type AsyncValue = {
|
|
2
|
+
readonly returnsPromise: boolean;
|
|
3
|
+
};
|
|
4
|
+
declare function bottom(): AsyncValue;
|
|
5
|
+
declare function promise(): AsyncValue;
|
|
6
|
+
declare function join(a: AsyncValue, b: AsyncValue): AsyncValue;
|
|
7
|
+
declare function equals(a: AsyncValue, b: AsyncValue): boolean;
|
|
8
|
+
declare function widen(_prev: AsyncValue, next: AsyncValue, _round: number): AsyncValue;
|
|
9
|
+
export { bottom, equals, join, promise, type AsyncValue, widen };
|