carrick 0.3.126 → 0.3.127

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carrick",
3
- "version": "0.3.126",
3
+ "version": "0.3.127",
4
4
  "carrickCache": {
5
5
  "cache_version": 22,
6
6
  "intent_cache_version": 2
@@ -113,11 +113,11 @@
113
113
  "zod": "^3.23.0"
114
114
  },
115
115
  "optionalDependencies": {
116
- "@carrick-tools/cli-darwin-arm64": "0.3.126",
117
- "@carrick-tools/cli-darwin-x64": "0.3.126",
118
- "@carrick-tools/cli-linux-arm64": "0.3.126",
119
- "@carrick-tools/cli-linux-x64": "0.3.126",
120
- "@carrick-tools/cli-win32-x64": "0.3.126"
116
+ "@carrick-tools/cli-darwin-arm64": "0.3.127",
117
+ "@carrick-tools/cli-darwin-x64": "0.3.127",
118
+ "@carrick-tools/cli-linux-arm64": "0.3.127",
119
+ "@carrick-tools/cli-linux-x64": "0.3.127",
120
+ "@carrick-tools/cli-win32-x64": "0.3.127"
121
121
  },
122
122
  "devDependencies": {
123
123
  "@types/node": "^24.13.3"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "carrick",
3
3
  "description": "Claude Code plugin for Carrick, which indexes TypeScript codebases across service and repository boundaries. After each edit it adds the file's routes, calls, and cross-service type mismatches to the session, and it registers Carrick's language server. It pairs with the Carrick MCP server, which lets agents search functions by intent rather than name.",
4
- "version": "0.3.126",
4
+ "version": "0.3.127",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -56,6 +56,18 @@ function initializedLoader() {
56
56
  }
57
57
  return projectLoader;
58
58
  }
59
+ /**
60
+ * carrick#2100: the unwidened reading's budget in milliseconds, when
61
+ * `CARRICK_UNWIDENED_BUDGET_MS` sets one. A switch for tests that drive the
62
+ * scanner's binary: it forces the budget to run out on any machine. Unset or
63
+ * not a whole number of milliseconds, the inferrer's default stands.
64
+ */
65
+ function unwidenedBudgetFromEnv() {
66
+ const value = process.env.CARRICK_UNWIDENED_BUDGET_MS;
67
+ if (value === undefined || !/^\d+$/.test(value.trim()))
68
+ return undefined;
69
+ return Number(value.trim());
70
+ }
59
71
  /**
60
72
  * Get the project-backed components, building the project if this is the
61
73
  * first request that needs it.
@@ -78,6 +90,7 @@ function projectComponents(key = '') {
78
90
  project,
79
91
  repoRoot,
80
92
  packageOf: (filePath) => loader.packageOf(filePath),
93
+ unwidenedBudgetMs: unwidenedBudgetFromEnv(),
81
94
  });
82
95
  built = {
83
96
  typeBundler: new TypeBundler({ project, repoRoot }),
@@ -456,6 +456,7 @@ export class LibraryClaimsVerifier {
456
456
  makerBindsName,
457
457
  scoped: receiverPath.scope !== undefined,
458
458
  makerHolding,
459
+ madeBy: receiverPath.maker?.member === null && receiverPath.scope === undefined ? check.export : undefined,
459
460
  })),
460
461
  };
461
462
  };
@@ -1735,6 +1736,15 @@ class DeclarationReader {
1735
1736
  return true;
1736
1737
  if (type.isUnionOrIntersection())
1737
1738
  return type.types.some(part => this.mentions(part, parameter, depth + 1));
1739
+ // A deferred conditional (`Server extends X ? A : B`) or an alias
1740
+ // instance names its parameter in its check type or its alias arguments
1741
+ // (carrick#2663).
1742
+ if (type.aliasTypeArguments?.some(argument => this.mentions(argument, parameter, depth + 1)))
1743
+ return true;
1744
+ if (type.flags & ts.TypeFlags.Conditional) {
1745
+ const conditional = type;
1746
+ return [conditional.checkType, conditional.extendsType].some(part => this.mentions(part, parameter, depth + 1));
1747
+ }
1738
1748
  const reference = this.referenceOf(type);
1739
1749
  if (reference !== undefined) {
1740
1750
  return this.checker.getTypeArguments(reference).some(argument => this.mentions(argument, parameter, depth + 1));
@@ -1927,11 +1937,17 @@ class DeclarationReader {
1927
1937
  return failure.verdict === 'failed' ? failed(reason) : unchecked(reason);
1928
1938
  }
1929
1939
  /**
1930
- * `yields: { param, export, side }` on a receive op: some signature of the
1931
- * handler this overload takes declares a parameter at `param` whose type
1932
- * is the package's own export named `export` (a class, or the interface
1933
- * an instance has), declared in the receiver's home packages. `side` is
1934
- * `client` or `server` (`yields_side_invalid`). Undefined when it holds.
1940
+ * `yields: { param, export, side }` on a receive op or a scope: some
1941
+ * signature of the function this overload takes declares a parameter at
1942
+ * `param` whose type is either
1943
+ * - the package's own export named `export` (a class, or the interface an
1944
+ * instance has), declared in the receiver's home packages; or
1945
+ * - when `export` is the maker whose own call made the instance the claim
1946
+ * is read on (`madeBy`, carrick#2663): that instance, compared by
1947
+ * declaration (`handsInstance`), since a factory's result has no export
1948
+ * name of its own.
1949
+ * `side` is `client` or `server` (`yields_side_invalid`). Undefined when it
1950
+ * holds.
1935
1951
  */
