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.
@@ -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
- * That is deliberately stricter than the retype check's reading of a status
24
- * test (`okWhenTrue` in retype.ts), which reads the false side of
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
- const UNDECIDED = { whenTrue: EVERY_STATUS, whenFalse: EVERY_STATUS };
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
- let admitted = EVERY_STATUS;
44
- let narrowed = false;
45
- const narrow = (by) => {
46
- if (by === EVERY_STATUS)
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
- const before = admitted;
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
- narrow(readTest(parent.getExpression(), isResponse).whenTrue);
79
+ take(readTest(parent.getExpression(), isResponse), true);
56
80
  }
57
81
  else if (child === parent.getElseStatement()) {
58
- narrow(readTest(parent.getExpression(), isResponse).whenFalse);
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
- narrow(readTest(parent.getCondition(), isResponse).whenTrue);
87
+ take(readTest(parent.getCondition(), isResponse), true);
64
88
  }
65
89
  else if (child === parent.getWhenFalse()) {
66
- narrow(readTest(parent.getCondition(), isResponse).whenFalse);
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
- narrow(readTest(statement.getExpression(), isResponse).whenFalse);
106
+ take(readTest(statement.getExpression(), isResponse), false);
83
107
  }
84
108
  else if (elseLeaves && !thenLeaves) {
85
- narrow(readTest(statement.getExpression(), isResponse).whenTrue);
109
+ take(readTest(statement.getExpression(), isResponse), true);
86
110
  }
87
111
  }
88
112
  }
89
113
  }
90
- if (!narrowed)
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
- return { whenTrue: inner.whenFalse, whenFalse: inner.whenTrue };
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 === UNDECIDED && right === UNDECIDED)
122
- return UNDECIDED;
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 UNDECIDED;
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
- * Which side of a test of the response's status `node` runs on: inside a
692
- * branch of one, or after an `if (test) return/throw` that leaves the rest of
693
- * the block to the other side. `undefined` when no test decides it.
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
- let found;
697
- const note = (side) => {
698
- if (side === 'failure' || found === 'failure')
699
- found = 'failure';
700
- else if (side === 'unclear' || found === 'unclear')
701
- found = 'unclear';
702
- else
703
- found = side ?? found;
704
- };
705
- for (let child = node, parent = node.getParent(); parent; child = parent, parent = parent.getParent()) {
706
- if (Node.isIfStatement(parent) && child !== parent.getExpression()) {
707
- note(branchSide(parent.getExpression(), child === parent.getThenStatement(), isResponse));
708
- }
709
- else if (Node.isConditionalExpression(parent) && child !== parent.getCondition()) {
710
- note(branchSide(parent.getCondition(), child === parent.getWhenTrue(), isResponse));
711
- }
712
- else if (Node.isCaseClause(parent) || Node.isDefaultClause(parent)) {
713
- const swtch = parent.getParent()?.getParent();
714
- if (swtch && Node.isSwitchStatement(swtch) && testsResponse(swtch.getExpression(), isResponse)) {
715
- note('unclear');
716
- }
717
- }
718
- if (Node.isBlock(parent) || Node.isSourceFile(parent) || Node.isCaseClause(parent)) {
719
- for (const statement of parent.getStatements()) {
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
- function branchSide(condition, whenTrue, isResponse) {
733
- const ok = okWhenTrue(condition, isResponse);
734
- if (ok === undefined || ok === 'unclear')
735
- return ok;
736
- return ok === whenTrue ? 'success' : 'failure';
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.text()` yields a body rather than
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 read has its own branch and is taken before this
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;