carrick 0.3.105 → 0.3.107

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.
Files changed (40) hide show
  1. package/dist/hook/refresh.js +8 -1
  2. package/dist/hook/refresh.js.map +1 -1
  3. package/package.json +6 -6
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/sidecar/dist/src/capture/anchors.d.ts +22 -2
  6. package/sidecar/dist/src/capture/anchors.js +178 -34
  7. package/sidecar/dist/src/capture/api.d.ts +42 -1
  8. package/sidecar/dist/src/capture/check-classify.d.ts +9 -1
  9. package/sidecar/dist/src/capture/check-classify.js +15 -0
  10. package/sidecar/dist/src/capture/check-poison.js +4 -14
  11. package/sidecar/dist/src/capture/check.js +13 -1
  12. package/sidecar/dist/src/capture/index.js +55 -29
  13. package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
  14. package/sidecar/dist/src/capture/installed-package.js +2 -1
  15. package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
  16. package/sidecar/dist/src/capture/outside-root.js +39 -2
  17. package/sidecar/dist/src/capture/self-check.js +12 -13
  18. package/sidecar/dist/src/capture/service-config.d.ts +2 -0
  19. package/sidecar/dist/src/capture/service-config.js +1 -1
  20. package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
  21. package/sidecar/dist/src/capture/specifiers.js +25 -0
  22. package/sidecar/dist/src/failure-path.d.ts +34 -3
  23. package/sidecar/dist/src/failure-path.js +59 -35
  24. package/sidecar/dist/src/function-line-index.d.ts +30 -0
  25. package/sidecar/dist/src/function-line-index.js +162 -0
  26. package/sidecar/dist/src/index.d.ts +5 -0
  27. package/sidecar/dist/src/index.js +29 -5
  28. package/sidecar/dist/src/line-index.d.ts +9 -0
  29. package/sidecar/dist/src/line-index.js +26 -0
  30. package/sidecar/dist/src/printed-names.d.ts +43 -0
  31. package/sidecar/dist/src/printed-names.js +186 -0
  32. package/sidecar/dist/src/progress.d.ts +22 -0
  33. package/sidecar/dist/src/progress.js +31 -0
  34. package/sidecar/dist/src/retype.js +58 -128
  35. package/sidecar/dist/src/type-inferrer.d.ts +124 -13
  36. package/sidecar/dist/src/type-inferrer.js +531 -146
  37. package/sidecar/dist/src/type-structural-expander.js +10 -1
  38. package/sidecar/dist/src/types.d.ts +37 -6
  39. package/sidecar/dist/src/validators.d.ts +76 -0
  40. package/sidecar/dist/src/validators.js +8 -0
@@ -25,6 +25,8 @@
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';
29
+ import { lineIndex } from './line-index.js';
28
30
  import { fileDiagnostics } from './unwidened.js';
29
31
  /** Names appended to a file that is not ours carry this prefix. */
30
32
  const PREFIX = '__carrick_';
@@ -688,124 +690,71 @@ function responseTest(read) {
688
690
  return (node) => Node.isIdentifier(node) && node.getSymbol() === symbol;
689
691
  }
690
692
  /**
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.
693
+ * Whether `node` sits on the response's error path (`'failure'`), on a path
694
+ * whose status tests do not say (`'unclear'`), or where the producer's body
695
+ * is read (`undefined`, also when no test of the response is on its path).
696
+ *
697
+ * The tests on the path are read as failure-path.ts reads them, each as the
698
+ * statuses that take the side `node` is on, and taken together. On top of
699
+ * that reading, in this order:
700
+ *
701
+ * - no success status reaches `node`: the error path;
702
+ * - a test lets through every status but one success status that carries a
703
+ * body (the side of `res.status === 200` the read after an early return on
704
+ * it is on): the error path. This is the consumer's own reading of that
705
+ * test, kept even when only success statuses get that far, because the
706
+ * read there is not the body the source singled out;
707
+ * - no status from 400 up reaches `node`: the producer's body;
708
+ * - a `switch` on the status, or a test the reading cannot follow exactly
709
+ * (`res.status === OK`, `res.ok || retry`), is on the path: unclear;
710
+ * - the tests take away a success status that carries a body
711
+ * (`res.status === 200 || res.status === 404`, `res.status > 200`):
712
+ * unclear;
713
+ * - otherwise they took away only error statuses, or statuses that carry no
714
+ * content (carrick#1813: the retype only runs against a producer that
715
+ * publishes a body, and that body never arrives with a 204 or 205), and
716
+ * `node` is where the body is read, as it is with no test at all.
694
717
  */
695
718
  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;