1936
1952
  yieldsOutcome(signature, handler, yields, receiver) {
1937
1953
  if (yields.side !== 'client' && yields.side !== 'server')
@@ -1955,10 +1971,44 @@ class DeclarationReader {
1955
1971
  (symbol.declarations ?? []).some(declaration => this.isOwnFile(declaration.getSourceFile(), receiver.home)));
1956
1972
  if (named)
1957
1973
  return undefined;
1974
+ if (yields.export === receiver.madeBy && this.handsInstance(given, receiver, signature.typeParameters ?? [])) {
1975
+ return undefined;
1976
+ }
1958
1977
  }
1959
1978
  }
1960
1979
  return failed('yields_not_export');
1961
1980
  }
1981
+ /**
1982
+ * `given` (each part, nullish dropped) is the instance `receiver` holds:
1983
+ * some part of its intersection is a class or interface the receiver's
1984
+ * home packages declare, and the same declaration (the same symbol, never
1985
+ * the same name) as a part of the instance's own intersection. A generic
1986
+ * instance is built at its defaults while `given` threads the overload's
1987
+ * own type parameters (`own`) through its arguments, so the arguments are
1988
+ * read as `fitsErased` reads a mounted instance (carrick#2652): a
1989
+ * definite mismatch refuses, an argument it cannot read leaves the
1990
+ * declaration to decide.
1991
+ */
1992
+ handsInstance(given, receiver, own) {
1993
+ const declared = (type) => {
1994
+ const symbol = type.getSymbol();
1995
+ if (!symbol || !(symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Interface)))
1996
+ return undefined;
1997
+ const home = (symbol.declarations ?? []).some(declaration => this.isOwnFile(declaration.getSourceFile(), receiver.home));
1998
+ return home ? symbol : undefined;
1999
+ };
2000
+ const intersected = (type) => (type.isIntersection() ? type.types : [type]);
2001
+ const instances = this.parts(receiver.type)
2002
+ .flatMap(intersected)
2003
+ .filter(part => declared(part) !== undefined);
2004
+ const parts = this.parts(given);
2005
+ return (parts.length > 0 &&
2006
+ parts.every(part => intersected(part).some(handed => {
2007
+ const symbol = declared(handed);
2008
+ return (symbol !== undefined &&
2009
+ instances.some(instance => declared(instance) === symbol && this.fitsErased(instance, handed, own) !== false));
2010
+ })));
2011
+ }
1962
2012
  /**
1963
2013
  * `request: { param, path?, url?, method? }` on a receive op (#2474 R0):
1964
2014
  * some signature of the handler this overload takes declares a parameter
@@ -548,14 +548,8 @@ function bodyReadsOf(call) {
548
548
  const chained = bodyReadOn(top);
549
549
  if (chained)
550
550
  return checkedBodyReads([chained]);
551
- const declaration = top.getParent();
552
- if (!declaration ||
553
- !Node.isVariableDeclaration(declaration) ||
554
- declaration.getInitializer() !== top) {
555
- return [];
556
- }
557
- const name = declaration.getNameNode();
558
- if (!Node.isIdentifier(name))
551
+ const name = responseBinding(top);
552
+ if (!name)
559
553
  return [];
560
554
  const reads = [];
561
555
  let handedOn = false;
@@ -583,6 +577,92 @@ function bodyReadsOf(call) {
583
577
  }
584
578
  return checkedBodyReads(reads);
585
579
  }
580
+ /**
581
+ * The name the located call's response is bound to, when the source binds it
582
+ * in one of three ways: `const res = await call`, the first parameter of a
583
+ * `.then` callback (`call.then((res) => res.json())`), or an element of an
584
+ * array destructured off `Promise.all` (`const [a, b] = await Promise.all([call, ...])`).
585
+ */
586
+ function responseBinding(top) {
587
+ const declaration = top.getParent();
588
+ if (declaration && Node.isVariableDeclaration(declaration) && declaration.getInitializer() === top) {
589
+ const name = declaration.getNameNode();
590
+ return Node.isIdentifier(name) ? name : undefined;
591
+ }
592
+ return thenParameter(top) ?? promiseAllElement(top);
593
+ }
594
+ /** `top.then((res) => ...)`: the callback's first parameter. */
595
+ function thenParameter(top) {
596
+ const access = top.getParent();
597
+ if (!access || !Node.isPropertyAccessExpression(access) || access.getExpression() !== top) {
598
+ return undefined;
599
+ }
600
+ if (access.getName() !== 'then')
601
+ return undefined;
602
+ const then = access.getParent();
603
+ if (!then || !Node.isCallExpression(then) || then.getExpression() !== access)
604
+ return undefined;
605
+ const callback = then.getArguments()[0];
606
+ if (!callback || !(Node.isArrowFunction(callback) || Node.isFunctionExpression(callback))) {
607
+ return undefined;
608
+ }
609
+ const name = callback.getParameters()[0]?.getNameNode();
610
+ return name && Node.isIdentifier(name) ? name : undefined;
611
+ }
612
+ /**
613
+ * The name `top` is destructured to, when `top` is one element of the array
614
+ * literal handed to the language library's `Promise.all` and the call's
615
+ * awaited result is destructured to names:
616
+ * `const [a, b] = await Promise.all([call, other])`. An element after a
617
+ * spread has no position the destructuring can be read against.
618
+ */
619
+ function promiseAllElement(top) {
620
+ const array = top.getParent();
621
+ if (!array || !Node.isArrayLiteralExpression(array))
622
+ return undefined;
623
+ const elements = array.getElements();
624
+ const index = elements.indexOf(top);
625
+ if (index < 0 || elements.slice(0, index + 1).some((e) => Node.isSpreadElement(e))) {
626
+ return undefined;
627
+ }
628
+ const all = array.getParent();
629
+ if (!all ||
630
+ !Node.isCallExpression(all) ||
631
+ all.getArguments()[0] !== array ||
632
+ !isLibraryPromiseAll(all)) {
633
+ return undefined;
634
+ }
635
+ let result = all;
636
+ while (Node.isAwaitExpression(result.getParentOrThrow()) ||
637
+ Node.isParenthesizedExpression(result.getParentOrThrow())) {
638
+ result = result.getParentOrThrow();
639
+ }
640
+ const declaration = result.getParent();
641
+ if (!declaration || !Node.isVariableDeclaration(declaration) || declaration.getInitializer() !== result) {
642
+ return undefined;
643
+ }
644
+ const pattern = declaration.getNameNode();
645
+ if (!Node.isArrayBindingPattern(pattern))
646
+ return undefined;
647
+ const element = pattern.getElements()[index];
648
+ if (!element || !Node.isBindingElement(element) || element.getDotDotDotToken())
649
+ return undefined;
650
+ const name = element.getNameNode();
651
+ return Node.isIdentifier(name) ? name : undefined;
652
+ }
653
+ /** `Promise.all` as the language library declares it, not a lookalike. */
654
+ function isLibraryPromiseAll(call) {
655
+ const callee = call.getExpression();
656
+ if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== 'all')
657
+ return false;
658
+ const declaration = resolvedDeclaration(call);
659
+ if (!declaration)
660
+ return false;
661
+ return call
662
+ .getProject()
663
+ .getProgram()
664
+ .compilerObject.isSourceFileDefaultLibrary(declaration.getSourceFile());
665
+ }
586
666
  /**
587
667
  * Why these body reads cannot be retyped, or the reads to retype.
588
668
  *
@@ -167,7 +167,10 @@ export declare class TypeInferrer {
167
167
  * read is the project the scan loaded.
168
168
  *
169
169
  * Nothing is read past `deadline`: a batch that runs out keeps the readings
170
- * it made and publishes none for the rest.
170
+ * it made and publishes none for the rest, and marks each of the rest
171
+ * `unwidened_out_of_budget` (carrick#2100). A request that never needed a
172
+ * reading (a declared return, nothing to mark) or lost it for a reason of
173
+ * its own (a marking that added a diagnostic) is never marked.
171
174
  */
