carrick 0.3.105 → 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 +7 -0
- package/sidecar/dist/src/capture/anchors.js +151 -24
- package/sidecar/dist/src/capture/api.d.ts +42 -1
- package/sidecar/dist/src/capture/check-classify.d.ts +9 -1
- package/sidecar/dist/src/capture/check-classify.js +15 -0
- package/sidecar/dist/src/capture/check.js +13 -1
- package/sidecar/dist/src/capture/index.js +8 -0
- package/sidecar/dist/src/capture/self-check.js +9 -0
- 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 +34 -3
- package/sidecar/dist/src/failure-path.js +59 -35
- 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 +57 -109
- package/sidecar/dist/src/type-inferrer.d.ts +33 -3
- package/sidecar/dist/src/type-inferrer.js +191 -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
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* any other condition lets everything through. A node is on the failure path
|
|
21
21
|
* only when NO status in 200-299 can reach it.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `res.status === 200` as the failure path so it can find a success read to
|
|
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
26
|
* judge. Here a decided failure REMOVES a read, so the reading has to be sound
|
|
27
27
|
* the other way round: after `if (res.status === 204) return null`, 200 still
|
|
28
28
|
* gets through, and the json read that follows is the payload.
|
|
@@ -32,38 +32,62 @@ const FIRST_STATUS = 100;
|
|
|
32
32
|
const LAST_STATUS = 599;
|
|
33
33
|
const EVERY_STATUS = () => true;
|
|
34
34
|
/** The statuses `ok` is true for. */
|
|
35
|
-
const SUCCEEDED = (status) => status >= 200 && status <= 299;
|
|
36
|
-
|
|
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 };
|
|
37
49
|
/**
|
|
38
50
|
* True when the source reaches `node` only after the response named by
|
|
39
51
|
* `isResponse` failed. The walk climbs from `node` to `boundary` (the function
|
|
40
52
|
* the call sits in) and never looks at a test outside it.
|
|
41
53
|
*/
|
|
42
54
|
export function reachedOnlyOnFailure(node, boundary, isResponse) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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)
|
|
47
73
|
return;
|
|
48
|
-
|
|
49
|
-
admitted = (status) => before(status) && by(status);
|
|
50
|
-
narrowed = true;
|
|
74
|
+
sides.push({ admits: whenTrue ? test.whenTrue : test.whenFalse, inexact: test.inexact });
|
|
51
75
|
};
|
|
52
76
|
for (let child = node, parent = node.getParent(); parent && parent !== boundary; child = parent, parent = parent.getParent()) {
|
|
53
77
|
if (Node.isIfStatement(parent)) {
|
|
54
78
|
if (child === parent.getThenStatement()) {
|
|
55
|
-
|
|
79
|
+
take(readTest(parent.getExpression(), isResponse), true);
|
|
56
80
|
}
|
|
57
81
|
else if (child === parent.getElseStatement()) {
|
|
58
|
-
|
|
82
|
+
take(readTest(parent.getExpression(), isResponse), false);
|
|
59
83
|
}
|
|
60
84
|
}
|
|
61
85
|
else if (Node.isConditionalExpression(parent)) {
|
|
62
86
|
if (child === parent.getWhenTrue()) {
|
|
63
|
-
|
|
87
|
+
take(readTest(parent.getCondition(), isResponse), true);
|
|
64
88
|
}
|
|
65
89
|
else if (child === parent.getWhenFalse()) {
|
|
66
|
-
|
|
90
|
+
take(readTest(parent.getCondition(), isResponse), false);
|
|
67
91
|
}
|
|
68
92
|
}
|
|
69
93
|
if (Node.isBlock(parent) ||
|
|
@@ -79,25 +103,15 @@ export function reachedOnlyOnFailure(node, boundary, isResponse) {
|
|
|
79
103
|
const thenLeaves = cannotComplete(statement.getThenStatement());
|
|
80
104
|
const elseLeaves = otherwise !== undefined && cannotComplete(otherwise);
|
|
81
105
|
if (thenLeaves && !elseLeaves) {
|
|
82
|
-
|
|
106
|
+
take(readTest(statement.getExpression(), isResponse), false);
|
|
83
107
|
}
|
|
84
108
|
else if (elseLeaves && !thenLeaves) {
|
|
85
|
-
|
|
109
|
+
take(readTest(statement.getExpression(), isResponse), true);
|
|
86
110
|
}
|
|
87
111
|
}
|
|
88
112
|
}
|
|
89
113
|
}
|
|
90
|
-
|
|
91
|
-
return false;
|
|
92
|
-
let reachable = false;
|
|
93
|
-
for (let status = FIRST_STATUS; status <= LAST_STATUS; status++) {
|
|
94
|
-
if (!admitted(status))
|
|
95
|
-
continue;
|
|
96
|
-
if (SUCCEEDED(status))
|
|
97
|
-
return false;
|
|
98
|
-
reachable = true;
|
|
99
|
-
}
|
|
100
|
-
return reachable;
|
|
114
|
+
return sides;
|
|
101
115
|
}
|
|
102
116
|
/** The statuses `condition` lets through when it is true and when it is false. */
|
|
103
117
|
function readTest(condition, isResponse) {
|
|
@@ -107,10 +121,12 @@ function readTest(condition, isResponse) {
|
|
|
107
121
|
if (Node.isPrefixUnaryExpression(test) &&
|
|
108
122
|
test.getOperatorToken() === SyntaxKind.ExclamationToken) {
|
|
109
123
|
const inner = readTest(test.getOperand(), isResponse);
|
|
110
|
-
|
|
124
|
+
if (inner === UNRELATED || inner === UNREAD)
|
|
125
|
+
return inner;
|
|
126
|
+
return { whenTrue: inner.whenFalse, whenFalse: inner.whenTrue, inexact: inner.inexact };
|
|
111
127
|
}
|
|
112
128
|
if (isMemberOfResponse(test, 'ok', isResponse)) {
|
|
113
|
-
return { whenTrue: SUCCEEDED, whenFalse: (status) => !SUCCEEDED(status) };
|
|
129
|
+
return { whenTrue: SUCCEEDED, whenFalse: (status) => !SUCCEEDED(status), inexact: false };
|
|
114
130
|
}
|
|
115
131
|
if (Node.isBinaryExpression(test)) {
|
|
116
132
|
const operator = test.getOperatorToken().getKind();
|
|
@@ -118,24 +134,27 @@ function readTest(condition, isResponse) {
|
|
|
118
134
|
operator === SyntaxKind.BarBarToken) {
|
|
119
135
|
const left = readTest(test.getLeft(), isResponse);
|
|
120
136
|
const right = readTest(test.getRight(), isResponse);
|
|
121
|
-
if (left ===
|
|
122
|
-
return
|
|
137
|
+
if (left === UNRELATED && right === UNRELATED)
|
|
138
|
+
return UNRELATED;
|
|
139
|
+
const inexact = left.inexact || right.inexact || left === UNRELATED || right === UNRELATED;
|
|
123
140
|
return operator === SyntaxKind.AmpersandAmpersandToken
|
|
124
141
|
? {
|
|
125
142
|
whenTrue: (status) => left.whenTrue(status) && right.whenTrue(status),
|
|
126
143
|
whenFalse: (status) => left.whenFalse(status) || right.whenFalse(status),
|
|
144
|
+
inexact,
|
|
127
145
|
}
|
|
128
146
|
: {
|
|
129
147
|
whenTrue: (status) => left.whenTrue(status) || right.whenTrue(status),
|
|
130
148
|
whenFalse: (status) => left.whenFalse(status) && right.whenFalse(status),
|
|
149
|
+
inexact,
|
|
131
150
|
};
|
|
132
151
|
}
|
|
133
152
|
const compared = statusComparison(test.getLeft(), operator, test.getRight(), isResponse);
|
|
134
153
|
if (compared) {
|
|
135
|
-
return { whenTrue: compared, whenFalse: (status) => !compared(status) };
|
|
154
|
+
return { whenTrue: compared, whenFalse: (status) => !compared(status), inexact: false };
|
|
136
155
|
}
|
|
137
156
|
}
|
|
138
|
-
return
|
|
157
|
+
return readsResponseStatus(test, isResponse) ? UNREAD : UNRELATED;
|
|
139
158
|
}
|
|
140
159
|
/**
|
|
141
160
|
* `res.status <op> N` or `N <op> res.status`, as the statuses it is true for;
|
|
@@ -186,6 +205,11 @@ function isMemberOfResponse(node, name, isResponse) {
|
|
|
186
205
|
node.getName() === name &&
|
|
187
206
|
isResponse(node.getExpression()));
|
|
188
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
|
+
}
|
|
189
213
|
/**
|
|
190
214
|
* A statement that never runs on into the statement after it: it returns,
|
|
191
215
|
* throws, breaks or continues, or every path through it does. A loop, a
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
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 } from 'ts-morph';
|
|
26
|
+
/** The prints of the inference running now, when one is recording. */
|
|
27
|
+
let recording;
|
|
28
|
+
/**
|
|
29
|
+
* Note a type the compiler printed as `text`, so the inference running now can
|
|
30
|
+
* tell what the names in that print meant. Does nothing outside a recording.
|
|
31
|
+
*/
|
|
32
|
+
export function notePrintedType(type, enclosing, text) {
|
|
33
|
+
recording?.push({ type: type.compilerType, enclosing: enclosing?.compilerNode, text });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Same flags the printers pass `typeToString` (`NoTruncation`, `InTypeAlias`),
|
|
37
|
+
* so the node read back is the one that was printed.
|
|
38
|
+
*/
|
|
39
|
+
const NODE_FLAGS = ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.InTypeAlias | ts.NodeBuilderFlags.IgnoreErrors;
|
|
40
|
+
const REFERENCE_MEANING = ts.SymbolFlags.Type | ts.SymbolFlags.Namespace | ts.SymbolFlags.Alias;
|
|
41
|
+
/** The type prints one inference made. */
|
|
42
|
+
export class PrintedTypes {
|
|
43
|
+
prints = [];
|
|
44
|
+
/** Run `read`, noting every type it prints. */
|
|
45
|
+
during(read) {
|
|
46
|
+
const outer = recording;
|
|
47
|
+
recording = this.prints;
|
|
48
|
+
try {
|
|
49
|
+
return read();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
recording = outer;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The declarations behind each name `texts` print that `source` does not
|
|
57
|
+
* resolve, sorted, one entry per distinct declaration. `checker` must be
|
|
58
|
+
* the one the prints were made with: call this before the program changes.
|
|
59
|
+
*/
|
|
60
|
+
namesIn(texts, source, checker) {
|
|
61
|
+
const wanted = new Set();
|
|
62
|
+
for (const text of texts) {
|
|
63
|
+
for (const name of referencedNames(text)) {
|
|
64
|
+
if (!checker.resolveName(name, source, REFERENCE_MEANING, false))
|
|
65
|
+
wanted.add(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (wanted.size === 0)
|
|
69
|
+
return [];
|
|
70
|
+
// Only a print whose text writes one of the names can say what it meant.
|
|
71
|
+
const alternatives = [...wanted].map((name) => name.replace(/\$/g, '\\$')).join('|');
|
|
72
|
+
const writes = new RegExp(`(?<![\\w$.])(?:${alternatives})(?![\\w$])`);
|
|
73
|
+
const found = new Map();
|
|
74
|
+
const identities = new Map();
|
|
75
|
+
const visited = new Map();
|
|
76
|
+
for (const { type, enclosing, text } of this.prints) {
|
|
77
|
+
if (!writes.test(text))
|
|
78
|
+
continue;
|
|
79
|
+
const at = visited.get(type) ?? new Set();
|
|
80
|
+
if (at.has(enclosing))
|
|
81
|
+
continue;
|
|
82
|
+
visited.set(type, at.add(enclosing));
|
|
83
|
+
let node;
|
|
84
|
+
try {
|
|
85
|
+
node = checker.typeToTypeNode(type, enclosing, NODE_FLAGS);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const visit = (current) => {
|
|
91
|
+
if (ts.isTypeReferenceNode(current)) {
|
|
92
|
+
const id = leftmost(current.typeName);
|
|
93
|
+
const name = ts.idText(id);
|
|
94
|
+
const symbol = wanted.has(name) ? symbolOf(id, checker) : undefined;
|
|
95
|
+
if (symbol) {
|
|
96
|
+
if (!identities.has(symbol))
|
|
97
|
+
identities.set(symbol, identityOf(symbol, checker));
|
|
98
|
+
const identity = identities.get(symbol);
|
|
99
|
+
if (identity) {
|
|
100
|
+
found.set(`${name}\0${identity.file}\0${identity.export_path.join('.')}`, { name, ...identity });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
ts.forEachChild(current, visit);
|
|
105
|
+
};
|
|
106
|
+
if (node)
|
|
107
|
+
visit(node);
|
|
108
|
+
}
|
|
109
|
+
return [...found.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([, entry]) => entry);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function leftmost(name) {
|
|
113
|
+
return ts.isIdentifier(name) ? name : leftmost(name.left);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The leftmost names of the type references `text` makes, less the type
|
|
117
|
+
* parameters the text declares itself (`[K in ...]`, `infer U`).
|
|
118
|
+
*/
|
|
119
|
+
function referencedNames(text) {
|
|
120
|
+
const parsed = ts.createSourceFile('printed.ts', `type __Printed = ${text};`, ts.ScriptTarget.Latest, true);
|
|
121
|
+
const names = new Set();
|
|
122
|
+
const declared = new Set();
|
|
123
|
+
const visit = (node) => {
|
|
124
|
+
if (ts.isTypeParameterDeclaration(node))
|
|
125
|
+
declared.add(node.name.text);
|
|
126
|
+
if (ts.isTypeReferenceNode(node))
|
|
127
|
+
names.add(ts.idText(leftmost(node.typeName)));
|
|
128
|
+
ts.forEachChild(node, visit);
|
|
129
|
+
};
|
|
130
|
+
visit(parsed);
|
|
131
|
+
for (const name of declared)
|
|
132
|
+
names.delete(name);
|
|
133
|
+
names.delete('__Printed');
|
|
134
|
+
return names;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The symbol a printed name was written for. The node builder sets it on
|
|
138
|
+
* every identifier it creates (`symbol` is not in the public typings); a name
|
|
139
|
+
* it copied from source carries none, and is read at its original node.
|
|
140
|
+
*/
|
|
141
|
+
function symbolOf(id, checker) {
|
|
142
|
+
const written = id.symbol;
|
|
143
|
+
if (written)
|
|
144
|
+
return written;
|
|
145
|
+
const original = ts.getOriginalNode(id);
|
|
146
|
+
return original !== id && original.parent ? checker.getSymbolAtLocation(original) : undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The module that declares `symbol`'s type and the export path to it there,
|
|
150
|
+
* or undefined for a type no module export reaches: a global, a type
|
|
151
|
+
* parameter, a declaration a function body holds, a module itself.
|
|
152
|
+
*/
|
|
153
|
+
function identityOf(symbol, checker) {
|
|
154
|
+
const resolve = (s) => s.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s;
|
|
155
|
+
const target = resolve(symbol);
|
|
156
|
+
if (!(target.flags & (ts.SymbolFlags.Type | ts.SymbolFlags.Namespace)) ||
|
|
157
|
+
target.flags & ts.SymbolFlags.TypeParameter) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const declaration = target.declarations?.[0];
|
|
161
|
+
if (!declaration || ts.isSourceFile(declaration))
|
|
162
|
+
return undefined;
|
|
163
|
+
const file = declaration.getSourceFile();
|
|
164
|
+
const moduleSymbol = checker.getSymbolAtLocation(file);
|
|
165
|
+
if (!moduleSymbol)
|
|
166
|
+
return undefined;
|
|
167
|
+
const direct = checker.getExportsOfModule(moduleSymbol).filter((e) => resolve(e) === target);
|
|
168
|
+
if (direct.length > 0) {
|
|
169
|
+
const chosen = direct.find((e) => e.getName() === target.getName()) ?? direct[0];
|
|
170
|
+
return { file: file.fileName, export_path: [chosen.getName()] };
|
|
171
|
+
}
|
|
172
|
+
// A namespace member: `"<module>".Billing.Kind`, checked step by step.
|
|
173
|
+
const qualified = checker.getFullyQualifiedName(target);
|
|
174
|
+
const close = qualified.startsWith('"') ? qualified.indexOf('"', 1) : -1;
|
|
175
|
+
if (close < 0)
|
|
176
|
+
return undefined;
|
|
177
|
+
const exportPath = qualified.slice(close + 2).split('.');
|
|
178
|
+
let current = moduleSymbol;
|
|
179
|
+
for (const part of exportPath) {
|
|
180
|
+
const next = current
|
|
181
|
+
? checker.getExportsOfModule(current).find((e) => e.getName() === part)
|
|
182
|
+
: undefined;
|
|
183
|
+
current = next && resolve(next);
|
|
184
|
+
}
|
|
185
|
+
return current === target ? { file: file.fileName, export_path: exportPath } : undefined;
|
|
186
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* project other requests read is the project the scan loaded.
|
|
26
26
|
*/
|
|
27
27
|
import { Node, SyntaxKind, ts } from 'ts-morph';
|
|
28
|
+
import { readsResponseStatus, statusesIn, SUCCEEDED, testsOnPath, } from './failure-path.js';
|
|
28
29
|
import { fileDiagnostics } from './unwidened.js';
|
|
29
30
|
/** Names appended to a file that is not ours carry this prefix. */
|
|
30
31
|
const PREFIX = '__carrick_';
|
|
@@ -688,124 +689,71 @@ function responseTest(read) {
|
|
|
688
689
|
return (node) => Node.isIdentifier(node) && node.getSymbol() === symbol;
|
|
689
690
|
}
|
|
690
691
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
692
|
+
* Whether `node` sits on the response's error path (`'failure'`), on a path
|
|
693
|
+
* whose status tests do not say (`'unclear'`), or where the producer's body
|
|
694
|
+
* is read (`undefined`, also when no test of the response is on its path).
|
|
695
|
+
*
|
|
696
|
+
* The tests on the path are read as failure-path.ts reads them, each as the
|
|
697
|
+
* statuses that take the side `node` is on, and taken together. On top of
|
|
698
|
+
* that reading, in this order:
|
|
699
|
+
*
|
|
700
|
+
* - no success status reaches `node`: the error path;
|
|
701
|
+
* - a test lets through every status but one success status that carries a
|
|
702
|
+
* body (the side of `res.status === 200` the read after an early return on
|
|
703
|
+
* it is on): the error path. This is the consumer's own reading of that
|
|
704
|
+
* test, kept even when only success statuses get that far, because the
|
|
705
|
+
* read there is not the body the source singled out;
|
|
706
|
+
* - no status from 400 up reaches `node`: the producer's body;
|
|
707
|
+
* - a `switch` on the status, or a test the reading cannot follow exactly
|
|
708
|
+
* (`res.status === OK`, `res.ok || retry`), is on the path: unclear;
|
|
709
|
+
* - the tests take away a success status that carries a body
|
|
710
|
+
* (`res.status === 200 || res.status === 404`, `res.status > 200`):
|
|
711
|
+
* unclear;
|
|
712
|
+
* - otherwise they took away only error statuses, or statuses that carry no
|
|
713
|
+
* content (carrick#1813: the retype only runs against a producer that
|
|
714
|
+
* publishes a body, and that body never arrives with a 204 or 205), and
|
|
715
|
+
* `node` is where the body is read, as it is with no test at all.
|
|
694
716
|
*/
|
|
695
717
|
function sideOf(node, isResponse) {
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (statement === child)
|
|
721
|
-
break;
|
|
722
|
-
if (Node.isIfStatement(statement) &&
|
|
723
|
-
!statement.getElseStatement() &&
|
|
724
|
-
exits(statement.getThenStatement())) {
|
|
725
|
-
note(branchSide(statement.getExpression(), false, isResponse));
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
return found;
|
|
718
|
+
const sides = testsOnPath(node, undefined, isResponse);
|
|
719
|
+
const switched = node.getAncestors().some((ancestor) => {
|
|
720
|
+
if (!Node.isCaseClause(ancestor) && !Node.isDefaultClause(ancestor))
|
|
721
|
+
return false;
|
|
722
|
+
const statement = ancestor.getParent()?.getParent();
|
|
723
|
+
return (!!statement &&
|
|
724
|
+
Node.isSwitchStatement(statement) &&
|
|
725
|
+
readsResponseStatus(statement.getExpression(), isResponse));
|
|
726
|
+
});
|
|
727
|
+
if (sides.length === 0 && !switched)
|
|
728
|
+
return undefined;
|
|
729
|
+
const admitted = (status) => sides.every((side) => side.admits(status));
|
|
730
|
+
const reaching = statusesIn(admitted);
|
|
731
|
+
if (!reaching.some(SUCCEEDED))
|
|
732
|
+
return 'failure';
|
|
733
|
+
if (sides.some((side) => singlesOutABody(side.admits)))
|
|
734
|
+
return 'failure';
|
|
735
|
+
if (!reaching.some((status) => status >= 400))
|
|
736
|
+
return undefined;
|
|
737
|
+
if (switched || sides.some((side) => side.inexact))
|
|
738
|
+
return 'unclear';
|
|
739
|
+
if (statusesIn((status) => !admitted(status)).some(carriesBody))
|
|
740
|
+
return 'unclear';
|
|
741
|
+
return undefined;
|
|
731
742
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
743
|
+
/** `statuses` is every status but one success status that carries a body. */
|
|
744
|
+
function singlesOutABody(statuses) {
|
|
745
|
+
const excluded = statusesIn((status) => !statuses(status));
|
|
746
|
+
return excluded.length === 1 && carriesBody(excluded[0]);
|
|
747
|
+
}
|
|
748
|
+
/** A success status whose response can carry the producer's body. */
|
|
749
|
+
function carriesBody(status) {
|
|
750
|
+
return SUCCEEDED(status) && !NO_CONTENT_STATUSES.has(status);
|
|
737
751
|
}
|
|
738
752
|
/**
|
|
739
753
|
* The success statuses whose response carries no content (RFC 9110: 204 No
|
|
740
754
|
* Content, 205 Reset Content).
|
|
741
755
|
*/
|
|
742
756
|
const NO_CONTENT_STATUSES = new Set([204, 205]);
|
|
743
|
-
/**
|
|
744
|
-
* Whether `condition` being true means the response succeeded: `res.ok`,
|
|
745
|
-
* `res.status === 200`, `res.status !== 200`, `res.status >= 400` and their
|
|
746
|
-
* negations. Any other test of the response is `'unclear'`; a condition that does not
|
|
747
|
-
* test the response is `undefined`.
|
|
748
|
-
*
|
|
749
|
-
* An equality or inequality with a no-content status is `undefined` too
|
|
750
|
-
* (carrick#1813). The retype only runs against a producer that publishes a
|
|
751
|
-
* response body, and that body never arrives with a 204 or 205, so
|
|
752
|
-
* `if (res.status === 204) return null` only takes away a status the body
|
|
753
|
-
* cannot come with. The read after it sits where a read no test decides
|
|
754
|
-
* sits, not on the error path.
|
|
755
|
-
*/
|
|
756
|
-
function okWhenTrue(condition, isResponse) {
|
|
757
|
-
let e = condition;
|
|
758
|
-
while (Node.isParenthesizedExpression(e))
|
|
759
|
-
e = e.getExpression();
|
|
760
|
-
if (Node.isPrefixUnaryExpression(e) && e.getOperatorToken() === SyntaxKind.ExclamationToken) {
|
|
761
|
-
const inner = okWhenTrue(e.getOperand(), isResponse);
|
|
762
|
-
return typeof inner === 'boolean' ? !inner : inner;
|
|
763
|
-
}
|
|
764
|
-
if (isMember(e, 'ok', isResponse))
|
|
765
|
-
return true;
|
|
766
|
-
if (Node.isBinaryExpression(e)) {
|
|
767
|
-
const op = e.getOperatorToken().getKind();
|
|
768
|
-
const [left, right] = [e.getLeft(), e.getRight()];
|
|
769
|
-
const value = isMember(left, 'status', isResponse) && Node.isNumericLiteral(right)
|
|
770
|
-
? right.getLiteralValue()
|
|
771
|
-
: undefined;
|
|
772
|
-
if (value !== undefined) {
|
|
773
|
-
const success = value >= 200 && value < 300;
|
|
774
|
-
const noContent = NO_CONTENT_STATUSES.has(value);
|
|
775
|
-
switch (op) {
|
|
776
|
-
case SyntaxKind.EqualsEqualsEqualsToken:
|
|
777
|
-
case SyntaxKind.EqualsEqualsToken:
|
|
778
|
-
return noContent ? undefined : success;
|
|
779
|
-
case SyntaxKind.ExclamationEqualsEqualsToken:
|
|
780
|
-
case SyntaxKind.ExclamationEqualsToken:
|
|
781
|
-
return noContent ? undefined : !success;
|
|
782
|
-
case SyntaxKind.GreaterThanEqualsToken:
|
|
783
|
-
if (value >= 300)
|
|
784
|
-
return false;
|
|
785
|
-
break;
|
|
786
|
-
}
|
|
787
|
-
return 'unclear';
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
return testsResponse(e, isResponse) ? 'unclear' : undefined;
|
|
791
|
-
}
|
|
792
|
-
function isMember(node, name, isResponse) {
|
|
793
|
-
return (Node.isPropertyAccessExpression(node) && node.getName() === name && isResponse(node.getExpression()));
|
|
794
|
-
}
|
|
795
|
-
/** The expression reads the response's `ok` or `status`. */
|
|
796
|
-
function testsResponse(node, isResponse) {
|
|
797
|
-
return [node, ...node.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)].some((n) => isMember(n, 'ok', isResponse) || isMember(n, 'status', isResponse));
|
|
798
|
-
}
|
|
799
|
-
/** A statement that always leaves the function. */
|
|
800
|
-
function exits(statement) {
|
|
801
|
-
if (Node.isReturnStatement(statement) || Node.isThrowStatement(statement))
|
|
802
|
-
return true;
|
|
803
|
-
if (Node.isBlock(statement)) {
|
|
804
|
-
const last = statement.getStatements().at(-1);
|
|
805
|
-
return !!last && exits(last);
|
|
806
|
-
}
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
757
|
/** `node.json()` with no arguments, where `node` is the receiver. */
|
|
810
758
|
function bodyReadOn(node) {
|
|
811
759
|
const access = node.getParent();
|
|
@@ -97,6 +97,11 @@ export declare class TypeInferrer {
|
|
|
97
97
|
* @returns InferResult with inferred types or errors
|
|
98
98
|
*/
|
|
99
99
|
infer(requests: InferRequestItem[], extractionConfig?: ExtractionConfig): InferResult;
|
|
100
|
+
/**
|
|
101
|
+
* carrick#1836: list on `result` the declarations behind the names its text
|
|
102
|
+
* prints that the request's file cannot resolve (`PrintedTypes.namesIn`).
|
|
103
|
+
*/
|
|
104
|
+
private recordPrintedNames;
|
|
100
105
|
/**
|
|
101
106
|
* carrick#1516: read each response inference again with the literals on its
|
|
102
107
|
* handler's path marked `as const`, and record the narrower type the handler
|
|
@@ -257,6 +262,25 @@ export declare class TypeInferrer {
|
|
|
257
262
|
*/
|
|
258
263
|
private receivingCallOf;
|
|
259
264
|
private inferCallResult;
|
|
265
|
+
/**
|
|
266
|
+
* The terminal of a call's def-use walk is a zero-argument `.text()` read of
|
|
267
|
+
* the call's own result (carrick#1842): on its binding (`res.text()`,
|
|
268
|
+
* through a declaration, `await` or a cast), or in the callback a `then` on
|
|
269
|
+
* the call hands the response to (`fetch(u).then((res) => res.text())`).
|
|
270
|
+
*/
|
|
271
|
+
private textReadAtTerminal;
|
|
272
|
+
/** `call` is `<receiver>.text()` with no arguments, and `accept` takes its receiver. */
|
|
273
|
+
private isTextReadOf;
|
|
274
|
+
/**
|
|
275
|
+
* The call's resolved signature, at this site, types a member of an object
|
|
276
|
+
* argument as exactly the literal `'text'`, and the source passes that
|
|
277
|
+
* literal there (carrick#1842). That is how a request library lets a caller
|
|
278
|
+
* choose a text body from the formats it offers (`{ type: 'text' }`): a
|
|
279
|
+
* generic config instantiated by the literal, or an overload taken by it.
|
|
280
|
+
* A member typed as a wider union (`kind: 'text' | 'image'`) chooses no
|
|
281
|
+
* format, and no member name is read.
|
|
282
|
+
*/
|
|
283
|
+
private callChoosesTextBody;
|
|
260
284
|
/**
|
|
261
285
|
* What the source states the body read at `terminal` to be, when the type
|
|
262
286
|
* `extractExplicitTypeFromAncestor` printed for it is stated AT the read
|
|
@@ -349,10 +373,10 @@ export declare class TypeInferrer {
|
|
|
349
373
|
* `query.data`, `envelope` in `envelope.list[0]` — or `undefined` when the
|
|
350
374
|
* identifier names the value itself.
|
|
351
375
|
*
|
|
352
|
-
* A member CALL is not a projection: `res.
|
|
376
|
+
* A member CALL is not a projection: `res.blob()` yields a body rather than
|
|
353
377
|
* a part of one, and what it returns stays the walk's business. The
|
|
354
|
-
* zero-argument json body
|
|
355
|
-
* is asked.
|
|
378
|
+
* zero-argument json and text body reads have their own branch and are
|
|
379
|
+
* taken before this is asked.
|
|
356
380
|
*/
|
|
357
381
|
private projectionOnReceiver;
|
|
358
382
|
/**
|
|
@@ -431,6 +455,12 @@ export declare class TypeInferrer {
|
|
|
431
455
|
* about the body and is skipped.
|
|
432
456
|
*/
|
|
433
457
|
private castsOfUnreadParameter;
|
|
458
|
+
/**
|
|
459
|
+
* The zero-argument whole-body read that takes `identifier` as its receiver,
|
|
460
|
+
* `res.json()` or `res.text()`, or `undefined`. A text read is a body read
|
|
461
|
+
* like a json one (carrick#1842): without it, `return res.text()` left the
|
|
462
|
+
* walk on the response binding and published the transport object.
|
|
463
|
+
*/
|
|
434
464
|
private bodyReadOnReceiver;
|
|
435
465
|
private collectDefUseNodes;
|
|
436
466
|
private expressionUsesNames;
|