carrick 0.3.104 → 0.3.106
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/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.d.ts +13 -0
- package/sidecar/dist/src/capture/anchors.js +229 -37
- package/sidecar/dist/src/capture/api.d.ts +64 -4
- package/sidecar/dist/src/capture/check-classify.d.ts +10 -1
- package/sidecar/dist/src/capture/check-classify.js +23 -12
- package/sidecar/dist/src/capture/check-fields.js +4 -6
- package/sidecar/dist/src/capture/check-probe.js +16 -6
- package/sidecar/dist/src/capture/check-scrub.d.ts +3 -0
- package/sidecar/dist/src/capture/check-scrub.js +8 -4
- package/sidecar/dist/src/capture/check.js +75 -49
- package/sidecar/dist/src/capture/deep-walk.d.ts +20 -0
- package/sidecar/dist/src/capture/deep-walk.js +51 -11
- package/sidecar/dist/src/capture/guarded-fs.d.ts +5 -1
- package/sidecar/dist/src/capture/guarded-fs.js +23 -1
- package/sidecar/dist/src/capture/index.js +14 -2
- package/sidecar/dist/src/capture/member-name.d.ts +20 -0
- package/sidecar/dist/src/capture/member-name.js +24 -0
- package/sidecar/dist/src/capture/self-check.js +50 -9
- package/sidecar/dist/src/capture/service-config.d.ts +2 -0
- package/sidecar/dist/src/capture/service-config.js +1 -1
- package/sidecar/dist/src/failure-path.d.ts +67 -0
- package/sidecar/dist/src/failure-path.js +236 -0
- package/sidecar/dist/src/printed-names.d.ts +43 -0
- package/sidecar/dist/src/printed-names.js +186 -0
- package/sidecar/dist/src/retype.js +60 -99
- package/sidecar/dist/src/type-inferrer.d.ts +35 -3
- package/sidecar/dist/src/type-inferrer.js +208 -13
- package/sidecar/dist/src/type-structural-expander.js +10 -1
- package/sidecar/dist/src/types.d.ts +24 -0
- package/sidecar/dist/src/validators.d.ts +76 -0
- package/sidecar/dist/src/validators.js +8 -0
|
@@ -34,7 +34,7 @@ import * as fs from 'node:fs';
|
|
|
34
34
|
import * as path from 'node:path';
|
|
35
35
|
import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
|
|
36
36
|
import { repairDanglingImports } from './repair-dangling.js';
|
|
37
|
-
import { findDisqualifyingTopTypes, provenanceOf, } from './deep-walk.js';
|
|
37
|
+
import { findDisqualifyingTopTypes, findUnresolvedPlaceholders, isErrorPlaceholder, provenanceOf, unemittedModuleProvenance, unresolvedInTreeProvenance, } from './deep-walk.js';
|
|
38
38
|
export function selfCheckStub(args) {
|
|
39
39
|
const typesDir = path.join(args.stubDir, 'types');
|
|
40
40
|
const treeFiles = [];
|
|
@@ -104,6 +104,7 @@ function runSelfCheck(args, treeFiles, repaired) {
|
|
|
104
104
|
const emptyFailures = () => ({
|
|
105
105
|
externalPinned: new Set(),
|
|
106
106
|
internal: new Set(),
|
|
107
|
+
unfoundNames: new Set(),
|
|
107
108
|
});
|
|
108
109
|
const bucketIn = (map, key) => {
|
|
109
110
|
let entry = map.get(key);
|
|
@@ -113,11 +114,24 @@ function runSelfCheck(args, treeFiles, repaired) {
|
|
|
113
114
|
}
|
|
114
115
|
return entry;
|
|
115
116
|
};
|
|
117
|
+
// A surface diagnostic outside every alias statement (a file-level import,
|
|
118
|
+
// a reference directive) is attributable to no alias and keeps the
|
|
119
|
+
// service-wide file bucket: soundness over precision, the same fallback
|
|
120
|
+
// check-poison.ts makes.
|
|
121
|
+
const bucketFor = (abs, start) => {
|
|
122
|
+
const owner = abs === surfaceAbs ? aliasAtSurfacePosition(start) : undefined;
|
|
123
|
+
return owner ? bucketIn(surfaceFailuresByAlias, owner) : bucketIn(failuresByFile, abs);
|
|
124
|
+
};
|
|
116
125
|
for (const d of diagnostics) {
|
|
117
126
|
if (!d.file)
|
|
118
127
|
continue;
|
|
119
128
|
const abs = path.resolve(d.file.fileName);
|
|
120
129
|
const msg = ts.flattenDiagnosticMessageText(d.messageText, ' ');
|
|
130
|
+
// A name the compiler cannot find is the placeholder's cause wherever a
|
|
131
|
+
// type uses it; the record names it beside those positions (carrick#1446).
|
|
132
|
+
const unfound = /Cannot find (?:name|namespace) '([^']+)'/.exec(msg);
|
|
133
|
+
if (unfound)
|
|
134
|
+
bucketFor(abs, d.start).unfoundNames.add(unfound[1]);
|
|
121
135
|
// A name the repair did not reach: the import that bound it is gone, so
|
|
122
136
|
// the module is no longer reported missing and only this diagnostic is
|
|
123
137
|
// left to say the tree is incomplete. Blamed on the specifier that bound
|
|
@@ -133,14 +147,7 @@ function runSelfCheck(args, treeFiles, repaired) {
|
|
|
133
147
|
if (!m)
|
|
134
148
|
continue;
|
|
135
149
|
const spec = m[1];
|
|
136
|
-
|
|
137
|
-
// a reference directive) is attributable to no alias and keeps the
|
|
138
|
-
// service-wide file bucket: soundness over precision, the same fallback
|
|
139
|
-
// check-poison.ts makes.
|
|
140
|
-
const owner = abs === surfaceAbs ? aliasAtSurfacePosition(d.start) : undefined;
|
|
141
|
-
const bucket = owner
|
|
142
|
-
? bucketIn(surfaceFailuresByAlias, owner)
|
|
143
|
-
: bucketIn(failuresByFile, abs);
|
|
150
|
+
const bucket = bucketFor(abs, d.start);
|
|
144
151
|
if (!isRelative(spec) && args.pinned[packageNameOf(spec)]) {
|
|
145
152
|
bucket.externalPinned.add(spec);
|
|
146
153
|
}
|
|
@@ -240,6 +247,13 @@ function buildSurfaceSpanIndex(surfaceSource) {
|
|
|
240
247
|
return spans.find((span) => position >= span.start && position <= span.end)?.alias;
|
|
241
248
|
};
|
|
242
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* carrick#1842: a literal anchor whose text is a body read as raw text passes
|
|
252
|
+
* that mark to its record, where the check phase reads it.
|
|
253
|
+
*/
|
|
254
|
+
function rawTextMark(request) {
|
|
255
|
+
return request.kind === 'literal' && request.raw_text_read ? { raw_text_read: true } : {};
|
|
256
|
+
}
|
|
243
257
|
/** An alias that never reached a capture-native tier: the failure reason was
|
|
244
258
|
* recorded at demotion time; the surface line is `unknown` by construction. */
|
|
245
259
|
function demotedRecord(anchor) {
|
|
@@ -254,12 +268,24 @@ function demotedRecord(anchor) {
|
|
|
254
268
|
self_check_detail: anchor.failureReason,
|
|
255
269
|
capture_failure_reason: anchor.failureReason,
|
|
256
270
|
top_type_at_self_check: true,
|
|
271
|
+
// A literal's text is what the index serves for it, and a module it
|
|
272
|
+
// names never reached the tree (carrick#1446). A symbol anchor's served
|
|
273
|
+
// declaration comes from elsewhere, so its demotion says nothing about it.
|
|
274
|
+
...(anchor.request.kind === 'literal' && anchor.namesUnemittedModule
|
|
275
|
+
? { unresolved_in_tree: [unemittedModuleProvenance()] }
|
|
276
|
+
: {}),
|
|
277
|
+
...rawTextMark(anchor.request),
|
|
257
278
|
};
|
|
258
279
|
}
|
|
259
280
|
function checkedRecord(anchor, ctx) {
|
|
260
281
|
const alias = anchor.request.alias;
|
|
261
282
|
let topType = true;
|
|
262
283
|
let deepFindings = [];
|
|
284
|
+
// Where the alias's type holds the unresolved-reference placeholder in this
|
|
285
|
+
// tree (carrick#1446): `''` for its own type. The deep walk leaves the
|
|
286
|
+
// placeholder out because the check heals a pinned external; what the index
|
|
287
|
+
// publishes is this tree, so the record says where it does not resolve.
|
|
288
|
+
let unresolvedPaths = [];
|
|
263
289
|
const seeds = [];
|
|
264
290
|
if (ctx.surfaceSource) {
|
|
265
291
|
for (const stmt of ctx.surfaceSource.statements) {
|
|
@@ -270,6 +296,10 @@ function checkedRecord(anchor, ctx) {
|
|
|
270
296
|
(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) !== 0;
|
|
271
297
|
if (!topType) {
|
|
272
298
|
deepFindings = findDisqualifyingTopTypes(type, ctx.program, ctx.checker, stmt.name);
|
|
299
|
+
unresolvedPaths = findUnresolvedPlaceholders(type, ctx.program, ctx.checker, stmt.name);
|
|
300
|
+
}
|
|
301
|
+
else if (isErrorPlaceholder(type)) {
|
|
302
|
+
unresolvedPaths = [''];
|
|
273
303
|
}
|
|
274
304
|
// Seed the closure with the alias's own import-type targets.
|
|
275
305
|
const visit = (node) => {
|
|
@@ -300,6 +330,7 @@ function checkedRecord(anchor, ctx) {
|
|
|
300
330
|
let blamedExternal;
|
|
301
331
|
let internalFailure;
|
|
302
332
|
const danglingSpecifiers = new Set();
|
|
333
|
+
const unfoundNames = new Set();
|
|
303
334
|
// This alias's own surface statement, then the closure's files. The surface
|
|
304
335
|
// file bucket now holds only the diagnostics no alias statement covers.
|
|
305
336
|
const closureFailures = [
|
|
@@ -315,6 +346,8 @@ function checkedRecord(anchor, ctx) {
|
|
|
315
346
|
internalFailure = [...failures.internal][0];
|
|
316
347
|
for (const specifier of failures.internal)
|
|
317
348
|
danglingSpecifiers.add(specifier);
|
|
349
|
+
for (const name of failures.unfoundNames)
|
|
350
|
+
unfoundNames.add(name);
|
|
318
351
|
}
|
|
319
352
|
// Classification consults the closure failures REGARDLESS of the root
|
|
320
353
|
// type: a dangling internal specifier means part of this alias's closure
|
|
@@ -416,6 +449,14 @@ function checkedRecord(anchor, ctx) {
|
|
|
416
449
|
? { dangling_specifiers: [...danglingSpecifiers].sort() }
|
|
417
450
|
: {}),
|
|
418
451
|
...(anchor.undeclaredNames ? { undeclared_names: anchor.undeclaredNames } : {}),
|
|
452
|
+
...(unresolvedPaths.length > 0
|
|
453
|
+
? {
|
|
454
|
+
unresolved_in_tree: [...unresolvedPaths]
|
|
455
|
+
.sort()
|
|
456
|
+
.map((p) => unresolvedInTreeProvenance(p, [...unfoundNames].sort())),
|
|
457
|
+
}
|
|
458
|
+
: {}),
|
|
459
|
+
...rawTextMark(anchor.request),
|
|
419
460
|
};
|
|
420
461
|
}
|
|
421
462
|
/** Resolve a relative specifier from `fromAbs` to a tree file, if present. */
|
|
@@ -17,3 +17,5 @@
|
|
|
17
17
|
*/
|
|
18
18
|
/** Absolute path of the config that types the service, or undefined for none. */
|
|
19
19
|
export declare function findServiceTsconfig(serviceRoot: string, scanRoot?: string): string | undefined;
|
|
20
|
+
/** The path with every symlink resolved; the path as given when it does not exist. */
|
|
21
|
+
export declare function realPath(p: string): string;
|
|
@@ -47,7 +47,7 @@ export function findServiceTsconfig(serviceRoot, scanRoot) {
|
|
|
47
47
|
return undefined;
|
|
48
48
|
}
|
|
49
49
|
/** The path with every symlink resolved; the path as given when it does not exist. */
|
|
50
|
-
function realPath(p) {
|
|
50
|
+
export function realPath(p) {
|
|
51
51
|
try {
|
|
52
52
|
return fs.realpathSync(p);
|
|
53
53
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the source reaches a node only when an HTTP response FAILED
|
|
3
|
+
* (carrick#1796).
|
|
4
|
+
*
|
|
5
|
+
* A consumer reads the success body on one path and the error text or the
|
|
6
|
+
* error body on another. Only the first is the call's response contract, so
|
|
7
|
+
* the def-use walk behind a `call_result` row must not take a read from the
|
|
8
|
+
* second. Which path a read is on is told by the source's own tests of the
|
|
9
|
+
* response, read two ways:
|
|
10
|
+
*
|
|
11
|
+
* - the read sits in a branch of a test: `if (!res.ok) { ... }`, the `else`
|
|
12
|
+
* of `if (res.ok)`, either arm of a conditional expression;
|
|
13
|
+
* - the read follows an `if` whose other branch cannot complete, so the rest
|
|
14
|
+
* of the block runs only on the side that did not leave:
|
|
15
|
+
* `if (res.ok) { return ... } const text = await res.text()`.
|
|
16
|
+
*
|
|
17
|
+
* A test is read as the set of statuses it lets through. `ok` is true for
|
|
18
|
+
* 200-299 and false for every other status; a comparison of the status with a
|
|
19
|
+
* number lets through what it admits; `!`, `&&` and `||` combine the sets, and
|
|
20
|
+
* any other condition lets everything through. A node is on the failure path
|
|
21
|
+
* only when NO status in 200-299 can reach it.
|
|
22
|
+
*
|
|
23
|
+
* The retype check (retype.ts) reads the same tests through `testsOnPath` and
|
|
24
|
+
* keeps its own policy on top: it also reads the false side of
|
|
25
|
+
* `res.status === 200` as the failure path, so it can find a success read to
|
|
26
|
+
* judge. Here a decided failure REMOVES a read, so the reading has to be sound
|
|
27
|
+
* the other way round: after `if (res.status === 204) return null`, 200 still
|
|
28
|
+
* gets through, and the json read that follows is the payload.
|
|
29
|
+
*/
|
|
30
|
+
import { Node } from 'ts-morph';
|
|
31
|
+
/** A set of HTTP statuses, as the predicate that admits them. */
|
|
32
|
+
export type Statuses = (status: number) => boolean;
|
|
33
|
+
/** The statuses `ok` is true for. */
|
|
34
|
+
export declare const SUCCEEDED: Statuses;
|
|
35
|
+
/** The statuses from 100 to 599 that `statuses` admits, in order. */
|
|
36
|
+
export declare function statusesIn(statuses: Statuses): number[];
|
|
37
|
+
/** One test of the response on the path to a node, as the side the node is on. */
|
|
38
|
+
export interface SideTaken {
|
|
39
|
+
/** Every status that can take this side, and maybe more (see `inexact`). */
|
|
40
|
+
admits: Statuses;
|
|
41
|
+
/**
|
|
42
|
+
* The test reads the response's `ok` or `status` in a form this reading
|
|
43
|
+
* cannot follow (`res.status === OK`, `codes.includes(res.status)`), or
|
|
44
|
+
* combines it with a condition that is not about the response
|
|
45
|
+
* (`res.ok && fresh`). `admits` then lets through more statuses than the
|
|
46
|
+
* side does: still sound for "no success status reaches the node", but no
|
|
47
|
+
* longer only the statuses the source singled out.
|
|
48
|
+
*/
|
|
49
|
+
inexact: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* True when the source reaches `node` only after the response named by
|
|
53
|
+
* `isResponse` failed. The walk climbs from `node` to `boundary` (the function
|
|
54
|
+
* the call sits in) and never looks at a test outside it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function reachedOnlyOnFailure(node: Node, boundary: Node, isResponse: (node: Node) => boolean): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Each test of the response on the path from `node` up to `boundary` (the
|
|
59
|
+
* whole file when it is `undefined`), as the side `node` is on: a branch of
|
|
60
|
+
* an `if` or a conditional expression it sits in, or an earlier `if` in an
|
|
61
|
+
* enclosing block one of whose branches cannot complete, which leaves the
|
|
62
|
+
* rest of the block to the other side. A test that does not read the
|
|
63
|
+
* response's `ok` or `status` is not listed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function testsOnPath(node: Node, boundary: Node | undefined, isResponse: (node: Node) => boolean): SideTaken[];
|
|
66
|
+
/** `node` reads the response's `ok` or `status`, itself or anywhere inside it. */
|
|
67
|
+
export declare function readsResponseStatus(node: Node, isResponse: (node: Node) => boolean): boolean;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the source reaches a node only when an HTTP response FAILED
|
|
3
|
+
* (carrick#1796).
|
|
4
|
+
*
|
|
5
|
+
* A consumer reads the success body on one path and the error text or the
|
|
6
|
+
* error body on another. Only the first is the call's response contract, so
|
|
7
|
+
* the def-use walk behind a `call_result` row must not take a read from the
|
|
8
|
+
* second. Which path a read is on is told by the source's own tests of the
|
|
9
|
+
* response, read two ways:
|
|
10
|
+
*
|
|
11
|
+
* - the read sits in a branch of a test: `if (!res.ok) { ... }`, the `else`
|
|
12
|
+
* of `if (res.ok)`, either arm of a conditional expression;
|
|
13
|
+
* - the read follows an `if` whose other branch cannot complete, so the rest
|
|
14
|
+
* of the block runs only on the side that did not leave:
|
|
15
|
+
* `if (res.ok) { return ... } const text = await res.text()`.
|
|
16
|
+
*
|
|
17
|
+
* A test is read as the set of statuses it lets through. `ok` is true for
|
|
18
|
+
* 200-299 and false for every other status; a comparison of the status with a
|
|
19
|
+
* number lets through what it admits; `!`, `&&` and `||` combine the sets, and
|
|
20
|
+
* any other condition lets everything through. A node is on the failure path
|
|
21
|
+
* only when NO status in 200-299 can reach it.
|
|
22
|
+
*
|
|
23
|
+
* The retype check (retype.ts) reads the same tests through `testsOnPath` and
|
|
24
|
+
* keeps its own policy on top: it also reads the false side of
|
|
25
|
+
* `res.status === 200` as the failure path, so it can find a success read to
|
|
26
|
+
* judge. Here a decided failure REMOVES a read, so the reading has to be sound
|
|
27
|
+
* the other way round: after `if (res.status === 204) return null`, 200 still
|
|
28
|
+
* gets through, and the json read that follows is the payload.
|
|
29
|
+
*/
|
|
30
|
+
import { Node, SyntaxKind } from 'ts-morph';
|
|
31
|
+
const FIRST_STATUS = 100;
|
|
32
|
+
const LAST_STATUS = 599;
|
|
33
|
+
const EVERY_STATUS = () => true;
|
|
34
|
+
/** The statuses `ok` is true for. */
|
|
35
|
+
export const SUCCEEDED = (status) => status >= 200 && status <= 299;
|
|
36
|
+
/** The statuses from 100 to 599 that `statuses` admits, in order. */
|
|
37
|
+
export function statusesIn(statuses) {
|
|
38
|
+
const admitted = [];
|
|
39
|
+
for (let status = FIRST_STATUS; status <= LAST_STATUS; status++) {
|
|
40
|
+
if (statuses(status))
|
|
41
|
+
admitted.push(status);
|
|
42
|
+
}
|
|
43
|
+
return admitted;
|
|
44
|
+
}
|
|
45
|
+
/** A condition that does not read the response's `ok` or `status`. */
|
|
46
|
+
const UNRELATED = { whenTrue: EVERY_STATUS, whenFalse: EVERY_STATUS, inexact: false };
|
|
47
|
+
/** A condition that reads the response's status in a form this reading cannot follow. */
|
|
48
|
+
const UNREAD = { whenTrue: EVERY_STATUS, whenFalse: EVERY_STATUS, inexact: true };
|
|
49
|
+
/**
|
|
50
|
+
* True when the source reaches `node` only after the response named by
|
|
51
|
+
* `isResponse` failed. The walk climbs from `node` to `boundary` (the function
|
|
52
|
+
* the call sits in) and never looks at a test outside it.
|
|
53
|
+
*/
|
|
54
|
+
export function reachedOnlyOnFailure(node, boundary, isResponse) {
|
|
55
|
+
const sides = testsOnPath(node, boundary, isResponse);
|
|
56
|
+
if (sides.length === 0)
|
|
57
|
+
return false;
|
|
58
|
+
const reaching = statusesIn((status) => sides.every((side) => side.admits(status)));
|
|
59
|
+
return reaching.length > 0 && !reaching.some(SUCCEEDED);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Each test of the response on the path from `node` up to `boundary` (the
|
|
63
|
+
* whole file when it is `undefined`), as the side `node` is on: a branch of
|
|
64
|
+
* an `if` or a conditional expression it sits in, or an earlier `if` in an
|
|
65
|
+
* enclosing block one of whose branches cannot complete, which leaves the
|
|
66
|
+
* rest of the block to the other side. A test that does not read the
|
|
67
|
+
* response's `ok` or `status` is not listed.
|
|
68
|
+
*/
|
|
69
|
+
export function testsOnPath(node, boundary, isResponse) {
|
|
70
|
+
const sides = [];
|
|
71
|
+
const take = (test, whenTrue) => {
|
|
72
|
+
if (test === UNRELATED)
|
|
73
|
+
return;
|
|
74
|
+
sides.push({ admits: whenTrue ? test.whenTrue : test.whenFalse, inexact: test.inexact });
|
|
75
|
+
};
|
|
76
|
+
for (let child = node, parent = node.getParent(); parent && parent !== boundary; child = parent, parent = parent.getParent()) {
|
|
77
|
+
if (Node.isIfStatement(parent)) {
|
|
78
|
+
if (child === parent.getThenStatement()) {
|
|
79
|
+
take(readTest(parent.getExpression(), isResponse), true);
|
|
80
|
+
}
|
|
81
|
+
else if (child === parent.getElseStatement()) {
|
|
82
|
+
take(readTest(parent.getExpression(), isResponse), false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (Node.isConditionalExpression(parent)) {
|
|
86
|
+
if (child === parent.getWhenTrue()) {
|
|
87
|
+
take(readTest(parent.getCondition(), isResponse), true);
|
|
88
|
+
}
|
|
89
|
+
else if (child === parent.getWhenFalse()) {
|
|
90
|
+
take(readTest(parent.getCondition(), isResponse), false);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (Node.isBlock(parent) ||
|
|
94
|
+
Node.isSourceFile(parent) ||
|
|
95
|
+
Node.isCaseClause(parent) ||
|
|
96
|
+
Node.isDefaultClause(parent)) {
|
|
97
|
+
for (const statement of parent.getStatements()) {
|
|
98
|
+
if (statement === child)
|
|
99
|
+
break;
|
|
100
|
+
if (!Node.isIfStatement(statement))
|
|
101
|
+
continue;
|
|
102
|
+
const otherwise = statement.getElseStatement();
|
|
103
|
+
const thenLeaves = cannotComplete(statement.getThenStatement());
|
|
104
|
+
const elseLeaves = otherwise !== undefined && cannotComplete(otherwise);
|
|
105
|
+
if (thenLeaves && !elseLeaves) {
|
|
106
|
+
take(readTest(statement.getExpression(), isResponse), false);
|
|
107
|
+
}
|
|
108
|
+
else if (elseLeaves && !thenLeaves) {
|
|
109
|
+
take(readTest(statement.getExpression(), isResponse), true);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return sides;
|
|
115
|
+
}
|
|
116
|
+
/** The statuses `condition` lets through when it is true and when it is false. */
|
|
117
|
+
function readTest(condition, isResponse) {
|
|
118
|
+
let test = condition;
|
|
119
|
+
while (Node.isParenthesizedExpression(test))
|
|
120
|
+
test = test.getExpression();
|
|
121
|
+
if (Node.isPrefixUnaryExpression(test) &&
|
|
122
|
+
test.getOperatorToken() === SyntaxKind.ExclamationToken) {
|
|
123
|
+
const inner = readTest(test.getOperand(), isResponse);
|
|
124
|
+
if (inner === UNRELATED || inner === UNREAD)
|
|
125
|
+
return inner;
|
|
126
|
+
return { whenTrue: inner.whenFalse, whenFalse: inner.whenTrue, inexact: inner.inexact };
|
|
127
|
+
}
|
|
128
|
+
if (isMemberOfResponse(test, 'ok', isResponse)) {
|
|
129
|
+
return { whenTrue: SUCCEEDED, whenFalse: (status) => !SUCCEEDED(status), inexact: false };
|
|
130
|
+
}
|
|
131
|
+
if (Node.isBinaryExpression(test)) {
|
|
132
|
+
const operator = test.getOperatorToken().getKind();
|
|
133
|
+
if (operator === SyntaxKind.AmpersandAmpersandToken ||
|
|
134
|
+
operator === SyntaxKind.BarBarToken) {
|
|
135
|
+
const left = readTest(test.getLeft(), isResponse);
|
|
136
|
+
const right = readTest(test.getRight(), isResponse);
|
|
137
|
+
if (left === UNRELATED && right === UNRELATED)
|
|
138
|
+
return UNRELATED;
|
|
139
|
+
const inexact = left.inexact || right.inexact || left === UNRELATED || right === UNRELATED;
|
|
140
|
+
return operator === SyntaxKind.AmpersandAmpersandToken
|
|
141
|
+
? {
|
|
142
|
+
whenTrue: (status) => left.whenTrue(status) && right.whenTrue(status),
|
|
143
|
+
whenFalse: (status) => left.whenFalse(status) || right.whenFalse(status),
|
|
144
|
+
inexact,
|
|
145
|
+
}
|
|
146
|
+
: {
|
|
147
|
+
whenTrue: (status) => left.whenTrue(status) || right.whenTrue(status),
|
|
148
|
+
whenFalse: (status) => left.whenFalse(status) && right.whenFalse(status),
|
|
149
|
+
inexact,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const compared = statusComparison(test.getLeft(), operator, test.getRight(), isResponse);
|
|
153
|
+
if (compared) {
|
|
154
|
+
return { whenTrue: compared, whenFalse: (status) => !compared(status), inexact: false };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return readsResponseStatus(test, isResponse) ? UNREAD : UNRELATED;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* `res.status <op> N` or `N <op> res.status`, as the statuses it is true for;
|
|
161
|
+
* `undefined` when the comparison is not one of the response's status with a
|
|
162
|
+
* number.
|
|
163
|
+
*/
|
|
164
|
+
function statusComparison(left, operator, right, isResponse) {
|
|
165
|
+
let op = operator;
|
|
166
|
+
let value;
|
|
167
|
+
if (isMemberOfResponse(left, 'status', isResponse) && Node.isNumericLiteral(right)) {
|
|
168
|
+
value = right.getLiteralValue();
|
|
169
|
+
}
|
|
170
|
+
else if (isMemberOfResponse(right, 'status', isResponse) && Node.isNumericLiteral(left)) {
|
|
171
|
+
value = left.getLiteralValue();
|
|
172
|
+
op = MIRRORED[op] ?? op;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
switch (op) {
|
|
178
|
+
case SyntaxKind.EqualsEqualsEqualsToken:
|
|
179
|
+
case SyntaxKind.EqualsEqualsToken:
|
|
180
|
+
return (status) => status === value;
|
|
181
|
+
case SyntaxKind.ExclamationEqualsEqualsToken:
|
|
182
|
+
case SyntaxKind.ExclamationEqualsToken:
|
|
183
|
+
return (status) => status !== value;
|
|
184
|
+
case SyntaxKind.LessThanToken:
|
|
185
|
+
return (status) => status < value;
|
|
186
|
+
case SyntaxKind.LessThanEqualsToken:
|
|
187
|
+
return (status) => status <= value;
|
|
188
|
+
case SyntaxKind.GreaterThanToken:
|
|
189
|
+
return (status) => status > value;
|
|
190
|
+
case SyntaxKind.GreaterThanEqualsToken:
|
|
191
|
+
return (status) => status >= value;
|
|
192
|
+
default:
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** `400 <= res.status` reads as `res.status >= 400`. */
|
|
197
|
+
const MIRRORED = {
|
|
198
|
+
[SyntaxKind.LessThanToken]: SyntaxKind.GreaterThanToken,
|
|
199
|
+
[SyntaxKind.LessThanEqualsToken]: SyntaxKind.GreaterThanEqualsToken,
|
|
200
|
+
[SyntaxKind.GreaterThanToken]: SyntaxKind.LessThanToken,
|
|
201
|
+
[SyntaxKind.GreaterThanEqualsToken]: SyntaxKind.LessThanEqualsToken,
|
|
202
|
+
};
|
|
203
|
+
function isMemberOfResponse(node, name, isResponse) {
|
|
204
|
+
return (Node.isPropertyAccessExpression(node) &&
|
|
205
|
+
node.getName() === name &&
|
|
206
|
+
isResponse(node.getExpression()));
|
|
207
|
+
}
|
|
208
|
+
/** `node` reads the response's `ok` or `status`, itself or anywhere inside it. */
|
|
209
|
+
export function readsResponseStatus(node, isResponse) {
|
|
210
|
+
return [node, ...node.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)].some((inner) => isMemberOfResponse(inner, 'ok', isResponse) ||
|
|
211
|
+
isMemberOfResponse(inner, 'status', isResponse));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* A statement that never runs on into the statement after it: it returns,
|
|
215
|
+
* throws, breaks or continues, or every path through it does. A loop, a
|
|
216
|
+
* `switch` or a `try` is read as one that can complete, which only ever
|
|
217
|
+
* leaves a read on the path it was on.
|
|
218
|
+
*/
|
|
219
|
+
function cannotComplete(statement) {
|
|
220
|
+
if (Node.isReturnStatement(statement) ||
|
|
221
|
+
Node.isThrowStatement(statement) ||
|
|
222
|
+
Node.isBreakStatement(statement) ||
|
|
223
|
+
Node.isContinueStatement(statement)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (Node.isBlock(statement)) {
|
|
227
|
+
return statement.getStatements().some((inner) => cannotComplete(inner));
|
|
228
|
+
}
|
|
229
|
+
if (Node.isIfStatement(statement)) {
|
|
230
|
+
const otherwise = statement.getElseStatement();
|
|
231
|
+
return (otherwise !== undefined &&
|
|
232
|
+
cannotComplete(statement.getThenStatement()) &&
|
|
233
|
+
cannotComplete(otherwise));
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* carrick#1836: what each bare name in an inference's printed text meant.
|
|
3
|
+
*
|
|
4
|
+
* The structural printer (`type-structural-expander.ts`) hands some subtrees
|
|
5
|
+
* back to the compiler's own print with no enclosing declaration, and that
|
|
6
|
+
* print writes every named type by its bare name: a database client's row,
|
|
7
|
+
* which is a library's mapped type, prints `{ status: EntityStatus; ... }`.
|
|
8
|
+
* The file a request names often never imports `EntityStatus`, so the text
|
|
9
|
+
* names nothing where it is read (TS2304), and the member reads `any` in the
|
|
10
|
+
* capture's surface.
|
|
11
|
+
*
|
|
12
|
+
* The compiler knew the symbol when it printed the name, and only then. So
|
|
13
|
+
* every type print made while an inference runs is noted (`notePrintedType`),
|
|
14
|
+
* and when it ends (`PrintedTypes.namesIn`) the names its text prints that the
|
|
15
|
+
* request's file cannot resolve are looked up in those prints: the node
|
|
16
|
+
* builder gives each name it writes the symbol it wrote it for. Each such
|
|
17
|
+
* symbol is recorded as the module that declares it and its export path
|
|
18
|
+
* there. A name printed for two declarations is recorded twice, and the
|
|
19
|
+
* reader of the record decides what an ambiguous name means.
|
|
20
|
+
*
|
|
21
|
+
* Only the prints whose text writes such a name are read back, and only when
|
|
22
|
+
* the inference's text has one, so an inference whose names all resolve costs
|
|
23
|
+
* no more than keeping the list.
|
|
24
|
+
*/
|
|
25
|
+
import { ts, type Node, type Type } from 'ts-morph';
|
|
26
|
+
import type { PrintedName } from './types.js';
|
|
27
|
+
/**
|
|
28
|
+
* Note a type the compiler printed as `text`, so the inference running now can
|
|
29
|
+
* tell what the names in that print meant. Does nothing outside a recording.
|
|
30
|
+
*/
|
|
31
|
+
export declare function notePrintedType(type: Type, enclosing: Node | undefined, text: string): void;
|
|
32
|
+
/** The type prints one inference made. */
|
|
33
|
+
export declare class PrintedTypes {
|
|
34
|
+
private readonly prints;
|
|
35
|
+
/** Run `read`, noting every type it prints. */
|
|
36
|
+
during<T>(read: () => T): T;
|
|
37
|
+
/**
|
|
38
|
+
* The declarations behind each name `texts` print that `source` does not
|
|
39
|
+
* resolve, sorted, one entry per distinct declaration. `checker` must be
|
|
40
|
+
* the one the prints were made with: call this before the program changes.
|
|
41
|
+
*/
|
|
42
|
+
namesIn(texts: readonly string[], source: ts.SourceFile, checker: ts.TypeChecker): PrintedName[];
|
|
43
|
+
}
|