172
175
  private addUnwidenedReadings;
173
176
  /**
@@ -295,6 +298,17 @@ export declare class TypeInferrer {
295
298
  * when the located expression is not part of what the handler returns.
296
299
  */
297
300
  private returnedSendHolding;
301
+ /**
302
+ * True when `node` is a bare reference to a parameter of the handler the
303
+ * route registration on the request's line hands in (found as
304
+ * `registrationAtLine` finds it, wrappers included): the request, the reply
305
+ * or the context the framework passes, never a body the route sends
306
+ * (carrick-cloud#2576). Where the parameter's type is declared is not read:
307
+ * a parameter of any other function is data, whether the repo or an
308
+ * installed package types it (`.then((account) => reply.send(account))`, a
309
+ * helper's `account: Account`).
310
+ */
311
+ private isRouteHandlerParameter;
298
312
  /**
299
313
  * True when a located call's own result is the route's payload, so the
300
314
  * transitional drill into its first argument must not run (carrick#1732).
@@ -1703,11 +1717,14 @@ export declare class TypeInferrer {
1703
1717
  */
1704
1718
  private registrationHandlerAt;
1705
1719
  /**
1706
- * The handler function referenced by a route-registration call: the first
1720
+ * The handler function referenced by a route-registration call: the LAST
1707
1721
  * argument that is an inline function, or an identifier bound to a function
1708
- * (typically the 2nd+ arg — the path literal is not function-valued, so it is
1709
- * skipped naturally). No callee-name check: a call whose argument resolves to
1710
- * a function is structurally a registration regardless of the framework.
1722
+ * (carrick#2472 P3-2b). The functions before it are middleware
1723
+ * (`route(path).post(auth, audit, handler)`), which runs first and hands on;
1724
+ * reading the first function read a middleware as the route. The path
1725
+ * literal is not function-valued, so it is skipped naturally. No callee-name
1726
+ * check: a call whose argument resolves to a function is structurally a
1727
+ * registration regardless of the framework.
1711
1728
  */
