@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as ts from '../adapter.js';
|
|
2
2
|
import { overlayThrows } from '../exceptions/channel.js';
|
|
3
|
-
import { bodyOf,
|
|
3
|
+
import { bodyOf, paramSymbols, unwrap } from '../kernel/ast.js';
|
|
4
4
|
import { isEmpty } from '../exceptions/value.js';
|
|
5
5
|
import { isFunctionLike, locationOf } from '../kernel/ids.js';
|
|
6
6
|
import { bottom, equals, fromParams, widen, } from './value.js';
|
|
@@ -32,13 +32,13 @@ function refsSym(env, expr, sym) {
|
|
|
32
32
|
}
|
|
33
33
|
return env.checker.getSymbolAtLocation(e) === sym;
|
|
34
34
|
}
|
|
35
|
-
function containsMatch(node, pred) {
|
|
35
|
+
function containsMatch(node, pred, descendIntoFunctions = false) {
|
|
36
36
|
let found = false;
|
|
37
37
|
const rec = (n) => {
|
|
38
38
|
if (found) {
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
if (n !== node && isFunctionLike(n)) {
|
|
41
|
+
if (n !== node && !descendIntoFunctions && isFunctionLike(n)) {
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
if (pred(n)) {
|
|
@@ -67,7 +67,7 @@ function callCanThrow(env, call) {
|
|
|
67
67
|
if (res.targets.length > 0) {
|
|
68
68
|
return res.targets.some((t) => env.peerThrows(t.id));
|
|
69
69
|
}
|
|
70
|
-
return res.unresolved
|
|
70
|
+
return res.unresolved;
|
|
71
71
|
}
|
|
72
72
|
function enclosingStatement(node) {
|
|
73
73
|
let n = node;
|
|
@@ -79,24 +79,13 @@ function enclosingStatement(node) {
|
|
|
79
79
|
function blockOf(stmt) {
|
|
80
80
|
return stmt.parent && ts.isBlock(stmt.parent) ? stmt.parent : undefined;
|
|
81
81
|
}
|
|
82
|
-
function
|
|
82
|
+
function hasDispose(env, type, includeAsync) {
|
|
83
83
|
if (!type) {
|
|
84
84
|
return false;
|
|
85
85
|
}
|
|
86
86
|
for (const prop of env.checker.getPropertiesOfType(type)) {
|
|
87
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')) {
|
|
88
|
+
(includeAsync && prop.name.startsWith('__@asyncDispose'))) {
|
|
100
89
|
return true;
|
|
101
90
|
}
|
|
102
91
|
}
|
|
@@ -134,7 +123,19 @@ function acquireAt(env, call) {
|
|
|
134
123
|
};
|
|
135
124
|
}
|
|
136
125
|
}
|
|
137
|
-
|
|
126
|
+
const parent = bindingParentOf(call);
|
|
127
|
+
const target = parent.parent;
|
|
128
|
+
const isInitializer = bindingDeclOf(call) !== undefined ||
|
|
129
|
+
(ts.isPropertyDeclaration(target) && target.initializer === parent) ||
|
|
130
|
+
(ts.isBinaryExpression(target) &&
|
|
131
|
+
target.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
132
|
+
target.right === parent &&
|
|
133
|
+
ts.isPropertyAccessExpression(target.left) &&
|
|
134
|
+
target.left.expression.kind === ts.SyntaxKind.ThisKeyword);
|
|
135
|
+
if (!isInitializer) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (hasDispose(env, env.checker.getTypeAtLocation(call), true)) {
|
|
138
139
|
return {
|
|
139
140
|
kind: 'handle',
|
|
140
141
|
releasedBy: undefined,
|
|
@@ -144,19 +145,24 @@ function acquireAt(env, call) {
|
|
|
144
145
|
}
|
|
145
146
|
return undefined;
|
|
146
147
|
}
|
|
148
|
+
function bindingDeclOf(call) {
|
|
149
|
+
const parent = bindingParentOf(call).parent;
|
|
150
|
+
return ts.isVariableDeclaration(parent) ? parent : undefined;
|
|
151
|
+
}
|
|
152
|
+
function bindingParentOf(call) {
|
|
153
|
+
let parent = call;
|
|
154
|
+
while (parent.parent &&
|
|
155
|
+
(ts.isParenthesizedExpression(parent.parent) ||
|
|
156
|
+
ts.isNonNullExpression(parent.parent) ||
|
|
157
|
+
ts.isAsExpression(parent.parent) ||
|
|
158
|
+
ts.isAwaitExpression(parent.parent))) {
|
|
159
|
+
parent = parent.parent;
|
|
160
|
+
}
|
|
161
|
+
return parent;
|
|
162
|
+
}
|
|
147
163
|
function handleBinding(env, call) {
|
|
148
|
-
|
|
149
|
-
|
|
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)) {
|
|
164
|
+
const decl = bindingDeclOf(call);
|
|
165
|
+
if (!decl || !ts.isIdentifier(decl.name)) {
|
|
160
166
|
return undefined;
|
|
161
167
|
}
|
|
162
168
|
const list = decl.parent;
|
|
@@ -168,7 +174,7 @@ function handleBinding(env, call) {
|
|
|
168
174
|
const sym = env.checker.getSymbolAtLocation(decl.name);
|
|
169
175
|
return sym ? { using: false, sym } : undefined;
|
|
170
176
|
}
|
|
171
|
-
function
|
|
177
|
+
function isDischargeCall(expr, acq, refs) {
|
|
172
178
|
if (!ts.isCallExpression(expr)) {
|
|
173
179
|
return false;
|
|
174
180
|
}
|
|
@@ -182,10 +188,10 @@ function isDischargeOf(env, expr, sym, acq) {
|
|
|
182
188
|
return false;
|
|
183
189
|
}
|
|
184
190
|
const arg = expr.arguments?.[0];
|
|
185
|
-
return arg ?
|
|
191
|
+
return arg ? refs(arg) : false;
|
|
186
192
|
}
|
|
187
193
|
if (ts.isPropertyAccessExpression(callee)) {
|
|
188
|
-
if (!
|
|
194
|
+
if (!refs(callee.expression)) {
|
|
189
195
|
return false;
|
|
190
196
|
}
|
|
191
197
|
const method = callee.name.text;
|
|
@@ -197,7 +203,7 @@ function isDischargeOf(env, expr, sym, acq) {
|
|
|
197
203
|
acq.releasedBy.slice(1) === method);
|
|
198
204
|
}
|
|
199
205
|
if (ts.isElementAccessExpression(callee)) {
|
|
200
|
-
if (!
|
|
206
|
+
if (!refs(callee.expression)) {
|
|
201
207
|
return false;
|
|
202
208
|
}
|
|
203
209
|
const argx = callee.argumentExpression;
|
|
@@ -207,6 +213,9 @@ function isDischargeOf(env, expr, sym, acq) {
|
|
|
207
213
|
}
|
|
208
214
|
return false;
|
|
209
215
|
}
|
|
216
|
+
function isDischargeOf(env, expr, sym, acq) {
|
|
217
|
+
return isDischargeCall(expr, acq, (e) => refsSym(env, e, sym));
|
|
218
|
+
}
|
|
210
219
|
function callOwnsArg(env, call, sym) {
|
|
211
220
|
const args = call.arguments ? Array.from(call.arguments) : [];
|
|
212
221
|
const indices = [];
|
|
@@ -218,13 +227,6 @@ function callOwnsArg(env, call, sym) {
|
|
|
218
227
|
if (indices.length === 0) {
|
|
219
228
|
return false;
|
|
220
229
|
}
|
|
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
230
|
const res = env.resolveCall(call);
|
|
229
231
|
const entry = (res.overlay?.entry ?? undefined);
|
|
230
232
|
if (entry && Array.isArray(entry.ownsParams)) {
|
|
@@ -273,8 +275,9 @@ function statementDischargesOrTransfers(env, s, sym, acq) {
|
|
|
273
275
|
}
|
|
274
276
|
return false;
|
|
275
277
|
}
|
|
276
|
-
function finallyDischarges(env, sym, acq, declStmt, body) {
|
|
278
|
+
function finallyDischarges(env, sym, acq, declStmt, body, sf) {
|
|
277
279
|
let found = false;
|
|
280
|
+
const declStart = declStmt.getStart(sf);
|
|
278
281
|
const rec = (n) => {
|
|
279
282
|
if (found) {
|
|
280
283
|
return;
|
|
@@ -284,7 +287,7 @@ function finallyDischarges(env, sym, acq, declStmt, body) {
|
|
|
284
287
|
}
|
|
285
288
|
if (ts.isTryStatement(n) &&
|
|
286
289
|
n.finallyBlock &&
|
|
287
|
-
|
|
290
|
+
declStart < n.finallyBlock.getStart(sf)) {
|
|
288
291
|
const discharged = containsMatch(n.finallyBlock, (m) => (ts.isCallExpression(m) || ts.isNewExpression(m)) &&
|
|
289
292
|
isDischargeOf(env, m, sym, acq));
|
|
290
293
|
if (discharged) {
|
|
@@ -356,7 +359,7 @@ function opaqueUseOf(env, sym, body) {
|
|
|
356
359
|
return hit;
|
|
357
360
|
}
|
|
358
361
|
function handleOutcome(env, acq, sym, declStmt, block, body) {
|
|
359
|
-
if (finallyDischarges(env, sym, acq, declStmt, body)) {
|
|
362
|
+
if (finallyDischarges(env, sym, acq, declStmt, body, env.fn.sourceFile)) {
|
|
360
363
|
return 'safe';
|
|
361
364
|
}
|
|
362
365
|
if (firstGuaranteed(env, sym, acq, declStmt, block)) {
|
|
@@ -367,7 +370,7 @@ function handleOutcome(env, acq, sym, declStmt, block, body) {
|
|
|
367
370
|
}
|
|
368
371
|
return 'leak';
|
|
369
372
|
}
|
|
370
|
-
function isReleaseCallForPair(node, acq, acquireCall) {
|
|
373
|
+
function isReleaseCallForPair(node, acq, acquireCall, acquireExpressionText) {
|
|
371
374
|
if (!ts.isCallExpression(node) || acq.releasedBy === undefined) {
|
|
372
375
|
return false;
|
|
373
376
|
}
|
|
@@ -380,7 +383,8 @@ function isReleaseCallForPair(node, acq, acquireCall) {
|
|
|
380
383
|
if (callee.name.text !== acq.releasedBy) {
|
|
381
384
|
return false;
|
|
382
385
|
}
|
|
383
|
-
if (
|
|
386
|
+
if (acquireExpressionText === undefined ||
|
|
387
|
+
callee.expression.getText() !== acquireExpressionText) {
|
|
384
388
|
return false;
|
|
385
389
|
}
|
|
386
390
|
for (const i of acq.pairKey ?? []) {
|
|
@@ -397,7 +401,11 @@ function hasMatchingRelease(env, acquireCall, acq) {
|
|
|
397
401
|
if (!body) {
|
|
398
402
|
return false;
|
|
399
403
|
}
|
|
400
|
-
|
|
404
|
+
const acqCallee = acquireCall.expression;
|
|
405
|
+
const acquireExpressionText = ts.isPropertyAccessExpression(acqCallee)
|
|
406
|
+
? acqCallee.expression.getText()
|
|
407
|
+
: undefined;
|
|
408
|
+
return containsMatch(body, (n) => isReleaseCallForPair(n, acq, acquireCall, acquireExpressionText), true);
|
|
401
409
|
}
|
|
402
410
|
function classDischargesField(cls, field, acq) {
|
|
403
411
|
const members = cls.members ?? [];
|
|
@@ -407,39 +415,7 @@ function classDischargesField(cls, field, acq) {
|
|
|
407
415
|
e.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
408
416
|
e.name.text === field);
|
|
409
417
|
};
|
|
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
|
-
};
|
|
418
|
+
const dischargesHere = (n) => ts.isCallExpression(n) && isDischargeCall(n, acq, refsThisField);
|
|
443
419
|
for (const member of members) {
|
|
444
420
|
const body = member.body;
|
|
445
421
|
if (body && containsMatch(body, dischargesHere)) {
|
|
@@ -494,30 +470,12 @@ function classFieldAcquires(env, cls, ctorBody) {
|
|
|
494
470
|
}
|
|
495
471
|
return out;
|
|
496
472
|
}
|
|
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
473
|
function usingFix(env, call) {
|
|
508
|
-
if (!
|
|
474
|
+
if (!hasDispose(env, env.checker.getTypeAtLocation(call), false)) {
|
|
509
475
|
return undefined;
|
|
510
476
|
}
|
|
511
|
-
|
|
512
|
-
|
|
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)) {
|
|
477
|
+
const decl = bindingDeclOf(call);
|
|
478
|
+
if (!decl) {
|
|
521
479
|
return undefined;
|
|
522
480
|
}
|
|
523
481
|
const list = decl.parent;
|
|
@@ -554,7 +512,7 @@ function releaseText(env, acq, name, call) {
|
|
|
554
512
|
? `${name}.${acq.releasedBy.slice(1)}()`
|
|
555
513
|
: `${acq.releasedBy}(${name})`;
|
|
556
514
|
}
|
|
557
|
-
if (
|
|
515
|
+
if (hasDispose(env, env.checker.getTypeAtLocation(call), false)) {
|
|
558
516
|
return `${name}[Symbol.dispose]()`;
|
|
559
517
|
}
|
|
560
518
|
return undefined;
|
|
@@ -603,47 +561,16 @@ function tryFinallyFix(env, acq, sym, declStmt, block, call) {
|
|
|
603
561
|
],
|
|
604
562
|
};
|
|
605
563
|
}
|
|
606
|
-
function leakDiagnostic(
|
|
564
|
+
function leakDiagnostic(node, acq, why, fixes) {
|
|
607
565
|
return {
|
|
608
566
|
channel: 'resources',
|
|
609
567
|
message: `\`${acq.label}\` resource can leak — ${why}`,
|
|
610
568
|
location: locationOf(node, node.getSourceFile()),
|
|
611
|
-
related:
|
|
569
|
+
related: [],
|
|
612
570
|
fixes,
|
|
613
571
|
};
|
|
614
572
|
}
|
|
615
|
-
function
|
|
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);
|
|
573
|
+
function makeEnv(checker, fn, summaryOf, resolveCall, resolveCallFor, peerSummaryValue, logDegrade, includeParamSymbols) {
|
|
647
574
|
const resolveExceptions = (call) => resolveCallFor('exceptions', call);
|
|
648
575
|
const peerThrows = (fnId) => {
|
|
649
576
|
const value = peerSummaryValue('exceptions', fnId);
|
|
@@ -651,15 +578,13 @@ function makeEnv(checker, dispatch, fn, ownership, summaryOf, resolveCall, resol
|
|
|
651
578
|
};
|
|
652
579
|
return {
|
|
653
580
|
checker,
|
|
654
|
-
dispatch,
|
|
655
581
|
fn,
|
|
656
|
-
ownership,
|
|
657
582
|
summaryOf,
|
|
658
583
|
resolveCall,
|
|
659
584
|
resolveExceptions,
|
|
660
585
|
peerThrows,
|
|
661
586
|
logDegrade,
|
|
662
|
-
paramSymbols:
|
|
587
|
+
paramSymbols: includeParamSymbols ? paramSymbols(checker, fn.node) : undefined,
|
|
663
588
|
};
|
|
664
589
|
}
|
|
665
590
|
function ownsParam(env, sym, body) {
|
|
@@ -683,17 +608,17 @@ function computeOwnership(env) {
|
|
|
683
608
|
if (!body) {
|
|
684
609
|
return owned;
|
|
685
610
|
}
|
|
686
|
-
for (const [sym, index] of env.paramSymbols) {
|
|
611
|
+
for (const [sym, index] of env.paramSymbols ?? []) {
|
|
687
612
|
if (ownsParam(env, sym, body)) {
|
|
688
613
|
owned.add(index);
|
|
689
614
|
}
|
|
690
615
|
}
|
|
691
616
|
return owned;
|
|
692
617
|
}
|
|
693
|
-
function diagnoseAcquire(env,
|
|
618
|
+
function diagnoseAcquire(env, call, acq, out) {
|
|
694
619
|
if (acq.kind === 'pair') {
|
|
695
620
|
if (!hasMatchingRelease(env, call, acq)) {
|
|
696
|
-
out.push(leakDiagnostic(
|
|
621
|
+
out.push(leakDiagnostic(call, acq, `no matching \`${acq.releasedBy ?? 'release'}\` on every path`));
|
|
697
622
|
}
|
|
698
623
|
return;
|
|
699
624
|
}
|
|
@@ -719,23 +644,21 @@ function diagnoseAcquire(env, ctx, call, acq, out) {
|
|
|
719
644
|
if (wrapped) {
|
|
720
645
|
fixes.push(wrapped);
|
|
721
646
|
}
|
|
722
|
-
out.push(leakDiagnostic(
|
|
647
|
+
out.push(leakDiagnostic(call, acq, 'no release, transfer, or `using` on every path', fixes.length > 0 ? fixes : undefined));
|
|
723
648
|
}
|
|
724
649
|
else if (outcome === 'degrade') {
|
|
725
650
|
env.logDegrade(`${acq.label} at ${env.fn.fileName}:${call.getStart()} flows to an opaque sink`);
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
});
|
|
733
|
-
}
|
|
651
|
+
out.push({
|
|
652
|
+
channel: 'resources',
|
|
653
|
+
message: `\`${acq.label}\` resource becomes untrackable — it flows to an opaque sink and release cannot be verified`,
|
|
654
|
+
location: locationOf(call, call.getSourceFile()),
|
|
655
|
+
related: [],
|
|
656
|
+
});
|
|
734
657
|
}
|
|
735
658
|
return;
|
|
736
659
|
}
|
|
737
660
|
}
|
|
738
|
-
function diagnoseClassFields(env,
|
|
661
|
+
function diagnoseClassFields(env, out) {
|
|
739
662
|
if (!ts.isConstructorDeclaration(env.fn.node)) {
|
|
740
663
|
return;
|
|
741
664
|
}
|
|
@@ -752,12 +675,11 @@ function diagnoseClassFields(env, ctx, out) {
|
|
|
752
675
|
channel: 'resources',
|
|
753
676
|
message: `\`${stored.acq.label}\` stored on \`this.${stored.field}\` can leak — class \`${className(cls)}\` has no release method that discharges it`,
|
|
754
677
|
location: locationOf(stored.node, stored.node.getSourceFile()),
|
|
755
|
-
related:
|
|
678
|
+
related: [],
|
|
756
679
|
});
|
|
757
680
|
}
|
|
758
681
|
}
|
|
759
|
-
const createResourcesChannel = (
|
|
760
|
-
const ownership = parseOwnership(channelConfig);
|
|
682
|
+
const createResourcesChannel = (onDegrade = () => { }) => {
|
|
761
683
|
return {
|
|
762
684
|
name: 'resources',
|
|
763
685
|
dependsOn: ['exceptions'],
|
|
@@ -765,16 +687,16 @@ const createResourcesChannel = (channelConfig, onDegrade = () => { }) => {
|
|
|
765
687
|
equals,
|
|
766
688
|
widen,
|
|
767
689
|
transfer(ctx) {
|
|
768
|
-
const env = makeEnv(ctx.checker, ctx.
|
|
690
|
+
const env = makeEnv(ctx.checker, ctx.fn, ctx.summaryOf, ctx.resolveCall, ctx.resolveCallFor, ctx.peerSummaryValue, onDegrade, true);
|
|
769
691
|
return {
|
|
770
692
|
value: fromParams(computeOwnership(env)),
|
|
771
693
|
fromCallbacks: new Set(),
|
|
772
694
|
};
|
|
773
695
|
},
|
|
774
696
|
diagnose(ctx) {
|
|
775
|
-
const env = makeEnv(ctx.checker, ctx.
|
|
697
|
+
const env = makeEnv(ctx.checker, ctx.fn, ctx.summaryOf, ctx.resolveCall, ctx.resolveCallFor, ctx.peerSummaryValue, onDegrade, false);
|
|
776
698
|
const out = [];
|
|
777
|
-
diagnoseClassFields(env,
|
|
699
|
+
diagnoseClassFields(env, out);
|
|
778
700
|
const body = bodyOf(ctx.fn.node);
|
|
779
701
|
if (body) {
|
|
780
702
|
const visit = (n) => {
|
|
@@ -784,7 +706,7 @@ const createResourcesChannel = (channelConfig, onDegrade = () => { }) => {
|
|
|
784
706
|
if (ts.isCallExpression(n) || ts.isNewExpression(n)) {
|
|
785
707
|
const acq = acquireAt(env, n);
|
|
786
708
|
if (acq) {
|
|
787
|
-
diagnoseAcquire(env,
|
|
709
|
+
diagnoseAcquire(env, n, acq, out);
|
|
788
710
|
}
|
|
789
711
|
}
|
|
790
712
|
ts.forEachChild(n, visit);
|
|
@@ -3,7 +3,6 @@ type ResourcesValue = {
|
|
|
3
3
|
};
|
|
4
4
|
declare function bottom(): ResourcesValue;
|
|
5
5
|
declare function fromParams(indices: Iterable<number>): ResourcesValue;
|
|
6
|
-
declare function join(a: ResourcesValue, b: ResourcesValue): ResourcesValue;
|
|
7
6
|
declare function equals(a: ResourcesValue, b: ResourcesValue): boolean;
|
|
8
7
|
declare function widen(_prev: ResourcesValue, next: ResourcesValue, _round: number): ResourcesValue;
|
|
9
|
-
export { bottom, equals, fromParams,
|
|
8
|
+
export { bottom, equals, fromParams, type ResourcesValue, widen };
|
|
@@ -6,19 +6,6 @@ function fromParams(indices) {
|
|
|
6
6
|
const set = new Set(indices);
|
|
7
7
|
return { ownsParams: set.size === 0 ? EMPTY : set };
|
|
8
8
|
}
|
|
9
|
-
function join(a, b) {
|
|
10
|
-
if (a.ownsParams.size === 0) {
|
|
11
|
-
return b;
|
|
12
|
-
}
|
|
13
|
-
if (b.ownsParams.size === 0) {
|
|
14
|
-
return a;
|
|
15
|
-
}
|
|
16
|
-
const set = new Set(a.ownsParams);
|
|
17
|
-
for (const i of b.ownsParams) {
|
|
18
|
-
set.add(i);
|
|
19
|
-
}
|
|
20
|
-
return { ownsParams: set };
|
|
21
|
-
}
|
|
22
9
|
function equals(a, b) {
|
|
23
10
|
if (a.ownsParams.size !== b.ownsParams.size) {
|
|
24
11
|
return false;
|
|
@@ -33,4 +20,4 @@ function equals(a, b) {
|
|
|
33
20
|
function widen(_prev, next, _round) {
|
|
34
21
|
return next;
|
|
35
22
|
}
|
|
36
|
-
export { bottom, equals, fromParams,
|
|
23
|
+
export { bottom, equals, fromParams, widen };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { DiagnosticSeverity } from 'vscode-languageserver/node';
|
|
2
2
|
import type { CodeAction, Diagnostic as LspDiagnostic, Hover } from 'vscode-languageserver/node';
|
|
3
3
|
import type { TextDocument } from 'vscode-languageserver-textdocument';
|
|
4
|
-
import type { Diagnostic as AnalyzeDiagnostic } from '../
|
|
4
|
+
import type { Diagnostic as AnalyzeDiagnostic } from '../guard/kernel/types.js';
|
|
5
5
|
export type DocumentResolver = (fileName: string) => TextDocument | undefined;
|
|
6
6
|
declare function toLspDiagnostic(diagnostic: AnalyzeDiagnostic, severity: DiagnosticSeverity, resolve: DocumentResolver, uriOf: (fileName: string) => string): LspDiagnostic;
|
|
7
|
-
declare function groupByFile(diagnostics: ReadonlyArray<AnalyzeDiagnostic>,
|
|
7
|
+
declare function groupByFile(diagnostics: ReadonlyArray<AnalyzeDiagnostic>, severityOf: (channel: string) => DiagnosticSeverity, resolve: DocumentResolver, uriOf: (fileName: string) => string): Map<string, LspDiagnostic[]>;
|
|
8
8
|
declare function hoverAt(diagnostics: ReadonlyArray<AnalyzeDiagnostic>, offset: number, document: TextDocument): Hover | undefined;
|
|
9
9
|
declare function codeActionsAt(diagnostics: ReadonlyArray<AnalyzeDiagnostic>, startOffset: number, endOffset: number, document: TextDocument, uriOf: (fileName: string) => string): CodeAction[];
|
|
10
10
|
export { codeActionsAt, DiagnosticSeverity, groupByFile, hoverAt, toLspDiagnostic };
|
package/build/lsp/diagnostics.js
CHANGED
|
@@ -22,7 +22,7 @@ function toLspDiagnostic(diagnostic, severity, resolve, uriOf) {
|
|
|
22
22
|
source: 'analyze',
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
function groupByFile(diagnostics,
|
|
25
|
+
function groupByFile(diagnostics, severityOf, resolve, uriOf) {
|
|
26
26
|
let grouped = new Map();
|
|
27
27
|
for (let i = 0, n = diagnostics.length; i < n; i++) {
|
|
28
28
|
let diagnostic = diagnostics[i], fileName = diagnostic.location.fileName, list = grouped.get(fileName);
|
|
@@ -30,7 +30,7 @@ function groupByFile(diagnostics, severity, resolve, uriOf) {
|
|
|
30
30
|
list = [];
|
|
31
31
|
grouped.set(fileName, list);
|
|
32
32
|
}
|
|
33
|
-
list.push(toLspDiagnostic(diagnostic,
|
|
33
|
+
list.push(toLspDiagnostic(diagnostic, severityOf(diagnostic.channel), resolve, uriOf));
|
|
34
34
|
}
|
|
35
35
|
return grouped;
|
|
36
36
|
}
|
package/build/lsp/server.js
CHANGED
|
@@ -9,6 +9,15 @@ function pathKey(fileName) {
|
|
|
9
9
|
let resolved = NodePath.resolve(fileName);
|
|
10
10
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
11
11
|
}
|
|
12
|
+
function lspSeverity(severity) {
|
|
13
|
+
if (severity === 'warn') {
|
|
14
|
+
return DiagnosticSeverity.Warning;
|
|
15
|
+
}
|
|
16
|
+
if (severity === 'info') {
|
|
17
|
+
return DiagnosticSeverity.Information;
|
|
18
|
+
}
|
|
19
|
+
return DiagnosticSeverity.Error;
|
|
20
|
+
}
|
|
12
21
|
function findTsconfig(rootDir) {
|
|
13
22
|
let candidate = NodePath.join(rootDir, 'tsconfig.json');
|
|
14
23
|
return NodeFS.existsSync(candidate) ? candidate : undefined;
|
|
@@ -19,12 +28,38 @@ function rootFromInitialize(params) {
|
|
|
19
28
|
}
|
|
20
29
|
function createServer(connection) {
|
|
21
30
|
let analyzed = new Map(), documents = new TextDocuments(TextDocument), pending = new Set(), published = new Set(), timer, workspace;
|
|
31
|
+
function publishConfigError() {
|
|
32
|
+
if (!workspace) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let uri = pathToFileURL(workspace.configPath).toString();
|
|
36
|
+
if (workspace.configError) {
|
|
37
|
+
connection.sendDiagnostics({
|
|
38
|
+
diagnostics: [{
|
|
39
|
+
message: workspace.configError,
|
|
40
|
+
range: { end: { character: 0, line: 0 }, start: { character: 0, line: 0 } },
|
|
41
|
+
severity: DiagnosticSeverity.Error,
|
|
42
|
+
source: 'analyze',
|
|
43
|
+
}],
|
|
44
|
+
uri,
|
|
45
|
+
});
|
|
46
|
+
published.add(uri);
|
|
47
|
+
}
|
|
48
|
+
else if (published.has(uri)) {
|
|
49
|
+
connection.sendDiagnostics({ diagnostics: [], uri });
|
|
50
|
+
published.delete(uri);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
22
53
|
function runAnalysis() {
|
|
23
54
|
timer = undefined;
|
|
24
|
-
if (!workspace
|
|
55
|
+
if (!workspace) {
|
|
25
56
|
return;
|
|
26
57
|
}
|
|
27
|
-
|
|
58
|
+
publishConfigError();
|
|
59
|
+
if (!workspace.config) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let config = workspace.config, changed = [...pending];
|
|
28
63
|
pending.clear();
|
|
29
64
|
try {
|
|
30
65
|
workspace.refresh(changed);
|
|
@@ -46,7 +81,10 @@ function createServer(connection) {
|
|
|
46
81
|
}
|
|
47
82
|
list.push(diagnostic);
|
|
48
83
|
}
|
|
49
|
-
let openByPath = new Map(documents.all().map((document) => [pathKey(fileURLToPath(document.uri)), document])), resolve = (fileName) => openByPath.get(pathKey(fileName)),
|
|
84
|
+
let openByPath = new Map(documents.all().map((document) => [pathKey(fileURLToPath(document.uri)), document])), resolve = (fileName) => openByPath.get(pathKey(fileName)), severityOf = (channel) => lspSeverity(config.channels[channel]?.severity), uriOf = (fileName) => openByPath.get(pathKey(fileName))?.uri ?? pathToFileURL(fileName).toString(), grouped = groupByFile(result.diagnostics, severityOf, resolve, uriOf), next = new Set();
|
|
85
|
+
if (workspace.configError) {
|
|
86
|
+
next.add(pathToFileURL(workspace.configPath).toString());
|
|
87
|
+
}
|
|
50
88
|
for (let [fileName, diagnostics] of grouped) {
|
|
51
89
|
let uri = uriOf(fileName);
|
|
52
90
|
next.add(uri);
|
package/build/lsp/workspace.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { AnalyzeResult } from '../
|
|
2
|
-
import type { AnalyzeConfig } from '../
|
|
1
|
+
import type { AnalyzeResult } from '../guard/kernel/analyze.js';
|
|
2
|
+
import type { AnalyzeConfig } from '../guard/kernel/types.js';
|
|
3
3
|
declare class AnalyzeWorkspace {
|
|
4
4
|
private api;
|
|
5
|
-
|
|
5
|
+
readonly configPath: string;
|
|
6
6
|
private snapshot;
|
|
7
|
+
private throwIndex;
|
|
7
8
|
config: AnalyzeConfig | undefined;
|
|
9
|
+
configError: string | undefined;
|
|
8
10
|
constructor(tsconfigPath: string);
|
|
11
|
+
private loadConfig;
|
|
9
12
|
reloadConfig(): void;
|
|
10
13
|
refresh(changed: ReadonlyArray<string>): void;
|
|
11
14
|
analyze(): AnalyzeResult | undefined;
|