719
+ const sides = testsOnPath(node, undefined, isResponse);
720
+ const switched = node.getAncestors().some((ancestor) => {
721
+ if (!Node.isCaseClause(ancestor) && !Node.isDefaultClause(ancestor))
722
+ return false;
723
+ const statement = ancestor.getParent()?.getParent();
724
+ return (!!statement &&
725
+ Node.isSwitchStatement(statement) &&
726
+ readsResponseStatus(statement.getExpression(), isResponse));
727
+ });
728
+ if (sides.length === 0 && !switched)
729
+ return undefined;
730
+ const admitted = (status) => sides.every((side) => side.admits(status));
731
+ const reaching = statusesIn(admitted);
732
+ if (!reaching.some(SUCCEEDED))
733
+ return 'failure';
734
+ if (sides.some((side) => singlesOutABody(side.admits)))
735
+ return 'failure';
736
+ if (!reaching.some((status) => status >= 400))
737
+ return undefined;
738
+ if (switched || sides.some((side) => side.inexact))
739
+ return 'unclear';
740
+ if (statusesIn((status) => !admitted(status)).some(carriesBody))
741
+ return 'unclear';
742
+ return undefined;
743
+ }
744
+ /** `statuses` is every status but one success status that carries a body. */
745
+ function singlesOutABody(statuses) {
746
+ const excluded = statusesIn((status) => !statuses(status));
747
+ return excluded.length === 1 && carriesBody(excluded[0]);
731
748
  }
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';
749
+ /** A success status whose response can carry the producer's body. */
750
+ function carriesBody(status) {
751
+ return SUCCEEDED(status) && !NO_CONTENT_STATUSES.has(status);
737
752
  }
738
753
  /**
739
754
  * The success statuses whose response carries no content (RFC 9110: 204 No
740
755
  * Content, 205 Reset Content).
741
756
  */
742
757
  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
758
  /** `node.json()` with no arguments, where `node` is the receiver. */
810
759
  function bodyReadOn(node) {
811
760
  const access = node.getParent();
@@ -894,22 +843,3 @@ function mapBack(pos, edits) {
894
843
  }
895
844
  return { kind: 'original', pos: pos - shift };
896
845
  }
897
- /** 1-based line of an original-file position. */
898
- function lineIndex(text) {
899
- const starts = [0];
900
- for (let i = 0; i < text.length; i++)
901
- if (text[i] === '\n')
902
- starts.push(i + 1);
903
- return (pos) => {
904
- let lo = 0;
905
- let hi = starts.length - 1;
906
- while (lo < hi) {
907
- const mid = (lo + hi + 1) >> 1;
908
- if (starts[mid] <= pos)
909
- lo = mid;
910
- else
911
- hi = mid - 1;
912
- }
913
- return lo + 1;
914
- };
915
- }
@@ -63,6 +63,11 @@ export interface TypeInferrerOptions {
63
63
  */
64
64
  unwidenedBudgetMs?: number;
65
65
  }
66
+ /**
67
+ * Called each time an `infer` batch is done with one of its requests,
68
+ * whatever that request answered: skipped, refused, failed or inferred.
69
+ */
70
+ export type InferRequestDone = () => void;
66
71
  /**
67
72
  * TypeInferrer - Extracts types from source code, both explicit and inferred
68
73
  *
@@ -94,9 +99,15 @@ export declare class TypeInferrer {
94
99
  *
95
100
  * @param requests - Array of inference requests
96
101
  * @param extractionConfig - Agent-generated extraction config for payload unwrapping
102
+ * @param onRequestDone - Called once per request, as the batch is done with it
97
103
  * @returns InferResult with inferred types or errors
98
104
  */
99
- infer(requests: InferRequestItem[], extractionConfig?: ExtractionConfig): InferResult;
105
+ infer(requests: InferRequestItem[], extractionConfig?: ExtractionConfig, onRequestDone?: InferRequestDone): InferResult;
106
+ /**
107
+ * carrick#1836: list on `result` the declarations behind the names its text
108
+ * prints that the request's file cannot resolve (`PrintedTypes.namesIn`).
109
+ */
110
+ private recordPrintedNames;
100
111
  /**
101
112
  * carrick#1516: read each response inference again with the literals on its
102
113
  * handler's path marked `as const`, and record the narrower type the handler
@@ -257,6 +268,25 @@ export declare class TypeInferrer {
257
268
  */
258
269
  private receivingCallOf;
259
270
  private inferCallResult;