1712
1729
  private handlerFromRegistrationCall;
1713
1730
  /**
@@ -1722,16 +1739,32 @@ export declare class TypeInferrer {
1722
1739
  * declaration (via the compiler's definition nodes) and returned when that
1723
1740
  * declaration is — or initializes to — a function. Anything else (a path
1724
1741
  * literal, an object, a non-function binding) yields undefined.
1742
+ * `followWrapperCalls` false reads a binding to a wrapper call as no
1743
+ * function, so a name the wrapper walk meets is never walked again.
1725
1744
  */
1726
1745
  private asHandlerFunction;
1727
1746
  /**
1728
1747
  * Extract a `FunctionLike` from a declaration node the compiler resolved an
1729
1748
  * identifier to: a function declaration is itself the function; a variable
1730
1749
  * declaration / binding whose initializer is an inline function yields that
1731
- * function (`const h = async () => { … }`). Import/re-export shims are walked
1732
- * by ts-morph's `getDefinitionNodes`, so no manual import chasing is needed.
1750
+ * function (`const h = async () => { … }`), and one whose initializer is a
1751
+ * wrapper call yields the function the call wraps
1752
+ * (`const h = wrap<Contract>(async (req, res) => { … })`, carrick#2472
1753
+ * P3-2b; `functionWrappedBy`). Import/re-export shims are walked by
1754
+ * ts-morph's `getDefinitionNodes`, so no manual import chasing is needed.
1733
1755
  */
1734
1756
  private functionFromDeclaration;
1757
+ /**
1758
+ * The function a call wraps, for a name bound to that call and used as a
1759
+ * handler (carrick#2472 P3-2b): the call's one argument that is a function
1760
+ * literal, a name bound to one, or another such call, a few calls deep.
1761
+ *
1762
+ * Two conditions keep this to wrappers. The call's own value has to be
1763
+ * callable, because the name is used as a function: `rows.map((row) => …)`
1764
+ * is handed a function and yields a list. And exactly one argument may be a
1765
+ * function: with two there is no saying which of them is wrapped.
1766
+ */
1767
+ private functionWrappedBy;
1735
1768
  /**
1736
1769
  * True when `handler` reads nothing from the incoming message (carrick#827):
1737
1770
  * every use of one of its parameters, named or destructured, is the receiver
@@ -1952,6 +1985,12 @@ export declare class TypeInferrer {
1952
1985
  * below reads it.
1953
1986
  */
1954
1987
  private bodyBuiltByCallee;
1988
+ /**
1989
+ * `bodyBuiltByCallee`, telling apart a callee that sends the argument
1990
+ * unchanged (`passes`) from one that states nothing about it (`none`).
1991
+ * `depth` counts the forwarded locals followed (carrick#2671).
1992
+ */
1993
+ private calleeBodyRead;
1955
1994
  /**
1956
1995
  * The request body value `use` is part of, inside `scope`: the value of a
1957
1996
  * body-named object member, or the argument at a body slot of a call,
@@ -1959,6 +1998,15 @@ export declare class TypeInferrer {
1959
1998
  * around it. `use` itself when it is that value whole.
1960
1999
  */
1961
2000
  private bodyValueAround;
2001
+ /**
2002
+ * carrick#2671: the `const` local whose initializer holds `use`, inside
2003
+ * `scope`, when `scope` sends that local unchanged as the body: whole in a
2004
+ * body slot, or as an argument into a function that passes it on as its
2005
+ * body. The local's name, so its declared type is read. Undefined when the
2006
+ * local is never sent, when a further body is built from it, or when a
2007
+ * function lies between `use` and the declaration.
2008
+ */
2009
+ private forwardedLocalHolding;
1962
2010
  /** `value` is `use` behind what `sentValue` sees through. */
1963
2011
  private isSameValue;
