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 +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/index.js +13 -0
- package/sidecar/dist/src/library-claims.js +55 -5
- package/sidecar/dist/src/retype.js +88 -8
- package/sidecar/dist/src/type-inferrer.d.ts +55 -7
- package/sidecar/dist/src/type-inferrer.js +206 -18
- package/sidecar/dist/src/types.d.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
117
|
-
"@carrick-tools/cli-darwin-x64": "0.3.
|
|
118
|
-
"@carrick-tools/cli-linux-arm64": "0.3.
|
|
119
|
-
"@carrick-tools/cli-linux-x64": "0.3.
|
|
120
|
-
"@carrick-tools/cli-win32-x64": "0.3.
|
|
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.
|
|
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
|
|
1931
|
-
*
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
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
|
|
552
|
-
if (!
|
|
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
|
|
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
|
-
* (
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
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 () => { … }`)
|
|
1732
|
-
*
|
|
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
|
|
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
|
-
* (
|
|
6311
|
-
*
|
|
6312
|
-
*
|
|
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 () => { … }`)
|
|
6375
|
-
*
|
|
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
|