271
+ /**
272
+ * The terminal of a call's def-use walk is a zero-argument `.text()` read of
273
+ * the call's own result (carrick#1842): on its binding (`res.text()`,
274
+ * through a declaration, `await` or a cast), or in the callback a `then` on
275
+ * the call hands the response to (`fetch(u).then((res) => res.text())`).
276
+ */
277
+ private textReadAtTerminal;
278
+ /** `call` is `<receiver>.text()` with no arguments, and `accept` takes its receiver. */
279
+ private isTextReadOf;
280
+ /**
281
+ * The call's resolved signature, at this site, types a member of an object
282
+ * argument as exactly the literal `'text'`, and the source passes that
283
+ * literal there (carrick#1842). That is how a request library lets a caller
284
+ * choose a text body from the formats it offers (`{ type: 'text' }`): a
285
+ * generic config instantiated by the literal, or an overload taken by it.
286
+ * A member typed as a wider union (`kind: 'text' | 'image'`) chooses no
287
+ * format, and no member name is read.
288
+ */
289
+ private callChoosesTextBody;
260
290
  /**
261
291
  * What the source states the body read at `terminal` to be, when the type
262
292
  * `extractExplicitTypeFromAncestor` printed for it is stated AT the read
@@ -326,12 +356,54 @@ export declare class TypeInferrer {
326
356
  */
327
357
  private resultCarrierPayload;
328
358
  /**
329
- * `Future<T>` -> `T` for a promise-like of the source's own making, read off
330
- * the await protocol rather than a name: a `then` whose first parameter is a
331
- * callback, whose own first parameter is the value awaiting it yields.
332
- * `Promise` and `PromiseLike` are peeled by `unwrapPromiseType` before this.
359
+ * The shape test of `resultCarrierPayload`: the carrier `type` is, once a
360
+ * promise-like around it is peeled, and the type arguments a branch of it
361
+ * holds as a member. `undefined` when `type` is not a carrier.
362
+ */
363
+ private resultCarrierArguments;
364
+ /**
365
+ * The decided abstain of a call whose result carries transport the
366
+ * service's wrapper rules verify and read no payload out of (carrick#1841,
367
+ * carrick#1843): `unknown` with `machinery_envelope` at the root and no
368
+ * anchor. The root reason is what keeps the capture's own locator from
369
+ * re-reading the raw call (`inference_decided_no_contract`,
370
+ * engine/type_compat_v2.rs).
371
+ */
372
+ private transportAbstain;
373
+ /**
374
+ * `Future<T>` -> `T` for a thenable, read off the await protocol rather
375
+ * than a name: a `then` whose first parameter is a callback, whose own
376
+ * first parameter is the value awaiting it yields. `Promise` and
377
+ * `PromiseLike` are peeled by `unwrapPromiseType` before this.
378
+ *
379
+ * The callback is read through `null` and `undefined` (carrick#1877). A
380
+ * `then` of the source's own making declares `(value: T) => void`; the
381
+ * platform's declares `onfulfilled?: ((value: T) => ...) | null`, and that
382
+ * is the `then` a subclass of `Promise` inherits. Read as written, an
383
+ * optional, nullable callback has no call signature, and the subclass was
384
+ * not seen as a thenable at all.
385
+ *
386
+ * A `then` that cannot be called, or whose first parameter is no callback,
387
+ * is no protocol, and the type is returned as it is.
388
+ *
389
+ * The compiler's own awaited type arbitrates. This walk reads the first
390
+ * signature of `then`; the language reads all of them. Where awaiting the
391
+ * type and awaiting what this walk found are not the same thing to the
392
+ * compiler (an overloaded `then` whose first signature is not the one
393
+ * `await` takes), the walk did not read the protocol, and the type is
394
+ * returned as it is rather than published as a guess.
333
395
  */
334
396
  private unwrapThenableType;
397
+ /** The value `then`'s first signature hands its callback, read until it stops changing. */
398
+ private firstThenValue;
399
+ /**
400
+ * What `await` yields for `type` where the names do not say: `type` is,
401
+ * once `Promise` and `PromiseLike` are peeled, a thenable by the protocol
402
+ * (a subclass of `Promise`, a class with a `then` of its own). `undefined`
403
+ * where the names say it all or `type` is no thenable, so a caller keeps
404
+ * the reading it had (carrick#1877).
405
+ */
406
+ private awaitedBeyondPromise;
335
407
  /**
336
408
  * The platform's error shape, in full: `name` and `message` strings AND a
337
409
  * `stack`, which is what the `Error` interface declares and every subclass
@@ -349,10 +421,10 @@ export declare class TypeInferrer {
349
421
  * `query.data`, `envelope` in `envelope.list[0]` — or `undefined` when the
350
422
  * identifier names the value itself.
351
423
  *
352
- * A member CALL is not a projection: `res.text()` yields a body rather than
424
+ * A member CALL is not a projection: `res.blob()` yields a body rather than
353
425
  * 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.
426
+ * zero-argument json and text body reads have their own branch and are
427
+ * taken before this is asked.
356
428
  */