1964
2012
  /**
@@ -176,6 +176,12 @@ const RESPONSE_INIT_MEMBER_NAMES = new Set([
176
176
  * two; the bound is a backstop against a pathologically nested return.
177
177
  */
178
178
  const RESPONSE_HELPER_MAX_DEPTH = 4;
179
+ /**
180
+ * How many wrapper calls a handler bound to one is followed through
181
+ * (carrick#2472 P3-2b): `wrap(trace(async (req, res) => …))` needs two. A
182
+ * backstop against a pathologically nested initializer.
183
+ */
184
+ const WRAPPER_CALL_MAX_DEPTH = 3;
179
185
  /**
180
186
  * Members an options object uses to state an HTTP status. A >= 400 status marks
181
187
  * the branch an error path, whose shape is not the endpoint's contract.
@@ -620,13 +626,19 @@ export class TypeInferrer {
620
626
  * read is the project the scan loaded.
621
627
  *
622
628
  * Nothing is read past `deadline`: a batch that runs out keeps the readings
623
- * it made and publishes none for the rest.
629
+ * it made and publishes none for the rest, and marks each of the rest
630
+ * `unwidened_out_of_budget` (carrick#2100). A request that never needed a
631
+ * reading (a declared return, nothing to mark) or lost it for a reason of
632
+ * its own (a marking that added a diagnostic) is never marked.
624
633
  */
