carrick 0.3.92 → 0.3.93

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.
@@ -32,16 +32,19 @@ export class Retyper {
32
32
  project;
33
33
  inferrer;
34
34
  jsonWire;
35
+ topTypes;
35
36
  /**
36
37
  * `jsonWire` is the check phase's own JSON wire transform
37
- * (`jsonWireDeclarations` in the capture bundle), handed in by the entry
38
- * point so both judges read the wire the same way without this file
39
- * crossing the bundle seam.
38
+ * (`jsonWireDeclarations` in the capture bundle) and `topTypes` its
39
+ * any/unknown walk, handed in by the entry point so both judges read the
40
+ * wire and a top type the same way without this file crossing the bundle
41
+ * seam.
40
42
  */
41
- constructor(project, inferrer, jsonWire) {
43
+ constructor(project, inferrer, jsonWire, topTypes) {
42
44
  this.project = project;
43
45
  this.inferrer = inferrer;
44
46
  this.jsonWire = jsonWire;
47
+ this.topTypes = topTypes;
45
48
  }
46
49
  /**
47
50
  * Judge every item, spending at most `budgetMs`. Each item rebuilds the
@@ -150,7 +153,8 @@ export class Retyper {
150
153
  const text = stated(useWire);
151
154
  const delta = text.length - (argEnd - argStart);
152
155
  return {
153
- edits: [{ start: argStart, end: argEnd, text }],
156
+ edits: [{ start: argStart, end: argEnd, text, statedAt: 0 }],
157
+ stated: text,
154
158
  wire: useWire && wire,
155
159
  typeArgumentCall: { start: callStart, end: callEnd + delta },
156
160
  };
@@ -164,7 +168,8 @@ export class Retyper {
164
168
  return (useWire) => {
165
169
  const text = `<${stated(useWire)}>`;
166
170
  return {
167
- edits: [{ start: at, end: at, text }],
171
+ edits: [{ start: at, end: at, text, statedAt: 1 }],
172
+ stated: stated(useWire),
168
173
  wire: useWire && wire,
169
174
  typeArgumentCall: { start: callStart, end: callEnd + text.length },
170
175
  };
@@ -190,6 +195,7 @@ export class Retyper {
190
195
  const text = stated(useWire);
191
196
  return {
192
197
  edits: edits.flatMap((edit) => edit(text)),
198
+ stated: text,
193
199
  wire: useWire && wire,
194
200
  };
195
201
  };
@@ -249,12 +255,76 @@ export class Retyper {
249
255
  added.push(mapped);
250
256
  }
251
257
  added.sort((a, b) => a.start - b.start || a.code - b.code);
258
+ const unresolved = this.unresolvedMembers(sourceFile, rewrite, added.length > 0);
259
+ if (unresolved)
260
+ return { kind: 'abstain', reason: unresolved };
252
261
  return { kind: 'checked', added };
253
262
  }
254
263
  finally {
255
264
  sourceFile.replaceWithText(original);
256
265
  }
257
266
  }
267
+ /**
268
+ * Why the stated type, as the consumer's program reads it, cannot be
269
+ * compared, or `undefined` when it can (carrick#1514). Read after the
270
+ * diagnostics diff, because the two top types fail in opposite directions.
271
+ *
272
+ * The producer's text is sent only when it holds no `any`/`unknown`, but
273
+ * the consumer's program is where it is read: under its compiler options,
274
+ * through the wire transform (a lib type whose `toJSON()` returns `any`),
275
+ * with its own declarations.
276
+ *
277
+ * - A member that reads as `unknown` makes reads fail that the producer's
278
+ * type would pass, so any diagnostic may be ours. It is never compared.
279
+ * - A member that reads as `any` can hide a failure but never make one, so
280
+ * the diagnostics the diff found stand; only an empty diff is not
281
+ * compared, since it may be the `any` agreeing.
282
+ *
283
+ * Either way the reason names the member, as the check phase does for a
284
+ * published type.
285
+ *
286
+ * The walk's budget sentinel is dropped. The scanner's text screen
287
+ * (`contains_disqualifying_top_type`, which has no budget) found no
288
+ * `any`/`unknown` in the producer's text before sending it, so a walk that
289
+ * runs out of budget can only miss one the consumer's program made deeper
290
+ * than the budget allows; the compiler's diagnostics stand.
291
+ */
292
+ unresolvedMembers(sourceFile, rewrite, diagnosed) {
293
+ let shift = 0;
294
+ let at;
295
+ for (const edit of [...rewrite.edits].sort((a, b) => a.start - b.start)) {
296
+ if (at === undefined && edit.statedAt !== undefined) {
297
+ at = edit.start + shift + edit.statedAt;
298
+ }
299
+ shift += edit.text.length - (edit.end - edit.start);
300
+ }
301
+ const node = at === undefined
302
+ ? undefined
303
+ : sourceFile.getDescendantAtStartWithWidth(at, rewrite.stated.length);
304
+ // Every rewrite states the type at a recorded offset, so this is only
305
+ // reached through a wrong offset; it abstains rather than skip the walk.
306
+ if (!node)
307
+ return 'the stated producer type could not be found again after the rewrite';
308
+ const type = node.getType().compilerType;
309
+ let found;
310
+ if (type.flags & ts.TypeFlags.Unknown)
311
+ found = [{ kind: 'unknown', path: '' }];
312
+ else if (type.flags & ts.TypeFlags.Any)
313
+ found = [{ kind: 'any', path: '' }];
314
+ else {
315
+ const program = this.project.getProgram().compilerObject;
316
+ found = this.topTypes(type, program, program.getTypeChecker(), node.compilerNode);
317
+ }
318
+ const unknowns = found.filter((finding) => finding.kind === 'unknown');
319
+ const anys = found.filter((finding) => finding.kind === 'any');
320
+ const deciding = unknowns.length > 0 ? unknowns : diagnosed ? [] : anys;
321
+ if (deciding.length === 0)
322
+ return undefined;
323
+ const members = deciding
324
+ .map(({ kind, path }) => (path === '' ? `'${kind}'` : `'${kind}' at '${path}'`))
325
+ .join(', ');
326
+ return `the producer's response reads as ${members} in the consumer's program, so it was not compared`;
327
+ }
258
328
  /**
259
329
  * The stated type argument must be what the call RETURNS (or a type
260
330
  * argument or member of it): the rewrite is only a statement about the
@@ -738,7 +808,11 @@ function bodyReadEdit(read) {
738
808
  const type = parent.getTypeNodeOrThrow();
739
809
  const start = type.getStart();
740
810
  const end = type.getEnd();
741
- return (stated) => [{ start, end, text: awaited ? stated : `Promise<${stated}>` }];
811
+ return (stated) => [
812
+ awaited
813
+ ? { start, end, text: stated, statedAt: 0 }
814
+ : { start, end, text: `Promise<${stated}>`, statedAt: 'Promise<'.length },
815
+ ];
742
816
  }
743
817
  else if (!Node.isParenthesizedExpression(parent))
744
818
  break;
@@ -750,7 +824,7 @@ function bodyReadEdit(read) {
750
824
  // The compiler reports past a parenthesis, so a finding on the cast read
751
825
  // lands on `res`, an original position, never on this one.
752
826
  { start, end: start, text: '(' },
753
- { start: end, end, text: ` as Promise<${stated}>)` },
827
+ { start: end, end, text: ` as Promise<${stated}>)`, statedAt: ' as Promise<'.length },
754
828
  ];
755
829
  }
756
830
  function declaresTypeParameters(call) {