@esportsplus/typescript 0.31.0 → 0.32.0
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 +91 -0
- package/bin/tsc-lsp +3 -0
- package/build/cli/diagnostics.d.ts +5 -1
- package/build/cli/diagnostics.js +13 -8
- package/build/cli/tsc.d.ts +3 -1
- package/build/cli/tsc.js +77 -87
- package/build/compiler/coordinator.d.ts +1 -2
- package/build/compiler/coordinator.js +33 -22
- package/build/compiler/imports.d.ts +2 -0
- package/build/compiler/imports.js +9 -2
- package/build/compiler/language-service.d.ts +15 -4
- package/build/compiler/language-service.js +56 -24
- package/build/compiler/plugins/vite.js +7 -5
- package/build/compiler/sourcemap.d.ts +3 -1
- package/build/compiler/sourcemap.js +23 -5
- 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 +669 -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/tsconfig.d.ts +2 -0
- package/build/tsconfig.js +150 -0
- package/package.json +14 -8
- package/tsconfig.base.json +19 -0
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
import * as ts from '../adapter.js';
|
|
2
|
+
import { overlayThrows } from '../exceptions/channel.js';
|
|
3
|
+
import { bodyOf, calleeSelectors, paramSymbols, unwrap } from '../kernel/ast.js';
|
|
4
|
+
import { isEmpty } from '../exceptions/value.js';
|
|
5
|
+
import { isFunctionLike, locationOf } from '../kernel/ids.js';
|
|
6
|
+
import { bottom, equals, fromParams, widen, } from './value.js';
|
|
7
|
+
const FREE_RELEASERS = new Set([
|
|
8
|
+
'clearImmediate',
|
|
9
|
+
'clearInterval',
|
|
10
|
+
'clearTimeout',
|
|
11
|
+
]);
|
|
12
|
+
const DISPOSE_METHODS = new Set([
|
|
13
|
+
'close',
|
|
14
|
+
'destroy',
|
|
15
|
+
'disconnect',
|
|
16
|
+
'dispose',
|
|
17
|
+
'release',
|
|
18
|
+
'unsubscribe',
|
|
19
|
+
]);
|
|
20
|
+
const OPAQUE_METHODS = new Set([
|
|
21
|
+
'add',
|
|
22
|
+
'append',
|
|
23
|
+
'enqueue',
|
|
24
|
+
'push',
|
|
25
|
+
'set',
|
|
26
|
+
'unshift',
|
|
27
|
+
]);
|
|
28
|
+
function refsSym(env, expr, sym) {
|
|
29
|
+
const e = unwrap(expr, { assertions: true, awaits: true });
|
|
30
|
+
if (!ts.isIdentifier(e)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return env.checker.getSymbolAtLocation(e) === sym;
|
|
34
|
+
}
|
|
35
|
+
function containsMatch(node, pred) {
|
|
36
|
+
let found = false;
|
|
37
|
+
const rec = (n) => {
|
|
38
|
+
if (found) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (n !== node && isFunctionLike(n)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (pred(n)) {
|
|
45
|
+
found = true;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
ts.forEachChild(n, rec);
|
|
49
|
+
};
|
|
50
|
+
rec(node);
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
function isExitStatement(n) {
|
|
54
|
+
return (ts.isReturnStatement(n) ||
|
|
55
|
+
ts.isThrowStatement(n) ||
|
|
56
|
+
ts.isBreakStatement(n) ||
|
|
57
|
+
ts.isContinueStatement(n));
|
|
58
|
+
}
|
|
59
|
+
function isCallLike(n) {
|
|
60
|
+
return ts.isCallExpression(n) || ts.isNewExpression(n);
|
|
61
|
+
}
|
|
62
|
+
function callCanThrow(env, call) {
|
|
63
|
+
const res = env.resolveExceptions(call);
|
|
64
|
+
if (res.overlay) {
|
|
65
|
+
return overlayThrows(res.overlay.entry);
|
|
66
|
+
}
|
|
67
|
+
if (res.targets.length > 0) {
|
|
68
|
+
return res.targets.some((t) => env.peerThrows(t.id));
|
|
69
|
+
}
|
|
70
|
+
return res.unresolved && env.dispatch === 'pessimist';
|
|
71
|
+
}
|
|
72
|
+
function enclosingStatement(node) {
|
|
73
|
+
let n = node;
|
|
74
|
+
while (n.parent && !ts.isBlock(n.parent) && !ts.isSourceFile(n.parent)) {
|
|
75
|
+
n = n.parent;
|
|
76
|
+
}
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
function blockOf(stmt) {
|
|
80
|
+
return stmt.parent && ts.isBlock(stmt.parent) ? stmt.parent : undefined;
|
|
81
|
+
}
|
|
82
|
+
function isDisposableType(env, type) {
|
|
83
|
+
if (!type) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
for (const prop of env.checker.getPropertiesOfType(type)) {
|
|
87
|
+
if (prop.name.startsWith('__@dispose') ||
|
|
88
|
+
prop.name.startsWith('__@asyncDispose')) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
function hasSyncDispose(env, type) {
|
|
95
|
+
if (!type) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
for (const prop of env.checker.getPropertiesOfType(type)) {
|
|
99
|
+
if (prop.name.startsWith('__@dispose')) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
function parsePairKey(raw) {
|
|
106
|
+
if (!Array.isArray(raw)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const v of raw) {
|
|
111
|
+
if (typeof v === 'number' && Number.isInteger(v)) {
|
|
112
|
+
out.push(v);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out.length > 0 ? out : undefined;
|
|
116
|
+
}
|
|
117
|
+
function acquireAt(env, call) {
|
|
118
|
+
const res = env.resolveCall(call);
|
|
119
|
+
if (res.overlay) {
|
|
120
|
+
const entry = res.overlay.entry;
|
|
121
|
+
if (entry.informational) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
if (typeof entry.acquires === 'string') {
|
|
125
|
+
const releasedBy = typeof entry.releasedBy === 'string'
|
|
126
|
+
? entry.releasedBy
|
|
127
|
+
: undefined;
|
|
128
|
+
const pairKey = parsePairKey(entry.pairKey);
|
|
129
|
+
return {
|
|
130
|
+
kind: pairKey ? 'pair' : 'handle',
|
|
131
|
+
releasedBy,
|
|
132
|
+
pairKey,
|
|
133
|
+
label: entry.acquires,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (isDisposableType(env, env.checker.getTypeAtLocation(call))) {
|
|
138
|
+
return {
|
|
139
|
+
kind: 'handle',
|
|
140
|
+
releasedBy: undefined,
|
|
141
|
+
pairKey: undefined,
|
|
142
|
+
label: 'Disposable',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
function handleBinding(env, call) {
|
|
148
|
+
let p = call;
|
|
149
|
+
while (p.parent &&
|
|
150
|
+
(ts.isParenthesizedExpression(p.parent) ||
|
|
151
|
+
ts.isNonNullExpression(p.parent) ||
|
|
152
|
+
ts.isAsExpression(p.parent) ||
|
|
153
|
+
ts.isAwaitExpression(p.parent))) {
|
|
154
|
+
p = p.parent;
|
|
155
|
+
}
|
|
156
|
+
const decl = p.parent;
|
|
157
|
+
if (!decl ||
|
|
158
|
+
!ts.isVariableDeclaration(decl) ||
|
|
159
|
+
!ts.isIdentifier(decl.name)) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
const list = decl.parent;
|
|
163
|
+
if (list &&
|
|
164
|
+
ts.isVariableDeclarationList(list) &&
|
|
165
|
+
(list.flags & ts.NodeFlags.Using) !== 0) {
|
|
166
|
+
return { using: true };
|
|
167
|
+
}
|
|
168
|
+
const sym = env.checker.getSymbolAtLocation(decl.name);
|
|
169
|
+
return sym ? { using: false, sym } : undefined;
|
|
170
|
+
}
|
|
171
|
+
function isDischargeOf(env, expr, sym, acq) {
|
|
172
|
+
if (!ts.isCallExpression(expr)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const callee = expr.expression;
|
|
176
|
+
if (ts.isIdentifier(callee)) {
|
|
177
|
+
const freeName = acq?.releasedBy !== undefined && !acq.releasedBy.startsWith('#')
|
|
178
|
+
? acq.releasedBy
|
|
179
|
+
: undefined;
|
|
180
|
+
const isFree = FREE_RELEASERS.has(callee.text) || callee.text === freeName;
|
|
181
|
+
if (!isFree) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const arg = expr.arguments?.[0];
|
|
185
|
+
return arg ? refsSym(env, arg, sym) : false;
|
|
186
|
+
}
|
|
187
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
188
|
+
if (!refsSym(env, callee.expression, sym)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const method = callee.name.text;
|
|
192
|
+
if (DISPOSE_METHODS.has(method)) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return (acq?.releasedBy !== undefined &&
|
|
196
|
+
acq.releasedBy.startsWith('#') &&
|
|
197
|
+
acq.releasedBy.slice(1) === method);
|
|
198
|
+
}
|
|
199
|
+
if (ts.isElementAccessExpression(callee)) {
|
|
200
|
+
if (!refsSym(env, callee.expression, sym)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const argx = callee.argumentExpression;
|
|
204
|
+
return (argx !== undefined &&
|
|
205
|
+
ts.isPropertyAccessExpression(argx) &&
|
|
206
|
+
(argx.name.text === 'dispose' || argx.name.text === 'asyncDispose'));
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
function callOwnsArg(env, call, sym) {
|
|
211
|
+
const args = call.arguments ? Array.from(call.arguments) : [];
|
|
212
|
+
const indices = [];
|
|
213
|
+
args.forEach((arg, i) => {
|
|
214
|
+
if (refsSym(env, arg, sym)) {
|
|
215
|
+
indices.push(i);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
if (indices.length === 0) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const selectors = calleeSelectors(call.expression);
|
|
222
|
+
for (const own of env.ownership) {
|
|
223
|
+
if (selectors.has(own.callee) &&
|
|
224
|
+
own.params.some((p) => indices.includes(p))) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const res = env.resolveCall(call);
|
|
229
|
+
const entry = (res.overlay?.entry ?? undefined);
|
|
230
|
+
if (entry && Array.isArray(entry.ownsParams)) {
|
|
231
|
+
for (const p of entry.ownsParams) {
|
|
232
|
+
if (typeof p === 'number' && indices.includes(p)) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const target of res.targets) {
|
|
238
|
+
const owned = env.summaryOf(target).value.ownsParams;
|
|
239
|
+
for (const i of indices) {
|
|
240
|
+
if (owned.has(i)) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
function isTransferExpr(env, expr, sym) {
|
|
248
|
+
if (ts.isBinaryExpression(expr) &&
|
|
249
|
+
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
250
|
+
const target = expr.left;
|
|
251
|
+
if ((ts.isPropertyAccessExpression(target) ||
|
|
252
|
+
ts.isElementAccessExpression(target)) &&
|
|
253
|
+
refsSym(env, expr.right, sym)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (ts.isCallExpression(expr) || ts.isNewExpression(expr)) {
|
|
258
|
+
return callOwnsArg(env, expr, sym);
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
function statementDischargesOrTransfers(env, s, sym, acq) {
|
|
263
|
+
if (ts.isReturnStatement(s)) {
|
|
264
|
+
if (!s.expression) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
return (refsSym(env, s.expression, sym) ||
|
|
268
|
+
isTransferExpr(env, s.expression, sym));
|
|
269
|
+
}
|
|
270
|
+
if (ts.isExpressionStatement(s)) {
|
|
271
|
+
return (isDischargeOf(env, s.expression, sym, acq) ||
|
|
272
|
+
isTransferExpr(env, s.expression, sym));
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
function finallyDischarges(env, sym, acq, declStmt, body) {
|
|
277
|
+
let found = false;
|
|
278
|
+
const rec = (n) => {
|
|
279
|
+
if (found) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (n !== body && isFunctionLike(n)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (ts.isTryStatement(n) &&
|
|
286
|
+
n.finallyBlock &&
|
|
287
|
+
declStmt.getStart() < n.finallyBlock.getStart()) {
|
|
288
|
+
const discharged = containsMatch(n.finallyBlock, (m) => (ts.isCallExpression(m) || ts.isNewExpression(m)) &&
|
|
289
|
+
isDischargeOf(env, m, sym, acq));
|
|
290
|
+
if (discharged) {
|
|
291
|
+
found = true;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
ts.forEachChild(n, rec);
|
|
296
|
+
};
|
|
297
|
+
rec(body);
|
|
298
|
+
return found;
|
|
299
|
+
}
|
|
300
|
+
function firstGuaranteed(env, sym, acq, declStmt, block) {
|
|
301
|
+
const stmts = block.statements;
|
|
302
|
+
const idx = stmts.findIndex((s) => s === declStmt);
|
|
303
|
+
if (idx < 0) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
for (let i = idx + 1; i < stmts.length; i += 1) {
|
|
307
|
+
const s = stmts[i];
|
|
308
|
+
if (statementDischargesOrTransfers(env, s, sym, acq)) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (containsMatch(s, isExitStatement)) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (containsMatch(s, (n) => isCallLike(n) &&
|
|
315
|
+
callCanThrow(env, n))) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
function opaqueUseOf(env, sym, body) {
|
|
322
|
+
let hit;
|
|
323
|
+
const rec = (n) => {
|
|
324
|
+
if (hit) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (n !== body && isFunctionLike(n)) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (ts.isCallExpression(n) &&
|
|
331
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
332
|
+
OPAQUE_METHODS.has(n.expression.name.text)) {
|
|
333
|
+
for (const arg of n.arguments ?? []) {
|
|
334
|
+
if (refsSym(env, arg, sym)) {
|
|
335
|
+
hit = n;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (ts.isBinaryExpression(n) &&
|
|
341
|
+
n.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
342
|
+
ts.isIdentifier(n.left) &&
|
|
343
|
+
refsSym(env, n.right, sym)) {
|
|
344
|
+
hit = n;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (ts.isVariableDeclaration(n) &&
|
|
348
|
+
n.initializer &&
|
|
349
|
+
refsSym(env, n.initializer, sym)) {
|
|
350
|
+
hit = n;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
ts.forEachChild(n, rec);
|
|
354
|
+
};
|
|
355
|
+
rec(body);
|
|
356
|
+
return hit;
|
|
357
|
+
}
|
|
358
|
+
function handleOutcome(env, acq, sym, declStmt, block, body) {
|
|
359
|
+
if (finallyDischarges(env, sym, acq, declStmt, body)) {
|
|
360
|
+
return 'safe';
|
|
361
|
+
}
|
|
362
|
+
if (firstGuaranteed(env, sym, acq, declStmt, block)) {
|
|
363
|
+
return 'safe';
|
|
364
|
+
}
|
|
365
|
+
if (opaqueUseOf(env, sym, body)) {
|
|
366
|
+
return 'degrade';
|
|
367
|
+
}
|
|
368
|
+
return 'leak';
|
|
369
|
+
}
|
|
370
|
+
function isReleaseCallForPair(node, acq, acquireCall) {
|
|
371
|
+
if (!ts.isCallExpression(node) || acq.releasedBy === undefined) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
const callee = node.expression;
|
|
375
|
+
const acqCallee = acquireCall.expression;
|
|
376
|
+
if (!ts.isPropertyAccessExpression(callee) ||
|
|
377
|
+
!ts.isPropertyAccessExpression(acqCallee)) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
if (callee.name.text !== acq.releasedBy) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (callee.expression.getText() !== acqCallee.expression.getText()) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
for (const i of acq.pairKey ?? []) {
|
|
387
|
+
const a = node.arguments?.[i];
|
|
388
|
+
const b = acquireCall.arguments?.[i];
|
|
389
|
+
if (!a || !b || a.getText() !== b.getText()) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
function hasMatchingRelease(env, acquireCall, acq) {
|
|
396
|
+
const body = bodyOf(env.fn.node);
|
|
397
|
+
if (!body) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
return containsMatch(body, (n) => isReleaseCallForPair(n, acq, acquireCall));
|
|
401
|
+
}
|
|
402
|
+
function classDischargesField(cls, field, acq) {
|
|
403
|
+
const members = cls.members ?? [];
|
|
404
|
+
const refsThisField = (expr) => {
|
|
405
|
+
const e = unwrap(expr, { assertions: true, awaits: true });
|
|
406
|
+
return (ts.isPropertyAccessExpression(e) &&
|
|
407
|
+
e.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
408
|
+
e.name.text === field);
|
|
409
|
+
};
|
|
410
|
+
const dischargesHere = (n) => {
|
|
411
|
+
if (!ts.isCallExpression(n)) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const callee = n.expression;
|
|
415
|
+
if (ts.isIdentifier(callee)) {
|
|
416
|
+
const freeName = acq?.releasedBy !== undefined && !acq.releasedBy.startsWith('#')
|
|
417
|
+
? acq.releasedBy
|
|
418
|
+
: undefined;
|
|
419
|
+
if (!FREE_RELEASERS.has(callee.text) && callee.text !== freeName) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const arg = n.arguments?.[0];
|
|
423
|
+
return arg ? refsThisField(arg) : false;
|
|
424
|
+
}
|
|
425
|
+
if (ts.isPropertyAccessExpression(callee) &&
|
|
426
|
+
refsThisField(callee.expression)) {
|
|
427
|
+
const method = callee.name.text;
|
|
428
|
+
return (DISPOSE_METHODS.has(method) ||
|
|
429
|
+
(acq?.releasedBy !== undefined &&
|
|
430
|
+
acq.releasedBy.startsWith('#') &&
|
|
431
|
+
acq.releasedBy.slice(1) === method));
|
|
432
|
+
}
|
|
433
|
+
if (ts.isElementAccessExpression(callee) &&
|
|
434
|
+
refsThisField(callee.expression)) {
|
|
435
|
+
const argx = callee.argumentExpression;
|
|
436
|
+
return (argx !== undefined &&
|
|
437
|
+
ts.isPropertyAccessExpression(argx) &&
|
|
438
|
+
(argx.name.text === 'dispose' ||
|
|
439
|
+
argx.name.text === 'asyncDispose'));
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
};
|
|
443
|
+
for (const member of members) {
|
|
444
|
+
const body = member.body;
|
|
445
|
+
if (body && containsMatch(body, dischargesHere)) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
function className(cls) {
|
|
452
|
+
const name = cls.name;
|
|
453
|
+
return name && ts.isIdentifier(name) ? name.text : '<anonymous>';
|
|
454
|
+
}
|
|
455
|
+
function classFieldAcquires(env, cls, ctorBody) {
|
|
456
|
+
const out = [];
|
|
457
|
+
const members = cls.members ?? [];
|
|
458
|
+
for (const member of members) {
|
|
459
|
+
if (ts.isPropertyDeclaration(member) &&
|
|
460
|
+
member.initializer &&
|
|
461
|
+
ts.isIdentifier(member.name)) {
|
|
462
|
+
const init = unwrap(member.initializer, {
|
|
463
|
+
assertions: true,
|
|
464
|
+
awaits: true,
|
|
465
|
+
});
|
|
466
|
+
if (ts.isCallExpression(init) || ts.isNewExpression(init)) {
|
|
467
|
+
const acq = acquireAt(env, init);
|
|
468
|
+
if (acq && acq.kind === 'handle') {
|
|
469
|
+
out.push({ field: member.name.text, node: member, acq });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (ctorBody) {
|
|
475
|
+
const rec = (n) => {
|
|
476
|
+
if (n !== ctorBody && isFunctionLike(n)) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (ts.isBinaryExpression(n) &&
|
|
480
|
+
n.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
481
|
+
ts.isPropertyAccessExpression(n.left) &&
|
|
482
|
+
n.left.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
483
|
+
const rhs = unwrap(n.right, { assertions: true, awaits: true });
|
|
484
|
+
if (ts.isCallExpression(rhs) || ts.isNewExpression(rhs)) {
|
|
485
|
+
const acq = acquireAt(env, rhs);
|
|
486
|
+
if (acq && acq.kind === 'handle') {
|
|
487
|
+
out.push({ field: n.left.name.text, node: n, acq });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
ts.forEachChild(n, rec);
|
|
492
|
+
};
|
|
493
|
+
rec(ctorBody);
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
function relatedPath(ctx) {
|
|
498
|
+
const related = [];
|
|
499
|
+
for (const f of ctx.pathToBoundary(ctx.fn)) {
|
|
500
|
+
related.push({
|
|
501
|
+
message: `on the path to boundary via ${f.name}`,
|
|
502
|
+
location: locationOf(f.node, f.sourceFile),
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return related;
|
|
506
|
+
}
|
|
507
|
+
function usingFix(env, call) {
|
|
508
|
+
if (!hasSyncDispose(env, env.checker.getTypeAtLocation(call))) {
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
let p = call;
|
|
512
|
+
while (p.parent &&
|
|
513
|
+
(ts.isParenthesizedExpression(p.parent) ||
|
|
514
|
+
ts.isNonNullExpression(p.parent) ||
|
|
515
|
+
ts.isAsExpression(p.parent) ||
|
|
516
|
+
ts.isAwaitExpression(p.parent))) {
|
|
517
|
+
p = p.parent;
|
|
518
|
+
}
|
|
519
|
+
const decl = p.parent;
|
|
520
|
+
if (!decl || !ts.isVariableDeclaration(decl)) {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
const list = decl.parent;
|
|
524
|
+
if (!list ||
|
|
525
|
+
!ts.isVariableDeclarationList(list) ||
|
|
526
|
+
(list.flags & ts.NodeFlags.Using) !== 0) {
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
const sf = call.getSourceFile();
|
|
530
|
+
const start = list.getStart(sf);
|
|
531
|
+
const keyword = (list.flags & ts.NodeFlags.Let) !== 0
|
|
532
|
+
? 'let'
|
|
533
|
+
: (list.flags & ts.NodeFlags.Const) !== 0
|
|
534
|
+
? 'const'
|
|
535
|
+
: undefined;
|
|
536
|
+
if (!keyword || sf.text.slice(start, start + keyword.length) !== keyword) {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
title: 'Convert to `using`',
|
|
541
|
+
edits: [
|
|
542
|
+
{
|
|
543
|
+
fileName: sf.fileName,
|
|
544
|
+
pos: start,
|
|
545
|
+
end: start + keyword.length,
|
|
546
|
+
newText: 'using',
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function releaseText(env, acq, name, call) {
|
|
552
|
+
if (acq.releasedBy) {
|
|
553
|
+
return acq.releasedBy.startsWith('#')
|
|
554
|
+
? `${name}.${acq.releasedBy.slice(1)}()`
|
|
555
|
+
: `${acq.releasedBy}(${name})`;
|
|
556
|
+
}
|
|
557
|
+
if (hasSyncDispose(env, env.checker.getTypeAtLocation(call))) {
|
|
558
|
+
return `${name}[Symbol.dispose]()`;
|
|
559
|
+
}
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
function tryFinallyFix(env, acq, sym, declStmt, block, call) {
|
|
563
|
+
const release = releaseText(env, acq, sym.name, call);
|
|
564
|
+
if (!release) {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
const stmts = block.statements;
|
|
568
|
+
const idx = stmts.findIndex((s) => s === declStmt);
|
|
569
|
+
if (idx < 0 || idx + 1 >= stmts.length) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
const sf = call.getSourceFile();
|
|
573
|
+
const text = sf.text;
|
|
574
|
+
const first = stmts[idx + 1];
|
|
575
|
+
const last = stmts[stmts.length - 1];
|
|
576
|
+
const declStart = declStmt.getStart(sf);
|
|
577
|
+
let lineStart = first.getStart(sf);
|
|
578
|
+
let declLineStart = declStart;
|
|
579
|
+
while (lineStart > 0 && text[lineStart - 1] !== '\n') {
|
|
580
|
+
lineStart -= 1;
|
|
581
|
+
}
|
|
582
|
+
while (declLineStart > 0 && text[declLineStart - 1] !== '\n') {
|
|
583
|
+
declLineStart -= 1;
|
|
584
|
+
}
|
|
585
|
+
const base = text.slice(declLineStart, declStart);
|
|
586
|
+
const unit = ' ';
|
|
587
|
+
const regionEnd = last.getEnd();
|
|
588
|
+
const region = text
|
|
589
|
+
.slice(lineStart, regionEnd)
|
|
590
|
+
.split('\n')
|
|
591
|
+
.map((line) => (line.length > 0 ? unit + line : line))
|
|
592
|
+
.join('\n');
|
|
593
|
+
const replacement = `${base}try {\n${region}\n${base}}\n${base}finally {\n${base}${unit}${release};\n${base}}`;
|
|
594
|
+
return {
|
|
595
|
+
title: 'Wrap in try/finally',
|
|
596
|
+
edits: [
|
|
597
|
+
{
|
|
598
|
+
fileName: sf.fileName,
|
|
599
|
+
pos: lineStart,
|
|
600
|
+
end: regionEnd,
|
|
601
|
+
newText: replacement,
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function leakDiagnostic(ctx, node, acq, why, fixes) {
|
|
607
|
+
return {
|
|
608
|
+
channel: 'resources',
|
|
609
|
+
message: `\`${acq.label}\` resource can leak — ${why}`,
|
|
610
|
+
location: locationOf(node, node.getSourceFile()),
|
|
611
|
+
related: relatedPath(ctx),
|
|
612
|
+
fixes,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function parseOwnership(channelConfig) {
|
|
616
|
+
if (typeof channelConfig !== 'object' || channelConfig === null) {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
const raw = channelConfig['ownership'];
|
|
620
|
+
if (raw === undefined) {
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
if (!Array.isArray(raw)) {
|
|
624
|
+
throw new Error('resources: "ownership" must be an array');
|
|
625
|
+
}
|
|
626
|
+
return raw.map((item, index) => {
|
|
627
|
+
if (typeof item !== 'object' || item === null) {
|
|
628
|
+
throw new Error(`resources: ownership[${index}] must be an object`);
|
|
629
|
+
}
|
|
630
|
+
const obj = item;
|
|
631
|
+
if (typeof obj['callee'] !== 'string') {
|
|
632
|
+
throw new Error(`resources: ownership[${index}].callee must be a string`);
|
|
633
|
+
}
|
|
634
|
+
const params = obj['params'];
|
|
635
|
+
if (!Array.isArray(params) ||
|
|
636
|
+
params.some((v) => typeof v !== 'number' || !Number.isInteger(v))) {
|
|
637
|
+
throw new Error(`resources: ownership[${index}].params must be an array of integers`);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
callee: obj['callee'],
|
|
641
|
+
params: params,
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function makeEnv(checker, dispatch, fn, ownership, summaryOf, resolveCall, resolveCallFor, peerSummaryValue, logDegrade) {
|
|
646
|
+
const symbols = paramSymbols(checker, fn.node);
|
|
647
|
+
const resolveExceptions = (call) => resolveCallFor('exceptions', call);
|
|
648
|
+
const peerThrows = (fnId) => {
|
|
649
|
+
const value = peerSummaryValue('exceptions', fnId);
|
|
650
|
+
return value !== undefined && !isEmpty(value);
|
|
651
|
+
};
|
|
652
|
+
return {
|
|
653
|
+
checker,
|
|
654
|
+
dispatch,
|
|
655
|
+
fn,
|
|
656
|
+
ownership,
|
|
657
|
+
summaryOf,
|
|
658
|
+
resolveCall,
|
|
659
|
+
resolveExceptions,
|
|
660
|
+
peerThrows,
|
|
661
|
+
logDegrade,
|
|
662
|
+
paramSymbols: symbols,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function ownsParam(env, sym, body) {
|
|
666
|
+
return containsMatch(body, (n) => {
|
|
667
|
+
if (ts.isCallExpression(n) || ts.isNewExpression(n)) {
|
|
668
|
+
if (ts.isCallExpression(n) &&
|
|
669
|
+
isDischargeOf(env, n, sym, undefined)) {
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
return isTransferExpr(env, n, sym);
|
|
673
|
+
}
|
|
674
|
+
if (ts.isBinaryExpression(n)) {
|
|
675
|
+
return isTransferExpr(env, n, sym);
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
function computeOwnership(env) {
|
|
681
|
+
const body = bodyOf(env.fn.node);
|
|
682
|
+
const owned = new Set();
|
|
683
|
+
if (!body) {
|
|
684
|
+
return owned;
|
|
685
|
+
}
|
|
686
|
+
for (const [sym, index] of env.paramSymbols) {
|
|
687
|
+
if (ownsParam(env, sym, body)) {
|
|
688
|
+
owned.add(index);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return owned;
|
|
692
|
+
}
|
|
693
|
+
function diagnoseAcquire(env, ctx, call, acq, out) {
|
|
694
|
+
if (acq.kind === 'pair') {
|
|
695
|
+
if (!hasMatchingRelease(env, call, acq)) {
|
|
696
|
+
out.push(leakDiagnostic(ctx, call, acq, `no matching \`${acq.releasedBy ?? 'release'}\` on every path`));
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const binding = handleBinding(env, call);
|
|
701
|
+
if (binding && binding.using) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (binding && !binding.using) {
|
|
705
|
+
const declStmt = enclosingStatement(call);
|
|
706
|
+
const block = blockOf(declStmt);
|
|
707
|
+
const body = bodyOf(env.fn.node);
|
|
708
|
+
if (!block || !body) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const outcome = handleOutcome(env, acq, binding.sym, declStmt, block, body);
|
|
712
|
+
if (outcome === 'leak') {
|
|
713
|
+
const fixes = [];
|
|
714
|
+
const asUsing = usingFix(env, call);
|
|
715
|
+
const wrapped = tryFinallyFix(env, acq, binding.sym, declStmt, block, call);
|
|
716
|
+
if (asUsing) {
|
|
717
|
+
fixes.push(asUsing);
|
|
718
|
+
}
|
|
719
|
+
if (wrapped) {
|
|
720
|
+
fixes.push(wrapped);
|
|
721
|
+
}
|
|
722
|
+
out.push(leakDiagnostic(ctx, call, acq, 'no release, transfer, or `using` on every path', fixes.length > 0 ? fixes : undefined));
|
|
723
|
+
}
|
|
724
|
+
else if (outcome === 'degrade') {
|
|
725
|
+
env.logDegrade(`${acq.label} at ${env.fn.fileName}:${call.getStart()} flows to an opaque sink`);
|
|
726
|
+
if (env.dispatch === 'pessimist') {
|
|
727
|
+
out.push({
|
|
728
|
+
channel: 'resources',
|
|
729
|
+
message: `\`${acq.label}\` resource becomes untrackable — it flows to an opaque sink and release cannot be verified`,
|
|
730
|
+
location: locationOf(call, call.getSourceFile()),
|
|
731
|
+
related: relatedPath(ctx),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function diagnoseClassFields(env, ctx, out) {
|
|
739
|
+
if (!ts.isConstructorDeclaration(env.fn.node)) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const cls = env.fn.node.parent;
|
|
743
|
+
if (!ts.isClassLike(cls)) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const ctorBody = bodyOf(env.fn.node);
|
|
747
|
+
for (const stored of classFieldAcquires(env, cls, ctorBody)) {
|
|
748
|
+
if (classDischargesField(cls, stored.field, stored.acq)) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
out.push({
|
|
752
|
+
channel: 'resources',
|
|
753
|
+
message: `\`${stored.acq.label}\` stored on \`this.${stored.field}\` can leak — class \`${className(cls)}\` has no release method that discharges it`,
|
|
754
|
+
location: locationOf(stored.node, stored.node.getSourceFile()),
|
|
755
|
+
related: relatedPath(ctx),
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const createResourcesChannel = (channelConfig, onDegrade = () => { }) => {
|
|
760
|
+
const ownership = parseOwnership(channelConfig);
|
|
761
|
+
return {
|
|
762
|
+
name: 'resources',
|
|
763
|
+
dependsOn: ['exceptions'],
|
|
764
|
+
bottom,
|
|
765
|
+
equals,
|
|
766
|
+
widen,
|
|
767
|
+
transfer(ctx) {
|
|
768
|
+
const env = makeEnv(ctx.checker, ctx.dispatch, ctx.fn, ownership, ctx.summaryOf, ctx.resolveCall, ctx.resolveCallFor, ctx.peerSummaryValue, onDegrade);
|
|
769
|
+
return {
|
|
770
|
+
value: fromParams(computeOwnership(env)),
|
|
771
|
+
fromCallbacks: new Set(),
|
|
772
|
+
};
|
|
773
|
+
},
|
|
774
|
+
diagnose(ctx) {
|
|
775
|
+
const env = makeEnv(ctx.checker, ctx.dispatch, ctx.fn, ownership, ctx.summaryOf, ctx.resolveCall, ctx.resolveCallFor, ctx.peerSummaryValue, onDegrade);
|
|
776
|
+
const out = [];
|
|
777
|
+
diagnoseClassFields(env, ctx, out);
|
|
778
|
+
const body = bodyOf(ctx.fn.node);
|
|
779
|
+
if (body) {
|
|
780
|
+
const visit = (n) => {
|
|
781
|
+
if (n !== body && isFunctionLike(n)) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (ts.isCallExpression(n) || ts.isNewExpression(n)) {
|
|
785
|
+
const acq = acquireAt(env, n);
|
|
786
|
+
if (acq) {
|
|
787
|
+
diagnoseAcquire(env, ctx, n, acq, out);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
ts.forEachChild(n, visit);
|
|
791
|
+
};
|
|
792
|
+
visit(body);
|
|
793
|
+
}
|
|
794
|
+
return out;
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
};
|
|
798
|
+
export { createResourcesChannel };
|