625
634
  addUnwidenedReadings(responses, extractionConfig, deadline) {
626
- const outOfTime = (stage) => {
635
+ const outOfTime = (stage, unread) => {
627
636
  if (performance.now() <= deadline)
628
637
  return false;
629
- this.log(`Unwidened reading stopped ${stage}: it ran out of its budget`);
638
+ this.log(`Unwidened reading stopped ${stage}: it ran out of its budget, ` +
639
+ `${unread.length} reading(s) not made`);
640
+ for (const plan of unread)
641
+ plan.result.unwidened_out_of_budget = true;
630
642
  return true;
631
643
  };
632
644
  const plans = [];
@@ -685,7 +697,7 @@ export class TypeInferrer {
685
697
  insertions.set(file, normalise(list));
686
698
  const before = new Map();
687
699
  for (const file of checkedFiles) {
688
- if (outOfTime('before the rewrite'))
700
+ if (outOfTime('before the rewrite', plans))
689
701
  return;
690
702
  before.set(file, fileDiagnostics(file));
691
703
  }
@@ -701,7 +713,7 @@ export class TypeInferrer {
701
713
  // holds it loses its reading.
702
714
  const tainted = new Set();
703
715
  for (const file of checkedFiles) {
704
- if (outOfTime('while checking the rewrite'))
716
+ if (outOfTime('while checking the rewrite', plans))
705
717
  return;
706
718
  const list = insertions.get(file) ?? [];
707
719
  for (const at of addedDiagnostics(before.get(file), fileDiagnostics(file), list)) {
@@ -713,9 +725,10 @@ export class TypeInferrer {
713
725
  }
714
726
  }
715
727
  }
716
- for (const plan of plans) {
717
- if (outOfTime('while re-reading'))
728
+ for (const [at, plan] of plans.entries()) {
729
+ if (outOfTime('while re-reading', plans.slice(at).filter((rest) => !tainted.has(rest)))) {
718
730
  return;
731
+ }
719
732
  if (tainted.has(plan)) {
720
733
  this.log(`Unwidened reading for ${plan.request.file_path}:${plan.request.line_number} ` +
721
734
  'dropped: marking its literals added a diagnostic on its path');
@@ -901,6 +914,16 @@ export class TypeInferrer {
901
914
  if (this.serialisedBodyEnvelopeEngages(awaitedType, func)) {
902
915
  return this.inferSerialisedBodyEnvelope(request, func);
903
916
  }
917
+ // A route handler that returns the reply it was handed (`return reply`)
918
+ // states no body by that return; the reply's type is built from the
919
+ // route's request type argument (carrick-cloud#2576).
920
+ const returned = this.returnedExpressions(func);
921
+ if (returned.length > 0 &&
922
+ returned.every((expression) => this.isRouteHandlerParameter(this.unwrapExpressionNode(expression), request))) {
923
+ this.log(`Return at ${request.file_path}:${request.line_number} is an object the ` +
924
+ 'framework handed the handler; it states no body');
925
+ return null;
926
+ }
904
927
  const unwrapResult = this.unwrapTypeWithConfig(awaitedType, func, extractionConfig);
905
928
  if (unwrapResult.wasUnwrapped && !unwrapResult.verifiedMachinery) {
906
929
  typeString = unwrapResult.typeString;
@@ -1278,6 +1301,16 @@ export class TypeInferrer {
1278
1301
  payloadNode = args[0];
1279
1302
  }
1280
1303
  }
1304
+ // A parameter the route registration hands its handler is never the
1305
+ // route's body: not the reply a handler returns (`return reply`), and not
1306
+ // the request the drill reaches in a call that forwards it
1307
+ // (`return finish(request, reply)`), whose type the framework built from
1308
+ // the route's request type argument (carrick-cloud#2576).
1309
+ if (this.isRouteHandlerParameter(payloadNode, request)) {
1310
+ this.log(`Response payload at ${request.file_path}:${request.line_number} is an object ` +
1311
+ 'the framework handed the handler; it states no body');
1312
+ return null;
1313
+ }
1281
1314
  const payloadType = payloadNode.getType();
1282
1315
  let typeString = typeText(payloadType, payloadNode);
1283
1316
  const unwrapResult = this.unwrapTypeWithConfig(payloadType, payloadNode, extractionConfig);
@@ -1447,6 +1480,30 @@ export class TypeInferrer {
1447
1480
  }
1448
1481
  return this.receivingCallOf(located, isReturned);
1449
1482
  }
1483
+ /**
1484
+ * True when `node` is a bare reference to a parameter of the handler the
1485
+ * route registration on the request's line hands in (found as
1486
+ * `registrationAtLine` finds it, wrappers included): the request, the reply
1487
+ * or the context the framework passes, never a body the route sends
1488
+ * (carrick-cloud#2576). Where the parameter's type is declared is not read:
1489
+ * a parameter of any other function is data, whether the repo or an
1490
+ * installed package types it (`.then((account) => reply.send(account))`, a
1491
+ * helper's `account: Account`).
1492
+ */
1493
+ isRouteHandlerParameter(node, request) {
1494
+ if (!Node.isIdentifier(node))
1495
+ return false;
1496
+ const owners = (node.getSymbol()?.getDeclarations() ?? [])
1497
+ .filter((decl) => Node.isParameterDeclaration(decl))
1498
+ .map((decl) => decl.getParent().compilerNode);
1499
+ if (owners.length === 0)
1500
+ return false;
1501
+ const sourceFile = this.project.getSourceFile(request.file_path);
1502
+ if (!sourceFile)
1503
+ return false;
1504
+ const handler = this.registrationAtLine(sourceFile, request.line_number)?.handler;
1505
+ return handler !== undefined && owners.includes(handler.compilerNode);
1506
+ }
1450
1507
  /**
1451
1508
  * True when a located call's own result is the route's payload, so the
1452
1509
  * transitional drill into its first argument must not run (carrick#1732).
@@ -6305,14 +6362,17 @@ export class TypeInferrer {
6305
6362
  return undefined;
6306
6363
  }
6307
6364
  /**
6308
- * The handler function referenced by a route-registration call: the first
6365
+ * The handler function referenced by a route-registration call: the LAST
6309
6366
  * argument that is an inline function, or an identifier bound to a function
6310
- * (typically the 2nd+ arg — the path literal is not function-valued, so it is
6311
- * skipped naturally). No callee-name check: a call whose argument resolves to
6312
- * a function is structurally a registration regardless of the framework.
6367
+ * (carrick#2472 P3-2b). The functions before it are middleware
6368
+ * (`route(path).post(auth, audit, handler)`), which runs first and hands on;
6369
+ * reading the first function read a middleware as the route. The path
6370
+ * literal is not function-valued, so it is skipped naturally. No callee-name
6371
+ * check: a call whose argument resolves to a function is structurally a
6372
+ * registration regardless of the framework.
6313
6373
  */
6314
6374
  handlerFromRegistrationCall(call) {
6315
- for (const arg of call.getArguments()) {
6375
+ for (const arg of [...call.getArguments()].reverse()) {
6316
6376
  const handler = this.asHandlerFunction(arg);
6317
6377
  if (handler) {
6318
6378
  return handler;
@@ -6351,15 +6411,17 @@ export class TypeInferrer {
6351
6411
  * declaration (via the compiler's definition nodes) and returned when that
6352
6412
  * declaration is — or initializes to — a function. Anything else (a path
6353
6413
  * literal, an object, a non-function binding) yields undefined.
6414
+ * `followWrapperCalls` false reads a binding to a wrapper call as no
6415
+ * function, so a name the wrapper walk meets is never walked again.
6354
6416
  */
6355
- asHandlerFunction(node) {
6417
+ asHandlerFunction(node, followWrapperCalls = true) {
6356
6418
  const expr = this.unwrapExpressionNode(node);
6357
6419
  if (Node.isArrowFunction(expr) || Node.isFunctionExpression(expr)) {
6358
6420
  return expr;
6359
6421
  }
6360
6422
  if (Node.isIdentifier(expr)) {
6361
6423
  for (const def of expr.getDefinitionNodes()) {
6362
- const func = this.functionFromDeclaration(def);
6424
+ const func = this.functionFromDeclaration(def, followWrapperCalls);
6363
6425
  if (func) {
6364
6426
  return func;
6365
6427
  }
@@ -6371,10 +6433,13 @@ export class TypeInferrer {
6371
6433
  * Extract a `FunctionLike` from a declaration node the compiler resolved an
6372
6434
  * identifier to: a function declaration is itself the function; a variable
6373
6435
  * declaration / binding whose initializer is an inline function yields that
6374
- * function (`const h = async () => { … }`). Import/re-export shims are walked
6375
- * by ts-morph's `getDefinitionNodes`, so no manual import chasing is needed.
6436
+ * function (`const h = async () => { … }`), and one whose initializer is a
6437
+ * wrapper call yields the function the call wraps
6438
+ * (`const h = wrap<Contract>(async (req, res) => { … })`, carrick#2472
6439
+ * P3-2b; `functionWrappedBy`). Import/re-export shims are walked by
6440
+ * ts-morph's `getDefinitionNodes`, so no manual import chasing is needed.
6376
6441
  */
6377
- functionFromDeclaration(decl) {
6442
+ functionFromDeclaration(decl, followWrapperCalls = true) {
6378
6443
  if (Node.isFunctionDeclaration(decl)) {
6379
6444
  return decl;
6380
6445
  }
@@ -6399,10 +6464,43 @@ export class TypeInferrer {
6399
6464
  if (Node.isArrowFunction(inner) || Node.isFunctionExpression(inner)) {
6400
6465
  return inner;
6401
6466
  }
6467
+ if (followWrapperCalls && Node.isCallExpression(inner)) {
6468
+ return this.functionWrappedBy(inner, 0);
6469
+ }
6402
6470
  }
6403
6471
  }
6404
6472
  return undefined;
6405
6473
  }
6474
+ /**
6475
+ * The function a call wraps, for a name bound to that call and used as a
6476
+ * handler (carrick#2472 P3-2b): the call's one argument that is a function
6477
+ * literal, a name bound to one, or another such call, a few calls deep.
6478
+ *
6479
+ * Two conditions keep this to wrappers. The call's own value has to be
6480
+ * callable, because the name is used as a function: `rows.map((row) => …)`
6481
+ * is handed a function and yields a list. And exactly one argument may be a
6482
+ * function: with two there is no saying which of them is wrapped.
6483
+ */
6484
+ functionWrappedBy(call, depth) {
6485
+ if (depth >= WRAPPER_CALL_MAX_DEPTH)
6486
+ return undefined;
6487
+ if (!this.isCallableType(call.getType()))
6488
+ return undefined;
6489
+ const wrapped = [];
6490
+ for (const arg of call.getArguments()) {
6491
+ const value = this.unwrapExpressionNode(arg);
6492
+ const func = Node.isArrowFunction(value) || Node.isFunctionExpression(value)
6493
+ ? value
6494
+ : Node.isCallExpression(value)
6495
+ ? this.functionWrappedBy(value, depth + 1)
6496
+ : Node.isIdentifier(value)
6497
+ ? this.asHandlerFunction(value, false)
6498
+ : undefined;
6499
+ if (func)
6500
+ wrapped.push(func);
6501
+ }
6502
+ return wrapped.length === 1 ? wrapped[0] : undefined;
6503
+ }
6406
6504
  /**
6407
6505
  * True when `handler` reads nothing from the incoming message (carrick#827):
6408
6506
  * every use of one of its parameters, named or destructured, is the receiver
@@ -7045,6 +7143,15 @@ export class TypeInferrer {
7045
7143
  * below reads it.
7046
7144
  */
7047
7145
  bodyBuiltByCallee(node) {
7146
+ const read = this.calleeBodyRead(node, 0);
7147
+ return read.kind === 'passes' ? { kind: 'none' } : read;
7148
+ }
7149
+ /**
7150
+ * `bodyBuiltByCallee`, telling apart a callee that sends the argument
7151
+ * unchanged (`passes`) from one that states nothing about it (`none`).
7152
+ * `depth` counts the forwarded locals followed (carrick#2671).
7153
+ */
7154
+ calleeBodyRead(node, depth) {
7048
7155
  const own = node.getType();
7049
7156
  if (own.isAny() || own.isUnknown())
7050
7157
  return { kind: 'none' };
@@ -7107,6 +7214,15 @@ export class TypeInferrer {
7107
7214
  built.push(value);
7108
7215
  continue;
7109
7216
  }
7217
+ // carrick#2671: the use builds a local (`const payload = { ...input,
7218
+ // code: norm(input.code) }`) the function then sends unchanged, itself
7219
+ // or through a helper that passes it on as the body. The body is that
7220
+ // local.
7221
+ const forwarded = this.forwardedLocalHolding(use, body, depth);
7222
+ if (forwarded) {
7223
+ built.push(forwarded);
7224
+ continue;
7225
+ }
7110
7226
  const ancestors = use.getAncestors();
7111
7227
  const insideLiteral = ancestors
7112
7228
  .slice(0, ancestors.indexOf(body))
@@ -7119,7 +7235,7 @@ export class TypeInferrer {
7119
7235
  }
7120
7236
  }
7121
7237
  if (built.length === 0)
7122
- return { kind: 'none' };
7238
+ return passedThrough ? { kind: 'passes' } : { kind: 'none' };
7123
7239
  if (passedThrough) {
7124
7240
  return { kind: 'abstain', why: `the called function both sends the argument and builds a body from it (${from})` };
7125
7241
  }
@@ -7170,6 +7286,78 @@ export class TypeInferrer {
7170
7286
  }
7171
7287
  return undefined;
7172
7288
  }
7289
+ /**
7290
+ * carrick#2671: the `const` local whose initializer holds `use`, inside
7291
+ * `scope`, when `scope` sends that local unchanged as the body: whole in a
7292
+ * body slot, or as an argument into a function that passes it on as its
7293
+ * body. The local's name, so its declared type is read. Undefined when the
7294
+ * local is never sent, when a further body is built from it, or when a
7295
+ * function lies between `use` and the declaration.
7296
+ */
7297
+ forwardedLocalHolding(use, scope, depth) {
7298
+ if (depth >= 2)
7299
+ return undefined;
7300
+ let current = use;
7301
+ let declaration;
7302
+ while (current !== scope) {
7303
+ const parent = current.getParent();
7304
+ if (!parent)
7305
+ return undefined;
7306
+ if (Node.isArrowFunction(parent) ||
7307
+ Node.isFunctionExpression(parent) ||
7308
+ Node.isFunctionDeclaration(parent) ||
7309
+ Node.isMethodDeclaration(parent)) {
7310
+ return undefined;
7311
+ }
7312
+ if (Node.isVariableDeclaration(parent) && parent.getInitializer() === current) {
7313
+ declaration = parent;
7314
+ break;
7315
+ }
7316
+ current = parent;
7317
+ }
7318
+ if (!declaration)
7319
+ return undefined;
7320
+ if (declaration.getVariableStatement()?.getDeclarationKind() !== VariableDeclarationKind.Const) {
7321
+ return undefined;
7322
+ }
7323
+ const name = declaration.getNameNode();
7324
+ if (!Node.isIdentifier(name))
7325
+ return undefined;
7326
+ const symbol = name.getSymbol()?.compilerSymbol;
7327
+ if (!symbol)
7328
+ return undefined;
7329
+ const checker = this.project.getTypeChecker().compilerObject;
7330
+ let sent = false;
7331
+ for (const ref of scope.getDescendantsOfKind(SyntaxKind.Identifier)) {
7332
+ if (ref === name)
7333
+ continue;
7334
+ const parent = ref.getParent();
7335
+ const refSymbol = parent && Node.isShorthandPropertyAssignment(parent)
7336
+ ? checker.getShorthandAssignmentValueSymbol(parent.compilerNode)
7337
+ : ref.getSymbol()?.compilerSymbol;
7338
+ if (refSymbol !== symbol)
7339
+ continue;
7340
+ if (this.inSendNothingTest(ref, scope))
7341
+ continue;
7342
+ const value = this.bodyValueAround(ref, scope);
7343
+ if (value === ref) {
7344
+ sent = true;
7345
+ continue;
7346
+ }
7347
+ if (value)
7348
+ return undefined;
7349
+ if (parent && Node.isCallExpression(parent) && parent.getArguments().includes(ref)) {
7350
+ const onward = this.calleeBodyRead(ref, depth + 1);
7351
+ if (onward.kind === 'passes') {
7352
+ sent = true;
7353
+ continue;
7354
+ }
7355
+ if (onward.kind !== 'none')
7356
+ return undefined;
7357
+ }
7358
+ }
7359
+ return sent ? name : undefined;
7360
+ }
7173
7361
  /** `value` is `use` behind what `sentValue` sees through. */
7174
7362
  isSameValue(value, use) {
7175
7363
  return this.sentValue(value) === use;
@@ -1113,6 +1113,14 @@ export interface InferredType {
1113
1113
  * was dropped as unsound.
1114
1114
  */
1115
1115
  unwidened_type_string?: string;
1116
+ /**
1117
+ * carrick#2100, response inferences only: `true` when this inference was
1118
+ * owed an unwidened reading (its handler's path has literals to mark) and
1119
+ * the batch's time budget ran out before it was read. The reading's absence
1120
+ * then says nothing about the handler, so the check must not judge the
1121
+ * widened `type_string` as what the handler sends. Absent otherwise.
1122
+ */
1123
+ unwidened_out_of_budget?: true;
1116
1124
  /**
1117
1125
  * carrick#1749, `call_result` only: present when the source itself states
1118
1126
  * the type of the body it reads, AT the read: a cast of the read