357
429
  private projectionOnReceiver;
358
430
  /**
@@ -431,6 +503,22 @@ export declare class TypeInferrer {
431
503
  * about the body and is skipped.
432
504
  */
433
505
  private castsOfUnreadParameter;
506
+ /**
507
+ * The zero-argument whole-body read taken in place on the value `callExpr`
508
+ * yields, or `undefined` (carrick#1851): `(await fetch(url)).text()`. The
509
+ * receiver is the call itself, through the wrappers that leave a value as
510
+ * it is (parentheses, `await`, `!`), so it is the same read
511
+ * `bodyReadOnReceiver` finds on a binding of that value. A call that is not
512
+ * awaited first is read the same way: a request that is a promise and reads
513
+ * its own body (`send(url).json()`) yields the body from that read too.
514
+ */
515
+ private bodyReadOnCallValue;
516
+ /**
517
+ * The zero-argument whole-body read that takes `receiver` as its receiver,
518
+ * `res.json()` or `res.text()`, or `undefined`. A text read is a body read
519
+ * like a json one (carrick#1842): without it, `return res.text()` left the
520
+ * walk on the response binding and published the transport object.
521
+ */
434
522
  private bodyReadOnReceiver;
435
523
  private collectDefUseNodes;
436
524
  private expressionUsesNames;
@@ -488,12 +576,27 @@ export declare class TypeInferrer {
488
576
  * Try to unwrap a type using a single ExtractionRule.
489
577
  */
490
578
  private tryUnwrapWithRule;
579
+ /**
580
+ * The names `type` goes by, own symbol first (carrick#1843).
581
+ *
582
+ * `type Task<A> = __Task<A>` has the class's symbol and arguments, and the
583
+ * alias's beside them. `type Reply<T> = { ... }` has the anonymous `__type`
584
+ * with no arguments of its own. `type Outcome<A, E> = Done<A, E> |
585
+ * Failed<A, E>` has no symbol of its own at all. In each, the alias and its
586
+ * arguments are what a rule naming `Task`, `Reply` or `Outcome` describes.
587
+ */
588
+ private wrapperReadings;
491
589
  /**
492
590
  * Extract the payload type from a matched wrapper. Returns null when the
493
591
  * rule matched the wrapper but no payload is recoverable from generics or
494
592
  * property paths — the caller decides what a payload-less match means
495
593
  * (verified machinery collapses to `unknown` after every rule has run;
496
594
  * a name-only match leaves the type untouched).
595
+ *
596
+ * `payloadGenericIndex` counts the arguments of `reading`, the name the
597
+ * rule matched (carrick#1843): an alias is free to order its parameters
598
+ * differently from the type it stands for, so the same index into the
599
+ * other list is a different argument.
497
600
  */
498
601
  private extractPayloadFromWrapper;
499
602
  /**
@@ -890,11 +993,19 @@ export declare class TypeInferrer {
890
993
  /**
891
994
  * Declaration file (absolute path) of the anchor symbol
892
995
  * `primaryTypeSymbol` reports for this type, or `undefined` when the type
893
- * has no user-facing anchor or no source declaration. The scanner's
894
- * pub/sub two-anchor arbitration (carrick#413) uses this to re-aim a
895
- * demoted explicit bundle request: the bundler resolves a `SymbolRequest`
896
- * only against declarations IN its `source_file`, so the request must
897
- * point at the file that actually declares the tsc-witnessed payload type.
996
+ * has no user-facing anchor or no source declaration. Every path that
997
+ * reports the symbol reports this beside it (carrick#1819): it is where a
998
+ * reader finds the type an anchor names. The scanner's pub/sub two-anchor
999
+ * arbitration (carrick#413) also uses it to re-aim a demoted explicit
1000
+ * bundle request: the bundler resolves a `SymbolRequest` only against
1001
+ * declarations IN its `source_file`, so the request must point at the file
1002
+ * that actually declares the tsc-witnessed payload type.
1003
+ *
1004
+ * The declarations read are those of the type's own symbol, so a name
1005
+ * imported through a barrel reports the file that declares it, and a name
1006
+ * two files declare reports the one this type resolves to. A declaration in
1007
+ * an installed package or a TypeScript lib is reported as it is found, as
1008
+ * an absolute path; what a reader is shown for it is the scanner's call.
898
1009
  *
899
1010
  * Only declaration kinds the bundler's `validateSymbols` can resolve
900
1011
  * (interface, type alias, class, enum, function, variable) count. A